Git for Vibe Coders

Welcome! This guide takes you from your first commit to running AI coding agents with confidence — every concept explained visually, then practiced hands-on. Three companions will follow you through all nine parts:

9 parts23 playgrounds9 challenges100% free

Try it now: Open the — run real Git commands in your browser with live commit graphs. No install required.

Quick reference: Need a command fast? Hit ⌘K / Ctrl+K to search, or open the Git Cheat Sheet from the header for a complete command reference.

Stuck on a passage? The in the header answers from this course and cites the section it drew on. It works the way the rest of the course asks you to work: it proposes a command, you approve it, and only then does it run.

New to the terminal? This guide only assumes you can open one and type a command. If even that feels foreign, TerminalVibes — GitVibes' sister course — teaches the command line from zero, in the same style with the same in-browser playgrounds. Start there, then come back; Git lives in the terminal.

What Is Git?

Branch the risky experiments, contain the failures, merge the wins — every save becomes a recoverable moment

Git is a version control system — software that tracks every change you make to your code and lets you travel back in time to any previous version. Think of it like an unlimited undo history for your entire project, not just a single file.

If you're using AI tools like Copilot, Cursor, or Claude to write code, Git becomes even more critical. AI can generate hundreds of lines in seconds — some brilliant, some broken. Without Git, a bad AI suggestion could overwrite your working code with no way back. With Git, you can:

  • Review every AI change before it becomes permanent
  • Undo any mistake instantly — whether it's yours or the AI's
  • Branch to experiment in a safe playground without risking stable code
  • Collaborate with teammates by sharing code through a structured review process

Git is used by virtually every software team in the world. Learning it is non-negotiable if you want to write code professionally — with or without AI. But where did it come from? The story is worth two minutes of your time.

A Brief History of Git

Git was born out of a crisis. In the early 2000s the Linux kernel — one of the largest collaborative software projects on Earth — was managed with a proprietary tool called BitKeeper. In April 2005 its owner withdrew the kernel community's free license, and overnight thousands of contributors had no version control at all.

Linus Torvalds, who had created Linux 14 years earlier, looked at the alternatives and found them all too slow or too centralized. So he stopped kernel work and wrote his own. Development began on April 3, 2005 — and by April 7, Git was already managing its own source code. Two months later it shipped an entire kernel release. A tool sketched out in about ten days now runs the world's software.

That speed wasn't luck. Git was designed around three ideas that still define it: it had to be fast (applying hundreds of patches in seconds), distributed (every developer holds a complete copy of the history — no central server required), and tamper-evident (every commit is checksummed, so history can never change silently).

Linus Torvalds, creator of Linux and Git. Photo: Krd / Wikimedia Commons, CC BY-SA 4.0
2002 The Linux kernel adopts BitKeeper, a proprietary version control tool, to coordinate thousands of contributors.
Apr 2005 BitKeeper withdraws its free license. Linus Torvalds pauses kernel work and starts writing Git — within four days it is tracking its own source code.
Jun 2005 Git manages its first Linux kernel release (2.6.12), two months after the first line of code.
Jul 2005 Linus hands maintenance to Junio Hamano, who still leads Git development today.
2008 GitHub launches, and Git rapidly becomes the default way the world shares code.
Today Over 90% of developers use Git — and every AI coding tool assumes it is there.
Why 'Git'?
In British slang, a git is an unpleasant person. Linus joked: "I'm an egotistical bastard, and I name all my projects after myself. First Linux, now Git." The manual page keeps the joke going — it describes Git as "the stupid content tracker."

Why does this history matter to you? Because Git was built to let thousands of strangers change the same codebase at once without destroying each other's work. That is exactly the problem you face when an AI assistant generates code at machine speed — Git is the safety net that was already waiting for the AI era.

Installing Git

One install — then every project on your machine gets a safety net

Before anything else, you need Git on your machine. Pick your operating system:

The easiest way is to install the Xcode Command Line Tools, which include Git. Open Terminal and run:

Install Xcode CLI Tools
xcode-select --install

A dialog will pop up asking you to install. Click Install and wait a few minutes.

Alternatively, if you use Homebrew:

Or via Homebrew
brew install git

To confirm Git is installed, open a terminal and run:

Verify installation
git --version
# git version 2.55.0

If you see a version number, you're ready to go. The exact number doesn't matter much — anything 2.30+ is fine.

What Is a Repository?

A repository (or "repo") is simply a project folder that Git is tracking. Inside it, Git maintains a hidden .git folder that stores the entire history of every file — every change, by whom, and when.

A repo is a normal folder — with its entire history in the hidden .git vault beneath it

There are two kinds of repositories you'll work with:

Local Repository

The copy on your own computer. You make changes, stage them, and commit them here. It's completely private until you decide to share.

Remote Repository

A copy hosted on a service like GitHub, GitLab, or Bitbucket. This is how you share code with teammates and keep a backup in the cloud.

The typical workflow is: you clone (download) a remote repo to your machine, make changes locally, then push (upload) your changes back. Git keeps both copies in sync.

Every change in Git goes through three stages — your Working Directory (where you edit files), the Staging Area (where you prepare a snapshot), and the Repository (where snapshots are permanently saved as "commits"). Here's how they relate:

You edit files in your Working Directory, stage the ones you approve, commit them as a permanent snapshot, and push to share with your team.

Part 1

Enterprise Onboarding: Connecting to Your Codebase

"Every great journey begins with a single git clone."

Before you write your first line of code — or prompt your first AI — you need Git set up and talking to your team's repository. This is a one-time ritual: configure your identity, authenticate, and clone. Do it once and it mostly stays done (expiring tokens are the one exception — SSH keys and gh sign-in are the true set-and-forget paths).

1.1 First-Time Local Configuration

Every commit you make carries a name and email — your digital signature. Before anything else, Git needs to know who you are.

Your identity is baked into every commit — configure it once and forget about it
Note
The Problem: You have a new machine with a fresh Git installation. Before your first commit, Git requires you to set your identity -- a permanent digital signature baked into every change you make.

In an enterprise, traceability is paramount. Every commit must be tied to a specific individual. Using your correct enterprise name and email is non-negotiable.

Set your identity
git config --global user.name "Your Name"
git config --global user.email "your-enterprise-email@company.com"
Tip
The --global flag saves this for every Git repository on your computer. You only need to do this once.

Don't want to use the terminal? No problem. VS Code will prompt you to configure your identity the first time you commit — answer the prompt and it runs those two commands for you. (There's no palette command for this; the prompt or the terminal are the two paths.)

To see what's already configured — or double-check what you just set — read it back:

Verify your config
git config user.name          # One value
git config --global --list    # Everything you've set globally

While you're here, four more one-time settings pay for themselves many times over:

Quality-of-life defaults
# New repos start on 'main' (matches GitHub; becomes Git's own
# built-in default in Git 3.0)
git config --global init.defaultBranch main

# Git messages open in VS Code instead of a terminal editor
git config --global core.editor "code --wait"

# First push of a new branch just works - no upstream error
git config --global push.autoSetupRemote true

# When you pull into local commits, replay yours on top
# (you'll meet the alternative in Part 3)
git config --global pull.rebase true

Try It: Introduce Yourself to Git

Meet your constant companion for the rest of this course: the Git Playground — a real Git repository running entirely in your browser. Nothing you type here can touch your actual machine, and most lessons from here on end with one of these, so you can practice each concept the moment you learn it. First exercise: the config commands you just read.

Every commit records who made it. This sandbox only knows the default identity — run git config user.name "Your Name" (and user.email), then commit the waiting file and check git log: the save point now carries your name. A ✔ appears when the log credits someone other than the default.
Introduce Yourself to Git

Loading playground...

Vibe it

"Set up my Git config with my name and email on this machine"

"Configure Git to use VS Code as my default editor"

1.2 Authentication: Tokens & SSH Keys

Your company's code lives in a private repository. To access it, you need to prove you're allowed in — and GitHub removed password authentication for Git back in 2021. Today there are two ways to authenticate: over HTTPS with a token, or over SSH with a key pair.

A personal access token proves your identity to GitHub without a password
Warning
The Problem: Your company's code is in a private repository, and password authentication for Git operations no longer exists. You need either a Personal Access Token (for HTTPS) or an SSH key — and you should know which one fits your situation.

Option 1: HTTPS with a Personal Access Token

A Personal Access Token (PAT) is a generated secret that acts as your password, but with scoped permissions and an expiration date. GitHub now recommends fine-grained tokens, which can be limited to specific repositories:

  1. Go to GitHub SettingsDeveloper settingsPersonal access tokensFine-grained tokens
  2. Click Generate new token and name it descriptively (e.g., "Work Laptop")
  3. Set an expiration (90 days recommended)
  4. Under Repository access, choose Only select repositories and pick your project
  5. Under Permissions, set Contents to Read and write
  6. Click Generate token and copy it immediately — you won't see it again
Note
Some organizations still use the older classic tokens (same page, under "Tokens (classic)" — select the repo scope; add read:org only if you'll also sign in the gh CLI with it). If your team tells you to use one, the rest of the workflow is identical.
Clone using your token
git clone https://github.com/Your-Enterprise/your-project.git
# Username: your-github-username
# Password: your-personal-access-token

Storing Your Credentials

Save credentials (choose your OS)
# macOS - usually preconfigured to use the Keychain; if not:
git config --global credential.helper osxkeychain

# Windows - Git for Windows ships with Credential Manager; if not:
git config --global credential.helper manager

# Linux - install Git Credential Manager (git-credential-manager),
# then:
git config --global credential.helper manager
# Lightweight alternative (holds credentials in memory ~15 min):
git config --global credential.helper cache
Tip
On macOS and Windows a helper is usually already configured by the installer — run these only if Git keeps re-prompting for your token. On Linux there's no default: install Git Credential Manager (or use cache). You may see older guides suggest libsecret — on Debian/Ubuntu that helper ships as source code you'd have to compile first, so it fails out of the box. Once a helper is set, Git prompts you once and saves the credentials.

Option 2: SSH Keys

SSH flips the model. Instead of pasting a secret, you generate a key pair: a private key that never leaves your machine, and a public key you upload to GitHub. Set it up once and every clone, pull, and push just works — no tokens to renew. It's the classic "set it and forget it" choice for a machine you develop on daily.

Generate your key
ssh-keygen -t ed25519 -C "your-enterprise-email@company.com"
# Press Enter to accept the default file location,
# then choose a passphrase (recommended)
Add the key to the ssh-agent
# macOS - store the passphrase in your Keychain
eval "$(ssh-agent -s)"
ssh-add --apple-use-keychain ~/.ssh/id_ed25519

# Windows (Git Bash) & Linux
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Caution
Make it survive a reboot (macOS): without a config file, the agent forgets your key when you restart — and SSH auth mysteriously breaks days later. Create ~/.ssh/config with the lines below (on Linux, drop the UseKeychain line):
~/.ssh/config — load the key automatically
Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519
Copy your public key
# macOS
pbcopy < ~/.ssh/id_ed25519.pub

# Windows (Git Bash)
cat ~/.ssh/id_ed25519.pub | clip

# Linux - print it, then copy the output
cat ~/.ssh/id_ed25519.pub

Then go to GitHub SettingsSSH and GPG keysNew SSH key, paste the key, and save. Verify the connection works:

Test the connection
ssh -T git@github.com
# Hi your-username! You've successfully authenticated...
Important
Only ever share the .pub file. The private key (the one without an extension) must never leave your machine — treat it like the master key to your accounts.

With SSH, repository URLs look different — use the SSH tab of the green Code button when copying a clone URL:

Clone over SSH
git clone git@github.com:Your-Enterprise/your-project.git
# No username or token prompt - your key authenticates you

Which One Should You Use?

Tip
SSH if it's your own machine and you push daily — one-time setup, then it disappears from your life. A fine-grained token for scripts, CI pipelines, or short-lived access to a specific repo. Or let a tool set one of those two up for you — browser sign-in (below) stores an HTTPS token behind the scenes, no copy-pasting required.

The Modern Shortcut: GitHub CLI

In practice, many developers today never copy a token at all. The GitHub CLI (gh) is GitHub's official command-line tool. Where git talks to the repository, gh talks to GitHub itself — authentication, pull requests, issues, releases — all without leaving your terminal. Download it from cli.github.com or install it with your package manager:

Install GitHub CLI
# macOS
brew install gh

# Windows
winget install --id GitHub.cli

# Linux - see cli.github.com for your distro's instructions

Then one command handles the entire authentication setup interactively — including generating and uploading an SSH key if you ask it to:

Authenticate once, interactively
gh auth login
# Pick HTTPS or SSH, sign in through your browser,
# and it configures Git for you
Tip
Once gh auth login succeeds, every git clone, pull, and push just works — and you get bonus commands like gh pr create to open a pull request straight from your terminal.

And if you're using VS Code, it handles GitHub authentication the same way — when you clone a private repo or push for the first time, it opens your browser to sign in. No tokens to generate, copy, or store:

VS Code
VS Code automatically opens your browser to sign in to GitHub -- no tokens to manage.
Warning
The enterprise wall: SAML SSO authorization. In most companies on GitHub Enterprise Cloud, a working token or SSH key is not enough — it must also be explicitly authorized for your organization, or every clone fails with the famously misleading repository not found (the repo exists; your credential just isn't SSO-blessed). The fix takes ten seconds: GitHub → Settings → your token or SSH key → "Configure SSO" → Authorize for your org. If day one at a new job ends in ERROR: Repository not found, check this before anything else.
Vibe it

"Generate an ed25519 SSH key and add it to my GitHub account"

"Help me decide between SSH and a personal access token for this machine"

1.3 Getting the Code (Cloning the Repository)

Your team's code exists on a remote server. Cloning is the act of bringing the entire project — every file, every branch, every commit in its history — right onto your machine.

Cloning creates a complete copy of the remote repository on your machine
Note
The Problem: The code exists on the server, but not on your machine. You need to download a complete copy ("clone") of the repository.

To get a copy of the project on your machine, run the clone command with the repository URL your team shared with you:

Clone the repository
git clone https://github.com/Your-Enterprise/your-project.git
cd your-project   # The clone lands in a NEW folder named after the repo

That new folder is the repository — specifically, the hidden .git directory inside it holds the entire history, every branch, all of it. Delete .git and you're left with an ordinary folder of files. (And the #1 "Git is broken!" beginner moment: running git commands from the parent folder — check you actually cd'd in.)

The VS Code Way

  1. Open VS Code. Click "Clone Repository" on the Welcome page
  2. Or use the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) and type Git: Clone
  3. Paste the HTTPS URL. VS Code handles authentication automatically
  4. Choose a save location, then open the folder
