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/arrow-up-right

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:

circle-check

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:

  1. Identifies commits on bugFix that aren't on main

  2. Saves these commits' diffs temporarily

  3. Resets bugFix to point to main's tip

  4. 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.0 to look at a release

  • Running 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 commit

  • main^ - parent of main's tip

  • HEAD^^ - 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~3 goes back 3 commits along the main line

  • ^ can specify which parent: HEAD^2 goes 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:

  1. For each specified commit, compute the diff from its parent

  2. Apply that diff to current HEAD

  3. 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 pick to:

    • reword: Edit commit message

    • edit: Pause to amend commit

    • squash: Combine with previous commit

    • fixup: Combine with previous, discard message

    • drop: 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:

  1. Use interactive rebase to move the target commit to the tip

  2. Amend it

  3. 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:

  1. Start fresh from main

  2. Cherry-pick commits one at a time

  3. Amend when you reach the commit that needs changes

  4. 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.0

  • Annotated: 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 production

  • git describe uses tags to create version strings

  • Tags 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:

  1. Identify which commits need to be on which branch

  2. Use cherry-pick to build each branch correctly

  3. Use branch -f to 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:

  1. A copy of the repository in a new directory

  2. Remote-tracking branches (e.g., origin/main) showing remote state

  3. A remote named "origin" pointing to the source URL

  4. 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 fetch and pull

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:

  1. Contacts the remote

  2. Downloads commits the remote has that you don't

  3. Updates origin/main (and other remote-tracking branches)

  4. Does NOT change your main branch

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:

  1. Uploads commits the remote doesn't have

  2. Updates the remote's branch pointer

  3. Updates your local origin/main to 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:

  1. Create a feature branch

  2. Push the feature branch (allowed)

  3. Reset local main to match origin/main (removing your commits from main)

  4. 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:

  1. Fetch to get remote updates

  2. Rebase each branch in order onto the new base

  3. 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 pull on side fetches and integrates o/main

  • git push on side pushes to o/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

Command
Purpose
Safe for Shared?

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