DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    Git & GitHubLesson 10 of 13
    Your progress · 0%
    On this page
    1. Why rewrite at all?
    2. Amend: fix the last commit
    3. Interactive rebase: edit a whole branch
    4. Force-pushing safely
    5. When it goes wrong
    ↖ Course roadmap
    Lesson 10 · Rewriting history

    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.

    35 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    Why 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.

    The rule

    Rewrite your own commits freely until you share them. Once others have pulled them (main, release branches, a branch a teammate works on), add new commits instead. Rewriting shared history forces everyone to repair their clones, and can silently delete work.

    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.txt
    • gitversion control
    • addstage changes
    git commit --amend --no-edit
    • commitsave 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:

    Edit the list, save, close. Git replays the branch the way you described.© Diagram: DevOps Academy

    Edit the list, save, close. Git replays the branch the way you described.© Diagram: DevOps Academy
    $ 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 control
    • loghistory

    Your editor opens with the oldest commit first. This is the real screen from Git 2.53, on a branch with four commits:

    Every line starts with pick. Only the first word matters to you: the ID says which commit, and the text after # is just a reminder of its message.© Screenshot: DevOps Academy

    Every line starts with pick. Only the first word matters to you: the ID says which commit, and the text after # is just a reminder of its message.© Screenshot: DevOps Academy

    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 control
    • loghistory
    Want to…Do
    combine "fix typo" into the commit before itfixup (or squash to keep both messages)
    fix a messagereword
    split a commit, or change its filesedit, then git reset HEAD~1, commit in pieces, git rebase --continue
    reorder commitsmove the lines
    delete a commitdrop (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 af9a5c4
    • gitversion control
    • commitsave staged changes
    git log --oneline main..
    • loghistory
    git config --global rebase.autoSquash true
    • configread 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:

    CommandOverwrites the remote when…
    git push --forcealways, even if a teammate pushed meanwhile: their commits vanish
    git push --force-with-leasethe remote still matches your origin/branch; refused otherwise
    … --force-with-lease --force-if-includesas 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 fetch
    • gitversion control
    git push --force-with-lease --force-if-includes
    • pushupload 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.

    Guard rails

    Block force-pushes on main and release branches with a ruleset (the Collaboration lesson), so even a mistaken --force can't rewrite them. Before rewriting a branch someone else has touched, tell them; they should commit or push their work first.

    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 -5
    • gitversion 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.

    ⌘
    Mini mission

    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.

    Input: Any repository with a remote (a bare repository works)Output: A branch with two commits, pushed with --force-with-lease --force-if-includes
    Reveal 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/login
    • gitversion 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 worked
    • addstage changes
    • .this folder
    • commitsave staged changes
    echo "typo" >> login.py && git commit -am "fix typo"
    • >>append output to a file
    git push -u origin feature/login
    • pushupload commits
    git log --oneline main..
    • loghistory
    git log --oneline main.. | wc -l
    • |pipe: send output to the next command
    • wccount 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 done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    You committed but forgot one file, and haven't pushed. What is the cleanest fix?

    Go deeper

    Official reading for this lesson

    • GuidePro Git: rewriting historyPro Git ↗
    • Docsgit-rebase documentationGit ↗
    • Docsgit-push: --force-with-lease and --force-if-includesGit ↗
    • DocsGitHub Docs: about Git rebaseGitHub Docs ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousHistory and diffs: find what changed, who changed it, and when it brokeNext lesson →Tags and releases: version numbers people can trust