6

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.

Jürgen
  • 387
  • 2
  • 8
  • 1
    I thing the [`--squash` option](https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---squash) to `merge` results in something close to what you’re asking for, but you’d have to know to use it in advance. – matt Apr 27 '19 at 02:50
  • 1
    Actually, with Git 2.23 (Q3 2019), `git merge` does have a `--quit` option! See [my answer below](https://stackoverflow.com/a/56605236/6309) – VonC Jun 14 '19 at 21:19
  • Does this answer your question? [Cancel git merge but keep local changes](https://stackoverflow.com/questions/34913989/cancel-git-merge-but-keep-local-changes) – Matt Joiner Jan 23 '22 at 05:31

3 Answers3

8

With Git 2.23 (Q3 2019), this will be easier (no more git reset), since "git merge" learned "--quit" option that cleans up the in-progress merge while leaving the working tree and the index still in a mess.

See commit f3f8311 (18 May 2019), and commit b643355 (09 May 2019) by Nguyễn Thái Ngọc Duy (pclouds).
(Merged by Junio C Hamano -- gitster -- in commit c4a38d1, 13 Jun 2019)

merge: add --quit

This allows to cancel the current merge without resetting worktree/index, which is what --abort is for.
Like other --quit(s), this is often used when you forgot that you're in the middle of a merge and already switched away, doing different things.
By the time you've realized, you can't even continue the merge anymore.

This also makes all in-progress commands, am, merge, rebase, revert and cherry-pick, take all three --abort, --continue and --quit (bisect has a different UI).


The documentation is in commit 437591a (17 Jun 2019) by Phillip Wood (phillipwood).
(Merged by Junio C Hamano -- gitster -- in commit 0af6d5d, 09 Jul 2019)

git merge --quit:

Forget about the current merge in progress.
Leave the index and the working tree as-is.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
2

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.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Great that you mention the entries in index slots 1 to 3 as constituents of an ongoing merge. I clarified the question accordingly. I very much appreciate the conceptual insights underneath the hood that you provide in your answers to this and other questions. This is what I occasionally miss in the Git documentation. – Jürgen Apr 27 '19 at 00:23
2

Is there any way to finish the "merge in progress" state without generating a commit

Don't bother. Commit the merge, git reset --soft @~, and you're done.

jthill
  • 55,082
  • 5
  • 77
  • 137
  • While this actually bypasses the heart of the question, it seems to be the most practical approach to achieve an equivalent result: Still doing a merge commit and abandoning it afterwards. This requires to resolve possible conflicts beforehand, as any solution will do (per definition). – Jürgen Apr 27 '19 at 00:31
  • Yes, it's the resulting history that matters. Git's a history-construction workbench, I don't see anything wrong with a bit of scrap as scaffolding, discarded notes and what not. – jthill Apr 27 '19 at 00:47