Version Control (Git)

7 min read

Reading Progress0%
Software Engineering Practices Index
Tier 1 -- Foundations
Tier 2 -- Core Practices
Tier 3 -- Platform & Delivery
Software Engineering Practices Index
Tier 1 -- Foundations
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.

QUICK CHECK

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?

Choose one answer

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.

QUICK CHECK

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?

Choose one answer

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

QUICK CHECK

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?

Choose one answer

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.


QUICK CHECK

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?

Choose one answer

Tradeoffs

Merge vs. Rebase

MergeRebase
History shapePreserves the exact branch topologyLinear, easier to read
SafetySafe on shared branchesDangerous on shared branches
Merge commitsCreates oneNone
Bisect/blameWorks fineWorks 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.


QUICK CHECK

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?

Choose one answer

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
QUICK CHECK

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?

Choose one answer
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.