What rebase does

Merging joins two lines of work with a merge commit (see Merging and Conflicts). A rebase takes another approach: it moves your branch so that it starts from a new base commit. Git takes the commits that are on your branch but not on the new base, and replays them one by one on top of it, as if you had started your work there. Then it moves your branch pointer to the last replayed commit.

In the example below, Sam's branch acl-update has two commits, and main got a new commit in the meantime. Sam rebases his branch onto main:

git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q -c acl-update
printf 'ip access-list MGMT\n' > acl.cfg
git add acl.cfg && git commit -q -m "Add management ACL"
printf ' permit 198.51.100.0/24\n' >> acl.cfg
git commit -q -am "Allow NOC subnet in ACL"
git switch -q main
printf 'logging host 192.0.2.20\n' >> router.cfg
git commit -q -am "Add syslog server"
git switch -q acl-update
git log --oneline --graph --all
git rebase -q main            # -q: no progress lines
git log --oneline --graph --all
* a8fe1f6 Allow NOC subnet in ACL
* f7ca96a Add management ACL
| * c8dae00 Add syslog server
|/
* 5a75fd4 Add NTP server
* aedb4dc Add edge-01 base config
* 93200a0 Allow NOC subnet in ACL
* c7c59e1 Add management ACL
* c8dae00 Add syslog server
* 5a75fd4 Add NTP server
* aedb4dc Add edge-01 base config

Before the rebase, the branch started at 5a75fd4; afterward it starts at c8dae00, the tip of main. Look at the hashes: the commits have the same messages and changes, but f7ca96a became c7c59e1 and a8fe1f6 became 93200a0. A commit's hash covers its parent, so a commit with a new parent is a new commit. The originals still exist for a while but no branch points to them any more. Without -q, Git ends with Successfully rebased and updated refs/heads/acl-update.

Merging acl-update into main now is a fast-forward. That is the typical workflow: rebase your topic branch onto the latest main, test it, then merge it without a merge commit.

Merge or rebase?

The same diverged history combined two ways. Top, with merge: main commit c8dae00 and branch commits f7ca96a and a8fe1f6 stay as they are and a merge commit joins them. Bottom, with rebase: the history is a straight line 5a75fd4, c8dae00, c7c59e1, 93200a0, where the last two are new copies of the branch commits; the old commits f7ca96a and a8fe1f6 are no longer on a branch. git merge: history keeps both lines, plus a merge commit 5a75fd4 c8dae00 f7ca96a a8fe1f6 main acl-update merge commit git rebase: the branch's commits are copied onto main, history is linear 5a75fd4 c8dae00 c7c59e1 93200a0 new copies, new hashes main acl-update f7ca96a a8fe1f6 old, no longer on a branch
Hashes from the example above; arrows point from a commit to its parent. Merge preserves what happened; rebase rewrites the branch into a straight line.
Merge Rebase
History Shows what really happened, including parallel work A straight line, as if work happened one after another
Existing commits Unchanged; one new merge commit Replaced by copies with new hashes
Conflicts Resolved once, in the merge commit Resolved per replayed commit, possibly several times
Safe on shared branches Yes No: only on commits nobody else has

Neither is better in general; teams choose. A common combination: rebase your own topic branch to keep it current and tidy, and merge it into the shared branch (often through a pull request). GitHub offers "rebase and merge" and "squash and merge" as merge methods for pull requests; a rebase-merge creates new commit hashes on the base branch (see Pull Requests and Code Review).

Interactive rebase

An interactive rebase (git rebase -i) lets you edit the list of commits before Git replays them. It is the tool for cleaning up your own work before you share it: combining "fix typo" commits, rewording unclear messages, putting commits in a logical order. Git opens your editor with a todo list, one line per commit, oldest first:

git rebase -i main            # rewrite the commits that are not on main
pick 0fbbd1b Add management ACL
squash c32c6f6 Allow NOC subnet in ACL
fixup a88ba9b typo
reword 3d41f07 add uplink descr
drop 9be27c1 Temporary debug logging

