When working on a feature, I always create a feature branch from the current master branch (E.G "feature/my-feature-name"), then I merge it back to master once I'm done.
The problem is that I end up bringing a lot of "Work in progress" meaningless commits to master this way.
To avoid this problem, I would like to create a new local feature branch (E.G. "feature/my-feature-name-CLEAN"), which does not contain any of the commits I made in "feature/my-feature-name", but has the diff between feature/my-feature-name and master as uncommitted changes. This way, I can then just commit those changes in "feature/my-feature-name-CLEAN" in one single commit, then merge it to master.
Something similar is described here: git create commit from diff between two branches
Do you know if there's a git command, a script or a Git client functionality that can do this automatically?
There are about three (or ten or hundreds, depending on how you want to count) different ways to do this in Git, but probably the easiest for your particular case is to use git merge --squash
.
Sidebar:
git merge
often makes merge commits.git merge --squash
never makes merge commits. This makesgit merge --squash
kind of a misleading command. Git uses the merge machinery to get the result, but makes you rungit commit
manually at the end, and you can supply a totally new commit message at this point.
Let's illustrate exactly how this works. Remember, Git is really all about commits—those entities with the big ugly hash IDs—and does not care much at all about branch names, which are the things like master
and feature/my-feature-name
. The name really just holds one commit hash ID. Each commit holds another hash ID,1 of its immediate parent commit, so that the commits can be handled by starting at the end and working backwards.
Real hash IDs look random, and are unpredictable. We'll use single uppercase letters like H
to stand in for the real hash IDs, and we'll allocate them sequentially so it's easy to see which commit we made when. Of course this will severely limit how many commits we can make (which is why Git uses such big ugly hash IDs). We start with the very first commit:
A <-- master
which has no parent, because there was no earlier commit. The name master
points to this commit (the name master
contains its actual hash ID).
Now we make a second commit. The new commit points to the first commit, and Git updates the name master
to make it point to the second commit:
A <-B <-- master
We make a third, fourth, etc., commit, and we get a chain:
... <-F <-G <-H <-- master
The name always points to (contains the hash ID of) the last commit in the chain. That is all a branch really is, in Git: a name that contains the hash ID of the last commit. Each commit contains the hash ID of the commit one-step-back, and that is how a Git repository holds history.
Let's get a little lazy here and stop drawing the internal arrows as arrows. It's not entirely laziness because now I want to start drawing a second branch name. We'll make a new name, feature/tall
:
...--F--G--H <-- feature/tall (HEAD), master
Note that both names point to the same commit. All the commits that are on branch master
are on our new branch, feature/tall
. We also add the word HEAD
in parentheses, attached to one of these names, so that we can remember which branch we're on.
Now we make new commits, some of which have silly subject likes like wip
or my hands are typing words
:
...--F--G--H <-- master
\
I--J--K--L--M--N <-- feature/tall (HEAD)
We've now reached the point where you want to get one single commit, which you'd like to label feature/tall-CLEAN
. So, first we need to find the hash ID of commit H
. If master
still points to H
, that's the easy way to find it.2 Assuming that's the case, you can simply do:
git checkout -b feature/tall-CLEAN master
which gives you this:
...--F--G--H <-- feature/tall-CLEAN (HEAD), master
\
I--J--K--L--M--N <-- feature/tall
Now you can run:
git merge --squash feature/tall
This compares commit H
—the commit you have out right now—to commit N
, to see what changed. It then applies those same changes to commit H
to get ready to make a new commit O
.
To actually make O
, you must run git commit
. This makes O
and moves the name feature/tall-CLEAN
to point to the new commit:
O <-- feature/tall-CLEAN (HEAD)
/
...--F--G--H <-- master
\
I--J--K--L--M--N <-- feature/tall
and you have exactly what you wanted.
1Actually, each commit has a list of such hash IDs. Most commits just have one entry in the list, though. A merge commit has two or more entries, and the very first commit you (or whoever) make in a new, empty repository has an empty list—no parent commits—because there is no earlier commit.
2If master
no longer points to H
, you will need to find commit H
. One way is to run git log --graph --oneline --decorate master feature/tall
, and examine the output. You can then cut and paste its abbreviated hash ID. Another is to use git merge-base --all master feature/tall
, which should print out H
's hash ID, which again you can cut and paste.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With