Level 5 — Advanced: rebase, cherry-pick, reset, reflog
Advanced Git CLI
Objective
Everything in this lab is recoverable, and by the end you will have recovered from most of it on purpose. Six short exercises, in one branch of one project: stash, interactive rebase, rebase onto a moved main with a safe force push, cherry-pick, revert, and a hard reset undone through the reflog.
Starting state
- Your own private project with the playground pushed, from Lab 2, and
mainup to date. - A second clone, or a colleague, to play the person who pushes to
mainwhile you work. The instructions use a second clone; Lab 4 set one up. - About an hour, and a willingness to break things deliberately.
Instructions
-
Start work, then get interrupted. On a new branch, begin a change and do not commit it:
Terminal $ git switch -c docs/21-difficulty-filterAdd a section to
docs/getting-started.md, then check what Git sees:Terminal $ git status --shortM docs/getting-started.md -
Park it with a name. Something urgent has come up on
main:Terminal $ git stash push -m "difficulty filter draft"Saved working directory and index state On docs/21-difficulty-filter: difficulty filter draftgit status --shortnow prints nothing: the tree is clean, and you can switch branches freely. -
Come back and unpark.
Terminal $ git stash liststash@{0}: On docs/21-difficulty-filter: difficulty filter draftTerminal $ git switch docs/21-difficulty-filter $ git stash popYour change is back, and the stash is dropped. Now commit it as
docs: add filtering section. -
Make a mess on purpose. Add two more commits with deliberately poor messages:
wipandfix typo. This is what a real branch looks like after an afternoon:Terminal $ git log --oneline -4c449a24 fix typo 6ac7e98 wip 874ce55 docs: add filtering section d3b92ec chore: add CI configuration and contributing guide -
Tidy it with an interactive rebase. Nothing is pushed yet, so rewriting is safe:
Terminal $ git rebase -i mainSuccessfully rebased and updated refs/heads/docs/21-difficulty-filter.An editor opens listing your three commits, each with
pick. Change the second and third frompicktosquash, save and close. A second editor opens for the combined message: keepdocs: add filtering sectionand delete the rest. Then:Terminal $ git log --oneline -3534c2c2 docs: add filtering section d3b92ec chore: add CI configuration and contributing guide b81702c docs: add getting started guide and FAQThree commits became one, with the same content.
-
Push the branch, then let
mainmove underneath you. In your second clone, commit something tomainand push it. Back in the first:Terminal $ git fetch $ git log --oneline --graph -4 origin/main docs/21-difficulty-filter* 9d4a39f docs: add a support section to the README | * 534c2c2 docs: add filtering section |/ * d3b92ec chore: add CI configuration and contributing guideThe fork in that graph is your branch and
maindiverging. -
Rebase onto the new
main.Terminal $ git rebase origin/mainSuccessfully rebased and updated refs/heads/docs/21-difficulty-filter.Your commit now sits on top of the support-section commit, with a new hash.
-
Meet the rejection, then push safely. An ordinary push is refused, because you rewrote a commit the server already had:
Terminal $ git push! [rejected] docs/21-difficulty-filter -> docs/21-difficulty-filter (non-fast-forward) error: failed to push some refs hint: Updates were rejected because the tip of your current branch is behindDo not follow that hint. Pulling would merge the old version back in. This is your own branch, so replace it:
Terminal $ git push --force-with-lease+ 534c2c2...0fec326 docs/21-difficulty-filter -> docs/21-difficulty-filter (forced update) -
Merge it, tag a release, and cut a release branch. Merge the branch into
mainhowever your project does it, then:Terminal $ git tag -a v1.2.0 -m "Release 1.2.0" $ git push origin main v1.2.0 $ git switch -c release/1.2 v1.2.0 -
Fix something on
mainand backport it. Onmain, commit a one-line fix, note its hash, then:Terminal $ git switch release/1.2 $ git cherry-pick -x 3fe0fe5[release/1.2 a4147aa] fix: ignore trails with an empty distance (#15) 1 file changed, 2 insertions(+)Read the resulting message:
Terminal $ git log -1 --format=%Bfix: ignore trails with an empty distance (#15) (cherry picked from commit 3fe0fe5…)Two hashes, one change, and a record of the relationship. That is a backport.
-
Undo something that is already pushed. Back on
main, push the fix, then decide it was wrong:Terminal $ git switch main $ git revert --no-edit HEAD[main ae7615f] Revert "fix: ignore trails with an empty distance (#15)" 1 file changed, 2 deletions(-)Terminal $ git log --oneline -3ae7615f Revert "fix: ignore trails with an empty distance (#15)" 3fe0fe5 fix: ignore trails with an empty distance (#15) 0fec326 docs: add filtering sectionBoth commits are there: the mistake and its undo. Nobody else has to do anything.
-
Destroy two commits and get them back. The finale:
Terminal $ git reset --hard HEAD~2HEAD is now at 0fec326 docs: add filtering sectionThey are gone from
git log. Now look at what Git still remembers:Terminal $ git reflog -50fec326 HEAD@{0}: reset: moving to HEAD~2 ae7615f HEAD@{1}: revert: Revert "fix: ignore trails with an empty distance (#15)" 3fe0fe5 HEAD@{2}: checkout: moving from release/1.2 to main a4147aa HEAD@{3}: cherry-pick: fix: ignore trails with an empty distance (#15) 0fec326 HEAD@{4}: checkout: moving from main to release/1.2And bring them back:
Terminal $ git reset --hard ORIG_HEADHEAD is now at ae7615f Revert "fix: ignore trails with an empty distance (#15)"git log --oneline -3shows all three commits again.
Expected result
- One tidy commit on a branch that started as three messy ones.
- A branch rebased onto a moved
mainand published with--force-with-lease. - The same fix on
mainand onrelease/1.2, under two hashes, with the relationship recorded. - A revert commit sitting above the commit it undoes.
- Two commits destroyed and restored, with the reflog as the evidence.
If something goes wrong
| Symptom | Fix |
|---|---|
| The interactive rebase editor is confusing or opens the wrong program | git rebase --abort, set your editor (lesson 3.3), try again |
| A conflict during the rebase | Resolve, git add, git rebase --continue; or git rebase --abort (lesson 12.6) |
--force-with-lease is rejected with "stale info" |
Someone pushed to your branch. git fetch, look at what arrived, then decide (lesson 12.8) |
| The cherry-pick conflicts | The release branch has diverged; resolve as you would a merge, or git cherry-pick --abort |
git revert complains about a merge |
Add -m 1 (command page) |
| You have lost track entirely | git reflog, find a line whose message you recognize, and git reset --hard to it |
What you learned
Every command in this lab has a reputation, and the reputation is mostly about not knowing the way back. You now have the way back for each: --abort during an operation, --force-with-lease instead of --force, revert instead of reset on pushed work, and git reflog when something has vanished.
The one rule that survives all of it: rewrite only what nobody else has. Steps 5, 7 and 8 were safe because the branch was yours. Step 11 used revert precisely because the commit was not.
Next
Level 6 puts the whole course together: one sprint at Northwind Trails, with issues, a hotfix, a red pipeline, a conflict and a release.