Learning Material

Version Control Beyond the Basics

Guide · Git & Team Workflows

Cover: Version Control Beyond the Basics

Beyond add, commit, push

Most developers learn Git as a linear sequence of commands and stop there. The real leverage is in branching strategy — how your team decides when branches get created, what they're for, and how they come back together. Get this wrong and merges become dreaded events; get it right and they're routine.

Feature branching: the default for a reason

Every piece of work — a feature, a bug fix — gets its own branch off main, and merges back in via a pull request once it's reviewed:

git checkout -b feature/user-avatars
# ... make changes, commit ...
git push -u origin feature/user-avatars
# open a pull request, get it reviewed, merge

The benefit isn't just organization — main stays deployable at all times, because nothing lands there without review. The cost is coordination overhead, which barely matters for a solo project but becomes real once several people are branching off the same codebase simultaneously.

Trunk-based development: fewer branches, faster integration

Instead of long-lived feature branches, everyone commits small, frequent changes directly to main (or very short-lived branches merged within a day), often behind feature flags so incomplete work doesn't affect real users. This avoids the classic problem of feature branches: the longer a branch lives separately from main, the more it diverges, and the more painful the eventual merge becomes.

Trunk-based development needs strong automated testing and a team discipline of small commits to work well — without that safety net, it just becomes "everyone breaks main sometimes."

Rebase vs. merge: same goal, different history

# Merge: creates a merge commit, preserves exact history
git checkout main
git merge feature/user-avatars

# Rebase: replays your commits on top of main, linear history
git checkout feature/user-avatars
git rebase main

Merge keeps an honest record of what actually happened, including when. Rebase produces a cleaner, linear history that's easier to read later, at the cost of rewriting commit hashes — which is exactly why you should never rebase a branch other people have already pulled from; you'd be rewriting history out from under them.

The rule that prevents real disasters

Never force-push to a shared branch. git push --force after a rebase overwrites the remote history — if someone else already pulled the old version, their local history now conflicts with what you've published, and reconciling it is genuinely painful. If you must force-push, --force-with-lease at least refuses to overwrite work you haven't seen yet.

Picking a strategy for your actual team

Solo project or a very small team shipping constantly: trunk-based, or lightweight feature branches merged within a day or two. Larger team, or a codebase where a broken main is expensive: feature branching with required reviews. Regulated environment needing an audit trail: merge commits over rebase, so the history reflects what actually happened, not a cleaned-up version of it.

There's no universally correct answer — the right strategy is the one that matches how fast your team actually ships and how much a broken main branch would cost you.

Comments