Why Git is non-negotiable with AI agents
Tina Huang said something important: vibe coders who didn't know Git destroyed their codebases when AI agents went wrong. This is not hypothetical. An AI coding agent that confidently refactors the wrong thing will touch 20 files in 30 seconds. Without Git, you have no way to undo that.
With Git, you can always say: "Revert to 10 minutes ago." That's the safety net. Your job is to commit frequently enough that the safety net is there when you need it.
The mental model
Git tracks a history of snapshots of your code. Each snapshot is a commit. A commit records: what changed, when, who changed it, and a message explaining why.
Three places your code lives in Git:
- Working directory โ your actual files on disk. Unsaved changes live here.
- Staging area โ files you've explicitly said "include this in the next commit." You choose what goes in each commit.
- Repository (.git) โ the permanent history. Once committed, a change is recorded forever (with an asterisk for advanced operations).
The commands you'll use 95% of the time
# Check current state โ run this constantly
git status
# See exactly what changed
git diff
git diff --staged # what's staged (about to be committed)
# Stage files
git add file.py # specific file
git add src/ # whole directory
git add -p # interactive โ stage specific chunks (powerful!)
# Commit
git commit -m "feat: add task priority validation"
# Push to GitHub
git push
# Pull latest from GitHub
git pull
# See history
git log --oneline # compact one-line view
git log --oneline --graph # with branch visualization
Branches โ work in isolation
A branch is a parallel version of your code where you can make changes without affecting the main codebase. Every feature should be its own branch.
# Create and switch to new branch
git checkout -b feature/task-priorities
# (modern equivalent)
git switch -c feature/task-priorities
# Switch between branches
git switch main
git switch feature/task-priorities
# See all branches
git branch
# Merge your feature branch into main
git switch main
git merge feature/task-priorities
# Delete branch after merging
git branch -d feature/task-priorities
Merge vs rebase โ the one thing to know
Merge combines two branches and creates a merge commit. The history shows that the branches existed and were joined. Clear, safe, standard.
Rebase replays your commits on top of another branch, rewriting history to look like you branched off later. Produces a cleaner, linear history. Never rebase shared branches โ if someone else is working off the same branch, you'll break their history.
Rule for now: use merge. Learn rebase once you're comfortable.
Writing good commit messages
This matters more than most beginners think. In six months, a good commit message tells you why a change was made. A bad one tells you nothing.
# โ Useless commit messages
git commit -m "fix"
git commit -m "changes"
git commit -m "asdf"
# โ Good commit messages โ what changed and why
git commit -m "fix: prevent division by zero in weight calculation"
git commit -m "feat: add priority queue for task ordering"
git commit -m "refactor: extract user validation into separate function"
The conventional commit format โ type: description โ is widely used and makes scanning history much faster. Common types: feat (new feature), fix (bug fix), refactor (code cleanup, no behaviour change), docs, test, chore.
Reading git log and git blame
When you join a codebase or review someone's AI-generated code, git log and git blame are invaluable.
# See compact history with file changes
git log --oneline --stat
# See what a specific commit changed
git show a3f8c21
# Who last changed each line of a file (and when)
git blame src/calculator.py
# History for a specific file
git log --oneline src/calculator.py
# Find when a specific string was added
git log -S "def process_weight" --source --all
git blame is your friend when reading code and wondering "why is this written this way?" โ you can find the commit that introduced it and read the message. This is how you understand context in an unfamiliar codebase.
Recovering from mistakes
Everyone makes mistakes. The important thing is knowing which recovery command to use:
# Undo staged changes (unstage, but keep the file changes)
git restore --staged file.py
# Discard changes in working directory (permanent โ file goes back to last commit)
git restore file.py
# Undo the last commit, keep changes staged
git reset --soft HEAD~1
# Undo the last commit, keep changes unstaged
git reset HEAD~1
# Create a new commit that undoes a previous one (safe โ doesn't rewrite history)
git revert a3f8c21
# Save work-in-progress without committing (e.g. to switch branches quickly)
git stash
git stash pop # bring it back
git reset --hard permanently discards uncommitted changes. There's no undo. Use git stash if you're unsure โ you can always pop the stash later.
Secrets and .gitignore
This section is critical โ it's the kind of mistake that leaks API keys to the public internet and costs real money or causes security incidents.
API keys, passwords, database URLs, OAuth tokens โ none of these should ever appear in a file that gets committed. GitHub scans public repos for common secret patterns and notifies providers (like Anthropic, AWS) when they're found. They revoke the keys immediately. You then need to rotate them, which is annoying and potentially costly.
The pattern: put secrets in a .env file, add it to .gitignore. Your code reads from the environment, not from hardcoded strings.
# .env โ never committed
OPENAI_API_KEY=sk-abc123...
DATABASE_URL=postgresql://user:password@localhost/mydb
# .gitignore โ tells Git to ignore these files
.env
.env.local
__pycache__/
*.pyc
.venv/
node_modules/
# Your code reads from environment
import os
api_key = os.environ["OPENAI_API_KEY"] # raises if missing
# or with python-dotenv:
from dotenv import load_dotenv
load_dotenv()
api_key = os.getenv("OPENAI_API_KEY")
If you accidentally commit a secret
If you realise immediately (before pushing): git reset HEAD~1, remove the secret, re-commit. If you've already pushed: rotate the key immediately โ assume it's already been seen. Then use git filter-branch or git filter-repo to remove it from history. But rotating the key is the critical step.
Pull Requests โ how code review works
A Pull Request (PR) is how you propose changes to a codebase. Even when working solo, PRs are valuable because they give you a diff view of all your changes before they land on main. Reviewing your own PR catches more than you'd expect.
A good PR:
- Has a title that describes what it does
- Has a description that explains why โ what problem does this solve?
- Is small โ one thing at a time, reviewable in under 30 minutes
- Has tests if it adds or changes logic
- Doesn't include unrelated changes ("while I was here I also fixed...")