Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automate creating a single commit from diff between two branches

Tags:

git

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?

like image 792
dopoto Avatar asked Oct 15 '25 13:10

dopoto


1 Answers

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 makes git merge --squash kind of a misleading command. Git uses the merge machinery to get the result, but makes you run git 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.

like image 54
torek Avatar answered Oct 18 '25 03:10

torek



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!