Skip to content

About

Hands-on Git lab: 11 essential skills (branch, merge, conflicts, rebase, squash, stash, reset, revert, cherry-pick, reflog, bisect) with animated guides, practice branches and risk labels.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

git-testing: a hands-on Git lab covering branch, merge, conflicts, rebase, squash, stash, reset, revert, cherry-pick, reflog and bisect

A safe, hands-on playground for the Git skills every developer needs.
Fork it. Break things. Fix them. Every exercise uses plain .txt files, so you can focus 100% on Git.


Fork it first

Skills Level Practice files Git Safety labels

Quick start · Skill map · The skills · Reset vs. revert · Danger zone · Recovery guide · Cheat sheet · Test yourself


For anyone, and for my team

For anyone — and for my team — who wants to learn or test their Git & GitHub skills: here it is.

Tip

Fork it first. It's the best way to test your skills. A fork is your own copy of this lab on GitHub. In your fork you can push, force-push, rebase, open pull requests and delete branches freely, without asking anyone and without affecting anyone else. This original stays read-only, so it's always a clean starting point for the next person.

  1. Click Fork and untick "Copy the main branch only". Otherwise your fork gets only main and none of the 11 practice branches.
  2. Clone your fork: git clone https://github.com/<your-username>/git-testing.git

Using the GitHub CLI? gh repo fork aimagician/git-testing --clone does both steps and keeps every branch.

This repository is a lab, not a project. Every practice branch has been prepared so that a specific Git situation is waiting for you: a conflict to resolve, a messy history to squash, a "lost" commit to rescue, a bug to hunt down. For every skill you get:

  • An animated diagram of what Git is doing under the hood
  • A hands-on exercise on its own branch, using its own test-XX.txt file
  • An expected result, so you can check yourself
  • A risk label and the pitfalls that hurt people in real projects

Nothing here can hurt a real codebase. Break it as often as you like.

Quick start

# 1. Fork first (see the tip above), then clone YOUR fork
git clone https://github.com/<your-username>/git-testing.git
cd git-testing
# Only want to look around? Cloning the original also works
# (you just can't push to it):
#   git clone https://github.com/aimagician/git-testing.git

# 2. Look around: every practice branch is there as origin/skill/...
git branch -a

# 3. Recommended: a readable history view used throughout this guide
git config --global alias.lg "log --oneline --graph --decorate --all"
git lg

# 4. Keep a link to the original lab, so you can reset from it later
#    (skip if you used gh repo fork --clone, which already did this)
git remote add upstream https://github.com/aimagician/git-testing.git

Tip

The first time you run git switch skill/04-rebase, Git creates a local skill/04-rebase branch that tracks origin/skill/04-rebase. Commands such as git merge or git cherry-pick need that local branch to exist, which is why exercises start by visiting the practice branch.

Note

Requirements: Git 2.23 or newer (git --version) for git switch and git restore. Commands are written for bash/zsh (macOS, Linux, or Git Bash on Windows, which ships with Git for Windows). In PowerShell, quote anything with braces: git reset --hard 'HEAD@{1}', git stash apply 'stash@{0}'. Interactive rebase opens your editor; pick a friendly one with git config --global core.editor "code --wait" (VS Code) or "nano".

How the lab works

git-testing/
├── README.md                    ← you are here
├── assets/                      ← the animated SVG guides
└── test-01.txt … test-11.txt    ← one practice file per skill

Every skill has one branch and one file, so exercises never interfere with each other:

