Keeping Track
The Art of Remembering What You Did and Why
An author writes a novel. Draft after draft, the manuscript evolves. Characters are killed off and resurrected. Entire chapters are rearranged, deleted, reluctantly restored. The author keeps every draft, numbered and dated, because she learned the hard way that the paragraph she cut on Tuesday is the one she desperately needs on Thursday. Her filing cabinet is an archaeological record of creative decisions, each layer preserving a moment when the story was this and not yet that.
An architect revises blueprints. Version 1 had the kitchen on the east side. Version 2 moved it west. Version 3 moved it back east because the plumbing was already there and the client’s budget had opinions. Every revision is stamped, signed, annotated: what changed, who approved it, and, crucially, why. When the building inspector asks about a structural decision made four months ago, the answer isn’t a shrug. It’s page 47 of revision 6, with a note in the margin explaining the load calculations.
A scientist keeps a lab notebook. Every experiment recorded. Every failed attempt documented alongside every success. Not because failure is fun to revisit, but because knowing what didn’t work is half of knowing what does. The notebook is sacrosanct. You don’t tear out pages. You don’t erase entries. You draw a single line through mistakes and write the correction alongside, so the record remains complete.
Programmers face the same problem. Code evolves. Changes accumulate. Some improve things; others break them in ways that aren’t immediately obvious. Without a system for tracking what changed and why, you’re the author burning her drafts, the architect sketching on napkins, the scientist trusting her memory. It works until it doesn’t, and when it doesn’t, you find out the way you find a step that isn’t there in the dark.
The Problem Everyone Has Solved Badly
You’ve done this. Everyone has done this. You have a file called project.js. You make some changes. They seem risky, so before you commit to them, you make a copy. Now you have project.js and project_backup.js. More changes. More uncertainty. Now you have project_v2.js. Then project_v3.js. Then project_final.js. Then, inevitably, project_FINAL_FINAL.js and its dark cousin project_FINAL_FINAL_actually_final.js.
Your folder looks like a crime scene. Which version actually works? Which one has that feature you added last Tuesday? Is project_v3.js newer than project_final.js? (It might be. “Final” is the most optimistic word in any programmer’s vocabulary.)
This system has exactly one virtue: it exists. It has many vices: it’s confusing, fragile, swells across your disk like junk mail nobody throws out, and breaks down completely the moment two people need to work on the same code. If Alice copies project_v3.js and makes changes, and Bob copies project_v3.js and makes different changes, someone’s work is about to be overwritten. The question is whose, and the answer is whoever saves second.
There’s another problem, one that’s become urgent in the age of AI-assisted programming. You ask AI to refactor your code. It rewrites three files, restructures your functions, and changes your data model. The result looks reasonable. You test it, and something is subtly broken. You want to go back to what you had before. But what you had before is gone, overwritten, lost to the digital void. You’re now debugging AI-generated code with no way to compare it against the version that was working ten minutes ago.
Version control solves all of these problems. Properly, elegantly, and permanently.
What Git Does
Git is the version control system that nearly every programmer on earth uses. It was created in 2005 by Linus Torvalds, the same person who created Linux, because the existing version control tools annoyed him the way a dripping tap annoys you at 3 a.m., until he spent two weeks building a better one. (Annoyance, it turns out, is a powerful motivator in software development.)
Git tracks snapshots of your entire project over time. Not just individual files, but the whole project, all at once, frozen mid-motion. Every snapshot captures the state of everything: which files exist, what they contain, how they relate to each other. You can scrub forward and backward through these snapshots like dragging the playhead across a video timeline. At any point you can see exactly what your project looked like at any moment in its history, including the moment right before you broke it.
This sounds simple, and the core idea really is simple. The catch, if there is one, is that all the power hides in the details.
Commits: Meaningful Save Points
A commit is a snapshot with a purpose. When you commit, you’re telling Git: “The project is in a meaningful state right now. Remember this.”
Each commit records three things: what changed (the actual modifications to your files), when it changed (a timestamp), and why it changed (a message you write explaining your intent).
git add recipe.js
git commit -m "Add search by ingredient feature"
That -m flag is followed by your commit message. It’s a note to your future self, and it deserves more thought than most people give it. “Fixed stuff” tells you nothing. “Fix crash when recipe has no ingredients” tells you exactly what this commit does and why it exists.
Good commit messages are an act of kindness toward future-you. Present-you understands the change. Present-you just made it. Future-you, arriving at this commit six months later while investigating a bug at 11 PM, will have no such context. Future-you will read the message and either thank you or curse you, depending on whether you wrote “Fix ingredient search crash when list is empty” or “asdfasdf.”
A good rhythm: commit when you’ve completed a meaningful unit of work. Not every five minutes (that’s too granular), and not once a week (that’s too coarse). When something works that didn’t before, when you’ve added a feature, when you’ve fixed a bug: those are natural commit points. Each commit should be a coherent thought, not half a sentence and not a rambling paragraph.
The Diff: Seeing What Changed
A diff shows you, line by line, exactly what’s different between two versions. Lines that were added show up in green. Lines that were removed show up in red. Lines that didn’t change don’t show up at all, because nobody needs to be told what stayed the same.
git diff
This is extraordinarily useful. When you’ve been editing for an hour and can’t remember everything you changed, git diff shows you. When someone else modified a file, the diff shows you exactly what they did. When AI rewrites your code, the diff reveals every single change it made, including the ones it didn’t mention.
Reading diffs is a skill that develops quickly. After a while, you can glance at a diff and immediately understand the shape of a change: “Ah, they renamed a variable throughout the file,” or “They added error handling to this function,” or “They deleted the validation entirely. Why?”
Diffs are the precise language of change. Where a commit message says what was intended, the diff shows what actually happened. These two things are not always the same, which is why both matter.
Branches: Parallel Timelines
Here is where Git becomes genuinely powerful. Branches let you create parallel versions of your project that don’t affect each other.
Imagine your project is a timeline, a straight line of commits marching forward. A branch is a fork in that timeline. You peel off in a new direction, make changes, experiment freely, and the main timeline continues undisturbed.
git branch try-new-search
git checkout try-new-search
Now you’re on a branch called try-new-search. Everything you do here, every edit, every commit, happens on this branch only. The main code (typically called main) remains exactly as it was. You can experiment wildly. Rewrite entire modules. Try an approach that might be brilliant or might be terrible. The main code doesn’t know or care.
If the experiment works: merge the branch back into main. Your changes flow into the main timeline as if they’d always been there.
git checkout main
git merge try-new-search
If the experiment fails: delete the branch. Pretend it never happened. No harm done. The main code was never touched.
git branch -d try-new-search
This is freedom. The freedom to try things without fear, because you can always get back to where you were. The freedom to explore dead ends without losing your progress. The freedom to say “what if?” and find out, safely.
The Staging Area
One more concept, briefly: Git has a staging area (sometimes called the “index”) that sits between your working files and a commit. When you run git add, you’re choosing which changes to include in the next commit. Not everything has to go in at once.
This matters because you might have touched five files, but only three of those changes actually belong to the feature you’re committing. The staging area lets you bundle those three together under one coherent message, and leave the other two for a separate commit with its own. You don’t have to cram everything into one box and hope the label makes sense.
git add recipe.js search.js
git commit -m "Add ingredient search feature"
git add README.md
git commit -m "Update documentation with search instructions"
Two commits, each telling a clear story. Rather than one muddled commit containing unrelated changes and a message that tries to describe all of them at once.
GitHub: Where Code Lives Together
Git tracks your project’s history on your own computer. GitHub puts it on the internet.
GitHub is a platform built on top of Git that takes your code and makes it visible, shareable, and collaborative. Once your code lives on GitHub, it’s no longer trapped on your laptop, one spilled coffee away from oblivion. It’s reachable from anywhere, backed up against hardware failure, and, if you choose, visible to anyone in the world.
Your GitHub profile becomes a portfolio. Not of polished presentations, but of actual work: code you’ve written, problems you’ve solved, projects you’ve built. When someone wants to see what you can do, you can point them to your repositories and say, “Here. Look.”
Pull Requests: Proposing Changes
A pull request is a formal proposal to merge changes from one branch into another. It’s Git’s merge mechanism wrapped in a conversation.
You create a branch, make your changes, push them to GitHub, and open a pull request. The pull request shows the diff, everything you changed, and invites others to review it. They can comment on specific lines, suggest improvements, ask questions, point out bugs you missed.
This is code review, and it’s one of the most effective quality-control mechanisms software has. A second pair of eyes catches the things you walked straight past, precisely because you were too close to the code to see them. The reviewer meets the change fresh, without the assumptions and context that made it look obviously correct to you.
Pull requests are conversations about code. They have a beginning (the proposal), a middle (the review and discussion), and an end (the merge or the decision to try a different approach). They create a written record of why changes were made and what was considered, a record that’s searchable and permanent.
Issues: Tracking What Needs Doing
Issues are GitHub’s way of tracking bugs, feature requests, and tasks. Someone discovers a bug? Open an issue. Want to add a feature? Open an issue. Need to refactor that module that makes everyone wince? Open an issue.
Issues are a shared to-do list for a project. You can assign them to specific people, label them by type or priority, and wire them to the pull requests that fix them. When a pull request closes a bug, it can point back at the issue: “Fixes #42.” Merge the pull request and the issue closes itself, no extra bookkeeping required. The trail from “problem identified” to “problem solved” stays clear and traceable.
Open Source: Code as a Commons
Remember libraries from Chapter 8? The code that other programmers wrote and shared freely? That code lives on GitHub. (Most of it, anyway.) Open source is the practice of making source code publicly available, free for anyone to read, use, modify, and distribute.
This is how millions of programmers share code. You find a library that does almost what you need but not quite? You can read its source code, understand how it works, and perhaps contribute an improvement. The library’s maintainers review your pull request, and if they like it, your code becomes part of a tool used by thousands of people.
Open source is a gift economy built on enlightened self-interest. People contribute because they need the software themselves, because contributing builds their reputation, because they believe in the commons, or because solving interesting problems is simply enjoyable. The result is a shared infrastructure of code that underpins nearly everything digital.
Version Control and AI
Version control is useful in all programming. It becomes essential when you’re working with AI.
Working with AI introduces a particular kind of risk: large, sweeping changes that you didn’t write yourself and don’t fully understand. AI can refactor an entire file in the time it takes you to reach for your coffee. It can restructure your data model, rename your functions, rewrite your algorithms. The changes might be brilliant. They might also be subtly, quietly wrong. Version control is your safety net.
Commit Before AI Changes
Before you ask AI for anything ambitious (a refactor, a new architecture, a rewrite of a core module), commit your current work. Make sure your working state is saved. This is your checkpoint, your save point before the boss fight.
If AI’s changes work: wonderful. Commit them too, with a message describing what changed.
If AI’s changes break things: revert to your previous commit. You’re back to working code before the kettle finishes boiling. No damage done, no frantic attempts to remember what the code looked like before AI got its hands on it.
git commit -m "Working search feature before refactor"
# Now ask AI to refactor...
# If it goes wrong:
git checkout .
# Back to your last commit, clean and working
This simple discipline, commit before asking AI for big changes, will save you hours of frustration. It transforms AI experimentation from risky to routine.
Use Diffs to Review AI’s Work
When AI modifies your code, don’t just glance at the result and assume it’s correct. Run git diff. Read what changed, line by line. This is exactly how you’d review a colleague’s work, and AI deserves no less scrutiny.
The diff reveals things that a casual reading might miss. Did AI remove error handling you’d carefully added? Did it change a variable name in three places but miss a fourth? Did it introduce a subtle change to the logic that “simplifies” the code but also changes its behavior?
AI is confident. It presents its changes with the breezy certainty of a tour guide who has never once checked the map. The diff is your reality check, a dispassionate record of what actually changed, free from AI’s natural tendency toward plausibility over precision.
Branches for AI Experiments
AI suggests a completely different approach to your search feature? Create a branch.
git checkout -b ai-search-experiment
Let AI rewrite the search on this branch. Test it. Compare it with your original approach on main. If it’s better, merge it. If it’s not, delete the branch. You’ve lost nothing except a few minutes, and you’ve gained information about an approach that didn’t work, which is, in its own way, valuable.
Branches make AI experimentation free. There’s no cost to trying something and no risk in abandoning it. This changes your relationship with AI’s suggestions. Instead of “should I let AI rewrite this?” (anxiety, hesitation, fear of losing working code), the question becomes “why not try it on a branch?” (curiosity, experimentation, confidence in your safety net).
Commit Messages as Communication
Even when AI writes the code, you write the commit message. This matters more than it might seem.
AI generates code. It doesn’t generate intent. The commit message is where you record why this change exists: what problem it solves, what decision it represents, what it means for the project. “Add fuzzy search using Levenshtein distance” communicates something the code alone doesn’t. It says: we considered exact matching and chose fuzzy matching instead. We picked Levenshtein distance specifically. This was a deliberate choice, not an accident.
Your commit history tells the story of your project’s evolution. AI can write individual scenes, but you’re the narrator. The story only makes sense if someone is paying attention to the arc.
Communication Across Time
Version control, at its heart, is a communication system. But the audience is unusual: it’s you, displaced in time.
Commit messages are letters to your future self. They cross the gap between present-you (who understands everything, who just lived through the decision, who has all the context) and future-you (who has forgotten everything, who is confused, who is possibly debugging at an unreasonable hour). The quality of those letters determines whether future-you solves the problem in five minutes or five hours.
Diffs are a precise language for describing change. Not vague (“I updated some things”), not aspirational (“I improved the code”), but exact. These lines were added. These lines were removed. This is what changed, character by character. It’s the most honest language in programming, because it shows what actually happened rather than what someone remembers happening.
Pull requests are conversations about code that persist long after the conversation ends. Six months from now, someone will wonder why the search feature works the way it does. The pull request that introduced it contains the discussion: the alternatives considered, the trade-offs weighed, the reasons for the final decision. It’s institutional memory, encoded in a format that doesn’t yellow in a drawer or depend on any particular person still being around to explain it.
And the commit history itself is a narrative. Read chronologically, it tells the story of how your program grew from nothing into something. Every feature added, every bug fixed, every experiment tried and abandoned. It’s the story of a project’s life, told in small, precise increments.
This connects to something we’ve explored throughout this course: programming is communication. You communicate with the computer through code. You communicate with other programmers through readable code, clear names, and helpful comments. And through version control, you communicate across time: with your future self, with future collaborators, with anyone who inherits your project after you’ve moved on.
The tools change. The audience changes. But the fundamental act remains the same: making your intent clear to someone who doesn’t share your current context.
You have the tools now. All of them. You can think precisely, read code, work with AI, debug, use libraries, write readable code, understand the broader landscape, and manage your code with version control. There’s one thing left: building something that’s entirely yours.
Next: With all these tools in your mental toolkit, it’s time to put them together. The capstone project awaits.