Learning Objectives

By the end of this module, you will be able to:

  1. Describe the commit graph as a directed acyclic graph (DAG) with backward-pointing parent references
  2. Explain why the tree/branch metaphor is "leaky" and how the graph model is more accurate
  3. Demonstrate that a branch is nothing more than a movable pointer to a commit
  4. Explain what HEAD is, how it moves, and when it becomes "detached"
  5. Distinguish between local branches, remote-tracking branches, and tags

1. The Commit Graph

In Module 3, you learned that every commit object points to a parent commit — the commit that came immediately before it. This parent reference is how Git records history. But history isn't a straight line — it branches and merges. The resulting structure is a directed acyclic graph (DAG).

From Linked Lists to Graphs

A simple linear history is a singly linked list. Each commit points back to exactly one parent:

    ○ ◄── ○ ◄── ○ ◄── ○
    C1     C2     C3     C4

    Time flows left to right →
    Pointers point right to left ←

Why do pointers point backwards? Because commits are immutable. When C2 is created, C1 already exists — so C2 can record C1's hash as its parent. But C1 can't be modified to record C2 as its child. Immutability means references always point to the past.

When two developers work in parallel and later merge, the graph gets a branch and a merge point:

           ○ ◄── ○           (feature branch)
          C3     C4
         ╱         ╲
    ○ ◄── ○          ○      (merge commit — two parents)
    C1     C2         C6
                    ╱
           ○ ◄── ○           (another branch)
          C3'    C5

The merge commit (C6) has two parents — that's how Git knows a merge occurred. No metadata flag, no label. Two parents = merge.

It's a DAG, Not a Tree

The word "branch" comes from the tree metaphor, but Git's history is not a tree. Trees cannot have cycles or convergence points. Git's commit history is a directed acyclic graph:

  • Directed — edges (parent references) have a direction (child → parent)
  • Acyclic — you can never follow parent references and arrive back where you started
  • Graph — nodes can have multiple incoming edges (merge commits) and multiple outgoing edges (branch points)
Tree (no convergence):          DAG (convergence allowed):

        ○                              ○
       / \                            / \
      ○   ○                          ○   ○
     / \                              \ /
    ○   ○                              ○
                                      / \
                                     ○   ○

The tree metaphor breaks down the moment you merge. After that, you're working with a graph. Understanding this matters because many Git operations — log traversal, rebase, merge-base calculation — are graph algorithms, not tree algorithms.

Anatomy of a Commit in the Graph

Every commit node in the graph carries:

┌─────────────────────────────────┐
│ Commit: a1b2c3d                 │
├─────────────────────────────────┤
│ tree:    8f3e2a1  (snapshot)    │
│ parent:  e4f5a6b  (prev commit)│
│ parent:  7c8d9e0  (2nd parent  │
│                    if merge)    │
│ author:  Jane Doe               │
│ date:    2024-01-15 10:30:00    │
│ message: "Merge feature into…"  │
└─────────────────────────────────┘
  • Zero parents — the very first commit (root commit). A repository can have multiple root commits (rare, but happens with git merge --allow-unrelated-histories).
  • One parent — a normal commit.
  • Two parents — a merge commit. The first parent is the branch you were on; the second parent is the branch you merged in.
  • Three+ parents — an octopus merge (rare, advanced).

Reading the Graph with git log

# Linear log
git log --oneline
# a1b2c3d Merge feature-auth into main
# 7c8d9e0 Add login page
# e4f5a6b Add signup form
# 3d4e5f6 Initial commit
 
# Graph visualization
git log --oneline --graph --all
# *   a1b2c3d Merge feature-auth into main
# |\
# | * 7c8d9e0 Add login page
# | * b2c3d4e Add auth middleware
# |/
# * e4f5a6b Add signup form
# * 3d4e5f6 Initial commit

The --graph flag draws ASCII art showing the DAG structure. The --all flag includes commits from all branches, not just the current one. This is one of the most useful commands in Git.


2. What a Branch Really Is

This is the single most important concept in Git's branching model, and it's surprisingly simple:

A branch is a pointer. A 40-character text file that contains the hash of a commit.

That's it. A branch is not a container of commits. It's not a copy of your files. It's not a timeline or a history line. It is a pointer — a label stuck on one commit.

Proof

cat .git/refs/heads/main
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

The file .git/refs/heads/main contains one line: the SHA-1 hash of the commit that main currently points to. Creating a branch means creating one of these tiny files. Deleting a branch means deleting it.

