如何退出(而不是中止)正在进行的Git合并,使更改未提交? [英] How to quit (not abort) a Git merge in progress keeping the changes uncommitted?

查看:163
本文介绍了如何退出(而不是中止)正在进行的Git合并,使更改未提交?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当Git合并由于冲突或由于用户使用--no-commit选项要求而停止时,认为合并仍在进行中,这由文件$GIT_DIR/MERGE_HEAD的存在指示. /p>

此状态通常在提交合并结果或中止合并时结束.在后一种情况下,合并所引入的更改将被撤销.

是否有任何方法可以完成正在进行的合并" 状态而不产生提交,也不会丢失合并中的更改?让我们假设所有可能的冲突已解决. 1 这类似于为樱桃采摘,还原和重新定标提供的--quit选项.

一种明显的方法是git reset --soft,但是在进行合并时,这会出错.

对于这个问题,我没有特定的用例,但是我想知道Git UI的完整性.


1 下面是torek精心编写的 answer 的以下内容,未解决的冲突表示索引中的条目插槽1到3,仅在进行合并时才有意义,严格来说甚至构成一个.这意味着:只要存在未解决的冲突,就可以进行合并.因此,要使该问题反映一个恰当的问题,就必须将其解决所有可能的冲突问题.

解决方案

您主要必须执行git reset,该操作会重置--mixed.那确实会丢失一些信息,这意味着答案是肯定的.如果没有冲突要解决,则可以执行git add -ugit add .或类似操作来恢复丢失的信息;但是无论如何,要使这个答案有意义,这里还有一些其他事情要实现.

首先,Git根本没有真正使用您的工作树.可以肯定的是,它确实将文件放入您的工作树中,并将在各种情况下(例如,与 index 的差异)检查它们的变化. git add -u,或者由于另一个git checkout而在破坏工作树文件之前.而且,当然,git merge以熟悉的形式将合并冲突写入工作树文件:

$ cat file
some contents
a few lines
of contents just
<<<<<<< HEAD
to make some
||||||| merged common ancestors
to make 
=======
to make
>>>>>>> branch
things more
interesting

