Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

git - squash before rebasing

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?

like image 687
Paymahn Moghadasian Avatar asked Feb 05 '15 22:02

Paymahn Moghadasian


People also ask

Should you squash before rebase?

Before rebasing such branches, you may want to squash your commits together, and then rebase that single commit, so you can handle all conflicts at once. Here's how to do that. Imagine you've been working on the feature branch show_birthday , and you want to squash and rebase it onto main .

How do you squash before rebasing?

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 git squash same as rebase?

So the differences are: squash does not touch your source branch ( tmp here) and creates a single commit where you want. rebase allows you to go on on the same source branch (still tmp ) with: a new base.


2 Answers

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.

like image 52
torek Avatar answered Nov 09 '22 23:11

torek


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:

like image 29
CodeWizard Avatar answered Nov 10 '22 00:11

CodeWizard