Merge pull request #53 from entireio/soph/force-clarification · Entire
Merge pull request #53 from entireio/soph/force-clarification
64a8d6d→main·Soph·2mo ago·23 files·+392 added/-70 removed
Clearer force params
Changes
23
Mclient.go+13/-6
Mclient_test.go+14
cmd
git-sync-bench
Mmain.go+13/-4
git-sync
Mmain_test.go+33/-2
- Msyncplan.go+11/-1
docs
Musage.md+38/-4
internal
convert
Mconvert.go+5/-1 - Mconvert_test.go+9/-1
gitproto
Mpush.go+40/-1 - Mpush_test.go+38
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+59/-11 - Msyncer_test.go+44
internalbridge
Mconfig.go+9/-7
Mtypes.go+28/-6
unstable
Mclient.go+11/-1
- Mclient_test.go+1/-1
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+13/-6
if err := validateOperationMode(r.Policy.Mode); err != nil {
return err
}
if err := r.Policy.Validate(); err != nil {
return err
}
if _, err := validation.NormalizeProtocolMode(string(r.Policy.Protocol)); err != nil {
return fmt.Errorf("normalize protocol: %w", err)
}
Mclient_test.go+14
if err := (SyncRequest{
Source: Endpoint{URL: "https://source.example/repo.git"},
Target: Endpoint{URL: "https://target.example/repo.git"},
Policy: SyncPolicy{ForceWithLease: true, ForceBlind: true},
}).Validate(); err == nil {
t.Fatalf("expected force-with-lease + force-blind to be rejected at the request edge")
}
if err := (SyncRequest{
Source: Endpoint{URL: "https://source.example/repo.git"},
Target: Endpoint{URL: "https://target.example/repo.git"},
Policy: SyncPolicy{Mode: ModeReplicate, ForceWithLease: true},
}).Validate(); err == nil {
t.Fatalf("expected replicate + force to be rejected at the request edge")
}
Mcmd/git-sync-bench/main.go+13/-4
func TestRun_Replicate_SubcommandRejectsForce(t *testing.T) {
err := run(context.Background(), []string{
modeReplicate,
"--force-with-lease",
"http://127.0.0.1:1/source.git",
"http://127.0.0.1:1/target.git",
})
if err == nil {
t.Fatal("expected replicate --force-with-lease to be rejected")
}
if !strings.Contains(err.Error(), "replicate does not support force flags") {
t.Fatalf("unexpected error: %v", err)
}
}
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.
bootstrap and replicate do not accept force flags. Bootstrap seeds an empty target where every ref is a create. Replicate's contract is source-authoritative overwrite: divergent branches and tags are retargeted against the source unconditionally, so there is no fast-forward gate for a force flag to opt out of.
The pre-0.5 --force flag is removed. Its semantics were lease-protected (it never sent a zero expected-old), so the closest direct replacement is --force-with-lease. --force-blind is new behavior with no pre-0.5 analog.
HEAD / Default Branch
git-sync surfaces the source's symref HEAD target — the source's default
- Source-side discovery and fetch can use protocol v2 when supported. Push stays on the existing v1
receive-packpath.--protocol autotries v2 first and falls back to v1.--protocol v2requires the source to negotiate v2. - Source fetch advertises current target tip hashes as
have, so reruns download less when source and target already share history. - Branches are updated only when the target tip is an ancestor of the source tip, unless
--forceis set. Tags are immutable by default. Retargeting an existing tag requires--force. With--prune, managed target refs that are absent on source are deleted. - Branches are updated only when the target tip is an ancestor of the source tip, unless
--force-with-leaseor--force-blindis set. Tags are immutable by default; retargeting an existing tag requires one of the force flags. With--prune, managed target refs that are absent on source are deleted. - If
syncfinds blocked refs, it exits non-zero before pushing anything. --statsadds per-service request, byte, want, have, and command counters to the output.