Split --force into --force-with-lease and --force-blind · Entire
Split --force into --force-with-lease and --force-blind
d02d15a→main·
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. Matchesgit push --forcesemantics. 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.