Working With git – History Manipulation
One of the most powerful features of git is that it gives you the ability to alter the commit history of your repository. This way I can have the best of both worlds by committing often during feature development, but then being able to consolidate all of the commits into a single “unit of work” when the feature is complete. In the case where no new commits have been made to the master branch, the feature branch can be combined into a single commit with:
git checkout master git merge feature-branch --squash
This will result in all of the commit changes being played on the master branch, but without anything being committed. Doing a git status would show all of the edits/adds/deletes as staged but not committed. The next step would be to do a git commit with a message about “feature branch x”. At this point all of the incremental commits have been replaced by one single commit. With this approach developers can commit as often as it makes sense during development, but the master branch will only contain the completed work in one commit.
The –squash merge option works great when the feature branch and master only differ by what has been added to the feature branch. But in the real world, it is often the case that while feature-a is being worked on, other commits have been made to master at the same time. To handle scenarios like this we can use git rebase…
When you are first learning git, rebase can be intimidating and is often ignored until you get more comfortable with the basic git features. The best way to explain rebase is to go through an example. Imagine a scenario where we have completed work on the feature-a branch and have maybe 7 commits we want to merge (and squash) into master. However, since we started feature-a work , let’s say feature-b guy finished their work and merged it to master. Ideally what we would like is to get the feature-b code into our feature-a branch. “git merge master” would work, but that would create a merge-commit and mess up our nice clean merge history in master. We can do better.
This is where rebase comes into play. Instead of merging the commits, rebase will undo the commits in the feature-a branch until the last commit that is in common with master, then pull in the commits in master that are not in feature-a, and then play-back the new commits in feature-a. It essentially makes it look like you didn’t start feature-a until feature-b was done.
When we first create the feature-a branch, it matches master:
We do some work on feature-a and are ready to merge back to master when we realize that master has been updated with feature-b:
This is where we use git rebase:
Let’s walk through what is happening here. As you can see, you need to have feature-a checked out first. The latest commit that the two branches have in common is “added file 2”, so rebase literally “rewinds” feature-a back to that common commit. Then the commits that are unique to master are applied to feature-a (in this case, the one commit is “feature-b work”). Finally, the “rewound” commits from feature-a are applied back to the feature-a branch. When the rebase completes, git log shows the new history on feature-a. Exactly what we wanted – the feature-a work is now AFTER the feature-b commit. Now a merge from feature-a back to master will be a fast-forward merge with no conflicts or merge-commits to deal with.
See – that wasn’t so scary. There are multiple options you can use with rebase to manipulate history in interesting ways that I will explore in another post.