can you look at https://github.com/entireio/git-sync/issues/46 and then chat

Claude Code·Opus 4.7[1m]·Soph·4mo ago·26hr 55min·7 Checkpoints·37 file changes·+427/-105·597.9K tokens
4mo ago·2m

I've read through issue #46. Quick summary of how I'm reading it:

  • The core ask is to use receive-pack's atomic capability so a single push is all-or-nothing.
  • The honest finding in the issue itself: atomic only helps within one push. The real non-atomicity comes from the multi-phase shapes — batched bootstrap (branches → tail) and replicate (relay push → separate delete PushCommands).
  • The two phase-shape issues each have a different answer: replicate can fold deletes into the same push, but batched bootstrap is multi-transaction by design.
  • And atomic + BestEffort cancel each other out — they need an explicit interaction rule.

A few things I'd want to chat about before touching code:

  1. Scope. Is the goal here just "negotiate and set atomic where pushes are already one-shot" (cheap, partial win), or also "restructure replicate to one push" (more invasive, actually closes the gap)? They're separable.
  2. BestEffort policy. Your sketch suggests "skip atomic when BestEffort is set." That seems right to me — silent skip with a debug log, error only if a user explicitly asks for both. Agree?
  3. Batched bootstrap. Are you okay leaving that as documented-multi-transaction, or is there appetite to make the tail push optional / fold it into the last batch when small?

What's your priority on this — quick win first, or do you want to design the replicate change too?

It

4mo ago

I think the idea is to support atomic where the change is small, for a intial flow with multiple pack files I think the assumption is that no target changes happen at the same time. If anxything maybe https://github.com/entireio/git-sync/issues/47 should help making sure after each packfile we fail if state has changed.

4mo ago·3m

Got it — that reframes #46 nicely. Reading the two together:

  • #47 is the safety net. Every PushCommand already carries Old: TargetHash captured at session start, so receive-pack rejects any update where the target moved underneath us. That's the lease.
  • #46 atomic is for "this single push is naturally all-or-nothing", not "make the whole sync run atomic."

So the scope you're describing is:

  1. Negotiate atomic and set it on the pushes that are already one transaction: one-shot bootstrap, sync relay, materialized fallback, sync+prune.
  2. For batched bootstrap: set atomic on each batch individually. We assume no concurrent target changes for the initial flow; if that assumption breaks, the next batch's lease check (#47) catches it because its Old won't match anymore.
  3. Don't restructure replicate's two-push shape. Atomic still applies to the relay push; the delete push remains its own transaction.
  4. BestEffort + atomic: skip atomic when BestEffort is set; error only if a user explicitly passes both.

Two things to nail down before I touch code:

  • Default on or opt-in? My read: on by default when the server advertises the capability — it's strictly safer and matches git push --mirror parity. Any reason to gate it behind a flag?
  • Per-batch atomic in batched bootstrap — confirming you want this. It adds nothing for the cross-batch story, but it does mean a single bad ref in batch N doesn't half-land that batch.

Sound right?

How would that work with something like github pull refs that should be warn skipped now if fail to create?

4mo ago·43.3s

Good question — that's the exact tension. Here's how I think it shakes out:

Pull refs today are handled one of two ways:

  1. --exclude-ref-prefix refs/pull/ — they never enter the push at all.
  2. --all-refs which bundles BestEffort — they hit the push, server returns ng, BestEffort downgrades each one to a warning.

Atomic conflicts only with (2). If BestEffort is on, even one ng from a pull ref under atomic fails the whole transaction → no refs land → the user gets nothing instead of "most things plus warnings." That defeats the BestEffort UX.

Resolution: atomic and BestEffort are mutually exclusive. Concretely:

  • BestEffort on → never set atomic, regardless of server capability. Per-ref ng continues to come back individually, warn-skip works.
  • BestEffort off → atomic safe to negotiate. If a pull ref (or anything else) gets ng, the user already wants the whole sync to fail; atomic just makes it fail cleanly with nothing partially landed.

So pull refs don't need any new handling — they ride on the existing BestEffort/exclude paths. The atomic feature only kicks in on the "strict" code path where rejection-fails-the-sync is already the contract.

