当前分支上有未提交的更改时签出另一个分支 [英] Checkout another branch when there are uncommitted changes on the current branch

查看:33
本文介绍了当前分支上有未提交的更改时签出另一个分支的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Most of the time when I try to checkout another existing branch, Git doesn't allow me if I have some uncommitted changes on the current branch. So I'll have to commit or stash those changes first.

但是,有时 Git 确实允许我在不提交或隐藏这些更改的情况下检出另一个分支,并且它会将这些更改传送到我检出的分支.

However, occasionally Git does allow me to checkout another branch without committing or stashing those changes, and it will carry those changes to the branch I checkout.

这里的规则是什么?更改是暂存的还是未暂存的有关系吗?将更改带到另一个分支对我来说没有任何意义,为什么 git 有时会允许它?也就是说,它在某些情况下有用吗?

What is the rule here? Does it matter whether the changes are staged or unstaged? Carrying the changes to another branch doesn't make any sense to me, why does git allow it sometimes? That is, is it helpful in some situations?

推荐答案

初步说明

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

Preliminary notes

This answer is an attempt to explain why Git behaves the way it does. It is not a recommendation to engage in any particular workflows. (My own preference is to just commit anyway, avoiding git stash and not trying to be too tricky, but others like other methods.)

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

The observation here is that, after you start working in branch1 (forgetting or not realizing that it would be good to switch to a different branch branch2 first), you run:

git checkout branch2

有时 Git 会说好吧,你现在在 branch2 上!";有时,Git 会说我不能那样做,我会丢失你的一些更改."

Sometimes Git says "OK, you're on branch2 now!" Sometimes, Git says "I can't do that, I'd lose some of your changes."

如果 Git 不会让你这样做,你必须提交你的更改,将它们永久保存在某个地方.您可能想使用 git stash 来保存它们;这是它的设计目的之一. 注意 git stash savegit stash push 实际上意味着提交所有更改,但根本不在任何分支上,然后将它们从我现在所在的位置删除."这使得切换成为可能:您现在没有进行中的更改.然后你可以在切换后git stash apply它们.

If Git won't let you do it, you have to commit your changes, to save them somewhere permanent. You may want to use git stash to save them; this is one of the things it's designed for. Note that git stash save or git stash push actually means "Commit all the changes, but on no branch at all, then remove them from where I am now." That makes it possible to switch: you now have no in-progress changes. You can then git stash apply them after switching.

侧边栏:git stash save 是旧语法;git stash push 是在 Git 2.13 版本中引入的,用于修复 git stash 参数的一些问题并允许新选项.当以基本方式使用时,两者都做同样的事情.

Sidebar: git stash save is the old syntax; git stash push was introduced in Git version 2.13, to fix up some problems with the arguments to git stash and allow for new options. Both do the same thing, when used in the basic ways.

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

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

You can stop reading here, if you like!

If Git won't let you switch, you already have a remedy: use git stash or git commit; or, if your changes are trivial to re-create, use git checkout -f to force it. This answer is all about when Git will let you git checkout branch2 even though you started making some changes. Why does it work sometimes, and not other times?

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

The rule here is simple in one way, and complicated/hard-to-explain in another:

也就是说——请注意,这仍然是简化的;有一些额外困难的极端情况,包括 git adds、git rms 等等——假设你在 branch1 上.git checkout branch2 必须这样做:

That is—and please note that this is still simplified; there are some extra-difficult corner cases with staged git adds, git rms and such—suppose you are on branch1. A git checkout branch2 would have to do this:

  • 对于branch1branch2的每个文件,1 删除那个文件.
  • 对于branch2branch1 的每个文件,创建该文件(具有适当的内容).
  • 对于两个分支中的每个文件,如果branch2 中的版本不同,请更新工作树版本.
  • For every file that is in branch1 and not in branch2,1 remove that file.
  • For every file that is in branch2 and not in branch1, create that file (with appropriate contents).
  • For every file that is in both branches, if the version in branch2 is different, update the working tree version.

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

Each of these steps could clobber something in your work-tree:

  • 删除文件是安全的";如果工作树中的版本与 branch1 中提交的版本相同;它是不安全的"如果您进行了更改.
  • 按照branch2 中显示的方式创建文件是安全的";如果它现在不存在.2它是不安全的".如果它现在确实存在但有错误"内容.
  • 当然,用不同版本替换文件的工作树版本是安全的";如果工作树版本已经提交给 branch1.
  • Removing a file is "safe" if the version in the work-tree is the same as the committed version in branch1; it's "unsafe" if you've made changes.
  • 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.
  • And of course, replacing the work-tree version of a file with a different version is "safe" if the work-tree version is already committed to branch1.

创建一个新分支(git checkout -b newbranch)总是被认为是安全的":不会在工作中添加、删除或更改文件-树作为此过程的一部分,并且索引/暂存区也未受影响.(警告:在不更改新分支起点的情况下创建新分支是安全的;但是如果您添加另一个参数,例如 git checkout -b newbranch different-start-point,这可能必须更改内容,移动到 different-start-point.然后 Git 将照常应用结帐安全规则.)

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.)