main
 ●  Initial commit: README and practice files
 ●  Add animated SVG guides            ← all skill/* branches start here
 ●  test-03: mark release as APPROVED  ← main moves on (skills 03 + 04)
 ●  test-04: add Release 2.0 banner
 ●  …later README updates (they never touch the test-XX.txt files)

skill/01-branching     1 commit    a line that only exists on this branch
skill/02-merge         2 commits   ticks the release checklist
skill/03-conflict      1 commit    "Status: REJECTED" (clashes with main)
skill/04-rebase        2 commits   steps 4 and 5, built on the old main
skill/05-squash        4 commits   a feature, "wip", "fix typo", "wip again"
skill/06-stash         1 commit    adds "tea" to the shopping list
skill/07-reset         3 commits   day 1, day 2, day 3 (undo me)
skill/08-revert        3 commits   a BAD commit hiding in the middle
skill/09-cherry-pick   3 commits   a fix sandwiched between two experiments
skill/10-reflog        3 commits   chapters 1 to 3
skill/11-bisect        8 commits   one of them silently breaks a setting

How to practise: watch the animation, run the Try it commands one at a time, read what Git prints, then open Check your result.

Reset the lab

Made a mess? That's the point. Put things back like this:

# 1. Leave anything half-finished (run only the one that applies)
git merge --abort
git rebase --abort
git cherry-pick --abort
git bisect reset

# 2. Put main back exactly as it is on GitHub
git switch main
git reset --hard origin/main

# 3. Put one practice branch back
git switch skill/04-rebase
git reset --hard origin/skill/04-rebase

# 4. Pushed experiments to your fork? Restore it from the original lab
git fetch upstream
git reset --hard upstream/skill/04-rebase
git push --force-with-lease origin skill/04-rebase   # only in YOUR fork

Caution

git reset --hard throws away every uncommitted change in the repository. That's exactly what you want in this lab, and exactly what you don't want in a real project unless you are sure. Starting completely fresh is always an option: delete the folder and clone again.

Skill map

Skill NN always uses branch skill/NN-… and file test-NN.txt.

# Skill You'll learn Practice branch Level Risk
01 Branch & switch Create, switch and delete branches skill/01-branching Beginner SAFE
02 Merge Fast-forward vs. merge commit skill/02-merge Beginner SAFE
03 Merge conflicts Read and resolve conflict markers skill/03-conflict Intermediate CAREFUL
04 Rebase Replay your work on the latest main skill/04-rebase Intermediate CAREFUL
05 Interactive rebase Squash, fixup, reword, drop skill/05-squash Advanced CAREFUL
06 Stash apply vs. pop skill/06-stash Beginner CAREFUL
07 Reset --soft, --mixed, --hard skill/07-reset Intermediate DANGER
08 Revert Undo safely on shared branches skill/08-revert Beginner SAFE
09 Cherry-pick Copy one commit to another branch skill/09-cherry-pick Intermediate CAREFUL
10 Reflog Recover "lost" commits and branches skill/10-reflog Advanced SAFE
11 Bisect Find the commit that introduced a bug skill/11-bisect Advanced SAFE

Risk labels

Label Meaning
SAFE Doesn't destroy or rewrite anything. Hard to hurt yourself.
CAREFUL Safe when used right, but can rewrite history, duplicate work or drop changes. Read the warning first.
DANGER Can permanently delete work. Double-check, and know your way back (see Danger zone).

The skills

01 · Branch & switch

Animation: git switch -c feature creates a new branch label at the current commit and moves HEAD onto it; new commits only move the feature branch; git switch main moves HEAD back.

A branch is just a movable label that points at a commit, and HEAD marks the branch you are on. Creating a branch is instant and free, so make one for every task: main stays clean while you experiment.

Branch skill/01-branching · File test-01.txt · Level Beginner · Risk SAFE

Try it

git switch skill/01-branching    # visit the practice branch
cat test-01.txt                  # note the extra "Hello" line
git switch main
cat test-01.txt                  # line gone: your files follow the branch

git switch -c my-first-branch    # create a branch and switch to it
echo "- my first branch commit" >> test-01.txt
git commit -am "test-01: my first branch commit"
git lg                           # my-first-branch is 1 commit ahead

git switch -                     # back to the previous branch (main)
git branch -d my-first-branch    # refused: the work isn't merged
git branch -D my-first-branch    # force-delete (it's only practice)
Check your result
  • On skill/01-branching, test-01.txt says This version lives on: skill/01-branching and has the extra hello line. On main it doesn't.
  • git branch -d printed error: the branch 'my-first-branch' is not fully merged.
  • git branch no longer lists my-first-branch.

Warning

git branch -D deletes a branch even if its commits exist nowhere else. Prefer -d, which refuses to delete unmerged work. Deleted by mistake? See Skill 10 · Reflog.


02 · Merge

Animation: case 1, fast-forward, the main label slides forward with no new commit; case 2, diverged branches, git merge creates a merge commit with two parents.

git merge brings the work of another branch into the branch you're on. If your branch hasn't moved since the other one split off, Git just slides your label forward (fast-forward). If both sides have new commits, Git creates a merge commit with two parents.

Branch skill/02-merge · File test-02.txt · Level Beginner · Risk SAFE

Try it

# Part A: fast-forward
git switch main
git switch -c my-ff
echo "- fast-forward me" >> test-02.txt
git commit -am "test-02: add a note"
git switch main
git merge my-ff               # "Fast-forward": no merge commit

# Part B: merge commit
git switch skill/02-merge     # visit once so the local branch exists
git switch main
git merge skill/02-merge      # your editor opens: save and close it
git lg                        # two histories joined by a merge commit
Check your result
  • Part A printed Fast-forward.
  • Part B printed Merge made by the 'ort' strategy.
  • git log -1 --format=%P shows two parent hashes.
  • test-02.txt has all three boxes ticked plus your note.

Tip

git merge --no-ff <branch> always creates a merge commit (keeps a feature visibly grouped). git merge --ff-only <branch> refuses anything but a fast-forward, which is a handy safety check when you want main to stay linear.


03 · Merge conflicts

Animation: main and skill/03-conflict both change the Status line; git merge stops and writes conflict markers; the line is fixed by hand; git add and git commit finish the merge.

A conflict happens when two branches change the same lines of the same file. Git refuses to guess: it pauses the merge and writes both versions into the file between conflict markers. Your job is to decide what the final text should be, then tell Git you're done.

Branch skill/03-conflict · File test-03.txt · Level Intermediate · Risk CAREFUL

Try it

git switch skill/03-conflict   # here: Status is REJECTED
git switch main                # here: Status is APPROVED
git merge skill/03-conflict    # CONFLICT (content) in test-03.txt
git status                     # "both modified: test-03.txt"

Open test-03.txt. You'll see:

<<<<<<< HEAD
Status:    APPROVED
=======
Status:    REJECTED
>>>>>>> skill/03-conflict

Replace the whole block, markers included, with the line you want, for example Status: APPROVED (after review). Then:

git add test-03.txt            # mark the conflict as resolved
git commit                     # finish (same as: git merge --continue)
Check your result
  • git log --oneline -1 shows Merge branch 'skill/03-conflict'.
  • grep -c "<<<<<<<" test-03.txt prints 0: no markers left behind.
  • git status says nothing to commit, working tree clean.

Warning

  • Never commit conflict markers. Search for <<<<<<< before git add. Many teams add a CI check for it.
  • git checkout --ours / --theirs throw one side away completely. Use them only when you're sure.
  • Lost? git merge --abort puts everything back exactly as it was before the merge.

Tip

git config --global merge.conflictStyle zdiff3 (Git 2.35+) also shows the original text between ||||||| markers, which makes conflicts much easier to understand.


04 · Rebase

Animation: feature commits D and E are replayed on top of main's newest commit C as brand-new commits D-prime and E-prime; the old commits fade away; main then fast-forwards.

git rebase moves your commits so they start from a new base. Git replays them one by one on top of the target branch, creating new commits with new hashes. The result is a straight history, as if you had started your work on today's main.

Branch skill/04-rebase · File test-04.txt · Level Intermediate · Risk CAREFUL rewrites history

Try it

git switch skill/04-rebase
git log --oneline --graph main skill/04-rebase   # main moved on
git rebase main                # "Successfully rebased and updated ..."
git log --oneline --graph -5   # your 2 commits now sit on top of main
cat test-04.txt                # main's banner + your steps 4 and 5
git status                     # "have diverged": expected (see below)

git switch main
git merge skill/04-rebase      # Fast-forward: no merge commit needed
Check your result
  • test-04.txt contains >>> Release 2.0 is live on main <<< and steps 1 to 5.
  • Before the final merge, git rev-list --count main..skill/04-rebase printed 2.
  • git status on the rebased branch said it and origin/skill/04-rebase have diverged: your copy now holds main's newer commits plus your 2 rewritten ones, while the remote still has the 2 originals. That's the rewrite: same changes, new commits.
  • The final git merge printed Fast-forward.

Warning

Rebase rewrites history. Only rebase commits that nobody else has pulled: your own local or personal feature branch.

  • Never rebase main or any branch other people build on.
  • After rebasing a branch you had already pushed, update it with git push --force-with-lease, never plain --force (see Danger zone).
  • Hit a conflict mid-rebase? Fix the file, git add it, then git rebase --continue. Want out? git rebase --abort restores everything.

Tip

git pull --rebase updates your branch without creating "Merge branch 'main' of …" noise. Make it the default with git config --global pull.rebase true.


05 · Interactive rebase

Animation: git rebase -i HEAD~4 opens a to-do list; pick is changed to fixup for three messy commits, which melt into the first one, leaving one clean commit.

git rebase -i lets you edit history before you share it: squash WIP commits together, fix messages, reorder or drop commits. Git opens a to-do list in your editor; you change the verbs, save, and Git replays the list.

Branch skill/05-squash · File test-05.txt · Level Advanced · Risk CAREFUL rewrites history

Try it

git switch skill/05-squash
git log --oneline -5     # 1 real change + wip + fix typo + wip again
git rebase -i HEAD~4     # your editor opens the to-do list

Change the to-do list (oldest commit first) to this, then save and close the editor:

pick  1a2b3c4 Add welcome message to login screen
fixup 5d6e7f8 wip
fixup 9a8b7c6 fix typo
fixup 4d3e2f1 wip again

Your hashes will be different; only change the words on the left.

git log --oneline -3     # one clean commit left
cat test-05.txt          # same content: Title, Subtitle, Button
Check your result
  • git log --oneline -2 shows Add welcome message to login screen directly on top of Add animated SVG guides….
  • test-05.txt still contains Please sign in to continue and Button: Sign in.
Verb Shortcut What it does
pick p Keep the commit as it is
reword r Keep the change, edit the message
edit e Stop so you can amend the commit
squash s Melt into the commit above, combine the messages
fixup f Melt into the commit above, discard this message
drop d Remove the commit entirely

Warning

Same rule as rebase: only rewrite commits nobody else has pulled. drop really removes the change from the branch (it's still reachable through reflog for a while). Panicking mid-way? git rebase --abort.

Tip

  • Another way to squash: git reset --soft HEAD~4 then git commit -m "Add login screen copy" (see Skill 07).
  • Pro workflow: git commit --fixup <hash> while you work, then git rebase -i --autosquash main lines everything up for you.

06 · Stash: apply vs. pop

Animation: git stash moves uncommitted changes from the working directory into the stash; apply copies them back and keeps the stash entry; pop moves them back and drops the entry.

git stash shelves your uncommitted changes and gives you a clean working directory, which is perfect when you have to switch branches mid-task. apply brings the changes back and keeps the stash entry; pop brings them back and deletes it.

Branch skill/06-stash · File test-06.txt · Level Beginner · Risk CAREFUL

Try it

git switch main
echo "- eggs" >> test-06.txt    # an uncommitted change
git switch skill/06-stash       # refused: local changes are in the way

git stash push -m "add eggs"    # shelve the change
git stash list                  # stash@{0}: On main: add eggs
git switch skill/06-stash       # works now
git switch main

git stash apply                 # the change is back...
git stash list                  # ...and the entry is still there
git restore test-06.txt         # throw the applied copy away again

git stash pop                   # the change is back...
git stash list                  # ...and the entry is gone
Check your result
  • After apply, git stash list still shows stash@{0}.
  • After pop, Git printed Dropped refs/stash@{0} (…) and git stash list is empty.
  • test-06.txt ends with - eggs.

Warning

  • git stash drop and git stash clear delete stashed work, and there is no reflog for it. Look before you drop: git stash show -p stash@{0}.
  • By default untracked (brand-new) files are not stashed. Use git stash -u.
  • Stashes are easy to forget. If the work matters for more than a few minutes, a WIP commit on a branch is safer.

Note

Bonus: stash - eggs again, switch to skill/06-stash (which already added - tea on that line) and run git stash pop. It conflicts, and Git keeps the stash entry: "The stash entry is kept in case you need it again." Clean up with git reset --hard, then git stash drop.


07 · Reset: soft, mixed, hard

Animation: the branch pointer moves from commit C back to B three times; --soft keeps the changes staged, --mixed keeps them unstaged, --hard discards them.

git reset moves your current branch to another commit. The flag decides what happens to the changes from the commits you step back over.

Branch skill/07-reset · File test-07.txt · Level Intermediate · Risk DANGER --hard deletes work

Mode Branch moves Staging area Working files Typical use
--soft yes changes kept, staged kept Redo or squash the last commit(s)
--mixed (default) yes reset, changes unstaged kept Unstage, split commits differently
--hard yes reset reset: changes deleted Throw away local work completely

Try it

git switch skill/07-reset
git log --oneline -4                     # day 1, 2, 3 (undo me)

git reset --soft HEAD~1                  # undo commit, keep change staged
git status                               # "Changes to be committed"
git reset --hard origin/skill/07-reset   # restore the branch

git reset HEAD~1                         # --mixed: kept, but unstaged
git status                               # "Changes not staged ..."
git reset --hard origin/skill/07-reset

git reset --hard HEAD~1                  # undo commit AND delete change
git status                               # "nothing to commit ..."
cat test-07.txt                          # "Day 3" is gone
git reset --hard origin/skill/07-reset   # restore again
Check your result
  • --soft: git status listed test-07.txt under Changes to be committed.
  • --mixed: Git printed Unstaged changes after reset: M test-07.txt.
  • --hard: Git printed HEAD is now at … test-07: day 2 (keep) and the file has no Day 3.

Caution

  • git reset --hard permanently deletes uncommitted changes. There is no undo for those, not even reflog. Run git status first, or git stash to be safe.
  • Safer alternative: git reset --keep HEAD~1 moves the branch like --hard but refuses if it would destroy uncommitted changes.
  • Never reset commits that are already pushed and shared; teammates still have them. Use revert instead.
  • Removed a commit by mistake? It's still recoverable with reflog.

08 · Revert

Animation: a bad commit B is already shared; git revert adds a new commit R that undoes B without rewriting history, so a normal push works.

git revert undoes a commit by creating a new commit that applies the exact opposite change. History is never rewritten, which makes it the way to undo something that's already pushed and shared.

Branch skill/08-revert · File test-08.txt · Level Beginner · Risk SAFE shared-branch friendly

Try it

git switch skill/08-revert
git log --oneline -4    # the middle commit is the BAD one
cat test-08.txt         # debug = true
git revert HEAD~1       # editor opens with "Revert ...": save, close
cat test-08.txt         # debug = false; timeout + retries are kept
git log --oneline -5    # a new "Revert ..." commit, nothing removed

Use git revert --no-edit HEAD~1 to skip the editor.

Check your result
  • The newest commit is Revert "test-08: enable debug in production (BAD)".
  • test-08.txt has debug = false and still contains timeout and retries.
  • git status says you are ahead of 'origin/skill/08-revert' by 1 commit: a normal push would work.

Warning

  • Reverting a merge commit needs -m 1 (git revert -m 1 <merge-hash>, keep the first parent's side). Later, merging that same branch again will not bring the reverted changes back. You'd have to revert the revert. Know this before you do it.
  • A revert can conflict when later commits touched the same lines. Resolve like any conflict, then git revert --continue.

Tip

On GitHub, every merged pull request has a Revert button that opens a revert PR for you.


Reset vs. revert: which undo should I use?

Animation comparing git reset --hard and git revert on an already-pushed commit C: reset removes C locally, the push is rejected and forcing it rewrites shared history; revert adds commit R that undoes C and a normal push works.

Both "undo" a commit, but in completely different ways:

git reset git revert
How it undoes Moves the branch back; commits after it disappear from the branch Adds a new commit with the opposite change
History Rewritten Preserved
Already pushed? Needs push --force, which breaks everyone who pulled A normal git push works
Uncommitted changes --hard deletes them Untouched (Git asks for a clean tree)
Use it for Local commits nobody else has Anything shared: main, release branches, pushed PRs
Risk DANGER with --hard SAFE
flowchart TD
    Q{"What do you want to undo?"} --> U["Uncommitted changes"]
    Q --> C["A commit"]
    U --> UK{"Keep them for later?"}
    UK -- "yes" --> S["git stash"]
    UK -- "no" --> RF["git restore FILE<br/>permanent!"]
    C --> P{"Already pushed or shared?"}
    P -- "yes" --> RV["git revert COMMIT<br/>safe for everyone"]
    P -- "no" --> K{"Keep the changes?"}
    K -- "yes, staged" --> RS["git reset --soft HEAD~1"]
    K -- "yes, unstaged" --> RM["git reset HEAD~1"]
    K -- "no, delete them" --> RH["git reset --hard HEAD~1<br/>careful!"]
    classDef safe fill:#1a7f37,stroke:#3fb950,color:#ffffff
    classDef warn fill:#9a6700,stroke:#d29922,color:#ffffff
    classDef danger fill:#cf222e,stroke:#f85149,color:#ffffff
    class S,RV safe
    class RS,RM warn
    class RF,RH danger
Loading

Important

Rule of thumb: reset is for your commits that only exist on your machine. revert is for everything that's already been pushed.


09 · Cherry-pick

Animation: a single fix commit E on the feature branch is copied onto main as a new commit E-prime; the unfinished commits D and F stay on feature.

git cherry-pick copies the change from one specific commit onto your current branch as a new commit. Perfect for taking a hotfix from a feature branch without dragging the unfinished work along.

Branch skill/09-cherry-pick · File test-09.txt · Level Intermediate · Risk CAREFUL duplicates commits

Try it

git switch skill/09-cherry-pick
git log --oneline -4    # experiment A, the fix, experiment B
git switch main
git cherry-pick -x skill/09-cherry-pick~1   # ~1 = the fix
cat test-09.txt         # email fixed, no experiments came along
git log -1              # "(cherry picked from commit ...)"

You can of course use the hash from git log instead: git cherry-pick -x <hash>.

Check your result
  • test-09.txt on main now says support@example.com and lists no experiments.
  • The commit message ends with (cherry picked from commit …) thanks to -x.

Warning

  • A cherry-pick creates a duplicate: same change, different hash. If the source branch is merged later, Git usually copes, but repeated picking back and forth leads to confusing conflicts. To bring over a whole branch, merge it instead.
  • Picking a commit that depends on earlier commits will conflict or produce broken code. Pick the dependencies too, oldest first.
  • Conflict? Resolve, git add, then git cherry-pick --continue, or git cherry-pick --abort to back out.

10 · Reflog

Animation: git reset --hard HEAD~2 makes commits C and D disappear from the branch; git reflog lists every position HEAD has been in; git reset --hard HEAD@{1} brings them back.

The reflog is Git's flight recorder: a local log of every position HEAD has pointed to. After a reset --hard, a botched rebase or a deleted branch, your commits still exist. The reflog tells you where they are.

Branch skill/10-reflog · File test-10.txt · Level Advanced · Risk SAFE your rescue tool

Try it

git switch skill/10-reflog
git reset --hard HEAD~2      # chapters 2 and 3 are "gone"
git log --oneline -3

git reflog -5                # HEAD@{1} = where you were before
git reset --hard HEAD@{1}    # travel back
cat test-10.txt              # all three chapters are back

Bonus: rescue a deleted branch

git switch main                  # now HEAD@{1} = old tip of the branch
git branch -D skill/10-reflog    # oops
git reflog -3                    # the old tip is still listed
git switch -c rescued HEAD@{1}   # bring it back as a new branch
Check your result
  • git reflog showed HEAD@{0}: reset: moving to HEAD~2 at the top.
  • After git reset --hard HEAD@{1}, git log --oneline -3 lists chapter 3, 2 and 1 again.
  • rescued points to the old tip of skill/10-reflog (chapter 3).

Important

  • The reflog is local only: it isn't pushed, and a fresh clone starts with an empty one.
  • Entries expire: after 90 days by default, and after 30 days for commits no longer on any branch.
  • It only records commits. Uncommitted changes destroyed by reset --hard, restore or clean are gone for good. Commit early, commit often.

11 · Bisect

Animation: eight commits, the tip is marked bad and the base good; git bisect checks out the middle commit, each good or bad answer halves the suspects, and after three tests the first bad commit is found.

Something worked last week and is broken today, and there are dozens of commits in between. git bisect runs a binary search through history: you mark one commit bad and one good, Git checks out the commit in the middle, you test it and answer good or bad. Each answer halves the suspects, so 1,000 commits take about 10 tests.

Branch skill/11-bisect · File test-11.txt · Level Advanced · Risk SAFE finish with git bisect reset

The story: somewhere in the last 8 commits, tax_rate changed from 0.20 to 2.0 (a 200% tax!). The commit messages won't tell you which one did it.

Try it: by hand

git switch skill/11-bisect
grep tax_rate test-11.txt    # tax_rate = 2.0  -> broken
git log --oneline -9         # 8 suspects, no message mentions tax

git bisect start
git bisect bad               # the current commit is broken
git bisect good HEAD~8       # 8 commits ago it was fine
# Git checks out a commit in the middle. Test it:
grep tax_rate test-11.txt
git bisect good              # if it says 0.20
git bisect bad               # if it says 2.0
# ...repeat until Git prints "<hash> is the first bad commit"

git bisect reset             # IMPORTANT: return to your branch

Try it: automatically

git bisect start HEAD HEAD~8   # bad first, then good
git bisect run grep -q "tax_rate = 0.20" test-11.txt
git bisect reset

git bisect run uses the command's exit code: 0 means good, 1–127 (except 125) means bad, and 125 means "can't test this one, skip it". Any test script or build command works.

Check your result
  • It took 3 answers: commits add support email (good), add newsletter opt-in (bad) and tweak price rounding (bad).
  • Git printed <hash> is the first bad commit for test-11: tweak price rounding, a harmless-sounding message hiding the bug.
  • After git bisect reset, git branch --show-current prints skill/11-bisect again.

Warning

  • During a bisect you're in detached HEAD: commits you make there belong to no branch and are easy to lose. Don't fix anything mid-bisect; note the culprit, git bisect reset, then fix it on a branch.
  • Always finish with git bisect reset, or you'll keep working on some random old commit.
  • One wrong good/bad answer sends the search to the wrong place. Not sure about a commit? Use git bisect skip. Check your answers with git bisect log.

Danger zone

Commands that can destroy work or hurt your teammates, and how to protect yourself:

Command What can go wrong Safety net / safer alternative
git reset --hard Deletes all uncommitted changes forever git stash first, or git reset --keep. Commits can be rescued with git reflog
git push --force Overwrites the remote branch and can delete teammates' commits git push --force-with-lease (refuses if the remote moved). Never force-push main
git rebase, git commit --amend on pushed commits Rewrites shared history; everyone else's copy now conflicts Only rewrite your own unshared branches. Use merge or revert on shared ones
git restore <file>, git checkout -- <file> Discards uncommitted edits to that file, no undo git stash, or check git diff first
git clean -fd Permanently deletes untracked files (they were never in Git!) Always preview with git clean -n first
git stash drop, git stash clear Deletes stashed work, with no reflog to fall back on git stash show -p before dropping; prefer pop
git branch -D Deletes a branch even if its work is unmerged Use git branch -d; recover with git reflog
git cherry-pick back and forth Duplicate commits and confusing conflicts later Merge whole branches; use -x to record origins
git revert of a merge commit Re-merging that branch later won't bring the changes back Plan to "revert the revert" if you merge again
Working in detached HEAD (bisect, git switch --detach) New commits belong to no branch and get lost git switch -c <branch> before committing; git bisect reset when done

Caution

Before any command in this table on a real project: run git status, make sure nothing important is uncommitted, and if in doubt create a backup branch first: git branch backup/before-cleanup. It costs nothing.

Oh no! Recovery guide

Oh no, I... Do this Shared-branch safe?
committed to the wrong branch (not pushed) git branch new-branch → git reset --keep HEAD~1 → git switch new-branch local only
need to fix the last commit message (not pushed) git commit --amend -m "Better message" local only
forgot a file in the last commit (not pushed) git add the-file → git commit --amend --no-edit local only
staged something by accident git restore --staged <file> yes
want to discard my edits to a file git restore <file> (permanent!) yes
just did a merge or rebase that went wrong git reset --hard ORIG_HEAD local only
am stuck in the middle of a merge, rebase or cherry-pick git merge --abort / git rebase --abort / git cherry-pick --abort yes
ran git reset --hard and lost commits git reflog → git reset --hard HEAD@{n} local only
deleted a branch git reflog → git switch -c <name> <hash> yes
pushed a bad commit to a shared branch git revert <hash> → git push yes
need to switch branches but my work isn't ready git stash → switch → git stash pop later yes
forgot git bisect reset git bisect reset (it's never too late) yes

Cheat sheet

Skill Everyday commands
Inspect git status · git lg · git diff · git diff --staged · git show <hash>
Branch & switch git switch -c <new> · git switch <branch> · git switch - · git branch -d <branch>
Merge git merge <branch> · git merge --no-ff <branch> · git merge --ff-only <branch> · git merge --abort
Conflicts git status · edit the file · git add <file> · git merge --continue
Rebase git rebase main · git rebase --continue · git rebase --abort · git pull --rebase
Interactive rebase git rebase -i HEAD~N · git commit --fixup <hash> · git rebase -i --autosquash main
Stash git stash push -m "msg" · git stash list · git stash apply · git stash pop · git stash -u
Reset git reset --soft HEAD~1 · git reset HEAD~1 · git reset --hard HEAD~1 · git reset --keep HEAD~1
Revert git revert <hash> · git revert --no-edit <hash> · git revert -m 1 <merge-hash>
Cherry-pick git cherry-pick <hash> · git cherry-pick -x <hash> · git cherry-pick --continue · git cherry-pick --abort
Reflog git reflog · git reset --hard HEAD@{n} · git switch -c rescued HEAD@{n}
Bisect git bisect start · git bisect bad · git bisect good <ref> · git bisect run <cmd> · git bisect reset
Remote git fetch · git pull --rebase · git push -u origin <branch> · git push --force-with-lease

Golden rules

  1. Run git status before and after every command. It tells you where you are and what Git expects next.
  2. Commit early, commit small. Small commits are easy to review, move, revert and bisect. Only committed work can be rescued.
  3. Never rewrite shared history. No rebase, reset or --amend on commits others have pulled. Use revert.
  4. Never --force, always --force-with-lease, and only on your own branches.
  5. Make a backup branch before anything scary: git branch backup/<name>.
  6. Don't panic. Committed work is almost never lost for weeks: git reflog.

Team workflow on GitHub

Everything above works locally. To practise the GitHub side (push, pull requests, reviews), use your fork:

  1. Fork this repository. In the fork dialog, untick "Copy the main branch only" so you get all skill/* branches (or run gh repo fork aimagician/git-testing --clone, which copies every branch).
  2. Create a branch: git switch -c practice/<your-name>.
  3. Make small, focused commits to any test-XX.txt.
  4. Stay up to date: git fetch origin, then git rebase origin/main.
  5. Push: git push -u origin practice/<your-name>. After a rebase: git push --force-with-lease.
  6. Open a pull request in your fork (from practice/<your-name> into your fork's main), ask a teammate to review, answer comments with new commits (or --fixup + autosquash).
  7. Merge with Squash and merge for a tidy history, then delete the branch.
  8. Broke something after merging? Use the Revert button on the merged pull request.

Important

main and skill/* are protected: pushes, force-pushes and deletions there are blocked for everyone except the maintainer, because they are everyone's starting point. Team members with write access: use practice/<your-name>/... branches.

Test yourself

1. You created feature from main and made two commits. Which branch moved?

Only feature. A new commit moves only the branch HEAD is on; main still points to the old commit.

2. When does git merge fast-forward?

When your branch has no commits the other branch lacks (it's an ancestor), so Git can simply move the label forward. No merge commit is created.

3. What sits between <<<<<<<, ======= and >>>>>>>?

Your version (HEAD) above =======, the incoming branch's version below it. You write the final text and delete all three markers.

4. Why do you need --force-with-lease after rebasing a branch you already pushed?

Rebase creates new commits, so the remote still has the old ones and a normal push is rejected. --force-with-lease overwrites it only if nobody else pushed in the meantime.

5. squash vs. fixup in an interactive rebase?

Both melt a commit into the one above it. squash lets you combine and edit the messages; fixup throws the fixup's message away.

6. git stash apply vs. git stash pop?

Both bring the changes back. apply keeps the stash entry; pop deletes it (unless there's a conflict, in which case the entry is kept).

7. Which reset mode keeps your changes staged, and which one deletes them?

--soft keeps them staged. --hard deletes them. --mixed (the default) keeps them unstaged.

8. A bad commit is already on the shared main. Reset or revert?

git revert. It adds a new commit, so nobody's history breaks and a normal push works.

9. Does git cherry-pick move the original commit?

No. It copies the change as a new commit with a new hash; the original stays on its branch.

10. You ran git reset --hard HEAD~3 by mistake. How do you get the commits back?

git reflog to find where you were (usually HEAD@{1}), then git reset --hard HEAD@{1}.

11. Roughly how many tests does git bisect need for 1,000 commits, and what must you never forget?

About 10 (log₂ 1000 ≈ 10). And always finish with git bisect reset.

Contributing

To keep the lab clean and safe:

  • Issues and pull requests on this repository are limited to collaborators.
  • main and skill/* are protected branches (no pushes, force-pushes or deletions except by the maintainer).
  • Secret scanning push protection blocks pushes that contain tokens or keys.

You don't need any access to learn: fork it and make it your own. Spotted a mistake or have an idea for skill #12? Get in touch with @aimagician.


Built for my team, and for anyone who wants to get really good at Git.
Maintained by @aimagician · Animations are plain SVG + CSS, no JavaScript

About

Hands-on Git lab: 11 essential skills (branch, merge, conflicts, rebase, squash, stash, reset, revert, cherry-pick, reflog, bisect) with animated guides, practice branches and risk labels.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors