Skip to content

Version Control

Version control records changes as reviewable history. It enables collaboration, rollback, investigation, and reproducible releases. This chapter uses Git, a distributed version control system.

Git's Model

Keep four places distinct:

  1. working tree — files you are editing;
  2. index — the proposed contents of the next commit;
  3. local repository — commits and named references such as branches;
  4. remote repository — another repository reached through a named remote.

A commit stores a snapshot, metadata, and parent commit identifiers. A branch is a movable name pointing to a commit. HEAD identifies the current branch or, when detached, a commit directly.

working tree --add--> index --commit--> local history --push--> remote
      ^                  │
      └---- restore -----┘

The Daily Workflow

git status
git diff
git add path/to/file
git diff --staged
git commit -m "Explain the completed change"
git fetch origin
git push

Stage deliberately. git add -p selects individual changes. Before committing, inspect both unstaged and staged diffs and run the relevant checks.

Good commits are coherent, buildable where practical, and explain why the change exists. Do not mix generated files, formatting, refactoring, and behavior unless they are inseparable.

Branches and Integration

git switch -c fix/session-expiry
git switch main
git merge fix/session-expiry

Keep branches short-lived and integrate frequently. Protect the shared branch with review and automated checks.

A merge preserves both histories and creates a merge commit when needed. A rebase copies commits onto a new base, producing new commit identities and a linear history:

git fetch origin
git rebase origin/main

Rebase unpublished work freely. Avoid rebasing history other people may have based work on. If rewriting your own published branch is agreed, prefer git push --force-with-lease over unguarded force.

Inspecting History

git log --oneline --graph --decorate --all
git show <commit>
git diff <base>...<branch>
git blame path/to/file
git log -S 'removed_text' -- path/to/file

git blame identifies the last change to lines, not the person responsible for a defect. Follow the commit and surrounding history for context.

Use binary search to locate a regression:

git bisect start
git bisect bad
git bisect good <known-good-commit>
git bisect run ./small-reproduction.sh
git bisect reset

Undoing Safely

The official Git documentation distinguishes three commonly confused commands:

  • git restore changes working-tree or staged file content;
  • git reset moves a branch or changes the index and can rewrite local history;
  • git revert creates a new commit that reverses an earlier commit.

Examples:

git restore path/to/file             # discard unstaged changes
git restore --staged path/to/file    # unstage; keep working changes
git commit --amend                   # replace the latest local commit
git revert <commit>                  # safe reversal in shared history

Inspect before discarding. git reset --hard, git clean, and forced pushes can destroy uncommitted or shared work.

The reflog records recent local reference movements and often recovers commits after an accidental reset or rebase:

git reflog
git branch recovery <commit-from-reflog>

Reflogs are local and expire; they are not a backup.

Conflicts

When an integration stops:

  1. read git status;
  2. understand both intended changes;
  3. edit the files to the correct combined result;
  4. remove conflict markers;
  5. stage resolved files and run tests;
  6. continue or abort the operation.
git merge --continue       # or git rebase --continue
git merge --abort          # or git rebase --abort

Do not resolve by blindly taking “ours” or “theirs”; those labels also vary by operation.

Remotes and Collaboration

git fetch downloads objects and updates remote-tracking references without changing the working tree. git pull fetches and immediately integrates; use it only when the configured integration behavior is understood.

Review requests should be small, explain the problem and trade-offs, include verification evidence, and separate required changes from optional suggestions. Delete merged branches; commits remain reachable through the target history.

Ignore, Attributes, and Large Files

.gitignore prevents untracked files from being considered for addition; it does not remove already tracked files. Ignore local build output, caches, and secrets—but commit reproducibility inputs such as lockfiles when the ecosystem expects them.

.gitattributes controls repository-level text normalization and diff/merge behavior. Use Git LFS or artifact storage for large binary assets that genuinely need versioning; avoid committing generated archives and build products.

Tags, Releases, and Authenticity

Tags give stable names to commits. Annotated tags carry metadata and can be signed. A release should map to an immutable commit and reproducible artifact.

Commit or tag signatures prove control of a configured key, not that code is safe or reviewed. Protect keys, verify contributor identity through the hosting workflow, and secure branch and release permissions.

Repository Safety

  • Never commit secrets; rotate an exposed secret even if history is rewritten.
  • Review executable hooks, CI configuration, and dependency changes as code.
  • Limit who can bypass protected branches or publish releases.
  • Back up the authoritative remote and test restoration where loss matters.
  • Prefer reviewed history rewriting tools for removing sensitive data.

Useful Advanced Tools

  • git stash temporarily records a dirty working state; a small WIP commit is often easier to understand.
  • git worktree checks out multiple branches into separate directories.
  • git cherry-pick copies selected commits; repeated use may signal poorly aligned branches.
  • submodules pin another repository commit but add coordination work; use them only when separate ownership and history are essential.
  • sparse checkout and partial clone reduce working data for very large repositories.

Checklist

  • Is the staged diff exactly the intended change?
  • Does the commit explain why and pass relevant checks?
  • Is shared history preserved unless rewriting was agreed?
  • Are generated output, binaries, and secrets excluded?
  • Can a release be traced to an immutable commit?
  • Are destructive commands preceded by inspection and a recovery plan?

Version control is most valuable when history is trustworthy enough to review, diagnose, and restore the software.