By day 86 of your Git and GitHub journey, you've likely encountered merge conflicts in production, wondered why a rebase went sideways, or scrambled to recover a deleted branch. This article consolidates the Git and GitHub learning roadmap: lessons from production into a structured progression that mirrors how real teams work, not how tutorials present the material.
We've learned across trading systems, e-commerce platforms, and ride-hailing services that there's a natural order to mastering version control. Follow this roadmap and you'll build muscle memory that prevents costly mistakes.
Commits, branches and remotes
Everything starts here. A commit is a snapshot of your work; a branch is a pointer to a series of commits; a remote is where your code lives when you're not looking at it. Until these three concepts feel automatic, nothing else matters.
In production systems, we commit often but deliberately. Each commit should represent one logical change—not a day's work, not "stuff I did before lunch." We've seen teams where commits read like a diary ("fixed thing", "still broken", "ok now it works") and teams where they read like a changelog. The second group ships faster.
Create a branch for every feature or fix:
git checkout -b feature/user-auth-retry
git add src/auth/retry.ts
git commit -m "add exponential backoff to auth requests"
git push -u origin feature/user-auth-retryThe branch name tells the story. The commit message is actionable. When you push to the remote (usually GitHub), your team can see it immediately. This is not optional in professional work.
Understand that your local branch, the remote-tracking branch (origin/feature/user-auth-retry), and the remote branch itself are three separate things. Most developers conflate them for years. They're not. Fetching updates your local knowledge of the remote; pushing sends your local commits up.
Merge, rebase and conflict resolution
Now you need to get your work into the main line. Merge and rebase both integrate one branch into another, but they do it differently.
A merge creates a commit that has two parents. Your feature branch history stays intact. You see exactly when the integration happened. We use merges for long-lived features and when the history itself matters:
git checkout main
git pull origin main
git merge feature/user-auth-retry
git push origin mainA rebase replays your commits on top of the target branch, rewriting history. The result is linear, clean, easier to bisect. We use rebases for short-lived feature branches before they're merged to main, and always locally:
git fetch origin
git rebase origin/main
# Or interactively to clean up commits:
git rebase -i origin/mainNever rebase commits that are already on a shared remote branch. Rewriting shared history breaks your teammates' work. This is the number-one rebase mistake in production.
Conflicts happen when two branches change the same lines. Git can't decide which version to keep. You have to:
- Resolve the conflict manually in your editor (look for
<<<<<<<markers) - Stage the resolved file:
git add - Complete the merge or rebase:
git commitorgit rebase --continue
In a real scenario: two developers add different validation rules to the same config file. You pull main, see the conflict, talk to your teammate about which rule should win (or combine them), resolve it, and commit. The commit message should mention the conflict and why you resolved it that way. Your future self will thank you.
Pull requests, reviews and Actions
A pull request (PR) is your feature branch asking permission to join main. It's where code quality, architecture, and correctness get enforced. It's also where junior and senior engineers learn from each other.
Push your branch and open a PR on GitHub. Write a clear title and description:
# Bad PR title
Fix stuff
# Good PR title
Add exponential backoff to authentication requests (fixes #1234)In the description, explain why the change exists, not just what it does. Link to the issue. If there are tradeoffs, mention them. If tests are needed, add them before opening the PR.
Your team reviews it. GitHub Actions runs automated checks—linting, tests, build verification—before a human ever looks at the code. This is non-negotiable in 2024. A failing Action is a blocker.
Reviews go back and forth. "Why did you use `Promise.all` here instead of `Promise.allSettled`?" forces the original author to articulate the decision. This is how teams develop shared standards. Approve the PR only when you'd be comfortable supporting it in production.
At scale, we've seen teams where PRs languish because review SLAs aren't clear. Set expectations: a PR should be reviewed within 24 hours. If it's blocked longer, escalate. Slow reviews kill velocity.
Recovery, history tools and releases
Eventually, you or someone on your team will:
- Delete a branch by accident and need it back
- Revert a commit that broke production
- Wonder what changed between version 1.0 and 1.1
- Need to find which commit introduced a bug
Git has tools for all of this. First, recovery. Git keeps a log of every position your HEAD pointer has been. Accidentally reset hard? Use reflog:
git reflog
# Shows something like:
# abc1234 HEAD@{0}: reset --hard
# def5678 HEAD@{1}: commit (message here)
git reset --hard def5678That branch you deleted? It's probably still there:
git reflog show feature/lost-branch
git checkout -b feature/lost-branch abc1234To revert a bad commit that's already on main, use git revert, which creates a new commit that undoes the old one. Never force-push main:
git revert abc1234
git push origin mainFor history exploration, log is your telescope:
# See commits touching a specific file
git log -p src/auth/retry.ts
# Find who changed what and when
git blame src/auth/retry.ts
# Bisect to find the exact commit that broke something
git bisect start
git bisect bad HEAD
git bisect good v1.0
# Git checks out commits; you test and mark them good/bad
# until it narrows to the breaking commitReleases are tags. Tag stable versions:
git tag -a v1.2.3 -m "release: version 1.2.3"
git push origin v1.2.3Use semantic versioning. Major.minor.patch. Tools like GitHub Releases let you attach binaries, notes, and checksums to a tag. This is how your users know what they're running.
Common mistakes
Committing to main directly. Even for small fixes. Always branch. It keeps history clean, enables code review, and gives you an escape hatch if something goes wrong.
Writing vague commit messages. "Update code" tells no one anything. "Add retry logic to auth with exponential backoff per #542" is a sentence from your project's history.
Rebasing shared branches. If the branch exists on the remote and teammates are using it, don't rebase. Merge instead. Rebase locally only.
Ignoring failed Actions. If tests fail, the PR is not ready. Fix it. Merging broken code saves 5 minutes now and costs 5 hours in debugging later.
Leaving stale branches everywhere. Delete merged branches. Use git branch -d and GitHub's "Delete branch" button. Hundreds of dead branches confuse new developers and slow down tab completion.
Key takeaways
- Commits, branches, and remotes are the foundation. Master them before worrying about advanced workflows.
- Merge to integrate long-lived or important branches; rebase locally to clean up short-lived feature branches before review.
- Conflicts are a feature, not a bug. Resolve them deliberately and document your reasoning in the commit message.
- Pull requests enforce quality through automation and human review. Treat them as mandatory, not optional.
- Learn reflog, log, blame, and bisect. These tools save hours of debugging in production environments.
- Tag releases consistently. Your users, your CI/CD pipeline, and future you all depend on clear version markers.
- Write for your team and your future self. Every commit message, PR title, and branch name is documentation.






Comments
Loading comments…