Branches Are Cheap

Because a branch is just a 41-byte file (40 hex chars + newline), creating a branch is nearly instantaneous and costs almost nothing. This is revolutionary compared to SVN, where creating a branch meant copying the entire directory tree.

# Create a branch — Git creates a 41-byte file
git branch feature-login
 
# That's equivalent to:
# echo "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" > .git/refs/heads/feature-login

Branches Move When You Commit

The special thing about a branch isn't that it exists — it's that it moves forward automatically when you commit (if HEAD points to it). This is what makes branches useful for tracking ongoing work:

Before commit:

    ○ ◄── ○ ◄── ○
    C1     C2     C3
                   ▲
                  main
                   ▲
                  HEAD


After "git commit":

    ○ ◄── ○ ◄── ○ ◄── ○
    C1     C2     C3     C4
                          ▲
                         main    ← moved forward
                          ▲
                         HEAD

The commit was created, and main automatically moved to point to it. HEAD still points to main.

Creating a Branch Doesn't Switch to It

git branch feature-login    # creates the pointer, HEAD stays where it is
git switch feature-login     # moves HEAD to point to feature-login

Or in one step:

git switch -c feature-login  # create + switch
git checkout -b feature-login  # older syntax, same effect

Visualizing Multiple Branches

    ○ ◄── ○ ◄── ○ ◄── ○
    C1     C2     C3     C4
                   ▲      ▲
                  main   feature-login
                          ▲
                         HEAD

Both main and feature-login point to commits in the same graph. There's no separate container or copy. Commits aren't "on" a branch — rather, a branch points to a commit, and all ancestors of that commit are reachable from the branch.

What Happens When Branches Diverge

# On feature-login, make two commits:
git switch feature-login
# ... make changes, commit twice ...
 
# Switch back to main, make a commit:
git switch main
# ... make changes, commit ...
                    ○ ◄── ○          feature-login
                   C4     C5
                  ╱
    ○ ◄── ○ ◄── ○
    C1     C2     C3 ◄── ○          main ← HEAD
                          C6

Main and feature-login have diverged — they share C1–C3 as common ancestors, but each has commits the other doesn't. This is normal and expected. Merging (Module 7) or rebasing (Module 9) will bring them back together.


3. HEAD — Your Current Position

HEAD is a special pointer that tells Git where you are right now. It determines:

  • Which branch's history git log shows by default
  • Which commit new commits will be attached to
  • Which branch pointer moves forward when you commit

Normal State: HEAD Points to a Branch

cat .git/HEAD
# ref: refs/heads/main

HEAD doesn't point to a commit — it points to a branch name, which in turn points to a commit. This indirection is what allows the branch to move when you commit.

    HEAD ──► main ──► C4

    When you commit:
    HEAD ──► main ──► C5 (new commit, main moved, HEAD followed)

How HEAD Moves

ActionWhat Happens to HEAD
git switch mainHEAD → refs/heads/main
git switch featureHEAD → refs/heads/feature
git commitHEAD stays on current branch; branch pointer moves forward
git checkout <hash>HEAD → commit hash directly (detached!)
git reset --hard <hash>Current branch moves to <hash>; HEAD follows

The Dance of Commit

When you run git commit, this is the sequence:

  1. Git reads HEAD → finds refs/heads/main → finds the commit hash
  2. That hash becomes the parent of the new commit
  3. Git creates the new commit object (tree + parent + metadata)
  4. Git updates refs/heads/main to point to the new commit
  5. HEAD still points to refs/heads/main — but main now points to the new commit

HEAD itself didn't change (it still says ref: refs/heads/main). But the commit HEAD resolves to did change, because main moved.


4. Detached HEAD State

Normally, HEAD points to a branch name. But you can point HEAD directly at a commit:

git checkout a1b2c3d     # checkout a specific commit by hash
git checkout v1.0.0       # checkout a tag
git checkout HEAD~3       # checkout 3 commits ago

Git will warn you:

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

What Detached HEAD Looks Like

cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0    ← raw hash, not a ref
Normal:                         Detached:

HEAD ──► main ──► C4            HEAD ──► C2 (directly)

    ○ ◄── ○ ◄── ○ ◄── ○            ○ ◄── ○ ◄── ○ ◄── ○
    C1     C2     C3     C4        C1     C2     C3     C4
                          ▲                ▲              ▲
                         main             HEAD           main
                          ▲
                         HEAD