# Rebase aedb4dc..9be27c1 onto aedb4dc (5 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 [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, ...
# d, drop <commit> = remove commit
# ...
Command Effect
pick Keep the commit as it is (the default for every line)
reword Keep the changes, but edit the message (Git opens the editor)
edit Stop after this commit so you can amend it, then git rebase --continue
squash Meld into the previous commit and combine both messages (editor opens)
fixup Meld into the previous commit and discard this commit's message
drop Remove the commit and its changes (deleting the line does the same)

You can also reorder lines to reorder commits. When you save and close the editor, Git replays the list from top to bottom and stops where it needs you: for a message to write, an edit, or a conflict. To rewrite the last few commits of the current branch regardless of other branches, use git rebase -i HEAD~3.

Squashing

To squash is to combine several commits into one. A branch with "Add ACL", "fix", "fix again" and "typo" tells reviewers and later readers nothing extra; one commit "Add management ACL" with a good message tells them everything. The squash and fixup commands of an interactive rebase do this on your branch; GitHub's "squash and merge" does it when a pull request is merged. Keep commits separate when they are separate, meaningful steps — squashing is for noise, not for hiding structure.

Fixup commits and autosquash

Often you notice during review that an earlier commit on your branch needs a correction. git commit --fixup=<commit> records the correction as a separate commit named fixup! <original message>. git rebase -i --autosquash later moves each such commit right below its target and marks it fixup automatically. Normally you would just save the prepared todo list; here GIT_SEQUENCE_EDITOR=: accepts it unchanged without opening an editor (: is the shell command that does nothing), so the example can run on its own:

git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git switch -q -c acl-update
printf 'ip access-list MGMT\n permit 198.51.100.0/24\n' > acl.cfg
git add acl.cfg && git commit -q -m "Add management ACL"
printf 'interface ge-0/0/0\n description uplink\n' >> router.cfg
git commit -q -am "Describe uplink interface"
# review comment: the ACL also needs the jump host
printf ' permit 203.0.113.10/32\n' >> acl.cfg
git commit -q -a --fixup=HEAD~1     # "fixup! Add management ACL"
git log --oneline
GIT_SEQUENCE_EDITOR=: git rebase -q -i --autosquash main
git log --oneline
cat acl.cfg
482a7e2 fixup! Add management ACL
8ecefa4 Describe uplink interface
c0fcc10 Add management ACL
aedb4dc Add edge-01 base config
eb2718f Describe uplink interface
9a62549 Add management ACL
aedb4dc Add edge-01 base config
ip access-list MGMT
 permit 198.51.100.0/24
 permit 203.0.113.10/32

The fixup commit has disappeared into "Add management ACL", which now contains both permit lines, and both remaining commits on the branch have new hashes. Setting git config --global rebase.autoSquash true makes --autosquash the default for interactive rebases.

Cherry-pick

A cherry-pick (git cherry-pick <commit>) copies the changes of one existing commit onto the current branch as a new commit. It is the tool for "we need that one fix on main now, but the rest of the branch is not ready":

git init -q netops && cd netops
printf 'hostname edge-01\nntp server 192.0.2.10\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git switch -q -c snmp-rework
printf 'snmp-server location rack-3\n' > snmp.cfg
git add snmp.cfg && git commit -q -m "Start SNMP rework"
sed -i 's/192.0.2.10/192.0.2.11/' router.cfg
git commit -q -am "Fix NTP server address"
git log --oneline -1
git switch -q main            # main needs the fix now, not the unfinished rework
git cherry-pick snmp-rework
git log --oneline --graph --all
1e67b03 Fix NTP server address
[main d4ad17b] Fix NTP server address
 Date: Mon Mar 2 09:00:00 2026 +0000
 1 file changed, 1 insertion(+), 1 deletion(-)
* d4ad17b Fix NTP server address
| * 1e67b03 Fix NTP server address
| * a8e1ab3 Start SNMP rework
|/
* 82f7071 Add edge-01 base config

The new commit d4ad17b has the same message, author and author date (the Date: line) as the original 1e67b03, but a different parent, so a different hash. The two now exist side by side; when snmp-rework is merged later, the merge usually goes through cleanly, because both sides made the same change. With -x, Git appends a line (cherry picked from commit …) to the message, which is useful when you backport fixes to release branches. A cherry-pick can conflict like a merge; resolve the file, git add it, and run git cherry-pick --continue (or --abort).

