Skip to main content

Axiom VIII: Version Control is Memory

It is Tuesday morning. Your teammate asks: "Why did we switch from REST to GraphQL for the user service?" You know the decision happened three months ago, but you cannot remember the reasoning. Was it performance? Was it the mobile team's request? Was it a library limitation?

You search Slack. Nothing relevant. You check the docs folder. The architecture decision record was never written. You look at the git history:

commit a1b2c3d
Author: dev-team
Date: Mon Oct 14 14:22:33

updated api

One line. No context. No rationale. Three months of institutional knowledge, gone.

Now imagine the alternative:

commit a1b2c3d
Author: dev-team
Date: Mon Oct 14 14:22:33

feat(user-service): migrate from REST to GraphQL

Mobile team reported N+1 query problems fetching user profiles
with nested preferences. GraphQL resolvers reduce round trips
from 6 to 1 for the profile screen.

Trade-off: Increased backend complexity for resolver layer.
Accepted because mobile is 73% of traffic.

Related: ARCH-047, mobile-team RFC from Sept standup

Same change. But this commit is a memory. It records not just what changed, but why it changed, what alternatives were considered, and where to find the broader context.

This is the difference between using git as a backup tool and using git as the memory of your project.


The Problem Without This Axiom

Without version control as memory, teams face compounding knowledge loss:

SituationWithout Git MemoryWith Git Memory
"Why did we add this validation?"Nobody remembers, afraid to remove itgit blame reveals the bug report that prompted it
"When did performance degrade?"Manual log searching, guessworkgit bisect finds the exact commit
"What did the API look like before?""I think it was something like..."git show v2.1.0:src/api.py shows exactly
"Who decided to use Redis here?"Tribal knowledge, lost when they leaveCommit message explains the caching rationale
"Can we undo last week's changes?"Risky manual reversalgit revert abc123 safely creates inverse commit
AI asks "What's the project context?"You explain from scratch every sessionAI reads git log for recent decisions

The cost is invisible day-to-day but catastrophic over time. Every undocumented decision becomes a landmine. Every unexplained change becomes technical debt. Every departed team member takes irreplaceable context with them.


The Axiom Defined

Axiom VIII: Version Control is Memory. Git provides the persistent memory layer for all work. Every decision, every change, every experiment is recorded. Git is not just version control -- it is the system of record for software evolution.

This axiom elevates git from a tool (something you use to save work) to a system (the authoritative record of how and why your software became what it is).

The key insight: files give you current state; git gives you all past states and the story between them.

What Git Actually Records

When used with discipline, git captures four dimensions of project memory:

DimensionGit MechanismWhat It Preserves
DecisionsCommit messagesWhy changes were made, what alternatives were rejected
ExperimentsBranchesParallel approaches tried, including failed ones
MilestonesTagsStable points you can always return to
AccountabilityBlame/LogWho made each decision and when

Together, these form a complete institutional memory that survives team changes, context switches, and the passage of time.


From Principle to Axiom

In Chapter 4, you learned Principle 5: Persisting State in Files. That principle established a critical insight: AI systems are stateless between sessions, so all important context must live in files that AI can read.

Axiom VIII builds directly on that foundation:

Principle 5Axiom VIII
Persist state in filesManage that state with history
Files give you current stateGit gives you all past states
CLAUDE.md tells AI what to do nowGit log tells AI what was tried before
Files are the interfaceGit is the memory behind the interface
Solves: "AI forgot my conventions"Solves: "Nobody remembers why"

The relationship is complementary: Principle 5 says where to persist (files). Axiom VIII says how to manage persistence over time (version control). Files without git are snapshots. Files with git are a narrative.

Consider the progression:

  • Without Principle 5: Knowledge lives in your head. AI cannot access it.
  • With Principle 5: Knowledge lives in files. AI can read current state.
  • With Axiom VIII: Knowledge lives in versioned files. AI can read the full history of how current state evolved.

Git as System of Record

Commits Are Decisions

Every commit should answer one question: "What decision was made, and why?"

The code diff shows what changed. The commit message explains why it changed. Together, they form a decision record:

# The commit message is the decision record
git log --oneline --since="2024-01-01" --grep="feat"

# Full context for a specific decision
git show abc123

# Who made this decision and when?
git blame src/config.py

Branches Are Experiments

Branches are not just for "features." They are parallel experiments -- hypotheses being tested:

# Start an experiment
git checkout -b experiment/try-redis-caching

# Work on the experiment...
# If it succeeds: merge it
git checkout main && git merge experiment/try-redis-caching

# If it fails: keep the record, delete the branch
git checkout main
git branch -d experiment/try-redis-caching
# The commits still exist in reflog for 90 days

Even failed experiments have value. The commit history on a deleted branch records what was tried and why it did not work -- preventing the team from repeating the same failed approach six months later.

