# Merge pull request #53 from entireio/soph/force-clarification

`64a8d6d`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·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

```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+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

```go
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. 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.

`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-pack` path. `--protocol auto` tries v2 first and falls back to v1. `--protocol v2` requires 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 `--force` is 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-lease` or `--force-blind` is 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 `sync` finds blocked refs, it exits non-zero before pushing anything.
- `--stats` adds per-service request, byte, want, have, and command counters to the output.
