在当前分支存在未提交的更改时,切换到另一个分支。

503

大多数情况下,当我尝试切换到另一个已存在的分支时,如果当前分支有未提交的更改,Git会不允许我切换。 因此,我必须先提交或隐藏这些更改。

然而,偶尔地,Git允许我在不提交或隐藏更改的情况下切换到另一个分支,并将这些更改带到我要切换的分支上。

这里有什么规则吗? 这是否取决于更改是暂存还是未暂存? 把更改带到另一个分支似乎没有任何意义,为什么Git有时允许这样做? 也就是说,这在某些情况下是否有帮助?

9个回答

516

初步说明

本回答旨在解释Git的行为原理,不是推荐采用任何特定的工作流程。(我个人偏好直接提交,避免使用git stash并且不试图过于狡猾,但其他人可能喜欢其他方法。)

观察到的情况是,在你开始在branch1中工作(忘记或没有意识到先切换到另一个分支branch2较好),你运行以下命令:

git checkout branch2

有时候Git会说“好的,你现在在branch2分支上了!”但有时候,Git会说“我不能这样做,否则你会失去一些更改。”如果Git不允许你这样做,你必须提交你的更改,以便将它们保存在一个永久的地方。你可能想使用git stash来保存它们;这是它设计的其中之一。请注意,git stash save或git stash push实际上意味着“提交所有更改,但不在任何分支上,然后从当前位置删除它们。”这使得切换成为可能:你现在没有正在进行的更改。然后,在切换后,你可以使用git stash apply应用它们。

侧边栏:git stash save是旧语法;git stash push在Git 2.13版本中引入,用于修复git stash参数的一些问题并允许新选项。在基本用法下,两者都是相同的。

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

如果 Git 不允许您切换分支,您已经有解决方法:使用 git stashgit commit;或者,如果您的更改很容易重新创建,请使用 git checkout -f 强制执行。本答案讨论的是当您开始进行一些更改时 Git 何时允许您 git checkout branch2。为什么有时会起作用,而有时不会呢?

这里的规则在某种程度上很简单,但在另一方面却很复杂/难以解释:

只有在不需要覆盖这些更改的情况下,您才能使用未提交的更改在工作树中切换分支。

也就是说——请注意,这仍然是简化的;还有一些关于已暂存的 git addgit rm 等的额外困难情况——假设您在 branch1 上。执行 git checkout branch2 将需要执行以下操作:

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

这些步骤中的每一个都可能会覆盖你的工作树中的某些内容:

  • 如果工作树中的版本与branch1提交的版本相同,则删除文件是“安全”的;如果您已经进行了更改,则是“不安全”的。
  • branch2中显示的方式创建文件,如果该文件现在不存在,则是“安全”的。2如果该文件现在存在但具有“错误”的内容,则为“不安全”。
  • 当然,如果工作树版本的文件已经提交到branch1,则用不同版本替换工作树版本的文件是“安全”的。

创建一个新分支(git checkout -b newbranch)始终被认为是“安全”的:这个过程中不会向工作树中添加、删除或更改任何文件,并且索引/暂存区也保持不变。(警告:当创建一个新分支时,如果更改新分支的起始点,例如git checkout -b newbranch different-start-point,这可能需要更改一些内容,以移动到different-start-point。Git将按照通常的检查安全规则应用检查。)



1这需要我们定义文件在分支中是什么意思,而这又需要适当地定义"branch"的含义。(另请参见 我们所说的“分支”到底是什么?) 在这里,我真正的意思是分支名称解析为提交:如果路径为P的文件的git rev-parse branch1:P会产生哈希值,则该文件位于branch1中。 如果您得到错误消息,则该文件不在branch1中。当回答这个问题时,索引或工作树中路径P的存在并不重要。因此,这里的秘密是检查每个branch-name:pathgit rev-parse结果。 这要么失败,因为该文件最多只能“在”一个分支中,要么给我们两个哈希ID。 如果这两个哈希ID相同,则在两个分支中的文件相同。不需要更改。 如果哈希ID不同,则两个分支中的文件不同,必须更改才能切换分支。