Tags Are Milestones

Tags mark stable, known-good states you can always return to:

# Mark a release
git tag -a v1.2.0 -m "Feature complete: user preferences with GraphQL"

# Mark a significant decision point
git tag -a pre-graphql-migration -m "Last commit before REST->GraphQL migration"

# Return to any milestone instantly
git checkout v1.2.0

Blame Is Context, Not Accusation

Despite its name, git blame is a context tool. It answers: "Who wrote this line, when, and as part of what change?"

# Find the context for a confusing line
git blame src/auth.py -L 42,42

# Result:
# a1b2c3d (Sarah Chen 2024-03-15) MAX_RETRIES = 7 # RFC-2891 requires min 5

# Now you know: Sarah set this, on March 15, and the comment tells you why

Commit Discipline

The power of git-as-memory depends entirely on commit quality. Sloppy commits produce sloppy memory.

Atomic Commits: One Logical Change

Each commit should contain exactly one logical change. If you have to use "and" to describe it, split it:

# BAD: Multiple unrelated changes in one commit
git add .
git commit -m "fix login bug and update styles and add new endpoint"

# GOOD: Three separate atomic commits
git add src/auth.py tests/test_auth.py
git commit -m "fix(auth): prevent session fixation on password reset

The session ID was not regenerated after password change,
allowing an attacker with the old session to maintain access.

Fixes: SEC-2024-031"

git add src/styles/theme.css
git commit -m "style(theme): increase contrast ratio to meet WCAG AA

Body text was 3.8:1 contrast. WCAG AA requires 4.5:1 minimum.
Updated from #767676 to #595959."

git add src/api/preferences.py tests/test_preferences.py
git commit -m "feat(api): add user preference endpoint

Supports GET/PUT for notification settings.
Mobile team needs this for v2.3 release (March 20)."

Conventional Commits: Structured Prefixes

Conventional commits use a structured prefix that makes history scannable and automatable:

PrefixMeaningExample
feat:New featurefeat(api): add batch export endpoint
fix:Bug fixfix(auth): prevent token reuse after logout
docs:Documentationdocs(readme): add deployment prerequisites
refactor:Code restructure (no behavior change)refactor(db): extract connection pooling to module
test:Adding/fixing teststest(auth): add edge case for expired tokens
chore:Maintenancechore(deps): update fastapi to 0.109.0
perf:Performance improvementperf(query): add index on user_id for profile lookups
ci:CI/CD changesci(github): add Python 3.12 to test matrix

The format: type(scope): description

# Scannable history with conventional commits
git log --oneline

# a1b2c3d feat(api): add user preference endpoint
# b2c3d4e fix(auth): prevent session fixation on password reset
# c3d4e5f docs(api): document rate limiting behavior
# d4e5f6g refactor(db): extract connection pooling
# e5f6g7h test(auth): add expired token edge cases
# f6g7h8i chore(deps): update fastapi to 0.109.0

At a glance, you can see: two auth-related changes (a fix and new tests), a new API feature, documentation, a refactor, and a dependency update. This is scannable memory.

The WHY Rule

The most important discipline: commit messages explain WHY, not WHAT.

The diff already shows what changed. The message must explain what the diff cannot:

# BAD: Describes WHAT (redundant with the diff)
git commit -m "change MAX_RETRIES from 3 to 7"

# GOOD: Explains WHY (context the diff cannot provide)
git commit -m "fix(retry): increase MAX_RETRIES to meet RFC-2891 requirement

Production monitoring showed 12% of requests failing on first 3 attempts
due to cold-start latency on the auth service. RFC-2891 mandates minimum
5 retries for credential exchanges. Set to 7 for safety margin."

Six months from now, when someone asks "why is this 7 and not 3?", the commit message answers immediately. No Slack archaeology required.


Git and AI: The Collaboration Protocol

When you work with an AI coding assistant, git becomes the shared memory layer between you:

AI Can Read Git History for Context

AI tools can examine your project's history to understand decisions:

# AI reads recent changes to understand current direction
git log --oneline -20

# AI reads a specific file's evolution
git log --follow --oneline src/models/user.py

# AI reads the full context of why something was done
git show abc123

When your CLAUDE.md says "we use GraphQL for user-facing APIs," the git history explains why -- and the AI can provide better suggestions because it understands the reasoning, not just the rule.

AI Commits Should Be Clearly Labeled

When AI generates code that gets committed, label it clearly:

# Clear attribution in commit message
git commit -m "feat(api): add batch export with pagination

Implements cursor-based pagination for large exports.
Page size defaults to 100, max 1000.

Co-Authored-By: Claude <noreply@anthropic.com>"