Why Detached HEAD Is "Dangerous"

If you make commits in detached HEAD state, no branch pointer moves to track them:

    ○ ◄── ○ ◄── ○ ◄── ○    ← orphaned commits
    C2     C5     C6     C7
           ▲
          HEAD

    ○ ◄── ○ ◄── ○ ◄── ○
    C1     C2     C3     C4
                          ▲
                         main

If you then git switch main, HEAD moves to main, and commits C5–C7 become unreachable — no branch points to them. They'll eventually be garbage collected (after ~2 weeks). They're not lost immediately — you can find them with git reflog — but they're in danger.

When Detached HEAD Is Useful

  1. Inspecting old commits — you want to see the project at a specific point in history without creating a branch
  2. Running old code — testing whether a bug existed in a previous version
  3. Temporary experiments — try something without committing to a branch; switch back to discard

Recovering from Detached HEAD

If you made commits in detached HEAD state and want to keep them:

# While still in detached state, create a branch:
git switch -c my-experiment
 
# Now those commits are safe — a branch points to them

If you already switched away:

git reflog
# Find the hash of your detached commit
git switch -c my-experiment <hash>

5. Remote-Tracking Branches

When you clone a repository, Git creates remote-tracking branches — read-only pointers that represent the state of branches on the remote server.

git branch -a
# * main                        ← local branch
#   feature-login               ← local branch
#   remotes/origin/main         ← remote-tracking branch
#   remotes/origin/feature-login ← remote-tracking branch

How They Work

          Remote (GitHub)              Local Machine
        ┌──────────────┐            ┌──────────────────────┐
        │ main ──► C4  │            │ main ──► C4          │ ← local branch
        │              │            │ origin/main ──► C4   │ ← remote-tracking
        └──────────────┘            └──────────────────────┘

Remote-tracking branches:

  • Are stored at refs/remotes/<remote>/<branch> (e.g., refs/remotes/origin/main)
  • Are updated by git fetch (and git pull, which includes a fetch)
  • Cannot be checked out directly — if you try, you get a detached HEAD
  • Represent Git's last known state of the remote — they may be stale if you haven't fetched recently

The Fetch-Then-Compare Pattern

git fetch origin                    # update all remote-tracking branches
git log main..origin/main --oneline # show commits on remote that you don't have
git log origin/main..main --oneline # show commits you have that remote doesn't

After a fetch, if origin/main has moved ahead of your local main:

    ○ ◄── ○ ◄── ○ ◄── ○ ◄── ○
    C1     C2     C3     C4     C5
                          ▲      ▲
                         main   origin/main
                          ▲
                         HEAD

Your local main is at C4; the remote has a new commit C5. You need git merge origin/main or git pull to catch up.

Creating a Local Branch from a Remote-Tracking Branch

git switch feature-login
# If there's no local 'feature-login' but there IS 'origin/feature-login',
# Git automatically creates a local branch tracking the remote one.

Or explicitly:

git switch -c feature-login origin/feature-login

6. Tags — Named Snapshots

Tags are pointers to specific commits, typically used to mark releases. Unlike branches, tags don't move — they permanently mark a point in history.

Lightweight Tags

A lightweight tag is just a named pointer to a commit — identical to a branch, except it never moves:

git tag v1.0.0                  # tags the current HEAD commit
git tag v0.9.0 a1b2c3d         # tags a specific commit

Stored as a simple file:

cat .git/refs/tags/v1.0.0
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

Annotated Tags

An annotated tag is a full Git object with its own metadata:

git tag -a v1.0.0 -m "Release version 1.0.0"
git cat-file -p v1.0.0
# object a1b2c3d4...
# type commit
# tag v1.0.0
# tagger Jane Doe <jane@example.com> 1705334400 +0000
#
# Release version 1.0.0

Annotated tags store the tagger's name, email, date, and a message. They can also be GPG/SSH signed for verification.

When to Use Which

LightweightAnnotated
Stored asA file pointing to a commitA Git object with metadata
Has a messageNoYes
Has author infoNoYes
Can be signedNoYes
Use caseTemporary/private markersReleases, public milestones

Recommendation: Use annotated tags (git tag -a) for anything you'll share with others. Use lightweight tags for personal bookmarks.

Listing and Managing Tags

git tag                          # list all tags
git tag -l "v1.*"                # list tags matching a pattern
git show v1.0.0                  # show tag details + commit
git tag -d v1.0.0                # delete a local tag
git push origin v1.0.0           # push a specific tag to remote
git push origin --tags           # push all tags to remote
git push origin --delete v1.0.0  # delete a remote tag

