Learn Git Branching - Walkthrough
Great interactive game tutorial for learning Git from the beginners to more advanced users.
Link to "Learn Git Branching": https://learngitbranching.js.org/
This guide provides solutions to all levels with detailed explanations of what each command does and why it's used in that context.

Game is divided into two main sections: Main and Remote.
Each of these sections has several parts divided into levels - like in below screen:

I strongly advise to go through each part and each level step by step to learn basic first and then go to more advanced topics.
Have fun!
MAIN SECTION
Introduction Sequence
Level 1: Introduction to Git Commits
Goal: Create two commits to complete the level.
Explanation:
git commit creates a new commit object in the repository. A commit is a snapshot of all tracked files at a specific point in time.
Why two commits? This level teaches that Git history is built by chaining commits together. Each commit points to its parent, creating a linked list of snapshots. The first commit establishes a baseline, and the second shows how history grows linearly.
Key concept: Commits are immutable snapshots, not diffs. Git stores the complete state of files, using deduplication to save space.
Level 2: Branching in Git
Goal: Create a new branch named bugFix and switch to it.
Alternative (Git 2.23+):
Explanation:
git branch bugFix creates a new branch pointer called "bugFix" pointing to the current commit. Branches in Git are lightweight - they're simply 41-byte files containing a commit SHA.
git checkout bugFix moves HEAD to point to the bugFix branch. HEAD is a special pointer that indicates "where you are now" in the repository.
Why separate commands? Understanding that branch creation and switching are distinct operations helps when you need to create a branch without switching (e.g., git branch backup-before-rebase to mark a point before a risky operation).
Why branches are cheap: Since branches are just pointers, creating hundreds of branches has negligible cost. This encourages experimentation - create a branch, try something, delete if it fails.
Level 3: Merging in Git
Goal: Create a branch, make commits on both branches, then merge.
Explanation:
git checkout -b bugFix creates and switches to bugFix in one command. The -b flag means "create branch if it doesn't exist."
git commit (on bugFix) advances the bugFix branch pointer to a new commit.
git checkout main switches back to main branch. The bugFix branch pointer stays where it was.
git commit (on main) advances main to a new commit. Now main and bugFix have diverged - they share a common ancestor but have different tips.
git merge bugFix creates a merge commit on main that has TWO parents: the previous main tip and the bugFix tip. This merge commit combines the changes from both branches.
Why merge creates a new commit: The merge commit represents the point where two lines of development came together. It preserves the complete history of both branches, making it clear that parallel work happened.
When to use merge: Use merge when you want to preserve the fact that work happened on a separate branch, especially for shared/public branches where rewriting history would affect others.
Level 4: Rebase Introduction
Goal: Create a branch, make commits, then rebase onto main.
Explanation:
The first four commands create the same diverged state as the merge level.
git checkout bugFix switches to the bugFix branch before rebasing.
git rebase main performs these steps internally:
Identifies commits on bugFix that aren't on main
Saves these commits' diffs temporarily
Resets bugFix to point to main's tip
Replays each saved diff as a NEW commit on top of main
Why commits change: The rebased commits have different SHA hashes because a commit's hash is computed from its content AND its parent. Since the parent changed (now it's main's tip instead of the original branch point), the hash must change.
Why rebase instead of merge: Rebase produces a linear history without merge commits. This makes git log easier to read and git bisect more straightforward. The history looks as if the feature was developed after main's latest changes, even though it was developed in parallel.
Critical rule: Never rebase commits that have been pushed to a shared repository. Rebasing rewrites history, and if others have based work on the original commits, their history will diverge from yours, causing confusion and merge conflicts.
Ramping Up
Level 1: Detach HEAD
Goal: Detach HEAD by checking out a specific commit.
Explanation:
git checkout C4 moves HEAD directly to commit C4 instead of to a branch. This is called "detached HEAD" state.
Why HEAD normally points to a branch: When HEAD points to a branch (e.g., main), and you make a commit, the branch pointer moves forward automatically. Your new commit is "on" that branch.
Why detached HEAD is different: When HEAD points directly to a commit, new commits you create won't be on any branch. If you switch away, those commits become orphaned and may eventually be garbage collected.
When to use detached HEAD:
Examining old code:
git checkout v1.0to look at a releaseRunning tests on specific commits
Starting a new branch from a historical point:
git checkout abc123 && git checkout -b new-feature
Warning: The visualization shows "C4" but in real Git you'd use the actual SHA hash (e.g., git checkout 7d3b2c1) or a tag/reference.
Level 2: Relative Refs (^)
Goal: Navigate to the parent of HEAD using relative references.
Explanation:
^ means "parent of." bugFix^ refers to the commit that is the parent of wherever bugFix points.
Why relative refs exist: Typing full SHA hashes is tedious and error-prone. Relative refs let you navigate based on relationships:
HEAD^- parent of current commitmain^- parent of main's tipHEAD^^- grandparent (parent of parent)bugFix^2- second parent (only meaningful for merge commits)
Merge commits have multiple parents: When you merge branch B into branch A, the merge commit's first parent (^1 or just ^) is from branch A, and second parent (^2) is from branch B.
Real-world usage:
Level 3: Relative Refs (~)
Goal: Move branch pointer using relative refs and branch forcing.
Explanation:
git branch -f main C6 forcibly moves the main branch pointer to commit C6. The -f (force) flag is required because main already exists; without it, Git would refuse to overwrite.
git checkout HEAD~1 moves HEAD one commit back. ~1 means "one generation back." ~n is shorthand for applying ^ n times: HEAD~3 equals HEAD^^^.
git branch -f bugFix HEAD~1 moves bugFix to one commit before current HEAD position.
Difference between ^ and ~:
~only follows first parents:HEAD~3goes back 3 commits along the main line^can specify which parent:HEAD^2goes to the second parent of a merge
Why force-move branches: This is useful for:
Fixing mistakes: move branch back before a bad commit
Reorganizing: point a branch at a different commit
After rebase conflicts: manually set branch position
Warning: Force-moving branches that others have pulled will cause problems. Only do this with local branches or when you're certain no one else is affected.
Level 4: Reversing Changes in Git
Goal: Use reset and revert to undo commits appropriately.
Explanation:
git reset HEAD~1 moves the current branch pointer back one commit. The commit that was at HEAD is now orphaned (not on any branch). This effectively "undoes" the commit by removing it from the branch's history.
git checkout pushed switches to a branch that simulates commits that have been shared with others.
git revert HEAD creates a NEW commit that undoes the changes from HEAD. Unlike reset, revert doesn't remove any commits - it adds a commit that applies the inverse diff.
When to use reset:
Commit hasn't been pushed yet
Working alone on a branch
Want to completely eliminate commit from history
When to use revert:
Commit has been pushed/shared
Working with others who have the commit
Need to maintain consistent history across all clones
Why this matters: If you reset a pushed commit and force-push, everyone who pulled that commit now has "orphaned" commits. They'll see conflicts or duplicate commits when they try to sync. Revert is safe because it only adds history, never removes it.
Reset modes (not shown in game but important):
--soft: Move branch, keep changes staged--mixed(default): Move branch, unstage changes, keep in working directory--hard: Move branch, discard all changes (DANGEROUS)
Moving Work Around
Level 1: Cherry-pick Intro
Goal: Copy specific commits to the current branch.
Explanation:
git cherry-pick C3 C4 C7 copies commits C3, C4, and C7 onto the current branch in that order. Each original commit is re-applied as a new commit with:
Same changes (diff)
Same commit message
Different SHA (because different parent)
Different timestamp (when cherry-pick happened)
Why cherry-pick exists: Sometimes you need specific changes without merging an entire branch:
Hotfix on production: A critical bug fix on develop needs to go to main immediately
Wrong branch: You accidentally committed to main instead of feature branch
Selective backport: Only some commits from a feature are ready for release
How cherry-pick works internally:
For each specified commit, compute the diff from its parent
Apply that diff to current HEAD
Create new commit with same message
Potential issues:
If cherry-picked commit depends on earlier commits not being picked, you may get conflicts
If you later merge the source branch, Git usually handles the duplicates intelligently, but history can look confusing
Real-world example:
Level 2: Interactive Rebase Intro
Goal: Reorder, drop, or modify commits using interactive rebase.
Then in the interactive editor, reorder commits as: C3, C5, C4 (omitting one commit).
Explanation:
git rebase -i HEAD~4 opens an interactive editor showing the last 4 commits. You can:
Reorder lines to change commit order
Delete lines to drop commits entirely
Change command from
pickto:reword: Edit commit messageedit: Pause to amend commitsquash: Combine with previous commitfixup: Combine with previous, discard messagedrop: Remove commit
Why interactive rebase matters:
Development is messy. You might have:
Before pushing, clean this up:
Result: One clean commit "Add login feature" with all the changes.
Best practice: Rebase to clean up local work before pushing. Never interactive rebase commits that have been shared.
A Mixed Bag
Level 1: Grabbing Just 1 Commit
Goal: Get a specific commit from a branch without merging everything.
Explanation:
This level reinforces cherry-pick for extracting single commits.
Scenario: The bugFix branch has multiple commits, but only C4 contains the actual fix. Other commits might be debugging code, experiments, or unfinished work.
git checkout main ensures you're on the target branch.
git cherry-pick C4 copies only C4's changes to main.
Why not merge? Merging would bring ALL commits from bugFix, including the unwanted ones. Cherry-pick is surgical - it takes exactly what you specify.
Real-world scenario: Release branch needs one specific bugfix from develop, but develop has other changes not ready for release.
Level 2: Juggling Commits
Goal: Reorder commits to make an amendment, then restore original order.
Explanation:
--amend modifies the most recent commit, but what if you need to modify an older commit?
Strategy:
Use interactive rebase to move the target commit to the tip
Amend it
Use interactive rebase again to restore original order
Why this complexity? Git commits are immutable. You can't edit a commit in the middle of history directly. By reordering, you temporarily make the target commit the tip, which CAN be amended.
Simpler alternative for this specific case:
Key insight: Interactive rebase's edit command exists precisely for this use case.
Level 3: Juggling Commits #2
Goal: Similar to above but using cherry-pick instead of rebase.
Explanation:
This demonstrates an alternative approach using cherry-pick:
Start fresh from main
Cherry-pick commits one at a time
Amend when you reach the commit that needs changes
Continue cherry-picking remaining commits
When cherry-pick approach is better:
When you need to be very selective about which commits to include
When the branch history is complex with merges
When you want to completely rebuild a sequence
When rebase approach is better:
When preserving most of the branch structure
When there are many commits
When you just need to reorder or squash
Level 4: Git Tags
Goal: Create tags at specific commits.
Explanation:
git tag v1 C1 creates a tag named "v1" pointing to commit C1.
Tags are similar to branches but with key differences:
Tags don't move: A tag always points to the same commit
Tags are for marking: Releases, milestones, important points
Branches move: When you commit on a branch, the branch pointer advances
Types of tags (in real Git):
Lightweight: Just a pointer, like
git tag v1.0Annotated: Full object with message, tagger, date:
git tag -a v1.0 -m "Release 1.0"
Why tags matter for DevOps:
CI/CD pipelines often trigger on tags: push
v*→ deploy to productiongit describeuses tags to create version stringsTags provide stable reference points: "deploy the v2.3.1 tag" is unambiguous
Best practice: Use annotated tags for releases (git tag -a v1.0.0 -m "Release 1.0.0") and lightweight tags for temporary/personal markers.
Level 5: Git Describe
Goal: Understand git describe output.
Explanation:
git describe outputs: <tag>_<numCommits>_g<hash>
For example: v1_2_gC4 means:
Nearest ancestor tag: v1
Number of commits since that tag: 2
Current commit abbreviated hash: C4 (the 'g' prefix means "git")
Why git describe is useful:
Automatic version strings in builds
Identifying exactly which code is running
Communicating specific commits without full SHA
Real-world usage:
Flags:
--tags: Use any tag, not just annotated--always: Fall back to commit hash if no tags--dirty: Append "-dirty" if working directory has changes
Advanced Topics
Level 1: Rebasing over 9000 times
Goal: Complex rebasing across multiple branches.
Explanation:
git rebase main bugFix is shorthand for:
This chain rebases each branch onto the previous one, creating a linear sequence.
Why chain rebases? In complex workflows:
main → feature → sub-feature → experimental
To linearize all work before merging to main
Order matters: Rebase the branches in dependency order (oldest/base first), or you'll create conflicts.
Level 2: Multiple Parents
Goal: Navigate merge commit parents using ^ and ~.
Explanation:
Breaking down HEAD~^2~:
HEAD~- Go to first parent (one commit back)^2- Go to second parent (the merged branch)~- Go to first parent of that
When you need this: Navigating complex merge histories to find specific commits.
Practical example:
Level 3: Branch Spaghetti
Goal: Untangle complex branch relationships using rebase and cherry-pick.
Explanation:
When branches are tangled:
Identify which commits need to be on which branch
Use cherry-pick to build each branch correctly
Use
branch -fto reposition branch pointers
Key insight: You can always reconstruct branches as long as the commits exist. Cherry-pick lets you "rebuild" any branch with exactly the commits you want.
REMOTE SECTION
Push & Pull -- Git Remotes
Level 1: Clone Intro
Goal: Understand what happens when you clone.
Explanation:
git clone creates:
A copy of the repository in a new directory
Remote-tracking branches (e.g.,
origin/main) showing remote stateA remote named "origin" pointing to the source URL
Local branches tracking their remote counterparts
Remote-tracking branches (origin/main):
Show where the remote's branches were at last fetch/pull
Cannot be directly committed to
Updated by
fetchandpull
Why remote-tracking branches exist: They provide a "bookmark" of the remote's state, letting you see how your local work compares to the remote without network access.
Level 2: Remote Branches
Goal: Understand remote branch behavior.
Explanation:
Committing on o/main (remote-tracking branch) results in detached HEAD. You can make commits, but they won't update o/main.
Why? Remote-tracking branches reflect the REMOTE's state. Only fetching from the remote should update them. This prevents local changes from corrupting your understanding of the remote's state.
Level 3: Git Fetchin'
Goal: Fetch updates from remote.
Explanation:
git fetch downloads new commits from the remote and updates remote-tracking branches. It does NOT modify your local branches.
What fetch does:
Contacts the remote
Downloads commits the remote has that you don't
Updates
origin/main(and other remote-tracking branches)Does NOT change your
mainbranch
Why fetch is safe: Since it doesn't touch your local branches, you can always fetch without fear of conflicts or losing work. You can then decide how to integrate the changes.
Level 4: Git Pullin'
Goal: Pull changes from remote.
Explanation:
git pull = git fetch + git merge origin/<branch>
It fetches remote changes AND merges them into your current branch.
Why pull combines two operations: For convenience. Most of the time when you fetch, you want to integrate those changes. Pull does both in one command.
Potential issues: If you have local commits, pull creates a merge commit. For cleaner history, many prefer git pull --rebase.
Level 5: Faking Teamwork
Goal: Simulate remote changes.
Explanation:
git fakeTeamwork 2 is a learngitbranching-specific command that simulates colleagues pushing 2 commits to the remote.
This creates the common scenario: while you worked locally, others pushed to the remote. Your local branch and remote have diverged.
The solution - pull: Fetches the remote commits and merges them with your local work.
Level 6: Git Pushin'
Goal: Push local commits to remote.
Explanation:
git push uploads your local commits to the remote and updates the remote's branch pointer.
Prerequisites for push:
Your local branch must be ahead of or equal to the remote
If the remote has commits you don't have, push is rejected (you must pull first)
What push does:
Uploads commits the remote doesn't have
Updates the remote's branch pointer
Updates your local
origin/mainto match
Level 7: Diverged History
Goal: Handle diverged local and remote branches.
Explanation:
When both you and the remote have new commits, you must reconcile before pushing.
git pull --rebase fetches and rebases your commits on top of the remote's commits (instead of merging).
Why rebase for diverged history?
Creates linear history without merge commits
Your commits appear "after" the remote's commits
Cleaner
git log
Alternative (merge):
Both work; rebase is often preferred for feature branches, merge for long-running branches.
Level 8: Locked Main
Goal: Work around push rejection on protected main.
Explanation:
Many repositories protect main - you cannot push directly to it. Work must go through pull requests.
Workflow:
Create a feature branch
Push the feature branch (allowed)
Reset local main to match origin/main (removing your commits from main)
Create a pull request from feature to main
Why protected branches? Prevents accidental pushes, enforces code review, enables CI checks before merge.
To Origin and Beyond -- Advanced Git Remotes
Level 1: Push Main
Goal: Push when not on the main branch.
Explanation:
You can update and push main even when working on other branches.
Strategy:
Fetch to get remote updates
Rebase each branch in order onto the new base
Push main when it's updated
Level 2: Merging with Remotes
Goal: Use merge instead of rebase with remotes.
Explanation:
Alternative to rebasing - merge each branch into main. Creates merge commits but preserves all branch history.
When to merge vs rebase with remotes:
Merge: When history of parallel development matters
Rebase: When you want clean, linear history
Level 3: Remote Tracking
Goal: Understand and set up remote tracking.
Explanation:
git checkout -b side o/main creates branch "side" that tracks o/main. This means:
git pullon side fetches and integrateso/maingit pushon side pushes too/main
Setting tracking explicitly:
Why custom tracking? Sometimes you want a local branch with a different name than the remote branch, or you want to track a different remote branch.
Level 4: Git Push Arguments
Goal: Push to specific remote branches.
Explanation:
git push origin main explicitly specifies:
Remote: origin
Branch: main (both local source and remote destination)
Full syntax: git push <remote> <local-branch>:<remote-branch>
Level 5: Git Push Arguments -- Expanded
Goal: Push with refspec syntax.
Explanation:
git push origin main^:foo pushes the parent of main to remote's foo branch.
Refspec format: <src>:<dst>
src: What to push (can use any reference: branch, tag, SHA, relative ref)
dst: Where to push it on the remote
Power of refspecs: You can push any commit(s) to any remote branch, enabling complex workflows.
Level 6: Fetch Arguments
Goal: Fetch specific branches.
Explanation:
git fetch origin main fetches only the main branch from origin.
Why selective fetch?
Faster than fetching everything
Useful in large repositories with many branches
CI/CD systems often fetch only needed branches
Full syntax: git fetch <remote> <remote-branch>:<local-branch>
Level 7: Source of Nothing
Goal: Delete remote branches using empty source.
Explanation:
git push origin :foo pushes "nothing" to foo, which deletes the remote branch.
git fetch origin :bar creates a new local branch bar (fetches "nothing" but creates the destination).
Why this syntax? The refspec <src>:<dst> with empty <src> means "no source" - for push, this deletes; for fetch, this creates an empty branch.
Level 8: Pull Arguments
Goal: Pull with specific arguments.
Explanation:
git pull origin main = git fetch origin main + git merge origin/main
Explicitly pulling a specific branch is useful when:
You want to pull a branch different from your tracking branch
You're pulling into a branch that doesn't track any remote
Full syntax:
Quick Reference
git commit
Create snapshot
Yes
git branch X
Create branch pointer
Yes
git checkout X
Switch to X
Yes
git merge X
Combine X into current
Yes
git rebase X
Replay commits onto X
No (rewrites history)
git cherry-pick X
Copy specific commits
Yes (creates new)
git reset
Move branch pointer back
No (removes commits)
git revert
Create undo commit
Yes
git fetch
Download from remote
Yes
git pull
Fetch + merge
Yes
git push
Upload to remote
Yes
Last updated