当前分支上有未提交的更改时签出另一个分支


349

在大多数情况下,当我尝试签出另一个现有分支时,如果我在当前分支上有一些未提交的更改,Git不允许我这样做。因此,我必须首先提交或存储这些更改。

但是,偶尔Git确实允许我签出另一个分支而不提交或存储那些更改,并且它将那些更改带到我签出的分支中。

这是什么规则?变更是分阶段还是分阶段是否重要?对我来说,将更改转移到另一个分支毫无意义,为什么git有时会允许它?也就是说,在某些情况下是否有帮助?

Answers:


350

初步说明

这里的观察结果是,在您开始工作之后branch1(忘记或不意识到先切换到另一个分支会很好branch2),您可以运行:

git checkout branch2

有时Git会说:“好,您现在在branch2上!” 有时候,吉特说:“我做不到,我会丢失一些更改。”

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

边栏:git stash save是旧语法;git stash push是在Git 2.13版中引入的,用于解决自变量的一些问题git stash并允许使用新选项。当以基本方式使用时,两者的作用相同。

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

如果Git 不允许您进行切换,则说明您已经有解决方法:请使用git stashgit commit;或者,如果您所做的更改很难重新创建,请使用git checkout -f它来强制执行。这个答案是所有关于何时混帐会让你git checkout branch2即使你开始做一些改变。为什么它的工作有时,而不是其他时间?

这里的规则以一种方式是简单的,而以另一种方式是复杂的/难以解释的:

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

也就是说,请注意,这仍然是简化的。有一些极难处理的极端情况,其中包括git addgit rm等等branch1。A git checkout branch2必须这样做:

  • 对于每一个文件,该文件branch1branch21删除该文件。
  • 对于每一个文件,该文件branch2branch1,创建该文件(在适当的内容)。
  • 对于两个分支中的每个文件,如果其中的版本branch2不同,则更新工作树版本。

这些步骤中的每一个都可能破坏您的工作树中的某些内容:

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

创建新分支(git checkout -b newbranch始终被认为是“安全”的:在此过程中,不会在工作树中添加,删除或更改任何文件,并且索引/暂存区也保持不变。(注意:创建新分支而不更改新分支的起点是安全的;但是,如果添加了另一个参数(例如)git checkout -b newbranch different-start-point,则可能需要更改某些内容以移至different-start-point。Git随后将照常应用结帐安全规则)


1这要求我们定义文件在分支中的含义,而这又需要正确定义单词branch。(参见我们究竟由“分支”是什么意思?)这里,我真正的意思是提交到分支名称解析:其路径是一个文件在如果产生了哈希值。如果收到错误消息,则该文件不在其中。回答此特定问题时,索引或工作树中路径的存在无关紧要。因此,这里的秘诀是检查每个P branch1git rev-parse branch1:Pbranch1Pgit rev-parsebranch-name:path。这可能由于文件最多位于一个分支而失败,或者给了我们两个哈希ID。如果两个哈希ID 相同,则两个分支中的文件相同。无需更改。如果哈希ID不同,则两个分支中的文件不同,并且必须更改以切换分支。

此处的关键概念是永久冻结提交中的文件您将编辑的文件显然没有冻结。至少在一开始,我们仅查看两个冻结提交之间的不匹配。 不幸的是,我们-或Git的,还必须处理的文件是不是在提交你打算从开关路程,在提交你要切换到。这导致了剩下的复杂性,因为文件也可以存在于索引和/或工作树中,而不必存在我们正在使用的这两个特定的冻结提交。

2如果它已经包含“正确的内容”,则可以将其视为“安全排序”,因此Git毕竟不必创建它。我记得至少有一些版本的Git允许这样做,但是现在进行的测试表明,它在Git 1.8.5.4中被视为“不安全”。相同的参数将应用于修改后的文件,该文件恰好被修改为与to-to-switch-to分支匹配。同样,1.8.5.4只是说“将被覆盖”。也请参阅技术说明的末尾:我的内存可能有问题,因为自从我从1.5版本开始使用Git以来,我不认为读取树规则已更改。


变更是分阶段还是分阶段是否重要?

是的,在某些方面。特别是,您可以进行更改,然后“取消修改”工作树文件。这是两个分支中的文件,在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

至此,即使我们打开,工作树文件也将inboth匹配其中的一个。此更改未上演提交,显示如下:branch2branch1git status --short

$ 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'

在这里,暂存和工作副本都与in中的匹配branch2,因此允许结帐。

让我们尝试另一步:

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

我所做的更改现在从登台区域丢失(因为结帐会通过登台区域进行写入)。这有点极端。这种变化是不是走了,但我已经上演它其实走了。

让我们暂存该文件的第三个变体,与任何一个分支副本不同,然后将工作副本设置为与当前分支版本匹配:

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

M这里的两个s表示:暂存文件不同于HEADfile,,工作树文件不同于暂存文件。工作树版本与branch1(aka HEAD)版本匹配:

$ 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,暂存文件也不匹配,所以a git checkout会丢失该副本,而会git checkout被拒绝。

技术说明-仅适用于非常好奇的人:-)

所有这些的基本实现机制是Git的index。索引,也称为“临时区域”,是您构建下一个提交的地方:它开始与当前提交匹配,即,无论您现在已经签出什么,然后每次git add文件时,都替换索引版本无论您在工作树中拥有什么。

