Why this matters
You make a branch to add a menu page. While you work, a teammate changes the opening hours on main. Your branch still starts from the old version of main, so it does not have their change. When you open a pull request, the branch may show conflicts, or your tests may run against old code.
git rebase fixes this by moving your branch's starting point to the latest main. Your work is replayed on top of it, one commit at a time. The result looks as if you started your branch today.
Rebase also has a risk that causes real damage on teams: it rewrites history. This lesson shows what it changes, how to finish one when a conflict stops it, and when not to use it. It was checked with Git 2.53.0. Branches are covered in "Branches and merging", and stashing in the lesson before this one. Both are used here only where rebase needs them.
The idea: moving the starting point
Some words first:
- A commit is a saved snapshot of the project, with a message. Each commit stores the change it made compared with the commit before it.
- A commit's parent is the commit before it. The first commit in a project has no parent.
- A branch is a name that points at one commit, the newest one on that line of work.
- A conflict happens when two versions change the same line, and Git cannot choose one by itself.
Think of a chapter you wrote for a book. You wrote it from the first edition. The publisher has since released a second edition with new text. Rebasing your chapter means taking each of your paragraphs, placing them on top of the second edition, and checking that each one still makes sense. Your paragraphs stay the same. Their starting point moves.
In Git terms, each of your commits is replayed onto a new parent. The change in each commit stays the same, but the parent changes. A new parent means a new commit, with a new hash (the long ID Git gives each commit). This is why a rebase changes commit IDs even when your text stays the same.
Set up two branches that have diverged
Practise in a new folder. The steps below make a project where main and add-menu have each moved on since they split:
mkdir rebase-practice
cd rebase-practice
git init -b main
git config user.name "Your Name"
git config user.email "you@example.com"
printf '<h1>Corner Cafe</h1>\n<p>Open every day.</p>\n' > index.html
git add index.html
git commit -m "Add home page"
git switch -c add-menu
printf '<h1>Menu</h1>\n<p>Coffee, tea, toast.</p>\n' > menu.html
git add menu.html
git commit -m "Add menu page"
printf 'Latte 4.50\nTea 2.00\n' > prices.txt
git add prices.txt
git commit -m "Add prices"
printf '<h1>Menu</h1>\n<p>Coffee, tea and toast.</p>\n' > menu.html
git commit -am "Fix menu wording"
git switch main
printf '<h1>Corner Cafe</h1>\n<p>Open 7am to 9pm.</p>\n' > index.html
git commit -am "Change opening hours"
printf '<h1>Contact</h1>\n<p>Call 555-0100.</p>\n' > contact.html
git add contact.html
git commit -m "Add contact page"
git log --oneline --graph --all --decorateThe last command draws the history. The output below is from a run of these steps. Your hashes will be different, but the shape will match:
* 1596b1e (add-menu) Fix menu wording
* 57d60a2 Add prices
* f3d45b6 Add menu page
| * 19245a4 (HEAD -> main) Add contact page
| * e3c5387 Change opening hours
|/
* 24dce0c Add home pageRead the picture from the bottom. Both lines start at Add home page. The left side is main, which has two commits after the split. The right side is add-menu, which has three. The |/ mark shows where the two lines join. This is a diverged history: each branch has commits the other does not have.
Worked example 1: rebase a clean branch
Switch to your branch and rebase it onto main:
git switch add-menu
git rebase mainRebasing (1/3)Rebasing (2/3)Rebasing (3/3)Successfully rebased and updated refs/heads/add-menu.The three numbers are your three commits, replayed one at a time. Git did not stop, so there was nothing to decide. Check the history:
git log --oneline --graph --all --decorate* 3c304db (HEAD -> add-menu) Fix menu wording
* 5c15124 Add prices
* 6362308 Add menu page
* 19245a4 (main) Add contact page
* e3c5387 Change opening hours
* 24dce0c Add home pageCompare the two pictures. Before the rebase, your three commits sat beside main. After it, they sit on top of main, in one straight line. The commit names are the same, but the hashes changed, because each commit now has a new parent.
The files are also different. Your add-menu branch now has contact.html and the new opening hours in index.html, because it starts from the latest main. Run git status --short and it prints nothing, which means you have no uncommitted changes.
Why a straight line is useful
Once your branch sits on top of main, merging it back is simple. main only has to move forward to your newest commit. Git calls this a fast-forward merge, and it does not create an extra merge commit.
Worked example 2: a conflict during a rebase
A conflict can stop a rebase. Make a second branch that changes the home page in a way main also changed. Start it from the home page, which is two commits before main:
git switch -c site-copy main~2
printf '<h1>Corner Cafe</h1>\n<p>Open daily, 8am to 8pm.</p>\n' > index.html
git commit -am "Change hours wording"
git rebase mainRebasing (1/1)Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
error: could not apply cdd5a4b... Change hours wording
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".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply cdd5a4b... # Change hours wordingThe rebase stopped at the first commit that could not be placed. Check the status:
git statusinteractive rebase in progress; onto 19245a4
Last command done (1 command done):
pick cdd5a4b # Change hours wording
No commands remaining.
You are currently rebasing branch 'site-copy' on '19245a4'.
(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 modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")Open index.html. The conflict is marked in the file:
<h1>Corner Cafe</h1>
<<<<<<< HEAD
<p>Open 7am to 9pm.</p>
=======
<p>Open daily, 8am to 8pm.</p>
>>>>>>> cdd5a4b (Change hours wording)During a rebase, the top part, marked HEAD, is the version from the branch you are rebasing onto, which is main. The bottom part is your commit, the one being replayed. Pick the text you want, delete the marker lines, and save. Here the hours should read the way you wrote them:
printf '<h1>Corner Cafe</h1>\n<p>Open daily, 8am to 8pm.</p>\n' > index.html
git add index.html
GIT_EDITOR=true git rebase --continue[detached HEAD 51b2128] Change hours wording
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/site-copy.git add marks the file as resolved. git rebase --continue tells Git to carry on with the next commit. The environment variable GIT_EDITOR=true tells Git to keep the commit message as it is without opening an editor. Leave it out and Git opens your editor, so you can change the message.
If the conflict is too hard to resolve, run git rebase --abort. It returns the branch to exactly how it was before you started the rebase, so nothing is lost.
Worked example 3: an interactive rebase
A plain rebase replays every commit as it is. An interactive rebase lets you change the list first. You can reorder commits, combine them, change their messages, or remove them. It is useful when your branch has a few small commits, such as "Fix menu wording", that belong in an earlier commit.
Start the interactive rebase from main:
git switch add-menu
git rebase -i mainGit opens a file that lists the commits, oldest first, in your editor. Here is the first part of that file, from a run:
pick 6362308 # Add menu page
pick 5c15124 # Add prices
pick 3c304db # Fix menu wordingGit also prints a list of commands below these lines. The most used ones are shown in the table.
| Command | What it does |
|---|---|
pick | Keep the commit as it is |
reword | Keep the change, but edit its message |
fixup | Fold the change into the commit above it and drop this commit's message |
drop | Remove the commit and its change |
The goal here is to fold "Fix menu wording" into "Add menu page". Change the file so the lines read:
pick 6362308 Add menu page
fixup 3c304db Fix menu wording
pick 5c15124 Add pricesTwo things changed. The Fix menu wording line moved directly under the commit it belongs to. Its pick became fixup, which means "fold this into the line above". Save and close the file. Git replays the commits in the order of the list:
Rebasing (2/3)Rebasing (3/3)Successfully rebased and updated refs/heads/add-menu.Check the history:
git log --oneline --graph --all --decorate* ae4765a (HEAD -> add-menu) Add prices
* eb814d6 Add menu page
| * 51b2128 (site-copy) Change hours wording
|/
* 19245a4 (main) Add contact page
* e3c5387 Change opening hours
* 24dce0c Add home pageThere are now two commits on your branch, not three. Check that the fix is in the menu file:
cat menu.html<h1>Menu</h1>
<p>Coffee, tea and toast.</p>The wording fix is still there. It now lives inside the "Add menu page" commit, so the history reads cleanly.
Because fixup throws away the message of the folded commit, the commit "Add menu page" keeps its original message. Use squash instead if you want to keep both messages and edit the combined message yourself. Use reword to change one message without changing its contents.
Why not to rebase a shared branch
This is the part of rebase that causes the most trouble. Every rebase creates new commits with new hashes and leaves the old commits behind. If other people have a copy of the old commits, their copy and yours no longer agree.
The example below uses a bare repository, which is a remote copy with no working files. Think of it as a shared server, the same role GitHub plays for a team. Run these from inside rebase-practice. The remote is created in the folder above it, so it is not nested inside your project:
git init --bare -b main ../origin.git
git remote add origin ../origin.git
git push -u origin main add-menuNow a teammate clones the project into a folder next to yours, switches to add-menu, adds a commit, and pushes it:
git clone ../origin.git ../teammate
cd ../teammate
git config user.name "Teammate"
git config user.email "teammate@example.com"
git switch add-menu
printf 'Small 3.00\n' > sizes.txt
git add sizes.txt
git commit -m "Add drink sizes"
git push origin add-menu
cd ../rebase-practiceBack in your own copy, rewrite the newest commit on the same branch. Start an interactive rebase for that one commit, and change its line to reword. Git then opens the commit message in your editor, where you change "Add prices" to "Add price list" and save:
git rebase -i HEAD~1Rebasing (1/1)[detached HEAD 6cc2cb2] Add price list
Date: Thu Nov 5 10:30:17 2026 +0000
1 file changed, 2 insertions(+)
create mode 100644 prices.txt
Successfully rebased and updated refs/heads/add-menu.The commit has a new hash now, so your branch and the shared branch no longer agree. Now try to share your rewritten branch:
git push origin add-menuTo ../origin.git
! [rejected] add-menu -> add-menu (fetch first)
error: failed to push some refs to '../origin.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.The rejection protects the teammate's commit. Git refuses a push that would remove commits the remote has and you do not. To see both sides, fetch and look at the graph:
git fetch origin
git statusOn branch add-menu
Your branch and 'origin/add-menu' have diverged,
and have 1 and 2 different commits each, respectively.
(use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree cleanThe line "have diverged" means each side has commits the other does not. Running git pull here does not work, because Git does not know how to combine the two sides without a setting:
git pullhint: 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 safe path is to make your branch include the teammate's commit, not to delete it. You could merge or rebase your own unpushed commits on top of theirs. Both keep their commit.
The unsafe path is --force or --force-with-lease, which overwrite the remote branch with your version. Here is what happens when the force push is used:
git push --force-with-lease origin add-menuTo ../origin.git
To ../origin.git
+ b5e46bb...6cc2cb2 add-menu -> add-menu (forced update)Now the teammate fetches the shared branch:
cd ../teammate
git fetch origin
git log --oneline --graph --all --decorate -n 5* 6cc2cb2 (origin/add-menu) Add price list
| * b5e46bb (HEAD -> add-menu) Add drink sizes
| * ae4765a Add prices
|/
* eb814d6 Add menu page
* 19245a4 (origin/main, origin/HEAD, main) Add contact pageThe shared branch no longer has Add drink sizes. The teammate's commit is still in their own copy, because it is in their local history, but the branch on the server has moved past it. The next person who pulls will not get it.
--force-with-lease is safer than --force. It refuses to push if the remote has moved since your last fetch. In this example it allowed the push, because the earlier git fetch origin had already brought the teammate's commit into your copy. It only checks that you have seen the newest version, not that you agree with it. Do not force push a branch that other people use.
On a branch only you use, a rebase followed by a force push is a normal way to clean up history. Before you do it, run git fetch origin and then git log origin/<branch name> to confirm nobody else has pushed to that branch.
Common mistakes
Rebasing with changes you have not committed. Git will not start a rebase while your working tree has changes it does not know what to do with:
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.Commit the changes, or stash them as in the lesson before this one, then rebase.
Rebasing the wrong branch. git rebase main moves the branch you are on, not main. Typing git rebase add-menu while on main by mistake rewrites main. Check the current branch first:
git branch --show-currentLeaving a rebase half finished. While a conflict is open, git status says "interactive rebase in progress". Finish it with git rebase --continue after the fix, or return to where you started with git rebase --abort.
Force pushing a shared branch. Covered above. The remote branch loses commits that other people may already depend on.
Using git pull without a setting. On a diverged branch, git pull stops with the fatal message above. Choose pull.rebase or pull.ff in your Git configuration once, so you do not get stopped each time.
Practice
Exercise 1. You have a branch called feature with three commits, and main has moved on. Write the two commands that move your branch onto the latest main.
Show the answer
Run git switch feature, then git rebase main. Check first with git branch --show-current that you are on feature and not main. The commits are replayed, and each one gets a new hash.
Exercise 2. While on main, you type git rebase add-menu by mistake. Explain what this does to main, and what you should do.
Show the answer
Git replays the commits that main has but add-menu does not onto add-menu, and it moves main to the result. That rewrites main, which other people use. Use git reflog to find where main was before, then move it back there, and switch to the branch you meant to rebase.
Exercise 3. A conflict stops your rebase. The top part of the marked section has the text HEAD, and the bottom part has the hash of your commit. Which version should you keep, and what does that mean for the hours in main?
Show the answer
Neither part is automatically right. The top part is main's version, and the bottom part is your commit. Edit the file so it says what you want. If you keep the top part, the change you made in your commit is lost. If you keep the bottom part, main's change is replaced by yours. Choose on purpose, then run git add, then git rebase --continue.
Exercise 4. In an interactive rebase, you want to combine a small "Fix typo" commit with the commit above it, and keep only the message of the commit above. Which command do you use, and where does its line go?
Show the answer
Use fixup. Move the fixup line directly under the commit it belongs to, so it comes after that commit in the list. Change pick to fixup on that line.
Exercise 5. A teammate has pushed a commit to your shared branch. You have a commit that you have not pushed. Describe the safe way to share your commit without deleting theirs, and the command you would avoid.
Show the answer
Fetch their commit with git fetch origin, then use git pull --rebase (or merge) so your unpushed commit sits on top of theirs, and push normally. Avoid git push --force and git push --force-with-lease, because they overwrite the teammate's commit on the remote.
Summary
git rebase mainreplays your branch's commits on top of the latestmain, which gives a straight history and changes the commit hashes.- A conflict stops the rebase. Fix the file, run
git add, thengit rebase --continue, or rungit rebase --abortto start again. git rebase -i mainlets you reorder, fold, reword or drop commits.fixupfolds a commit into the one above it and keeps the earlier message.- Rebase rewrites history. Use it on branches only you use.
- A force push to a shared branch removes the commits other people have pushed to it. Fetch and integrate their work first.
git statustells you whether a rebase is still in progress.