This matters for three reasons:

  1. Accountability: Code review knows which commits need extra scrutiny
  2. Learning: You can filter git log --author="Claude" to see AI contribution patterns
  3. Audit: In regulated environments, AI-generated code may require additional review

Git Diff as Code Review Input

The most natural input for AI code review is a git diff:

# Review staged changes before committing
git diff --staged

# Review a feature branch against main
git diff main...feature/new-auth

# Ask AI to review the diff
git diff main...feature/new-auth | pbcopy
# Paste into AI: "Review this diff for security issues"

Branches for AI Experiments

When asking AI to try something experimental, always use a branch:

# Create a safe sandbox for AI experimentation
git checkout -b ai/experiment-graphql-subscriptions

# AI makes changes...
# You review...

# If good: merge to main
git checkout main && git merge ai/experiment-graphql-subscriptions

# If bad: discard without risk
git checkout main
git branch -D ai/experiment-graphql-subscriptions

The branch prefix ai/ makes it immediately clear which branches contain AI-generated experiments.

The Agentic Development Workflow

The standard workflow for AI-assisted development:

main (stable, protected)

├── feature/user-preferences (human + AI work)
│ ├── commit: feat(api): add preference model (human)
│ ├── commit: feat(api): add CRUD endpoints (AI, reviewed)
│ ├── commit: test(api): add preference integration tests (AI, reviewed)
│ └── commit: docs(api): document preference endpoints (AI, reviewed)

└── Pull Request → Human reviews all AI commits → Merge to main

Key rules:

  • main is always stable: Never commit directly to main
  • Feature branches isolate work: Both human and AI changes go here
  • Pull requests require review: Especially for AI-generated code
  • Each commit is atomic: One logical change, clearly attributed

Anti-Patterns: How Git Memory Fails

Anti-PatternWhy It FailsBetter Approach
Giant commits ("fix everything")Impossible to understand, revert, or bisectOne logical change per commit
Empty messages ("wip", "stuff", "asdf")Zero memory value; future you learns nothingExplain WHY with conventional prefix
Committing secrets/credentialsSecurity breach waiting to happenUse .gitignore and environment variables
Force-pushing shared branchesRewrites other people's historyOnly force-push your own unshared branches
Not using branches for experimentsExperiments pollute main historyBranch first, merge only if successful
Committing generated filesNoise in diffs, merge conflicts.gitignore build outputs, node_modules/, etc.
Squashing all commits on mergeDestroys the detailed decision historyPreserve atomic commits; only squash true "wip"
Never tagging releasesNo stable milestones to reference or rollback toTag every release and significant milestone

The "Giant Commit" Problem in Detail

Consider this commit:

commit x9y8z7
Message: "weekly update"
Files changed: 47
Insertions: 2,391
Deletions: 856

This commit is anti-memory. It records that 47 files changed but provides no way to understand why. You cannot revert part of it. You cannot bisect through it. You cannot explain any individual change to a new team member.

Compare with 12 atomic commits over the same week:

feat(auth): add OAuth2 PKCE flow for mobile clients
fix(db): resolve connection leak under high concurrency
refactor(api): extract validation into middleware layer
test(auth): add PKCE challenge verification tests
docs(deploy): update Kubernetes manifest for v2.3
perf(search): add trigram index for fuzzy name matching
...

Each commit is a discrete memory. Each can be individually understood, reverted, or referenced.


Git as Time Machine

Git does not just record history -- it lets you travel through it:

Bisect: Finding When Things Broke

git bisect performs a binary search through history to find the exact commit that introduced a bug:

# Start bisecting
git bisect start

# Mark current state as bad (the bug exists now)
git bisect bad

# Mark a known-good state (the bug did not exist here)
git bisect good v1.2.0

# Git checks out a middle commit. You test it:
python -m pytest tests/test_auth.py
# Tell git the result:
git bisect good # or: git bisect bad

# Repeat until git finds the exact commit:
# "abc123 is the first bad commit"
# feat(auth): add session timeout handling

# Clean up
git bisect reset

With atomic commits, bisect pinpoints the problem in log2(n) steps. With giant commits, even finding the problem does not help -- you still have to sift through hundreds of changes.

Revert: Safe Undo

git revert creates a new commit that undoes a previous commit, without rewriting history:

# Safely undo a specific commit
git revert abc123

# Revert creates a NEW commit:
# "Revert 'feat(auth): add session timeout handling'"
# This preserves the full story: we tried it, it broke things, we reverted it.

Unlike git reset, revert is safe for shared branches because it adds to history rather than erasing it.

Cherry-Pick: Selective Application

git cherry-pick applies a specific commit from one branch to another:

# A critical fix was made on a feature branch
# Apply just that fix to main without merging everything
git checkout main
git cherry-pick def456

# The fix is now on main, with full attribution preserved