One edge I want to flag: --all-refs on sync/plan implies BestEffort, which means atomic is effectively off for the most common GitHub-mirror configuration. That's arguably fine (correctness > parity), but it does mean the git push --mirror parity claim is narrower than it sounds — atomic mainly helps non---all-refs runs and replicate (which doesn't bundle BestEffort with --all-refs).

Want me to write that scope clarification into the issue, or leave it and just implement?

but from what we looked git push mirror did also only warn about github special refs and not fail? or does it?

4mo ago·24.5s

You're right to push on this — I was sloppy and so is the issue's framing.

git push --mirror by itself is not atomic. It's git push --mirror --atomic that's all-or-nothing. Default --mirror behavior:

  • Sends all ref updates in one push.
  • Each ref gets its own ok/ng from the server.
  • GitHub's refs/pull/* come back ng, the rest land.
  • git prints ! [remote rejected] refs/pull/1/head (deny updating a hidden ref) per failing ref.
  • Exit code is non-zero, but partial application on the target is exactly what happens.

So "git push --mirror warns about hidden refs and the rest go through" matches reality for the non-atomic form. That's not the parity #46 is going for — the issue is really after git push --mirror --atomic parity, which is opt-in on the git side too.

This actually argues for a different default than I suggested earlier:

  • Don't enable atomic by default even when the server advertises it. Default git-sync behavior should match default git push --mirror: per-ref ng flows back, BestEffort can downgrade.
  • Add a flag (--atomic?) that opts in, same as git. Then atomic + BestEffort being incompatible is the user's explicit choice, not something we hide.

That also fixes the awkward case I flagged at the end of my last message — --all-refs no longer silently disables atomic, because atomic was never on unless asked.

Does that match how you think about it? If so, the issue title and "Sketch" section want a small revision before I touch code.

but then we should update the issue since it claims git does it without the atomic flag, would then more consider rewriting it, that atomic is currently not supported and then we wait if people start askin?

4mo ago·17.4s

Agreed on both counts. Let me draft a rewrite — want to confirm before I edit the issue since it's public.

Proposed new body:


Status

git-sync doesn't negotiate or set the receive-pack atomic capability today. Logging this so we don't lose track of the option.

Background

The atomic capability (gitprotocol-pack.adoc) lets a client signal that all ref commands in one update-request apply or none do. It's what git push --atomic sets — not what git push --mirror sets. Default --mirror is per-ref: each command gets its own ok/ng, partial application is normal.

Where git-sync stands relative to git push:

  • Default git push --mirror → per-ref outcomes, partial apply. git-sync today matches this.
  • git push --mirror --atomic → all-or-nothing within one push. git-sync has no equivalent.

Why we haven't built it

  1. No demand yet. No one has asked for transactional pushes; the per-ref model has been fine.
  2. Conflicts with BestEffort. --all-refs on sync/plan bundles BestEffort, which downgrades per-ref ng to warnings — atomic would defeat that, so atomic would have to be off for the most common GitHub-mirror config anyway.
  3. Phase shape, not capability, is the real gap. Where git-sync is non-atomic across phases (batched bootstrap tail, replicate's separate delete push), the atomic capability alone doesn't help — those are multi-transaction by design.

If we revisit

  • Add an explicit --atomic flag (opt-in, matching git's UX).
  • Negotiate via gitproto.TargetFeaturesFromAdvRefs, set on packp.UpdateRequests in buildUpdateRequest.
  • Mutually exclusive with BestEffort: error if both are set explicitly.
  • Per-batch only in batched bootstrap; the cross-batch story stays the lease check from #47.

Two things to decide:

  • Keep the issue open as "logged, not planned" or close it with this as the rationale comment?
  • Strip the implementation sketch entirely, or leave the "If we revisit" section as a hint for future-us?

keep issue open, but remove implementation, flag that commenting on the issue if you need it, add a comment explain the current state

4mo ago·1h 59m

Done. Issue body rewritten to drop the implementation sketch and the misclaim about git push --mirror being atomic, with a "comment if you need this" call to action. Added a comment explaining the rewrite and the current-state rationale.

4mo ago·1m

Before I touch code, here's what I've found and how I'd scope it.

Current state, verified:

  • convert.PlansToPushCommands (internal/convert/convert.go:62-77) sets Old: p.TargetHash from the session-captured s.target.refMap. That's the lease.
  • cfg.Force already exists, but only for syncbootstrap and replicate reject --force explicitly (syncer/syncer.go:930, :563).
  • The server's ng reason text flows through onRejection(name, status) or wraps via report.Error() (gitproto/push.go:149-163). No git-sync-side interpretation today.

Issue #47 has three sub-items with very different sizes:

  1. Docs — note in docs/usage.md that --force is already lease-safe within a run (target tip captured at session start). Cheap, no code change.
  2. Reason hint on lease failure — wrap/augment the ng reason when it matches the "stale info / does not match expected old value" pattern. Low-risk; lives in the rejection-handling path.
  3. --force-blind flag — new config, requires --force, only meaningful for sync, plumbed into convert so non-FF updates send Old: ZeroHash. Most invasive; needs tests across relay/materialized/batched paths.

My recommendation: ship 1 and 2 now; defer 3 with the same "comment if you need it" pattern we just applied to #46. Reasoning: the lease behavior is already correct and safe; the actual user-facing gap is just discoverability. --force-blind is a recovery escape hatch nobody has asked for yet, and it adds non-trivial surface.

Want me to:

  • (a) Do 1+2 now, edit #47 to note 3 is deferred pending demand, or
  • (b) Do all three, or
  • (c) Just 1 (docs) and leave 2 and 3 as logged?

so the force that exists is force with lease?

4mo ago·14.0s

Yes — and that's exactly what #47 is calling out. The naming is misleading.

What --force actually does today:

  1. Planner allows non-fast-forward updates and tag retargets (without --force, those would be blocked with a "use --force" reason) — planner.go:325-355.
  2. Push command still sends Old: TargetHash from the session-captured refMap via convert.PlansToPushCommands — never zero. There's no code path that sends Old: ZeroHash for force updates.
  3. Receive-pack rejects the command if the target's current value doesn't match Old. That's the lease.

So --force semantically is git push --force-with-lease, where the lease window is "this sync run." The flag controls planner permissiveness (allow non-FF), not protocol-level clobber (bypass the old-hash check). It's never raw --force.

That's why #47 proposes --force-blind as the real git push --force equivalent — the actual zero-old, don't-care-what's-there override. It doesn't exist today.

Given this, the docs item in #47 is more important than I framed it — users reading --force will assume git's default semantics and be surprised when their concurrent-writer scenario "works correctly." It's not a bug, but the name oversells the danger.

Want me to also consider renaming or aliasing --force--force-with-lease (with --force kept as a compatibility alias)? Could be part of the docs pass, or a separate decision.

I'd go with renaming --force to --force-with-lease and just add a real normal --force

4mo ago·45.3s

Before I start typing — three decisions where I want a quick call:

  1. Migration approach. Project is pre-1.0 (0.4.3), so hard breaking changes are tolerable. Options:

    • (a) Hard switch: --force immediately means blind; old --force users get blind semantics silently. Risky if anyone scripted it.
    • (b) Deprecation alias: --force still works for one release but prints a warning; require --force-with-lease or new --force-blind (then flip in next release). Safer but adds a transitional name.
    • (c) Removal: --force errors out with "use --force-with-lease or --force-blind"; no silent behavior change. Forces users to read the change.

    My pick: (c) — pre-1.0, no installed base of scripts we know of, and silent semantics change is the worst outcome. CHANGELOG entry + clear error message.

  2. Both flags at once. --force --force-with-lease should error out (incompatible) — yes?

  3. Blind = zero all olds, or only forced refs? Real git push --force zeros Old for every ref in the push (not just the non-FF ones). Matching that is simpler and matches user mental model — agreed?

If you're good with (c), yes, yes — I'll start with task #1.

I do 1

4mo ago·10.9s

To make sure — by "1" do you mean migration option (a) hard switch (silent: --force immediately means blind, old scripts get new semantics with no warning), or did you mean "start with task 1"?

The difference matters: (a) is silent breaking; (c) is loud breaking (errors out, forces users to pick). Pre-1.0 either is defensible, but I want to be sure.

yes,

4mo ago·13.7s

Going with hard switch then. --force flips to blind semantics, --force-with-lease carries the old behavior. Starting on task #1.

or no, do c

4mo ago·14h 22m

All green. Summary of what landed:

  • Renamed --force--force-with-lease for the existing lease-protected behavior (sends captured target tip as expected-old; receive-pack rejects if the target moved during the run).
  • Added --force-blind as the real git push --force equivalent — zeros expected-old, overwrites regardless of current target value.
  • Old --force errors out with a migration hint pointing to the two replacements; flags are mutually exclusive (both at once errors).
  • API rename: SyncPolicy.ForceSyncPolicy.ForceWithLease + SyncPolicy.ForceBlind; same for internal syncer.Config. New Config.ForceAny() helper at internal call sites for the "either flag set" sense.
  • Plumbing: convert.PlansToPushCommands(plans, forceBlind bool) zeros Old for non-delete commands when blind. Threaded through incremental.Params.ForceBlind and materialized.Params.ForceBlind. Bootstrap and replicate pass false (both reject force flags entirely).
  • Lease-failure hint: annotateLeaseFailure in internal/gitproto/push.go wraps CommandStatusErr with "target ref X moved or differs from session start; rerun, or use --force-blind to overwrite" when the server's ng reason matches stale-info / fetch-first / non-fast-forward / does-not-match patterns.
  • Docs: new "Force Updates and the Per-Run Lease" section in docs/usage.md and Unreleased entry in CHANGELOG.md flagging the breaking change.
  • Tests: convert/PlansToPushCommands covers both modes; CLI tests for legacy --force rejection, mutual exclusion, and replicate-with-force-flag rejection; annotateLeaseFailure table test.

Full short test suite passes.

can you do meaninful commits, also mayxbe we update the changelog when we do a release?

4mo ago·3m

Two commits landed on main, CHANGELOG left untouched for release time:

  • d02d15a Split --force into --force-with-lease and --force-blind — 19 files, the rename + new blind path + CLI + docs.
  • 322747c Hint at --force-blind on receive-pack lease failures — push.go annotation + tests.

Simplify: Code Review and Cleanup

Review all changed files for reuse, quality, and efficiency. Fix any issues found.

Phase 1: Identify Changes

Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.

Phase 2: Launch Three Review Agents in Parallel

Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.

Agent 1: Code Reuse Review

For each change:

  1. Search for existing utilities and helpers that could replace newly written code. Look for similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
  2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.
  3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.

Agent 2: Code Quality Review

Review the same changes for hacky patterns:

  1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
  2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones
  3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction
  4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
  5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
  6. Unnecessary JSX nesting: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior
  7. Nested conditionals: ternary chains (a ? x : b ? y : ...), nested if/else, or nested switch 3+ levels deep — flatten with early returns, guard clauses, a lookup table, or an if/else-if cascade
  8. Unnecessary comments: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)

Agent 3: Efficiency Review

Review the same changes for efficiency:

  1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
  2. Missed concurrency: independent operations run sequentially when they could run in parallel
  3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths
  4. Recurring no-op updates: state/store updates inside polling loops, intervals, or event handlers that fire unconditionally — add a change-detection guard so downstream consumers aren't notified when nothing changed. Also: if a wrapper function takes an updater/reducer callback, verify it honors same-reference returns (or whatever the "no change" signal is) — otherwise callers' early-return no-ops are silently defeated
  5. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
  6. Memory: unbounded data structures, missing cleanup, event listener leaks
  7. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one

Phase 3: Fix Issues

Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.

When done, briefly summarize what was fixed (or confirm the code was already clean).

4mo ago·28m

Done. Simplify summary:

  • syncplan.go: replaced manual if ForceWithLease && ForceBlind check in RunE with cmd.MarkFlagsMutuallyExclusive("force-with-lease", "force-blind"). Shortened the legacy --force migration message.
  • push.go: pulled the four lease-failure substrings to a package-level leaseFailureMarkers var and looped the contains-checks.
  • convert.go: trimmed the 9-line doc on PlansToPushCommands down to two lines, since the lease/blind semantics already live on SyncPolicy.
  • main_test.go: updated mutex test assertion to match cobra's error format.

Skipped: Agent 1's pflag.MarkDeprecated suggestion (that's silent-warning, we want hard-error); Agent 2's enum refactor of SyncPolicy.Force (you already approved the two-bool API); Agent 2's "move ForceBlind to BranchPlan" (bigger refactor, judgment call).

