A Field Guide

Git & GitHub

Version control from first principles — commits, branches, pull requests, and the workflows real teams actually use.

🧠 Part 1: Mental Model
⚙️ Part 2–3: Setup
🔄 Part 4–5: Daily Workflow
🌿 Part 6–7: Branching
⏪ Part 8–9: Undo & History
🤝 Part 10: Collaboration
🔧 Part 11–12: Advanced
🤖 Part 13: Git & AI

Who This Is For

Developers, builders, and technically-minded people who use git but feel like they're only using 20% of it. Or people who've avoided it and want to actually understand what's happening, not just memorize commands.

This is a 45-minute read. By the end, you'll understand what git is actually doing under the hood, be fluent in the daily workflow, write commit messages that serve you six months later, manage branches with confidence, resolve merge conflicts without panic, collaborate through pull requests effectively, and understand how git fits into modern AI-assisted development.

Why Git Matters More Now

Git is the backbone of every serious software project. It's also increasingly the backbone of AI-assisted development — where you might make 10x more changes in the same time, which makes good git hygiene 10x more important, not less.

When you're working with Claude Code, Cursor, or any agentic coding tool, things move fast. The AI makes changes across multiple files, refactors things, tries approaches. Without clean git checkpoints, a single bad session can be hard to untangle. With them, you can always roll back to a known-good state, understand what changed and why, and delegate confidently because you're not afraid of losing work.

Git also tells the story of your project. A year from now, when you're wondering why some piece of code exists in a particular way, the git history either explains it or it doesn't — depending on how you used it.

Part One
The Mental Model
What git actually is. Most people learn commands first — the model is what saves you.

Git Is a Snapshot Database

Git doesn't store diffs (the difference between versions). It stores complete snapshots of your entire project at every commit.

Each snapshot is identified by a hash — a long string like 3a7f9c2b4d... — that's mathematically derived from the content. Change a single character in any file, and you get a completely different hash. This is why git can tell immediately if anything has changed.

