大多数时候,当我尝试签出另一个现有分支时,Git不允许我在当前分支上有一些未提交的更改。因此,我必须先提交或隐藏这些更改。

然而,Git偶尔允许我签出另一个分支而不提交或存储这些更改,它将把这些更改携带到我签出的分支。

这里的规则是什么?更改是阶段性的还是非阶段性的有关系吗?对我来说,把这些变化带到另一个分支没有任何意义,为什么git有时允许这样做?也就是说,它在某些情况下有用吗?


当前回答

我最近也面临着同样的问题。我的理解是,如果你签入的分支有一个你修改过的文件,它碰巧也被那个分支修改和提交了。然后git会阻止你切换到分支,在你提交或存储之前保证你的变化安全。

其他回答

我最近也面临着同样的问题。我的理解是,如果你签入的分支有一个你修改过的文件,它碰巧也被那个分支修改和提交了。然后git会阻止你切换到分支,在你提交或存储之前保证你的变化安全。

如果您不希望这些更改被提交,请做 Git重置——很难。

接下来,您可以签出到想要的分支,但请记住,未提交的更改将会丢失。

正确答案是

Git checkout -m origin/master

它将来自源主分支的更改与本地甚至未提交的更改合并。

初步的笔记

这个答案试图解释为什么Git会有这样的行为。不建议参与任何特定的工作流程。(我自己的偏好是无论如何都要提交,避免git stash,不要试图太棘手,但其他人喜欢其他方法。)

这里的观察结果是,在你开始在branch1中工作之后(忘记或没有意识到先切换到不同的分支branch2会很好),你运行:

git checkout branch2

有时候Git会说“好的,你现在在branch2上了!”有时候,Git会说:“我不能这样做,否则会丢失一些更改。”

如果Git不允许你这样做,你必须提交你的更改,把它们保存在某个永久的地方。你可能想要使用git stash来保存它们;这是它设计的目的之一。注意,git stash save或git stash push实际上意味着“提交所有的更改,但完全不在分支上,然后从我现在的位置删除它们。”这使得切换成为可能:您现在没有正在进行的更改。然后你可以在切换后应用它们。

边栏:git stash save是旧语法;git 2.13版本引入了git stash push,以修复git stash参数的一些问题,并允许新的选项。两者的作用是一样的,只要以基本的方式使用。

如果你愿意,你可以在这里停止阅读!

如果Git不让你切换,你已经有了补救措施:使用Git stash或Git commit;或者,如果您的更改很容易重新创建,则使用git checkout -f强制执行。这个答案是关于Git什么时候会让你Git签出branch2,即使你已经开始做一些更改。为什么它有时有效,有时无效?

这里的规则一方面很简单,另一方面又很复杂/难以解释:

当且仅当所述切换不需要破坏这些更改时,您可以切换工作树中未提交更改的分支。

请注意,这仍然是简化的;有一些额外困难的情况下,分期git添加,git RMS等-假设你在branch1上。git签出分支2必须这样做:

对于每个在branch1中而不在branch2中的文件,1删除该文件。 对于branch2中而不是branch1中的每个文件,创建该文件(包含适当的内容)。 对于两个分支中的每个文件,如果branch2中的版本不同,则更新工作树版本。

这些步骤中的每一步都可能破坏你工作树中的某些东西:

如果工作树中的版本与branch1中提交的版本相同,则删除文件是“安全的”;如果您已经进行了更改,则它是“不安全的”。 如果一个文件现在不存在,那么以它在branch2中出现的方式创建它是“安全的”如果它现在确实存在,但内容是“错误的”,那么它就是“不安全的”。 当然,如果工作树版本已经提交给branch1,那么用不同的版本替换文件的工作树版本是“安全的”。

Creating a new branch (git checkout -b newbranch) is always considered "safe": no files will be added, removed, or altered in the work-tree as part of this process, and the index/staging-area is also untouched. (Caveat: it's safe when creating a new branch without changing the new branch's starting-point; but if you add another argument, e.g., git checkout -b newbranch different-start-point, this might have to change things, to move to different-start-point. Git will then apply the checkout safety rules as usual.)


1This requires that we define what it means for a file to be in a branch, which in turn requires defining the word branch properly. (See also What exactly do we mean by "branch"?) Here, what I really mean is the commit to which the branch-name resolves: a file whose path is P is in branch1 if git rev-parse branch1:P produces a hash. That file is not in branch1 if you get an error message instead. The existence of path P in your index or work-tree is not relevant when answering this particular question. Thus, the secret here is to examine the result of git rev-parse on each branch-name:path. This either fails because the file is "in" at most one branch, or gives us two hash IDs. If the two hash IDs are the same, the file is the same in both branches. No changing is required. If the hash IDs differ, the file is different in the two branches, and must be changed to switch branches.

The key notion here is that files in commits are frozen forever. Files you will edit are obviously not frozen. We are, at least initially, looking only at the mismatches between two frozen commits. Unfortunately, we—or Git—also have to deal with files that aren't in the commit you're going to switch away from and are in the commit you're going to switch to. This leads to the remaining complications, since files can also exist in the index and/or in the work-tree, without having to exist these two particular frozen commits we're working with.