Three commits on soph/force-clarification ahead of origin/main. Tests green.

can you give me a short pr descriptikon

4mo ago·15.1s

Clarify force semantics: --force-with-lease and --force-blind

Closes #47.

The old --force was always lease-protected — it sent the captured session-start target tip as the push command's expected-old, so 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.

Replace it with two explicit flags that mirror git's surface:

  • --force-with-lease — previous behavior (allow non-FF, captured tip as expected-old; server rejects on lease miss).
  • --force-blind — new path (zero expected-old, overwrite regardless of current target value; matches git push --force).

The two flags are mutually exclusive. Legacy --force errors out with a migration hint. bootstrap and replicate continue to reject force flags entirely. Pre-0.5, so hard switch — no silent semantics flip.

On the API side, SyncPolicy.Force splits into ForceWithLease + ForceBlind. convert.PlansToPushCommands takes a forceBlind bool; sync's incremental and materialized strategies plumb it through.

Also: when receive-pack returns a lease-failure ng (stale info / fetch first / non-fast-forward / does not match), the error is wrapped with a "target ref X moved or differs from session start; rerun, or use --force-blind to overwrite" hint.

Notes

  • Docs: new "Force Updates and the Per-Run Lease" section in docs/usage.md.
  • CHANGELOG left untouched — to be updated at release time.
  • Tests cover both convert modes, legacy --force rejection, mutual exclusion, replicate-with-force rejection, and the lease-failure annotation.

