Configuring Branch Protection Rules on GitHub and GitLab: Protecting the Main Branch from Bypassed Code Reviews

Git tutorial - IT technology blog
Git tutorial - IT technology blog

2 AM. A Slack notification jolted me out of bed: “Production down. Someone just pushed directly to main.”

Not the first time. Our team had 8 people, nobody was being malicious — but without guardrails, accidents like this happened constantly. A single git push --force origin main in a moment of haste was enough to overwrite someone else’s commits, or deploy untested code straight to production. That night cost us 3 hours to roll back.

Branch Protection Rules are what saved our team from that cycle. But GitHub and GitLab implement them quite differently — getting the configuration wrong can hurt more than having no configuration at all.

Comparing the Approaches: GitHub vs GitLab

Both platforms have branch protection, but their design philosophies are completely different. GitHub goes granular — you tick each action you want to block. GitLab goes role-based — you define who can do what.

GitHub Branch Protection Rules

Go to Settings → Branches → Add branch protection rule. The pattern syntax is fairly flexible: main, release/*, or v[0-9]* all work.

The most important options to know:

  • Require a pull request before merging — forces PR creation; direct pushes are blocked immediately
  • Require approvals — minimum number of reviewers, typically 1 or 2
  • Dismiss stale pull request approvals when new commits are pushed — approvals reset when new commits arrive. This is more important than most people realize: without it, a reviewer approves, the developer adds 5 new files, then merges immediately with nobody reviewing the new code
  • Require review from Code Owners — only people listed in the CODEOWNERS file can approve
  • Require status checks to pass before mergingCI must be green before merging is allowed
  • Restrict who can push to matching branches — whitelist of people/teams allowed to push directly
  • Allow force pushes / Allow deletions — disabled by default; don’t enable unless you have a specific reason

GitLab Protected Branches

GitLab puts this under Settings → Repository → Protected branches. Rather than blocking individual actions like GitHub, GitLab uses roles — you define who can push, who can merge, and who can force push:

  • Allowed to merge: No one / Developers + Maintainers / Maintainers
  • Allowed to push and merge: No one / Developers + Maintainers / Maintainers
  • Allowed to force push: toggle on/off
  • Code owner approval required: enable when a CODEOWNERS file is present

One commonly overlooked point: Merge request approvals on GitLab lives under Settings → Merge requests, completely separate from Protected branches. This is the most common source of confusion — people new to the setup often only configure protected branches and assume they’re done.

Real-World Pros and Cons

GitHub: Clear, But Full of Hidden Traps

Everything is on one screen, easy to see. But the most common trap: enabling Require approvals: 1 without enabling Dismiss stale approvals. A reviewer approves, the developer adds 5 new files, merges immediately — and nobody reviewed what was just added.

Another downside: GitHub Free for private repos limits certain options. Specifically, Require review from Code Owners requires GitHub Pro or a Team plan.

GitLab: More Logical for Larger Teams

Separating push and merge permissions is GitLab’s strength — developers can merge but not push directly, or vice versa, depending on your workflow. With GitLab Free self-hosted, nearly all features are available. The downside: approval rules are scattered across 2-3 different places, making it easy to miss things during initial setup.

Choosing the Right Configuration

After trying both on real teams, here are my recommendations by team size:

  • Small team, 2-5 people, using GitHub: Require PR + 1 approval + dismiss stale + CI check. Simple, sufficient, minimal overhead.
  • Mid-size team, 6-15 people: Add CODEOWNERS + require Code Owner review for critical folders (src/, infra/). Either GitHub or GitLab works fine.
  • Self-hosted GitLab, large team: Role-based access shines here. Set Maintainer-only merges, prevent Developers from pushing directly.

Our current 8-person team uses GitHub with 1 required approval + dismiss stale + CI check — that’s enough. Merge conflicts dropped significantly because everyone is forced to create PRs and review each other’s work, instead of everyone pushing whenever they feel like it.

Step-by-Step Setup

GitHub: Configuring Branch Protection for Main

Go to Settings → Branches → Add branch protection rule and enter:

Branch name pattern: main

Check the following options:

✅ Require a pull request before merging
   ✅ Require approvals: 1
   ✅ Dismiss stale pull request approvals when new commits are pushed
   ✅ Require review from Code Owners (if CODEOWNERS exists)
✅ Require status checks to pass before merging
   ✅ Require branches to be up to date before merging
✅ Do not allow bypassing the above settings
❌ Allow force pushes  (leave disabled)
❌ Allow deletions     (leave disabled)

To automatically assign reviewers, create a .github/CODEOWNERS file:

# File: .github/CODEOWNERS

# Default: everything requires approval from @lead-devs
*  @your-org/lead-devs

# Infrastructure: only @ops-team can approve
/infra/  @your-org/ops-team
/docker/ @your-org/ops-team

# Frontend: frontend team reviews their own code
/src/components/ @your-org/frontend-team

Test it by pushing directly to main:

git checkout main
echo "test" >> README.md
git add . && git commit -m "test direct push"
git push origin main
# Expected output:
# remote: error: GH006: Protected branch update failed for refs/heads/main.
# remote: error: At least 1 approving review is required by reviewers with write access.

Getting blocked is the correct behavior. Setup successful.

GitLab: Protected Branch + Approval Rules

Step 1: Go to Settings → Repository → Protected branches:

Branch: main
Allowed to merge: Maintainers
Allowed to push and merge: No one
Allowed to force push: OFF
Code owner approval required: ON (if CODEOWNERS exists)

Step 2: Go to Settings → Merge requests → Merge request approvals — this section is separate, don’t skip it:

✅ Prevent approval by author
✅ Prevent approvals by users who add commits
✅ Remove all approvals when commits are added

Approval rules:
  Rule name: "Require 1 approval"
  Approvals required: 1
  Eligible approvers: [select a group or specific users]

Step 3: Create an MR and try to merge without approval — GitLab will block it with the message “Approval rules not satisfied”. If you see this, the setup is correct.

Handling Emergencies (Hotfix)

There will be times when you need to bypass — production is on fire and you can’t wait for a review. Don’t disable the protection rules. There’s a better way:

GitHub: Go to Settings → Branches, temporarily enable Allow force pushes for admins, push the fix, then disable it again within 10 minutes.

GitLab: Change Allowed to push from “No one” to “Maintainers”, push the hotfix, then change it back.

Important: log every bypass. We use a Slack channel called #git-bypass-log — every bypass gets a message clearly stating: who did it, why, when, and whether it’s been reverted. After 3 months, that channel had only 4 messages. That’s a good sign.

Commonly Overlooked Settings

These four settings are ones our team has each missed at least once:

  • “Require branches to be up to date” (GitHub) — forces a rebase before merging, preventing the scenario where a PR was created 3 days ago, main has 20 new commits, and merging creates silent conflicts
  • “Do not allow bypassing the above settings” (GitHub) — even admins must go through a PR. Sounds overkill, but this is why 95% of incidents happen: an admin is in a rush and bypasses everything
  • “Prevent approvals by users who add commits” (GitLab) — blocks the loophole where someone adds a tiny commit then self-approves to unlock the merge
  • Require status checks (GitHub) — if you don’t have CI yet, don’t enable this. Enabling it with no passing checks will permanently block merges with no clear explanation

Branch protection isn’t a silver bullet. It prevents accidents, but it can’t stop bad code if reviewers aren’t actually reading. For our team, though, simply forcing everyone to create PRs and wait for 1 approval was enough — the habit of communicating through comments gradually took hold, reviews became more meaningful, and that 2 AM incident has never happened again since.

Share: