Git Version Control
Git Rebase Merge Version Control
November 15, 2022 - Dave S.

A Comprehensive Guide to Rebase vs Merge in Git

Thankfully, revision control systems have become a ubiquitous and expected tool in modern software development. No longer are we arguing about whether or not we should use revision control. Instead, we are arguing about how we should use version control.

This was a very large step for the industry that often goes unnoticed. Not having to deal with multiple copies of a code base or having to manually apply code patches is a huge win for productivity, and removing the drudgery of those error-prone processes is a boon for morale.

Now, thanks to advancements in revision control technology, many of us are on the other side of yet another large step forward, in which version control becomes less of a tool to control the source code, and more of a tool to help craft the source code into its best form.

The first concept that took large steps in this direction was branches, which gave developers a bit of independence from the changes in the mainline code base. Allowing developers to not constantly re-integrate their code with upstream changes can save a lot of repetitive effort that adds no real value. This gave developers the ability to think more easily at a higher level about ongoing changes and when they will make their way to the product.

We primarily work with ruby on rails here at Entrision, but these principles apply to any codebase in any language. All code bases tell a story. Without revision control, that history was much harder to find and often took the form of changelogs and roadmaps and other such files that happened to be left around in the copy you are looking through.

When that changed to a full accounting of every change to the code base, the quality of that story started to diverge wildly. Early on, many repositories became minutia collections; simple lists of each file changed, maybe with a brief note.

This blog post is about how rebase and merge can be used not only to make development easier, but more importantly, to convey more meaning into the future.

The Basics of Git

This blog post will refer generically to the concepts of rebase and merge, as they apply to many revision control systems, but Git will be the tool used for examples.

We’ll assume you know the basics of Git, but we’ll lay down enough to set a solid starting point. In git, a branch is made from the current point in the repository with the checkout command. First, we’ll create a repo with a single file, then create a branch:

$ git init .
$ echo "1" > file # Gotta start somewhere
$ git add file
$ git commit -m "The best file!"
$ git checkout -b add-2 # The add-2 branch will be even better!

Assuming that development continues on both master and add-2,

$ echo "2" >> file # 2 is better than 1
$ git add file
$ git commit -m "Even better file!"
$
$ git checkout master
$ echo -e "0\n1" > file # Forgot to start at 0
$ git add file
$ git commit -m "Start at zero"

there will come a point in time when we need to get the changes from add-2 onto the master branch.

$ git log —all
* cba8efd (3 seconds) <Dave Strock> (HEAD -> master) Start at zero
| * 575b69c (70 seconds) <Dave Strock> (add-2) Even better file!
|/
* 59e4575 (2 minutes) <Dave Strock> The best file!
* 763d779 (4 minutes) <Dave Strock> Initial commit

The simplest way to do this is with the merge command, which will merge the branch given as an argument into whatever branch is currently checked out:

$ git checkout master
$ git merge add-2
Auto-merging file
Merge made by the 'recursive' strategy.
file | 1 +
1 file changed, 1 insertion(+)

This results in a single file containing three lines: 0, 1, and 2. The repository history then looks like this:

$ git log --all
* 02ac03a (18 minutes) <Dave Strock> (HEAD -> master) Merge branch 'add-2'
|\
| * 575b69c (21 minutes) <Dave Strock> (add-2) Even better file!
* | cba8efd (20 minutes) <Dave Strock> Start at zero
|/
* 59e4575 (22 minutes) <Dave Strock> The best file!
* 763d779 (24 minutes) <Dave Strock> Initial commit

Notice the new commit “Merge branch ‘add-2’” listed as the head commit. We didn’t tell git to make a commit, nor did we give it a message; instead git created this commit to merge the content of the two branches (master and add-2), and it had to make a new commit to do so. The other option for merging two branches is to use rebase. Instead of merging the contents of both branches by creating a new commit, what if we could rearrange the commits to show the changes that were made, even if they were made on the branch? For that, we’ll need time travel, which is exactly what git rebase can do. In this example, with respect to time, the file was modified to add a line with 2 (on add-2) temporally before the line with 0 was added (on master). You can see this is the log output, which is out of order: “Even better file!” was created 21 minutes ago, while “Start at 0” was created 20 minutes, even though it is listed right after (that is, above) “The best file!” which was 22 minutes ago. Ideally, we’d have added 0 to start with, before we even created the add-2 branch. Then we wouldn’t even need a merge, since only one of the two branches changed. What if we could change the history of the repository to make things cleaner? First, let’s make two new changes similar to the first set, by creating a new branch and adding a new value to the file. Then we make a different change to the file on master:

$ git checkout -b add-4
$ echo "4" >> file
$ git add file && git commit -m "Four is more"
$ git checkout master
$ echo "3" >> file
$ git add file && git commit -m "Three is what we need"
$ git log --all
* 3bd999e (5 minutes) <Dave Strock> (HEAD -> master) Three is what we need
| * 8c4492e (10 minutes) <Dave Strock> (add-4) Four is more
|/
* 02ac03a (20 minutes) <Dave Strock> (HEAD -> master) Merge branch 'add-2'
|\
| * 575b69c (23 minutes) <Dave Strock> Even better file!
* | cba8efd (22 minutes) <Dave Strock> Start at zero
|/
* 59e4575 (24 minutes) <Dave Strock> The best file!
* 763d779 (26 minutes) <Dave Strock> Initial commit

Now, instead of using git merge to create a merge commit, we want to alter the repository’s timeline to make it look like the add-4 branch was created after the “Three is what we need” change was made. Then once we go to merge add-4, there won’t be any changes on the master that are not also on add-4, so there is no chance for a conflict. However, the important part is that we’re not just changing the repository log; we’re actually going back in time and altering the timeline so that when we get back to the present time, things will have worked out differently and we’ll see a different result.

First, we’ll check out the branch that we want to change the timeline of. Then we’ll tell git which timeline we want to move add-4 relative to, in this case, master. We’re going to move the commit made on add-4 into the future slightly so that it was created after “Three is what we need” instead of before.

$ git checkout add-4
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Four is more
Using index info to reconstruct a base tree...
M file
Falling back to patching base and 3-way merge...
Auto-merging file
CONFLICT (content): Merge conflict in file
error: Failed to merge in the changes.
Patch failed at 0001 Four is more

Well, that doesn’t feel good, does it? We just wanted to muck about with time; we weren’t expecting nasty repercussions like merge conflicts. However, it’s impossible to ignore conflicting changes (unless you want to delete one of them) so we’ll have to deal with them sooner or later. Rebase forces, or allows you to deal with merge conflicts up front, before the code gets written on to master. So let’s edit our file to resolve the conflict and see what the repository looks like:

$ emacs file
$ git add file
$ git rebase --continue
Applying: Four is more
$ git log --all
* ecd8aa2 (6 minutes) <Dave Strock> (HEAD -> add-4) Four is more
* 3bd999e (6 minutes) <Dave Strock> (master) Three is what we need
* 02ac03a (21 minutes) <Dave Strock> Merge branch 'add-2'
|\
| * 575b69c (24 minutes) <Dave Strock> Even better file!
* | cba8efd (23 minutes) <Dave Strock> Start at zero
|/
* 59e4575 (25 minutes) <Dave Strock> The best file!
* 763d779 (27 minutes) <Dave Strock> Initial commit

The astute reader will notice that the “Four is more” change is not yet part of the master branch, but we’ll get to that. The most important changes to notice are the timestamps and commit hashes of the two commits we’re working with, both of which have changed for the “Four is more” commit. This is because we successfully changed the timeline! In our new reality, the add-4 branch was created after our file was modified by the “Three is what we need” commit. Since there are no changes on the master that are not on add-4, we know that merging add-4 to the master cannot have any conflicts. So let’s merge it.

$ git checkout master
$ git merge add-4
Updating 3bd999e..ecd8aa2
Fast-forward
file | 1 +
1 file changed, 1 insertion(+)
$ git log --all
* ecd8aa2 (6 minutes) <Dave Strock> (HEAD -> master, add-4) Four is more
* 3bd999e (6 minutes) <Dave Strock> Three is what we need
* 02ac03a (21 minutes) <Dave Strock> Merge branch 'add-2'
|\
| * 575b69c (24 minutes) <Dave Strock> Even better file!
* | cba8efd (23 minutes) <Dave Strock> Start at zero
|/
* 59e4575 (25 minutes) <Dave Strock> The best file!
* 763d779 (27 minutes) <Dave Strock> Initial commit

