How would you decide and enforce, in Git itself, which branches may be force-pushed?
answer
- classify refs by who depends on them
- drafts versus refs with dependants
- client flags are courtesy, not policy
- the receiving end is where rules bind
- plan the exception before you need it
basics
~20 sAllow rewriting on short-lived branches with a single owner, forbid it on anything others build on or that is deployed from. Enforce it at the receiving repository — receive.denyNonFastForwards and a pre-receive hook — because client-side flags are only courtesy.
solid answer
~40 sSplit branches by who depends on them. A feature branch with one author is a draft: rewriting it produces a clean, reviewable series and costs nobody, so allow it. Anything integrated into, deployed from, or built on by others — the trunk, release branches, anything referenced by a stored SHA — must never be rewritten, because the cost is every clone's history plus every external reference. Then enforce rather than merely document: on the receiving repository, `receive.denyNonFastForwards` rejects any non-fast-forward update and `receive.denyDeletes` rejects ref deletion, while a `pre-receive` or `update` hook gives per-branch granularity by inspecting the old and new values. That enforcement is server-side on purpose: `--force-with-lease` is a habit a client can skip, whereas a rejected receive is a rule. Hosted platforms implement their own protection features above these Git primitives.
code
ini · 3 lines[receive]
denyNonFastForwards = true
denyDeletes = truego deeper
You are not expected to set policy. Know the practical rule: rewrite only branches you alone publish, and never the trunk or a release branch.
Explain why a shared branch must not be rewritten in terms of what it costs each clone, and know that the receiving repository can refuse non-fast-forward updates outright.
Argue the classification by dependants, name the receive-side settings and the hook that gives per-branch granularity, and show why client-side flags cannot be the control.
Own the whole policy: which refs are protected and why, where enforcement lives so it cannot be bypassed, how history quality is preserved despite the restriction, and how the emergency exception is granted and restored.
## The decision, before the mechanism The question is not "is force-pushing bad" but "who pays when this ref moves non-linearly". Classify refs by dependants: - **Single-owner, short-lived branches** — one author, nobody branching from it. Rewriting is how you get a clean series: squash the fixups, fold in review feedback, rebase onto a moved base. The cost is zero because nobody else's history references it. Allow it. - **Integration branches** — the trunk, anything people branch from daily. A rewrite splits every clone in the organisation and each person must transplant their work by hand. Forbid it. - **Release branches and tags** — referenced by deployment records, changelogs, support tickets, submodule pointers, build artefacts. Rewriting invalidates references that live outside the repository entirely and cannot be fixed by a fetch. Forbid it, and treat moving a tag as the same class of event. - **Shared feature branches** — two or more authors pushing. Technically rewritable, practically a trap, because the second author is the one who discovers it. Default to forbidding it and require an explicit heads-up for exceptions. A useful test to state: *can everyone affected by this rewrite fix themselves with a command they already know, without being told?* If not, the branch is not rewritable. ## Why enforcement must be server-side Everything a developer types is advisory. `--force-with-lease` protects against stale information, not against someone typing `--force`; an alias helps until a script or a new hire bypasses it; a documented rule is invisible at 6 p.m. before a release. The only durable control is the receiving end refusing the update, because a receive that fails cannot be talked into succeeding. Git's own primitives on the receiving repository: - `receive.denyNonFastForwards` — when set, the repository rejects any push that would update a ref to a commit that is not a descendant of its current value. This is the blunt, repository-wide version of branch protection and it exactly matches "no rewriting here". - `receive.denyDeletes` — rejects ref deletion over the wire, closing the delete-then-recreate route around the first setting. - `receive.denyCurrentBranch` — governs pushes into the branch a non-bare repository has checked out, relevant when the receiving repository is not bare. - A **`pre-receive`** or **`update`** hook — the granular option. Each proposed ref update arrives with its old and new values, so the hook can allow rewrites under `refs/heads/feature/` while rejecting them on the trunk and on `refs/tags/`, or allow them for specific identities. Rejecting there is authoritative because the update never lands. Hosted platforms layer their own protection features above these primitives; the Git-level answer is the portable one you can reason about without depending on a particular host. ## The second-order effects to weigh - **Forbidding rewrites raises history noise.** A trunk that only ever fast-forwards accumulates whatever shape people push. Pair the policy with a convention for how branches are integrated, so "no rewriting the trunk" does not translate into "the trunk's history is unreadable". - **Blanket bans push work elsewhere.** If nobody can rewrite anything, people stop cleaning up their series and reviewers pay. Protect the refs with dependants; leave drafts alone. - **Emergencies need a defined path.** A repository-wide purge of a leaked secret *requires* rewriting protected branches. Decide in advance who can lift the setting, who is told, and how it is restored — an undocumented exception process becomes a permanently disabled control. - **Tags deserve their own rule.** They are the refs most likely to be referenced by systems outside Git, and the ones people most often assume are immutable. ## What good sounds like in an interview Name the classification rule, name the enforcement point, and admit the exception path: "rewritable if it has no dependants; enforced by the receiving repository refusing non-fast-forwards and deletions, with a `pre-receive` hook where I need per-branch granularity; plus a documented, announced procedure for the rare repository-wide rewrite."
- Why isn't requiring --force-with-lease sufficient as a policy?It is a client-side habit. It can be omitted, aliased away, or bypassed by a script, and it only checks that nobody moved the ref since you looked — it never asks whether this ref should be rewritten at all. Policy has to bind where the update is applied.
- Why also deny ref deletion if non-fast-forwards are already denied?Because delete-then-recreate achieves the same outcome by another route: the branch disappears and is pushed fresh at an unrelated commit, with no non-fast-forward update ever proposed. Denying deletes closes that path, and it protects tags people assume are immutable.
- How do you handle the case where a protected branch genuinely must be rewritten?Treat it as a planned change: decide in advance who can relax the setting, announce the window and the exact recovery commands, do the rewrite, restore the setting, and confirm restoration. An exception process invented under pressure tends to leave the control switched off.
saying these in an interview costs you the question
- Bans all force-pushing including private branches
- Relies on documentation instead of enforcement
- Forgets deletion as a route around the rule
- Treats tags as automatically immutable
- Has no defined path for a necessary rewrite