VS Code
Use Cmd+Shift+P → 'Git: Clone' and paste the repository URL. VS Code handles the rest.

One thing to keep in mind after cloning:

Note
When you clone, you only get the default branch (e.g., main) checked out. Other branches exist as remote-tracking branches until you explicitly check them out.
Warning
If you see a .gitmodules file, stop and read this. The repo uses submodules — other Git repositories pinned inside this one. A plain clone leaves those directories empty; you need git clone --recurse-submodules <url> (or, after the fact, git submodule update --init --recursive). Two rules until you've studied them properly: don't hand-edit inside a submodule directory, and watch AI agents around them — a careless git add . can silently commit a moved submodule pointer, which is a notorious way for agents to break builds.
Vibe it

"Clone the repo at github.com/our-team/project into my projects folder"

"Clone this repository and set up the development environment"

Challenge: Sign Your Work

Loading challenge...

Part 2

The Core Safety Loop: Your "Save Game" for AI Coding

"Commit early, commit often. Every save point is a universe you can return to."

This is the heartbeat of your entire Git workflow. Every time the AI generates code, you'll follow this three-step rhythm: check what changed, approve what you trust, and freeze it in time. Master this loop and you'll never lose work again. We'll close the part with the flip side of saving: the files that should never enter history at all.

The diagram above shows the full cycle you'll repeat every time you work with AI-generated code. This chapter covers the middle of the loop — discarding is Part 4's territory, and pushing comes in Part 3. Let's break it down:

Important
This is your fundamental daily workflow. It's a three-step process: the AI makes a change, you review it, and you "save" it. In Git, this loop is Status → Stage → Commit. For this to be an effective safety net, commit after every single logical, working change.

2.1 "What Did the AI Just Do?"

You just prompted the AI to refactor a module, and it went silent for a moment. What exactly did it touch? Before you do anything, you need situational awareness.

Always check your status before staging — know exactly what the AI changed
Note
The Problem: You prompted Copilot or Cursor to "refactor this function." It applies changes across several files. What exactly did it touch?

git status is your "heads-up display." The output shows three categories:

"Changes not staged" (Red)

Files Git tracks that were modified but not yet approved for the next save.

"Untracked files" (Red)

Brand-new files the AI created that Git has never seen before.

"Changes to be committed" (Green)

Staged files — reviewed, approved, and going into the next commit. Your goal is moving the right files here.

A typical git status, all three sections
On branch feature/login
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   src/auth.py

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
        modified:   src/routes.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/session_store.py

The good news? You don't actually need to type git status in the terminal. VS Code shows you all of this visually. Just click the Source Control icon in the Activity Bar (the branch icon on the left sidebar). The badge shows how many files changed, and each file is labeled M (modified), U (untracked), or D (deleted):

VS Code
The Source Control panel is your visual 'git status' — modified (M), untracked (U), and deleted (D) files are listed with clear badges.

You can also spot changes right in the editor without opening the Source Control panel. VS Code adds colored indicators in the gutter (the narrow strip to the left of your code): green for added lines, blue for modified lines, and a red triangle for deleted lines. Click any indicator to preview the change inline:

VS Code
Colored gutter indicators let you spot changes at a glance — green (added), blue (modified), and red (deleted). Click to preview.
Vibe it

"What files did you just change? Show me a summary"

"Check git status and explain what each changed file does"

2.2 Reviewing and Staging the AI's Work

Now you know what changed. The next step is the most important one: reviewing the AI's work line by line, and only approving what you trust. This is staging — your quality gate.

Selective staging lets you approve AI changes hunk by hunk
Warning
The Problem: git status shows the AI changed five files. Blindly trusting this is how bugs are introduced. You must review every line and then "stage" the changes you approve.

Step Zero: Actually Read the Diff

git status tells you which files changed; git diff shows you what changed inside them — removed lines in red with a -, added lines in green with a +. It's the terminal twin of the VS Code Diff Editor you'll meet below, and it's the command every AI agent runs before staging (Part 6 makes it a rule).

The two diffs you'll run constantly
git diff            # Unstaged changes: working directory vs staging area
git diff --staged   # Staged changes: what the next commit will contain
Tip
Make git diff --staged your ritual final check: it shows exactly — and only — what's about to be frozen into the commit. If a secret or debug print shows up there, you just caught it in time.

Option 1: Stage All (the "trusting" add)

Stage everything
git add .
Caution
This is fast but dangerous. If the AI added a temporary debug file or a flawed change, you just approved it without review.

Option 2: Stage One File (the "surgical" add)

Stage specific file
git add src/my_file.py

Option 3: Stage Parts of a File (the "AI-first developer's" add)

Tip
This is your single most powerful review tool. The AI made multiple changes in one file, and you only want to accept some of them. git add --patch walks you through every individual block of changes ("hunk") interactively. One blind spot: it only shows modifications to tracked files — brand-new files the AI created won't appear at all. Stage those by name with git add <file> after reading them.

Here's how to start an interactive staging session:

Interactive staging
git add --patch  # or git add -p

For each hunk, Git asks: y (stage), n (skip), s (split into smaller pieces), q (quit). In the Try It: The Complete Loop playground (next section), git add -p is simplified to file-by-file staging — type y or n on the next line for each file.

The VS Code Way: Visual Staging

This is where VS Code really shines. Instead of typing cryptic y/n/s/q responses in the terminal, you get a beautiful side-by-side Diff Editor. To stage an entire file, just hover over it and click the + button:

VS Code
Hover over any file and click + to stage it. The file moves from 'Changes' to 'Staged Changes'.

When you click a file in the Changes list, VS Code opens the Diff Editor -- a side-by-side view showing the old version (left) and new version (right). This makes it easy to review exactly what changed before staging:

VS Code
The Diff Editor shows your changes side-by-side: red highlights deletions, green highlights additions. Review before you stage.

For even more precision, click a file to open the Diff Editor, then select specific lines and right-click → "Stage Selected Ranges". This is the visual equivalent of git add -p, but far easier to use:

VS Code
In the Diff Editor, select specific lines and use the gutter to stage just those changes -- even more precise than git add -p.
Vibe it

"Stage only the files related to the authentication feature"

"Show me the diff of each changed file one at a time so I can decide what to stage"

Warning
Accidentally staged everything with git add .? Use git restore --staged <file> to unstage specific files before committing. This is especially critical if you accidentally staged .env files with secrets or debug files. You'll practice exactly this in the "Unstage Secrets" playground in section 4.2 (or right now — open the playground from the header and pick the scenario).

2.3 Creating the Save Point

You've reviewed the changes and staged the ones you trust. Now it's time to freeze this moment in time — a commit is your permanent save point that you can always travel back to.

Each commit is a checkpoint you can always travel back to
Note
The Problem: You've staged your reviewed changes. Now, bundle them into an immutable "save point" -- a commit. (Immutable, not untouchable: Part 4 teaches the history-editing tools. What's written can be rewritten — but never silently changed in place.)
Create a commit with a message
git commit -m "feat: add user authentication endpoint"

The command itself is simple, but the message you write matters more than you might think.

Important
Writing Good Commit Messages: Your code shows how a change was made; your commit message must explain the why. Use Conventional Commits format:
TypeWhen to UseExample
feat:A new feature for the userfeat: add login button to homepage
fix:A bug fix for the userfix: resolve dimension mismatch
docs:Documentation only changesdocs: update API reference
style:Formatting, missing semicolons, etc.style: fix indentation in utils
refactor:Code change that neither fixes a bug nor adds a featurerefactor: simplify form validation
test:Adding or correcting teststest: add unit tests for auth
chore:Build process or tools changeschore: update dependencies
perf:A code change that improves performanceperf: cache database queries

Where the "Why" Actually Goes: the Body

The subject line above says what changed. The "why" lives in the commit body — a blank line after the subject, then as many lines of context as the change deserves. Run git commit with no -m and Git opens your editor (VS Code, if you set core.editor in Part 1) for exactly this:

A commit message with a body
fix(auth): reject expired tokens before the DB lookup

The AI's first fix checked expiry after loading the user, which
let expired tokens trigger a full query. Checking first avoids
the load and matches how the session middleware already behaves.

Save and close the editor tab, and the commit completes. (Changed your mind? Close it with an empty message and Git aborts the commit.)

Tip
AI Integration: GitHub Copilot and Cursor can analyze your staged changes and suggest a commit message. Always verify it follows your team's standards — and remember the bar: six months from now, does this message explain why?