Viewing Past States

# See a file as it was at any point in history
git show v1.0.0:src/config.py

# See all files at a past state (read-only exploration)
git stash # save current work
git checkout v1.0.0
# explore...
git checkout main
git stash pop # restore current work

# Compare current file to its past self
git diff v1.0.0 -- src/config.py

Try With AI

Test your understanding of git as a memory system by working through these prompts.

Prompt 1: Transform Bad Commits into Good Memory

I have these five commits in my project history:

1. "updated stuff"
2. "fix"
3. "wip"
4. "changes"
5. "done"

For each one, I'll give you the actual diff summary. Rewrite each commit
message using conventional commit format, explaining the WHY not the WHAT:

1. Changed MAX_CONNECTIONS from 10 to 50 in database.py
2. Added null check before accessing user.email in auth.py
3. Created new file tests/test_payment.py with 3 test functions
4. Renamed "calculate_total" to "compute_order_total" across 4 files
5. Added Dockerfile and docker-compose.yml

For each rewritten message, explain what future-you would learn from it
that the original message failed to communicate.

What you're learning: This prompt reveals the difference between commits as file-saves versus commits as decisions. Notice how each rewritten message captures reasoning that would otherwise be lost. The original messages treat git as a backup tool; the rewrites treat it as institutional memory.

Prompt 2: Design a Branching Strategy for AI Collaboration

I'm working on a web application with one other developer and using
Claude Code as my AI coding assistant. We deploy to production weekly.

Design a branching strategy that:
- Keeps main always deployable
- Gives AI a safe space to experiment
- Makes AI contributions clearly identifiable in history
- Allows easy rollback of AI-generated code specifically
- Supports code review before AI changes reach main

Show me the exact git commands for a typical workflow where AI adds
a new feature, I review it, and we merge it to main.

What you're learning: This prompt forces you to think about git not just as a solo tool but as a collaboration protocol between human and AI. The branching strategy becomes a trust boundary -- AI works freely within branches, but human review gates the path to production.

Prompt 3: Investigate a Bug Using Git's Memory

Imagine a scenario: my application's login page started showing a
blank screen sometime in the last 50 commits. I know commit abc123
(50 commits ago) worked fine.

Walk me through EXACTLY how to use git bisect to find the breaking
commit. Show me:
1. The exact commands to start bisecting
2. What I test at each step
3. How to handle a commit that I can't easily test
4. What to do once I find the bad commit
5. How to safely fix the problem (revert vs fix-forward)

Then explain: why does atomic commit discipline make this process
faster and more effective than if all 50 commits were giant
"weekly update" commits?

What you're learning: This prompt demonstrates git as an investigative tool, not just a storage tool. Binary search through history only works when commits are atomic -- each commit is a single hypothesis to test. Giant commits make bisect useless because even finding the bad commit does not tell you which of its 200 changes caused the problem.


Safety Note: Never Commit Secrets

One anti-pattern deserves special emphasis because it is irreversible: never commit secrets to git.

Once a password, API key, or credential is committed, it exists in git history permanently -- even if you delete the file in a later commit. Anyone with repository access can find it with:

# This finds secrets in ALL of history, not just current files
git log -p --all -S 'API_KEY'

Prevention:

# Create .gitignore BEFORE your first commit
echo ".env" >> .gitignore
echo "*.pem" >> .gitignore
echo "credentials.json" >> .gitignore

# Verify nothing sensitive is staged
git diff --staged --name-only
# Check: do any of these files contain secrets?

# Use environment variables instead
export DATABASE_URL="postgresql://user:pass@host/db"
# Reference in code: os.environ["DATABASE_URL"]

If you accidentally commit a secret:

  1. Rotate the credential immediately -- assume it is compromised
  2. Remove from current files and commit the removal
  3. For sensitive repositories, use git filter-branch or BFG Repo-Cleaner to purge from history
  4. Force-push the cleaned history (this is the one valid use of force-push)

This is the one area where git's perfect memory works against you. Treat secrets as radioactive: they never touch version control.


Key Takeaways

Git is not a backup tool. It is the memory of your project.

  • Commits are decisions: Each one records what you chose and why
  • Branches are experiments: Safe spaces to try, fail, and learn
  • Tags are milestones: Stable points you can always return to
  • Blame is context: Who decided, when, and as part of what change
  • Bisect is investigation: Binary search through the memory to find when things broke
  • Revert is safe undo: Add to history rather than erasing it

The discipline is simple: atomic commits, conventional prefixes, and messages that explain why. The payoff is compound: every disciplined commit makes the project more understandable, more debuggable, and more maintainable -- for humans and AI alike.

Principle 5 told you to persist state in files. Axiom VIII tells you to version that state with discipline. Together, they create a system where no decision is ever truly lost.