1这要求我们定义一个文件在一个分支中意味着什么,这反过来又需要正确定义branch这个词.(另请参阅我们所说的分支"究竟是什么意思?)在这里,我真正的意思是分支名称解析到的提交: 路径为 P isbranch1 中的文件如果 git rev-parse branch1:P 产生一个哈希值.如果您收到错误消息,该文件不在branch1 中.在回答此特定问题时,索引或工作树中路径 P 的存在与否无关.因此,这里的秘密是在每个 branch-name:path 上检查 git rev-parse 的结果.这要么失败,因为文件在"最多一个分支,或者给我们两个哈希 ID.如果两个哈希 ID 相同,则两个分支中的文件相同.无需更改.如果hash ID不同,说明两个分支的文件不同,必须改成切换分支.

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.

这里的关键概念是提交中的文件永远冻结.您将编辑的文件显然没有冻结.至少在最初,我们只关注两个冻结提交之间的不匹配.不幸的是,我们或 Git 还必须处理不在中的文件您将要切换的提交和在您要切换到的提交中.这导致了剩余的复杂性,因为文件也可以存在于索引和/或工作树中,而不必存在我们正在处理的这两个特定的冻结提交.

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.

2它可能被认为是安全的";如果它已经存在正确的内容",那么 Git 毕竟不必创建它.我记得至少有一些 Git 版本允许这样做,但刚刚的测试表明它被认为是不安全的".在 Git 1.8.5.4 中.相同的参数适用于修改后的文件,该文件恰好被修改以匹配 to-be-switch-to 分支.同样,1.8.5.4 只是说将被覆盖",不过.另请参阅技术说明的结尾:我的记忆可能有问题,因为我认为自从我在 1.5.something 版本开始使用 Git 以来,读取树规则没有改变.

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.

是的,在某些方面.特别是,您可以进行更改,然后取消修改".工作树文件.这是两个分支中的文件,在 branch1branch2 中是不同的:

Yes, in some ways. In particular, you can stage a change, then "de-modify" the work tree file. Here's a file in two branches, that's different in 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

此时,工作树文件inbothbranch2 中的文件匹配,即使我们在branch1 上.此更改不会为提交而暂存,这是 git status --short 此处显示的内容:

At this point, the working tree file inboth matches the one in branch2, even though we're on branch1. This change is not staged for commit, which is what git status --short shows here:

$ git status --short
 M inboth

space-then-M 表示修改但未上演";(或者更准确地说,工作树副本不同于暂存/索引副本).

The space-then-M means "modified but not staged" (or more precisely, working-tree copy differs from staged/index copy).

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

好的,现在让我们暂存工作树副本,我们已经知道它也与 branch2 中的副本匹配.

OK, now let's stage the working-tree copy, which we already know also matches the copy in branch2.

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

这里的暂存和工作副本都与 branch2 中的内容匹配,因此允许结帐.

Here the staged-and-working copies both matched what was in branch2, so the checkout was allowed.

让我们尝试另一个步骤:

Let's try another step:

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

我所做的更改现在从暂存区丢失了(因为结帐是通过暂存区写入的).这是一个角落案例.变化并没有消失,但我已经上演的事实,已经消失了.

The change I made is lost from the staging area now (because checkout writes through the staging area). This is a bit of a corner case. The change is not gone, but the fact that I had staged it, is gone.

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

Let's stage a third variant of the file, different from either branch-copy, then set the working copy to match the current branch version:

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

这里的两个 M 的意思是:staged 文件不同于 HEAD 文件,,working-tree 文件不同于 staged 文件.工作树版本与 branch1(又名 HEAD)版本匹配:

The two Ms here mean: staged file differs from HEAD file, and, working-tree file differs from staged file. The working-tree version does match the branch1 (aka HEAD) version:

$ git diff HEAD
$

但是 git checkout 不允许结帐:

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

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

Let's set the branch2 version as the working version:

$ 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结帐被拒绝.

Even though the current working copy matches the one in branch2, the staged file does not, so a git checkout would lose that copy, and the git checkout is rejected.

所有这些的底层实现机制是 Git 的 index.索引,也称为暂存区",是您构建下一个提交的地方:它开始匹配当前提交,即,无论您现在签出什么,然后每次您git add 一个文件,你替换索引版本为你的工作树中的任何内容.

The underlying implementation mechanism for all of this is Git's index. The index, also called the "staging area", is where you build the next commit: it starts out matching the current commit, i.e., whatever you have checked-out now, and then each time you git add a file, you replace the index version with whatever you have in your work-tree.

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

Remember, the work-tree is where you work on your files. Here, they have their normal form, rather than some special only-useful-to-Git form like they do in commits and in the index. So you extract a file from a commit, through the index, and then on into the work-tree. After changing it, you git add it to the index. So there are in fact three places for each file: the current commit, the index, and the work-tree.

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

When you run git checkout branch2, what Git does underneath the covers is to compare the tip commit of branch2 to whatever is in both the current commit and the index now. Any file that matches what's there now, Git can leave alone. It's all untouched. Any file that's the same in both commits, Git can also leave alone—and these are the ones that let you switch branches.

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

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.

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

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 checkout 文档中,而是 git read-tree 文档,在标题为双树合并"的部分下.)

(The complete rules for all of this are described in, not the git checkout documentation as you might expect, but rather the git read-tree documentation, under the section titled "Two Tree Merge".)

这篇关于当前分支上有未提交的更改时签出另一个分支的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