Findings:

  1. High: BestEffort currently defeats the new --force-with-lease guarantee. In internal/syncer/syncer.go, any best-effort run installs OnRejection for all per-ref ng statuses. Then internal/gitproto/push.go skips report.Error() entirely when that callback is present, so lease-mismatch statuses get downgraded to warnings instead of failing the run. That means sync --all-refs --force-with-lease can exit successfully after a concurrent target update, which contradicts the documented lease semantics.

  2. Medium: the public and unstable APIs no longer reject invalid force combinations at the request edge. client.go validates URLs/protocol/mappings, but never rejects ForceWithLease && ForceBlind or ModeReplicate plus force flags. The unstable client has the same issue: unstable/client.go goes straight into buildSyncConfig, and the actual rejection only happens later in internal/syncer/syncer.go. That’s a regression from the stated “fail before I/O / at the API edge” intent, and it also means custom auth providers may run before the request is rejected.

  3. Low: the updated docs now describe replicate incorrectly. docs/usage.md says “replicate is fast-forward-only by design,” but the implementation and tests still treat it as overwrite-oriented replication; for example internal/syncer/integration_test.go explicitly asserts divergent tag overwrite succeeds in replicate mode. That wording will mislead users choosing between sync and replicate.

I also tried to run go test ./internal/gitproto ./internal/syncer ./cmd/git-sync, but the sandbox blocks httptest from binding a local port, so I couldn’t use test execution to validate these paths here.

4mo ago·5m

All three findings addressed in 583fcf1a:

  1. BestEffort + lease (high)gitproto.IsLeaseFailure exported; syncer.leaseFailureError runs after finalizeCounts and escalates lease-class rejections to fatal regardless of BestEffort. sync --all-refs --force-with-lease now fails when the target moves, instead of silently downgrading to a warning.

  2. Request-edge validation (medium)SyncPolicy.Validate added; called from gitsync.SyncRequest.Validate, gitsync.PlanRequest.Validate, and unstable.Client.{Sync,Plan,Replicate}. The syncer-level check stays as defense-in-depth for direct syncer.Config callers (tests).

  3. Replicate doc wording (low) — rewritten. Replicate's contract is source-authoritative overwrite (divergent refs retargeted unconditionally), not fast-forward-only; force flags are unnecessary rather than gated.

Tests: TestLeaseFailureErrorEscalatesPastBestEffort covers (1); the existing TestValidateRequests got two new cases for (2). Full short test suite green.

