henriettalLUx
henriettalLUx

Reputation: 81

Git: How to rebase into 1 commit when there's a merge in the middle?

I am on a feature branch, feature-1, which is off of master

My commit history is:

`Adding rehX project`,                     // <-- my first commit
`Adding factory resets for rehX `,                      // <-- my second commit
`Setting regulatory values for rehX `,                  // <-- my third commit
'Merging in master and resolving conflicts'  / / <--- my fourth commit was merging in `master`
'Adding last minute request from QA'.        // <-- my unexpected 5th commit

When I don't merge master, I usually use git rebase -interactive HEAD~(number of commits) so that I can squash all of the commits into the initial commit.

Now that I merged master in though, it complicates things. Do I have any options here?

Upvotes: 0

Views: 3182

Answers (1)

torek
torek

Reputation: 487745

TL;DR

Just use git rebase --interactive master. See below for why.

Edit: I didn't notice that, in the subject line, you wanted to get just one commit, which means there's one more way.

Long-ish

You have multiple options. If anything, you have too many options:

  • You can make a new branch and cherry-pick commits one at a time.
  • You can make a new branch and cherry-pick commits with -n so that they're pre-squashed.
  • You can use git rebase -i and carefully split apart the "what commits to cherry pick" section from the "where to put the copies" section using --onto.
  • You can use git rebase -i without that kind of care, and delete extraneous pick commands, if there are any (but here there won't be).
  • Or, to turn everything into a single commit, use git reset --soft and git commit.

Note that the first four of these options work the same way, in the end, so it's really just a matter of personal preference. Regardless of which method you use, you won't copy the existing merge. In fact, rebase can't copy a merge, though the newfangled --rebase-merges (-r) option will offer / claim to do it. It doesn't: it just runs git merge again to make a new merge. If you wish to do that yourself, you can do that yourself when using the first two (manual mode, hand-operated cherry-pick) methods.

Let's start with a drawing of what you have:

          ----------------M--N   <-- feature-1 (HEAD)
         /               /
...--H--I   <-- master  /
         \             /
          J-----K-----L

The uppercase letters stand in for commit hash IDs. The name feature-1 is your current branch name, as indicated by the attached HEAD in parentheses, and commit N is your current commit, which is the one whose subject line is Adding last minute request from QA.

(Exercise: redraw this graph in a simpler way. On paper or a whiteboard it can be very simple. Check to see if you have commits on master that I didn't draw in, though. Try out all of this if you do have such commits.)

What you need is to build some new series of commits that come after commit I—commit I being the last commit on master—by using the contents of existing commits J-K-L and N, but not those of M as we plan to drop the merge entirely.

For the special case of a single replacement commit

To make just one new commit that preserves the existing state of commit N, all we need to do is use git reset to move the current branch name back. Note that we want a git reset --soft, which leaves both Git's index and the current working tree state undisturbed. The result is:

          ----------------M--N   [abandoned]
         /               /
...--H--I   <-- master, / feature-1 (HEAD)
         \             /
          J-----K-----L

That is, now both branch names select existing commit I.

We can now run git commit. This makes a new commit from whatever is in Git's index, which—as git status should show—should match the current commit: we'll have files marked staged for commit and no files that are being called not staged for commit. (If we do have some such files, we can git add them if desired.)

The one drawback to this git commit is that we must re-build the entire commit message. If that's a problem, it's possible to save each of the other commit messages first (with, e.g., git log redirected to a file). Unlike the rebasing methods, there is no built-in trivial way to combine the existing commit messages, though.

The final result is one new commit, with the original series of commits being hard to find; we can draw the one new commit like this:

...--H--I   <-- master
         \
          O   <-- feature-1 (HEAD)

Note that this only works for the "make one single new commit" case. If you'd like to combine two commits into one, but keep the others separate, you cannot use this short-cut.

For all the rebase cases (including manual cherry-pick "rebase")

What git rebase needs is:

  • the hash ID of the place where the copies go: --onto master will supply that; and
  • the hash ID of a commit not to copy. This is from the non---onto argument.

Git will not copy this commit, nor any commit reachable by starting at this commit and working backwards (leftwards, in the drawing above; git log --graph draws a graph with newer commits at the top, rather than at the right, so with git log --graph you'd skip commits that are connected "downwards").

The commits we need Git not to copy are commits I, H, and everything before them. So we could supply git rebase with the hash ID of I, or the name master:

git rebase --onto master master

Whenever the --onto and remaining argument name the same commit, we don't actually need the --onto, so this simplifies into:

git rebase master

Whether interactive or not, this kind of rebase—without the -r option—drops merge commits entirely. So the list of commits to copy will be J, K, L, and N in that order.

Without --interactive, Git will try to copy each commit on its own. With --interactive, Git puts up an instruction sheet, which you can then edit, changing some picks into squashes or whatever.

Git then goes off the instruction sheet: for each pick, Git runs git cherry-pick. For each squash, Git runs a git commit --amend with a carefully chosen set of arguments. If you do this yourself with individual git cherry-pick commands, you can plan ahead and run some of them with -n: this is slightly more computer-efficient, but mostly a waste of time since computer resources are cheap and humans are expensive.

So, for this particular rebase, all you need is the same interactive rebase you would normally do: but instead of trying to count commits for HEAD~number, just use the branch name master to find the right point. If you wanted to count commits, it's tricky because of the merge commit: there are five commits down one path, and two commits—just N and M—down the other. Which path does ~ take? (That's another exercise for the student.)

Upvotes: 3

Related Questions