Git rebase

The commands git rebase and git merge allow you to merge Git branches. While git merge is always a moving forward change approach, git rebase has powerful history rewrite functions. Here we take a look at its configuration, use cases and pitfalls.

In doing so, git rebase moves a sequence of commits to a new base commit and can be useful for Feature branch workflows workflows. Internally, Git achieves this by creating new commits and applying them to the specified base; so the same-looking commits from branches are entirely new commits.

The main reason for git rebase is to maintain a linear project progression. If the main branch has evolved since you started working on a feature branch, you might want to keep the latest updates to the main branch in your feature branch, but keep the history of your branch clean. This would have the advantage that you could later do a clean git merge of your functional branch into the main branch. This clean history also makes it easier for you to find a regression with Find regressions with git bisect. A more realistic scenario would be the following:

  1. An error is found in the main branch in a function that once worked without errors.

  2. With the clean history of the main branch, Review should allow for quick conclusions.

  3. If Review does not lead to the desired result, git bisect will probably help. In this case, the clean Git history helps git bisect in the search for the regression.

Warning

The published history should only be changed in very rare exceptional cases, as the old commits would be replaced by new ones and it would look as if this part of the project history had suddenly disappeared.

Note

git rebase is also covered briefly in Jupyter Notebooks with Git and Feature branch workflows.

Rebasing dependent branches with –update-refs

When you are working on a large feature, it is often helpful to spread the work over several branches that build on each other.

However, these branches can be cumbersome to manage if you need to overwrite the history in an earlier branch. Since each branch depends on the previous branches, rewriting commits in one branch will result in subsequent branches no longer being connected to the history.

Git 2.38 ships with a new --update-refs option for git rebase that will perform such updates for you without you having to manually update each branch and without subsequent branches losing their history.

If you want to use this option on every rebase, you can run git config --global rebase.updateRefs true to make Git behave as if the --update-refs option is always specified.

Delete commits with git rebase

This can also be easily realised with git rebase, whereby you do not have to delete the line in your editor but replace the line pick with r (reword).

$ git rebase -i SHA origin/main
-i

Interactive mode, in which your standard editor is opened and a list of all commits after the commit with the hash value SHA to be removed is displayed, for example

pick d82199e Update readme
pick 410266e Change import for the interface

If you now remove a line, this commit will be deleted after saving and closing the editor. Then the remote repository can be updated with:

$ git push origin HEAD:main -f

Modify a commit message with rebase

This can also be easily with rebase by not deleting the line in your editor but replace pick with r (reword).

rebase as standard git pull strategy

Normally, git pull fetches and merges new remote commits without any problems. Usually only new commits from the remote branch are added, a so-called fast-forward merge. However, if both the local and remote branches have new commits, the branches will diverge. You must then somehow harmonise the different histories. By default, as of Git 2.33.1, any discrepancy will cause git pull to stop and display the following message:

$ git pull
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

The notes allow three options:

git config pull.rebase false

merges the local and remote commits. Before Git 2.33.1, Git always used this merge.

git config pull.rebase true

The local commits are transferred to the remote commits.

git config pull.ff only

always leads to an error with divergent branches. You can then decide on a case-by-case basis with --no-rebase (which means merge) or --rebase whether you want to merge or rebase.

Tip

I recommend git config pull.rebase true, as merging can be confusing. Rebasing the local commits to the remote ones makes the story linear, which is more understandable.

Make rebase part of your standard strategy:

$ git config --global pull.rebase interactive

If git pull then encounters divergent local and remote branches, it will perform a rebase:

$ git pull
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not apply e50dfe5...
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 e50dfe5...