初步说明
这个答案试图解释whyGit 的行为方式就是这样。不建议参与任何特定的工作流程。 (我自己的偏好是无论如何都要提交,避免git stash
并且不想太棘手,但其他人喜欢其他方法。)
这里的观察是,当你开始工作后branch1
(忘记或没有意识到切换到不同的分支会很好branch2
首先),你运行:
git checkout branch2
有时 Git 会说“好吧,你现在在分支 2 上!”有时,Git 会说“我不能这样做,我会丢失你的一些更改”。
If Git won't让你这样做,你必须提交你的更改,将它们永久保存在某个地方。您可能想使用git stash拯救他们;这是它的设计目的之一。 https://git-scm.com/docs/git-stash注意git stash save
or git stash push
实际上means“提交所有更改,但根本不在任何分支上,然后将它们从我现在所在的位置删除。”这使得切换成为可能:您现在没有正在进行的更改。那么你可以git stash apply
切换后的他们。
侧边栏:git stash save
是旧语法;git stash push
在 Git 2.13 版本中引入,以修复参数的一些问题git stash
并允许新的选择。当以基本方式使用时,两者都做同样的事情。
如果你愿意的话,你可以在这里停止阅读!
If Git won't让你切换,你已经有补救措施:使用git stash
or git commit
;或者,如果您的更改很容易重新创建,请使用git checkout -f
来强迫它。这个答案是关于whenGit 会让你git checkout branch2
即使你开始做出一些改变。为什么它有效有时, 并不是other times?
这里的规则一方面很简单,另一方面又复杂/难以解释:
当且仅当所述切换不需要破坏这些更改时,您才可以切换工作树中具有未提交更改的分支。
也就是说,请注意,这仍然是简化的;有一些特别困难的极端情况需要分阶段进行git add
s, git rm
等等——假设你在branch1
. A git checkout branch2
必须这样做:
- For every file that is in
branch1
and not in branch2
,1 remove that file.
- 对于每个文件is in
branch2
and not in branch1
,创建该文件(具有适当的内容)。
- 对于两个分支中的每个文件,如果版本
branch2
不同,更新工作树版本。
这些步骤中的每一个都可能会破坏您的工作树中的某些内容:
- 如果工作树中的版本与中提交的版本相同,则删除文件是“安全的”
branch1
;如果您进行了更改,则“不安全”。
- Creating a file the way it appears in
branch2
is "safe" if it does not exist now.2 It's "unsafe" if it does exist now but has the "wrong" contents.
- 当然,如果工作树版本已经提交到,则用不同版本替换文件的工作树版本是“安全的”
branch1
.
创建一个新分支(git checkout -b newbranch
) is always被视为“安全”:在此过程中,不会在工作树中添加、删除或更改任何文件,并且索引/暂存区域也不会受到影响。 (警告:创建新分支而不更改新分支的起点是安全的;但是如果您添加另一个参数,例如,git checkout -b newbranch different-start-point
,这可能需要改变一些事情,移动到different-start-point
。然后 Git 将照常应用结帐安全规则。)
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"? https://stackoverflow.com/q/25068543/1256452) 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.
这里的关键概念是文件提交被永远冻结。您要编辑的文件显然是 not 冻结了。至少在最初,我们只关注两个冻结提交之间的不匹配。不幸的是,我们(或 Git)还必须处理以下文件:aren't在你要切换的提交中are在您要切换到的提交中。这导致了剩余的复杂性,因为文件也可以存在于索引和/或工作树中,而不必存在我们正在处理的这两个特定的冻结提交。
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
and 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
与其中的一个匹配branch2
,即使我们在branch1
。此更改不是为提交而暂存的,这就是git 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'
这里的舞台和工作副本都与里面的内容相匹配branch2
,因此允许结账。
让我们尝试另一个步骤:
$ git checkout branch1
Switched to branch 'branch1'
$ cat inboth
this file is in both branches
我所做的更改现在已从暂存区域丢失(因为结账会通过暂存区域进行写入)。这是一个有点特殊的情况。改变并没有消失,但事实是我已经上演了它,is gone.
让我们暂存文件的第三个变体,与任一分支副本不同,然后设置工作副本以匹配当前分支版本:
$ echo 'staged version different from all' > inboth
$ git add inboth
$ git show branch1:inboth > inboth
$ git status --short
MM inboth
The two M
此处的意思是:暂存文件不同于HEAD
file, and,工作树文件与暂存文件不同。工作树版本确实匹配branch1
(aka HEAD
) 版本:
$ git diff HEAD
$
But 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的index。索引也称为“暂存区”,是您构建索引的地方next提交:它开始匹配当前提交,即您现在签出的任何内容,然后每次您git add
一个文件,你replace索引版本与工作树中的任何内容。
请记住,工作树是您处理文件的地方。在这里,它们具有正常的形式,而不是像在提交和索引中那样仅对 Git 有用的特殊形式。所以你提取一个文件from一次提交,through索引,然后进入工作树。改变之后,你git add
它到索引。因此,每个文件实际上有三个位置:当前提交、索引和工作树。
当你跑步时git checkout branch2
,Git 在幕后所做的就是比较提示提交 of branch2
到当前提交和索引中的任何内容。任何与现有文件匹配的文件,Git 都可以不理会。一切都未曾动过。两者相同的任何文件commits,Git 可以also不用管——这些是可以让你切换分支的。
Git 的大部分内容(包括提交切换)都相对较快因为这个指数。索引中实际上不是每个文件本身,而是每个文件的hash。文件本身的副本存储为 Git 所称的斑点对象,在存储库中。这也类似于文件在提交中的存储方式:提交实际上并不包含files,它们只是将 Git 引导至每个文件的哈希 ID。因此,Git 可以比较哈希 ID(当前为 160 位长的字符串)来决定是否提交X and Y有same文件与否。然后,它也可以将这些哈希 ID 与索引中的哈希 ID 进行比较。
这就是导致上述所有奇怪的极端情况的原因。我们有承诺X and Y两者都有文件path/to/name.txt
,我们有一个索引条目path/to/name.txt
。也许所有三个哈希值都匹配。也许其中两个匹配,一个不匹配。或许三者各有不同。而且,我们也可能有another/file.txt
那只是在X或仅在Y现在是否在索引中。这些不同的情况中的每一种都需要单独考虑:Git 是否need将文件从提交复制到索引,或从索引中删除它,以切换X to Y?如果是这样的话,也has to将文件复制到工作树,或将其从工作树中删除。而如果that在这种情况下,索引和工作树版本最好至少与已提交的版本之一匹配;否则 Git 会破坏一些数据。
(所有这些的完整规则都在中描述,而不是git checkout
正如您所期望的那样,而是文档the git read-tree文档,标题为“两棵树合并”的部分下 https://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html#_two_tree_merge.)