Marcin Wosinek
Posted on December 7, 2022
Beginners often experience several points of confusion about rebases when using Git:
- what they are
- why people do them
- why pushes might fail after successful rebase
In this article, I’ll provide answers to those questions. In the meantime, TL;DR: rebases are a way of making your crude commit history into something you’ll want to share with the rest of your team.
How to do a rebase
To perform a rebase, you run git rebase <commit-reference>
. Commit reference can be anything—for example:
- branch name,
- tag, or
- commit id.
A rebase is a fairly complicated operation, so let’s walk through various aspects of it.
Simple rebase
In its simplest form, rebasing takes changes from one place (one base), and moves them to another. It changes the spot where the history branched off. So with the tree alias I wrote about previously, we start with a branch graph like this:
$ git tree
* 293b722 (HEAD -> test) add test.txt file
| * abc01e7 (origin/main, origin/HEAD, main) Add lorem ipsum to readme
|/
* edd3504 Add readme
and we move our branch to start from a different place—the top of the main branch:
$ git rebase main
Successfully rebased and updated refs/heads/test.
and as a result, we get a tree like this:
$ git tree
* fe4254e (HEAD -> test) add test.txt file
* abc01e7 (origin/main, origin/HEAD, main) Add lorem ipsum to readme
* edd3504 Add readme
Our test
branch used to start at edd3504 Add readme
, and now it’s starting at abc01e7 Add lorem ipsum to readme
.
Rebase in progress
Even a simple rebase may require additional input from your side. For example, you might have some conflict to resolve:
$ git rebase main
Auto-merging test.txt
CONFLICT (add/add): Merge conflict in test.txt
error: could not apply 293b722... add test.txt file
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 293b722... add test.txt file
When checking the status in such a situation, you will get an instruction from Git on how to proceed:
$ git status
interactive rebase in progress; onto a03989b
Last command done (1 command done):
pick 293b722 add test.txt file
No commands remaining.
You are currently rebasing branch 'test' on 'a03989b'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both added: test.txt
no changes added to commit (use "git add" and/or "git commit -a")
You need to edit the file to resolve the conflict. When you are done with it, you add the change to staging:
$ git add test.txt
(no output)
and you continue with the rebase:
$ git rebase --continue
[detached HEAD b81cad4] add test.txt file
1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/test.
Subsequently, Git will continue with the rebase: it can finish smoothly as in my case, or you may encounter more conflicts to resolve manually.
Rebase interactive
A more advanced option for rebase is rebase interactive. It allows you to make very precise changes on the branch—not just move it from one place to another. Let’s take a look at a few examples below.
Reword commits
The simplest thing you can do is just change the commit message. The good thing is that there is no chance of conflict because the files are left the same.
Squashing commits
You can choose a few commits and turn them into one. It preserves the file's changes—it just collapses a chain of changes into one commit. It requires writing a new commit message for the new commit, but on its own it will not cause conflicts.
Editing commits
You can edit the file changes in the commit too. This allows you to make sure the commits contain all the relevant changes. If there are changes to the same places later in the branch, then you will need to resolve conflicts manually.
Removing commits
Another operation is removing commits from the branch—the commit and its changes. If there were additional modifications to the same code later in the branch, removing the commit will cause some conflicts. The most basic use case is when you realize that some changes are not needed, and you want to remove them from the history. Besides that, I often use it when I realize my branch is getting too big, and I intend to make it more focused and merge it sooner.
Reordering commits
You can reorder commits as well. There are situations in which it can make sense, but it gets complicated quickly if you try to reorder a commit that changes the same area of code.
File interface
As you can see, interactive rebasing requires a lot of subtle input. Git gets this input in the form of a text file. When I run git rebase main -i
, I get my editor with the following content:
pick a03989b add test.txt
# Rebase abc01e7..a03989b onto abc01e7 (1 command)
#
# 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 [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# 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.
#
When you save the file and exit the editor, Git will follow the instructions you provided there. It’s an interface that can feel awkward at first, but one can get used to it pretty quickly. The only “gotcha” is that you need to know your default editor, and know how to use it. I’ve written a bit more about it in a section of another article.
Practical tip
While doing interactive rebasing, try to do one thing at the time. Git can manage to do the following in one rebase:
- change the branch starting point
- reorder some commits
- squash others
- etc.
However, you’ll probably get lost in all the conflicts that will happen all at once. It’s easier to do one thing at the time and keep the following order:
- remove superfluous commits first,
- squash related commits, and then
- move the branch to another base.
In this way, you reduce the number of commits that have to be moved around—thereby reducing the number of conflicts you must resolve.
History changes
Rebasing changes the history of the repository. This means that the same changes will appear as different commits on different branches. It’s not a mistake, but it can be confusing when you see the following logs for the first time:
$ git push origin test
To github.com:how-to-js/git.md.git
! [rejected] test -> test (non-fast-forward)
error: failed to push some refs to 'github.com:how-to-js/git.md.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details
non-fast-forward
means there is no direct route way for one point in the repository history toward another. In this case, you need to integrate remote and local changes yourself. There are some subtleties to getting branches in sync, and it’s a topic for another article.
Depending on the workflow in your project, it can be forbidden or required to override the history of some branches. The workflow I use prohibits history changes to the master/main branch, and it requires rebases of all the other branches. There are no right or wrong approaches to rebasing: each policy has its pros and cons, but it’s likely that your team will insist that you maintain consistency with the selected approach.
Possible side effects
Besides the intrinsic complexity of rebases, they introduce some challenges in team environments:
local-only branches
When your work is on your computer only, it’s always safe to rebase or change the history in any other way. It’s a good practice because it always helps you to clean up your work before others can see it. If you started some work a month ago and since then, you’ve made changes to main regularly—it’s not worth preserving information with commits such as Merge remote-tracking branch 'origin/main' into test
. Instead, you can rebase regularly and keep history as a straight line.
Branches that were uploaded to remote
Once you share your branch, any change to its history can break stuff. For example, your continuous integration (CI) will remember the results of a test run of a commit, but the commit will be gone. I’ve met people who are very cautious about changing a history that was already shared, so I guess there are teams where this might be discouraged.
Remote branches that other works on
The most complicated case is to change the history of a branch that other people are working on. For that, I would recommend:
- making sure everybody is up-to-date when the change happens—that there are no two people introducing changes to the branch at the same time;
- getting everyone updated to the new, rebased branch before they start the new work in it.
This is a delicate situation because it breaks a lot of the automated conflict resolution Git employs. It’s good to be careful in such cases and make sure everybody knows what’s happening.
Keep on learning
If you are interested in learning more about branches in Git, a great (and pretty) resource is available at Learn Git Branching. If you are interested in learning more about Git, sign up here to get updates about my Git-focused content.
Posted on December 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024