问题描述
显示的错误消息是“更新被拒绝,因为远程存储库包含您没有的工作。”我会想到,如果特定文件没有被其他人更改,则该文件的推送将会成功-为什么Git会以这种方式工作?
我想问题的中心是我是否了解正在发生的事情-Git是否真的按照我的理解工作,这是对回购中 any 文件的更改,从而阻止了用户推送即使其他用户也没有更改提交中的文件,也可以提交。对我来说,这是令人惊讶的部分,因为如果仅在对提交中的文件进行了更改(由另一个用户或多个用户更改)的情况下拒绝推送,它似乎同样可以工作(甚至更好)。
解决方法
您的标题为which no one else changed
,但此处可能的情况是实际上其他人已将新提交推送到您当前正在使用的远程分支。解决此问题的典型方法是只执行git pull
:
# from your branch
git pull origin your_branch
这将从远程引入更改。解决任何可能的合并冲突后,您可以提交您的工作,然后进行推送即可。
,注意:Git thinks in project tree snapshots not individual file revisions.
如果您在本地(例如,A
上修改了文件master
,而其他人在上游(例如,B
)分支上修改了文件origin/master
,则历史记录图将如下所示:
(master,"change file A")
|
v
1 -- 2 -- 3
\
4
^
|
(origin/master,"change file B")
因此,在将更改推送到远程服务器之前,您将必须git pull
(即fetch
和merge
)将更改从上游分支扩展到本地分支。这将在本地创建一个新的合并提交。
(master,"merge origin/master into master")
|
v
1 -- 2 -- 3 -- 5
\ /
4 -----
^
|
(origin/master,"change file B")
现在您将能够进行推送。
,这只是告诉您本地分支的状态与您要更新的远程服务器不同步。哪个文件更改都没有关系,必须同步。
在推送本地更改之前,您必须运行git pull
。
正如answer的注释中所述,之所以不能推送到分支机构,是因为您的本地分支机构在原点位于该分支机构的后面。
在由多个人更改的分支上工作的最佳策略是从该分支(feature
)创建一个功能分支(master
),并从{{1} }和pull
从master
更改为merge
,无论是本地还是通过PR。
此策略允许您使feature
分支与master
不同步,但只要没有冲突,就可以合并并推送到feature
。
Git与提交有关。尽管提交包含文件,但Git并非真正与文件有关。 Git也与分支无关,尽管分支名称非常有帮助-实际上,是必需的-帮助您(和Git)找到正确的提交。但最后,Git与 commits 有关。
当您的Git使用git fetch
调用其他Git时,您的Git将从该Git中获得Git所需要的,您不需要的任何提交。然后,您开始在本地工作:
-
您通常使用名称(例如,
master
的分支名称或诸如origin/master
的远程跟踪名称)来选择一些提交,尽管最终Git实际上只需要提交的哈希ID,然后切换到该ID,或使用git merge
将其与您的最新作品或其他内容结合起来。 -
您需要做一些工作,使用
git add
将新的和/或更新的文件复制回Git的 index AKA 临时区域(我们不会此处不做详细介绍),然后运行git commit
。 -
Git打包Git索引中的文件副本,以为Git知道的每个文件(即Git索引中的文件)制作新快照。 Git会适当地添加您的姓名和电子邮件地址,以表示您已进行提交,并使用计算机的时钟在新提交上添加日期和时间戳,并将新提交存储到其“每次提交此提交”的大型数据库中Git知道”。该操作将为新提交生成一个新的唯一哈希ID。
-
Git现在会更新您用来记住此最新提交的哈希ID的任何分支名称。
在这一点上,重要的是要意识到每次提交:
- 具有一个唯一的数字(其哈希ID),该数字看起来是随机的,但不是:它实际上是提交中每个数据位的校验和;
- 包含Git知道的每个文件的快照:这是提交的数据;和
- 包含上一个提交的哈希ID,Git将此提交称为 parent 。
这意味着,如果我们使用单个大写字母来代表提交哈希ID,我们可以像这样在您的存储库中绘制提交:
... <-F <-G <-H
其中H
是 last 提交的哈希ID。提交H
包含该所有文件的快照,以及对一个文件的最新更新。但是提交H
还包含您的姓名和电子邮件地址,以及日期和时间戳(实际上是其中的两个),和较早提交G
的哈希ID。
这意味着您当前的分支名称(假设它为master
)仅需要保留新提交的哈希ID。我们来画一下:
...--F--G--H <-- master
Git(在本例中为Git)将从H
开始,然后向后工作,查找任何更早的提交。
同时,由于您是从与origin
上的Git中的提交相同的一组提交开始的,因此其的提交链必须最近以提交G
结束,就像这样:
...--F--G <-- master
您自己的Git保留了对其Git的最新提交的记忆,因此我们可能希望这样绘制您的状态:
H <-- master
/
...--F--G <-- origin/master
现在,这里的问题是,在执行此操作时,连接到origin
上Git的其他人通过提交G
进入他们的存储库,执行相同的操作,然后进行了一些更改,并git add
编辑已更新的文件并运行git commit
。因此,他们在其存储库中获得了一个新提交,具有新的唯一哈希ID,与提交H
不同。我们将此提交称为I
。他们的新提交I
的 parent 是现有的提交G
,与您的提交H
的父提交相同。然后他们跑了git push origin master
并赶到origin
。
现在现在 origin
在其存储库中有此文件:
...--F--G--I <-- master
您现在运行git push
,让您的Git调用他们的Git,找到您不希望拥有的所有新提交,然后将它们发送出去。这会向他们发送您的提交H
:
H [your commit]
/
...--F--G--I <-- master
您的git push
的最后一步是让您的Git礼貌地询问他们的Git,让他们为他们设置他们的名称master
是否可以记住您的新Git提交H
。 这不行!这样做不行的原因是,他们这样做会:
H <-- master
/
...--F--G--I <-- ???
其他人发送给他们的提交I
不再可以找到。它们将从您的提交H
开始,然后向后工作以提交G
,然后再返回到提交F
,依此类推。没有找到提交I
的名称,因此根本找不到。
您应该做什么
这时,您必须再次运行git fetch
。这将使您的Git调用他们的Git并获取他们所拥有,不需要,需要的任何新提交。在这种情况下,将提交I
。您的Git将从他们那里获得提交I
,然后更新您的origin/master
:
H <-- master
/
...--F--G
\
I <-- origin/master
现在您已经拥有所有提交-您的提交,包括H
,以及所有共享的提交,包括I
-现在,您可以将工作与完成的工作结合起来由提交I
的人进行。
有两种常见的合并方法:可以使用git merge
,也可以使用git rebase
。有些人对必须使用哪种观点持有强烈的意见,但有些人则说只允许合并,而另一些人说只允许重新设置。我在这里要做的是画出git merge
的工作方式,但是是否加入“调整基准,不要合并”阵营由您决定。
如果您现在运行git merge
并指定Git应该使用的另一个提交是提交I
,则可以使用I
的原始哈希ID或使用名称origin/master
或其他任何可以让Git 查找提交I
的方式-您的Git现在将弄清提交G
中的哪些文件在全部三个提交(G
,H
和I
),哪些是您更改的 ,哪些是更改的。正如您所说,由于您和他们更改了完全不同的文件,因此Git相信合并是有效的。 (它可能实际上没有用:也许他们所做的更改与您所做的更改不兼容。这取决于您是否以某种方式进行验证; Git只能执行机械部分。)
假设一切都很好,您让Git进行新的 merge commit ,Git进行此新的提交,如下所示:
H
/ \
...--F--G J <-- master
\ /
I <-- origin/master
新提交J
与其他任何提交一样,除了一件事:除了存储一个两个父提交哈希ID以外,它还没有一个父提交哈希ID H
。第一个是像往常一样提交H
,但是提交J
也会记住以前的 second 提交,即提交I
。
这意味着当Git从 last 提交执行往后工作时,它可以同时找到 提交H
和I
。在返回到较早的提交G
之前,它将查找并显示两个提交。
如果您现在运行git push origin master
,则您的Git会调用他们的Git,并向他们提供您拥有的,没有的新提交。他们可能仍然有您先前尝试的H
,也可能没有。 1 他们肯定没有J
,因为您刚刚这么做。因此,您的Git发送了J
和H
(如果需要),现在它们拥有了您的所有提交。
现在我们回到有礼貌的要求:您的Git对他们说请,如果可以,请设置您的master
指向现在提交J
。因为他们的master
仍会提交I
,所以这次可以。他们将使用J
并将其名称master
提交为J
。提交J
可以同时找到提交H
和I
,因此它们不会丢失I
。然后,两个提交H
和I
都找到G
,因此他们也不会丢失G
或任何先前的提交。
Git调用此属性-在更新分支名称以指向新的提交哈希ID之后,能够找到是分支的结尾的提交- fast-前进。如果该操作是快进操作,则允许执行简单的git push
操作。其他一些操作也可能导致快进:特别是git merge
将检查是否需要真正的合并,或者检查是否需要进行快进而不是实际上全部合并。
1 它们是否仍然有H
通常并不重要:如果没有,您的Git会再次将其提供给他们。但是,如果提交H
非常大,那么如果它们放弃了H
,则每次推送都可能需要等待一段时间。对于早期失败的git push
操作,不同版本的Git和Git安装具有不同的清理量。
摘要
Git与 commits 有关。分支名称只是让Git find 提交。但是,查找提交很重要:如果找不到它们,实际上它们就消失了。您的git push
将更改origin
存储库中的内容,使其不再能够找到其提交之一,这就是Git拒绝尝试的原因。
接收git push
的服务器永远不会进行合并,即使可以。 Git不知道某些合并操作是否有效:git merge
仅遵循简单的文本替换规则。有人或其他人(例如人工或某些外部测试系统(例如CI / CD系统))需要首先验证合并。 Git将负担加在运行git push
的个人或实体上,而不是服务器上。