This series follows a fictional conversation between an experienced engineer and his nephew. Every episode explores one stage of how software moves from an idea to production.
π¦ Nephew: Uncle, code's ready. git add ., git commit, git push to main, done?
π¨β𦳠Uncle: Stop. Two problems already β "code's ready" and "push to main." Let's go all the way back to the beginning, because you skipped the part that actually matters most.
π¦ Nephew: The beginning of what?
π¨β𦳠Uncle: Of this branch's entire life. Before we get there β do you know what Git actually changed, when it showed up?
π¦ Nephew: Version control, right?
π¨β𦳠Uncle: Before Git, most teams worked off a shared folder, or a system where a file was locked while one person edited it β nobody else could touch it until they were done. Git changed the whole model: everyone works on a full copy of their own, completely independently, and merging intelligently became possible instead of "wait your turn." That's why, decades later, it's still how nearly every team on earth builds software together. But that same freedom is exactly why a team needs real discipline around it β a tool that lets everyone work independently can also let everyone quietly step on each other, if nobody agrees on the rules. Let me show you the whole journey of one feature, properly, from the ticket to the merge.
Step One: The Branch Starts From an Updated Main
π¨β𦳠Uncle: WISH-24 β the Wishlist ticket from Episode 1 β just got assigned to you. What's the very first Git command you run?
π¦ Nephew: git checkout -b feature/wishlist?
π¨β𦳠Uncle: Straight from whatever your local main happened to be, possibly days old? No.
git checkout main
git pull origin main
git checkout -b feature/WISH-24-add-wishlist
Enter fullscreen mode Exit fullscreen mode
π¦ Nephew: Why does it matter if my main is a few days old before branching?
π¨β𦳠Uncle: Because every commit that's landed on main since you last pulled is a commit you're not building on top of. Branch from stale code, and you're setting yourself up for a bigger conflict later, for no reason. And notice the branch name β WISH-24 isn't decoration. It's how anyone β a teammate, a reviewer, you in three months β can trace this branch straight back to Episode 1's ticket without asking.
One more thing, right now, before you write a single line: you will never push this branch directly to main. main is protected β everything you're about to do happens here, on feature/WISH-24-add-wishlist, until it's earned its way onto main through review. Keep that in the back of your mind for the rest of this journey; we'll come back to exactly why it's enforced, not just agreed upon.
Step Two: Small Commits, Not One Giant One
π¦ Nephew: Now I write the whole feature, and commit once at the end?
π¨β𦳠Uncle: That's how you end up with a commit history that looks like this.
Final changes
more fixes
again
final final
asdf
Enter fullscreen mode Exit fullscreen mode
Useless to anyone reading it later, including you. Instead:
Add wishlist endpoint
Handle duplicate item validation
Add unit tests for duplicate case
Update API docs for /wishlist
Enter fullscreen mode Exit fullscreen mode
π¦ Nephew: Why does it actually matter, if it all ends up in the same PR anyway?
π¨β𦳠Uncle: Because six months from now, someone runs git blame on a line that's misbehaving, and finds either "Handle duplicate item validation" β instantly useful β or "asdf" β useless. Small, honest commits are a gift to whoever debugs this after you, and that person is often you.
Step Three: Open a Draft PR Early, Not at the End
π¦ Nephew: So I keep working, and only open a PR once everything's finished?
π¨β𦳠Uncle: On a lot of teams, no β you open a Draft PR almost immediately, after the first meaningful commit.
Create branch
β
Write first commit
β
Open Draft PR
β
Keep pushing commits
β
Mark "Ready for Review"
β
Merge
Enter fullscreen mode Exit fullscreen mode
π¦ Nephew: Why open it before it's even done?
π¨β𦳠Uncle: A draft PR is visible to the whole team, but explicitly marked "not ready yet" β nobody's expected to review it seriously. It lets a teammate glance at your direction early and flag a problem before you've built three more days on top of it. It also means your CI starts running immediately, on every push, instead of surprising you all at once at the end. And a pull request isn't only for finding bugs β it's for making sure two engineers never spend three days independently building different solutions to the same problem.
Step Four: Sync With Main While You Work
π¦ Nephew: And while I'm still working?
π¨β𦳠Uncle: You rebase against main regularly β not once, right before merging.
git fetch origin
git rebase origin/main
Enter fullscreen mode Exit fullscreen mode
Remember Episode 3 β we drew boundaries so one feature wouldn't force you to reopen a box someone else owns. If those boundaries hold, you and a teammate rarely touch the same file at the same time, and syncing often keeps whatever overlap remains small enough to resolve in minutes, not hours.
Step Five: CI Has to Pass β Nobody Merges on "Looks Good"
π¦ Nephew: Reviewer left a comment: "Looks good, approving." Can I merge now?
π¨β𦳠Uncle: Look at the bottom of the PR first.
β
Unit Tests
β
Build
β
Lint
β Integration Tests
Enter fullscreen mode Exit fullscreen mode
One red cross. No merge β not because I said so, because the repository itself won't allow it.
π¦ Nephew: Even with a human approval already on it?
π¨β𦳠Uncle: Even then. A real production pipeline runs this automatically, on every single push, before a human's opinion even matters:
PR opens
β
Lint
β
Unit tests
β
Security scan
β
Build
β
Coverage check
β
Reviewer approval
β
Merge enabled
Enter fullscreen mode Exit fullscreen mode
Human approval and CI passing are two separate gates. Both have to open before the merge button even becomes clickable. That's the single most important production Git concept most juniors never see, because on a personal project, there's no CI watching over your shoulder at all.
π¦ Nephew: What if someone else's PR merges into main while mine is still being reviewed?
π¨β𦳠Uncle: Then your branch is now behind main again, even though your CI passed a moment ago. You sync β rebase against the latest main β and CI runs again from scratch. Only once it's green against the current main, not the main from an hour ago, does merge become possible. A green check next to an outdated branch means nothing.
Step Six: Who Actually Reviews This?
π¦ Nephew: Who gets assigned as reviewer β whoever's online?
π¨β𦳠Uncle: On a real repo, it's usually decided before anyone even opens the PR, through something like a CODEOWNERS file.
/services/wishlist/** β @backend-team
/infra/** β @platform-team
/auth/** β @security-team
Enter fullscreen mode Exit fullscreen mode
Touch a file under /auth/, even accidentally, and the security team is automatically requested β whether you meant to touch auth or not. That's not bureaucracy for its own sake; it's how a large company makes sure the person reviewing actually knows the part of the system you touched.
Step Seven: Merge β but Which Kind?
π¦ Nephew: CI's green, review's approved. Now git merge?
π¨β𦳠Uncle: Now you find out which merge strategy this specific team has chosen β because companies genuinely differ here, and none of them is universally "correct."
Squash Merge β all your commits become one clean commit on main
Merge Commit β your full commit history is preserved, plus a merge commit
Rebase Merge β your commits are replayed onto main individually, no merge commit at all
Enter fullscreen mode Exit fullscreen mode
π¦ Nephew: Which one's right?
π¨β𦳠Uncle: The one your team already agreed on and uses consistently. Squash keeps main's history short and readable, at the cost of losing your intermediate commits. Merge commits preserve everything, at the cost of a noisier history. What actually matters isn't which one you pick β it's that the whole team uses the same one, so the history means the same thing everywhere in the repo.
Step Eight: The Branch Dies, on Purpose
π¦ Nephew: And after it merges?
π¨β𦳠Uncle: The branch gets deleted. Its job is done β it existed only to carry this one change safely from your machine to main. Keeping it around just clutters the repo with branches nobody will ever look at again.
The Two Rules That Protect All of This
π¨β𦳠Uncle: Underneath this whole journey, two rules make it safe. First β main is protected. Nobody, including the most senior engineer in the building, can push to it directly. Every change goes through this exact path. Second β force-push is banned on shared branches.
Teammate pushes commit A
You force-push, based on an older history
β commit A is now gone from the remote branch
β nobody gets an error
β someone just discovers their work missing, later
Enter fullscreen mode Exit fullscreen mode
On your own branch, git push --force-with-lease is fine β it checks nobody else's work would be erased before it lets you push. On main, or any shared branch, it's blocked outright, by the repository itself, not just by convention.
Branch β Sync β Review
π¦ Nephew: The framework β does it still hold, after all of this?
π¨β𦳠Uncle: It holds, it just carries more now.
Branch
β
Sync
β
Review
Enter fullscreen mode Exit fullscreen mode
Branch β from an updated main, named after the ticket, opened as a draft the moment there's something worth seeing.
Sync β small, honest commits, rebased against main often, so conflicts stay small and rare.
Review β two independent gates before merge: CI passing, and a human who actually owns that part of the codebase approving it β then a merge strategy the whole team already agreed on, applied the same way every time.
Uncle's Line
"Git doesn't protect your code. Team discipline does. Git is just the notebook."
π¦ Nephew: So the whole journey β ticket, branch, small commits, syncing, draft PR, CI, review, merge, branch deleted β that's what actually happens every time, on a real team.
π¨β𦳠Uncle: Every single time, on every serious team you'll ever work on. Now your code is on main. That feels like the finish line. It isn't β it's the starting gun. What happens to it next is a different lesson entirely.
π§ Think Like an Engineer β Homework
Take the same feature you've been designing since Episode 3. Walk it through the entire journey yourself.
- Branch β write the branch name, tied to a ticket ID, and the two commands you'd run before creating it.
- Sync β write three small, honest commit messages you'd realistically make while building it, instead of one giant one.
-
Review β name one CI check you'd want gating this PR beyond tests (lint, security scan, coverage), and one file path that should automatically pull in a specific team via something like
CODEOWNERS.
Don't worry about the exact commands. The goal is to practice seeing your code as one small piece moving through a pipeline the whole team relies on β not as something finished the moment you stop typing.
β End of Episode 8 β
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.