TL;DR answer
You cannot achieve what you want via git rebase
. You may be able to achieve what you want via git merge --squash
, but unless you know what you are doing, you should in general not use git merge --squash
at all. (That is, this may be the wrong approach to the overall problem.)
As a rule of thumb, after running git merge --squash
for some set of commits, you should delete the branch or branches that contain those commits, as they're no longer good to develop on. (There may be specific exceptions.) In this case, that would mean deleting each of the branches that you had merged.
Things to know about rebase
As evolutionxbox said in a comment, when you ran your git rebase -i branchname
command, you selected to copy no commits at all, placing the copies after the current commit ac174b8dc1dc44e91b56c89c55003942070b9742
.
This is probably a good thing, because when you do select some set of commits to copy, placing the copies after some other specifed commit, the commits that Git will copy omit all the merge commits. This is because it is both impossible, and generally unproductive, to copy a merge commit. Since 9 of the 11 commits in the list above are merges, you cannot copy them.
It's important to remember that what git rebase
does is to copy commits, as if via using the git cherry-pick
command. In general, you will have a Git repository that has some set of commits in it, and you can and should draw a picture—a graph—of those commits, or at least, those that are to be involved in the rebase:
E--F--G <-- new-desired-base
/
...--*
\
A--B--C--D <-- four-commits
Here, each of the four commits that are reachable only from the name four-commits
are ordinary (non-merge) commits. The parent of commit D
is commit C
; the parent of C
is B
; the parent of B
is A
; and the parent of A
is the commit I marked with an asterisk *
.
If you now run git checkout four-commits && git rebase new-desired-base
, Git will, starting from the commit to which four-commits
points (i.e., D
), select commits that are not reachable by starting from the commit to which new-desired-base
points, i.e., G
. The commits reachable from G
are G
, F
, E
, *
, and every commit before commit *
. So subtracting that list from the list D, C, B, A, *, ...
leaves D, C, B, A
.
Git will now switch to commit G
, as identified by new-desired-base
, and copy each of the four commits, in reversed order, i.e., the most-ancestral A
great-grand-parent of D
, then the grandparent B
, then the parent C
, and finally D
itself. Each copy is done as if by git cherry-pick
.1 If all goes well, the result looks like this:
E--F--G <-- new-desired-base
/ \
...--* A'-B'-C'-D'
\
A--B--C--D <-- four-commits
where the '
(prime) marks indicate which commit was copied to a new commit with a new hash ID.
Finally, if all has gone well, Git will take the name four-commits
off the original chain, and paste it onto the end of the new chain, re-attaching HEAD
in the process:
E--F--G <-- new-desired-base
/ \
...--* A'-B'-C'-D' <-- four-commits (HEAD)
\
A--B--C--D [abandoned]
Now that there is no name for original commit D
, it seems to vanish entirely. This takes the entire chain with it, so A-B-C-D
seem to have been replaced with A'-B'-C'-D'
:
E--F--G <-- new-desired-base
/ \
...--* A'-B'-C'-D' <-- four-commits (HEAD)
The original commit chain is still in your repository, retrievable by its hash ID if you've saved that somewhere (and by some special mostly-hidden names in case you haven't saved it), typically for at least 30 more days, in case you change your mind about the rebase.
Note that if anyone else has those commits in their repositories—i.e., has those hash IDs and has their own names for them—those commits will remain forever in their repositories. You must get them to remove their names for those commits, before those commits will eventually go away from their repositories too.
1An interactive rebase really does run git cherry-pick
on each commit to be copied. Some other forms of rebase don't, but the effect is normally the same.
What to consider before proceeding
Since that's how rebase works, it is a good idea to keep it in mind. It's even more important to remember that it doesn't copy merge commits, so if you asked it to copy your chain of commits, it would omit the merges. In general, though, it's a bad idea to ask Git to rebase something that includes a merge, because the commits that are selected to be copied tend to include the commits being merged.
Consider the following simplified diagram:
...--A--B----F <-- master
\ \
\ D <-- feature-B
\
C--E <-- feature-A
Remember that commits are to the left if they are more-ancestral (parents or grandparents), and to the right if they are less-ancestral (children). So master
has had one commit added, namely F
, since feature-B
branched off. It's had two commits added, namely B
and F
, since feature-A
branched off. Meanwhile feature-A
contains two commits, C
and E
, that are not on master
, and feature-B
contains one commit, D
, that is not on master
.
If your goal is to make a single new commit on master
that incorporates both features at once, you might choose to first merge the two feature branches. You can do this with either:
git checkout feature-A && git merge feature-B
or:
git checkout feature-B && git merge feature-A
The result of the merge, if successful, will be a new commit G
. This new commit will be based upon (as in, the merge base is) commit A
:
Git will compare the contents of the snapshot for commit A
with that for the snapshot in D
. This is, in effect, what happened in feature-B since commit A
. Note that this includes what happened in B
, which is a commit that is reachable from the tip of master
(by looking at D
's parent).
Git will, separately, compare the contents of the snapshot for commit A
with that for the snapshot in E
. This is, in effect, what happend in feature-A since commit A
. Comparing A
vs E
includes whatever happened in C
, since the snapshot in E
includes whatever happened in C
(well, minus any reversions back to what was in A
).
Now that Git has the two sets of changes, Git will combine them, and apply the resulting bigger change to the snapshot in commit A
. This gives it the contents needed to produce new snapshot/commit G
:
...--A--B----F <-- master
\ \
\ D---G
\ /
C--E
If you do this with git checkout feature-B && git merge feature-A
, new commit G
is added to the feature-B
branch:
...--A--B----F <-- master
\ \
\ D---G <-- feature-B (HEAD)
\ /
C--E <-- feature-A
If you do this with git checkout feature-A && git merge feature-B
, new commit G
is added to the feature-A
branch instead, so we might draw it more like this:
...--A--B----F <-- master
\ \
\ D <-- feature-B
\ `--_
C--E--G <-- feature-A
Either way, though, G
is a merge commit, and therefore cannot be rebased, unlike commits D
or C--E
.
Using git merge --squash
It is, however, now possible to run:
git checkout master && git merge --squash <something>
The <something>
here is anything that produces the hash ID of new commit G
. You could type in the raw hash ID; or, if you did the merge so that commit G
has been added to feature-B
, you could use the name feature-B
.
The git checkout master
step attaches HEAD
to master
, and of course checks out the tip commit of master
, i.e., commit F
. So assuming G
is on feature-B
, this looks like:
...--A--B----F <-- master (HEAD)
\ \
\ D---G <-- feature-B
\ /
C--E <-- feature-A
The git merge --squash
step does the verb part of git merge
, i.e., the work that git merge
would do: it finds the merge base, just as before, and then runs two git diff --find-renames
commands, just as before.
The merge base of commits F
and G
is commit B
. This is because the merge base is, roughly speaking, the first commit that's reachable from both branch tips. From F
, we step back once to B
. From G
, we step back once to D
and once to E
, then a second time from both D
and E
to both B
and C
. We've now reached a common point—commit B
—so B
is the merge base.
So, as before, Git runs git diff --find-renames B F
and git diff --find-renames B G
, to see what has happened in the two branches. Git then combines those changes together, applies them to the contents of snapshot B
, and readies a new snapshot H
.
Here git merge --squash
departs from regular git merge
in two ways:
- It skips the commit, as if you had added the
--no-commit
option, or as if there had been a merge conflict.
- When you do make the commit yourself manually, it records only one parent hash ID in the new commit.
So when you run git commit
now, you get:
...--A--B----F--H <-- master (HEAD)
\ \
\ D---G <-- feature-B
\ /
C--E <-- feature-A
The contents of commit H
, i.e., the snapshot, is the same as if you had done a regular merge; but a regular merge would record the result as:
...--A--B----F--H <-- master (HEAD)
\ \ /
\ D---G <-- feature-B
\ /
C--E <-- feature-A
Since the squash merge did not record the extra parent, Git will not know, later, that H
already has all the work from G
. (With a real merge, Git would know that, because the merge base would be commit G
.)
What this means for the person doing the squash-merge is that it now becomes counterproductive to do any further development on either feature-A
or feature-B
, because they will be more difficult to merge into master
later, because of the missing parent linkage. It's therefore probably appropriate to delete both names feature-A
and feature-B
at this point:
...--A--B----F--H <-- master (HEAD)
\ \
\ D---G [abandoned]
\ /
C--E [abandoned]
As with rebasing, the extra commits C--E
and D--G
will stick around a short while (perhaps shorter than with rebase) even once the names are gone, but eventually they do go away. But remember, if anyone else has names for those original commits, saved away in some other repository, those commits—and those names—will remain forever in their repositories, unless you convince them to delete them. If they base their own work on those commits, they may have trouble, later, combining their work with your squashed commit H
.