GitProtected Branches

Protected Branches

A protected branch is a branch with extra rules attached on the server side that prevent certain operations — like force-pushing, deleting, or merging without a review. Protections live on the hosting platform (GitHub, GitLab, Bitbucket, Gerrit), not in Git itself, because Git on your laptop has no concept of "the team's policy." They turn an honour-system workflow into an enforced one.

What protection rules actually do
  • Block force-pushgit push --force to a protected branch is rejected by the server, so nobody can rewrite shared history by accident.

  • Block deletiongit push origin --delete main fails; the branch cannot vanish.

  • Require pull requests — direct pushes are rejected; all changes must go through a PR/MR.

  • Require N reviewers — the PR cannot merge until N approvals are recorded.

  • Require status checks — CI must be green; if tests fail, merge is blocked.

  • Require signed commits — every commit on the branch must be GPG/SSH-signed and verified.

  • Require linear history — merge commits (or non-fast-forward merges) are rejected; you must rebase or squash.

  • Require conversation resolution — every review comment thread must be marked resolved before merge.

A protected branch in your push output

What rejection looks like

Bash
git push --force origin main
# To github.com:acme/payments.git
#  ! [remote rejected] main -> main (protected branch hook declined)
# error: failed to push some refs to 'github.com:acme/payments.git'
Enabling protection on GitHub

On GitHub, open the repo and go to Settings → Branches → Branch protection rules → Add rule. Set a branch name pattern (e.g. main or release/*), then check the boxes you want. Save. From that moment, anyone pushing to that branch is held to the rules.

Typical GitHub protection setup for main

Text
Branch name pattern: main

[x] Require a pull request before merging
    [x] Require approvals: 1
    [x] Dismiss stale pull request approvals when new commits are pushed
    [x] Require review from Code Owners

[x] Require status checks to pass before merging
    [x] Require branches to be up to date before merging
    Required checks:  ci/build, ci/test, lint

[x] Require conversation resolution before merging
[x] Require signed commits
[x] Require linear history
[x] Do not allow bypassing the above settings
[x] Restrict who can push to matching branches
[ ] Allow force pushes
[ ] Allow deletions
The same idea on GitLab and Bitbucket
  • GitLab — Settings → Repository → Protected branches. Pick the branch, choose "Allowed to push" and "Allowed to merge" roles. Combine with Merge Request approval rules for review requirements.

  • Bitbucket Cloud — Repository settings → Branch restrictions. Add restrictions for "Prevent all changes," "Prevent deletion," or "Require merge checks."

  • Bitbucket Data Center — Branch permissions live under Project or Repository settings, with finer-grained per-user rules.

  • Azure DevOps — Repos → Branches → Branch policies. Same vocabulary: reviewers, builds, linked work items.

A mental model

Where the policy lives

Text
+--------------------+        +----------------------+
| your laptop (Git)  | -----> | hosting platform     |
| no rules, no judge |  push  |  pre-receive hook    |
|                    |        |  checks protection   |
+--------------------+        |  ACCEPT or REJECT    |
                              +----------------------+
Bypass rules and emergency overrides

Sometimes you have a genuine emergency — production is down and the fix has to land now, review be damned. Most platforms let admins bypass protection, but the safer pattern is to leave bypass off and instead temporarily relax the rule, then turn it back on. GitHub has an explicit "Allow specified actors to bypass required pull requests" option; use it sparingly.

Emergency push (admin) — leaves an audit trail

Bash
# Only works if your role has bypass permission
git push origin hotfix:main
# Hosting platform logs: bypass used by alice@acme.com
Recommended rules by branch type

Branch

Reviews

CI required

Linear history

Force push

main / master

1–2 + Code Owners

Yes

Yes

No

release/*

2 + release lead

Yes (full suite)

Yes

No

develop (Gitflow)

1

Yes

Optional

No

feature/*

0 (PR-only review)

No

No

Yes (your own)

gh-pages

0

No

No

Yes

Self-hosted equivalents
  • Gerrit — every change is reviewed via Change-Ids; the server refuses pushes that don't come through Gerrit's review workflow. Protection is the default, not the exception.

  • Plain Git server — write a server-side pre-receive or update hook that inspects the ref being pushed and rejects writes to protected refs unless conditions are met.

  • Gitea / Forgejo / Codeberg — same UI concept as GitHub; Settings → Branches → Protected Branches.

Minimal pre-receive hook to protect main

Bash
#!/bin/sh
# hooks/pre-receive on a bare repo
while read oldrev newrev refname; do
  if [ "$refname" = "refs/heads/main" ]; then
    if [ "$oldrev" != "0000000000000000000000000000000000000000" ]; then
      # Reject non-fast-forward
      if ! git merge-base --is-ancestor "$oldrev" "$newrev"; then
        echo "Force pushes to main are not allowed" >&2
        exit 1
      fi
    fi
  fi
done
Common errors you'll see
  • protected branch hook declined — the branch is protected and your push violates a rule.

  • GH006: Required status check is expected — CI hasn't run or hasn't passed yet. Wait, or fix the failure.

  • At least N approving review is required — your PR doesn't have enough approvals.

  • Changes must be made through a pull request — you tried to push directly to a branch that requires PRs.

  • Required signature missing — signed commits are required and your commit isn't signed. See the SSH/GPG signing tutorial.

Real-world workflow with protections

Day-to-day flow when main is protected

Bash
# 1. Branch from main
git switch -c feat/checkout-button main

# 2. Work, commit, push your branch
git add .
git commit -m "Add checkout button"
git push -u origin feat/checkout-button

# 3. Open a PR on the platform UI (or with gh)
gh pr create --fill --base main

# 4. CI runs. Reviewer leaves comments. You push fixes:
git commit -am "Address review"
git push

# 5. Once approved and green, merge via the UI (the only path
#    that is allowed). Local main updates on next pull.
git switch main
git pull
Warning
Don't pile on every protection at once on day one. Start with "require PR" and "block force-push". Add more rules as the team grows. Heavy protection on a tiny repo creates ceremony without payoff and trains people to look for workarounds.
Protections are server-side, always
Anyone with write access to your laptop can edit any local branch — Git itself has no notion of "protected." Protection is enforced when commits hit the remote. That's why force-with-lease can't save you on a misconfigured server: only the server can refuse to accept the push.
Tip
Pair protection with **Code Owners** (a `CODEOWNERS` file at the repo root). Reviewers get auto-requested by path, and you can require their approval for changes in their area without forcing everyone to review everything.