The 2016 Git Retrospective: Rebase
The 2016 Git Retrospective: Rebase
Rebasing is a convenient and powerful way to restructure your Git history. You can now rebase and pull with a single command, or perform a rebase exec without needing to drop into interactive mode.
Join the DZone community and get the full member experience.Join For Free
Discover how TDM Is Essential To Achieving Quality At Speed For Agile, DevOps, And Continuous Delivery. Brought to you in partnership with CA Technologies.
Welcome to the fourth part of our Git in 2016 retrospective series! In part three, we covered changes made to Git and Git LFS to improve support for tracking large files with Git repositories. This time, we'll be looking at some enhancements made to the
git rebase command throughout the year.
In March, Git v2.8 added the ability to interactively rebase whilst pulling with a
git pull --rebase=interactive. Conversely, June's Git v2.9 release implemented support for performing a rebase exec without needing to drop into interactive mode via
git rebase -x.
Before we dive in, I suspect there may be a few readers who aren't totally familiar or comfortable with the rebase command or interactive rebasing. Conceptually, it's pretty simple, but as with many of Git's powerful features, the rebase is steeped in some complex-sounding terminology. So, before we dive in, let's quickly review what a rebase is.
Rebasing means rewriting one or more commits on a particular branch. The
git rebase command is heavily overloaded, but the name rebase originates from the fact that it is often used to change a branch's base commit (the commit that you created the branch from).
Conceptually, rebase unwinds the commits on your branch by temporarily storing them as a series of patches, and then reapplying them in order on top of the target commit.
Rebasing a feature branch on master (
git rebase master) is a great way to "freshen" your feature branch with the latest changes from master. For long-lived feature branches, regular rebasing minimizes the chance and severity of conflicts down the road.
Some teams also choose to rebase immediately before merging their changes onto master in order to achieve a fast-forward merge (
git merge --ff <feature> ). Fast-forwarding merges your commits onto master by simply making the master ref point at the tip of your rewritten branch without creating a merge commit:
Rebasing is so convenient and powerful that it has been baked into some other common Git commands, such as
git pull. If you have some unpushed changes on your local master branch, running
git pull to pull your teammates' changes from the origin will create an unnecessary merge commit:
This is kind of messy, and on busy teams, you'll get heapsof these unnecessary merge commits.
git pull --rebase rebases your local changes on top of your teammates' without creating a merge commit:
This is pretty neat! Git v2.8 also introduced an even cooler feature that lets you rebase interactivelywhilst pulling.
Interactive rebasing is a more powerful form of rebasing. It also rewrites commits but gives you a chance to modify them as they are reapplied onto the new base.
When you run
git rebase --interactive (or
git pull --rebase=interactive), you'll be presented with a list of commits in your text editor of choice:
$ git rebase master --interactive pick 2fde787 ACE-1294: replaced miniamalCommit with string in test pick ed93626 ACE-1294: removed pull request service from test pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch pick e68f710 ACE-1294: added testing data to batch email file # Rebase f32fa9d..0ddde5f onto f32fa9d (4 commands) # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST.
Notice that each commit has the word
pick next to it. That's rebase-speak for, "Keep this commit as-is." If you quit your text editor now, it will perform a normal rebase as described in the last section. However, if you change
edit or one of the other rebase commands, rebase will let you mutate the commit before it is reapplied! There are several available rebase commands:
reword: Edit the commit message.
edit: Edit the files that were committed.
squash: Combine the commit with the previous commit (the one above it in the file), concatenating the commit messages.
fixup: Combine the commit with the commit above it, and uses the previous commit's log message verbatim (this is handy if you created a second commit for a small change that should have been in the original commit, i.e., you forgot to stage a file).
exec: Run an arbitrary shell command (we'll look at a neat use-case for this later, in the next section).
drop: This kills the commit.
You can also reorder commits within the file, which changes the order in which they're reapplied. This is handy if you have interleaved commits that are addressing different topics and you want to use
fixup to combine them into logically atomic commits.
Once you've set up the commands and saved the file, Git will iterate through each commit, pausing at each
edit for you to make your desired changes and automatically applying any
drop commands for you.
When you rebase, you’re essentially rewriting history by applying each of your new commits on top of the specified base.
git pull --rebase can be a little risky because depending on the nature of the changes from the upstream branch, you may encounter test failures or even compilation problems for certain commits in your newly created history. If these changes cause merge conflicts, the rebase process will pause and allow you to resolve them. However, changes that merge cleanly may still break compilation or tests, leaving broken commits littering your history.
However, you can instruct Git to run your project’s test suite for each rewritten commit. Prior to Git v2.9, you could do this with a combination of
git rebase −−interactive and the
exec command. For example, this:
$ git rebase master −−interactive −−exec=”npm test”
...would generate an interactive rebase plan that invokes
npm test after rewriting each commit, ensuring that your tests still pass:
pick 2fde787 ACE-1294: replaced miniamalCommit with string in test exec npm test pick ed93626 ACE-1294: removed pull request service from test exec npm test pick b02eb9a ACE-1294: moved fromHash, toHash and diffType to batch exec npm test pick e68f710 ACE-1294: added testing data to batch email file exec npm test # Rebase f32fa9d..0ddde5f onto f32fa9d (4 command(s))
In the event that a test fails, rebase will pause to let you fix the tests (and apply your changes to that commit):
291 passing 1 failing 1) Host request “after all” hook: Uncaught Error: connect ECONNRESET 127.0.0.1:3001 … npm ERR! Test failed. Execution failed: npm test You can fix the problem, and then run git rebase −−continue
This is handy, but needing to do an interactive rebase is a bit clunky. As of Git v2.9, you can perform a non-interactive rebase exec, with:
$ git rebase master -x “npm test”
npm test with
mvn clean install, or whatever you use to build and test your project.
A Word of Warning
Just like in the movies, rewriting history is risky business. Any commit that is rewritten as part of a rebase will have it's SHA-1 ID changed, which means that Git will treat it as a totally different commit. If rewritten history is mixed with the original history, you'll get duplicate commits, which can cause a lot of confusion for your team.
To avoid this problem, you only need to follow one simple rule: Never rebase a commit that you've already pushed!
Stick to that and you'll be fine.
More to Come!
We've only touched on a couple of user-facing features of
git rebase. A heck of a lot of refactoring work also landed in 2016 that improved rebase performance and the robustness of interactive rebase's parsing and sequencing. This work will continue into 2017 and yield further improvements to one of Git's most unique and powerful commands. If you'd like to learn more about rebasing, check out Atlassian's great tutorial on rewriting Git history. Then keep an eye out for the next article in our retrospective series, which will cover a powerful feature with a slightly checkered past: Git submodules.
As always, if you've got some Git tips to share, come say hi on Twitter — I'm @kannonboy.
If you stumbled on these articles out of order, you can check out the other topics covered in our Git in 2016 retrospective below:
Or, if you've read 'em all and still want more, check out Atlassian's Git tutorials (I'm a regular contributor there) for some tips and tricks to improve your workflow.
Opinions expressed by DZone contributors are their own.