这里的关键概念是提交中的文件永远被冻结。显然,您将要编辑的文件并没有被冻结。最初,我们只考虑两个冻结提交之间的不匹配之处。遗憾的是,我们或者说Git也必须处理那些不在您要切换的提交中,但却存在于您要切换到的提交中的文件。这导致了剩余的复杂性,因为文件也可以存在于索引和/或工作树中,而无需存在于我们正在处理的这两个特定的冻结提交中。

2如果文件已经存在且“内容正确”,那么它可能被认为是“有点安全的”,这样Git就不必创建它了。我记得至少有一些Git版本允许这样做,但是现在的测试显示Git 1.8.5.4认为这是“不安全的”。同样的论点也适用于已修改的文件,恰好被修改以匹配要切换到的分支。再次说明,1.8.5.4只是说“会被覆盖”。请参见技术注释的末尾:我的记忆可能有误,因为我认为自从我第一次使用Git(版本为1.5)以来,read-tree规则并没有改变。


更改是暂存的还是未暂存的,这是否重要?

在某些方面是有影响的。特别地,您可以将更改暂存,然后“取消修改”工作树文件。以下是两个分支中不同的文件,在branch1branch2中:

$ 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

此时,工作树文件inbothbranch2中的文件匹配,尽管我们在branch1上。这个更改没有为提交做准备,这就是git status --short在这里显示的内容:
$ git status --short
 M inboth

空格加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

现在我所做的更改已经从暂存区中丢失了(因为checkout通过暂存区进行写入)。 这是一个有点特殊的情况。 更改并没有消失,但是我已经暂存它的事实已经消失。
让我们将文件的第三个变体暂存,与任一分支副本不同,然后将工作副本设置为与当前分支版本匹配:
$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth

这里的两个分别表示:暂存文件与文件不同,工作树文件与暂存文件不同。工作树版本与(又名)版本匹配:
$ git diff HEAD
$

但是git 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 checkout会丢失该副本,因此git checkout被拒绝。

技术注释-仅供极度好奇者使用 :-)

所有这些的底层实现机制都是基于Git的索引。索引,也称为“暂存区”,是您构建下一个提交的地方:它开始匹配当前提交,即您现在检出的内容,然后每次您git add一个文件时,您就会用您工作树中的内容替换索引版本。

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

当你运行git checkout branch2时,Git在底层比较branch2末尾提交与当前提交和索引中的内容。任何与现在相匹配的文件,Git都可以保持不变。它们都没有受到影响。任何在两个提交中相同的文件,Git也可以保持不变——这些文件让你可以切换分支。

许多 Git 的操作,包括提交切换,都相对较快,这是由于索引的缘故。索引中实际上并不包含每个文件本身,而是每个文件的哈希值。文件本身的副本被存储为 Git 所谓的“blob 对象”在代码库中。这与提交中文件的存储方式类似:提交实际上并不包含文件,只是将 Git 导向每个文件的哈希 ID。因此,Git 可以比较哈希 ID(目前为 160 位长的字符串),以确定提交 X 和 Y 是否具有相同的文件。它还可以将这些哈希 ID 与索引中的哈希 ID 进行比较。
这就是导致上述所有奇怪情况的原因。我们有提交记录 X 和 Y,它们都有文件 path/to/name.txt,并且我们有一个索引条目用于 path/to/name.txt。也许这三个哈希值都匹配,也许其中两个匹配而另一个不匹配,也许这三个哈希值都不同。此外,我们可能还有另一个文件 another/file.txt,它只在 X 或只在 Y 中存在,现在是否在索引中也不确定。每种情况都需要单独考虑:Git 是否需要从提交记录复制文件到索引中,或者从索引中删除它,以从 X 切换到 Y?如果是这样,它还必须将文件复制到工作树中,或者从工作树中删除它。如果是这种情况,索引和工作树版本至少要与其中一个已提交版本匹配;否则 Git 将覆盖某些数据。

