7 min read
Software Engineering Practices Index
Tier 1 -- Foundations
Developer Skills
Testing Basics
Source Control
Tier 2 -- Core Practices
Tier 3 -- Platform & Delivery
Software Engineering Practices Index
Tier 1 -- Foundations
Developer Skills
Testing Basics
Source Control
Tier 2 -- Core Practices
Tier 3 -- Platform & Delivery
Version Control (Git)
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, git bisect, and git rebase 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.
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-auth
Commit 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 layer
Bad 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 reset
Undoing 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 a3f9b21
Use 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 merge
Configure a visual merge tool to avoid manual marker editing:
git config --global merge.tool vimdiff # or: vscode, intellij, etc. git mergetool
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.
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 changes
You 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?
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.