记住,工作树是您处理文件的地方。在这里,它们具有常规形式,而不是像在提交和索引中那样特殊的仅对Git有用的形式。因此,您提交中提取文件,通过索引,然后进入工作树。更改后,将git add其更改为索引。因此,每个文件实际上有三个位置:当前提交,索引和工作树。

当您运行git checkout branch2,什么混帐做封面的下面是比较尖承诺branch2到无论是在双方最近提交现在的指数。任何与当前文件匹配的文件,Git都可以保留。都没动过。两次提交中相同的任何文件,Git可以可以保留下来-这些文件使您可以切换分支。

由于该索引,很多Git(包括提交切换)相对较快。实际上,索引中的内容不是每个文件本身,而是每个文件的hash。文件本身的副本作为Git所谓的blob对象存储在存储库中。这也类似于文件在提交中的存储方式:提交实际上并不包含文件,它们只是将Git引向每个文件的哈希ID。因此,Git可以比较哈希ID(当前为160位长的字符串),以确定提交XY是否具有相同的文件。然后,它也可以将这些哈希ID与索引中的哈希ID进行比较。

这就是导致上述所有奇怪情况的原因。我们提交的XY都有文件path/to/name.txt,并且有一个索引条目path/to/name.txt。也许所有三个哈希都匹配。也许其中两个匹配,一个不匹配。也许这三个都不一样。而且,我们也可能another/file.txt只在XY中使用它,而现在不在索引中。这些不同情况中的每一种都需要单独考虑:Git 是否需要将文件从提交复制到索引,或者从索引中删除,以从X切换到Y?如果是这样,还必须将文件复制到工作树,或将其从工作树中删除。如果这种情况,则索引和工作树版本最好至少匹配提交的版本之一;否则,Git将破坏一些数据。

(关于这一切的完整规则,在标题为“两棵树的合并”的部分中git checkout,而不是您可能期望git read-tree文档中,而是文档中进行了描述。)


3
...还有git checkout -m,它将您的工作树和索引更改合并到新的结帐中。
jthill

1
感谢您的精彩解释!但是我在哪里可以找到官方文档中的信息?还是不完整?如果是这样,git的权威参考是什么(希望与其源代码不同)?
最大

1
(1)不能,以及(2)源代码。主要问题是Git不断发展。例如,现在,大力推动或放弃SHA-1并支持或支持SHA-256。但是,Git的这一特定部分已经很长时间了,而且其基本机制很简单:Git将当前索引与当前提交和目标提交进行比较,并根据目标提交决定要更改的文件(如果有) ,然后如果索引条目需要替换,则测试工作树文件的“清除”状态。
torek '17

6
简短的回答:有一条规则,但是对于普通用户来说,太过钝了,希望没有任何理解的希望,更不要说记住了,因此,与其依靠工具的行为来理解您,不如依靠只有在结账时才签出的有纪律的惯例当前分支已提交且干净。我看不到这如何回答何时将出色的更改转移到另一个分支会很有用,但是我可能会错过它,因为我很难理解它。
Neutrino

2
@HawkeyeParker:这个答案已经进行了许多编辑,我不确定它们中的任何一个都有很大的改进,但是我将尝试添加一些有关文件“在分支中”的含义的信息。最终,这将变得不确定,因为首先没有正确定义“分支”的概念,但这是另外一项。
torek

50

您有两种选择:存储更改:

git stash

然后再将它们找回来:

git stash apply

或将更改放在分支上,以便可以获取远程分支,然后将更改合并到该分支上。这是git的最大优点之一:您可以创建一个分支,提交它,然后将其他更改提取到您所在的分支上。

您说这没有任何意义,但是您只是在做,因此您可以在拉动之后随意合并它们。显然,您的另一选择是提交分支的副本,然后执行拉取。假设是您不想这样做(在这种情况下,我很困惑您不想要分支),或者您担心冲突。


1
这不是正确的命令git stash apply吗?这里的文档。
Thomas8'3

1
正是我想要的,要临时切换到不同的分支,查找某些内容并返回到我正在工作的分支的相同状态。谢谢罗布!
内斯塔(Naishta),2016年

1
是的,这是正确的方法。我很欣赏接受的答案中的细节,但这使事情变得比实际需要的要难。
迈克尔·伦纳德

5
另外,如果您不需要保留所有存储,则可以使用git stash pop,如果成功应用,它将从列表中删除该存储。
迈克尔·伦纳德

1
更好地使用git stash pop,除非您打算在回购记录中
保留隐藏的

14

如果新分支包含与该特定已更改文件的当前分支不同的编辑,则在提交或隐藏更改之前,不允许您切换分支。如果两个分支上更改的文件相同(即该文件的提交版本),则可以自由切换。

例:

$ 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

一个很好的例子说明了为什么要在进行更改的同时在分支之间移动,如果您正在对master进行一些实验,想要提交它们,但还不想掌握...

$ 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"

其实我还是不太明白。在第一个示例中,在添加“并且我们回来了”之后,它说本地更改将被覆盖,到底什么是本地更改?“我们回来了”?为什么git不会只将此更改传递给master,以便在master中文件包含“ hello world”和“而我们又回来了”
Xufeng,2014年

在第一个示例中,主控仅承诺执行“ hello world”。实验已承诺执行“ hello world \ nbybye world”。要进行分支更改,需要修改file.txt,问题是,存在尚未提交的更改“ hello world \ ngoodbye world \ n而我们回来了”。
2014年

1

正确答案是

git checkout -m origin/master

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


0

如果您根本不希望执行此更改,请执行 git reset --hard

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.