When git seems to store things efficiently (and it does — a million commits don't use a million times the space), that's because it deduplicates identical files across snapshots. If 900 of your files didn't change between commit A and commit B, those 900 files are stored once, not twice.

Three Places Your Code Lives

At any moment, your code exists in three places.

Working directory — the actual files on your disk. This is what you see in your editor. Git knows about these files but isn't actively tracking them until you stage them.

Staging area (index) — a prepared snapshot in progress. When you git add something, you're putting it in the staging area. Think of it as the table where you arrange exactly what goes into the next commit.

Repository (.git folder) — the permanent record. When you git commit, the staging area becomes a commit that joins the history forever. Everything in the .git folder is git's database.

Working Directory → [git add] → Staging Area → [git commit] → Repository
💡 Why Three Steps?
This three-step process lets you be precise about what goes into each commit. You might have made three different changes to three different files, but only want to commit two of them. Stage just those two, commit, then deal with the third separately.

Commits Are a Linked Chain

Each commit knows its parent commit. Following the chain backward from the latest commit gives you the entire history.

This chain is the thing git operates on — branches are just named pointers to a specific commit in the chain, not separate copies of anything.

Local and Remote

Your local git repository is complete and fully functional without any network connection.

You can commit, branch, merge, and inspect history entirely offline. The "remote" (usually GitHub) is just another copy of the repository that you sync with via push and pull. They're peers, not client and server in the traditional sense.

This is different from older version control systems where the server was the authority. In git, every clone is a full copy. This makes git fast, resilient, and flexible — and also means you have the full history locally, always.

Part Two
Setup
Getting git configured right, and connecting to GitHub.

Configuration

Before anything else, tell git who you are. These values get embedded in every commit you make.

git config --global user.name "Your Name"
git config --global user.email "[email protected]"

A few other configurations worth setting once:

git config --global core.editor "nano"          # or "code --wait" for VS Code
git config --global init.defaultBranch main     # use 'main' for new repos
git config --global pull.rebase false           # use merge on pull by default
git config --global core.autocrlf input         # macOS/Linux: normalize line endings

Check your current configuration:

git config --list                               # see all settings
git config --global --edit                      # open the config file directly
💡 Config Files
Your global config lives at ~/.gitconfig. Project-specific configs live at .git/config in each repo and override global settings.

Setting Up GitHub Authentication

To push and pull from GitHub without typing your password constantly, use SSH keys.

ssh-keygen -t ed25519 -C "[email protected]"     # generate a key pair
cat ~/.ssh/id_ed25519.pub                       # copy the public key

Add the public key to GitHub at Settings → SSH and GPG keys → New SSH key.

ssh -T [email protected]                           # should say "Hi username!"

Now clone repos with SSH URLs ([email protected]:user/repo.git) and authentication is automatic.

Starting and Cloning Repositories

Starting from Scratch

mkdir myproject
cd myproject
git init                        # creates .git folder, initializes repo
git add README.md               # stage a file
git commit -m "initial commit"  # first commit

Then connect to a remote:

git remote add origin [email protected]:yourname/myproject.git
git push -u origin main         # push and set upstream tracking
💡 The -u Flag
The -u flag sets the upstream — after this, git push and git pull know where to go without you specifying.

Cloning an Existing Repository

git clone [email protected]:user/repo.git          # clone into folder named 'repo'
git clone [email protected]:user/repo.git myname   # clone into folder named 'myname'
git clone --depth 1 [email protected]:user/repo.git  # shallow clone: latest commit only

Shallow clones are useful for large repos where you don't need the full history — CI/CD pipelines and quick explorations especially.

Part Four
The Daily Workflow
What you do dozens of times per day — add, commit, push, and the habits that keep it clean.

Add, Commit, Push

Check Status Constantly

git status                      # what's changed? what's staged?
git diff                        # what changed in working directory (not yet staged)?
git diff --staged               # what's staged and ready to commit?

git status is your orientation. Run it constantly. Before staging, before committing, after operations that might have changed things. It's fast and costs nothing.

Staging

git add filename.txt            # stage a specific file
git add .                       # stage everything in current directory (careful)
git add -p                      # interactive: stage parts of files (patch mode)
git add -u                      # stage only tracked files that changed (not new files)
📝 git add -p (Patch Mode)
git add -p is underused and powerful. It walks you through each changed "hunk" of each file and asks whether to stage it. This is how you make precise, atomic commits even when you've been making multiple changes at once.

y = stage this hunk   n = skip   s = split into smaller hunks   e = manually edit   q = quit

Committing

git commit -m "Add user authentication endpoint"    # commit with inline message
git commit                                          # opens editor for longer message
git commit -am "Fix typo"                           # stage all tracked changes and commit
⚠️ About -am
The -am shorthand is convenient but skips the review step. Use it only for small, obvious changes where you know exactly what you're committing.

Viewing What You've Done

git log                         # full log
git log --oneline               # compact: one line per commit
git log --oneline -10           # last 10 commits
git log --oneline --graph       # with ASCII branch graph
git log --author="Name"         # commits by a specific person
git log -- filename.txt         # commits that touched a specific file
git show <hash>                 # see a specific commit in detail
git show HEAD                   # see the latest commit
git show HEAD~3                 # see the commit 3 before HEAD

Pushing and Pulling

git push                        # push to upstream (set via -u)
git push origin main            # explicit: push main to origin
git push --force-with-lease     # safer force push: fails if remote has unexpected changes

git pull                        # fetch + merge from upstream
git fetch                       # download remote changes but don't merge yet
git fetch --prune               # fetch and remove references to deleted remote branches
💡 Fetch vs. Pull
git fetch without merge is useful when you want to see what's changed remotely before deciding how to integrate it. It's always safe — it just updates your remote-tracking branches.

Writing Good Commit Messages

Commit messages are time-traveling communication. You're writing to your future self and your teammates who will read git history while debugging, reviewing, or understanding the codebase months later.

The Anatomy of a Good Commit Message

Short summary line (50 characters or less, imperative mood)

Optional body: explain the why, not the what. The what is visible
in the diff. The why is not. Why did you make this change? What
problem does it solve? Were there alternatives you considered?

- Bullet points work in the body
- Keep body lines to ~72 characters
- Reference issues: Closes #123

The Imperative Mood Rule

Git itself uses imperative mood for its auto-generated messages: "Merge branch X", "Revert commit Y". Following this convention makes your history read consistently.

✅ Good
Add email validation to signup form
Fix race condition in authentication handler
Remove deprecated user_v1 API endpoints
⚠️ Bad
Added email validation (past tense)
Adding email validation (progressive)
email validation (not imperative, not descriptive)
fix stuff (useless)

What to Put in the Body

The body is for explaining things the diff can't show:

Increase connection pool size from 5 to 20

The application was hitting timeout errors under load because all
connections in the pool were occupied during traffic spikes. Profiling
showed we were spending 40% of request time waiting for a pool slot.

20 connections is based on our current server capacity. Revisit if
we scale beyond 8 app servers.

Related to monitoring alert: #482

Six months later, this commit tells you exactly why the change was made, what the data showed, and what assumptions it's based on. A commit that just says "increase connection pool size" leaves all that knowledge on the floor.

Conventional Commits

A standardized format that makes history machine-readable and enables automated tooling.

feat: add cursor-based pagination to search API
fix: prevent null pointer in user profile lookup
docs: update API authentication guide
refactor: extract validation logic into shared utility
chore: update dependencies to latest minor versions
test: add integration tests for checkout flow
perf: cache product catalog queries for 5 minutes

Format: type(optional scope): description

TypeWhen to use
featNew feature
fixBug fix
docsDocumentation only
refactorCode restructure, no behavior change
choreMaintenance, dependency updates
testAdding or fixing tests
perfPerformance improvement
ciCI/CD pipeline changes

The scope is optional but useful in larger codebases: feat(auth): add OAuth provider support. For breaking changes, add ! after the type: feat!: change API response format.

💡 Why Bother?
Conventional Commits enable automated changelog generation, semantic versioning, and better filtering of git history. Even without the tooling, the discipline of categorizing changes makes your history more navigable.

Atomic Commits

Each commit should represent one complete, logical change that could be reverted independently without breaking anything else.

⚠️ Bad
"Fix auth bug and update styles and add logging" — three unrelated changes in one commit.
✅ Good
Three separate commits, one for each change.

Why this matters: git bisect does binary search through commits to find bugs. If your commits mix unrelated concerns, bisect becomes useless. Code review is also harder — reviewers can't isolate what to focus on. And reverting "add logging" from a commit that also fixes a production bug means you lose the bug fix too.

The discipline: when you've been working and made multiple unrelated changes, use git add -p to carefully stage only the changes that belong to one commit, commit those, then stage and commit the rest separately.

Part Six
Branching
Parallel lines of work — how you work on things without stepping on each other.

Branching — Parallel Lines of Work

A branch is just a named pointer to a commit. Creating a branch doesn't copy anything — it's just a label. When you commit on a branch, the pointer moves forward. This is why branches are cheap and instant in git.

HEAD is a special pointer that indicates which branch (or commit) you're currently working on. When you commit, HEAD's branch pointer advances.

Creating and Switching Branches

git branch                          # list local branches
git branch -a                       # list all branches including remote-tracking
git branch feature/user-auth        # create a branch (stay on current branch)
git switch feature/user-auth        # switch to that branch
git switch -c feature/user-auth     # create and switch in one step (preferred)
git switch -                        # switch back to the previous branch (toggle)
💡 switch vs. checkout
git switch is the modern replacement for git checkout for branch operations. checkout still works, but switch is more explicit and less confusing.

Branch Naming Conventions

feature/user-authentication
feature/payment-flow-redesign
fix/login-timeout-error
fix/null-pointer-in-search
chore/update-node-18
docs/api-authentication-guide
experiment/ai-powered-search

The feature/, fix/, chore/ prefixes group related branches. Many teams also include ticket numbers: feature/AUTH-123-oauth-provider.

Merging Branches

git switch main
git merge feature/user-auth         # merge feature into current branch (main)
git merge --no-ff feature/user-auth # always create a merge commit
git merge --squash feature/user-auth # combine all feature commits into one staged change

--no-ff (no fast-forward) preserves the branch history visually even when it could technically be a simple advance of the pointer. It makes git log --graph show the branch structure clearly.

Deleting Branches After Merging

git branch -d feature/user-auth     # delete local branch (safe: refuses if not merged)
git branch -D feature/user-auth     # force delete (even if not merged)
git push origin --delete feature/user-auth  # delete remote branch
git remote prune origin             # remove local references to deleted remote branches

Delete merged branches. Keeping them around forever creates noise and makes it hard to see what's actually in progress.

Merge vs. Rebase

Both integrate changes from one branch into another. They do it differently and produce different histories.

Merge creates a merge commit that has two parents. The history shows exactly what happened — branch came off here, rejoined there, parallel work happened in between.

Rebase replays your commits on top of the target branch as if you'd branched from there. The result looks like linear history — no branch, no merge commit, just a clean sequence of commits.

# Merge approach
git switch main
git merge feature                   # creates merge commit

# Rebase approach
git switch feature
git rebase main                     # replay feature commits on top of main
git switch main
git merge feature                   # now a fast-forward, no merge commit needed

Merge is safer, non-destructive, and preserves the true history. The downside: with many feature branches, the history becomes a tangle of merge commits.

Rebase produces cleaner, linear history that's easier to read. The downside: it rewrites commit history. The commits get new hashes. Fine on your local branch before sharing. Dangerous on public branches others might be based on.

⚠️ The Golden Rule of Rebasing
Never rebase commits that others have already based work on. In practice: rebase your local feature branches to stay current with main before opening a PR. Never rebase main, develop, or any branch others are working on.
# Common workflow: keep feature branch current with main via rebase
git switch feature
git fetch origin
git rebase origin/main              # replay your feature commits on latest main
# resolve any conflicts, then:
git rebase --continue               # after resolving each conflict
# or:
git rebase --abort                  # bail out and return to pre-rebase state

Resolving Merge Conflicts

Conflicts happen when the same part of a file was changed in two different ways that git can't automatically reconcile. They're not a problem — they're git saying "I need a human to decide."

What a Conflict Looks Like

<<<<<<< HEAD
const timeout = 5000;
=======
const timeout = 10000;
>>>>>>> feature/increase-timeout

Everything between <<<<<<< HEAD and ======= is what your current branch has. Everything between ======= and >>>>>>> is what's coming in. You decide what the final version should be — and remove all the conflict markers.

📝 The Final Version Might Be New
The resolution doesn't have to be one side or the other:

const timeout = process.env.NODE_ENV === 'test' ? 5000 : 10000;

Resolving Conflicts

# After a merge or rebase that hits conflicts:
git status                          # shows which files have conflicts
# Edit the conflicted files, resolve each conflict, remove markers
git add resolved-file.js            # mark as resolved
git commit                          # complete the merge

For rebases, you resolve conflicts one commit at a time:

git rebase --continue               # after resolving current commit's conflicts
git rebase --skip                   # skip this commit entirely (rarely right)
git rebase --abort                  # give up and return to pre-rebase state

Preventing Conflicts

  • Keep branches short-lived — the longer a branch lives, the more divergence accumulates
  • Pull/rebase frequently from main to stay current
  • Communicate with teammates when working on the same files
  • Break large changes into smaller, sequential branches
Part Eight
Undoing Things
Git's time machine — the right tool depends on what you want to undo and whether you've pushed.

Undoing Changes

Undoing Changes in the Working Directory

git restore filename.txt            # discard changes in working directory (permanent!)
git restore .                       # discard all working directory changes
git restore --staged filename.txt   # unstage a file (move back from staging to working)
git restore --staged .              # unstage everything
⚠️ Permanent
git restore (working directory) permanently throws away your changes without git keeping a record. Use it carefully.

Reset vs. Revert

git reset — moves the branch pointer backward, "erasing" commits:

git reset --soft HEAD~1             # undo last commit, keep changes staged
git reset --mixed HEAD~1            # undo last commit, keep changes unstaged (default)
git reset --hard HEAD~1             # undo last commit, discard all changes (destructive)

--soft is the safe version — it uncommits but keeps your work, ready to re-commit differently. --hard throws away the changes completely. All three rewrite history.

⚠️ Local Only
Use git reset only on local commits you haven't pushed. On shared/public commits, use git revert instead.

git revert — creates a new commit that undoes the changes from a previous commit:

git revert HEAD                     # create a commit that undoes the last commit
git revert abc123                   # create a commit that undoes a specific commit
git revert HEAD~3..HEAD             # undo the last 3 commits

git revert is safe on public branches because it adds to history rather than rewriting it. Everyone can see what was reverted and why.

Amending the Last Commit

git commit --amend -m "Better message"          # change the message
git commit --amend --no-edit                    # add staged changes to last commit

--amend rewrites the last commit (changes its hash). Only use it before pushing.

Git Stash — A Temporary Shelf

Stash saves your working directory changes without committing, so you can switch context and come back.

git stash                           # stash working directory changes
git stash push -m "WIP: auth refactor"  # stash with a description
git stash list                      # see all stashes
git stash pop                       # apply the most recent stash and remove it
git stash apply stash@{2}           # apply a specific stash (keep it in stash list)
git stash drop stash@{2}            # delete a specific stash
git stash clear                     # delete all stashes
💡 When to Stash
Stash is useful when you're mid-feature and something urgent needs your attention, or you realize you should have branched before starting changes.

Finding Lost Commits — Git Reflog

Even after git reset --hard, commits aren't immediately gone.

git reflog shows a log of every position HEAD has been at, including commits that are no longer in the branch history:

git reflog                          # see all recent HEAD positions
git switch -c recovery-branch abc123  # create a branch at a "lost" commit hash
✅ Your Safety Net
Reflog is your safety net. Before panicking about lost work, check reflog. The commits persist for 30 days by default before git garbage-collects them.

Investigating History

The git history is a searchable, interrogatable record. Learning to read it is what separates comfort from guesswork.

git log — Reading History

git log --oneline --graph --all     # visual branch graph including all branches
git log --since="2 weeks ago"       # filter by date
git log --author="Alice"            # filter by author
git log -- src/auth/                # commits that touched this folder
git log -p                          # show the actual diff with each commit
git log -S "functionName"           # commits that added or removed this string
git log --grep="ticket-123"         # commits whose messages match a pattern
💡 The Pickaxe (-S)
The -S flag is especially useful — "show me every commit that introduced or removed this string anywhere in the codebase."

git blame — Who Changed What

git blame filename.py               # annotate each line with author and commit
git blame -L 40,60 filename.py      # blame just lines 40-60

git blame shows for each line: who last changed it, in what commit, and when. This is how you find the context for a piece of code that doesn't make sense — find the commit, then read the commit message.

git diff — Comparing States

git diff                            # working directory vs staging area
git diff --staged                   # staging area vs last commit
git diff main..feature              # compare two branches
git diff abc123..def456             # compare two specific commits
git diff --stat                     # just file list and change counts

Git Bisect — Binary Search for Bugs

git bisect is one of git's most powerful and underused features. It does binary search through your commit history to find exactly which commit introduced a bug.

git bisect start                    # start bisect session
git bisect bad                      # current commit is bad (has the bug)
git bisect good abc123              # this earlier commit was good (no bug)
# git checks out the midpoint commit
# test whether the bug exists, then:
git bisect good                     # this commit is good
git bisect bad                      # this commit is bad
# git keeps halving the search space until it finds the exact commit
git bisect reset                    # end session and return to HEAD

Git typically needs 7–10 rounds to find the guilty commit in a history of hundreds. For automated testing:

git bisect run npm test             # automated: 0 exit = good, non-0 = bad
💡 Why Atomic Commits Matter Here
This is extraordinarily powerful when something broke "sometime in the last month." The key is having good atomic commits — if commits mix unrelated changes, bisect finds the guilty commit but you still have to untangle what caused the problem. For more on binary search debugging and other systematic techniques, see the Debugging field guide.
Part Ten
Collaborating
Pull requests, code review, and the workflows that make teams work.

Pull Requests

Pull requests (PRs) are the standard mechanism for proposing changes, getting them reviewed, and merging them. They're a GitHub invention, not native to git, but they've become the default everywhere.

git switch -c feature/my-feature    # create a feature branch
# ... make commits ...
git push -u origin feature/my-feature  # push branch to remote
# Then go to GitHub and open a PR from that branch to main

Writing a Good PR Description

The PR description is documentation. Write it for someone who doesn't have your context:

  • What: A one-sentence summary of what this PR does
  • Why: The motivation. What problem does it solve?
  • How: Any non-obvious technical decisions. Why this way?
  • Testing: How did you verify this works? Edge cases you checked?
  • Screenshots/recordings: For UI changes, always include before/after
💡 PR Templates
A .github/pull_request_template.md file in the repo pre-fills the description. These enforce consistency and remind people what to include.

Good PR Practices

Keep PRs small. A PR that touches 10 files and 300 lines is hard to review well. A PR that touches 3 files and 80 lines gets reviewed faster, more carefully, and with better feedback. If a feature is large, break it into a sequence of PRs.

One purpose per PR. Don't bundle unrelated changes. If you fix a bug while building a feature, make them separate PRs.

Keep your PR current. If main moves while your PR is open:

git fetch origin
git rebase origin/main              # rebase on latest main
git push --force-with-lease         # update the remote branch

Respond to review comments promptly and concretely. For each comment: either make the change and push, or explain why you're not. Don't leave threads open without resolution.

Reviewing Code

Code review is a skill separate from writing code.

Understand before critiquing. Read through the whole PR before commenting. Get a sense of the overall approach before focusing on details.

Distinguish blocking from non-blocking. Be clear: "This will cause a bug in X scenario" vs. "Minor style nit, take it or leave it." Consider prefixes: [blocking], [nit], [question], [suggestion].

Comment on the why. Don't just say "change this to X." Explain why X is better. The author learns something; the explanation lives in the PR history.

Ask questions genuinely. Sometimes the right response to code you don't understand is "can you explain what this is doing?" The author might have a good reason. Or they might discover explaining it reveals a problem.

Be constructive about significant issues. "I think this approach will have trouble with Y — could we discuss alternatives?" opens a conversation. "This is wrong" closes it.

Merge Strategies

When merging a PR on GitHub, you have three options.

Merge commit (default) — preserves all commits from the branch plus adds a merge commit. Full history, but can get noisy.

Squash and merge — combines all branch commits into one commit on main. Produces clean main history; loses the detailed branch history. Good for messy work-in-progress branches.

Rebase and merge — replays each branch commit directly onto main with no merge commit. Linear history, preserves individual commits. Best for well-crafted branches with clean, meaningful commits.

💡 Pick One
Most teams pick one strategy and stick to it for consistency. Squash-and-merge is increasingly popular because it keeps main history clean regardless of how messy the branch work was.
Part Eleven
Advanced Git
Interactive rebase, cherry-pick, worktrees, tags, hooks — the powerful stuff.

Interactive Rebase

git rebase -i lets you edit commits before they become permanent — reorder them, squash them together, edit messages, split them apart.

git rebase -i HEAD~5                # interactively edit the last 5 commits
git rebase -i origin/main          # edit everything not yet in main

This opens an editor with your commits and action keywords:

pick a1b2c3d Add user model
pick b2c3d4e Add authentication endpoint
pick c3d4e5f WIP - need to fix tests
pick d4e5f6a Fix failing tests from previous commit
pick e5f6a7b Add login UI

Change pick to an action:

ActionWhat it does
pickKeep as-is
rewordKeep the commit but edit the message
squash (s)Combine with the previous commit (merges messages)
fixup (f)Combine with previous, discard this commit's message
edit (e)Pause to make changes to this commit
drop (d)Delete the commit entirely
📝 Cleaning Up WIP Commits
For the WIP commits above, change to:

pick a1b2c3d Add user model
pick b2c3d4e Add authentication endpoint
fixup c3d4e5f WIP - need to fix tests
fixup d4e5f6a Fix failing tests from previous commit
pick e5f6a7b Add login UI

This squashes the WIP and fix commits into the auth endpoint commit, producing a clean history before the PR.
⚠️ Local Only
Only do this on branches you haven't shared publicly. Rewriting history changes commit hashes, which breaks anyone else's work based on those commits.

Cherry-Pick & Worktree

git cherry-pick

git cherry-pick abc123              # apply a single commit from anywhere to current branch
git cherry-pick abc123..def456      # apply a range of commits
git cherry-pick abc123 --no-commit  # apply changes without committing

Cherry-pick is useful when you want one specific fix from a feature branch without merging the whole thing, or when you accidentally committed to the wrong branch.

git worktree — Multiple Checkouts

git worktree add ../myproject-feature feature/auth   # new working directory
git worktree list                                     # see all worktrees
git worktree remove ../myproject-feature             # remove when done

Each worktree is a separate checkout of a different branch, sharing the same repository database. Especially useful when working with AI coding agents — you can have Claude Code working on one feature in one worktree while you work on something else in the main checkout.

Tags & Hooks

git tag — Marking Releases

git tag v1.0.0                      # lightweight tag (just a pointer)
git tag -a v1.0.0 -m "Version 1.0.0 release"   # annotated tag (has message)
git push origin v1.0.0              # push a specific tag
git push origin --tags              # push all tags
git tag                             # list all tags

Tags are how you mark release points. Unlike branch names, tags don't move when you make new commits. They're permanent markers in history.

Hooks — Automating Git Workflows

Git hooks are scripts that run automatically at certain points in the git workflow. They live in .git/hooks/:

HookWhen it runs
pre-commitBefore a commit is created — linting, secret scanning
commit-msgAfter you write a commit message — validate format
pre-pushBefore git push — running tests
post-commitAfter commit — notifications
📝 Example: Pre-commit Hook to Block .env Files
#!/bin/sh
if git diff --cached --name-only | grep -q "\.env$"; then
  echo "Error: .env file staged. Remove it before committing."
  exit 1
fi
Make it executable: chmod +x .git/hooks/pre-commit
💡 Sharing Hooks
Since .git/hooks isn't tracked by git, use a tool like husky (for Node.js projects) which stores hooks in the repo and installs them on npm install.

GitHub Beyond Pull Requests

Branch Protection Rules

For any serious project, protect your main branch. In GitHub: Settings → Branches → Add rule. Key protections:

  • Require pull request reviews — nobody can push directly to main
  • Require status checks to pass — CI must pass before merging
  • Require branches to be up to date — PR must be rebased on latest main
  • Restrict who can push — limit direct pushes to admins
  • Require signed commits — cryptographic verification

GitHub Actions — CI/CD in the Repo

GitHub Actions lets you run workflows on git events. A minimal test workflow (.github/workflows/test.yml):

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install
      - run: npm test
💡 High-Value Setup
Once this is linked to branch protection, no untested code can merge. This is one of the highest-value things you can add to a project.

Issues and Project Management

Reference issues in commit messages and PRs: Closes #42 or Fixes #42 in a PR description automatically closes the issue when the PR merges. Use issue templates (.github/ISSUE_TEMPLATE/) to standardize bug reports and feature requests. Labels create filterable taxonomy: bug, enhancement, good first issue, blocked, priority: high. Milestones group issues toward a release or deadline.

Part Thirteen
Git & AI
How AI-assisted development changes the way you work with version control.

Git and AI-Assisted Development

The AI tooling era changes how you work with git in several concrete ways worth understanding.

Committing AI-Assisted Work

When Claude Code, Cursor, or Copilot writes or significantly modifies code, that code is still your responsibility. You're the author. Review it before committing — not because AI tools are untrustworthy, but because you need to understand what's in your codebase, AI-generated code can have subtle bugs, and your commit message should explain the why, which only you know.

Git as an AI Checkpoint System

With AI tools making sweeping changes, commits become safety checkpoints. Before handing a task to an AI agent:

git add .
git commit -m "chore: checkpoint before AI refactor"

Now if the AI session goes sideways — writes something that breaks tests, goes in the wrong direction — you have a clean rollback point:

git reset --hard HEAD              # throw away all AI changes and return to checkpoint
💡 Not a Crutch
This is a legitimate workflow. The ability to experiment boldly because you can always roll back is one of git's most valuable properties — and it's especially valuable when delegating to AI.

Branching for AI Experiments

git switch -c experiment/ai-checkout-redesign
# delegate to Claude Code on this branch
# review the results
# merge if good, delete if not
git branch -D experiment/ai-checkout-redesign   # delete if it didn't work out

The branch creates an isolated playground. If the experiment fails, you delete it. If it succeeds, you merge (or cherry-pick the good parts).

CLAUDE.md — Teaching AI Your Git Conventions

Claude Code reads a CLAUDE.md file in your project root and follows the instructions there:

## Git workflow
- Always work on feature branches, never commit directly to main
- Branch naming: feature/description, fix/description, chore/description
- Commit messages must follow Conventional Commits format
- Run tests before committing: npm test
- Include the ticket number in PRs: [TICKET-123]

AI for Understanding History

AI tools are particularly good at explaining git history. Try prompts like:

  • "What did this branch change? Summarize the commits between feature/auth and main."
  • "Explain what the commit abc123 was doing and why."
  • "I have a merge conflict. Here are both sides. What are each of these changes trying to do?"

Keeping Clean History with AI-Heavy Workflows

When AI tools generate lots of commits ("checkpoint 1," "checkpoint 2," "fix that," "try again"), clean them up before merging to main:

git rebase -i origin/main          # squash the WIP commits
# turn 15 "checkpoint" commits into 3 meaningful commits

The AI's interim work is useful in the moment, but the long-term record should show meaningful units of work, not every iteration the AI tried.

Part Fourteen
Troubleshooting
Common problems and their fixes — because git error messages are more helpful than they look.

Common Problems and Fixes

"Your branch is behind origin/main"

git pull                            # merge latest remote changes in
# or:
git fetch origin
git rebase origin/main              # replay your commits on latest main

"Detached HEAD state"

You checked out a specific commit instead of a branch. You're not on any branch, so new commits won't be tracked.

git switch -                        # go back to previous branch
git switch main                     # or go to a specific branch
git switch -c new-branch            # or create a branch here to keep your work

"Fatal: refusing to merge unrelated histories"

Usually happens when you initialized a repo locally and also created one on GitHub with a README.

git pull origin main --allow-unrelated-histories    # force the merge

"Rejected: non-fast-forward"

You tried to push, but the remote has commits you don't have.

git pull --rebase                   # fetch remote changes and rebase on top
git push                            # now push

You Pushed Secrets to GitHub

  1. Revoke the secret immediately in whatever service issued it
  2. Generate a new secret
  3. Remove from history: git filter-repo --path-glob='*.env' --invert-paths or use BFG Repo-Cleaner
  4. Force push the cleaned history: git push --force --all
  5. Contact GitHub support — they can invalidate cached versions
⚠️ Priority
Revocation is the priority. The history cleaning is cleanup. Treat the old secret as permanently compromised. For a thorough treatment of secrets management, .gitignore strategies, and what to do when credentials leak, see the Security field guide.

Reading Git Error Messages

Git error messages are more readable than they look. They generally tell you what went wrong and often suggest a fix. Read them completely before googling. Most of the time the answer is in the message — especially with conflicts and push rejections.

Quick Reference Cheat Sheet

Daily Workflow

CommandWhat it does
git statusWhat's changed, what's staged
git diffWorking directory changes
git diff --stagedWhat's about to be committed
git add -pInteractively stage changes
git commit -m "..."Commit staged changes
git log --oneline -10Last 10 commits
git pushPush to upstream
git pullPull from upstream

Branching

CommandWhat it does
git switch -c feature/nameCreate and switch to branch
git switch -Toggle to previous branch
git branch -d feature/nameDelete merged branch
git fetch --pruneClean up remote-tracking refs
git rebase origin/mainReplay branch on latest main

Undoing

CommandWhat it does
git restore filenameDiscard working directory changes
git restore --staged filenameUnstage a file
git reset --soft HEAD~1Undo last commit, keep changes staged
git reset --hard HEAD~1Undo last commit, discard changes
git revert HEADNew commit that undoes last commit
git stashTemporarily save working changes
git stash popRestore last stash
git reflogFind "lost" commits

Investigating

CommandWhat it does
git log -S "string"Commits that changed this string
git blame filenameWho last changed each line
git bisect startBinary search for guilty commit
git diff main..featureCompare two branches
git show abc123See a specific commit

Fixing Things

CommandWhat it does
git commit --amendEdit last commit
git rebase -i HEAD~5Interactively edit last 5 commits
git cherry-pick abc123Apply a specific commit here
git push --force-with-leaseSafe force push