Git & GitHub
Version control from first principles — commits, branches, pull requests, and the workflows real teams actually use.
Who This Is For
Developers, builders, and technically-minded people who use git but feel like they're only using 20% of it. Or people who've avoided it and want to actually understand what's happening, not just memorize commands.
This is a 45-minute read. By the end, you'll understand what git is actually doing under the hood, be fluent in the daily workflow, write commit messages that serve you six months later, manage branches with confidence, resolve merge conflicts without panic, collaborate through pull requests effectively, and understand how git fits into modern AI-assisted development.
Why Git Matters More Now
Git is the backbone of every serious software project. It's also increasingly the backbone of AI-assisted development — where you might make 10x more changes in the same time, which makes good git hygiene 10x more important, not less.
When you're working with Claude Code, Cursor, or any agentic coding tool, things move fast. The AI makes changes across multiple files, refactors things, tries approaches. Without clean git checkpoints, a single bad session can be hard to untangle. With them, you can always roll back to a known-good state, understand what changed and why, and delegate confidently because you're not afraid of losing work.
Git also tells the story of your project. A year from now, when you're wondering why some piece of code exists in a particular way, the git history either explains it or it doesn't — depending on how you used it.
Git Is a Snapshot Database
Git doesn't store diffs (the difference between versions). It stores complete snapshots of your entire project at every commit.
Each snapshot is identified by a hash — a long string like 3a7f9c2b4d... — that's mathematically derived from the content. Change a single character in any file, and you get a completely different hash. This is why git can tell immediately if anything has changed.
When git seems to store things efficiently (and it does — a million commits don't use a million times the space), that's because it deduplicates identical files across snapshots. If 900 of your files didn't change between commit A and commit B, those 900 files are stored once, not twice.
Three Places Your Code Lives
At any moment, your code exists in three places.
Working directory — the actual files on your disk. This is what you see in your editor. Git knows about these files but isn't actively tracking them until you stage them.
Staging area (index) — a prepared snapshot in progress. When you git add something, you're putting it in the staging area. Think of it as the table where you arrange exactly what goes into the next commit.
Repository (.git folder) — the permanent record. When you git commit, the staging area becomes a commit that joins the history forever. Everything in the .git folder is git's database.
Working Directory → [git add] → Staging Area → [git commit] → Repository
Commits Are a Linked Chain
Each commit knows its parent commit. Following the chain backward from the latest commit gives you the entire history.
This chain is the thing git operates on — branches are just named pointers to a specific commit in the chain, not separate copies of anything.
Local and Remote
Your local git repository is complete and fully functional without any network connection.
You can commit, branch, merge, and inspect history entirely offline. The "remote" (usually GitHub) is just another copy of the repository that you sync with via push and pull. They're peers, not client and server in the traditional sense.
This is different from older version control systems where the server was the authority. In git, every clone is a full copy. This makes git fast, resilient, and flexible — and also means you have the full history locally, always.
Configuration
Before anything else, tell git who you are. These values get embedded in every commit you make.
git config --global user.name "Your Name" git config --global user.email "[email protected]"
A few other configurations worth setting once:
git config --global core.editor "nano" # or "code --wait" for VS Code git config --global init.defaultBranch main # use 'main' for new repos git config --global pull.rebase false # use merge on pull by default git config --global core.autocrlf input # macOS/Linux: normalize line endings
Check your current configuration:
git config --list # see all settings git config --global --edit # open the config file directly
~/.gitconfig. Project-specific configs live at .git/config in each repo and override global settings.Setting Up GitHub Authentication
To push and pull from GitHub without typing your password constantly, use SSH keys.
ssh-keygen -t ed25519 -C "[email protected]" # generate a key pair cat ~/.ssh/id_ed25519.pub # copy the public key
Add the public key to GitHub at Settings → SSH and GPG keys → New SSH key.
ssh -T [email protected] # should say "Hi username!"
Now clone repos with SSH URLs ([email protected]:user/repo.git) and authentication is automatic.
Starting and Cloning Repositories
Starting from Scratch
mkdir myproject cd myproject git init # creates .git folder, initializes repo git add README.md # stage a file git commit -m "initial commit" # first commit
Then connect to a remote:
git remote add origin [email protected]:yourname/myproject.git git push -u origin main # push and set upstream tracking
-u flag sets the upstream — after this, git push and git pull know where to go without you specifying.Cloning an Existing Repository
git clone [email protected]:user/repo.git # clone into folder named 'repo' git clone [email protected]:user/repo.git myname # clone into folder named 'myname' git clone --depth 1 [email protected]:user/repo.git # shallow clone: latest commit only
Shallow clones are useful for large repos where you don't need the full history — CI/CD pipelines and quick explorations especially.
Add, Commit, Push
Check Status Constantly
git status # what's changed? what's staged? git diff # what changed in working directory (not yet staged)? git diff --staged # what's staged and ready to commit?
git status is your orientation. Run it constantly. Before staging, before committing, after operations that might have changed things. It's fast and costs nothing.
Staging
git add filename.txt # stage a specific file git add . # stage everything in current directory (careful) git add -p # interactive: stage parts of files (patch mode) git add -u # stage only tracked files that changed (not new files)
git add -p is underused and powerful. It walks you through each changed "hunk" of each file and asks whether to stage it. This is how you make precise, atomic commits even when you've been making multiple changes at once.y = stage this hunk n = skip s = split into smaller hunks e = manually edit q = quitCommitting
git commit -m "Add user authentication endpoint" # commit with inline message git commit # opens editor for longer message git commit -am "Fix typo" # stage all tracked changes and commit
-am shorthand is convenient but skips the review step. Use it only for small, obvious changes where you know exactly what you're committing.Viewing What You've Done
git log # full log git log --oneline # compact: one line per commit git log --oneline -10 # last 10 commits git log --oneline --graph # with ASCII branch graph git log --author="Name" # commits by a specific person git log -- filename.txt # commits that touched a specific file git show <hash> # see a specific commit in detail git show HEAD # see the latest commit git show HEAD~3 # see the commit 3 before HEAD
Pushing and Pulling
git push # push to upstream (set via -u) git push origin main # explicit: push main to origin git push --force-with-lease # safer force push: fails if remote has unexpected changes git pull # fetch + merge from upstream git fetch # download remote changes but don't merge yet git fetch --prune # fetch and remove references to deleted remote branches
git fetch without merge is useful when you want to see what's changed remotely before deciding how to integrate it. It's always safe — it just updates your remote-tracking branches.Writing Good Commit Messages
Commit messages are time-traveling communication. You're writing to your future self and your teammates who will read git history while debugging, reviewing, or understanding the codebase months later.
The Anatomy of a Good Commit Message
Short summary line (50 characters or less, imperative mood) Optional body: explain the why, not the what. The what is visible in the diff. The why is not. Why did you make this change? What problem does it solve? Were there alternatives you considered? - Bullet points work in the body - Keep body lines to ~72 characters - Reference issues: Closes #123
The Imperative Mood Rule
Git itself uses imperative mood for its auto-generated messages: "Merge branch X", "Revert commit Y". Following this convention makes your history read consistently.
Add email validation to signup formFix race condition in authentication handlerRemove deprecated user_v1 API endpointsAdded email validation (past tense)Adding email validation (progressive)email validation (not imperative, not descriptive)fix stuff (useless)What to Put in the Body
The body is for explaining things the diff can't show:
Increase connection pool size from 5 to 20 The application was hitting timeout errors under load because all connections in the pool were occupied during traffic spikes. Profiling showed we were spending 40% of request time waiting for a pool slot. 20 connections is based on our current server capacity. Revisit if we scale beyond 8 app servers. Related to monitoring alert: #482
Six months later, this commit tells you exactly why the change was made, what the data showed, and what assumptions it's based on. A commit that just says "increase connection pool size" leaves all that knowledge on the floor.
Conventional Commits
A standardized format that makes history machine-readable and enables automated tooling.
feat: add cursor-based pagination to search API fix: prevent null pointer in user profile lookup docs: update API authentication guide refactor: extract validation logic into shared utility chore: update dependencies to latest minor versions test: add integration tests for checkout flow perf: cache product catalog queries for 5 minutes
Format: type(optional scope): description
| Type | When to use |
|---|---|
| feat | New feature |
| fix | Bug fix |
| docs | Documentation only |
| refactor | Code restructure, no behavior change |
| chore | Maintenance, dependency updates |
| test | Adding or fixing tests |
| perf | Performance improvement |
| ci | CI/CD pipeline changes |
The scope is optional but useful in larger codebases: feat(auth): add OAuth provider support. For breaking changes, add ! after the type: feat!: change API response format.
Atomic Commits
Each commit should represent one complete, logical change that could be reverted independently without breaking anything else.
Why this matters: git bisect does binary search through commits to find bugs. If your commits mix unrelated concerns, bisect becomes useless. Code review is also harder — reviewers can't isolate what to focus on. And reverting "add logging" from a commit that also fixes a production bug means you lose the bug fix too.
The discipline: when you've been working and made multiple unrelated changes, use git add -p to carefully stage only the changes that belong to one commit, commit those, then stage and commit the rest separately.
Branching — Parallel Lines of Work
A branch is just a named pointer to a commit. Creating a branch doesn't copy anything — it's just a label. When you commit on a branch, the pointer moves forward. This is why branches are cheap and instant in git.
HEAD is a special pointer that indicates which branch (or commit) you're currently working on. When you commit, HEAD's branch pointer advances.
Creating and Switching Branches
git branch # list local branches git branch -a # list all branches including remote-tracking git branch feature/user-auth # create a branch (stay on current branch) git switch feature/user-auth # switch to that branch git switch -c feature/user-auth # create and switch in one step (preferred) git switch - # switch back to the previous branch (toggle)
git switch is the modern replacement for git checkout for branch operations. checkout still works, but switch is more explicit and less confusing.Branch Naming Conventions
feature/user-authentication feature/payment-flow-redesign fix/login-timeout-error fix/null-pointer-in-search chore/update-node-18 docs/api-authentication-guide experiment/ai-powered-search
The feature/, fix/, chore/ prefixes group related branches. Many teams also include ticket numbers: feature/AUTH-123-oauth-provider.
Merging Branches
git switch main git merge feature/user-auth # merge feature into current branch (main) git merge --no-ff feature/user-auth # always create a merge commit git merge --squash feature/user-auth # combine all feature commits into one staged change
--no-ff (no fast-forward) preserves the branch history visually even when it could technically be a simple advance of the pointer. It makes git log --graph show the branch structure clearly.
Deleting Branches After Merging
git branch -d feature/user-auth # delete local branch (safe: refuses if not merged) git branch -D feature/user-auth # force delete (even if not merged) git push origin --delete feature/user-auth # delete remote branch git remote prune origin # remove local references to deleted remote branches
Delete merged branches. Keeping them around forever creates noise and makes it hard to see what's actually in progress.
Merge vs. Rebase
Both integrate changes from one branch into another. They do it differently and produce different histories.
Merge creates a merge commit that has two parents. The history shows exactly what happened — branch came off here, rejoined there, parallel work happened in between.
Rebase replays your commits on top of the target branch as if you'd branched from there. The result looks like linear history — no branch, no merge commit, just a clean sequence of commits.
# Merge approach git switch main git merge feature # creates merge commit # Rebase approach git switch feature git rebase main # replay feature commits on top of main git switch main git merge feature # now a fast-forward, no merge commit needed
Merge is safer, non-destructive, and preserves the true history. The downside: with many feature branches, the history becomes a tangle of merge commits.
Rebase produces cleaner, linear history that's easier to read. The downside: it rewrites commit history. The commits get new hashes. Fine on your local branch before sharing. Dangerous on public branches others might be based on.
# Common workflow: keep feature branch current with main via rebase git switch feature git fetch origin git rebase origin/main # replay your feature commits on latest main # resolve any conflicts, then: git rebase --continue # after resolving each conflict # or: git rebase --abort # bail out and return to pre-rebase state
Resolving Merge Conflicts
Conflicts happen when the same part of a file was changed in two different ways that git can't automatically reconcile. They're not a problem — they're git saying "I need a human to decide."
What a Conflict Looks Like
<<<<<<< HEAD const timeout = 5000; ======= const timeout = 10000; >>>>>>> feature/increase-timeout
Everything between <<<<<<< HEAD and ======= is what your current branch has. Everything between ======= and >>>>>>> is what's coming in. You decide what the final version should be — and remove all the conflict markers.
const timeout = process.env.NODE_ENV === 'test' ? 5000 : 10000;Resolving Conflicts
# After a merge or rebase that hits conflicts: git status # shows which files have conflicts # Edit the conflicted files, resolve each conflict, remove markers git add resolved-file.js # mark as resolved git commit # complete the merge
For rebases, you resolve conflicts one commit at a time:
git rebase --continue # after resolving current commit's conflicts git rebase --skip # skip this commit entirely (rarely right) git rebase --abort # give up and return to pre-rebase state
Preventing Conflicts
- Keep branches short-lived — the longer a branch lives, the more divergence accumulates
- Pull/rebase frequently from main to stay current
- Communicate with teammates when working on the same files
- Break large changes into smaller, sequential branches
Undoing Changes
Undoing Changes in the Working Directory
git restore filename.txt # discard changes in working directory (permanent!) git restore . # discard all working directory changes git restore --staged filename.txt # unstage a file (move back from staging to working) git restore --staged . # unstage everything
git restore (working directory) permanently throws away your changes without git keeping a record. Use it carefully.Reset vs. Revert
git reset — moves the branch pointer backward, "erasing" commits:
git reset --soft HEAD~1 # undo last commit, keep changes staged git reset --mixed HEAD~1 # undo last commit, keep changes unstaged (default) git reset --hard HEAD~1 # undo last commit, discard all changes (destructive)
--soft is the safe version — it uncommits but keeps your work, ready to re-commit differently. --hard throws away the changes completely. All three rewrite history.
git reset only on local commits you haven't pushed. On shared/public commits, use git revert instead.git revert — creates a new commit that undoes the changes from a previous commit:
git revert HEAD # create a commit that undoes the last commit git revert abc123 # create a commit that undoes a specific commit git revert HEAD~3..HEAD # undo the last 3 commits
git revert is safe on public branches because it adds to history rather than rewriting it. Everyone can see what was reverted and why.
Amending the Last Commit
git commit --amend -m "Better message" # change the message git commit --amend --no-edit # add staged changes to last commit
--amend rewrites the last commit (changes its hash). Only use it before pushing.
Git Stash — A Temporary Shelf
Stash saves your working directory changes without committing, so you can switch context and come back.
git stash # stash working directory changes git stash push -m "WIP: auth refactor" # stash with a description git stash list # see all stashes git stash pop # apply the most recent stash and remove it git stash apply stash@{2} # apply a specific stash (keep it in stash list) git stash drop stash@{2} # delete a specific stash git stash clear # delete all stashes
Finding Lost Commits — Git Reflog
Even after git reset --hard, commits aren't immediately gone.
git reflog shows a log of every position HEAD has been at, including commits that are no longer in the branch history:
git reflog # see all recent HEAD positions git switch -c recovery-branch abc123 # create a branch at a "lost" commit hash
Investigating History
The git history is a searchable, interrogatable record. Learning to read it is what separates comfort from guesswork.
git log — Reading History
git log --oneline --graph --all # visual branch graph including all branches git log --since="2 weeks ago" # filter by date git log --author="Alice" # filter by author git log -- src/auth/ # commits that touched this folder git log -p # show the actual diff with each commit git log -S "functionName" # commits that added or removed this string git log --grep="ticket-123" # commits whose messages match a pattern
-S flag is especially useful — "show me every commit that introduced or removed this string anywhere in the codebase."git blame — Who Changed What
git blame filename.py # annotate each line with author and commit git blame -L 40,60 filename.py # blame just lines 40-60
git blame shows for each line: who last changed it, in what commit, and when. This is how you find the context for a piece of code that doesn't make sense — find the commit, then read the commit message.
git diff — Comparing States
git diff # working directory vs staging area git diff --staged # staging area vs last commit git diff main..feature # compare two branches git diff abc123..def456 # compare two specific commits git diff --stat # just file list and change counts
Git Bisect — Binary Search for Bugs
git bisect is one of git's most powerful and underused features. It does binary search through your commit history to find exactly which commit introduced a bug.
git bisect start # start bisect session git bisect bad # current commit is bad (has the bug) git bisect good abc123 # this earlier commit was good (no bug) # git checks out the midpoint commit # test whether the bug exists, then: git bisect good # this commit is good git bisect bad # this commit is bad # git keeps halving the search space until it finds the exact commit git bisect reset # end session and return to HEAD
Git typically needs 7–10 rounds to find the guilty commit in a history of hundreds. For automated testing:
git bisect run npm test # automated: 0 exit = good, non-0 = badPull Requests
Pull requests (PRs) are the standard mechanism for proposing changes, getting them reviewed, and merging them. They're a GitHub invention, not native to git, but they've become the default everywhere.
git switch -c feature/my-feature # create a feature branch # ... make commits ... git push -u origin feature/my-feature # push branch to remote # Then go to GitHub and open a PR from that branch to main
Writing a Good PR Description
The PR description is documentation. Write it for someone who doesn't have your context:
- What: A one-sentence summary of what this PR does
- Why: The motivation. What problem does it solve?
- How: Any non-obvious technical decisions. Why this way?
- Testing: How did you verify this works? Edge cases you checked?
- Screenshots/recordings: For UI changes, always include before/after
.github/pull_request_template.md file in the repo pre-fills the description. These enforce consistency and remind people what to include.Good PR Practices
Keep PRs small. A PR that touches 10 files and 300 lines is hard to review well. A PR that touches 3 files and 80 lines gets reviewed faster, more carefully, and with better feedback. If a feature is large, break it into a sequence of PRs.
One purpose per PR. Don't bundle unrelated changes. If you fix a bug while building a feature, make them separate PRs.
Keep your PR current. If main moves while your PR is open:
git fetch origin git rebase origin/main # rebase on latest main git push --force-with-lease # update the remote branch
Respond to review comments promptly and concretely. For each comment: either make the change and push, or explain why you're not. Don't leave threads open without resolution.
Reviewing Code
Code review is a skill separate from writing code.
Understand before critiquing. Read through the whole PR before commenting. Get a sense of the overall approach before focusing on details.
Distinguish blocking from non-blocking. Be clear: "This will cause a bug in X scenario" vs. "Minor style nit, take it or leave it." Consider prefixes: [blocking], [nit], [question], [suggestion].
Comment on the why. Don't just say "change this to X." Explain why X is better. The author learns something; the explanation lives in the PR history.
Ask questions genuinely. Sometimes the right response to code you don't understand is "can you explain what this is doing?" The author might have a good reason. Or they might discover explaining it reveals a problem.
Be constructive about significant issues. "I think this approach will have trouble with Y — could we discuss alternatives?" opens a conversation. "This is wrong" closes it.
Merge Strategies
When merging a PR on GitHub, you have three options.
Merge commit (default) — preserves all commits from the branch plus adds a merge commit. Full history, but can get noisy.
Squash and merge — combines all branch commits into one commit on main. Produces clean main history; loses the detailed branch history. Good for messy work-in-progress branches.
Rebase and merge — replays each branch commit directly onto main with no merge commit. Linear history, preserves individual commits. Best for well-crafted branches with clean, meaningful commits.
Interactive Rebase
git rebase -i lets you edit commits before they become permanent — reorder them, squash them together, edit messages, split them apart.
git rebase -i HEAD~5 # interactively edit the last 5 commits git rebase -i origin/main # edit everything not yet in main
This opens an editor with your commits and action keywords:
pick a1b2c3d Add user model pick b2c3d4e Add authentication endpoint pick c3d4e5f WIP - need to fix tests pick d4e5f6a Fix failing tests from previous commit pick e5f6a7b Add login UI
Change pick to an action:
| Action | What it does |
|---|---|
| pick | Keep as-is |
| reword | Keep the commit but edit the message |
| squash (s) | Combine with the previous commit (merges messages) |
| fixup (f) | Combine with previous, discard this commit's message |
| edit (e) | Pause to make changes to this commit |
| drop (d) | Delete the commit entirely |
pick a1b2c3d Add user modelpick b2c3d4e Add authentication endpointfixup c3d4e5f WIP - need to fix testsfixup d4e5f6a Fix failing tests from previous commitpick e5f6a7b Add login UIThis squashes the WIP and fix commits into the auth endpoint commit, producing a clean history before the PR.
Cherry-Pick & Worktree
git cherry-pick
git cherry-pick abc123 # apply a single commit from anywhere to current branch git cherry-pick abc123..def456 # apply a range of commits git cherry-pick abc123 --no-commit # apply changes without committing
Cherry-pick is useful when you want one specific fix from a feature branch without merging the whole thing, or when you accidentally committed to the wrong branch.
git worktree — Multiple Checkouts
git worktree add ../myproject-feature feature/auth # new working directory git worktree list # see all worktrees git worktree remove ../myproject-feature # remove when done
Each worktree is a separate checkout of a different branch, sharing the same repository database. Especially useful when working with AI coding agents — you can have Claude Code working on one feature in one worktree while you work on something else in the main checkout.
Tags & Hooks
git tag — Marking Releases
git tag v1.0.0 # lightweight tag (just a pointer) git tag -a v1.0.0 -m "Version 1.0.0 release" # annotated tag (has message) git push origin v1.0.0 # push a specific tag git push origin --tags # push all tags git tag # list all tags
Tags are how you mark release points. Unlike branch names, tags don't move when you make new commits. They're permanent markers in history.
Hooks — Automating Git Workflows
Git hooks are scripts that run automatically at certain points in the git workflow. They live in .git/hooks/:
| Hook | When it runs |
|---|---|
| pre-commit | Before a commit is created — linting, secret scanning |
| commit-msg | After you write a commit message — validate format |
| pre-push | Before git push — running tests |
| post-commit | After commit — notifications |
#!/bin/sh if git diff --cached --name-only | grep -q "\.env$"; then echo "Error: .env file staged. Remove it before committing." exit 1 fi
chmod +x .git/hooks/pre-commit.git/hooks isn't tracked by git, use a tool like husky (for Node.js projects) which stores hooks in the repo and installs them on npm install.GitHub Beyond Pull Requests
Branch Protection Rules
For any serious project, protect your main branch. In GitHub: Settings → Branches → Add rule. Key protections:
- Require pull request reviews — nobody can push directly to main
- Require status checks to pass — CI must pass before merging
- Require branches to be up to date — PR must be rebased on latest main
- Restrict who can push — limit direct pushes to admins
- Require signed commits — cryptographic verification
GitHub Actions — CI/CD in the Repo
GitHub Actions lets you run workflows on git events. A minimal test workflow (.github/workflows/test.yml):
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install
- run: npm testIssues and Project Management
Reference issues in commit messages and PRs: Closes #42 or Fixes #42 in a PR description automatically closes the issue when the PR merges. Use issue templates (.github/ISSUE_TEMPLATE/) to standardize bug reports and feature requests. Labels create filterable taxonomy: bug, enhancement, good first issue, blocked, priority: high. Milestones group issues toward a release or deadline.
Git and AI-Assisted Development
The AI tooling era changes how you work with git in several concrete ways worth understanding.
Committing AI-Assisted Work
When Claude Code, Cursor, or Copilot writes or significantly modifies code, that code is still your responsibility. You're the author. Review it before committing — not because AI tools are untrustworthy, but because you need to understand what's in your codebase, AI-generated code can have subtle bugs, and your commit message should explain the why, which only you know.
Git as an AI Checkpoint System
With AI tools making sweeping changes, commits become safety checkpoints. Before handing a task to an AI agent:
git add . git commit -m "chore: checkpoint before AI refactor"
Now if the AI session goes sideways — writes something that breaks tests, goes in the wrong direction — you have a clean rollback point:
git reset --hard HEAD # throw away all AI changes and return to checkpointBranching for AI Experiments
git switch -c experiment/ai-checkout-redesign # delegate to Claude Code on this branch # review the results # merge if good, delete if not git branch -D experiment/ai-checkout-redesign # delete if it didn't work out
The branch creates an isolated playground. If the experiment fails, you delete it. If it succeeds, you merge (or cherry-pick the good parts).
CLAUDE.md — Teaching AI Your Git Conventions
Claude Code reads a CLAUDE.md file in your project root and follows the instructions there:
## Git workflow
- Always work on feature branches, never commit directly to main
- Branch naming: feature/description, fix/description, chore/description
- Commit messages must follow Conventional Commits format
- Run tests before committing: npm test
- Include the ticket number in PRs: [TICKET-123]AI for Understanding History
AI tools are particularly good at explaining git history. Try prompts like:
- "What did this branch change? Summarize the commits between feature/auth and main."
- "Explain what the commit
abc123was doing and why." - "I have a merge conflict. Here are both sides. What are each of these changes trying to do?"
Keeping Clean History with AI-Heavy Workflows
When AI tools generate lots of commits ("checkpoint 1," "checkpoint 2," "fix that," "try again"), clean them up before merging to main:
git rebase -i origin/main # squash the WIP commits # turn 15 "checkpoint" commits into 3 meaningful commits
The AI's interim work is useful in the moment, but the long-term record should show meaningful units of work, not every iteration the AI tried.
Common Problems and Fixes
"Your branch is behind origin/main"
git pull # merge latest remote changes in # or: git fetch origin git rebase origin/main # replay your commits on latest main
"Detached HEAD state"
You checked out a specific commit instead of a branch. You're not on any branch, so new commits won't be tracked.
git switch - # go back to previous branch git switch main # or go to a specific branch git switch -c new-branch # or create a branch here to keep your work
"Fatal: refusing to merge unrelated histories"
Usually happens when you initialized a repo locally and also created one on GitHub with a README.
git pull origin main --allow-unrelated-histories # force the merge"Rejected: non-fast-forward"
You tried to push, but the remote has commits you don't have.
git pull --rebase # fetch remote changes and rebase on top git push # now push
You Pushed Secrets to GitHub
- Revoke the secret immediately in whatever service issued it
- Generate a new secret
- Remove from history:
git filter-repo --path-glob='*.env' --invert-pathsor use BFG Repo-Cleaner - Force push the cleaned history:
git push --force --all - Contact GitHub support — they can invalidate cached versions
Reading Git Error Messages
Git error messages are more readable than they look. They generally tell you what went wrong and often suggest a fix. Read them completely before googling. Most of the time the answer is in the message — especially with conflicts and push rejections.
Quick Reference Cheat Sheet
Daily Workflow
| Command | What it does |
|---|---|
| git status | What's changed, what's staged |
| git diff | Working directory changes |
| git diff --staged | What's about to be committed |
| git add -p | Interactively stage changes |
| git commit -m "..." | Commit staged changes |
| git log --oneline -10 | Last 10 commits |
| git push | Push to upstream |
| git pull | Pull from upstream |
Branching
| Command | What it does |
|---|---|
| git switch -c feature/name | Create and switch to branch |
| git switch - | Toggle to previous branch |
| git branch -d feature/name | Delete merged branch |
| git fetch --prune | Clean up remote-tracking refs |
| git rebase origin/main | Replay branch on latest main |
Undoing
| Command | What it does |
|---|---|
| git restore filename | Discard working directory changes |
| git restore --staged filename | Unstage a file |
| git reset --soft HEAD~1 | Undo last commit, keep changes staged |
| git reset --hard HEAD~1 | Undo last commit, discard changes |
| git revert HEAD | New commit that undoes last commit |
| git stash | Temporarily save working changes |
| git stash pop | Restore last stash |
| git reflog | Find "lost" commits |
Investigating
| Command | What it does |
|---|---|
| git log -S "string" | Commits that changed this string |
| git blame filename | Who last changed each line |
| git bisect start | Binary search for guilty commit |
| git diff main..feature | Compare two branches |
| git show abc123 | See a specific commit |
Fixing Things
| Command | What it does |
|---|---|
| git commit --amend | Edit last commit |
| git rebase -i HEAD~5 | Interactively edit last 5 commits |
| git cherry-pick abc123 | Apply a specific commit here |
| git push --force-with-lease | Safe force push |