Notice that the only thing that changed is that master now points to our “Four is more” commit, instead of “Three is what we need”. No conflicts, no worries, and no additional merge commit is required. Just the commits we actually created. This is what git means when it outputs “Fast-forward”; it was able to move the master branch pointer and didn’t have to make any other changes to the repository.

At first, you may be thinking “Sure time travel is fun, but we just did more work for nearly the same result”, and you’d be right. However, this technique becomes more useful, maybe even essential, as projects grow larger and more complex and as more people are involved in changing them.

Rebase vs Merge in Git

One of the main advantages of rebase is that it allows us to travel back in time and alter the timeline of our source code’s development. We can rearrange, and even combine and re-write commits at will. What we didn’t discuss was why you’d want to do such a thing.

Keeping track of source code changes can sometimes be looked at as more of a security and integrity concern. We want to make sure we have a copy of our app’s source code in case something happens, and we want to have old copies so we can revert to them in case something worse happens. This way of looking at it ignores the story that is being told of the development of your app.

Even if you don’t purposely try to tell a story, your code will still tell one. So if a story will be told either way, why not tell the clearest story possible? Why not make the storytelling adapt to the needs of the project?

Using the Merge Strategy

The major advantage of the merge strategy is that it is the simpler of the two. In fact, the rebase strategy usually still includes merging. For simple projects, that don’t change frequently and have very few developers, this can be a fine strategy, but as we’ll see, doing so leaves a lot of power on the table.

The merge strategy also has the disadvantage of entwining [Rich Hickey would say “complecting”], the merge process with the conflict resolution process. Many a build has failed due to a bad auto-merge. It can be hard to predict all merge conflicts before the merge, so you have to change the code after approving the main changes. If you’re a shop that does code reviews, do you review your post-merged code?

Using the Rebase Strategy

As we showed in part 1, one of the biggest advantages of the rebase strategy is how it separates conflict resolution from branch merging. Rebase lets you perform the sometimes complex conflict resolution process at your leisure, letting you thoroughly test and even make changes on your schedule, removing any pressure to hurry due to fear of new changes on the master and introducing additional conflicts to deal with.

During times of extremely high churn, it can even reach the point of being too difficult to merge a complex change, as the target keeps moving too fast, that teams will resort to so-called “code freezes” or other complex scheduling solutions to give people room to breathe and get merges done.

Rebase removes almost all of this, by letting developers resolve conflicts, along with the subsequent testing and changes, all on their branch. This independence was the main reason branch-based development became the norm in the first place, so it makes sense to retain as much of it as possible where the rubber meets the road in making changes to the mainline codebase.

Another minor advantage of rebase is that it leaves the repository in what some would describe as a “cleaner” state, in that it doesn’t require the creation of an additional commit that marks the point at which the branch was merged. Some teams find these merge commits to be useful in visual grouping related commits, and the difference in having merge commits versus not having them is usually negligible, but we mention it for completeness since it’s often one of the first features of rebase that proponents will mention.

Interactive Revision History

While a clean revision history is nice, even nicer is the ability to go back in time and clean up a revision history. Like all time travel abilities, this can be a very sharp tool that should be used with care, but it can deliver very good results.

Since rebase can be used to go back in time and reorder things, like we altered the branch pointers in part 1, it can also be used to go back in time and modify commits. If you’ll remember, we had a file containing lines of a single counter increasing sequentially from 0 to 4. Let’s say we create a new branch and add a new value to our file:

$ cat file
0
1
2
3
4
$ git checkout -b add-5
$ echo "50" >> file
$ git add file
$ git commit -m "Added five"

Upon reviewing our changes, we notice that we made a typo when adding the value, adding 50 instead of 5. So we go fix it:

$ emacs file
$ git add file
$ git commit -m "Fixed five"

So we fixed the issue, but now our history looks like this:

$ git log —all
* aa8605c (2 seconds) <Dave Strock> (HEAD -> add-5) Fixed five
* 07f56db (13 minutes) <Dave Strock> Added five
* ecd8aa2 (6 minutes) <Dave Strock> (HEAD -> master, add-4) Four is more
* 3bd999e (6 minutes) <Dave Strock> Three is what we need
* 02ac03a (21 minutes) <Dave Strock> Merge branch 'add-2'
|\
| * 575b69c (24 minutes) <Dave Strock> (add-2) Even better file!
* | cba8efd (23 minutes) <Dave Strock> Start at zero
|/
* 59e4575 (25 minutes) <Dave Strock> The best file!
* 763d779 (27 minutes) <Dave Strock> Initial commit