2It might be considered "sort-of-safe" if it already exists with the "right contents", so that Git does not have to create it after all. I recall at least some versions of Git allowing this, but testing just now shows it to be considered "unsafe" in Git 1.8.5.4. The same argument would apply to a modified file that happens to be modified to match the to-be-switch-to branch. Again, 1.8.5.4 just says "would be overwritten", though. See the end of the technical notes as well: my memory may be faulty as I don't think the read-tree rules have changed since I first started using Git at version 1.5.something.


更改是阶段性的还是非阶段性的有关系吗?

是的,在某些方面。特别是,您可以先进行更改,然后“反修改”工作树文件。这是一个在两个分支中的文件,在branch1和branch2中是不同的:

$ git show branch1:inboth
this file is in both branches
$ git show branch2:inboth
this file is in both branches
but it has more stuff in branch2 now
$ git checkout branch1
Switched to branch 'branch1'
$ echo 'but it has more stuff in branch2 now' >> inboth

此时,both中的工作树文件与branch2中的文件匹配,尽管我们在branch1上。这个变化不是为了提交而上演的,这是git的状态——短显示在这里:

$ git status --short
 M inboth

space-then-M表示“修改但不分段”(或者更准确地说,工作树复制不同于分段/索引复制)。

$ git checkout branch2
error: Your local changes ...

好,现在让我们运行工作树副本,我们已经知道它也匹配branch2中的副本。

$ git add inboth
$ git status --short
M  inboth
$ git checkout branch2
Switched to branch 'branch2'

在这里,分期和工作副本都匹配branch2中的内容,因此允许签出。

让我们尝试另一个步骤:

$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches

我所做的更改现在从暂存区丢失了(因为签出是通过暂存区写入的)。这是一个极端的情况。变化并没有消失,但我策划的这个事实已经消失了。

让我们创建第三个不同于branch-copy的文件,然后将工作副本设置为与当前分支版本匹配:

$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth

这里的两个Ms表示:阶段性文件不同于HEAD文件,工作树文件不同于阶段性文件。工作树版本确实匹配branch1(又名HEAD)版本:

$ git diff HEAD
$

但是git checkout不允许checkout:

$ git checkout branch2
error: Your local changes ...

让我们将branch2版本设置为工作版本:

$ git show branch2:inboth > inboth
$ git status --short
MM inboth
$ git diff HEAD
diff --git a/inboth b/inboth
index ecb07f7..aee20fb 100644
--- a/inboth
+++ b/inboth
@@ -1 +1,2 @@
 this file is in both branches
+but it has more stuff in branch2 now
$ git diff branch2 -- inboth
$ git checkout branch2
error: Your local changes ...

即使当前的工作副本与branch2中的工作副本匹配,但暂存在的文件不匹配,因此git签出将丢失该副本,并且git签出将被拒绝。

技术说明-仅供疯狂好奇的人使用:-)

所有这些的底层实现机制是Git的索引。索引,也称为“staging区域”,是你构建下一次提交的地方:它开始匹配当前提交,即你现在签出的任何文件,然后每次你添加一个文件,你用你工作树中的任何文件替换索引版本。

请记住,工作树是处理文件的地方。在这里,它们有正常的形式,而不是像在提交和索引中那样只对git有用的特殊形式。因此,您从提交中提取文件,通过索引,然后进入工作树。在改变它之后,你可以将它添加到索引中。因此,每个文件实际上有三个位置:当前提交、索引和工作树。

当您运行git checkout branch2时,git在幕后所做的是将branch2的tip提交与当前提交和当前索引中的内容进行比较。任何与现有文件匹配的文件,Git都可以保留。一切都是原封不动的。Git也可以保留两次提交中相同的任何文件——这些文件可以让您切换分支。

Much of Git, including commit-switching, is relatively fast because of this index. What's actually in the index is not each file itself, but rather each file's hash. The copy of the file itself is stored as what Git calls a blob object, in the repository. This is similar to how the files are stored in commits as well: commits don't actually contain the files, they just lead Git to the hash ID of each file. So Git can compare hash IDs—currently 160-bit-long strings—to decide if commits X and Y have the same file or not. It can then compare those hash IDs to the hash ID in the index, too.

This is what leads to all the oddball corner cases above. We have commits X and Y that both have file path/to/name.txt, and we have an index entry for path/to/name.txt. Maybe all three hashes match. Maybe two of them match and one doesn't. Maybe all three are different. And, we might also have another/file.txt that's only in X or only in Y and is or is not in the index now. Each of these various cases requires its own separate consideration: does Git need to copy the file out from commit to index, or remove it from index, to switch from X to Y? If so, it also has to copy the file to the work-tree, or remove it from the work-tree. And if that's the case, the index and work-tree versions had better match at least one of the committed versions; otherwise Git will be clobbering some data.

(所有这些的完整规则都在git签出文档中描述,而不是你可能期望的git签出文档,而是在名为“两棵树合并”部分的git读树文档中描述。)

分支切换只发生在您更改两个分支之间没有差异的文件时。在这种情况下,git对两个文件的更改都一视同仁。 当您更改两个分支之间存在差异的文件时,这种情况将被阻止。在这种情况下,你会得到中止信号。

经过一个小时的调查和当地测试,得出了这个结论。