GitHub Pull Requests — Force Push Lost 47 Commits
A force push to main deleted 47 commits, blocking production 6 hours.
- Branch: your isolated copy of the code where you develop a feature or fix
- PR description: answers three questions — what changed, why it changed, how to test it
- Code review: at least one teammate reads your diff line by line before approving
- Merge strategy: how your commits land on main — merge commit, squash, or rebase
- Branch protection: rules that enforce reviews, tests, and prevent direct pushes to main
Imagine you're writing a chapter for a shared school textbook. Before your chapter gets printed, your teacher reads it, suggests edits, and only adds it to the book once everyone agrees it's good. A Pull Request is exactly that — you write your code on your own copy, then formally ask your teammates to read it, suggest changes, and approve it before it becomes part of the main project. The 'review' is the feedback your teacher gives you.
Every professional software team on the planet shares code. Not by emailing zip files or copying folders — they use a structured process where every single change gets proposed, discussed, and approved before it touches the production codebase. GitHub Pull Requests are the mechanism that makes this possible, and they're the beating heart of collaborative software development at companies like Google, Netflix, and Spotify.
Without a review process, one developer's typo can break the app for every user at 2am on a Friday. One misunderstood requirement can send a team down the wrong path for a week. Pull Requests solve this by creating a formal checkpoint: before any code merges into the main branch, at least one other human looks at it. That human catches bugs, asks clarifying questions, and ensures the change actually fits the bigger picture.
The merge strategy you choose — merge commit, squash, or rebase — directly affects how debuggable your history is six months later when you're chasing a regression at 3am. Branch protection rules are what enforce the review process; without them, anyone can push directly to main.
By the end of this article you'll know exactly what a Pull Request is, how to open one from scratch on GitHub, how to leave a code review that your teammates will actually appreciate, and how to avoid the three mistakes that make junior developers cringe in retrospect. You'll also have real terminal commands you can run right now, today.
What Is a Branch and Why You Need One Before a Pull Request
Before you can open a Pull Request, you need to understand branches — because a PR is really just a request to merge one branch into another.
Think of the main branch (often called main or master) as the official published version of your project — the printed textbook. A branch is your personal draft copy where you can experiment, write new features, or fix bugs without touching the published version. Once your work is ready and reviewed, you merge your branch back into main.
Here's the workflow in plain English: you copy the current state of main into a new branch, make your changes there, push that branch to GitHub, and then open a Pull Request to say 'hey team, I've made these changes on my branch — can someone review them before we include them in main?'
The branch name should describe the work you're doing. fix-login-button-alignment is a great branch name. my-branch or test123 will confuse everyone including future you. One branch per feature or bug fix is the golden rule — keep your PRs focused and small.
At production scale, branch naming conventions matter for automation. Teams use prefixes like feature/, fix/, hotfix/, and chore/ so CI/CD pipelines can auto-assign reviewers, auto-label PRs, and trigger different test suites based on branch type. A hotfix/ branch might skip integration tests and deploy directly to staging, while a feature/ branch runs the full suite.
- Branch names should describe the work: fix-login-button-alignment, not my-branch
- Use prefixes: feature/, fix/, hotfix/, chore/ — CI/CD pipelines can use these for automation
- One branch per feature or bug fix — keep PRs focused and small
- Never work directly on main — always branch off, even for one-line fixes
How to Open a Pull Request on GitHub Step by Step
Once your branch is pushed to GitHub, opening the Pull Request takes about two minutes — but writing a good PR description takes a bit more thought, and that effort pays off every single time.
After you push a branch, GitHub usually shows a yellow banner on the repository page saying 'Your recently pushed branch... Compare & pull request'. Click that button. If the banner is gone, click the 'Pull requests' tab, then 'New pull request', and select your branch from the dropdown.
The PR form has three key parts: the title, the description, and the reviewers. The title should be a one-line summary of what the change does. The description is where you explain the context — why does this change exist? What does it do? How can the reviewer test it? A template like 'What, Why, How to Test' makes this systematic.
Assigning reviewers is critical. GitHub lets you request specific people to review your code. In a team setting, at least one approval is typically required before merging. You can also add labels ('bug', 'enhancement'), link to a related issue, and mark a PR as a 'Draft' if it's not ready for review yet — that's a great way to share work-in-progress and get early feedback without it accidentally getting merged.
At senior levels, the PR description is also where you document your design decisions. If you chose Event Sourcing over CRUD, explain why. If you skipped a test type, explain why. Future developers (including future you) will read the PR description when investigating why a piece of code exists. A PR with no description is a black box six months later.
- Draft PRs cannot be merged accidentally — GitHub enforces this
- Use Draft PRs to get architecture feedback before writing all the code
- Convert to 'Ready for Review' when done — GitHub auto-notifies assigned reviewers
- Draft PRs are especially useful for large features where you want early buy-in on the approach
.github/PULL_REQUEST_TEMPLATE.md file ensure every PR follows the same structure. The second biggest bottleneck is missing reviewers — a PR with no assigned reviewers sits in limbo. Always assign at least one reviewer when you open the PR.How to Do a Code Review That Actually Helps
Being asked to review code is a responsibility, not a chore. A good review catches real bugs, shares knowledge across the team, and makes the codebase better. A bad review is either rubber-stamping everything or leaving vague, discouraging comments.
On GitHub, click the 'Files changed' tab in a PR to see a side-by-side diff — the red lines are what was removed, the green lines are what was added. You can click the '+' icon that appears when you hover over any line to leave an inline comment on that specific line.
There are three things to look for when reviewing: correctness (does it actually work?), clarity (can I understand what this code does in 30 seconds?), and consistency (does it follow the patterns the rest of the codebase uses?).
When you leave a comment, be specific and constructive. Instead of 'this is wrong', say 'this function will throw a null reference error if the user has no profile photo set — what if we add a fallback here?' GitHub lets you prefix comments with Nitpick: for minor style preferences so the author knows what's critical versus optional.
When you're done reviewing, click 'Review changes' and choose one of three options: Comment (general feedback, no decision), Approve (looks good to merge), or Request Changes (specific issues that must be fixed first). Only approve when you'd genuinely be comfortable with this code shipping.
At the senior level, code review is also about architectural consistency. Does this new service follow the same error handling pattern as the rest of the codebase? Does it use the same logging format? Does it introduce a new dependency that the team needs to evaluate? These are the questions that separate a senior review from a junior review.
- Rubber-stamping approvals lets bugs reach production — your approval is a safety gate
- If a PR is too large to review, say so explicitly: 'please split this into smaller PRs'
- Use 'Request Changes' when there are specific issues that must be fixed — do not just leave comments and approve
- Prefix minor style feedback with 'Nitpick:' so the author knows what is critical vs optional
Merging a Pull Request and Keeping History Clean
Once a PR has the required approvals and all CI checks pass (tests, linting, etc.), it's ready to merge. GitHub gives you three merge strategies and picking the right one matters for keeping your Git history readable.
'Create a merge commit' preserves every commit from your branch and adds a merge commit on top. This is great for long-running features where you want to see the full development history.
'Squash and merge' takes all your commits and compresses them into a single commit on main. This is perfect for small features or bug fixes where your branch had messy 'WIP' or 'fix typo' commits that aren't worth preserving. The result is a clean, linear history.
'Rebase and merge' replays your branch commits directly on top of main without a merge commit, keeping history linear. It's the cleanest option but can cause confusion if your branch had public commits others were building on.
After merging, always delete the feature branch — GitHub shows a 'Delete branch' button right after the merge. Stale branches pile up fast and confuse the whole team. Locally, run git branch -d to clean up too.
If the PR has merge conflicts — meaning main changed in ways that clash with your branch — you'll need to resolve them before merging. GitHub can handle simple conflicts in the browser, but for complex ones, resolve them locally.
At the production level, the merge strategy you choose affects how debuggable your history is. When you're chasing a regression at 3am using git bisect, a clean linear history (from squash or rebase) makes bisect dramatically faster. A branched history with 50 merge commits makes bisect nearly unusable because each merge commit is a diff of diffs.
- Squash and Merge collapses all branch commits into one clean commit on main
- Use for small features and bug fixes where individual commit history doesn't matter
- Do NOT squash when commit history is important for debugging (e.g., refactors where each step matters)
- After merging, always delete the feature branch — stale branches confuse the team
git bisect, a clean linear history (from squash or rebase) makes bisect fast and reliable — each commit is a meaningful unit of change. A branched history with dozens of merge commits makes bisect nearly unusable because merge commits are diffs-of-diffs, and bisect can land on a merge commit that represents 30 changes at once. The trade-off: squash loses the individual commit history, which can be valuable for understanding the development process of a complex feature. The rule: squash for small, self-contained changes. Merge commit for large, multi-developer features where the commit-by-commit history is part of the documentation. The second production issue is stale branches: after merging, if you don't delete the branch, it stays in the remote. After 6 months, the repo has 200 stale branches and nobody knows which ones are safe to delete. Configure GitHub to auto-delete branches after merge (Settings > General > 'Automatically delete head branches').Force Push to Main: 47 Commits Lost, Production Deploy Blocked for 6 Hours
git push --force and overwrote 47 commits from 5 other developers. The CI/CD pipeline deployed the old code. Production broke within minutes.--force as the solution.
4. The force push replaced the remote main branch history with their local history, which was 47 commits behind.
5. CI/CD automatically deployed the overwritten code to production.
6. The team had no backup strategy — the only recovery option was Git reflog on machines that still had the correct commits.git reflog on the senior developer's machine (they had the most recent pull).
3. Force-pushed the correct history back to main from the recovered reflog.
4. Configured branch protection rules: require PR reviews, require CI checks to pass, disable force push to main.
5. Added a pre-push hook that warns when pushing to main without a PR.
6. Result: production restored in 6 hours. No data was permanently lost because team members had local copies.- Never force push to a shared branch — main, develop, or any branch others depend on. Force push replaces history; it does not merge.
- Always work on a feature branch, never directly on main. One branch per feature is the golden rule.
- Branch protection rules are not optional — they are the safety net that prevents catastrophic mistakes. Require PR reviews and CI checks before merging.
- If you get a 'rejected' push error, the answer is never
--force. The answer isgit pull --rebaseto incorporate remote changes first.
.github/workflows/ for GitHub Actions).
5. If tests pass locally but fail in CI, the most common cause is timing-dependent tests (flaky tests) or missing test data setup.git checkout your-branch && git fetch origin && git merge origin/main.
3. Open the conflicted files — Git marks conflicts with <<<<<<<, =======, >>>>>>> markers.
4. Decide which version to keep (or combine both), remove the markers, save the file.
5. Run git add on the resolved files, then git commit to complete the merge.
6. Push the updated branch — the PR on GitHub will update automatically.Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
That's Git. Mark it forged?
6 min read · try the examples if you haven't