I don’t know about you, but I find “fixed it” commits to be a bit annoying. I’d rather do it right the first time, and I don’t see a lot of historical value in looking back at how many times I had to fix a typo after I’d already created a commit. So surely the lesson to learn is “Be more careful about what you commit”, right? Why? If you really investigate this question, you’ll start to hit some deep truths about how you view revision control. We’re taught to think of it as the “permanent record”, unchanging except in the rare cases of Herculean efforts to correct titanic problems, but that need not always be the case. There is no real reason that branches have to be treated the same as the master, but many of us learned branching long before we had the time-traveling powers of tools like git so it may not be an obvious thing to question. Once you realize that the requirement for code on a branch is simply that it be ready for the “permanent record” by the time it is merged to master, you realize you can do whatever you want before that. We can always get up to the point in time right before the merge, and then time travel back to change things on the branch, and we can do this as many times as we need to tell the story we want to tell. This is another way that the rebase strategy gives developers more breathing room. Let’s go ahead and fix our minor historical blemish by squashing our two commits together into a single commit that looks like we just used the correct value the first time. To do that we’ll use the –interactive (or -i) flag to tell git that we want to go back in the timeline specifically to interact with the commits and files found there:

$ echo $EDITOR
emacs -nw
$ git rebase -i ecd8aa2
pick 07f56db Add five
pick aa8605c Fixed five

# Rebase ecd8aa2..aa8605c onto ecd8aa2 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

The first thing you may wonder is why we’re printing the $EDITOR shell variable, but we’ll get to that after we talk about invoking the interactive rebase. The argument given to interactive rebase effectively tells git how far you want to go back in time; you give it the hash of the commit farthest back in time that you do not want to change. This is usually one commit further back in time than the first commit you want to change. In this case, we want to change the two commits on our add-5 branch, so the commit before that is ecd8aa2. Git will allow you to use labels like the master, but we think it’s a better practice to get used to using commit hashes to be more precise and avoid making mistakes due to assumption. Remember: sharp tool. So once you tell git how far back to go, you’ll be presented with your editor of choice, configured with the $EDITOR shell variable, already filled in with a bunch of text that looks vaguely like the commit history you wanted to change and instructions on how to modify it. For this example, we’ll be using the squash operation, which will take the commit that we mark as ’s’ and squash it into the commit before it in time, that is into the commit above it. So let’s squash aa8605c into 07f56db by changing ‘pick’ next to aa8605c to ’s’:

pick 07f56db Add five
s aa8605c Fixed five


# Rebase ecd8aa2..aa8605c onto ecd8aa2 (2 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

One important thing to note is that any lines starting with ‘#’ will be ignored when the file is saved, and the rebase process will only act on commits that are listed in the file when saving. This means that git’s interactive rebase has “get out of jail free” card if you mess anything up: Clear out the file (or comment all lines), save, and git will reply that there is “nothing to be done”.

In this example, when we save the file and exit the editor, we will immediately be presented with another editor displaying the commit messages for both commits we’re attempting to squash together:

# This is a combination of 2 commits.
# This is the 1st commit message:


Add five


# This is the commit message #2:


Fixed five


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Tue Nov 5 16:58:08 2019 -0600
#
# interactive rebase in progress; onto ecd8aa2
# Last commands done (2 commands done):
# pick 07f56db Add five
# squash aa8605c Fixed five
# No commands remaining.
# You are currently rebasing branch 'add-5' on 'ecd8aa2'.
#
# Changes to be committed:
# modified: file
#

Just like during the interactive rebase, any lines starting with ‘#’ will be ignored upon saving. This is your chance to craft a single commit message that tells the story of the two commits you melded into one. Something like “Added five correctly” should suffice. When you save and exit the editor, git will construct and save your new, single commit. Note the different commit hash and message.

[detached HEAD 563b63c] Add five correctly the first time
Date: Tue Nov 5 16:58:08 2019 -0600
1 file changed, 2 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/add-5.
$ git log --all
* 563b63c (49 minutes) <Dave Strock> (HEAD -> add-5) Add five correctly
* ecd8aa2 (23 hours) <Dave Strock> (master) Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit

While we were fixing our typo, another team member merged their commit to master causing a conflict, but we already know how to easily handle that with rebase:

