What and Why
Git is a distributed version control system that tracks changes to files over time. Every working copy is a full repository — no server required to commit, branch, or inspect history.
You encounter Git constantly: committing features, resolving conflicts, bisecting regressions, reviewing diffs, rolling back broken deployments. Mastery here isn't optional — it determines whether you can move fast without burning things down.
Why it matters in production:
- Auditability: every change has an author, timestamp, and message. When something breaks at 2am, the commit log is your first forensic tool.
- Reversibility: bad deploys can be reverted in seconds if commits are small and scoped.
- Parallelism: branching lets multiple engineers work on unrelated features without stepping on each other.
A deployment goes wrong at 2am and a critical bug is now live in production. Your team uses Git with small, well-scoped commits. Which Git capability most directly enables you to recover quickly in this situation?
Core Concepts
The Three Areas
Git tracks your work across three distinct areas:
Working Directory → Staging Area (Index) → Repository (.git)
(your edits) (git add) (git commit)- Working directory: files you can see and edit on disk.
- Staging area: a snapshot you're preparing to commit. Lets you craft precise commits even when your working directory is messy.
- Repository: the permanent, compressed history of all committed snapshots.
Understanding these three areas explains most confusing Git behavior. git diff shows working directory vs staging. git diff --staged shows staging vs last commit. git status shows both deltas simultaneously.
Commits Are Snapshots, Not Diffs
Git stores each commit as a complete snapshot of the entire tree (via content-addressable objects), not as a diff from the previous commit. Diffs are computed on demand by comparing two snapshots.
This is why operations like git checkout and git bisect can be fast — Git doesn't need to replay a chain of patches; it can jump directly to any snapshot.
The DAG
Git history is a directed acyclic graph (DAG) of commit objects. Each commit points to its parent(s). Branches are just named pointers (refs) to a specific commit. HEAD is a pointer to the currently checked-out branch (or commit in detached HEAD state).
A ← B ← C ← D ← main
↑
feature (branch pointer)When you commit on feature, the pointer moves forward. When you merge or rebase onto main, the graph changes shape.
Branching and Merging
A branch is 41 bytes (a file containing a commit SHA). Creating branches is cheap — do it often.
Merge creates a new merge commit that joins two branch histories. The DAG gains a node with two parents. History is preserved exactly as it happened.
Rebase replays commits from one branch onto another. No merge commit, but commits get new SHAs — the history is rewritten. Makes logs linear and easy to read; breaks shared history if done carelessly on public branches.
Content Addressing
Every object in Git (blob, tree, commit, tag) is identified by a SHA-1 hash of its content. This means:
- Identical content is stored once regardless of filename.
- You can verify integrity: if the SHA doesn't match, the content was corrupted or tampered with.
- Commit SHAs change when anything in the commit changes — including the parent SHA. Rewriting history always produces new SHAs.
Choose a snapshot, then commit it
Git add prepares selected changes in the staging area; git commit records the staged snapshot in the repository.
Your edits live in the working directory, separate from the staging area and saved history.
Text version
- Your edits live in the working directory, separate from the staging area and saved history.
- Git add stages selected changes so a messy working directory can still produce a precise commit.
- Git commit saves the staged snapshot in the local repository, so you can commit offline.
- Git diff compares your working directory with the staging area, so you can see unstaged changes.
- Git diff dash dash staged compares the staging area with the last commit to reveal what you will commit.
A developer runs git rebase main on a feature branch that a teammate is also working on, then force-pushes it to the shared remote. The teammate now has merge conflicts and broken history. What is the root cause of this problem?
How It Works in Practice
Daily Workflow
# Start fresh feature work
git checkout -b feature/user-auth
# Check what changed
git status
git diff
# Stage specific files (not everything)
git add src/auth/login.go
git add src/auth/token.go
# Stage hunks (not whole files) — lets you split a messy working dir into clean commits
git add -p src/auth/login.go
# Commit with a useful message
git commit -m "Add JWT token generation with RSA signing"
# Push to remote
git push -u origin feature/user-authCommit Message Convention
Most teams follow a variation of Conventional Commits or the imperative subject style:
<type>(<scope>): <short summary>
<body — wrap at 72 chars>
<footer — issue refs, breaking changes>Examples:
feat(auth): add JWT refresh token rotation
fix(db): handle connection timeout on cold start
refactor(billing): extract invoice calculation into service layerBad commit messages lose the "why". A commit titled "fix bug" gives you nothing to work with during git bisect or git log --grep.
Inspecting History
# Graph view of branches and merges
git log --oneline --graph --decorate --all
# Search commit messages
git log --grep="billing"
# Find who changed a specific line and when
git blame -L 42,55 src/payment/processor.go
# Show what a commit actually changed
git show a3f9b21
# Find which commit introduced a bug (binary search)
git bisect start
git bisect bad HEAD
git bisect good v2.1.0
# Git checks out midpoints; you test and mark good/bad
git bisect good # or: git bisect bad
# Repeat until Git identifies the culprit commit
git bisect resetUndoing Things
# Unstage a file (keep changes in working dir)
git restore --staged src/auth/login.go
# Discard working directory changes (destructive)
git restore src/auth/login.go
# Undo last commit but keep changes staged
git reset --soft HEAD~1
# Undo last commit and unstage changes (keep files)
git reset HEAD~1
# Undo last commit and discard all changes (destructive)
git reset --hard HEAD~1
# Create a new commit that reverses a previous commit (safe for shared branches)
git revert a3f9b21Use reset --hard only on local commits that haven't been pushed. Use revert when you need to undo something on a shared branch without rewriting history.
Resolving Conflicts
git merge feature/payments
# On conflict:
git status # see conflicted files
# Edit files — remove <<<<, ====, >>>> markers
git add src/payment/processor.go # mark as resolved
git commit # complete the mergeConfigure a visual merge tool to avoid manual marker editing:
git config --global merge.tool vimdiff # or: vscode, intellij, etc.
git mergetoolFind a regression with Git bisect
Git selects midpoints; you test and mark them good or bad to find the first bad commit.
The search starts between the known good release A and the bad commit H.
Text version
- The search starts between the known good release A and the bad commit H.
- The midpoint D tests good, so the good boundary moves to D.
- The midpoint F tests bad, so the bad boundary moves to F.
- The midpoint E tests good, so the good boundary moves to E.
- E is good and the next commit, F, is bad, so F introduced the regression.
Your team's main branch has a broken deployment caused by a commit pushed two days ago. Several other developers have since built new commits on top of it. Which Git command is the safest way to undo that faulty commit without disrupting your teammates' work?
Common Mistakes
Committing directly to main/master. Even solo projects benefit from branches — they give you a clean unit for PRs, rollbacks, and code review. Most teams enforce this via branch protection rules.
Giant commits that mix unrelated changes. Makes code review hard, makes git bisect less useful, makes reverts dangerous. Commit one logical change at a time.
Force-pushing shared branches. git push --force on a branch others have checked out rewrites their history, causing divergence and confusion. Use --force-with-lease at minimum, and coordinate before rewriting shared history.
Committing secrets. Passwords, API keys, and private keys committed to a repo are compromised — even if you delete them in a later commit, they remain in history. Use pre-commit hooks or tools like gitleaks to prevent this. Once committed, assume the secret is exposed and rotate it.
Ignoring .gitignore. Build artifacts, .env files, IDE configs, and compiled binaries don't belong in the repo. Set up .gitignore before the first commit. For secrets specifically, add .env and any credential files explicitly.
Rebasing public branches. Rebasing rewrites commit SHAs. If others have checked out those commits, their history diverges from yours. Only rebase local, un-pushed branches — or feature branches that you own and haven't shared.
A developer accidentally commits a file containing a database password to a shared Git repository. They quickly delete the file in a follow-up commit and push it. Is the secret still at risk?
Tradeoffs
Merge vs. Rebase
| Merge | Rebase | |
|---|---|---|
| History shape | Preserves the exact branch topology | Linear, easier to read |
| Safety | Safe on shared branches | Dangerous on shared branches |
| Merge commits | Creates one | None |
| Bisect/blame | Works fine | Works well; cleaner |
Neither is universally better. Teams that value "what actually happened" prefer merge. Teams that value readable, linear history prefer rebase-then-merge. Many teams use both: rebase locally to clean up, merge to integrate.
Commit granularity
Small commits are easier to review, revert, and bisect. Large commits are faster to write and sometimes unavoidable for atomic features. The right default is: one commit per logical change. Squash before merging if your working commits are noisy — but don't squash across logically separate changes.
Centralized vs. distributed workflow
Git is distributed, but most teams operate with a central remote (GitHub, GitLab, Bitbucket). The distributed model means you can commit, branch, and view history offline — then sync when ready.
Choose how to integrate history
Merge preserves branch topology; default rebase makes replayed history linear but rewrites commit identities and is dangerous on shared branches.
Merge keeps the branch shape, while rebase replays your commits on top in a straight line.
Text version
- Merge keeps the branch shape, while rebase replays your commits on top in a straight line.
- Merging diverged branches adds a merge commit with two parents, and rebase adds none.
- Rebase gives replayed commits new identities, so use it on unshared work to avoid disrupting other developers.
Your team uses a shared main branch that multiple developers push to throughout the day. A teammate suggests rebasing their feature branch onto main and then force-pushing to update the shared branch. Why is this approach risky?
Quick Reference
# Setup
git init # initialize new repo
git clone <url> # clone remote
# Branch
git checkout -b <branch> # create and switch
git branch -d <branch> # delete merged branch
git branch -D <branch> # force delete
# Stage & commit
git add -p # interactive staging (hunk-by-hunk)
git commit -m "message" # commit staged changes
git commit --amend # rewrite last commit (local only)
# Sync
git fetch --prune # fetch and remove stale remote refs
git pull --rebase # pull and rebase local commits on top
git push -u origin <branch> # push and set upstream
# Inspect
git log --oneline --graph # visual branch history
git show <sha> # show commit diff
git blame <file> # per-line authorship
git diff HEAD~3..HEAD # diff across last 3 commits
# Undo
git restore --staged <file> # unstage
git reset HEAD~1 # undo commit, keep changes
git revert <sha> # safe undo for shared branches
# Debug
git bisect start / good / bad # binary search for regression
git stash / git stash pop # shelve and restore working changesYou committed a bug fix to your local feature branch and immediately realized the commit message was unclear and you forgot to include one small change. The branch has NOT been pushed to the remote yet. Which Git command lets you fix the commit message and add the missing change in one step?