所有这些的完整规则都在git read-tree文档中的“Two Tree Merge”章节中描述,而不是像你可能预期的那样在git checkout文档中。


7
还有一个命令 git checkout -m,它会将工作目录和索引的更改合并到新的检出中。 - jthill
1
谢谢您的出色解释!但是我在官方文档中找不到这些信息,它们是不完整的吗?如果是这样,那么除了源代码之外,Git 的权威参考资料在哪里可以找到呢? - max
2
(1) 你做不到, (2) 源代码。主要问题是Git在不断发展。例如,现在有一个大力推动用SHA-256来增强或取代SHA-1的趋势。尽管如此,Git的这个特定部分已经相当稳定了很长一段时间,底层机制也很简单:Git将当前索引与当前和目标提交进行比较,并根据目标提交决定要更改哪些文件(如果有的话),然后测试工作树文件的“清洁度”,以确定是否需要替换索引条目。 - torek
17
简短回答:有一个规则,但对于普通用户来说过于晦涩,他们很难理解更别提记住,因此,与其指望工具能够表现得聪明,不如依靠遵守纪律的惯例——只有在当前分支已经提交并干净的情况下才切换分支。我不明白这个回答如何解决何时将未完成的更改带到另一个分支会有用的问题,但我可能错过了什么,因为我很难理解它。 - Neutrino
2
@HawkeyeParker:这个答案经历了多次编辑,我不确定它们中的任何一项都有所改善,但我会尝试添加一些关于文件“在分支中”的含义的内容。最终,这将是摇摆不定的,因为“分支”的概念在第一位并没有得到恰当的定义,但这又是另一个问题。 - torek
显示剩余10条评论

78

你有两个选择:把你的更改存储起来:

git stash

然后稍后再把它们拿回来:

git stash apply

或者你可以将更改放在一个分支上,这样你就可以获取远程分支,然后将你的更改合并到它上面。这是Git最伟大的事情之一:你可以创建一个分支,提交更改,然后将其他更改拉取到你所在的分支。

你说这没什么意义,但你只是这么做是为了在拉取后随意合并它们。显然,你的另一个选择是在你的分支副本上进行提交,然后再执行拉取操作。假设你不想那样做(在这种情况下,我感到困惑,不知道为什么你不想要一个分支),或者你害怕冲突。


1
正确的命令不是git stash apply吗?这里是文档。 - Thomas8
1
正是我所需要的,可以暂时切换到不同的分支,查找某些内容并回到我正在工作的分支的状态。感谢Rob! - Naishta
1
是的,这是正确的方法。我很感激被接受答案中的细节,但那只会让事情变得更加困难。 - Michael Leonard
7
另外,如果您不需要保留存储的内容,可以使用 git stash pop 命令,如果应用成功,它将从列表中删除该存储。 - Michael Leonard
5
最好使用git stash pop,除非您想在存储库历史记录中保留存储记录。 - Damilola Olowookere

24
如果新分支在特定修改文件上有不同于当前分支的编辑内容,那么在改变之前您将无法切换分支,直到该变更被提交或隐藏。如果该修改文件在两个分支上是相同的(也就是该文件的已提交版本),那么您可以自由地切换分支。
例子:
$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "adding file.txt"

$ git checkout -b experiment
$ echo 'goodbye world' >> file.txt
$ git add file.txt
$ git commit -m "added text"
     # experiment now contains changes that master doesn't have
     # any future changes to this file will keep you from changing branches
     # until the changes are stashed or committed

$ echo "and we're back" >> file.txt  # making additional changes
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
    file.txt
Please, commit your changes or stash them before you can switch branches.
Aborting