Conflicts during a rebase

Because a rebase replays commits one at a time, each replayed commit can conflict with the new base. Git then stops in the middle of the rebase, on a detached HEAD:

git init -q netops && cd netops
printf 'hostname edge-01\nlogging host 192.0.2.20\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git switch -q -c new-collector
sed -i 's/192.0.2.20/192.0.2.25/' router.cfg
git commit -q -am "Move syslog to new collector"
git switch -q main
sed -i 's/192.0.2.20/198.51.100.20/' router.cfg
git commit -q -am "Send syslog to NOC server"
git switch -q new-collector
git rebase -q main
Auto-merging router.cfg
CONFLICT (content): Merge conflict in router.cfg
error: could not apply 5e1fd03... Move syslog to new collector
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 5e1fd03... Move syslog to new collector

Resolve it the same way as a merge conflict — edit the file, remove the conflict markers, git add it — but then continue the rebase instead of committing. Git replays the remaining commits and moves the branch:

git init -q netops && cd netops
printf 'hostname edge-01\nlogging host 192.0.2.20\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git switch -q -c new-collector
sed -i 's/192.0.2.20/192.0.2.25/' router.cfg
git commit -q -am "Move syslog to new collector"
git switch -q main
sed -i 's/192.0.2.20/198.51.100.20/' router.cfg
git commit -q -am "Send syslog to NOC server"
git switch -q new-collector
git rebase -q main > /dev/null 2>&1   # stops with the conflict shown above
printf 'hostname edge-01\nlogging host 192.0.2.25\n' > router.cfg
git add router.cfg
GIT_EDITOR=true git rebase --continue > /dev/null   # (hides progress lines)
git log --oneline
18f6b42 Move syslog to new collector
bdba149 Send syslog to NOC server
1c9b5a4 Add edge-01 base config

GIT_EDITOR=true accepts the commit message unchanged without opening an editor; in daily use you just run git rebase --continue. The three ways out of a stopped rebase:

  • git rebase --continue: after resolving and git add, go on with the next commit.
  • git rebase --skip: drop the commit that conflicts and go on (its changes are lost from the result).
  • git rebase --abort: stop and put the branch back exactly where it was before the rebase started.

Note the side effect of rebasing: in a conflict, HEAD ("ours") is the new base with the commits replayed so far, and "theirs" is your own commit being replayed — the reverse of a merge. If a long branch conflicts again and again, git rebase --abort followed by a merge is often the calmer choice.

The golden rule

Rewriting history means replacing existing commits with new ones: by rebasing, amending, squashing, dropping commits, or moving a branch back. Locally, on commits only you have, this is everyday cleanup. The golden rule:

Never rewrite commits that you have shared with others — never rebase, amend or squash commits that are already on a shared branch.

Why? Your colleagues' clones still contain the old commits, and their new work builds on them. After you replace those commits, the server's branch and their branch have diverged with what looks like duplicate commits — same messages, different hashes. The next pull merges both versions back together, conflicts appear in code nobody changed, and in the worst case someone's force push erases the other person's work. Untangling it costs the whole team time.

In practice: rebase and squash freely on your own topic branch before you push it, or while it is pushed but clearly yours (for example a pull request branch only you work on). Never rewrite main, release branches, or anything others build on; undo published commits with git revert instead (see Undoing Things). Many teams enforce this on the server with branch protection rules or rulesets that block force pushes.

Force pushing safely

A normal git push only succeeds if it is a fast-forward for the server's branch: your commits must build on what is already there. After rewriting commits you already pushed, that is no longer true, so Git refuses. A force push overwrites the server's branch with yours anyway, discarding whatever commits were only on the server.

The examples use a bare repository in the same folder as the shared "server" (see Remotes and Collaboration), connected with git remote add origin ../shared.git so the output shows a short path. Sam pushed his branch, then amended the commit:

