61

git log reveals the following:

commit 1abcd[...]
Author: [...]
Date: [...]

    [Useful commit]

commit 2abcd[...]
Author: [...]
Date: [...]

    Merge branch [...] of [etc. etc.]

commit 3abcd[...]
Author: [...]
Date: [...]

    [Useful commit]

That merge commit is useless to me - it doesn't represent a meaningful state of the branch and was generated from a remote pull, so I have the real commits of the remote history - no need for a commit to mark the fact that I pulled. I would like to squash this merge commit. My usual technique for doing a squash is:

git rebase --interactive HEAD~2 (or however far back I need to go)

And then I would squash it into a neighboring commit. I do this some times if for example I make a commit, realize I missed a tiny important detail (single file, or hadn't changed a line in one of the files), and do another commit that's basically just a quick oops. That way when I push my changes back to the remote, everything is nice and clean and tells a cohesive narrative.

However, in this case, when I run the git rebase ... command, commit 2abcd doesn't appear! It seems to skip right over 2abcd and instead displays 1abcd and 3abcd. Is there something special about a merge commit that prevents it from being appearing in git rebase --interactive? What other technique could I use to squash that merge commit?

UPDATE per @Cupcake's request:

The output of git log --graph --oneline --decorate looks like this:

* 1abcd (useful commit)
* 2abcd (merge)
|  \ <-- from remote
|   * 3abcd (useful commit)
|   |

Helpful?

Elias Zamaria
  • 96,623
  • 33
  • 114
  • 148
NWard
  • 2,016
  • 2
  • 20
  • 24
  • 1
    " Merge branch [...] of [etc. etc.]" is normally a git merge problem which happens usually during git pull. So, don't worry about, if you have another cloned repo, it won't be present there. Just tell me one thing, Commit "1abcd" is whether pushed to git repo or not. – love Jul 16 '14 at 17:25
  • I think i know what your problem might be, hold on... –  Jul 16 '14 at 17:30
  • 1
    @anujitm2007 I haven't pushed my commit history to the remote, so none of these three commits exists anywhere but in my local repository. – NWard Jul 16 '14 at 17:32
  • 2
    FYI, rebase doesn't normally preserve merge commits, you need to pass the `-p` or `--preserve-merges` flag for that...but there's probably an easier way to do what you want to do. Also, using interactive rebase while preserving merges can produce unexpected results if you reorder commits (see the documentation), but if you just squash you might be ok, but let's still see if there's an alternative. Please add the output of `git log --graph --oneline --decorate` for the commits in question. –  Jul 16 '14 at 17:33
  • @Cupcake Done. So do you mean that a rebase automatically squashes merge commits if you don't specify -p? – NWard Jul 16 '14 at 17:47
  • @NWard I'm not sure if you can think of it as a squash (I'll have to think about it), but it's sort of like a rebase (without merge-preserving) will kind of just cherry-pick everything to make the history linear again. You can test it out in a test repo to see what I mean. –  Jul 16 '14 at 17:48
  • If there a reason that you just pulled into your local branch instead of rebasing it on top of the remote one? That will have avoided a merge commit in the first place. –  Jul 16 '14 at 17:49
  • @Cupcake No, it was done accidentally. I typically use a pull --rebase especially when it's a simple synchronization pull. – NWard Jul 16 '14 at 17:52
  • @NWard is `1abcd` the last commit on the branch? –  Jul 16 '14 at 17:54
  • @Cupcake Yes, it is the HEAD commit. Sorry for the ambiguity. – NWard Jul 16 '14 at 17:59
  • The best way to avoid those is just to always use `git pull --rebase`. It's a bit more work if you have to do it a lot and your branch gets far away from the remote, but it is "cleaner" in the sense you seem interested in. – pattivacek Feb 21 '18 at 16:24

2 Answers2

49

Rebase doesn't normally preserve merge commits without --preserve-merges

Ok, so I'm not exactly sure what would happen if you tried to squash a merge commit using an interactive rebase with --preserve-merges...but this is how I would remove the merge commit in your case and make your history linear:

  1. Rebase everything before the merge commit on top of the remote branch.

  2. Cherry-pick or rebase everything after the merge commit on top of the previously rebased commits.

If you only have 1 commit after the merge commit

So in terms of commands, that would look something like this:

# Reset to commit before merge commit
git reset --hard <merge>^

# Rebase onto the remote branch
git rebase <remote>/<branch>

# Cherry-pick the last commit
git cherry-pick 1abcd 

If you have more than 1 commit after the merge commit

# Leave a temporary branch at your current commit
git branch temp

# Reset to commit before merge commit
git reset --hard <merge>^

# Rebase onto the remote branch
git rebase <remote>/<branch>

# Cherry-pick the last commits using a commit range.
# The start of the range is exclusive (not included)
git cherry-pick <merge>..temp

# Alternatively to the cherry-pick above, you can instead rebase everything
# from the merge commit to the tip of the temp branch onto the other
# newly rebased commits.
#
# You can also use --preserve-merges to preserve merge commits following
# the first merge commit that you want to get rid of...but if there were
# any conflicts in those merge commits, you'll need to re-resolve them again.
git rebase --preserve-merges --onto <currentBranch> <merge> temp

# These next steps are only necessary if you did the rebase above,
# instead of using the cherry-pick range.
#
# Fast-forward your previous branch and delete temp
git checkout <previousBranch>
git merge temp
git branch -d temp

Documentation

  • I'm going to wait a bit before accepting this to see if anyone else wants to weigh in, Git being as much an art as a science - but thank you for your help and effort. – NWard Jul 16 '14 at 18:24
  • @NWard sure, no problem...but, just so that I'm clear, this worked for you? –  Jul 16 '14 at 18:24
  • 1
    It did, otherwise I wouldn't be threatening to accept it ;) – NWard Jul 16 '14 at 18:26
  • @NWard cool, glad to know it worked. FYI, the instructions I originally gave you were overkill for your single commit on top of the merge, so I updated the instructions. If you have more than one commit afterwards, you could either still do the rebase I originally mentioned, or you could cherry-pick a range. –  Jul 16 '14 at 18:38
  • Using `git rebase` with both `--interactive` and `--preserve-merges` together shows the merge as a single commit. This then allows you to discard the merge commit but leave the others intact when rebasing. That way you don't need to `rebase` the part before the merge and `cherry-pick` the rest, a single rebase will do. – avivr Mar 06 '18 at 09:06
  • For people used to mercurial: > git config --global alias.histedit 'rebase -i --preserve-merges' – jaques-sam Aug 29 '18 at 07:53
  • 4
    (Note: Git 2.22, Q2 2019, actually [deprecates](https://github.com/git/git/commit/fa1b86e45743fd5895c33adcd3769782e608bb40) `--preserve-merge` in favor of `--rebase-merges`, and Git 2.25, Q1 2020, [stops advertising it](https://github.com/git/git/commit/0c51181ffb178a6581ecb471091cbd2d0c48f165) in the "git rebase --help" output) – Brian Olsen Dec 05 '20 at 12:29
22

Git offers a new way using --rebase-merges :

Before --rebase-merges I get 6 conflicts with my specific case.

git rebase -i --rebase-merges origin/master

And you may see something like :

label onto

reset 4127388 # Formatting
pick 87da5b5 feat: add something
pick 8fcdff4 feat: add ..
merge -C 80784fe onto # Merge remote-tracking branch 'origin/master' into a-branch-name
pick 3d9fec7 add unit tests
Hettomei
  • 1,934
  • 1
  • 16
  • 14