One last habit: prove to yourself the save point exists. Right after committing, run git log --oneline -1 — one line, your hash and message. (That's also the final step Part 6 teaches agents: commit, then report the hash.)

In VS Code, committing is just as simple: type your message in the input box at the top of the Source Control panel and click the Commit button (or press Cmd+Enter / Ctrl+Enter). Notice the sparkle icon next to the input -- click it to let AI generate a commit message from your staged changes:

VS Code
The full Source Control view: your staged changes, the commit message input, and the Commit button. Click the sparkle icon to auto-generate a message.

Here's the AI commit message feature in action. After staging your changes, click the sparkle icon and Copilot will analyze your diff and write a descriptive message for you:

VS Code
Click the sparkle icon and AI generates a commit message based on your staged changes. Always review it before committing!

Try It: The Complete Loop

Run real Git commands in your browser. Type help in the terminal for the full command list.
The Core Safety Loop

Loading playground...

Vibe it

"Commit the staged changes with a good conventional commit message"

"Write a descriptive commit message for these changes and commit them"

2.4 What NOT to Commit (.gitignore)

You asked the AI to scaffold your project, and it delivered: dependencies installed, an .env file holding your API keys, and a working app. Then it helpfully runs git add . — and now your secrets, your node_modules folder, and a stray .DS_Store are all staged for your next commit. You just learned how permanent a commit is — before you freeze that mistake into history, you need to teach Git what to ignore.

A .gitignore file is the guardrail that makes 'git add .' safe — secrets and junk never enter history
Note
The Problem: AI agents move fast and stage everything. Without a guardrail, one git add . can commit API keys, gigabytes of dependencies, and OS junk into a history that everyone on your team will clone.

A .gitignore file is a plain text file at the root of your repository that lists patterns of files Git should pretend don't exist. Ignored files never show up as "untracked" in git status, and git add . silently skips them. The file itself gets committed, so the whole team — and every AI agent working in the repo — shares the same guardrail.

.gitignore — a realistic starter for a JS/Python project
# Dependencies
node_modules/
.venv/
__pycache__/
*.pyc

# Secrets & local config
.env
.env.*
!.env.example

# Build output
dist/
build/
coverage/

# Logs & caches
*.log
.cache/

# AI tool local state (personal settings stay personal)
.claude/settings.local.json
CLAUDE.local.md
.cursor/
.aider*

(Purely personal noise like .DS_Store or .idea/ belongs in your global ignore file instead — see the end of this section. And while a repo's main .gitignore lives at the root, you can drop extra ones in any subfolder to scope rules to just that corner of the project.)

Pattern Syntax Essentials

  • *.log — a glob: matches any file ending in .log, in any directory
  • dist/ — a trailing slash matches only directories (and everything inside them)
  • /config.json — a leading slash anchors the pattern to the repository root
  • docs/**/*.tmp** matches any depth of nested directories
  • !.env.example — a leading ! negates a pattern, re-including a file an earlier rule excluded (it can't re-include anything inside an excluded directory, though)

The Gotcha: Already-Tracked Files Stay Tracked

.gitignore only affects untracked files. If a file was already committed before you added it to .gitignore, Git keeps tracking it — and keeps committing your changes to it. To stop tracking a file without deleting it from your disk, remove it from the staging area (which Git's own messages call the index — same thing, older name):

Untrack a file that's already committed
git rm --cached .env
# For a whole directory:
git rm -r --cached node_modules

# Then commit the removal
git commit -m "chore: stop tracking ignored files"
Warning
If a secret was ever committed, removing it in a new commit is not enough. The key still exists in every older commit, and anyone who clones the repository can read it. Treat a pushed secret as compromised: rotate the credential immediately (revoke the key and issue a new one), then scrub it from history with a rewriting tool like git filter-repo — specialized surgery beyond this guide, but worth knowing the name. Rotation comes first; no amount of history surgery un-leaks a key that's already been seen.

A Global Ignore File for OS and Editor Junk

Files like .DS_Store (macOS) or Thumbs.db (Windows) are about your machine, not the project. Instead of asking every repository to ignore them, configure a global ignore file that applies to all your repositories:

Set up a global ignore file (once per machine)
git config --global core.excludesFile ~/.gitignore_global

echo ".DS_Store" >> ~/.gitignore_global
echo "Thumbs.db" >> ~/.gitignore_global
Tip
Rule of thumb: project artifacts (builds, dependencies, secrets) go in the repository's .gitignore so the whole team is protected; personal noise (your OS, your editor) goes in your global ignore file.

Line Endings: the Cross-Platform Papercut

Windows ends lines with CRLF, macOS and Linux with LF. On a mixed team this produces the classic horror diff: every line of the file "changed" because a teammate's editor rewrote the endings — and AI agents reformatting files make it worse. The team-level fix is a committed .gitattributes file, which overrides everyone's personal settings:

.gitattributes — one line ends the war
# Commit LF, let Git normalize what each OS checks out
* text=auto

# Files that must keep exact bytes, no conversion ever
*.png binary
*.webp binary

(You may also see core.autocrlf in older guides — that's the per-machine version of the same idea. Prefer .gitattributes: it lives in the repo, so it protects teammates and agents who never configured anything. If a diff ever claims every line changed, check line endings before anything else.)

Vibe it

"Write a .gitignore for this project — look at my stack and include editor and OS artifacts"

"Check my repo for tracked files that look like secrets or build artifacts that shouldn't be committed"

Challenge: Stage Only What You Trust

Loading challenge...

Part 3

Parallel Universes: Branching for AI Experiments

"A branch is a safe space to be wrong. Create one every time you have an idea."

Branches are what make Git magical. They let you experiment freely — try a wild AI refactor, explore an alternative architecture — without ever risking the stable codebase. Think of them as parallel timelines that you can merge back or discard entirely.

Important
The most important rule in collaborative software development: you never, ever work directly on the main branch. The main branch represents the official, stable, production-ready code. Your work must happen in an isolated "parallel universe" called a branch.

3.1 "I Have a New Idea (or AI Prompt)"

You have a feature idea — or maybe an ambitious AI prompt that might refactor half your codebase. Before you do anything, create a branch where it's safe to experiment.

Branches let you experiment without risking the stable codebase
Note
The Problem: You want to try a new feature or a massive AI-driven refactor. Before you write a single line or prompt, create a safe, isolated branch.

First, make sure you're on main and it's up to date. Then create your branch:

Create and switch to a new branch
git switch -c feature/my-new-idea
# Older equivalent: git checkout -b feature/my-new-idea

In VS Code, this is even easier. Look at the bottom-left corner of your window -- you'll see the current branch name (e.g., "main"). Click it and you'll get a dropdown where you can switch to existing branches or select "+ Create new branch..." to make a new one:

VS Code
Click the branch name in the bottom-left corner to switch branches or create a new one.

When you click the branch name, VS Code opens a Quick Pick menu listing all your branches. Select "+ Create new branch..." at the top, type a name like feature/my-new-idea, and you're immediately switched to it:

VS Code
The branch Quick Pick lets you create, switch, or check out branches without touching the terminal.
Tip
Your new branch starts exactly where main is — but it isn't a copy of anything. A branch is just a label pointing at a commit (which is why creating one is instant, and why you can have hundreds). From here on, new commits move your label while main's stays put. If the AI destroys everything, it does not matter: discard the branch and switch back. This workflow enables fearless experimentation.
Warning
Common mistake — committed to main by accident? Don't panic. If you haven't pushed yet, the fix is simple: create the feature branch (your commits come with it), then switch back to main and git reset --hard HEAD~1 to remove the commit from main. (Reading that address: HEAD is the commit you're standing on, and ~1 means "one step back" — so HEAD~1 is the previous commit, HEAD~3 three commits back.) You'll master git reset in Part 4. One precondition: make sure git status is clean first — --hard also wipes any uncommitted edits, and those have no undo. Try it yourself below:

Try It: Oops — Committed to Main

The playground starts with a payment commit on main that should be on a feature branch. Create the branch, then reset main with git reset --hard HEAD~1.
Move Commit to Feature Branch

Loading playground...

Vibe it

"Create a new branch called feature/user-auth and switch to it"

"I need to start working on the payment integration — set up a branch for me"

3.2 "My Teammate Pushed Updates" (Syncing)

You're not working in a vacuum. While you've been building your feature, your teammates have been merging theirs. Staying in sync is how you avoid painful surprises later.

Fetch checks for updates, pull downloads them, push shares your work
Note
The Problem: You've been working on your feature branch for a few days. Your teammates have merged their work into main. Your branch is now "stale."

Before diving into the commands, here's how the three main remote operations relate to each other:

Fetch downloads without merging. Pull = fetch + merge. Push uploads your commits.

Option 1: The "Safe" Sync (fetch + merge)

Two-step controlled sync
git fetch origin        # Download new commits (doesn't apply them)
git merge origin/main   # Merge the updates into your branch

Option 2: The "Easy" Sync (pull)

One-step sync
git pull origin main    # Fetch + merge in one command
Tip
As a best practice, git fetch first to see what's coming before merging. git pull is just a "black box" shortcut.
Important
When plain git pull refuses to run: if your branch and the remote have both moved (you committed locally, a teammate pushed), modern Git stops with fatal: Need to specify how to reconcile divergent branches. It's asking which strategy you want: git pull --no-rebase (merge, like this section) or git pull --rebase (replay your commits on top — the cleaner habit you'll learn in section 5.2). Pick a default once with git config --global pull.rebase true and you'll never see the error again.

VS Code makes syncing visual. Look at the status bar at the bottom of your window -- you'll see small arrows with numbers showing how many commits are incoming (to pull) and outgoing (to push). Click the sync icon (circular arrows) to pull and push in one step:

VS Code
The incoming/outgoing section shows exactly which commits you need to pull and which you'll push.

You'll also see a prominent "Sync Changes" button right in the Source Control panel. It shows the exact count of incoming and outgoing commits, so you always know what's about to happen:

VS Code
The Sync Changes button combines pull + push in one click. The numbers show incoming (↓) and outgoing (↑) commit counts.

Try It: Fetch and Merge Remote Updates

The playground simulates a remote called origin — no real network calls. After git fetch origin, run git log --oneline --all to see both local and remote branches.
Sync with Remote

Loading playground...

Vibe it

"Pull the latest changes from main and update my branch"

"Fetch from origin and tell me if my branch is behind main"

3.3 "My AI-Generated Feature is Ready" (The Pull Request)

Your feature is built, tested, and committed. But you don't just push it into production — you propose it. A pull request is a conversation: "Here's what I built. Let's review it together."

Pull requests are the quality gate between your branch and production
Note
The Problem: Your feature is complete, tested, and ready to be merged into main. You do not merge it directly. You "propose" the change via a Pull Request (PR).

Once your feature is ready, push your branch to the remote so your teammates can see it:

Push your branch to the remote
git push -u origin feature/my-new-idea
# -u links your local branch to the remote branch

Then on GitHub, you'll see a yellow banner: "feature/my-new-idea had recent pushes. Compare & pull request." Click it to create your PR.

Important
A Pull Request is a request for discussion. It is the formal, auditable gate where your human teammates review your AI-generated code, suggest changes, and ultimately "sign off" before it enters main. (One vocabulary note for job interviews: "pull request" is the GitHub/Bitbucket name. GitLab calls the identical thing a merge request (MR) — and it's a forge feature, not a Git command. Part 9 tours the forges beyond GitHub.)

The Modern PR Arc: Draft → Ready → Review → Merge

Open work-in-progress as a draft PR — visible to the team, CI running, but explicitly not asking for review yet. Click "Ready for review" when it is. Drafts stopped being a nicety in the agent era: every cloud coding agent (GitHub Copilot's coding agent, Claude, Codex, Devin) delivers its work as a draft PR for you to review — so this arc is the one you'll live in daily.

Two more things you'll meet on your first real PR:

  • The merge button is a menu. "Merge commit" preserves your branch's commits; "Squash and merge" (the most common team default) collapses the whole PR into one clean commit on main; "Rebase and merge" replays them individually. Squash is why messy WIP commits on your branch are fine — they vanish at the gate.
  • An AI reviewer may get there first. Many repos auto-request a review from GitHub Copilot (or a similar bot). Read it like a helpful-but-junior teammate: it only ever leaves comments — it never counts toward the required human approval, and it's sometimes wrong. Push back when it is.
Note
Teams enforce all of this with rulesets (repo Settings → Rules) — require a PR, require passing checks, require an approval before anything reaches main. If your push to main is rejected at work, that's not an error — that's the process working.

Writing a good PR description can feel tedious, but AI tools can help with that too:

Tip
AI Integration: GitHub Copilot can write your PR descriptions by summarizing your commits. Claude Code can even automate the PR creation process.

After the Merge: Leave the Campsite Clean

Post-merge hygiene
git switch main
git pull                  # Bring the merged work home
git branch -d feature/my-new-idea   # Delete the merged branch (-d is safe:
                                    # it refuses if work isn't merged)
git fetch --prune         # Drop tracking refs for branches deleted on GitHub

(GitHub can auto-delete the remote branch on merge — repo Settings → General → "Automatically delete head branches." Turn it on; nobody misses stale branches.)

You don't even need to leave VS Code to create a PR. Install the GitHub Pull Requests extension and you can create PRs, review diffs, add comments, approve, and merge -- all without opening your browser:

VS Code
Create Pull Requests directly in VS Code with the GitHub Pull Requests extension. AI can even generate the PR description for you.

Try It: Branch, Commit, and Push

git push in the playground updates a simulated remote — try git remote -v to see it.
Branching Workflow

Loading playground...

Vibe it

"Push this branch and create a pull request with a good description"

"Create a PR from this branch to main, summarizing all the changes we made"

Challenge: Branch Before You Build

Loading challenge...

Part 4

The "Undo" Toolkit: Reversing AI Mistakes

"The AI will break things. Your job isn't to prevent that — it's to recover instantly."

No matter how good your AI assistant is, it will occasionally hallucinate, delete the wrong file, or introduce a subtle bug. Git gives you a full spectrum of undo tools — from gentle nudges to nuclear resets. Knowing which tool to reach for in each situation is what separates a confident developer from a panicked one.

Choose the least destructive tool that solves the problem
Important
This is the most critical section. The AI will misunderstand a prompt, generate buggy code, or delete something important. Your value as an engineer is your ability to recover instantly and safely.

4.1 "Discard This Mess" (Local, Not Committed)

The AI just rewrote half your file and it's completely wrong. You haven't committed anything yet. This is the simplest undo — just throw it all away and go back to your last save point.

Discard local edits and return to the last committed state
Note
The Problem: The AI modified files and the result is completely wrong. You haven't staged or committed. You want to revert to your last save point.

The fix is simple -- one command wipes the slate clean and takes you back to your last commit:

Discard changes
git restore .              # Discard edits to tracked files
git restore src/bad_file.py  # Discard a single file

One blind spot: git restore only rewinds files Git already tracks. Brand-new files the AI scaffolded are untracked, so they survive it. Preview the leftovers with git clean -n (a dry run), then delete them with git clean -fd.

And one more trick for the road — restore doesn't have to stop at "the last commit." The most common real-world rescue is one file, from an older commit: the AI broke src/parser.py three commits ago and you only just noticed. No reset, no history surgery:

Pull one file back from the past
git restore --source=HEAD~3 src/parser.py
# The file's content is now as it was 3 commits ago;
# everything else stays untouched. Review it, then commit.

In VS Code, you don't need the terminal for this. In the Source Control panel, hover over the file you want to discard under "Changes" and click the curved arrow icon. To discard ALL changes at once, click the curved arrow next to the "Changes" header. You can also right-click any file and choose "Discard Changes".

Caution
This is a "dangerous" command: your local changes are gone forever. But in this case, that's exactly what you want.
Vibe it

"That last change broke everything — throw it all away and go back to my last commit"

"Discard all the changes you just made, they're not working"

4.2 "I Staged This by Accident" (Staged, Not Committed)

You hit git add . a bit too quickly and staged files you didn't mean to include. No worries — unstaging is completely harmless and doesn't touch your code.

Unstage files without losing your edits — a gentle undo
Note
The Problem: You used git add . and accidentally staged a file with a bad AI change. You need to "unstage" it.

No worries -- unstaging is a safe operation that simply moves a file back out of the staging area:

Unstage a file
git restore --staged src/bad_file.py
# Older equivalent: git reset HEAD src/bad_file.py

In VS Code, this is a one-click fix. In the Source Control panel, look under "Staged Changes", hover over the file you want to unstage, and click the (minus) button. The file moves right back to "Changes":

VS Code
Click the − button next to any staged file to unstage it. It moves back to the 'Changes' section.

Try It: Unstage Dangerous Files

You ran git add . too quickly and staged .env with credentials and a debug file. Unstage them with git restore --staged before committing — then remember the permanent fix from Part 2: add them to .gitignore so the next git add . can't stage them again.
Unstage Secrets & Debug Files

Loading playground...

Vibe it

"Unstage config.py, I don't want that in this commit"

"I accidentally staged everything — unstage all files except auth.py"

4.3 "I Forgot a File in My Last Commit"

You just committed — and immediately realized you forgot a file, or there's a typo in the message. Instead of creating a messy "oops" commit, you can quietly fix the last one.

Amend reopens your last commit — add the forgotten file, fix the message, seal it again
Note
The Problem: You just committed but missed a file, or there's a typo in your commit message. The commit has not been pushed yet.

Instead of creating a whole new commit, you can tack the missing file onto the one you just made:

Amend the last commit
git add src/forgotten_file.py   # Stage the missed file
git commit --amend --no-edit     # Add it to the last commit

# Or just fix the message:
git commit --amend

In VS Code, click the dropdown arrow next to the Commit button and select "Commit (Amend)". This adds your newly staged files to the last commit without needing the terminal. You can also find this under the ... menu → Commit → Commit Staged (Amend).

Warning
This rewrites your last commit. Amending is safe when nobody else could have based work on that commit — either it was never pushed, or it lives on your own personal branch and you follow up with a careful --force-with-lease push (see section 4.6). If teammates may have pulled the commit, don't amend — revert instead.
Vibe it

"I forgot to include the test file in my last commit — add it without creating a new commit"

"Fix my last commit message, it should say 'fix' not 'feat'"

4.4 "Nuke This Whole Feature" (Locally, Committed)

Sometimes the AI experiment was a dead end — three commits deep, and none of it is salvageable. If you haven't pushed yet, you can erase those commits entirely and start fresh.

Three reset modes from gentle (soft) to nuclear (hard)
Note
The Problem: Your last three commits were a single bad AI experiment. You have not pushed them. You want to permanently delete them.

This is where the nuclear option comes in. A hard reset rolls your branch back as if those commits never happened:

Hard reset: destroy commits and changes
git reset --hard HEAD~3   # Remove last 3 commits + all changes

"Remove," not quite "delete": the commits vanish from your branch, but Git keeps them around for ~30 days and the reflog (section 4.9) can bring them back. The truly unrecoverable loss is different — see the --hard card below.

The "Safer" Resets

--soft: Keep changes staged

Deletes commits but keeps changes in the Staging Area. Useful for "squashing" commits into one.

--mixed (default): Keep changes unstaged

Deletes commits but keeps changes in the Working Directory (unstaged).

--hard: Destroy everything

Removes commits AND all code changes. Your files reset to the older commit's state — including any uncommitted work sitting in your working tree, which is the one thing the reflog can never bring back. Run git status first; if it isn't clean, stash or commit before you reset.

Caution
CRITICAL: git reset rewrites history. Never use this on a branch your teammates have already pulled. This is for local cleanup only.
Vibe it

"The last 3 commits were all bad — nuke them but keep the code changes so I can redo it"

"Completely undo my last 2 commits, I want to start fresh from before them"

4.5 "I Pushed a Bug to the Team!" (Public, Pushed)

This is the "oh no" moment — you pushed a bad commit and your teammates already pulled it. You can't erase history, but you can create a new commit that perfectly reverses the damage.

Revert creates a new commit that undoes the damage — safe for shared branches
Caution
The Problem: You pushed a bad AI-generated commit. It's on main. Your teammates have already pulled it.

The WRONG Solution: You cannot use git reset. It rewrites history that others have, causing repository divergence.

The RIGHT Solution: Create a new commit that undoes the bad commit. This is a revert.

Here's how to create that revert commit in the terminal:

Safely undo a pushed commit
git log --oneline           # Find the hash: a1b2c3d
git revert --no-edit a1b2c3d # Create an inverse commit
git push                    # Push the revert

Without --no-edit, Git opens your editor so you can customize the revert message — fine once you expect it, startling the first time (especially if that editor turns out to be vim).

Notice that you're not erasing anything -- you're adding a new commit on top that reverses the damage:

Important
The bad commit stays in history, but a new "revert" commit undoes its changes. This is safe because no history is deleted. The history clearly shows: "Feature was added" → "Feature was reverted."

VS Code has this built in too. Open the ... menu in the Source Control panel -- this is your gateway to all advanced Git operations. From here you can access Commit, Changes, Pull, Push, Branch, Stash, and more:

VS Code
The ... menu is your Git command center. Look under Commit for Undo Last Commit, Commit (Amend), and other recovery options.

For reverting pushed commits, use the Source Control Graph: right-click any commit and select "Revert Commit" to create the inverse commit safely.

Vibe it

"I pushed a broken commit to main — safely undo it without rewriting history"

"Revert commit a1b2c3d, it introduced a bug in production"

4.6 The "Break Glass" Command

You rewrote local history with a reset or amend — and now Git refuses to push because local and remote have diverged. This is the emergency tool: a force push with a built-in safety net.

Force push with --force-with-lease protects teammates' work
Warning
The Problem: You used git reset or git commit --amend on a branch you already pushed. Local and remote history have diverged. Git refuses to let you push.

You have two flavors of force push, and picking the right one matters a lot:

git push --force

Replaces the server unconditionally. If a teammate pushed in the last 5 minutes, you blow their commits off the server (they survive only in that teammate's local clone).

git push --force-with-lease

Conditional force push. Only succeeds if the remote branch hasn't changed since your last fetch. Always use this instead.

One subtlety: the lease compares the remote against what you last fetched — so run git fetch first, review what changed on the remote, and only then push.

Caution
And one trap: anything that fetches in the background quietly renews the lease. VS Code's autofetch (git.autofetch), GitLens, and other IDE tooling can fetch every minute — after which --force-with-lease passes even though you never looked at what arrived. The lease proves the remote hasn't changed since the last fetch — it can't prove you reviewed it. Fetch, read, then push.
Caution
What does the error look like? When you try to push after rewriting history, Git will reject it with: ! [rejected] (non-fast-forward) — hint: Updates were rejected because the tip of your current branch is behind This is Git protecting you. But careful — the full hint goes on to suggest git pull, and after a deliberate amend or reset that's exactly wrong: pulling merges the old commit right back in, recreating the mess you just cleaned up. When you rewrote the history on purpose, the answer is the lease push, not a pull. Try it yourself below:

Try It: Reset and Force Push

Your feature branch has two bad commits already pushed. Use git reset --hard HEAD~2 to go back, watch a plain git push get rejected, then overwrite the remote with git push --force-with-lease. Never do this on shared branches!
Reset and Force Push

Loading playground...

Vibe it

"I amended a commit I already pushed and now I can't push — help me fix it safely"

"What's the safest way to force push after rewriting history on my branch?"

4.7 The Git "Undo" Recovery Matrix

The recovery matrix — match your mistake to the right undo tool

Before moving to advanced topics, here's a quick-reference matrix summarizing every undo technique and when to use it:

ScenarioCommandWhat It DoesSafe?VS Code
AI's change is bad, not committedgit restore .Discards edits to tracked files (new untracked files survive — see git clean)Safe (Local)Right-click file → "Discard Changes"
One file broke several commits agogit restore --source=HEAD~3 <file>Brings back that file's old content; history and other files untouchedSafe (Local)Timeline → click old commit → copy the old version
File staged by accidentgit restore --staged <file>Unstages a file, moving it from Staging back to ChangesSafe (Local)Right-click staged file → "Unstage Changes"
Typo in last commit messagegit commit --amendEdits the message of the most recent commitSafe (if not pushed yet)... → Commit → Commit (Amend)
Forgot a file in last commitgit add <file>
git commit --amend --no-edit
Adds new files to the most recent commitSafe (if not pushed yet)Stage files → ... → Commit Staged (Amend)
Last 3 local commits are badgit reset --hard HEAD~3Removes the last 3 commits and all their code changes (reflog can recover the commits)Local Only! (Rewrites history)... → Commit → Undo Last Commit (once per commit)
Commits "vanished" after a hard resetgit reflog
git reset --hard HEAD@{1}
Finds the lost commit in the reflog and moves the branch backSafe (Local)Terminal only (see section 4.9)
Pushed a bug to the teamgit revert <hash>Creates a new commit that is the inverse of the bad one100% Safe (Public)Source Control Graph → Right-click commit → "Revert Commit"
Reset a public branch, need to pushgit push --force-with-leaseForcefully overwrites remote, only if no one else pushedEnterprise "Break Glass"Git: Push (Force With Lease)
Stuck in "detached HEAD"git switch main
git switch -c keep-this
Reattaches HEAD to a branch — create one first if you made commits to keep (see section 4.8)Safe (Local)Click the branch name in the status bar

Try It: The Undo Toolkit

The playground includes git revert HEAD and git commit --amend. Try reverting the pushed bad commit, then amending after staging a fix.
Undo Operations

Loading playground...

4.8 Detached HEAD — Time Travel Safely

You (or the agent) checked out an old commit hash to see how the code looked before a refactor — and the terminal barks back: "You are in 'detached HEAD' state." It sounds like an injury. It isn't. It's Git telling you exactly where you are: in the past.

Detached HEAD: you're standing on a commit, not riding a branch
Note
The Problem: You ran git checkout a1b2c3d (or git checkout HEAD~2) to inspect an old version. Git warns about a "detached HEAD" and you're not sure if you broke something.

Normally, HEAD points at a branch, and the branch moves forward with every commit. When you check out a commit hash directly, HEAD points at that commit instead — no branch is along for the ride. Looking around is completely safe:

Time-travel to inspect an old commit
git log --oneline        # Find the commit you want to visit
git checkout HEAD~2      # Detach HEAD two commits back
cat src/app.py           # Look around — reading is 100% safe

The catch is committing while detached. Any commits you make there belong to no branch — the moment you switch away, nothing points at them anymore and they become orphaned. They don't disappear instantly (the reflog still remembers them — see section 4.9), but they vanish from git log and real Git will eventually garbage-collect them.

There are exactly two escape hatches, and choosing is easy:

The two ways out
# Made something worth keeping? Put a branch under your feet:
git switch -c inspect-v02

# Just sightseeing? Go back to the present:
git switch main
Tip
Think of detached HEAD as a read-only visit to the past. Inspect freely, run things, compare files. The instant you want to keep new work, run git switch -c <branch> — and even if you forget, the reflog is your safety net.

Try It: Time-Travel and Escape

Version 0.4 is misbehaving. Use git checkout HEAD~2 to visit version 0.2, inspect src/app.py, then escape — git switch -c inspect-v02 to keep a foothold, or git switch main to return to the present.
Try it: Time-travel and escape

Loading playground...

Vibe it

"Check out the commit from before yesterday's refactor so I can see how auth.py looked"

"I'm in detached HEAD state and I made a commit here — save it to a new branch and take me back to main"

4.9 The Reflog — Your Time Machine

The agent ran git reset --hard and the log now looks like two days of work never happened. Before you panic: those commits are not gone. Git keeps a private journal of every place HEAD has ever been — the reflog — and it's about to save your week.

The reflog records every HEAD movement — commits, resets, checkouts, merges
Note
The Problem: A hard reset (or a botched rebase, or an agent gone rogue) erased commits from git log. You need them back.

git log only shows commits reachable from your branches. The reflog shows everywhere HEAD has moved, reachable or not. Here's how to read it:

Anatomy of git reflog output
git reflog

a9f8e21 HEAD@{0}: reset: moving to HEAD~2         <- where you are NOW
4c7d3b9 HEAD@{1}: commit: feat: add caching layer <- one move ago (the "lost" work!)
8e2f1a0 HEAD@{2}: commit: feat: add ranking algorithm
1d0c5f4 HEAD@{3}: commit (initial): Initial commit

HEAD@{n} means "where HEAD was n moves ago" — HEAD@{0} is now, HEAD@{1} is one move back. Each line also shows the short hash, so you have two ways to name any point in time. Pick your recovery recipe:

Three recovery recipes
# 1. Undo the reset entirely — move the branch back:
git reset --hard HEAD@{1}

# 2. Rescue a single lost commit onto your current branch
#    (cherry-pick gets its own section in 5.4):
git cherry-pick 4c7d3b9

# 3. Inspect first — park a rescue branch on the lost commit:
git switch -c rescue 4c7d3b9
Warning
The reflog's limits: it is local-only — it's never pushed, and your teammates' machines each have their own. In real Git, entries expire (unreachable ones after about 30 days, the rest after about 90). And crucially: uncommitted work is NOT in the reflog — Git can only time-travel to states you committed. The real lesson of this whole part: commit early, commit often.
Tip
This completes the undo toolkit from the recovery matrix in section 4.7: restore for the working directory, revert for public history, reset for local history — and reflog when a reset itself was the mistake.

Try It: Rescue Lost Commits

An agent ran git reset --hard HEAD~2 and two commits vanished. Run git reflog to find where HEAD was before the reset, then git reset --hard HEAD@{1} to bring everything back.
Try it: Rescue lost commits

Loading playground...

Vibe it

"The last reset deleted commits I still need — check the reflog and restore them"

"Find the commit where tests still passed in the reflog and create a rescue branch on it"

The reflog answers "where did my commits go?" Its sibling question — "which commit broke this?" — has its own tool: git bisect binary-searches your history. Mark one commit as bad and one as good, and Git checks out midpoints for you to test — 1,000 commits take ten rounds, not a thousand. In the agent era this is the tool for "somewhere in the bot's forty commits, the tests started failing."

Try It: Find the Bad Commit

The tests fail on main but passed at v1.0, eight commits ago. Use git bisect plus the run-tests command to pin down the culprit in three rounds instead of eight.
Bisect: Binary-Search the History

Loading playground...

Vibe it

"The tests fail now but passed at v1.0 — bisect between them and tell me the first bad commit"

"Run git bisect to find which of your last 20 commits broke the login flow"

Challenge: Pick the Right Undo

Loading challenge...

Part 5

Advanced Scenarios: Managing a Multi-Branch Workflow

"Real projects are messy. Stash your work, resolve your conflicts, and keep moving."

By now you know the fundamentals. But real-world development is rarely linear — you'll be mid-feature when a critical bug drops, your branch will diverge from a teammate's, and two files will clash during a merge. These advanced tools handle the chaos.

Note
As you grow, you'll often work on multiple tasks at once. Your AI-driven workflow will be interrupted by urgent bugs or questions. Git provides the tools to manage this context-switching seamlessly.

5.1 "I Need to Switch Branches, but My Work Isn't Ready"

You're deep in a feature branch with ten modified files when your manager says "urgent bug on main." You can't commit half-finished work, and you can't lose it either. The stash is your escape hatch.

Stash your work-in-progress to switch context without losing anything
Note
The Problem: You're in the middle of a complex AI refactor with 10 modified files. Your manager says: "Urgent bug on main!" You can't commit half-baked work, and Git may block you from switching branches if your uncommitted changes conflict with the target branch.

The stash is a temporary, private holding area for your dirty changes.

Stash, fix, and return
# 1. Stash your changes (-u includes brand-new files)
git stash push -u -m "WIP: refactoring pipeline, AI changes"

# 2. Fix the urgent bug
git switch main
git pull
git switch -c hotfix/urgent-bug
# ... fix, test, commit, push, create PR ...

# 3. Return to your work
git switch feature/A
git stash pop
Warning
The untracked-files gotcha: a plain git stash only saves changes to tracked files. Brand-new files the AI just created — never committed, never staged — get left behind in the working tree. Add -u (--include-untracked) to take them along — that's also why VS Code's menu has a separate "Stash (Include Untracked)" item.

git stash pop re-applies your changes and removes them from the stash. Use git stash apply to keep the stash entry for reuse.

In VS Code, you can do all of this without memorizing commands. Open the ... menu in the Source Control panel -- you'll see a Stash submenu with all the options you need:

VS Code
The ... menu includes a Stash submenu with 'Stash (Include Untracked)' and 'Pop Latest Stash' -- everything you need for context-switching.

Choose "Stash (Include Untracked)" to save all your work. When you're ready to come back, go to ... → Stash → "Pop Latest Stash" to restore everything exactly where you left off.

Try It: The Stash Workflow

You're mid-refactor on feature/A when a critical bug comes in. Stash your work, fix the bug on a hotfix branch, then come back and pop the stash.
Stash: Context-Switch Safely

Loading playground...

Vibe it

"I need to switch branches but I'm not done here — save my work temporarily"

"Stash my current changes, switch to main to fix a bug, then come back and restore them"

5.2 "My Branch is Out of Date" (Rebase vs. Merge)

Your feature branch has been alive for a few days and main has moved on without you. Now you need to catch up — and Git offers two philosophies with very different trade-offs.

Merge preserves history, rebase rewrites it — choose based on your team's convention
Note
The Problem: Your feature branch is "stale." main has moved on. There are two philosophies for updating it.

You have two options for catching up with main, and each tells a different story in your commit history.

git merge main

Creates a new "Merge Commit" on your branch. Preserves the exact history -- messy but 100% accurate.

History: "Worked on feature... merged main... worked on feature..."

git rebase main

"Replays" your commits on top of the latest main. Creates a clean, linear history as if you started today. The replayed commits are re-created — same changes, brand-new commit ids.

History: main's commits, then copies of yours (the ' marks: same change, new id) — all in a straight line.

Caution
The Golden Rule of Rebasing: Never rebase a public branch (one your team is also using). Because rebase re-creates the commits with new ids, everyone else's copy of the branch still points at the old ones — you've rewritten a history they're standing on.

VS Code supports both approaches. Use the ... menu in Source Control → "Pull (Rebase)" to rebase instead of merge when pulling. For merging, use the Command Palette (Cmd+Shift+P / Ctrl+Shift+P) → "Git: Merge Branch..." and select the branch to merge.

Tip
The AI-First Developer's Choice: Since your experiment branch is your private playground, rebase is preferred to keep it clean before creating a PR. It avoids cluttering the PR with "I merged main" commits.
Important
The push after a rebase. If your branch was already on GitHub, the rebase just re-created commits the remote still has the old versions of — so a plain git push gets rejected. The correct follow-up is git push --force-with-lease — the safe force from section 4.6 — and it's only OK because this is your branch. Rebase, lease-push, open the PR: that's the full ritual.

One config gem while you're here: git config --global rebase.autostash true makes Git stash your uncommitted work automatically before a rebase (or pull --rebase) and pop it after — dissolving the "Git blocked my switch" problem 5.1 opened with, for the rebase case at least.

Try It: Merge vs. Rebase

Your feature branch and main have diverged. Try git merge main first, then reset and try git rebase main to compare the resulting history.
Merge vs. Rebase

Loading playground...

Vibe it

"My branch is behind main — rebase my changes on top of the latest main"

"Update my feature branch with the latest changes from main using rebase"

Rebase has one more trick: git rebase -i (interactive) lets you rewrite your own branch's history commit by commit — squash five "wip" commits into one, reword a sloppy message, drop an experiment entirely. It's how a messy working branch becomes a clean, reviewable PR. The Golden Rule applies double here: only ever on commits that haven't been shared.

Try It: Squash the WIP

Your branch works but its history is three wip: commits. Run git rebase -i main, reword the first commit to something worthy, and squash the other two into it.
Interactive Rebase: Clean the History

Loading playground...

Vibe it

"Squash my wip commits on this branch into one commit with a proper message"

"Clean up this branch history with an interactive rebase before I open the PR"

5.3 "We Both Edited the Same File" (Merge Conflicts)

This is the moment every developer dreads the first time — and handles calmly by the tenth. Two people changed the same lines, and Git needs a human to decide which version wins.

When two edits collide, Git asks you to choose — this is a merge conflict
Warning
The Problem: You run git pull (or merge main into your branch) and Git halts with CONFLICT. You and a teammate edited the same lines. Git needs you, the human, to resolve it.

Don't panic -- conflicts look intimidating at first, but they follow a simple pattern. Git inserts special markers into your file to show you exactly where the disagreement is.

The Conflict Markers

What you'll see in src/model.py
<<<<<<< HEAD
x = 10
# AI refactor
=======
x = 5
# teammate fix
>>>>>>> main

Everything between <<<<<<< HEAD and the ======= divider is your side (here, the AI's refactor on your branch). Everything below the divider is the incoming side — the label after >>>>>>> names where it came from — the branch you merged (like main here; after a pull it can be a commit id or a longer label instead).

Delete all the markers (<<<, ===, >>>) and edit the code to be the correct final version, then stage and commit. In the playground, you can write the resolved file with echo 'x = 10' > src/model.py.

Not ready to deal with it? There's an eject button here too: git merge --abort cancels the merge and returns your branch to exactly how it was before you ran git merge — the same guilt-free escape you'll meet again with rebase (5.5) and cherry-pick (5.4).

The VS Code Way (The Superior Way)

Editing conflict markers by hand works, but VS Code makes the whole process much more visual and less error-prone.

Tip
This is one of the best features of the IDE. Open a conflicted file and VS Code highlights each block inline, with clickable links right above it: "Accept Current" | "Accept Incoming" | "Accept Both". For tangled, overlapping conflicts, click "Resolve in Merge Editor" to open the full 3-way view:

Left Pane: "Incoming" (teammate's changes)
Right Pane: "Current" (your changes)
Bottom Pane: "Result" (what will be saved)
VS Code
VS Code highlights conflicts inline with clickable actions: Accept Current Change, Accept Incoming Change, or Accept Both Changes.

For complex conflicts with multiple overlapping changes, click "Resolve in Merge Editor" to open the full 3-way view. This gives you the most control over the final result:

VS Code
The 3-way Merge Editor: Incoming changes (left), your changes (right), and the final result (bottom). Use checkboxes to select which changes to keep.

Try It: Resolving a Merge Conflict

The scenario starts mid-merge with conflict markers in src/model.py. Use echo to overwrite the file, then git add and git commit to finish.
Merge Conflict Resolution

Loading playground...

Vibe it

"I have a merge conflict in model.py — help me resolve it, keeping both changes"

"Show me the conflicts and suggest the best resolution for each one"

5.4 Cherry-Pick — Take Only the Gems

Your AI experiment branch is a mess — half-finished rewrites, abandoned TODOs, dead ends. But buried in the middle is one brilliant commit: a currency rounding fix that actually works. You don't want the branch. You want that one commit.

Cherry-pick copies exactly one commit onto your branch and leaves the rest behind
Note
The Problem: An experiment branch has one valuable fix buried among junk commits. Merging would bring everything. You want to extract a single commit.

git cherry-pick copies one commit's changes onto your current branch as a new commit — same change, same message, but a brand-new hash, because it now sits on a different parent. The original commit stays untouched on its branch.

Pick the gem, leave the junk
git log --oneline --all      # Find the gem's hash on the experiment branch
git switch main              # Stand on the branch that should receive it
git cherry-pick e4f5a6b      # Copy exactly that commit here
git log --oneline            # The fix is on main — the junk is not

For an audit trail, add -x: it appends "(cherry picked from commit e4f5a6b...)" to the message, so anyone reading main later can trace where the fix came from. Perfect for the reject-the-PR workflow below.

When to prefer it over merging: merge when the whole branch is worth keeping; cherry-pick when only part of it is. And because a cherry-picked commit replays changes onto code that may have moved on, conflicts can happen here too — the escape hatches mirror rebase exactly:

If the pick conflicts
# Fix the conflicted file, then:
git add <file>
git cherry-pick --continue

# Or walk away as if nothing happened:
git cherry-pick --abort
Tip
The AI reviewing strategy — "reject the PR, cherry-pick the gems": when an agent's branch is 80% noise, don't agonize over salvaging it. Close the PR, cherry-pick the one or two commits that earned their place, and delete the branch. This pairs perfectly with the guidance in section 6.1 on teaching your AI to work in small, single-purpose commits — small commits are what make cherry-picking possible.

Try It: Cherry-Pick the Gem

The experiment branch has a half-finished dashboard rewrite and one gem: a currency rounding fix. Use git log --oneline --all to find it, then git cherry-pick it onto main — and check src/billing.py to confirm the fix arrived.
Cherry-Pick: Take Only the Gem

Loading playground...

Vibe it

"The experiment branch is mostly junk but the rounding fix is good — cherry-pick just that commit onto main"

"Find the commit that fixed the login bug on the old branch and apply only that one here"

5.5 When Rebase Goes Wrong — Conflicts, Continue, Abort

Section 5.2 sold you on rebase for clean history — but it skipped the scary part. Your feature branch tuned src/config.py, main changed the same lines, and halfway through the rebase Git slams the brakes. Here's how to read the wreck and drive out of it.

A paused rebase is a fork in the road: fix and continue, or abort and go home
Note
The Problem: You ran git rebase main and Git stopped with CONFLICT. The rebase is half-done and you're not sure whether to fix it or flee.

Remember what rebase does: it replays your commits one at a time on top of the latest main. If a replayed commit touches lines that main also changed, Git can't guess the winner — so it pauses mid-replay and hands you the keys. Run git status to see the paused state: it reports a rebase in progress and lists the conflicted files under "unmerged paths". The files contain the conflict markers you learned to read in section 5.3 — with one crucial twist.

Warning
The sides are swapped during a rebase. Git rebuilds your branch by standing on main and replaying your commits onto it — so <<<<<<< HEAD is main's version, and the bottom block is your own commit arriving as the "incoming" change. Exactly backwards from a merge. This is the single most famous rebase trap — read the labels, not the positions.
What you'll see in src/config.py — note who is who
<<<<<<< HEAD
TIMEOUT = 10     # main's version (HEAD during a rebase!)
=======
TIMEOUT = 120    # your commit, being replayed
>>>>>>> a1b2c3d (feat: raise the worker timeout)

From here, it's a three-step ritual — the same one every time:

The three-step ritual
# 1. Fix the file — remove the markers, keep the right code
echo 'TIMEOUT = 120' > src/config.py

# 2. Tell Git the conflict is resolved
git add src/config.py

# 3. Resume the replay
git rebase --continue

If more commits remain, Git keeps replaying — and may pause again on the next conflict. Just repeat the ritual until the rebase completes. And if at any point you're lost, confused, or late for dinner, there's a guilt-free eject button:

The guilt-free escape
git rebase --abort   # Everything returns EXACTLY as it was before the rebase
Important
--abort is the reason rebasing your local branches is always safe to attempt. The rebase doesn't touch your original commits until it finishes — abort at any pause and your branch is restored exactly as it was. The Golden Rule from 5.2 still stands (never rebase shared branches), but on your own branch, the worst case is typing one command and being back where you started.
Tip
Resolving the same conflict twice? Never again: git config --global rerere.enabled true turns on reuse recorded resolution — Git remembers how you resolved each conflict and replays the answer the next time the identical conflict appears. On a long-lived AI branch that gets rebased onto main every day, this is the difference between resolving the agent's conflict once and resolving it every single morning.

Try It: Survive a Rebase Conflict

Your feature/tuning branch raised the timeout in src/config.py, but main lowered the same line. Run git rebase main, then cat src/config.py to read the conflict markers. Fix the file with echo, then git add and git rebase --continue. Try git rebase --abort too — watch everything snap back.
Rebase: Resolve, Continue, or Abort

Loading playground...

Vibe it

"My rebase stopped on a conflict in config.py — resolve it keeping the higher timeout and continue"

"This rebase is a mess — abort it and get my branch back to how it was"

5.6 Tags & Releases — Naming Your Milestones

Branches move — every commit drags them forward. But some moments deserve a permanent name: the commit you shipped, the version that passed the audit, the last known-good state before a big AI refactor. That's what tags are: labels that stick to one commit, forever.

Tags are permanent name plates on commits — branches move, tags stay

Git has two kinds of tags, and the choice matters more than it looks:

Lightweight: git tag v1.1.0

Just a name pointing at a commit. No author, no date, no message. Fine for private bookmarks.

Annotated: git tag -a v1.1.0 -m "..."

A full object with author, date, and message. Always annotate releases — future-you will want to know who cut it and why.

Naming convention: most projects use semantic versioningMAJOR.MINOR.PATCH. Bump MAJOR when you break existing users (v1.x → v2.0.0), MINOR when you add features compatibly (v1.0 → v1.1.0), and PATCH for pure bug fixes (v1.1.0 → v1.1.1). One glance at a version tells users how scared to be.

Cut and inspect a release
git tag -a v1.1.0 -m "Release: import support"   # Tag HEAD
git tag                                          # List all tags
git log --oneline                                # Tags appear as decorations
git show v1.0.0                                  # Inspect what a release points at

One surprise: in real Git, git push does not send tags. You push them explicitly — git push origin v1.1.0 for one tag, or git push origin --tags for all of them — though beware: --tags throws every tag at the remote, including private bookmarks like the pre-refactor checkpoint below. git push --follow-tags is the discerning version: it sends only annotated tags on commits you're pushing — which is exactly why this section told you to annotate releases and keep bookmarks lightweight. Need to remove a tag? git tag -d v1.1.0 deletes it locally — and if you deleted one by mistake, just re-tag the same commit (find it with git log --oneline, or fetch the tag back from the remote if you'd already pushed it).

Tip
The AI-first habit: tag before letting an agent loose on a big refactor — git tag -a pre-refactor -m "Last known good before agent rewrite". If things go sideways, a named restore point beats scrolling through the reflog trying to remember which HEAD@{n} was the good one.

Try It: Cut a Release

v1.0.0 shipped two commits ago and the import feature is ready. Tag the current commit with git tag -a v1.1.0 -m "Release: import support", list your tags, then use git show v1.1.0 and git log --oneline to see what each release points at.
Tags: Cut a Release

Loading playground...

Vibe it

"Tag the current commit as v2.0.0 with a message summarizing what's in this release"

"Before you start the refactor, create an annotated tag so we can get back to this exact state"

Challenge: Rescue the Buried Fix

Loading challenge...

Part 6

Git for AI Agents: Rules, Guardrails, and Scale

"An agent with good Git habits is worth ten that just move fast."

You now know the toolkit — the safety loop, branches, the undo arsenal, the advanced moves. This chapter turns that knowledge into a system your AI assistants follow as reliably as you do: written rules they read (AGENTS.md and skills), mechanical guardrails they cannot skip (hooks), and a layout that lets several agents work the same repository at once without stepping on each other (worktrees).

6.1 Teaching Your AI to Use Git

Encode your conventions once and let AI agents follow them automatically

The next level is not just using Git to manage AI — it's encoding your Git workflow so agents follow it automatically. Modern tools share a simple pattern: always-on project instructions for conventions that apply everywhere, plus skills for detailed procedures that load only when relevant.

Note
The Problem: Your agent commits straight to main, invents its own branch names, and writes messages like "fixed stuff" — and re-explaining your team's rules at the start of every session doesn't scale. The rules need to live in the repository, where every agent reads them automatically.

Always-on instructions: AGENTS.md & friends

AGENTS.md is the cross-tool standard for project-wide rules — an open spec (now stewarded by the Linux Foundation) read by OpenAI Codex, Cursor, GitHub Copilot, Gemini CLI, and twenty-plus other agents. In monorepos, the nearest file to the code you're editing takes precedence. The one big exception: Claude Code reads CLAUDE.md, not AGENTS.md (as of mid-2026). The documented bridge is a one-line CLAUDE.md containing @AGENTS.md — an import that pulls the shared file in — or a symlink (ln -s AGENTS.md CLAUDE.md). In VS Code, add .github/copilot-instructions.md for Copilot-specific standards. Put your team's Git constitution here: branch naming, Conventional Commits, "never reset shared branches," PR expectations. Keep it concise — this loads on every session.

Agent Skills (SKILL.md)

Skills are the open Agent Skills format: a folder with a SKILL.md file (name + description in YAML frontmatter, instructions in the body). The format is portable; the folder isn't yet — each tool has its own discovery path, like .agents/skills/ for Codex and .claude/skills/ for Claude Code — so check where your agent looks. Either way they're version-controlled workflows your whole team shares. Agents discover skills at startup and load the full instructions only when a task matches — perfect for detailed Git procedures without bloating every chat. Example: a git-save-game/ skill that walks through the full save-game ritual — branch, review, stage, commit — before every PR.

VS Code: scoped rules & custom agents

For rules that apply only to certain files, add *.instructions.md files under .github/instructions/ with an applyTo glob in the frontmatter. For specialized personas, define custom agents as .github/agents/*.agent.md — YAML frontmatter sets the name, description, tools, and model; the body holds focused instructions (e.g., a "Git Review" agent that inspects staged diffs and suggests commit splits). Use /create-instruction or /create-agent in VS Code chat to scaffold these files.

CI & repository automations

Repo instructions also power GitHub Copilot code review and cloud agents on pull requests. Combine with GitHub Actions to auto-review PRs, flag risky Git operations, or update docs when code changes — Git remains the audit trail even when agents operate in CI.

A complete AGENTS.md you can steal

Theory is nice, but what does a Git constitution actually look like? Here's a complete, copy-pastable example. Drop it in your repo root, adjust the branch prefixes and test command to match your project, and every agent that opens the repo inherits your rules.

AGENTS.md
# Git Rules for AI Agents

## Branches
- Never commit directly to main. Always work on a branch.
- Branch names: feature/<topic> for features, fix/<topic> for
  bug fixes, agent/<topic> for autonomous or experimental work.
- One branch per task. Do not reuse branches across tasks.

## Before staging anything
- Run `git status` and `git diff` first. Read the diff.
- Stage only files you intentionally changed. Never `git add .`
  when the diff contains files you don't recognize.

## Commits
- Use Conventional Commits with a scope:
  feat(auth): ..., fix(api): ..., refactor(parser): ...,
  docs(readme): ..., test(payments): ..., chore(deps): ...
- Keep commits small and focused: one logical change per commit.
- Run the test suite (`npm test`) before every commit.
  If tests fail, fix them or ask — do not commit broken code.

## Pushing and history
- Never use `git push --force`. If a push is rejected after a
  rebase, use `git push --force-with-lease` — and only on your
  own feature branches, never on main or shared branches.
- Never rewrite history that has been pushed and shared
  (no rebase/reset/amend on public commits — use `git revert`).

## When unsure
- Stop and ask before any destructive command
  (reset --hard, clean, filter-repo, branch -D).

For multi-step procedures, a skill keeps the detail out of your always-on instructions. Here's a small worked example: a "save-game checkpoint" skill that encodes the status-review-stage-commit ritual from Part 2 so the agent runs it the same way every time.

.agents/skills/save-game-checkpoint/SKILL.md
---
name: save-game-checkpoint
description: Create a clean Git checkpoint after a working change.
---

# Save-game checkpoint

1. Run `git status` — list every modified and untracked file.
2. Run `git diff` and summarize the changes for the user.
3. Flag anything unexpected (unrelated files, debug prints,
   secrets, lockfile churn) before proceeding.
4. Stage only the files that belong to this change:
   `git add <file> <file>` — never `git add .`
5. Commit with a Conventional Commit message and scope, e.g.
   `git commit -m "feat(payments): add refund endpoint"`
6. Confirm with `git log --oneline -1` and report the hash.

Reviewing large AI diffs

Even well-instructed agents produce big diffs, and a 40-file diff read top-to-bottom is where review discipline goes to die. Triage instead: run git diff --stat first to see which files changed and by how much — that one screen tells you whether the change matches the task you assigned. Then review file by file with git diff <file>, starting with the files you'd least expect to change. When only part of the work is good, use git add -p to stage the keepers hunk by hunk and discard the rest.

Tip
Reject, don't salvage. When a huge AI pull request is mostly wrong, resist the urge to edit it into shape — salvage-editing someone else's sprawling diff takes longer than redoing it and hides mistakes you didn't notice. Close the PR, git cherry-pick the few good commits onto a fresh branch, and re-prompt for the rest with tighter scope. Cheap branches (Part 3) exist precisely so throwing work away costs nothing.

Who Wrote This — You or the Machine?

Six months from now, someone running git blame will want to know whether a human decided this line or an agent generated it. Three layers of attribution, weakest to strongest:

  • Trailers. Claude Code and Copilot append Co-Authored-By: lines to their commits by default — you'll see these in your own history. Useful as a hint, but easy to strip or forget, so never treat them as a measurement of anything.
  • Per-worktree identity. Running agents in worktrees (6.3)? Give each its own name so blame tells Agent A from Agent B: git config extensions.worktreeConfig true, then git config --worktree user.name "Agent A (auth)" inside each worktree.
  • Signing. The strong claim runs the other way: commits you sign are verifiably yours. Modern setup is SSH-based and reuses the key from Part 1 (gpg.format ssh + commit.gpgsign true); GitHub shows "Verified" badges, and its vigilant mode flags anything unsigned that claims to be you. In a world of agents committing under borrowed names, "the human signed off here" is worth making cryptographic.
Vibe it

"Read AGENTS.md and follow its Git rules for every change in this session"

"Draft an AGENTS.md with our Git branch naming and Conventional Commits rules, then create a save-game-checkpoint SKILL.md to match"

6.2 Automating Quality with Git Hooks

You told your agent: "always run the tests before committing." It did — for the first three commits. Then, deep in a refactor, it forgot, and a broken commit landed in history. Rules that live in a prompt are suggestions. Rules that live in Git are law. That's what hooks are for.

A hook is a script Git runs at key moments — a mechanical gate no commit can skip
Note
The Problem: You can't rely on yourself — or an AI agent — to remember to lint, test, and format before every commit. You need the check to happen automatically, every time, with no memory required.

A Git hook is a script that Git runs automatically at a specific moment — before a commit is created, after a merge, before a push. Hooks live in the .git/hooks/ directory of your repository, where Git puts a set of .sample files to get you started. One crucial detail: the .git directory is never committed, so hooks are not versioned — they don't travel with a clone. We'll fix that in a moment.

A Minimal pre-commit Hook

The pre-commit hook runs before Git creates a commit. If the script exits with a non-zero status, the commit is blocked. Create a file named .git/hooks/pre-commit (no file extension) and make it executable:

.git/hooks/pre-commit
#!/bin/sh
echo "Running checks before commit..."

npm run lint || { echo "Lint failed - commit blocked." >&2; exit 1; }
npm test || { echo "Tests failed - commit blocked." >&2; exit 1; }
Make it executable (once)
chmod +x .git/hooks/pre-commit

From now on, every git commit in this repository runs your lint and test suite first. If either fails, nothing gets committed — no exceptions, no forgetting.

Enforcing Commit Message Standards

The commit-msg hook receives the path to a file containing the proposed commit message. Here's a compact one that enforces Conventional Commits (messages like feat: add login form):

.git/hooks/commit-msg
#!/bin/sh
# Git's own merge and revert commits ("Merge branch ...", "Revert ...")
# also pass through this hook - wave them through.
grep -qE '^(Merge|Revert)' "$1" && exit 0

if ! grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\(.+\))?!?: .+' "$1"; then
  echo "Commit message must follow Conventional Commits, e.g. 'feat: add login form'" >&2
  exit 1
fi

Making Hooks Shareable

Since .git/hooks/ isn't versioned, your teammates (and their agents) won't get your hooks automatically. The lightweight fix is a committed hooks folder plus core.hooksPath:

Option 1: a versioned hooks folder
mkdir .githooks
# move your hook scripts into .githooks/, then:
git config core.hooksPath .githooks

# commit the folder - each teammate runs the config command once

In the JavaScript world, Husky is the popular tool that automates exactly this — it wires up the hooks path when anyone runs npm install:

Option 2: Husky
npm install --save-dev husky
npx husky init

# init already created .husky/pre-commit (it runs "npm test").
# Overwrite it with whatever checks you want:
echo "npm run lint && npm test" > .husky/pre-commit
Note
Fine print: pre-commit hooks run against your working tree, not the staged snapshot. If you stage only part of a file with git add -p, the tests pass on code that isn't what you're committing. Tools like lint-staged exist to close exactly that gap.
Important
Hooks fire on agent commits too. When Claude Code, Cursor, or any AI agent runs git commit, your pre-commit and commit-msg hooks run exactly as if you'd typed the command yourself. This is how you enforce standards on AI-authored code without trusting the AI to remember — the agent literally cannot commit failing code or a sloppy message. Better yet, agents read the error output and usually fix the problem and retry on their own.

One escape hatch to know about: git commit --no-verify skips the pre-commit and commit-msg hooks entirely. It exists for genuine emergencies — a broken hook blocking a critical hotfix — but use it sparingly. Every --no-verify is a hole in your safety net, and it's a habit you especially don't want your agents learning.

Important
Which means: client hooks are seatbelts, not laws. Anything that can run git commit can also run it with --no-verify — and agents do learn that trick from error loops. The layer nothing can skip lives on the server: branch protection rulesets on main (require a PR, require passing CI, require an approval — section 3.3). Hooks catch problems in seconds on the developer's machine; the ruleset is the law at the gate. Use both.
Note
A naming collision to keep straight: your agent tools have "hooks" too — Claude Code hooks, Cursor hooks, VS Code agent hooks — which fire on agent lifecycle events (before a tool runs, after an edit). Those govern the agent; the hooks in this section live in the repository and govern anyone who commits, human or machine. They complement each other: an agent hook can stop a bad command before it runs, a Git hook stops a bad commit no matter who makes it.

Try It: The Hooks Say No

This repo has (simulated) husky hooks installed. An agent left a BREAKPOINT in src/app.py — try to commit it and watch pre-commit block you, then get vetoed by commit-msg for a sloppy message. Fix both properly.
Hooks: Mechanical Guardrails

Loading playground...

Vibe it

"Set up a pre-commit hook that runs our lint and test scripts and blocks the commit if either fails"

"Add a commit-msg hook enforcing Conventional Commits, and make the hooks shareable with the team via Husky"

6.3 Parallel Agents with Git Worktrees

One .git, many working directories — run an agent per worktree with zero interference

Here's the endgame scenario: you want Agent A refactoring the auth module while Agent B builds the payments feature — in the same repository, at the same time. If both agents share one working directory, they'll trample each other's files, and every git switch by one agent yanks the floor out from under the other. Worktrees solve this.

A worktree is an extra working directory attached to the same repository. All worktrees share one .git database — same commits, same branches, same remotes — but each directory has its own checked-out branch, its own files on disk, and its own staging area. Creating one takes a second, because nothing is copied except the files of the branch you check out.

One agent per worktree
# From your main checkout (~/repos/proj on main)
git worktree add ../proj-auth feature/auth-refactor
git worktree add ../proj-payments feature/payments

# See every working directory attached to this repo
git worktree list
# /Users/you/repos/proj           abc1234 [main]
# /Users/you/repos/proj-auth      def5678 [feature/auth-refactor]
# /Users/you/repos/proj-payments  9ab0cde [feature/payments]

# Open a terminal in each directory and start one agent per worktree:
#   Terminal 1: cd ../proj-auth      -> Agent A refactors auth
#   Terminal 2: cd ../proj-payments  -> Agent B builds payments

If the branch doesn't exist yet, git worktree add -b feature/payments ../proj-payments creates it in one step. Note that a branch can only be checked out in one worktree at a time — Git enforces this, which is exactly the isolation guarantee you want: two agents can never be editing the same branch's files.

Every worktree commits into the same repository — the branches just live in different folders until they're merged.

Why not just clone the repo twice? Clones work, but worktrees are better on every axis: the object database is shared (no duplicated gigabytes), creation is instant, remotes and config come along for free, and a commit made in one worktree is immediately visible from all the others — no pushing and pulling between your own directories. When both agents are done, merging their branches is the normal PR flow from Part 3: push each branch, open a pull request, review the diff, merge. Nothing about worktrees changes how integration works.

A few habits keep this tidy: name each worktree directory after its branch (proj-auth for feature/auth-refactor) so you always know which terminal belongs to which agent, and clean up when a branch merges.

Cleanup after the merge
# Remove a worktree you're done with (its branch survives)
git worktree remove ../proj-auth

# If you deleted a worktree folder manually, clear the stale record
git worktree prune
Warning
Two sharp edges. First, worktrees share the repository — local config (.git/config) and the stash list are per-repo, not per-worktree, so a git stash made in one worktree is visible (and poppable) from all of them. Second, always create worktrees outside the main repo folder (../proj-auth, not ./proj-auth) — a worktree nested inside the repo shows up as an untracked directory, and an agent running git add . will sweep it up as a confusing embedded-repo pointer (Git prints a warning — which everyone ignores).
Caution
The #1 practical gotcha: a fresh worktree is code-only. It contains the branch's tracked files and nothing else — no node_modules/, no .venv, no untracked .env. Agent B's first command will fail until someone runs the install step there, and two agents starting dev servers will fight over the same port. Budget one setup command per worktree (and a port per agent) into your plan — or into the agent's instructions.

And a sign of how central this pattern has become: the agent tools now create worktrees for you. Claude Code has claude --worktree (agents get isolated worktrees under .claude/worktrees/, auto-cleaned when untouched), Cursor spins up a worktree per parallel agent, and desktop agent apps do it per session. When you find a mystery directory or a worktree-quiet-fox branch, that's what it was — git worktree list is how you audit them, and now you know how to clean them up.

Important
Checkpoints are not commits. Claude Code and Cursor both auto-checkpoint the agent's edits so you can rewind a session — genuinely useful, and genuinely not Git. Checkpoints are session-local: they don't capture what shell commands did, they can't be pushed or shared, and they expire with the session. The rule of thumb: rewind with checkpoints between commits; save with commits. Anything worth keeping past the current session goes into a real commit — the durable layer this whole guide is about.

Two monorepo-scale companions worth knowing by name: git sparse-checkout set pkg/api materializes only the directories an agent actually needs (a worktree per agent, a package per worktree, nothing else on disk), and git maintenance start schedules background housekeeping — worth turning on once a fleet of agents starts churning out objects faster than any human team ever did.

Try It: One Repo, Three Working Directories

Give Agent A and Agent B a worktree each, run into the branch-exclusivity guard on purpose, then clean up with git worktree remove. (The playground simulates the bookkeeping — the terminal stays in the main worktree.)
Worktrees: Parallel Agents

Loading playground...

Vibe it

"Create a worktree for branch feature/payments at ../proj-payments and start working there"

"List all worktrees in this repo, and remove any whose branches are already merged into main"

Challenge: Read the Gate First

Loading challenge...

Part 7

Your AI-Assisted Cockpit: Mastering Git in VS Code

"The best tools disappear into your workflow. VS Code makes Git visual, intuitive, and fast."

Everything you've learned so far in the terminal works beautifully from VS Code's built-in Git UI. Staging, committing, branching, merging, resolving conflicts — all with rich visual feedback and just a few clicks. Let's explore the cockpit.

VS Code bundles a rich set of Git tools right into the editor, so you can do almost everything without opening a terminal.

Note
While the command line is powerful, the VS Code UI is your "cockpit." It provides rich, visual feedback that makes abstract Git concepts concrete.

7.1 The Source Control View

This single panel replaces a dozen terminal commands. Status, staging, committing, branching — it's all here in one visual interface that makes Git feel approachable.

VS Code's Source Control panel — your visual command center for Git

Everything you've learned maps directly to the UI:

Changes

Your working directory (git status). Modified and untracked files appear here.

Staged Changes

Your staging area. Files you've approved for the next commit.

Commit Box

Your git commit -m "...". Type the message and click the checkmark.

... Menu

All advanced commands, tucked into submenus: Pull, Push, Stash, Branch, and Commit (where Amend and Undo Last Commit live).

Here's what it looks like in action. Press Ctrl+Shift+G to open the Source Control panel from anywhere — and yes, it's Ctrl even on a Mac (⌘⇧G is taken by search):

VS Code
Your command center: the Source Control panel shows everything at a glance -- changed files, staged files, commit input, and the branch graph.

Below the commit area, you'll find the Source Control Graph -- a visual representation of your commit history and branch structure. This is incredibly helpful for understanding how branches relate to each other:

VS Code
The Source Control Graph visualizes your commit history and branch structure -- a powerful way to understand how branches relate.
Vibe it

"Show me how to stage and commit changes using the VS Code Source Control panel"

"Walk me through the VS Code Git workflow without using the terminal"

7.2 The Timeline View & GitLens

When you're staring at a line of code and wondering "who changed this, and why?" — VS Code has the answer built right in. Timeline and GitLens turn your editor into a time machine.

The Timeline pane: the full history of one file, one click per commit
Note
The Problem: An AI changed a line of code, and you have no idea why or when.

Open any file, then look in the Explorer panel for the "Timeline" pane at the bottom. This shows the complete commit history for that specific file. Click any commit to see a diff of what it changed. The Timeline also mixes in Local History snapshots of your saves — and those entries have a right-click → "Restore Contents" that can rescue even changes you never committed.

Here's the Timeline in action. Each entry represents a commit that touched this file -- click any entry to see exactly what changed in that commit:

VS Code
The Timeline view (in the Explorer panel) shows the complete commit history for any file. Click any entry to see the diff.

VS Code also has built-in Git Blame. Put your cursor on any line of code and the status bar shows who last changed it and when (an always-on inline annotation is one setting away — search Settings for "blame"). The GitLens extension goes deeper — rich line history, comparisons across branches and commits — though many of its advanced features now sit behind a paid Pro plan. Start with the built-ins; add GitLens when you hit their ceiling:

VS Code
Built-in Git blame shows the author and commit message for the current line right in the status bar.
Vibe it

"Show me the history of changes to this file — who changed what and when"

"Who last modified this function and what was the commit message?"

7.3 The 3-Way Merge Editor

We touched on merge conflicts earlier -- now let's look at the tool that makes resolving them almost enjoyable.

Yours on one side, theirs on the other — you compose the result in the middle
Important
This tool transforms merge conflicts from a terrifying, marker-filled text-editing nightmare into a visual, point-and-click process. This alone is a reason to use VS Code for Git integration.

When a conflict occurs, VS Code highlights the conflicting files in the Source Control view. Clicking one opens it with the inline conflict markers you know from section 5.3 — look for the "Resolve in Merge Editor" button in the corner to switch to the 3-way view with three panes:

Left Pane: "Incoming" Your teammate's changes
Right Pane: "Current" Your local changes
Bottom Pane: "Result" The final merged output
Warning
One trap to know: those labels describe a merge. During a rebase, they swap — "Current" is the branch you're rebasing onto, and "Incoming" is your own replayed commit (the same side-flip explained in section 5.5). Read the code, not just the labels.

Here's the merge editor in action. Use the checkboxes next to each change to select which version you want to keep -- or manually edit the Result pane at the bottom for a custom resolution:

VS Code
The 3-way Merge Editor transforms scary merge conflicts into a visual, point-and-click experience. Use the checkboxes to select changes.

And if you have GitHub Copilot installed, there's an even easier option. VS Code can use AI to analyze both sides of a conflict and suggest an intelligent resolution. Look for the "Resolve with AI" option in the merge editor:

VS Code
GitHub Copilot can analyze conflicting changes and suggest an intelligent resolution -- the future of merge conflict handling.
Vibe it

"I have a merge conflict — open the VS Code merge editor and help me resolve it"

"Accept the incoming changes for all conflicts in this file"

Challenge: The Cockpit, By Hand

Loading challenge...

Part 8

Ship It: CI, Bots, and Releases

"The robots handle the vigilance so you can spend your attention on judgment."

Everything so far happened between you, your agent, and your repository. But open any active project on GitHub and you'll see an entourage you didn't create: green checkmarks appearing on every pull request, PRs opened by accounts named dependabot and release-please, security scans running on a schedule, version numbers bumping themselves. None of it is magic, and none of it is optional knowledge anymore — this machinery is how modern software actually ships, and every piece of it is built from things you already know: branches, commits, PRs, and tags.

One lens makes the whole chapter click: everything here is either a safety net (it catches mistakes before users see them) or a memory (it records what happened and why, for the future maintainer — usually you). CI checks are safety nets. Changelogs and releases are memories. The bots just run both without being asked.

8.1 CI — Where the Green Checkmark Comes From

Every push gets a fresh machine and the full gauntlet — green means the gate opens
Note
The Problem: "It works on my machine" — but your machine has files that aren't committed, packages that aren't declared, and a you that forgot to run the tests. Multiply that by an AI agent pushing ten branches a day, and hoping everyone remembered to check everything stops being a plan.

CI — Continuous Integration — is the fix, and the idea is almost embarrassingly simple: every time anyone pushes, a robot builds the project from scratch and runs every check against it. Not "when someone remembers." Every push, every PR, every time. On GitHub the robot service is called GitHub Actions: each run spins up a runner — a fresh, disposable Linux machine in the cloud — that clones your repo, installs dependencies from the lockfile, and works through the checklist. If everything passes, your PR gets the green checkmark. If anything fails, you get a red X and a log pointing at the failure — minutes after pushing, not weeks later in production.

The checklist itself is just a YAML file committed to your repo, which means it's versioned, reviewed, and branch-protected like everything else. A real, minimal one:

.github/workflows/ci.yml
name: CI
on:
  pull_request:
  push:
    branches: [main]

jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci        # install EXACTLY the lockfile versions
      - run: npm run lint  # style + suspicious patterns
      - run: npm test      # the whole suite, every time
      - run: npm run build # prove a production build works

Read it top to bottom: on these triggers, run these steps. That's the entire programming model. The steps are the same commands you (or the hooks you wrote in Automating with Hooks) run locally — the difference is where and always. A pre-commit hook is a seatbelt you buckle on your own machine, and --no-verify unbuckles it. CI runs on a machine nobody can sweet-talk. Hooks are the seatbelt; CI is the law.

Branch protection (the rulesets from Pull Requests) is what wires the checkmark to the merge button: no green, no merge — for you, your teammates, and every agent equally.

The sibling acronym, CD — Continuous Delivery or Deployment — is what happens after green: code that reaches main ships to users automatically. Many projects (including this site) deploy on every merge — a second workflow builds the app and publishes it the moment a PR lands. That immediacy is exactly why the gate in front of main matters: when merging is shipping, "we'll fix it before the release" is not a sentence that exists.

Tip
Why vibe coders should care most: CI is the one reviewer that scales with your agents. You can't personally re-run the test suite for every branch three agents push in parallel — but the runner can, does, and never gets tired. Green checks are what let you supervise outcomes instead of babysitting every command.
Vibe it

"Add a GitHub Actions workflow to my repo that runs lint, tests, and a build on every pull request"

"My tests pass locally but fail in CI — walk me through the usual suspects (lockfile, node version, environment)"

8.2 The Robot Coworkers: Dependabot, CodeQL & Friends

Bots open PRs like everyone else — and face the same green-check gate

Once CI guards the gate, something clever becomes possible: you can let robots propose changes, because no proposal — human, agent, or bot — gets through without passing the same checks. A bot's PR is a completely ordinary PR: a branch, a diff, a conversation, a checkmark. You already know how to review one. Meet the three coworkers you'll see most.

Dependabot — the dependency gardener

Your project stands on dozens of open-source packages, and dependencies age like food, not wine: every one has its own release stream and, occasionally, its own security holes. Dependabot watches all of them and opens PRs on your behalf — branches named dependabot/npm_and_yarn/... with commits like chore(deps): bump lodash from 4.17.20 to 4.17.21. The elegant part: its PR triggers your CI. The robot proposes, your test suite disposes. If the update breaks the build, you find out in the PR — not in production.

CodeQL — the code detective

GitHub's code-scanning engine treats your codebase like a database and runs queries written by security researchers against it — hundreds of known-dangerous shapes, like user input flowing unsanitized into HTML (cross-site scripting) or into file paths. It runs on PRs and on a schedule, so when a new attack pattern is discovered, your old code gets re-checked too. Think of it as a security specialist who re-reads your entire repo every week and only speaks up on a match.

Secret scanning — the leak alarm

Remember the staged .env drama from What NOT to Commit? GitHub runs a last line of defense: it recognizes the formats of API keys and tokens and — with push protection on — refuses the push outright. A blocked push is a gift. Treat any key that reaches a public commit as burned, and rotate it.

Dependabot is configured with — you guessed it — a YAML file in the repo. The one setting worth knowing on day one is grouping, which turns thirty tiny weekly PRs into one digestible one:

.github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
    groups:
      minor-and-patch:
        update-types: ["minor", "patch"]
        # major bumps stay separate — those need real review

Two quieter pieces complete the safety net. The lockfile (package-lock.json) pins the exact version of every package and every package's packages, so your laptop, CI, and production all install byte-identical dependencies — it's why the lockfile belongs in Git even though you never edit it by hand. And supply-chain pinning: careful repos reference third-party Actions by full commit hash instead of a friendly tag. You know from Tags & Releases that a tag is just a movable label — and if an attacker compromises an Action's repo, they can quietly move v4 to malicious code. A commit hash can't be moved. Same Git concept, now a security boundary.

Here's what a healthy history looks like with the robots at work — one of these commits was written by a human:

git log on a well-tended repo
git log --oneline -n 4
e4f5a6b feat: add csv export
b7c8d9e chore(deps): bump the npm group with 3 updates
a1b2c3d chore(main): release 1.4.2 (#118)
9f8e7d6 fix: handle empty header row
Warning
Bots are agents with one narrow job. Everything Part 6 taught you about AI agents applies: read the diff before you merge, be extra awake for major version bumps (breaking changes ride in on those), and never grant a bot more permissions than its one job needs. The green check tells you the tests still pass — it cannot tell you whether the new major version quietly changed a behavior your tests never covered.

Try It: Review the Robot's PR

Dependabot has pushed a branch. Inspect exactly what it wants to change with git diff, merge it, and clean up the branch — the same moves the "Merge" button does for you on GitHub.
Review the Robot's PR

Loading playground...

Vibe it

"Set up Dependabot for my repo with weekly, grouped minor/patch updates"

"Dependabot opened a major-version bump PR — read the changelog of the dependency and tell me what could break"

8.3 Releases on Autopilot: SemVer, Conventional Commits, release-please

Structured commit messages are the fuel — the changelog and version number write themselves

A green merge says the code is good. A release answers two different questions: what do we call this state, and what changed since the last one? In Tags & Releases you did this by hand — decided a version number, wrote an annotated tag. This lesson is about the grammar behind those version numbers, and the robot that does the paperwork.

Semantic Versioning: the number is a promise

A version like 2.4.1 reads as major.minor.patch, and each position carries a promise to whoever upgrades: a patch bump (2.4.1 → 2.4.2) means "bug fixes only, upgrade blind"; a minor bump (2.4 → 2.5) means "new features, nothing you rely on changed"; a major bump (2 → 3) means "something breaks — read the notes before touching it." That's why Dependabot's major-version PRs deserve your full attention while patch bumps barely need a glance: the versioning scheme is literally telling you how scared to be.

Conventional Commits: the payoff

Since Committing you've been writing feat: and fix: prefixes, and in Automating with Hooks a hook started enforcing them. Here's the payoff: those prefixes map straight onto SemVer. fix: means the next release is at least a patch. feat: promotes it to a minor. A feat!: or a BREAKING CHANGE: footer forces a major. Your commit history stopped being prose and became data — which means a machine can read it.

release-please: the release accountant

release-please (Google's oddly polite release bot) watches main and keeps a running draft of the next release. It reads every conventional commit since the last tag, computes the right version bump, and opens — a pull request. The PR contains exactly two things: an updated CHANGELOG.md grouping your commits into Features and Bug Fixes, and the version bump. As more commits land on main, the bot quietly amends its own PR. When you decide it's release time, you merge the PR like any other — and the bot tags the commit and publishes a GitHub Release. If you've ever wondered what a commit like chore(main): release 1.1.0 (#42) is: that's someone merging the accountant's paperwork.

The changelog it maintains is the memory half of this chapter at its purest — the human-readable answer to "what changed since 1.0.0?", assembled from messages you were already writing:

CHANGELOG.md — written by the robot, from your commits
## 1.1.0 (2026-07-17)

### Features

* add csv export (#31)

### Bug Fixes

* handle empty header row (#33)
Note
A release is not a deploy. If your project deploys on every merge (8.1), users may be running code from ten minutes ago while your latest release is v1.1.0 from last week. Deploying is code reaching users; releasing is giving a state a name, a changelog entry, and a tag you can return to. Small tools may release without deploying anything; a website deploys constantly and releases occasionally, as a bookmark.

Try It: Be release-please for a Day

Two conventional commits have landed since v1.0.0. Do the accountant's job by hand exactly once — read the log, write the changelog, commit the paperwork, cut the tag — and you'll never wonder what the bot does again.
Be release-please for a Day

Loading playground...

Vibe it

"Set up release-please for my repository and explain what its first PR will contain"

"Read my commits since the last tag and tell me the next version number — and why"

Challenge: Review the Robot

Loading challenge...

Part 9

Conclusion: Best Practices for AI-Augmented Teams

"Git isn't just version control — it's the bridge between human intent and AI capability."

You've learned the full toolkit. Now let's put it all together into a cohesive workflow — the daily rhythm that keeps you productive, your code safe, and your AI assistants working within guardrails you control.

9.1 The AI-First Workflow (Summary)

The complete 8-step AI-first Git workflow — your daily rhythm

This is your new "save game" loop — the practical rhythm between you and your agent. Follow these 8 steps for every piece of work. Encode your Git conventions once in the repo (see Teaching AI Git) so agents follow them automatically instead of re-explaining branch rules in every chat. And remember: you can practice any step of this loop in the — real Git commands, right in your browser.

Here's what each step looks like in practice, along with the commands you'll use.

1

Branch

Create an isolated branch so the AI can never touch main directly. git switch -c ai-experiment/new-feature

2

Generate

Work with your AI agent to implement the change. With AGENTS.md and skills configured, it already knows your branch naming, commit format, and safety rules.

3

Review

Use git add -p or VS Code "Stage Selected Ranges" to review every line.

4

Save

git commit -m "feat: <message>" -- Commit small, commit often.

5

Sync

git fetch origin followed by git rebase origin/main.

6

Push

git push --force-with-lease if you rebased. Updates remote safely.

7

Propose

Create a Pull Request for human review.

8

Recover

If you push a mistake, never reset a public branch. Always use git revert.

Vibe it

"Walk me through the full Git workflow for starting a new feature from scratch"

"I just finished coding — what Git steps should I follow before creating a PR?"

9.2 Quick Reference Card

Keep this cheat sheet handy — terminal commands and their VS Code equivalents

Keep this handy. It covers the most common Git tasks with both the terminal command and the VS Code equivalent, so you can use whichever feels more natural.

TaskCommandVS Code
Check what changedgit statusSource Control panel
Stage specific linesgit add -pStage Selected Ranges
Commit changesgit commit -m "feat: ..."Type message + checkmark
Create new branchgit switch -c feature/nameClick branch name (bottom-left)
Discard local changesgit restore .Discard Changes
Undo last commit (keep)git reset --soft HEAD~1... menu: Commit → Undo Last Commit
Revert public commitgit revert <hash>Revert Commit
Stash work in progressgit stash push -m "message"... menu: Stash
Update branchgit fetch && git rebase origin/main... menu: Pull (Rebase)
Safe force pushgit push --force-with-leaseGit: Push (Force With Lease) — needs the git.allowForcePush setting
Practice all commandsTry it yourself tabs in Parts 2–5

9.3 The Final Challenge

Reading is not knowing. Here's the exam — one repository, three simultaneous messes, no step-by-step instructions. Everything you need is in Parts 2 through 5, and the playground will tell you the moment you've won.

Three simultaneous messes, one repository — everything from Parts 2–5 at once
Important
Your mission: a payment feature was committed straight to main, a live Stripe key is sitting staged and one careless commit away from leaking, and the cleaned-up main still needs its v1.0.0 release tag. Fix all three, in any order.

Try It: The Final Challenge

Start with git status and git log --oneline to survey the damage. A ✔ appears in the terminal when all three goals are met — no partial credit.
The Final Challenge

Loading playground...

The Skill Checklist

Beyond the challenge, here's the honest self-test. Check each item only when you could do it right now, without looking anything up. (Saved locally in your browser — nobody's grading you but you.)

9.4 Keep Learning — The References That Matter

This guide ends here — the history graph keeps going

TerminalVibes — the sister course

Git lives in the terminal, and the terminal deserves the same treatment this guide gave Git. The sister course teaches bash from zero — navigating, pipes, permissions, scripts, and auditing what AI agents run — with the same in-browser playgrounds and the same no-signup deal. If any command line moment in this course felt shaky, this is where to firm it up.

You've practiced everything here in a real repository — but Git is deep, and the best references are worth knowing by name. These six will cover you from quick lookups to true mastery:

git-scm.com — the official Git site

Downloads, release notes, and the authoritative command reference — the same pages git help <command> shows you locally.

Pro Git — the book, free forever

The definitive deep dive, from first commit to Git internals. When you want to know why Git works the way it does, this is the answer.

Learn Git Branching — branching puzzles

Thirty minutes of visual rebase-and-merge puzzles. A great gym for the branch topology instincts you started building in Parts 3 and 5 — the muscle memory will save you hours.

GitHub Docs — the collaboration layer

Pull requests, protected branches, Actions, and everything else that lives above Git itself at most workplaces.

Oh Shit, Git!?! — panic-mode recipes

Blunt, funny, and correct recovery recipes for the moments Part 4 trained you for. Keep it bookmarked next to your reflog.

Oh My Git! — Git as a video game

An open-source game that visualizes Git's internals live as you play cards and type commands. The gentlest way to make the commit graph feel physical — and genuinely fun.

GitHub Is a Choice, Not a Given

This guide uses GitHub because it's where most of the industry (and most of the AI-agent ecosystem) lives — but here's an honest secret: almost nothing you learned is GitHub-specific. Git itself is decentralized; every clone carries the full history, and the "forge" — GitHub, or any of the sites below — is just the hosting and collaboration layer on top. Even the pull request is a forge invention, not a Git feature. Switching forges is one command (git remote set-url origin <new-url>) and a push. The alternatives worth knowing:

GitLab — the whole-pipeline platform

GitHub's biggest rival, and the one you're most likely to meet at work. Pull requests are called merge requests (MRs) — same thing, different name. Its edge: one integrated application from issue to CI/CD to deployment to security scanning (it had built-in pipelines years before GitHub Actions existed), and an open-source core you can self-host for free — which is why regulated companies that can't put code on someone else's cloud often run their own GitLab. What GitHub has that it doesn't: the network — the world's largest open-source community, the Actions marketplace, and the deepest AI-agent integrations (Copilot, Agent HQ).

Bitbucket — the Atlassian citizen

Atlassian's forge. Its reason to exist is deep, native integration with Jira and Confluence — branch from a ticket, and the ticket tracks the PR's whole lifecycle automatically. If your company runs on Jira, you may well find your code here. Outside that ecosystem it offers little GitHub doesn't, and its open-source presence is small.

Codeberg & Forgejo — the community option

Codeberg is a nonprofit, donation-funded forge run for open source — no ads, no tracking, no AI training on your code, and that is the pitch. It runs Forgejo, free software you can self-host yourself as a single small binary (its ancestor Gitea works the same way) — the lightweight answer when a whole GitLab is overkill. The trade-off is the same network effect in reverse: fewer eyes, fewer integrations, no agent ecosystem.

SourceHut — Git the old way, on purpose

A deliberately minimal, JavaScript-free forge built around Git's original collaboration model: patches reviewed over email, the way the Linux kernel still works. No pull-request button at all. Worth knowing because it proves the point above — the PR is a convention, not a law — and because some significant projects genuinely work this way.

(Also out there: Azure DevOps in Microsoft-stack enterprises, and experimental peer-to-peer forges like Radicle with no central server at all. Wherever you land: same Git, same commands, same you.)

Note
And a glimpse past Git: Jujutsu (jj) is the most credible next-generation version control tool — a Git-compatible frontend that stores real Git commits, so your team never has to know you're using it. It snapshots your working copy automatically (every command is undoable with jj undo), which is making it popular for agent-heavy workflows. Still pre-1.0 and evolving fast — but everything you learned here transfers, because underneath, it is Git. Steve Klabnik's tutorial is the place to start when you're curious.
Important
Final Thoughts: Your AI assistants are powerful tools that lack context and accountability. Git is your system of accountability. It provides the immutable history, the instant "undo" button, and the human-in-the-loop review layer that transforms high-velocity AI coding from a risky experiment into a professional, safe, and scalable engineering discipline. Master it, and you'll transform AI-assisted coding into a superpower.
Challenge: Three Messes, No Hints

Loading challenge...