Reputation: 81
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
Reputation: 487745
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.
You have multiple options. If anything, you have too many options:
-n
so that they're pre-squashed.git rebase -i
and carefully split apart the "what commits to cherry pick" section from the "where to put the copies" section using --onto
.git rebase -i
without that kind of care, and delete extraneous pick
commands, if there are any (but here there won't be).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.
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.
What git rebase
needs is:
--onto master
will supply that; and--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 pick
s into squash
es 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