24

I have two branches: master and test-branch (which is branched off of master). My work has looked something like the following:

  1. git checkout master
  2. git checkout -b test-branch
  3. make a bunch of changes and commit them
  4. make more changes and do another commit
  5. git checkout master
  6. git pull -> out other people have made changes master
  7. git checkout test-branch
  8. git rebase -i master
  9. Change all but the first pick in the interactive console to s
  10. I have to resolve two merge conflicts for, one for the first commit and another for the second commit

What I'd like to do is to squash all of the commits on test-branch before rebasing so that I only have to solve merge conflicts once. Is this possible? If so, how?

Paymahn Moghadasian
  • 9,301
  • 13
  • 56
  • 94
  • You can just use `git rebase -i master` from your `test-branch` and then elect to squash all the commits except the most recent. Is that what you're looking for? – larsks Feb 05 '15 at 22:13
  • 1
    whoops, that should have said ```master```. My bad. It's been fixed. @larsks, I did do that. I squashed all but the first commit but still had to resolve more than 1 merge conflict. – Paymahn Moghadasian Feb 05 '15 at 22:20

2 Answers2

18

It is possible, even easy. Git being what it is, there are many different ways to do it.

To literally do what you suggested originally:

... squash all of the commits on test-branch before rebasing

is easiest if you do it before running git merge while on branch master. (I know you didn't list git merge as a command, but you did run git merge in your step 6:

  1. git pull -> out other people have made changes master

because git pull is just git fetch followed by git merge.) But it's still pretty easy to do after; we just need to target the correct commit-ID.

Let's make a drawing of the commit graph that you have at step 4:

...<- o <- *            <-- master, origin/master
             \
               A <- B   <-- HEAD=test-branch

What this drawing shows is that there are two labels1 pointing to the commit I marked *, namely master and origin/master. Commit * points back to commit o which points back to yet more commits: that's the history of branch master.

The label for the branch you created (and are now on, hence the HEAD= part) points to commit B. Commit B then points to commit A, which points to commit *. That's the history of branch test-branch: you created it when at *, then added A, then added B.

Here are two ways you can easily squash commits A and B at this point:

  1. git rebase -i master

    This gives you an interactive edit session where you can "pick" the first commit and then "squash" the second, and it will gather together both commit messages and let you edit the result, in the usual way. It then makes a (single) new commit whose tree is that of commit B.

  2. git reset --soft master; git commit

    This does not open an interactive edit session for the rebase: it just keeps the staging-area-and-tree from commit B (that's the --soft part of git reset --soft), moves the label test-branch back to point to commit * directly (that's the git reset part of git reset --soft), and makes a new commit as usual (git commit).

    The drawback is that you have to compose a new commit message, but you can recover the commit message from commit A or B in any number of ways. For instance, you can use the -c or -C flag to git commit (you'd have to identify commit A or B, e.g., using @{1} or @{1}^ or @{yesterday} or some other reflog specifier). Or, before doing the git reset --soft, you can use git log and save the log message(s) in a file, or whatever.

    (This second method shines when instead of just two commits to squash, you have 42 or so.)

Both of these methods really do the same thing: they add a new commit (let's call it AB), leaving A and B behind and kind of greyed-out, which I can't really draw correctly:

              AB        <-- HEAD=test-branch
             /
...<- o <- *            <-- master, origin/master
             \
               A <- B   [ghost version of test-branch]

The ghost-version of your branch is invisible to most normal usage, and eventually (after 30 days or so by default) is garbage-collected away. (Until then it's still in your repository, and in your reflogs, so that you can find original commits A and B in case you need them after all.)

What if you've already done step 6? In that case, you must still identify commit *. You can do it the way jsexpert suggested while I was writing this up, or you can find it with git merge-base:

$ mergebase=$(git merge-base HEAD master)
# then pick just ONE of the next two
$ git rebase -i $mergebase
$ git reset --soft $mergebase; git commit

Here's how this works. After git checkout master; git fetch; git merge; git checkout test-branch (steps 5 and 6, more or less), your commit-graph now looks more like this:

...<- o <- * <- o       <-- master, origin/master
             \
               A <- B   <-- HEAD=test-branch

That new o commit that master and origin/master are pointing to—or it may be a whole chain of commits—are "in the way", but the "merge base" of test-branch (where you are now) and master is commit *: the nearest shared commit before the two branches diverge.

We then simply target the rebase or reset --soft at that commit. When we're done and have a single new AB commit, it looks like this:

              AB        <-- HEAD=test-branch
             /
...<- o <- * <- o       <-- master, origin/master
             \
               A <- B   [ghost version of test-branch]

Once you have the squashed AB commit, you can then git rebase that onto master in the usual way.

Note that the other answer is doing exactly the same thing, it's just identifying commit * by counting commits. If you know there are two commits between the tip of test-branch and the "interesting" commit *, HEAD~2 identifies the same commit as $(git merge-base HEAD master). Using git merge-base just allows you to avoid counting.


1"References" is the actual general git term. In this case they're branch names, and I am using the word "label" to distinguish them from the branch history formed by this commit graph. The word "branch" in git is used to refer to at least these two different things, and it gets confusing.

Community
  • 1
  • 1
torek
  • 448,244
  • 59
  • 642
  • 775
  • This squash solution is great to avoid same conflicts repeating from multiple commits during rebase. I'm wondering is there a way to keep original commits without squashing into one to avoid same conflicts repeating during rebase? – Johnson Sep 24 '17 at 02:15
  • @Johnson: there is an alternative: Git can record, and re-use, merge conflict resolutions. [The command that does this is `git rerere`, which normally you don't run directly at all.](https://git-scm.com/blog/2010/03/08/rerere.html) Be careful when using this: Git works only by text strings, not actual knowledge, and can re-use inappropriate recorded resolutions. But it's still way better than repeatedly resolving the same conflict. – torek Sep 24 '17 at 03:20
5

Of course it possible, yoiu simply need to type: git rebase -i HEAD~<# of commits to squash>

The -i is for interactive rebase. once you do it you will see the vi with instructions what to so next per commit.

A very details post about it can be found here Rewriting history:

CodeWizard
  • 128,036
  • 21
  • 144
  • 167
  • So I would run ```git rebase -i HEAD~<# of commits to squash>``` which can be used to alter the history of ```test-branch``` to only have a single commit (through squashing) and then run ```git rebase master``` to rebase the single-commit-feature-branch onto master. Correct? – Paymahn Moghadasian Feb 05 '15 at 22:24
  • yep. sqaush your desired commits and then do a `pull --reabse` to rebase your changes. read this post on how to do it. http://gitready.com/advanced/2009/02/11/pull-with-rebase.html – CodeWizard Feb 05 '15 at 22:26
  • So far as I can tell, the answer here does not work for more complicated Git histories, with merges since the original branching point. [This answer](https://stackoverflow.com/questions/1527234/finding-a-branch-point-with-git) is probably relevant, but the only solution not requiring `zsh` usage involves manually looking at a visualization of the graph and copying the correct commit hash. Ugh! – 75th Trombone Apr 01 '22 at 19:32