Your question is not all that well formed, which means the answer is going to be rather long and perhaps inconclusive and unhelpful. But, well, here goes. You may be looking for git merge --squash
, but you may be engaged in a snipe hunt.
An actual merge normally creates a merge commit. A merge commit has two parents, by definition.1 Of course, the first statement requires defining what I mean by actual merge, but before we go there, let's look at git commit --amend
, since that command seems to be the focus of your question. (The emphasis here on seems is because, as RomainValeri noted in a comment, this may be an XY problem.)
1Technically, a merge commit has two or more parents.
The word merge, in Git, is both a verb—to merge, the action of combining work—and an adjective or noun. As an adjective, merge commit, it modifies the word commit, and as a noun, a merge, it is short-hand for the phrase merge commit. In the noun or adjective case it refers to a commit with at least two parents. Such commits are made by git merge
under certain conditions, which we'll describe below.
git commit
vs git commit --amend
The git commit
command builds a new commit. In general—ignoring options like --only
or --include
, which complicate the picture—this works by:
- gathering any metadata needed for the new commit, such as your name and email address and a log message;
- using
git write-tree
to turn the index into a snapshot, producing a tree hash ID;
- using
git commit-tree
to create a new commit with the tree hash ID from step 2 and the metadata from step 1, plus the appropriate parent hash IDs; and
- writing the resulting commit hash ID from step 3 into the current branch name (attached
HEAD
) or HEAD
itself (detached HEAD
).
The --amend
flag affects the way that Git gets the "appropriate" parent hash IDs for step 3.
In most cases, the (single) appropriate hash ID is the hash ID to which the name HEAD
resolves: that is, we just run git rev-parse HEAD
and we have the right hash ID. If we're committing a merge, however—as indicated by the presence of a MERGE_HEAD
pseudo-ref—then the appropriate hash IDs, plural, are the one from git rev-parse HEAD
plus the one from git rev-parse MERGE_HEAD
. Hence, if the merge-as-a-verb process from git merge
fails due to merge conflicts, and the person driving Git has fixed up those conflicts and runs git commit
, git commit
will now make a merge-as-an-adjective commit. This concludes the merge, so git commit
will now remove the MERGE_HEAD
pseudo-ref (git update-ref -d MERGE_HEAD
).2
With --amend
, we tell git commit
that instead of using HEAD
to find the (single) parent of the new commit, it should use the commit hash IDs in the current commit. That is, instead of git rev-parse HEAD
it should use git rev-parse HEAD^@
(see the ^@
suffix in the gitrevisons documentation), knowing that this may produce more than one hash ID.
It's not obvious what --amend
should do when concluding a merge, but the source code has the answer: it's simply forbidden. This completely sidesteps the "what should we do" question, by saying "we don't". We can only use --amend
when we do a normal commit, not when we conclude a merge.
2The commit command can also finish a cherry-pick, including a cherry-pick from an ongoing rebase, or a revert, which internally is also a cherry-pick. See the possible "whence" values listed in wt-status.h
.
Actual merges
It's time to define the phrase actual merge. What exactly do I mean by this phrase? I'm afraid the easy definition is overly circular: an actual merge is one where the result is a merge commit, and a merge commit is one made by an actual merge. It's not completely circular since we also know that a merge commit is one with two or more parents, but it's also not quite accurate because I'm using actual merge a bit sloppily (on purpose) so that I can get to git merge --squash
. We can use that to work backwards. Let's look at this a different way, and define actual merge in terms of the process that git merge
itself kicks off.
When you run git merge
, you give it options and arguments. Ignoring the options for now, let's look at the simple case of:
git merge thing
where thing
is either a branch name or a commit hash ID. In fact, if thing
is a branch name, git merge
runs git rev-parse
on it to get its hash ID, keeping the branch name only for building the default log message: it's the hash ID that matters for the rest of the job. So this really devolves into git merge hash-id
.
Git now uses the current commit hash ID, as from git rev-parse HEAD
, and this other hash ID to inspect the commit graph. It uses a Lowest Common Ancestor algorithm to find the merge base commit of these two commits. Ideally, this produces a single commit hash ID: if it produces more than one hash ID, we have to get into the -s resolve
vs -s recursive
merge strategies. So let's just ignore that case, along with the one where there is no merge base commit at all.
Having found the (single) merge base commit, we now have three cases:
The merge base commit is the other commit. The other commit is already merged; git merge
says there is nothing to do, and quits. (This includes the case when the current and other commit are the same commit.)
The merge base commit is the current commit. The other commit is strictly ahead of this commit, and git merge
can do a fast-forward merge.
The merge base commit is behind both the current commit and the other commit. A true merge is required, and git merge
will do an actual merge.
If a fast-forward merge is possible, Git won't do an actual merge by default, but by adding --no-ff
to the git merge
arguments, Git will do an actual merge. So this gets us a definition of actual merge: it's one where a merge commit is either required, or forced through --no-ff
, from a git merge
command that finds a merge base that's not the other commit.
All of this is a rather long-winded way of arriving at my real meaning of actual merge, which is: a git merge
command that invokes the to merge, or merge as a verb, action. The merge as a verb action is what we want to consider here. It has three inputs:
- the merge base commit, as found by
git merge
when it checked to see if there was something to merge and decided that fast-forwarding (instead of merging) was either impossible, or forbidden by command-line flags;
- the current commit, whose hash ID has been found via the name
HEAD
; and
- the other commit, whose hash ID was the argument to
git merge
(or git merge
found it from a branch name).
The merge-as-a-verb process now compares the merge base snapshot with each of the other two commits' snapshots, to find out what changed, and then combines these changes and applies the combined changes to the merge base snapshot.3 We'll ignore this process entirely, except to note that it can have a merge conflict.
If it does have a merge conflict, it stops, leaving a bit of a mess in your working tree—you can use this to finish the merge, if you like—and whatever it successfully merged in its own index, which is where it will look when you use git commit
to conclude the merge later. What it failed to resolve stays in its index, in slots that mark the merge as unresolved. As you resolve each conflict, you will un-mark those slots, usually with git add
.4
If there are no merge conflicts, the merge—as represented by running a merge strategy (see footnote 3)—finishes the merge in Git's index, and git merge
normally now makes a merge commit on its own.
3This glosses over the role of the merge strategy. In particular the -s ours
strategy doesn't combine anything at all. It just takes the snapshot from the "ours" (HEAD
) commit. Some strategies also take more than one "other commit": -s ours
and -s octopus
both do so. Tossing the strategy stuff into the mix here, however, makes the process obscure, and probably does not matter for the original question.
4If you use git mergetool
, the git mergetool
code will run the git add
step for you, under various conditions.
Using --no-commit
The merge-as-a-verb process combines work in two different branches. It does so by comparing a single common snapshot, as stored in the merge base commit, to two different snapshots, as stored in the --ours
(HEAD
) commit and --theirs
(other) commit. These comparisons produce two line-by-line diffs, and Git applies simple text-combining rules to combine the diffs and apply those combined diffs to the files that are stored in the merge-base commit.
This process doesn't always work. Git has no idea about the semantics of the input files, and line-by-line combining may be the wrong thing to do. If the result isn't right, the merge commit that Git makes might not be what you wanted. It might be close to what you wanted, but maybe you'd like to have git merge
stop before committing, so that you can check. If you find that Git's merge was close, but needs some tweaking, you can fix it up,5
and then run git commit
or git merge --continue
to conclude the merge.6
Using git merge --no-commit
makes git merge
stop before committing, the same way it would stop if the merge-as-a-verb process failed. Since the process didn't actually fail, the copies of files in your working tree and in Git's index are all successfully merged, but since it did stop, you can edit the files in your working tree, and then use git add
to copy the edited files back into Git's index. So this allows you to make the final merge commit without having to use git commit --amend
later.
The result, of course, is the same even if you do allow git merge
to make the commit on its own, then discover some bugs and fix them up and use git commit --amend
, because --amend
will keep both parents when you use it on a merge commit. So, presumably, --no-commit
isn't the option that you want for this particular problem.
5There are drawbacks and dangers here, mostly having to do with the potential need to repeat a merge later, or so-called evil merges. That does not mean never do it, but rather, be aware. See also Evil merges in git?
6git merge --continue
simply checks that there is an ongoing merge—that the MERGE_HEAD
pseudo-ref exists—and then literally runs git commit
. It's just a sort of safety check, to make sure you are doing what you think you are doing.
Using --squash
The --squash
option is similar to the --no-commit
option, with one extra feature. It not only stops the merge from committing, it also removes the MERGE_HEAD
pseudo-ref (or never creates it, really). This means your eventual git commit
, to conclude the merge, makes a non-merge commit. The commit that your git commit
makes is an ordinary, single-parent commit.
(It's kind of odd that this stops the commit: git merge --squash
should just omit the pseudo-ref and commit. If you want it not to make the commit too, you could run git merge --squash --no-commit
. The two options make sense either stand-alone or combined: there's no need to make git merge --squash
combine them. The only reason for Git to work this way is that it was convenient to write, back in the days when this stuff was all just shell scripts.)
Note that the result is that the merged branch isn't merged after all. In the typical merge case, we start with:
I--J <-- ours (HEAD)
/
...--H
\
K--L <-- theirs
and end up with:
I--J
/ \
...--H M <-- ours (HEAD)
\ /
K--L <-- theirs
This is how we know that theirs
is merged. With git merge --squash
, though, we end up with:
I--J--S <-- ours (HEAD)
/
...--H
\
K--L <-- theirs
where the snapshot in commit S
matches the snapshot we would have in commit M
, had we made commit M
. But commit S
has just a single parent, J
, instead of two parents J
and L
. It is therefore impossible to know for sure, later, whether commit L
was actually merged in. The only thing that is sensible to do with the theirs
branch at this point is to delete it entirely:
I--J--S <-- ours (HEAD)
/
...--H
\
K--L ???
Now that we can't find commits K-L
, we won't wonder whether they were ever merged. But since commits K-L
might exist in many other Git repositories—Git spreads commits to other Git repositories like they were viruses—they could come back to haunt us later (like viruses). It's very hard, but usually pretty important, to make sure they're stamped out everywhere, if you're going to squash like this.