7. Putting It All Together — The Pointer Map

Here's the complete picture of how all pointers relate:

    .git/HEAD
        │
        ▼
    ref: refs/heads/main          ← "I'm on the main branch"
              │
              ▼
    .git/refs/heads/main
              │
              ▼
         a1b2c3d                  ← commit hash
              │
              ▼
    ┌─────────────────┐
    │ Commit a1b2c3d  │
    │ tree: 8f3e2a1   │──────► tree object (project snapshot)
    │ parent: e4f5a6b │──────► previous commit
    └─────────────────┘


    .git/refs/heads/feature-login
              │
              ▼
         7c8d9e0                  ← different commit hash


    .git/refs/remotes/origin/main
              │
              ▼
         a1b2c3d                  ← same hash as local main (in sync)


    .git/refs/tags/v1.0.0
              │
              ▼
         3d4e5f6                  ← permanently fixed to this commit

Everything resolves to a commit hash. Branches, tags, HEAD, remote-tracking branches — they're all pointers into the same commit graph.


Command Reference

CommandDescription
git log --oneline --graph --allVisualize the full commit graph with ASCII art
git log --oneline --graph --all --decorateSame, with branch/tag labels (default in modern Git)
git branchList local branches
git branch -aList all branches (local + remote-tracking)
git branch -vList branches with last commit message
git branch <name>Create a branch (don't switch to it)
git branch -d <name>Delete a branch (safe — refuses if unmerged)
git branch -D <name>Force-delete a branch (even if unmerged)
git branch -m <old> <new>Rename a branch
git switch <branch>Switch to a branch (moves HEAD)
git switch -c <branch>Create a branch and switch to it
git checkout <branch>Switch to a branch (older syntax)
git checkout -b <branch>Create and switch (older syntax)
git checkout <hash>Detach HEAD at a specific commit
git tag <name>Create a lightweight tag at HEAD
git tag -a <name> -m "msg"Create an annotated tag at HEAD
git tag -l "pattern"List tags matching a glob pattern
git tag -d <name>Delete a local tag
git rev-parse HEADShow the full hash that HEAD resolves to
git rev-parse <ref>Show the full hash that any ref resolves to
cat .git/HEADSee what HEAD currently points to
cat .git/refs/heads/<branch>See what commit a branch points to

Hands-On Lab: Exploring the Commit Graph and Pointer Movement

This lab builds a small repository with branches, merges, and tags, then inspects the graph and pointer mechanics directly.

Setup

mkdir ~/git-graph-lab
cd ~/git-graph-lab
git init

Step 1: Build a Linear History

echo "# Graph Lab" > README.md
git add README.md
git commit -m "C1: Initial commit"
 
echo "line 2" >> README.md
git add README.md
git commit -m "C2: Add line 2"
 
echo "line 3" >> README.md
git add README.md
git commit -m "C3: Add line 3"

Checkpoint:

git log --oneline --graph --all
# * abc1234 C3: Add line 3
# * def5678 C2: Add line 2
# * 9ab0123 C1: Initial commit

Three commits in a straight line. main points to C3.

Step 2: Inspect the Pointers

cat .git/HEAD
# ref: refs/heads/main
 
cat .git/refs/heads/main
# (hash of C3)
 
git rev-parse HEAD
# (same hash)

HEAD → main → C3. Verify they all resolve to the same hash.

Step 3: Create a Branch and Observe

git branch feature

Checkpoint:

cat .git/refs/heads/feature
cat .git/refs/heads/main

Both files contain the same hash — both branches point to C3. No commits were copied, no directories duplicated. Just a new 41-byte file.

git log --oneline --graph --all
# * abc1234 (HEAD -> main, feature) C3: Add line 3
# * def5678 C2: Add line 2
# * 9ab0123 C1: Initial commit

Notice (HEAD -> main, feature) — both branches and HEAD are at C3.

Step 4: Switch and Commit on the Feature Branch

git switch feature

Checkpoint:

cat .git/HEAD
# ref: refs/heads/feature     ← HEAD now points to 'feature', not 'main'

Now make two commits:

echo "feature work 1" > feature.txt
git add feature.txt
git commit -m "C4: Feature work 1"
 
echo "feature work 2" >> feature.txt
git add feature.txt
git commit -m "C5: Feature work 2"

Checkpoint:

git log --oneline --graph --all
# * 111aaa (HEAD -> feature) C5: Feature work 2
# * 222bbb C4: Feature work 1
# * abc1234 (main) C3: Add line 3
# * def5678 C2: Add line 2
# * 9ab0123 C1: Initial commit

feature moved forward to C5. main stayed at C3. HEAD is on feature.

Step 5: Switch Back to Main and Diverge

git switch main

Notice the working directory changes — feature.txt disappears because it doesn't exist in C3.

ls
# README.md   (no feature.txt!)

Make a commit on main:

echo "main work" > main-only.txt
git add main-only.txt
git commit -m "C6: Main-only work"

Checkpoint:

git log --oneline --graph --all
# * 333ccc (HEAD -> main) C6: Main-only work
# | * 111aaa (feature) C5: Feature work 2
# | * 222bbb C4: Feature work 1
# |/
# * abc1234 C3: Add line 3
# * def5678 C2: Add line 2
# * 9ab0123 C1: Initial commit

The branches have diverged. The graph shows two paths from C3.

Step 6: Merge and Observe a Two-Parent Commit

git merge feature -m "C7: Merge feature into main"

Checkpoint:

git log --oneline --graph --all
# *   444ddd (HEAD -> main) C7: Merge feature into main
# |\
# | * 111aaa (feature) C5: Feature work 2
# | * 222bbb C4: Feature work 1
# * | 333ccc C6: Main-only work
# |/
# * abc1234 C3: Add line 3
# * def5678 C2: Add line 2
# * 9ab0123 C1: Initial commit

Inspect the merge commit:

git cat-file -p HEAD

You should see two parent lines — this is what makes it a merge commit.

ls
# README.md  feature.txt  main-only.txt   ← all files from both branches

Step 7: Create and Inspect Tags

git tag v1.0.0                          # lightweight tag at HEAD
git tag -a v0.1.0 9ab0123 -m "Alpha"   # annotated tag at C1 (use your C1 hash)

Checkpoint:

cat .git/refs/tags/v1.0.0
# (hash of C7 — the merge commit)
 
git cat-file -t v0.1.0
# tag   (it's an object, not just a pointer)
 
git cat-file -p v0.1.0
# object 9ab0123...
# type commit
# tag v0.1.0
# tagger ...
#
# Alpha

Step 8: Experience Detached HEAD

git checkout def5678    # use your C2 hash

Git warns you about detached HEAD.

cat .git/HEAD
# def5678...   ← raw hash, not a branch ref
 
git log --oneline --graph --all
# Look for (HEAD) — it's floating, not attached to a branch name

Make a commit in detached state:

echo "detached experiment" > experiment.txt
git add experiment.txt
git commit -m "Detached commit"

Now switch back:

git switch main

Git warns that you're leaving a detached commit behind. Note the hash it mentions.

Checkpoint:

git log --oneline --graph --all
# The detached commit is NOT visible — no branch points to it

Recover it:

git reflog
# Find the "Detached commit" entry and note its hash
 
git switch -c recovered-experiment <hash>
git log --oneline --graph --all
# Now it's visible again — the branch 'recovered-experiment' points to it

Step 9: Delete a Branch and Understand Reachability

git switch main
git branch -d feature

Checkpoint:

git log --oneline --graph --all

The commits C4 and C5 are still there — they're reachable from the merge commit C7 (which is on main). Deleting the feature branch only removed the pointer. The commits are safe because another path leads to them.

Now try:

git branch -d recovered-experiment

If the branch had unmerged commits, Git refuses with -d. You'd need -D to force it. The commits would then become unreachable (visible only in reflog until garbage collection).

Challenge

  1. Create three branches from the same commit. Make one commit on each. Visualize the graph with git log --oneline --graph --all. Observe that three branches diverge from the same point.

  2. Use git merge-base main <branch> to find the common ancestor of two branches. Verify with git cat-file -p.

  3. Create a situation where deleting a branch would lose commits (the only branch pointing to them). Use git reflog to find them and recover them.

Cleanup

rm -rf ~/git-graph-lab

Common Pitfalls & Troubleshooting

PitfallExplanation
"I lost commits when I deleted a branch"You didn't lose them — they're still in the object database. Use git reflog to find the hash, then git switch -c <name> <hash> to recover. Unreachable commits survive for ~2 weeks before garbage collection.
Panicking at "detached HEAD"It's not an error. You're just looking at a specific commit without a branch. If you want to make changes, create a branch first: git switch -c my-branch.
Thinking branches copy filesBranches copy nothing. They're 41-byte pointers. All the data lives in the object database, shared by all branches.
Confusing origin/main with mainorigin/main is a remote-tracking branch — a read-only snapshot of where main is on the remote. main is your local branch. They may point to the same or different commits. Use git fetch to update origin/main.
"My branch is out of date"Your remote-tracking branches are stale. Run git fetch to update them, then check git log main..origin/main --oneline to see what's new.
Not understanding HEAD~ vs HEAD^HEAD~n follows the first parent n times (goes n commits back along the main line). HEAD^2 selects the second parent of a merge commit. HEAD~2 = grandparent. HEAD^2 = second parent of HEAD.

Pro Tips

  1. Alias the graph view. You'll use this constantly:

    git config --global alias.graph "log --oneline --graph --all --decorate"

    Now just type git graph.

  2. Understand reachability. A commit is "safe" if at least one branch, tag, or the reflog points to it (directly or through ancestors). git gc only deletes unreachable objects. When you delete a branch, ask yourself: "Is this commit reachable from any other pointer?"

  3. Use git switch over git checkout. The checkout command is overloaded — it switches branches, restores files, and detaches HEAD. Git 2.23 introduced git switch (for branches) and git restore (for files) to reduce confusion.

  4. HEAD~ vs HEAD^ cheat sheet:

    HEAD~1  = HEAD^   = parent (first parent)
    HEAD~2  = HEAD^^  = grandparent (first parent of first parent)
    HEAD^2             = second parent (of a merge commit)
    HEAD~2^2           = second parent of the grandparent
    
  5. Remote-tracking branches update on fetch, not on pull. git pull = git fetch + git merge (or rebase). If you just want to see what's new without changing your working directory, use git fetch first, then inspect with git log.

  6. Branches are your undo mechanism. Before doing anything risky (rebase, reset, merge), create a temporary branch pointing to your current position: git branch backup-before-rebase. If things go wrong, you can git switch backup-before-rebase and you're back to exactly where you were.


Quiz / Self-Assessment

1. What data structure is Git's commit history?

Answer
A directed acyclic graph (DAG). It's directed because parent references point from child to parent. It's acyclic because you can never follow parent references and arrive back at the same commit. The tree/branch metaphor breaks because trees can't have convergence (merges).

2. What is a branch in Git, physically?

Answer
A file in .git/refs/heads/ that contains the 40-character SHA-1 hash of a commit. That's it — a 41-byte text file (40 chars + newline).

3. What is HEAD?

Answer
A pointer stored in .git/HEAD that indicates your current position. Normally it contains a reference to a branch (e.g., ref: refs/heads/main). When detached, it contains a raw commit hash.

4. What happens to the branch pointer when you run git commit?

Answer
The branch that HEAD points to moves forward to the newly created commit. HEAD itself doesn't change (it still points to the same branch name), but the branch now points to the new commit.

5. What is "detached HEAD" state?

Answer
When HEAD points directly to a commit hash instead of to a branch name. This happens when you checkout a specific commit, tag, or remote-tracking branch. Commits made in this state aren't tracked by any branch and can become unreachable.

6. What's the difference between git branch feature and git switch -c feature?

Answer
git branch feature creates the branch but doesn't switch to it — HEAD stays where it is. git switch -c feature creates the branch AND moves HEAD to it.

7. How does Git know that a commit is a merge commit?

Answer
By the number of parent references. A normal commit has one parent. A merge commit has two (or more) parents. No special flag or label is needed — the parent count is the signal.

8. What is a remote-tracking branch?

Answer
A read-only pointer stored at refs/remotes/<remote>/<branch> that represents Git's last known state of a branch on the remote server. It's updated by git fetch. You can't commit to it directly.

9. What's the difference between a lightweight tag and an annotated tag?

Answer
A lightweight tag is just a pointer to a commit (like a branch that never moves). An annotated tag is a full Git object with its own hash, containing the tagger's name, email, date, a message, and a pointer to a commit. Annotated tags can be signed.

10. After deleting a branch, are the commits it pointed to deleted?

Answer
Not immediately. The commits remain in the object database. If they're reachable from another branch, tag, or the reflog, they're completely safe. If they're unreachable, they'll survive for ~2 weeks (the default gc.pruneExpire) before garbage collection removes them. Use git reflog to find and recover unreachable commits.