(这是merge.conflictStyle设置为diff3的部分,用于添加||||||| merged common ancestors部分,以便您可以看到合并冲突发生之前的内容-在这种情况下,尾随空白之前的内容通过删除将其固定在branch上,但通过添加更多单词将其固定在master上.

但是,我上面提到的 index 是真正的动作所在.这个东西-这个索引,Git也称它为登台区域缓存,这取决于谁/哪个部分的Git进行了调用-包含版本将进入 next 提交的文件的数量.

就像工作树一样,索引纯粹是临时的.但是,从某种意义上说,它对Git的影响要大于工作树.您可以建立一个--bare信息库.这样的存储库没有 工作树,但它仍然具有提交,并且仍然具有索引. 1 Git可以将提交读入索引,并写入新的提交从索引开始,所有这些都不需要工作树.

通常情况下,索引中的内容非常简单. Git中的每次提交都会保存每个文件的完整副本.它以特殊的,只读的,压缩的,仅Git的形式保存该文件.我喜欢将这些文件称为冻干".每个这样的文件都有一个唯一的blob哈希值,即文件内容唯一的哈希值.索引中的内容恰好是这些相同的冻干文件,或更确切地说是blob散列.提交中的文件和索引中的文件之间的主要区别在于,索引中的文件可以用新的不同的冻干文件(不同的blob散列)覆盖.好了,您可以再次添加新文件或删除现有文件,都以这种冻干的形式进行.

无论如何,在Git中执行新提交的动作如此之快,因为真正要做的就是获取索引中已经存在的所有冻干文件,并记录其blob哈希值在一次提交中(通过更多名为 trees 的Git对象,但这仅仅是实现细节).新的提交将获得一个新的,唯一的哈希ID.新提交会记住先前当前提交的哈希ID;然后,通过将哈希ID写入HEAD或直通到分支名称的行为,新的提交将成为当前的提交,并成为当前分支的头(如果您在分支上).

因此,在通常的情况下,在一个非裸露的存储库中,每个文件总是有三个个活动副本.如果您有一个名为README.md的文件,则实际上有:

  • HEAD:README.md:这是当前提交中的冻结副本.您无法更改它,Git也无法更改. (您可以HEAD移至另一个提交,但这不会影响提交的README.md.)
  • :README.md:这是索引中的副本.它采用冻干格式,但是您可以随时用新副本替换它.
  • README.md:这是您可以看到,品尝和闻到的唯一文件,或者您对计算机上的文件所做的任何操作.它不是冻干格式的.它在您的工作树中,因此您可以处理任何其他文件.

一次影响这三个文件的主要命令(忽略了一次会影响许多文件的明显的git checkout <branch>之类)是

  • git checkout [commit] [--] paths:从提交中获取单个文件,将它们复制到索引中,然后再复制到工作树中.
  • git reset [commit] [--] paths:将某些提交中的单个文件复制到索引中,而不接触工作树.
  • git add [--] paths:将单个文件从工作树复制到索引,并在复制过程中将其冻干.

但是,当您开始合并时,索引将扮演一个新的,扩大的角色.现在,索引中不仅有每个文件的一个副本,而且最多可以有三个个副本.上例中的file的三个副本是:

  • :1:file:这是来自合并基础提交的文件,是工作树冲突文件中提到的合并的共同祖先.
  • :2:file:这是来自HEAD提交的文件.
  • :3:file:这是来自另一个提交的文件,在这种情况下,该提交位于branch的尖端.

目前没有:0:file-名称:file:0:file的简称-因为存在这三个非零的阶段编号文件.

当最终解决所有冲突并运行git commitgit merge --continue时,git merge进行合并提交的另一件事是其他提交的原始哈希ID.因此,Git已将其保存到.git(如您所述的.git/MERGE_HEAD)中的文件中.因此,合并正在进行的事实记录在两个位置:这个.git/MERGE_HEAD文件以及存在未合并的索引条目的事实.

要停止合并,必须必须将所有索引条目放回零阶段.这意味着您必须选择一些文件以从阶段1、2和/或3移入阶段0,或使用工作树副本(通过git add)放入阶段0,或使用复制到第0阶段.

现在,如果它们已经是所有处于 阶段的零,要么是因为您已经解决了所有冲突,要么是因为您没有冲突,而只是使用git merge --no-commit进入了此状态,有一个相当不好的副作用:git reset --mixedHEAD提交读入索引,这样您就失去了所有已经添加的已解决冲突.但是,那些添加的已解决冲突现在也在您的工作树中,因此您可以(可能)将它们放回去(但请参阅脚注之前的最后一段).

从定义上讲,任何在零阶段没有的条目都意味着有一个正在进行的未解决的合并.如果不销毁一些正在进行的未解决的合并,就无法解决该问题.

git reset --mixed具有删除.git/MERGE_HEAD的其他副作用,因此不再有正在进行的合并.这样就解决了另一个问题.最大的问题是如何处理更高级别的索引条目,而该特定问题只能通过破坏信息来解决.无论您要保留的是信息,还是必须完成.

如果出于某种原因,您巧妙地上载了不在工作树中的HEAD commit nor 中的某些版本的file,则此git reset --mixed将破坏其哈希值.因此,您可能想使用git checkout-index将其提取到某个位置的临时文件中,或者至少记录其blob哈希并使用git update-index --index-info进行还原.除此之外,答案主要只是git reset.


1 确实,它也可能也没有索引.毕竟,当您将工作树添加到非裸仓库时,实际上是同时添加了索引和工作树.但是Git的某些内部部分坚持或曾经坚持锁定索引,因此它必须有一个索引才能锁定.

When a Git merge stops due to conflicts or because the user asked for it by using the --no-commit option, the merge is considered to be still in progress, what is indicated by the existence of the file $GIT_DIR/MERGE_HEAD.

This state typically ends when the merge result is committed or the merge is aborted. In the latter case, the changes that were introduced by the merge are rewound.

Is there any way to finish the "merge in progress" state without generating a commit and without loosing the changes from the merge? Let us assume that all possible conflicts are resolved.1 This would be analogous to the --quit option that is offered for cherry picking, reverting, and rebasing.

An obvious way would be git reset --soft, but this bugs out when a merge is in progress.

I do not have any specific use case in mind for this question, but I am wondering about the completeness of the Git UI in this point.


1As follows from torek's very elaborated answer, unresolved conflics imply entries in the index slots 1 to 3, which only make sense during an ongoing merge, and strictly speaking even constitute one. This means: As long as there are unresolved conflicts one has an ongoing merge. So for this question to reflect a well posed problem, it has to be restricted to cases where all possible conflics are resolved.

解决方案

You mainly have to do git reset, which does a --mixed reset. That does lose some information, which means that really, the answer is no. If there are no conflicts left to resolve, you can then do git add -u or git add . or similar to restore the lost information; but in any case, there are a few extra things to realize here to make this answer make sense.

First, Git doesn't really use your work-tree for much at all. To be sure, it does put files into your work-tree, and it will examine them for changes—for differences with what's in the index—under various conditions, such as git add -u, or before clobbering the work-tree file due to another git checkout. And, of course, git merge writes merge conflicts into work-tree files in the familiar form:

$ cat file
some contents
a few lines
of contents just
<<<<<<< HEAD
to make some
||||||| merged common ancestors
to make 
=======
to make
>>>>>>> branch
things more
interesting

(this is with merge.conflictStyle set to diff3, to add the ||||||| merged common ancestors section so that you can see what was there before the merge conflict occurred—in this case what was there before was trailing whitespace, which I fixed on branch by removing it, but on master by adding more words).

The index that I mentioned above is where the real action is, though. This thing—this index, which Git also calls the staging area or the cache, depending on who / which part of Git is doing the calling—contains the version(s) of the file(s) that will go into the next commit.

Like the work-tree, the index is purely temporary. But it matters more, in a sense, to Git than the work-tree does. You can make a --bare repository; such a repository has no work-tree, but it still has commits, and still has an index.1 Git can read a commit into the index, and write a new commit from the index, all without the need for a work-tree.

What's in the index is pretty simple, in the normal case. Every commit in Git saves a full, complete copy of every file. It saves that file in a special, read-only, compressed, Git-only form. I like to call these files freeze-dried. Each such file has a unique blob hash—unique to the file's content, that is. What's in the index is precisely these same freeze-dried files, or more precisely, the blob hashes. The key difference between the files in a commit and the files in the index is that the ones in the index can be overwritten with new, different freeze-dried files (different blob hashes). Well, that, and you can add new files, or remove existing files, again all in this freeze-dried form.

In any case, the act of making a new commit is so fast in Git because all it really does is take all the freeze-dried files that already in the index, and record their blob hashes in a commit (through more Git objects called trees but that's a mere implementation detail). The new commit gets a new, unique hash ID; the new commit remembers the previously-current commit's hash ID; and then the new commit becomes the current commit, and the head of the current branch if you're on a branch, by the act of writing the hash ID to HEAD or through to the branch name.

Hence, in normal use, in a non-bare repository, there are always three active copies of each file. If you have a file named README.md, you actually have:

  • HEAD:README.md: this is the frozen copy in the current commit. You can't change it, nor can Git. (You can move HEAD to another commit but that doesn't affect this commit's README.md.)
  • :README.md: this is the copy in the index. It's in the freeze-dried format, but you can replace it with a new copy any time.
  • README.md: this is the only file you can see, taste, and smell—or whatever it is that you do with files on your computer. It's not in the freeze-dried format. It's in your work-tree, so you can do everything you would with any other file.

The main commands that affect these three files one at a time—ignoring the obvious git checkout <branch> and the like that affect many at a time—are:

  • git checkout [commit] [--] paths: gets individual files out of some commit, copying them into the index and then on into the work-tree.
  • git reset [commit] [--] paths: copies individual files from some commit to the index, not touching the work-tree.
  • git add [--] paths: copies individual files from the work-tree to the index, freeze-drying them during the copy.

When you start a merge, though, the index takes on a new, enlarged role. Instead of just one copy of each file in the index, there are now up to three copies. The three copies of file in the example above are:

  • :1:file: this is the file from the merge base commit, the merged common ancestors in mentioned in the work-tree conflicted file.
  • :2:file: this is the file from the HEAD commit.
  • :3:file: this is the file from the other commit, in this case the commit at the tip of branch.

There is no :0:file at the moment—the name :file is short for :0:file—because there are these three non-zero stage numbered files.

The other thing that git merge needs, to make a merge commit when you eventually resolve all conflicts and run git commit or git merge --continue, is the raw hash ID of the other commit. So Git has saved that into a file in .git (.git/MERGE_HEAD, as you mentioned). Hence, the fact that a merge is going on is recorded in two places: this .git/MERGE_HEAD file, and the fact that there are unmerged index entries.

To stop merging, you must put all the index entries back to stage zero. That means you must choose some file(s) to move from stage 1, 2, and/or 3 into stage zero, or use the work-tree copy (via git add) to put into stage zero, or use the HEAD copy to put into stage zero.

Now, if they're already all at stage zero, either because you have resolved all the conflicts or because you had no conflicts and had merely used git merge --no-commit to get into this state, this has one rather bad side effect: git reset --mixed reads the HEAD commit into the index, so that you lose all the already-added, resolved conflicts. But those added resolved conflicts are also in your work-tree right now, so you can (probably) put them back (but see last paragraph before footnote).

Any entries that aren't at stage zero mean, by definition, that there's an ongoing unresolved merge. There is no way to resolve that without destroying some of the ongoing unresolved merge.

The git reset --mixed has the additional side effect of removing .git/MERGE_HEAD, so that there is no longer an ongoing merge. So that solves the other problem. The big one is what to do with higher-stage index entries, and that particular problem can only be solved by destroying information. Whether that's information you meant to keep, or not, it has to be done.

If you have, for some reason, cleverly staged some version of file that is neither in the HEAD commit nor in the work-tree, this git reset --mixed will clobber its hash. So you might want to use git checkout-index to extract it to a temporary file somewhere, or at least record its blob hash and use git update-index --index-info to restore that. Other than that, though, the answer is mainly just git reset.


1Really, it probably should not have an index either. After all, when you add a work-tree to a non-bare repository, you actually add an index-and-work-tree both. But some internal parts of Git insist, or used to insist, on locking the index, so it has to have an index to lock.

这篇关于如何退出(而不是中止)正在进行的Git合并,使更改未提交?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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