Lesson 4.2 · 13 min read
Git, as you now understand it, is a personal tool. A chain of snapshots on your machine, labels you move around, a history you can rewrite. Everything in the last post could be done alone in a room.
This post is about what happens when the codebase belongs to a team. Because the moment other people are pushing to the same repository, Git stops being a version control system and becomes a coordination system, and the constraints change completely. Now there is a shared reality that everyone depends on, and your job is to add to it without breaking it, in a way that other people can understand, review, and trust.
This is the transition that matters for you. Building alone, you can do anything. Contributing to a real codebase is a different skill, and it's the one that turns a designer who codes into someone an engineering team is glad to have. Most of it isn't technical. It's about making your work legible and safe.
You know a branch is a movable label. Now consider why teams organize their entire workflow around them.
There is one branch, usually main, that represents the truth. It is what runs in production, or what will next. It must always work. Everything else, all the half-finished ideas and experiments and features in progress, must happen somewhere else and only join main when it's genuinely ready.
That is the whole point of a feature branch. You create a label at the current state of main, do your work there, and main stays untouched and working the entire time, no matter how broken your branch gets. When your work is finished and reviewed, it joins main. Until then, your mess is entirely your own, which is a real kindness to everyone else.
Notice what this means practically. Ten people can work on ten different things simultaneously, each on their own branch, each starting from the same shared truth, without ever interfering with each other. Git makes this cheap, because a branch is a label, not a copy. And when each of them is ready, their work joins main one at a time, each time creating a new shared truth.
A few habits make branches work well, and they're all about being kind to the people who come after you, including yourself.
Keep branches short-lived. A branch that lives for three days rejoins main easily. A branch that lives for three weeks has drifted far from a main that kept moving, and merging it becomes an archaeological expedition. The longer you stay separate, the more reality diverges. This is why teams push toward small changes landed frequently rather than enormous ones landed rarely, and it's a genuine engineering principle rather than a process preference.
Keep them focused. One branch, one coherent change. A branch that fixes a bug and also refactors a component and also updates some copy is three things wearing one coat, and it is much harder to review, much harder to reason about, and much harder to undo if one of the three turns out to be wrong.
Stay current with main. While you work, main moves. Periodically bring those changes into your branch, by rebasing onto it (your own unshared branch, so this is safe, exactly as the last post established) or by merging main into it. Do this regularly and integration is trivial each time. Avoid it and you accumulate a debt that comes due all at once, painfully, on the day you try to merge.
Here is the thing people misunderstand most. A pull request is not a technical mechanism for merging code. Git already merges code, and always could, from the command line, with no platform involved.
A pull request is a proposal, opened for discussion. You are saying to your team: here is a change I would like to become part of our shared truth. Here is what it does, here is why, here is the code. Please look at it before it lands.
Everything about pull requests follows from understanding them as communication rather than plumbing. The diff is not the whole artifact; it's the evidence. The description is the argument. The conversation in the comments is where the real work of engineering culture happens.
Which means that writing a good pull request is a writing task, and you, of all people, should be exceptionally good at it. A reviewer arrives at your PR knowing nothing. What do they need?
What this changes, in one sentence. Not the file names. The user-visible or system-visible effect. "Adds the ability to archive a project" or "Fixes the crash when a task has no due date."
Why. The context that lives in your head and nowhere else. What problem prompted this, what alternatives you rejected and why, what tradeoff you knowingly accepted. This is the part everyone skips and the part reviewers most need, because code shows what you did and never why.
How to check it. How does someone verify this actually works? What did you test? Is there something specific you want them to look at closely, a decision you're unsure about, a place you'd genuinely welcome a second opinion?
What it doesn't do. The known limitations, the follow-up you're deliberately leaving for later. Naming these prevents a review that spends its energy pointing out things you already know.
A pull request written like that gets reviewed quickly, kindly, and well. A pull request with no description and forty changed files gets reviewed slowly, superficially, and with quiet resentment. The difference is entirely within your control, it takes five minutes, and it will do more for how a team perceives you than the code itself.
The corollary is that the shape of your change determines whether it can be reviewed properly at all.
Human attention for reading code is limited and it falls off a cliff. A change of fifty lines gets genuine, careful scrutiny, and real bugs are caught. A change of a thousand lines gets skimmed, and the reviewer approves it with a comment about a variable name, because nobody can hold a thousand lines of unfamiliar logic in their head. This is not laziness; it's a known property of people. Large pull requests do not get reviewed. They get waved through, which is worse than not reviewing them at all, because now everyone believes it was reviewed.
So the discipline is to make changes small on purpose. If a feature is large, land it in pieces that each make sense on their own. Do the refactoring in one pull request and the new behavior in the next, so the reviewer can see clearly that the first changed nothing and the second is the real work. Separate mechanical changes (renaming, reformatting, moving files) from meaningful ones, always, because a reviewer confronted with three hundred lines of renamed variables and one line of changed logic will not find the one line.
This is also where a habit from the last post pays off. Because you understand the staging area, you can commit deliberately, choosing what belongs in each snapshot. And because you understand interactive rebase, you can reshape a messy branch into a clean sequence of commits before anyone sees it. Your fifteen chaotic commits, "wip", "fix", "actually fix", "revert", can become three clear ones that tell the story of the change. That's not vanity. A reviewer who can walk your commits one at a time can follow your thinking, and a good sequence of commits is a form of explanation.
Now the human part, which is where teams succeed or quietly rot.
Somebody is going to read your code and tell you things about it. This can feel like an evaluation of you, and for many people it silently is, which is why code review goes wrong so often. It's worth being deliberate about how you hold it.
When receiving a review: the comments are about the code, and the code is not you. A reviewer catching a bug has saved you from shipping it, which is the entire point of the exercise and a gift, however it's phrased. Read every comment as a question about the code rather than a verdict about your competence, because that framing is both more useful and, in nearly every case, accurate.
You are allowed to disagree. A review is a conversation, not an instruction. If a reviewer suggests a change and you have a reason not to make it, say the reason. Often you know something they don't, and the discussion improves both of you. What you should not do is silently comply with a suggestion you believe is wrong, because now the code is worse and nobody learned anything.
And when a reviewer asks "why did you do it this way," that is almost always a real question rather than a criticism dressed as one. Answer it. If you find you can't, that's genuinely useful information about the change.
When giving a review, the responsibilities run the other way. Be specific and be kind, and understand that those aren't in tension. "This will break if the array is empty" is specific, kind, and useful. "This is wrong" is none of those. Distinguish clearly between things that must change, things you'd prefer, and things you're merely curious about, because a reviewer who marks everything as equally urgent forces the author to guess. Many teams prefix optional comments with something like "nit:" for exactly this reason.
And review for the things a machine cannot. Formatting, style, and obvious errors should be handled by automated tooling, not by a human's attention, which is scarce and better spent elsewhere. What a human should look for: is this the right approach, does this handle the cases the author didn't think about, will this be comprehensible in a year, does this create a problem for something else in the system? Those are judgments, and judgment is what review is for.
You've done your work. Meanwhile, main has moved. You need to integrate. This is the moment where the last post's theory becomes daily practice.
Fetch first, always, so your origin/main reflects the actual server rather than your stale memory of it. Then bring main's changes into your branch, either by rebasing your commits on top of the new main, which gives you a clean straight history, or by merging main into your branch, which is safe and honest and leaves a merge commit. Which one you use is usually your team's convention rather than a technical decision, and both are correct.
If both sides changed the same lines, you get a conflict, and Git stops and asks you to decide. You now know this is not an error and not a failure; it is Git declining to guess about intent. Resolving it means looking at both versions, understanding what each was trying to accomplish, and writing the version that accomplishes both. AI is genuinely good at proposing resolutions. The verification, whether the resulting code actually does what both changes intended, is yours, and it requires understanding both changes. This is one of the places where reading code fluently, from the last module, turns into something you get paid for.
Then, if you rebased a branch you'd already pushed, you force push, with the lease, because your history was rewritten and the remote's copy is now the old version. On your own feature branch, unremarkable and routine. That's the workflow, and you understand every piece of it.
Some of this is technical and most of it isn't. Here is what makes someone easy to work with on a shared codebase, which is a real skill and one that's noticed.
Read before you write. Before changing anything in an unfamiliar area, understand how it currently works and, more importantly, why. A pattern that looks wrong is often protecting against something you haven't encountered yet. Ask what it's for before you improve it away.
Follow the codebase's existing conventions, even where you'd have chosen differently. A codebase where every contributor imposed their own preferences is a codebase nobody can read. Consistency is worth more than any individual's taste, including yours, and this is exactly the argument you'd make about a design system.
Do not reformat things you aren't changing. Nothing makes a pull request unreviewable faster than an unrelated formatting sweep hiding the actual change.
Say what you're working on before you build it, if it's substantial. The most demoralizing pull request is the one that solves a problem someone else already solved, or that takes an approach the team decided against last month for good reasons, or that touches a file being heavily reworked by someone else right now. A one-line message before starting saves a week of work.
Never push directly to main, even when you technically can, and even when the change is small and obviously correct. The point of the process is that it holds for everyone. The exceptions are where the discipline dies.
Small, frequent, explained. If you take nothing else from this post: land small changes often, and explain them well. Everything else follows from those two habits, and they're both entirely within your control from your very first contribution.
AI can do a great deal of this. It will create your branch, write your commits, rebase you onto main, resolve your conflicts, draft your pull request description, and respond to review comments. It's fast and it's largely correct.
But notice what it cannot do, because this is where your value actually sits.
It cannot decide what belongs in one pull request and what belongs in three. That's a judgment about human attention and about the coherence of a change, made by someone who knows the team and the codebase.
It cannot supply the why. The context for a change lives in conversations, product decisions, constraints, and things you know that were never written down. An AI-drafted description will accurately state what the code does, which the reviewer could see anyway. Only you can say why it was done this way and what you rejected.
It cannot verify a conflict resolution is correct. It can propose one, plausibly. Knowing whether the merged code actually preserves both intentions requires understanding both intentions, and only one party to this collaboration has been in the meetings.
It cannot tell you that a reviewer is uncomfortable with a change and hasn't quite said so, or that the right move is to split this and ship the safe half now, or that this refactor should wait until after the launch. That's not code. That's working on a team.
Use AI for all the mechanics, without hesitation. The mechanics were never the hard part. What makes someone genuinely valuable on a codebase is judgment about scope, clarity about intent, care about the people who will read this after you, and the discipline to keep the shared thing working. None of that is typing, and none of it is going away.
Do this before the next post
Go find a real pull request in a public repository, on any project you use or admire, and read it as an artifact rather than as code.
Look at how big it is. Could you actually review it, or would you skim? Read the description: does it tell you what changed, and why, and how to verify it? Now read the conversation underneath. Watch how people ask questions, how they disagree, how the author responds. Notice which comments are specific and useful and which are noise. You are looking at how a team communicates, preserved in amber, and it's one of the best free lessons in engineering culture available.
Then find a small one, twenty or thirty lines, and genuinely review it yourself. Read every line. Ask what happens in the case the author didn't handle. Ask whether you'd understand this code in a year without the context you have right now. You won't necessarily catch anything, and that's fine. What you're building is the habit of reading a change with a reviewer's attention, which is a different and more valuable thing than reading code to understand it.
And if you have your own work sitting in a branch somewhere: before you push it, write the pull request description first, in a text file, in plain language. What does this change, why, and how would someone check it? If you struggle to write that, the problem is not the description. The change isn't clear yet, and you've just discovered it in the cheapest possible way.