Bob Kerns: Thinking

Thinking about Science, Engineering, and Technology


2024-04-02

<!doctype html>

Rebase Demo

A demonstration of using rebase to base commits on a different branch.

If you see HTML above, click the approriate branch below:

Note: main may be called master in older repositories.

Try it

This repository has this scenario set up for you to try. It is best viewed in Visual Studio code with the Markdown Preview Enhanced plugin enabled.

The line below is what changes between branches:

You are on branch: main

Use git switchbranch to switch to each of these branches, then switch to my-patch to try out the fix.

Scenario

Let's say you are creating a patch, so you start a branch. Let's call it my-patch1.

This is what you intend:

gitGraph%%{init { 'theme': 'default', 'themeVariables': {
    'git0': '#0000ff',
    'git1': '#00ff00'
} } }%%
commit
commit tag: "main" type: HIGHLIGHT
branch my-patch1
commit id: "fix-1"
commit id: "fix-2"

But let's say you forgot you were working on another branch, feature1, and do this instead:

gitGraph
%%{init { 'theme': 'default', 'themeVariables': {
    'git0': '#0000ff',
    'git1': '#ff0000',
    'git2': '#00ff00'
} } }%%
commit
commit tag: "main"
branch feature1
commit type: HIGHLIGHT tag: "feature1"
branch my-patch1
commit id: "fix-1"
commit id: "fix-2"

You want to fix it like this:

gitGraph
%%{init { 'theme': 'default', 'themeVariables': {
    'git0': '#0000ff',
    'git1': '#ff0000',
    'git2': '#00ff00'
} } }%%
commit
commit tag: "main"
branch feature1
commit type: HIGHLIGHT tag: "feature1"
checkout main
branch my-patch1
commit id: "fix-1"
commit id: "fix-2"

That is, you want to leave feature1 alone and not include it in your pull request.

The fix: git rebase --onto

To fix this, we need to tell git three things:

  1. Where we want to put our commits. Here, that's main
  2. The patch before the range we want to move. Here, that's the feature1 branch.
  3. The branch we want to move. Here, that's our my-patch1 branch

To do this, we use git rebase --onto:

git rebase --onto main feature1 my-patch1

The result

gitGraph
%%{init { 'theme': 'default', 'themeVariables': {
    'git0': '#0000ff',
    'git1': '#ff0000',
    'git2': '#808080',
    'git3': '#00ff00'
} } }%%
commit
commit tag: "main"
branch feature1
commit type: HIGHLIGHT tag: "feature1"
branch "-old-my-patch1-"
commit id: "fix-1"
commit id: "fix-2"
checkout main
branch my-patch1
commit id: "fix-1-copy"
commit id: "fix-2-copy"

The result is that your fixes are copied into a new chain of commits starting with the current main commit.

The Pull Request

One mistake that git is not very forgiving of is changing history that you've already shared with others via git push.

The easy case

If you haven't pushed your branch yet, you're all set. You can safely push my-patch and make your pull request.

git push --set-upstream origin my-patch

If not, you have two choices.

The safe option

The problem is that it can cause difficulties and confusion for others who have already cloned your branch.

To avoid this you can delete your pull request, if any, and then rename your branch, e.g. my-patch-1:

git branch -M my-patch my-patch-1
git push --set-upstream origin my-patch-1

Then make a new pull request.

The quick option

If nobody is using what you've pushed, you can do a force push to update your branch. Normally, you can only add commits to branches when you push. But with the --force-with-lease option you can tell the server to simply set my-patch to point to the new commit:

git push --set-upstream --force-with-lease origin my-patch

--force-with-lease is a newer, safer version of --force that avoids certain race conditions. It's good to get in the habit of using it instead of --force.

If you make a mistake

The old commits remain in the repository. They are no longer referenced by your branch. They might be referenced by another-branch like this:

gitGraph
%%{init { 'theme': 'default', 'themeVariables': {
    'git0': '#0000ff',
    'git1': '#ff0000',
    'git2': '#808080',
    'git3': '#ff8000',
    'git4': '#00ff00'
} } }%%
commit
commit tag: "main"
branch feature1
commit type: HIGHLIGHT tag: "feature1"
branch "-old-my-patch1-"
commit id: "fix-1"
commit id: "fix-2"
branch another-branch
commit
checkout main
branch my-patch1
commit id: "fix-1-copy"
commit id: "fix-2-copy"

But that's unlikely in this scenario.

You can also find the commit ID in the reflog and construct such a branch later if you need to recover. Git is very forgiving of most mistakes.

If nothing references your old branch, git will eventually garbage collect those commits when pruning the reflog: (default: >90 days).