push --force: the lease does not protect commits that madt_status fetched #635

Open
opened 2026-09-24 19:58:06 +00:00 by hexajon · 1 comment
Owner

What

ADR 0029 classifies push --force as machine-proof. Its reason: "git refuses the push if the remote ref moved since the last fetch, so no unseen work is clobbered".

That reason does not hold when madtea itself fetches:

  • internal/service/git/push.go:145 runs --force-with-lease with no expected value. In that form, git compares the remote branch with the local remote-tracking ref origin/<branch>.
  • madt_status runs a real git fetch on every call (internal/service/git/status.go:18). That fetch moves origin/<branch>.
  • The git-push manual (git 2.47.3) warns about this: "supplying this option without an expected value, i.e. as --force-with-lease or --force-with-lease= interacts very badly with anything that implicitly runs git fetch on the remote to be pushed to in the background". The protection "is trivially defeated if some background process is updating refs in the background."

Failure sequence

  1. Another clone pushes commit X to branch B.
  2. madt_status fetches. origin/B now points at X. The local branch B does not contain X.
  3. madt_push force=true pushes B. The remote ref equals origin/B, so the lease passes.
  4. The push replaces B on the forge. X is no longer on the forge branch.

X still exists in the local object store and in the origin/B reflog, but nothing tells the user. I derived this sequence from the manual and the code. I have not reproduced it yet.

Possible fixes

  • Add --force-if-includes. The manual gives it as the mitigation: git refuses the forced update unless the remote-tracking tip was integrated locally.
  • Pass an explicit expected value to --force-with-lease.
  • Remove force from madtea and redirect to raw git, as ADR 0029 does for other operations.

Acceptance

  • A test reproduces the failure sequence against a local bare remote. It fails before the fix.
  • The chosen fix makes that test pass.
  • The chosen git option exists at the git floor in ADR 0038.
  • A decision record updates the ADR 0029 classification of push --force.
## What ADR 0029 classifies `push --force` as machine-proof. Its reason: "git refuses the push if the remote ref moved since the last fetch, so no unseen work is clobbered". That reason does not hold when madtea itself fetches: - `internal/service/git/push.go:145` runs `--force-with-lease` with no expected value. In that form, git compares the remote branch with the local remote-tracking ref `origin/<branch>`. - `madt_status` runs a real `git fetch` on every call (`internal/service/git/status.go:18`). That fetch moves `origin/<branch>`. - The git-push manual (git 2.47.3) warns about this: "supplying this option without an expected value, i.e. as --force-with-lease or --force-with-lease=<refname> interacts very badly with anything that implicitly runs git fetch on the remote to be pushed to in the background". The protection "is trivially defeated if some background process is updating refs in the background." ## Failure sequence 1. Another clone pushes commit X to branch B. 2. `madt_status` fetches. `origin/B` now points at X. The local branch B does not contain X. 3. `madt_push force=true` pushes B. The remote ref equals `origin/B`, so the lease passes. 4. The push replaces B on the forge. X is no longer on the forge branch. X still exists in the local object store and in the `origin/B` reflog, but nothing tells the user. I derived this sequence from the manual and the code. I have not reproduced it yet. ## Possible fixes - Add `--force-if-includes`. The manual gives it as the mitigation: git refuses the forced update unless the remote-tracking tip was integrated locally. - Pass an explicit expected value to `--force-with-lease`. - Remove force from madtea and redirect to raw git, as ADR 0029 does for other operations. ## Acceptance - A test reproduces the failure sequence against a local bare remote. It fails before the fix. - The chosen fix makes that test pass. - The chosen git option exists at the git floor in ADR 0038. - A decision record updates the ADR 0029 classification of `push --force`.
Author
Owner

Decision: push --force is omitted from both surfaces, madtea push and madt_push.

git needs force only when the new tip does not contain the old remote tip. So every force push removes at least one commit from the remote branch. A lease, with or without --force-if-includes, proves only that a copy exists in the local clone. That is not enough: the work must still exist on the remote. So push --force does not meet the machine-proof exception in ADR 0029, and it falls under omission.

A force-free path already exists for a base that moved under a pushed branch. madtea orchestrate reconcile merges the base into the PR head (internal/service/orchestrate/reconcile.go:299), then pushes through realGitOps.Push (internal/service/orchestrate/types.go:205), which runs git push -u origin <branch> with no force (internal/git/push.go:22).

The fix needs a new decision record that moves push --force from machine-proof to omit in the ADR 0029 table. #637 is a second force path, through a leading + in the branch value.

Decision: `push --force` is omitted from both surfaces, `madtea push` and `madt_push`. git needs force only when the new tip does not contain the old remote tip. So every force push removes at least one commit from the remote branch. A lease, with or without `--force-if-includes`, proves only that a copy exists in the local clone. That is not enough: the work must still exist on the remote. So `push --force` does not meet the machine-proof exception in ADR 0029, and it falls under omission. A force-free path already exists for a base that moved under a pushed branch. `madtea orchestrate reconcile` merges the base into the PR head (`internal/service/orchestrate/reconcile.go:299`), then pushes through `realGitOps.Push` (`internal/service/orchestrate/types.go:205`), which runs `git push -u origin <branch>` with no force (`internal/git/push.go:22`). The fix needs a new decision record that moves `push --force` from machine-proof to omit in the ADR 0029 table. #637 is a second force path, through a leading `+` in the branch value.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sixfold-space/madtea#635
No description provided.