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

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

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

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

236101 次浏览

您有两个选择:隐藏您的更改:

git stash

然后再把它们拿回来:

git stash apply

或者将更改放在一个分支上,这样您就可以获得远程分支,然后将更改合并到该分支上。这是git最伟大的地方之一:你可以创建一个分支,提交给它,然后在你所在的分支上获取其他更改。

你说这没有任何意义,但你这么做只是为了在拉之后可以随意合并它们。显然,您的另一个选择是提交分支的副本,然后执行拉取。假设你不想这样做(在这种情况下,我对你不想要分支感到困惑),或者你害怕冲突。

如果新分支包含的编辑与特定更改文件的当前分支不同,那么它将不允许您切换分支,直到提交或存储更改。如果在两个分支上更改的文件是相同的(即,该文件的提交版本),那么您可以自由切换。

例子:

$ 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上执行一些实验,想要提交它们,但还不是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的行为方式。不建议参与任何特定的工作流程。(我自己的偏好是无论如何都要提交,避免git stash,也不要试图太棘手,但其他人喜欢其他方法。)

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

git checkout branch2

有时候Git会说“OK,你现在在branch2上了!”有时候,Git会说:“我不能那样做,那样会损失你的一些更改。”

如果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会让你git checkout branch2,即使你开始做一些改变。为什么它工作有时次,而不是其他次?

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

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

请注意,这仍然是简化的;有一些特别困难的极端情况,有分段的__abc0, __abc1等等——假设你在branch1上。A git checkout branch2必须这样做:

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

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

  • 删除文件是“安全的”;如果工作树中的版本与branch1中提交的版本相同;这是“unsafe"如果你做了更改。
  • branch2中出现的方式创建文件是“安全的”;如果它现在不存在。2“不安全”;如果它现在确实存在,但有“错误”;内容。
  • 当然,用不同的版本替换一个文件的工作树版本是“安全的”。如果工作树版本已经提交给branch1

创建一个新的分支(git checkout -b newbranch)被认为是总是“安全的”:在此过程中,不会在工作树中添加、删除或更改任何文件,索引/暂存区也不受影响。(注意:在创建新分支时不改变新分支的起始点是安全的;但如果你添加另一个参数,例如,git checkout -b newbranch different-start-point,这可能需要改变一些东西,移动到different-start-point。Git会像往常一样应用签出安全规则。)


__abc7这要求我们定义一个文件在分支中的意义,这反过来要求正确定义单词分支。(另见我们所说的“分支”到底是什么意思?)这里,我真正的意思是branch10,如果git rev-parse branch1:P产生哈希,则该文件的路径为branch11 branch12。如果你得到一个错误消息,该文件branch13在branch1。在你的索引或工作树中是否存在路径branch11与回答这个问题无关。因此,这里的秘密是检查git rev-parse在每个branch-name:path上的结果。这要么失败,因为文件是“;in”;最多一个分支,或者给出两个哈希id。如果两个哈希id为branch15,则两个分支中的文件是相同的。不需要更改。如果哈希id不同,则两个分支中的文件是不同的,必须更改为交换分支。

不幸的是,我们——或者git——也必须处理在提交时要切换到的文件和在提交时要切换到的文件。这导致了剩下的复杂性,因为文件也可以存在于索引和/或工作树中,而不需要存在我们正在处理的这两个特定的冻结提交。

它可能被认为是“有点安全”;如果它已经存在并且“正确的内容”,那么Git就不需要创建它了。我记得至少有一些版本的Git允许这样做,但刚刚的测试显示它被认为是“不安全的”。在Git 1.8.5.4中。同样的参数也适用于修改后的文件,该文件恰好被修改以匹配将要切换到的分支。不过,1.8.5.4只是说“将被覆盖”。请参见技术说明的最后部分:我的记忆可能有问题,因为我不认为读树规则从我第一次使用Git 1.5版本开始就发生了变化。


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

是的,在某些方面。特别地,你可以进行一次改变,然后“de-modify"工作树文件。下面是一个有两个分支的文件,在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

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

这里的两个__abc0表示:阶段性文件不同于HEAD文件,而且文件,工作树文件不同于阶段性文件。工作树版本确实匹配branch1(又名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中的工作副本匹配,但暂存文件不匹配,因此git checkout将丢失该副本,并且git checkout将被拒绝。

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

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

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

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

大部分Git,包括提交转换,都是相对较快的因为这个索引。索引中实际包含的不是每个文件本身,而是每个文件的哈希。文件本身的副本存储在存储库中,Git称之为blob对象。这也类似于文件在提交中的存储方式:提交实际上不包含文件,它们只是引导Git到每个文件的哈希ID。因此Git可以比较哈希id(目前是160位长的字符串),以确定提交XY是否有相同文件。然后,它也可以将这些哈希ID与索引中的哈希ID进行比较。

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

(所有这些的完整规则都在git checkout文档中描述,而不是你可能期望的git checkout文档,而是git read-tree文档,在标题为“两棵树合并”的部分;文档。)

正确答案是

git checkout -m origin/master

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

如果你不希望这个改变被提交,请做 git reset --hard . < / p >

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

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

我也在这件事上挣扎了一段时间,我想给出我的答案。首先,我对这件事的理解来自这里:https://medium.com/swimm/a-visualized-intro-to-git-internals-objects-and-branches-68df85864037

问题是:

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

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

当你从任何其他分支创建一个分支时,你只是创建了一个指向同一个提交的指针,所以除非你已经提交了你已经开始工作的任何更改,否则你将指向同一个提交,因此git将允许你以这种方式更改分支。只有当你向新分支提交任何更改时,不同分支之间的提交才会开始不同,如果有任何未提交的更改,git在尝试签出这些分支时会报错。

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

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