git init -q --bare shared.git           # the shared repository ("the server")
git init -q sam && cd sam
git remote add origin ../shared.git
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git push -q origin main
git switch -q -c acl-update
printf 'ip access-list MGMT\n' > acl.cfg
git add acl.cfg && git commit -q -m "Add management ACL"
git push -q -u origin acl-update
# Sam rewrites the commit he already pushed
printf ' permit 198.51.100.0/24\n' >> acl.cfg
git commit -q -a --amend --no-edit
git push
git push --force-with-lease
To ../shared.git
 ! [rejected]        acl-update -> acl-update (non-fast-forward)
error: failed to push some refs to '../shared.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.
To ../shared.git
 + 0fbbd1b...c0fcc10 acl-update -> acl-update (forced update)

The first push is rejected as non-fast-forward: the server has commit 0fbbd1b, which is not part of Sam's rewritten branch. The hint suggests pulling, which would be the right advice if someone else had pushed — but here Sam replaced the commit on purpose. --force-with-lease then succeeds: + 0fbbd1b...c0fcc10 and (forced update) show that the branch was replaced rather than extended.

The lease is the safety catch. Git only overwrites the server's branch if it still points to the commit that your remote-tracking branch (origin/acl-update) says it points to — that is, if nobody pushed since you last fetched. In the next example Amara pushes a commit to the same branch while Sam is rewriting:

git init -q --bare shared.git           # the shared repository ("the server")
git init -q sam && cd sam
git remote add origin ../shared.git
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git push -q origin main
git switch -q -c acl-update
printf 'ip access-list MGMT\n' > acl.cfg
git add acl.cfg && git commit -q -m "Add management ACL"
git push -q -u origin acl-update
# Sam rewrites the commit he already pushed
printf ' permit 198.51.100.0/24\n' >> acl.cfg
git commit -q -a --amend --no-edit
# meanwhile, Amara adds a commit to the same branch on the server
cd .. && git clone -q -b acl-update shared.git amara && cd amara
git config user.name "Amara Okafor"
git config user.email amara@example.com
printf ' permit 203.0.113.10/32\n' >> acl.cfg
git commit -q -am "Allow jump host in ACL" && git push -q
# Sam has not fetched since: his lease is out of date
cd ../sam
git push --force-with-lease
To ../shared.git
 ! [rejected]        acl-update -> acl-update (stale info)
error: failed to push some refs to '../shared.git'

(stale info) means: the server has moved since Sam last looked, so his view is out of date and the push is refused. A plain git push --force would have succeeded here and silently deleted Amara's commit from the branch. Sam should fetch, look at what Amara added, rebase onto it, and only then push again.

git pull --rebase

git pull is git fetch followed by integrating the fetched branch into yours. When both you and a colleague committed to the same branch, the branches have diverged, and there are two ways to integrate: a merge, which adds a "Merge branch 'main' of …" commit every time this happens, or a rebase. git pull --rebase fetches and then replays your local commits on top of what you fetched:

git init -q --bare shared.git
git clone -q shared.git sam 2>/dev/null && cd sam
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
git push -q origin main
cd .. && git clone -q shared.git amara && cd amara
git config user.name "Amara Okafor"
git config user.email amara@example.com
printf 'ntp server 192.0.2.10\n' > ntp.cfg
git add ntp.cfg && git commit -q -m "Add NTP config" && git push -q
cd ../sam                      # Sam has a local commit, Amara pushed first
printf 'logging host 192.0.2.20\n' > logging.cfg
git add logging.cfg && git commit -q -m "Add syslog config"
git pull -q --rebase           # fetch, then replay Sam's commit on top
git log --oneline --graph --format='%h %an: %s'
git push -q
* 96ba253 Sam Rivera: Add syslog config
* b1f7c8a Amara Okafor: Add NTP config
* aedb4dc Sam Rivera: Add edge-01 base config

Sam's local commit was replayed on top of Amara's, so the history is a straight line and the push that follows is a normal fast-forward — no force needed. This does not break the golden rule: the only rewritten commit is Sam's own, unpublished one. Without any setting, a plain git pull on diverged branches stops in Git 2.39 with fatal: Need to specify how to reconcile divergent branches. and lists the choices. Choose once with git config --global pull.rebase true (rebase), pull.rebase false (merge) or pull.ff only (only fast-forwards; otherwise stop).