$ git log --all
* bd0f5cd (51 seconds) <Dave Strock> (master, add-4.5) Added 4.5
| * 563b63c (56 minutes) <Dave Strock> (HEAD -> add-5) Add five correctly
|/
* ecd8aa2 (23 hours) <Dave Strock> (master) Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit
$ git checkout add-5
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Add five correctly the first time
Using index info to reconstruct a base tree...
M file
Falling back to patching base and 3-way merge...
Auto-merging file
CONFLICT (content): Merge conflict in file
$ emacs file
$ git add file
$ git rebase --continue
Applying: Add five correctly
$ git log —-all
* 37432bc (2 minutes) <Dave Strock> (HEAD -> add-5) Add five correctly
* bd0f5cd (57 minutes) <Dave Strock> (master) Added 4.5
* ecd8aa2 (23 hours) <Dave Strock> Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit

Best of Both Worlds: Rebase and Merge

There is no single “best” strategy. Since each method has advantages, the best solution likely is to use both methods. Many teams find it convenient to use web-based code collaboration tools like Gitlab and Github, largely for the simplicity they provide to the branching process. They allow teams to push up a branch of changes and create a special area, usually called a “Pull Request” or “Merge Request”, for performing code reviews and discussions, and then a nice “Merge” button to get that branch’s changes down to the master branch.

Making changes out in the open like this is one of the best ways to catch bugs and bring people up to speed quickly, but if we stick to solely the merge strategy we will end up with all of its negatives, namely the need to deal with conflicts in a way that isn’t as easily testable and the cluttering of the development history with minor “fixed it” style messages that add no value to readers in the future.

This is where a mixed approach becomes particularly useful. We can continue getting the benefits of these web tools, while also crafting cleaner, more easily tested code changes by employing the rebase strategy. One particularly effective strategy is to create new branches and Merge Requests for features immediately before any code is even written. This way, as the code changes, the commits can be pushed up for immediate review.

This can quickly catch mistakes in understanding or facilitate discussion of better approaches to achieving the desired results, but it doesn’t work if developers are scared of making sure their commits are perfect. At this early stage of development, we know as little about the feature as we’re likely ever to know, so it’s a bad idea to have anything resembling the idea of permanence floating around. Instead, we want developers to feel free enough to push up incomplete or messy changes to get feedback early and often.

This is where the ability to rebase changes on a branch to clean up history can become so powerful. You get the same result of a clean history as if you took extra time to be meticulous at every step, without the required to maintain a meticulous workflow at all times. Combine that with the ability to handle conflicts without slowing down changes to master, and you have a well-defined software development process that is tailored to the specific goals and needs of the separate parts of that process.

Lifecycle Differences

It can even sometimes make sense to alter the usage of these strategies temporally, based on what is exactly needed at this point in the code base’s lifecycle. Early in a feature’s development, it can be very useful to meticulously lay every card out on the table, making commits for each new thing that is learned or functionality that is added. Like noted above, this can be a large boon to team communication, but it can also help developers understand exactly where they are in their understanding of the feature.

Are you effortlessly making perfect commits? You probably understand the problem well.

Are you making lots of commits with comments like “WIP”, “fixed it”, and “really fixed it this time”? Maybe your understanding is still increasing rapidly and you haven’t plateaued to the point where perfect commits are yet possible.

These are both great bits of feedback that are very difficult to get back from others, and nearly impossible to get when you save all your committing for the very end, but it’s very easy to get a large-grained feel for the current state by just looking at your commit history.

What Not To Do

So far we’ve discussed the different strategies and ways you can use them to your benefit, and we’ve even discussed a few drawbacks on each, but we haven’t yet discussed what you should avoid in these strategies.

The biggest rule of thumb is “Don’t change shared history”.

Remember from Part 2 that the time traveling abilities of rebase are a very sharp tool that can easily cut you or your team if you’re not aware of its effects. Say we notice a bug in the latest commit on master and, since we don’t like those ugly “fixed it” commits, we decide to use our rebase power to fix it. It doesn’t seem like any harm was done.

