# Split --force into --force-with-lease and --force-blind

`d02d15a`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

Soph·2mo ago·19 files·+197 added/-68 removed

The previous --force was always lease-protected: PlansToPushCommands
sent the captured session-start target hash as the push command's
expected-old, and receive-pack rejected updates where the target had
moved during the run. The name oversold the danger — it never matched
git push --force's raw clobber semantics.

Replace with two explicit flags matching git push's surface:

\- --force-with-lease — previous behavior (allow non-FF, send captured
target tip as expected-old; server rejects on lease miss).
\- --force-blind — new path; zero the expected-old for non-delete
commands so receive-pack overwrites regardless of current target
value. Matches git push --force.

The two are mutually exclusive. Legacy --force errors out with a
migration hint pointing at both replacements (pre-0.5, no installed
script base to preserve). bootstrap and replicate continue to reject
force flags entirely.

SyncPolicy.Force splits into ForceWithLease + ForceBlind on the public
API; syncer.Config grows a ForceAny() helper for the internal "allow
non-FF" sense (planner permissiveness). convert.PlansToPushCommands
takes a forceBlind bool; incremental/materialized strategies plumb it
from cfg, others pass false since bootstrap/replicate reject force.

Closes the rename portion of #47.

## Sessions

a733497c4d7fView transcript

[?\
can you rebase soph/progress-indicators onto soph/smart-subdivisionClaude Code·1 step](/content/gh/entireio/git-sync/session/3ee1ca7a-a436-44c1-906a-a912c6d33f96#timeline-a733497c4d7f/index.html)

## Changes

19

- Mclient.go+7/-6

- cmd

- git-sync-bench

- Mmain.go+12/-3

- git-sync

- Mmain_test.go+33/-2

- Msyncplan.go+13/-1

- docs

- Musage.md+36/-4

- internal

- convert

- Mconvert.go+12/-1

- Mconvert_test.go+9/-1

- planner

- Mplanner.go+2/-2

- strategy

- bootstrap

- Mbootstrap.go+4/-4

- incremental

- Mincremental.go+2/-1

- materialized

- Mmaterialized.go+2/-1

- replicate

- Mreplicate.go+2/-2

- syncer

- Mgit_http_backend_test.go+2/-2

- Mintegration_test.go+12/-12

- Msyncer.go+22/-11

- internalbridge

- Mconfig.go+9/-7

- Mtypes.go+15/-6

- unstable

- Mclient.go+2/-1

- Mclient_test.go+1/-1

```
227 unmodified lines

```go
func bridgePolicy(policy SyncPolicy) internalbridge.SyncPolicy {
	return internalbridge.SyncPolicy{
		Mode:        internalbridge.OperationMode(policy.Mode),
		IncludeTags: policy.IncludeTags,
		Force:       policy.Force,
		Prune:       policy.Prune,
		BestEffort:  policy.BestEffort,
		Protocol:    internalbridge.ProtocolMode(policy.Protocol),
		Mode:           internalbridge.OperationMode(policy.Mode),
		IncludeTags:    policy.IncludeTags,
		ForceWithLease: policy.ForceWithLease,
		ForceBlind:     policy.ForceBlind,
		Prune:          policy.Prune,
		BestEffort:     policy.BestEffort,
		Protocol:       internalbridge.ProtocolMode(policy.Protocol),
	}
}
```

Mclient.go+7/-6

```
105 unmodified lines

...
```

## Force Updates and the Per-Run Lease

Non-fast-forward updates and tag retargets are opt-in. git-sync exposes two
flags that mirror `git push`'s force semantics:

- **`--force-with-lease`** — allow non-fast-forward updates, but include the
target tip captured at session start as the push command's expected-old
value. If another writer moves the target between session start and the
push, receive-pack rejects the update with a "remote ref does not match
expected old value" error and the sync fails without clobbering the racing
write. The lease window is one sync run; git-sync keeps no state between
runs.
- **`--force-blind`** — allow non-fast-forward updates and zero out the
expected-old, telling receive-pack to overwrite regardless of current
target value. Matches `git push --force` semantics. Use this when the
target was edited out-of-band and you intend to overwrite whatever is
there.

The two flags are mutually exclusive. Without either, divergent or
non-ancestor refs are reported as blocked and the sync exits non-zero
before any push, so the lease check is a second line of defense against
races for users who opt into non-fast-forward updates.
