Rewriting history: amend, interactive rebase and safe force-pushes
Clean a messy branch before review: fix the last commit with amend, squash and reword with interactive rebase, fold in fixups, and force-push without destroying anyone's work.
By the end of this lesson you'll be able to
0 of 6 completeWhy rewrite at all?
While you work, commits record how you got there: "wip", "fix typo", "actually fix it". Reviewers and future readers want what you did: a few commits, each one a complete, working step. Rewriting turns the first into the second before anyone depends on it.
Amend: fix the last commit
$ git add forgot.txt $ git commit --amend --no-edit # add it to the last commit, keep the message $ git commit --amend -m "Add session handling and the forgotten file" # change the message $ git commit --amend --reset-author --no-edit # after fixing user.email: fix the author too
git add forgot.txtgitversion controladdstage changes
git commit --amend --no-editcommitsave staged changes
Amend doesn't edit a commit; it replaces it with a new one (new ID). If the old one was pushed, the new one needs a force-push, so it's at its best before you push.
Interactive rebase: edit a whole branch
git rebase -i main lists every commit on your branch since main and lets you decide what happens to
each one:
$ git log --oneline main.. a09931b Add session handling 263b2c6 wip ceeb08b fix typo e9160ce Add login form $ git rebase -i main
git log --oneline main..gitversion controlloghistory
Your editor opens with the oldest commit first. This is the real screen from Git 2.53, on a branch with four commits:
For the login branch, change the first word of each line so it reads:
pick e9160ce # Add login form fixup ceeb08b # fix typo fixup 263b2c6 # wip pick a09931b # Add session handling
Change the words, save and close. Git replays the commits in order:
Successfully rebased and updated refs/heads/feature/login. $ git log --oneline main.. 86eda3b Add session handling and the forgotten file af9a5c4 Add login form
git log --oneline main..gitversion controlloghistory
| Want to… | Do |
|---|---|
| combine "fix typo" into the commit before it | fixup (or squash to keep both messages) |
| fix a message | reword |
| split a commit, or change its files | edit, then git reset HEAD~1, commit in pieces, git rebase --continue |
| reorder commits | move the lines |
| delete a commit | drop (or delete the line) |
If a step conflicts, resolve it as in the Merge strategies lesson, git add, then
git rebase --continue. git rebase --abort puts everything back as it was before you started.
Fixup commits: fold in review fixes
During review, make the fix as a fixup commit that names the commit it belongs to. Git sorts it into place for you later:
$ git commit -a --fixup af9a5c4 # "fixup! Add login form" $ git log --oneline main.. b57e38f fixup! Add login form 86eda3b Add session handling and the forgotten file af9a5c4 Add login form $ git rebase -i --autosquash main # the fixup is already moved and marked; just save $ git log --oneline main.. 4a19ac0 Add session handling and the forgotten file ff8fd10 Add login form $ git config --global rebase.autoSquash true # make --autosquash the default
git commit -a --fixup af9a5c4gitversion controlcommitsave staged changes
git log --oneline main..loghistory
git config --global rebase.autoSquash trueconfigread or set settings--globalfor your user, in every repo
Reviewers can read the fixups as a small diff of "what changed since my review", and you squash them just before merging.
Force-pushing safely
A branch you rewrote no longer contains the commits the remote has, so a normal push is refused. You need a force-push, and the plain one is dangerous:
| Command | Overwrites the remote when… |
|---|---|
git push --force | always, even if a teammate pushed meanwhile: their commits vanish |
git push --force-with-lease | the remote still matches your origin/branch; refused otherwise |
… --force-with-lease --force-if-includes | as above, and you have integrated what you fetched |
With a teammate's commit on the remote that you've never fetched, the lease protects you:
! [rejected] feature/login -> feature/login (stale info)
But --force-with-lease compares against origin/feature/login, which any git fetch updates,
including the background fetches many editors run. After a fetch, the lease passes and the teammate's
commit is overwritten anyway. --force-if-includes closes that gap by checking that their commits are
actually in your branch:
$ git fetch # an editor may do this without asking $ git push --force-with-lease --force-if-includes ! [rejected] feature/login -> feature/login (remote ref updated since checkout) $ git config --global alias.pushf "push --force-with-lease --force-if-includes" $ git pushf # use this for every force-push
git fetchgitversion control
git push --force-with-lease --force-if-includespushupload commits
git config --global alias.pushf "push --force-with-lease --force-if-includes"configread or set settings--globalfor your user, in every repo
When it's refused, git pull --rebase to take in their work, check the result, then git pushf again.
When it goes wrong
The reflog (the Undoing changes lesson) remembers the branch before the rebase:
$ git reflog -5 # find the line before "rebase (start)" $ git reset --hard feature/login@{1} # or that commit's ID $ git rebase --abort # if the rebase is still in progress
git reflog -5gitversion control
ORIG_HEAD also points at where the branch was before the last rebase or reset: git reset --hard ORIG_HEAD undoes a rebase you just finished.
Clean up a branch before review
Your feature branch has four commits: the real feature, 'fix typo', 'wip', and a second real commit with a poor message. Turn it into two clean commits with good messages, fold a later review fix into the first one with a fixup commit, and force-push safely.
A branch with two commits, pushed with --force-with-lease --force-if-includesReveal one safe solution
$ git switch -c feature/login $ echo "form" > login.py && git add . && git commit -m "Add login form" $ echo "typo" >> login.py && git commit -am "fix typo" $ echo "more" >> login.py && git commit -am "wip" $ echo "s" > session.py && git add . && git commit -m "session stuff" $ git push -u origin feature/login $ GIT_SEQUENCE_EDITOR="sed -i -e '2s/^pick/fixup/' -e '3s/^pick/fixup/' -e '4s/^pick/reword/'" git rebase -i main $ git log --oneline main.. # two commits; the second has your new message $ echo "review fix" >> login.py && git commit -a --fixup HEAD~1 $ GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash main $ git push --force-with-lease --force-if-includes $ git log --oneline main.. | wc -l 2
git switch -c feature/logingitversion control
echo "form" > login.py && git add . && git commit -m "Add login form"echoprint text>write output to a file, replacing it&&run the next command only if this one workedaddstage changes.this foldercommitsave staged changes
echo "typo" >> login.py && git commit -am "fix typo">>append output to a file
git push -u origin feature/loginpushupload commits
git log --oneline main..loghistory
git log --oneline main.. | wc -l|pipe: send output to the next commandwccount lines, words and bytes-lcount lines only
GIT_SEQUENCE_EDITOR edits the todo list with a script instead of an editor, the same change you'd make
by hand; reword still opens your editor for the new message ("Add session handling"). In real work,
just run git rebase -i main and edit the list yourself.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
You committed but forgot one file, and haven't pushed. What is the cleanest fix?
Official reading for this lesson
Or get every quiz answer right and it completes itself.