$ git log --all
* 7c8bea0 (35 minutes) <Dave Strock> (HEAD -> master) Add 6
* 37432bc (2 minutes) <Dave Strock> Add five correctly
* bd0f5cd (57 minutes) <Dave Strock> Added 4.5
* ecd8aa2 (23 hours) <Dave Strock> Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit
$ cat file
0
1
2
3
4
4.5
5
66
$ emacs file
$ git add file
$ git commit --amend -C HEAD
$ git log --all
* 707ae1b (39 minutes) <Dave Strock> (HEAD -> master) Add 6
* 37432bc (2 minutes) <Dave Strock> Add five correctly
* bd0f5cd (57 minutes) <Dave Strock> Added 4.5
* ecd8aa2 (23 hours) <Dave Strock> Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit

Woah, wait, what is this amend C HEAD stuff? The amend flag to commit is just a shorthand that means “Create a new commit that combines the changes I’m committing now with the changes in the previous commit.” It is identical in effect to the example in Part 2 where we created a commit to fix the bug and then used interactive rebase to squash it into the previous commit. The -C HEAD part just means “use the commit message from HEAD”, which is the commit we want to fix. So now we have a fixed bug, however, while we were fixing that bug on the master, our teammate created a branch off of the original commit on the master, the one you found a bug in. They went on to implement a big feature with many commits.

> git log --all
* c516341 (8 minutes) <Dave Strock> (HEAD -> add-7) Simplify to 7
* 0f5d81c (9 minutes) <Dave Strock> Make it Seven
* ed0c134 (10 minutes) <Dave Strock> Add seven
* 7c8bea0 (40 minutes) <Dave Strock> (origin/master, origin/HEAD, master) Add 6
* 37432bc (2 minutes) <Dave Strock> Add five correctly
* bd0f5cd (57 minutes) <Dave Strock> Added 4.5
* ecd8aa2 (23 hours) <Dave Strock> Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit
> git push origin add-7

Notice how our teammate’s “Add 6” commit has a hash of 7c8bea0 rather than the 707ae1b hash of our fixed commit. Once they are ready to merge their changes in, they realize there is a problem: Two “Add 6” commits!

> git log --all
* c516341 (8 minutes) <Dave Strock> (HEAD -> add-7) Simplify to 7
* 0f5d81c (9 minutes) <Dave Strock> Make it Seven
* ed0c134 (10 minutes) <Dave Strock> Add seven
* 7c8bea0 (40 minutes) <Dave Strock> (origin/master, origin/HEAD, master) Add 6
* 37432bc (2 minutes) <Dave Strock> Add five correctly
* bd0f5cd (57 minutes) <Dave Strock> Added 4.5
* ecd8aa2 (23 hours) <Dave Strock> Four is more
* 3bd999e (23 hours) <Dave Strock> 3 is what we need
* b5a1877 (23 hours) <Dave Strock> Merge branch 'add-2'
|\
| * b18a734 (24 hours) <Dave Strock> Even better file
* | f0ee5c2 (23 hours) <Dave Strock> Start at 0
|/
* 8785557 (25 hours) <Dave Strock> The best file
* 95878fc (27 hours) <Dave Strock> Initial Commit
> git push origin add-7

This is because we removed that old commit and replaced it with a new one, but only in our version of the repository. Our teammate’s timeline didn’t change like that, so their changes depend on the original commit. You changed shared history. Then when our teammate pushed their changes, git saw 37432bc as the common parent, which we can see as the log output looking as if the add-7 branch starts one commit earlier than it did. Git saw that 7c8bea0 didn’t exist on origin/master, so when our teammate pushed the branch, git pushed 7c8bea0 as well.

This can be fixed with a little effort, see below, but it’s much better if your processes can help you avoid the need for such a thing. Any time you think about doing a rebase that would rewrite commits, ask yourself whether someone else can have started work based on any of those commits.

Many teams will disallow rebasing on the master to avoid this problem, but it can be a problem anywhere. If you are working on a branch with another developer, when you are ready to rebase for whatever reason, it’s a good idea to have a quick chat with the other developer to let them know your plan. This lets them avoid starting work off of those commits that are going to change, and lets them notify you in the case that they already did.

Sometimes it can even make sense to treat a branch as being changed by multiple developers similar to the master and have everyone create branches off of the shared branch. Then the shared branch is changed only via merges.

Conclusion

As we’ve shown, git rebase is an incredibly powerful tool that can greatly increase the capabilities available to software developers. With that power comes the opportunity for both benefit and detriment, but hopefully, we’ve shown enough of both the advantages and how to resolve the hopefully rare problems that can arise when using such power, that you will be able to use these tools without much downside.

YOU MAY ALSO LIKE