Git, CI/CD & GitOpsXXXII · Protected BranchesPushRestrictions
Direct push restrictions — disallow direct writes, require pull requests
What you'll learn
- Distinguish "no direct pushes" from "no force pushes" and explain why both are usually required
- Configure a branch protection rule so that merges land only via an approved pull request
- Identify the recovery path for a force-push that is rejected on a protected branch
- Apply direct-push restrictions to an infrastructure repository without breaking incident response
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Direct push restrictions are the most important rule in branch protection. They turn the default branch from a target that any contributor with write access can land on directly into a target that accepts changes only via a pull request. Without this rule, every other protection - approvals, status checks, CODEOWNERS - is decoration around a hole. A reviewer cannot review what was never a pull request; a status check cannot run against a commit that was never proposed; a CODEOWNER cannot gate what was already landed. The job of direct-push restrictions is to force every change through the funnel.
This lesson walks the configuration, the operational consequences, and the recovery path for the common failure modes.
The two distinct rules
A branch can be protected against direct pushes and against force pushes. They are distinct rules because they protect against different bad outcomes:
flowchart LR
A[Push attempt] --> B{Direct push allowed?}
B -->|no| C[Server rejects]
B -->|yes| D{Force push allowed?}
D -->|no| E[Server rejects on history rewrite]
D -->|yes| F[Server accepts]
- “Disallow direct pushes” (or “require a pull request before
merging”) means a commit cannot be added to the protected
branch unless it arrives as the result of merging a pull
request. A
git push origin mainfrom a developer laptop is refused. - “Disallow force pushes” means even with the right
credentials, the branch’s existing history cannot be rewritten.
A
git push --force origin mainis refused. On GitHub the default when a protection rule is added is to also disable force pushes; on GitLab, “Allowed to push” defaults to “developers and maintainers” but force pushes are off by default on protected branches.
The second rule matters because a force push can rewrite the history of the protected branch itself. A direct push adds a commit; a force push rewrites the commits the branch points to. A force-push disaster is recoverable only from a backup or from the reflog - and the reflog is local, not server-side. A commit-by-commit disaster is recoverable by reverting the bad commit. Direct-push restrictions defend the entry to the branch; force-push restrictions defend the integrity of the history that has already landed.
Configuring the rule
The exact UI varies by forge, but the configuration is the same shape: select a branch pattern, then enable “require a pull request before merging” (GitHub terminology; GitLab calls it “Allowed to push: No one” plus “Require merge request to be merged”; Bitbucket calls it “Branch permissions: Write access: Denied”). Then enable “do not allow force pushes” and “do not allow deletions”.
The configuration belongs in source control if the forge
supports it as code. GitHub offers repository rulesets and
ruleset-bypass as API; GitLab offers .gitlab/merge_request_ approvers.yml and protected-branch API; Bitbucket offers
branch-permission API. Codifying the configuration turns
“main is protected” from a UI assertion into a reviewed,
auditable artefact.
A typical feature-branch policy is the inverse: feature branches are unprotected, so engineers can commit and force-push freely during development, and the force-push restrictions apply only on the default branch and release branches. This pattern reflects the operational reality - history rewriting is fine on a branch that has not yet shipped, and forbidden on a branch that has.
Recovery paths
Three common failure modes, with their recoveries:
-
An engineer pushes directly to a protected branch. The server refuses. The push attempt is logged. The engineer must move the commit to a branch, push the branch, and open a pull request. The change is not lost; it is rerouted.
git branch feature/in-progress git push origin feature/in-progress -
An engineer tries to force-push a cleaned-up feature branch onto the default branch. The server refuses. The engineer must merge the cleaned-up branch as a pull request instead. Reverting force-push to a rewrite-via-PR is the intended recovery.
-
An incident requires an immediate push to the default branch. The configured bypass actors can land the change (covered in lesson 4). The bypass leaves an audit trail and a recorded actor; the protection does not break.
Working with feature branches
Direct-push restrictions on the default branch do not prevent productive work. The workflow is unchanged:
git checkout -b feature/rotate-db-credential
git push -u origin feature/rotate-db-credential
The feature branch is unprotected. Direct pushes and force- pushes are fine on it; commits can be added iteratively; the branch can be rebased during code review. The protection engages only when a pull request is opened against the default branch, at which point the funnel kicks in.
A subtle failure mode: an engineer force-pushes their feature branch during cleanup, the PR was already opened, and the PR now contains a force-pushed rewrite. The default branch is unaffected; the PR’s diff may have changed drastically; some forges show a “force-pushed” warning to reviewers. The PR is still reviewable; the production branch was never at risk. This is why restricting force-pushes to the default branch - not prohibiting them globally - is the production pattern.
Production discipline
Five rules for direct-push restrictions on an infrastructure repository:
- The default branch is forbidden for direct pushes. No credential, no role, no rationale changes this. Direct pushes to the default branch are an incident, not a flow.
- Force pushes are forbidden on the default branch and on every branch that has shipped a release. Feature branches and throwaway branches are exempt by intent.
- The bypass list is configured separately from the rules themselves. Bypass is part of the rule, not a violation of it; lesson 4 covers the configuration.
- Branch deletion is forbidden on the default branch. A deleted default branch is a recovered-from-backup scenario, not a routine one.
- The protection configuration is in source control. If the forge supports it, the rule is an artefact that reviews, audits, and version control.
Cross-course references
- Linux for Production Sysadmins - Part XXXIV (ConfigMgmt) covers the same funnel pattern at the OS layer: changes arrive through a controlled interface, not via direct file edits on production hosts.
- Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers the same pattern in Ansible: feature branches with the funnel landing on the default branch through a PR with status checks.
- Terraform for Production Sysadmins - Parts XXXII-XXXIII (RepoGuard / StateGuard) cover direct-push restrictions on Terraform state and module repositories.
Quiz
Knowledge check · 4 questions
Q1. An engineer runs `git push --force origin main` after cleaning up a feature branch. The server rejects the push. Why is this the intended behaviour rather than a bug?
Q2. Disallowing direct pushes to the default branch means that force-push protection is no longer needed.
Q3. An engineer tries to push directly to the default branch and is rejected. List the three fields the rejection message must contain for the engineer to act correctly.
Q4. Diagnose why a protection rule on the default branch appears to be working in the UI but did not block a direct push.
An infrastructure engineer pushes a Terraform change to the default branch at 11:02 local time. The push is accepted by the forge, no PR is opened, and the change lands. The team is reviewing the protection configuration and finds a rule with the pattern 'main' that should have blocked direct pushes. The engineer reports that the rule never engaged.
Passing score: 75%. Answers are checked in this browser.