583fcf1Address force-clarification review findings Three issues from review on top of the force-clarification branch: 1. BestEffort silently defeated --force-with-lease. The OnRejection callback installed under BestEffort stored every per-ref ng status for downgrade to a warning, including lease-mismatch statuses. `sync --all-refs --force-with-lease` could then exit successfully after a concurrent target update, contradicting the lease. Export gitproto.IsLeaseFailure and add a syncer.leaseFailureError pass after finalizeCounts. Lease-class rejections (stale info / fetch first / non-fast-forward / does not match) now escalate to a fatal error even with BestEffort on; non-lease rejections continue to downgrade to warnings. 2. Public and unstable APIs accepted invalid force combinations and only rejected them deep in newSession (after auth resolution). Add SyncPolicy.Validate, invoke it from gitsync.SyncRequest.Validate and gitsync.PlanRequest.Validate, and from unstable.Client.Sync, .Plan, .Replicate. The syncer.go check stays as defense-in-depth for callers reaching syncer.Config directly (tests). 3. docs/usage.md described replicate as "fast-forward-only by design"; in fact replicate's contract is source-authoritative overwrite — divergent branches and tags are retargeted unconditionally, which is why force flags are unnecessary rather than disallowed by a gate. Rewrite the sentence. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: eaa899c96ddf+116/-8

One remaining finding:

  1. Medium: lease-failure escalation is now applied unconditionally to every best-effort rejection, even when the run did not use --force-with-lease. internal/syncer/syncer.go treats any gitproto.IsLeaseFailure(status) rejection as fatal, and both sync and replicate call it unconditionally after finalizeCounts at internal/syncer/syncer.go and internal/syncer/syncer.go. But IsLeaseFailure currently matches broad strings like "non-fast-forward" and "fetch first" in internal/gitproto/push.go. That is correct for --force-with-lease, but wrong for --force-blind or non-force best-effort runs: those same server messages can reflect ordinary policy rejection rather than a stale lease. As written, sync --all-refs --force-blind can fail fatally on a rejection that the docs describe as warnable best-effort behavior, even though no lease semantics are in effect. This check should be gated on s.cfg.ForceWithLease or otherwise distinguish true lease-mode pushes from all other rejection paths.

The earlier issues I flagged were fixed: request-edge policy validation is back, and the replicate docs now match the implementation. I couldn’t re-run the full Go test suite here because the sandbox still blocks httptest from binding local ports.

4mo ago·2m

Fixed in 5349be76. leaseFailureError now early-returns unless cfg.ForceWithLease is set:

  • --force-with-lease + BestEffort: lease-class rejections still escalate to fatal (the original fix).
  • --force-blind: broad markers like "non-fast-forward" stay warnable under BestEffort, since blind sends Old: ZeroHash and those messages reflect server policy, not a lease miss.
  • No force flag + BestEffort: same — markers are ambiguous in this mode and BestEffort's warn-and-continue contract holds.

New test TestLeaseFailureErrorOnlyAppliesUnderForceWithLease covers both the non-force and --force-blind cases with "non-fast-forward" rejections. Full short test suite green.

can you also look at the PR

4mo ago·8m

Pushed. Status summary for PR #53:

Resolved by bf2f4024: lint failed on internal/gitproto/push_test.go:416 — errorlint flagged != err comparison on a wrapped error. Switched to errors.Is. Local lint clean.

State of the PR:

  • Mergeable, approved by @nodo on commit 1ce7b69 (the simplify pass).
  • Four commits have landed after the approval: 583fcf1a (review-finding fixes — BestEffort/lease escalation, request-edge validation, replicate docs), 5349be76 (gating lease escalation on --force-with-lease), bf2f4024 (this lint fix). Worth a nudge to the approver to re-look since those weren't part of the approved snapshot — the escalation logic in particular is new behavior.
  • Other checks were already green: tests, license-check, Cursor Bugbot.

Unrelated: remote reported a high-severity Dependabot alert on main (https://github.com/entireio/git-sync/security/dependabot/2) — not part of this branch, but worth a separate look.

  1. Low: git-sync-bench’s usage text still advertises the removed --force flag even though the parser now rejects it. The benchmark CLI correctly errors on --force at cmd/git- sync-bench/main.go:125, but the help text built by usageError still lists --force at cmd/git-sync-bench/main.go:462. That will mislead users toward a flag the command explicitly tells them not to use.
4mo ago·20.4s