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

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:

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