这适用于未跟踪的文件和已跟踪的文件。以下是一个未跟踪文件的示例。

示例:

$ git checkout -b experimental  # creates new branch 'experimental'
$ echo 'hello world' > file.txt
$ git add file.txt
$ git commit -m "added file.txt"

$ git checkout master # master does not have file.txt
$ echo 'goodbye world' > file.txt
$ git checkout experimental
error: The following untracked working tree files would be overwritten by checkout:
    file.txt
Please move or remove them before you can switch branches.
Aborting

为什么在进行更改时需要在分支之间移动的一个很好的例子是,如果您正在主分支上执行某些实验,并希望提交它们,但还不想立即提交到主分支...

$ echo 'experimental change' >> file.txt # change to existing tracked file
   # I want to save these, but not on master

$ git checkout -b experiment
M       file.txt
Switched to branch 'experiment'
$ git add file.txt
$ git commit -m "possible modification for file.txt"

其实我还是不太明白。在你的第一个例子中,添加了“and we're back”之后,它说本地更改将被覆盖,那么具体指的是哪个本地更改呢?是“and we're back”吗?为什么 Git 不直接将此更改带到主分支,使得主分支中的文件包含“hello world”和“and we're back”呢? - Xufeng
1
在第一个示例中,主分支只提交了“hello world”。实验分支提交了“hello world\ngoodbye world”。为了进行分支更改,需要修改file.txt文件。问题是,有未提交的更改“hello world\ngoodbye world\nand we're back”。 - Gordolio

8

正确答案是

git checkout -m origin/master

它会将远程主分支(origin master)的变更与您的本地未提交的变更合并。


4
  1. 只有在你更改的文件在两个分支之间没有差异时,分支切换才会发生。 在这种情况下,Git 将该更改视为对两个文件都适用的共同变更。
  2. 当你更改的文件在两个分支之间存在差异时,就会阻止此操作。 在这种情况下,你会收到 ABORT 信号。

经过一小时的本地测试和调查后得出上述结论。


谢谢。这肯定也回答了我的问题在这里 - Tryer
这比那个更微妙,即使在你改变一个文件并且它的差异存在于两个分支之间时,分支切换也可能发生,如果更改删除了差异并且已经暂存,请参阅此答案中的“更改是暂存还是未暂存是否重要?”部分。 - Géry Ogam

1
最近我也遇到了同样的问题。我的理解是,如果你要检查的分支有一个文件,你修改了这个文件,而该分支也修改并提交了这个文件。那么git会阻止你切换到该分支,在提交或隐藏更改之前,以确保你的更改安全。

1
我也一直在努力理解这个问题,我想为答案提供我的意见。首先,我从这里了解到了相关的知识:https://medium.com/swimm/a-visualized-intro-to-git-internals-objects-and-branches-68df85864037 问题是:
有时Git允许我在不提交或存储更改的情况下切换到另一个分支,并将这些更改带到我切换的分支中。这里的规则是什么?这是否与更改是暂存还是未暂存有关?将更改带到另一个分支对我来说没有任何意义,为什么Git有时会允许它?也就是说,在某些情况下,这有帮助吗?
当你从任何其他分支创建一个分支时,你只是创建了一个指向相同提交的指针,所以除非你已经提交了你开始工作的任何更改,否则你将指向相同的提交,因此git会允许你以这种方式更改分支。只有在你向新分支提交任何更改后,提交才开始在分支之间不同,并且如果有任何未提交的更改,则在尝试检出这些分支时git会发出警告。

0
如果你想在对一个分支进行未提交的更改后,将这些更改也保留在新的分支中,可以使用命令git checkout -b <new-branch>来从现有分支检出一个新的分支。这将创建一个新的分支,你可以将你的更改提交到这个新的分支中。

0
如果您不想提交这些更改,请执行以下命令:git reset --hard
接下来,您可以切换到所需的分支,但请记住,未提交的更改将会丢失。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接