fix(desktop): classify pull/sync conflicts and rejected pushes as typed non-500s

Merged
KI
3mo ago
+107-22

Problem

Three Sentry issues (1 event each) from the desktop changes router: DESKTOP-M7 (changes.pull, 1.16.0), DESKTOP-MG (changes.sync pull leg, 1.15.0), DESKTOP-JK (changes.sync push leg, 1.15.0). All are expected user-repo outcomes — a rebase merge conflict during git pull --rebase, and a push rejected by the remote (non-fast-forward / repository rules) — escaping as INTERNAL_SERVER_ERROR and getting reported. Because the raw simple-git error message is git's full stderr transcript, the issue titles also embed remote branch listings and third-party repository URLs.

Root cause

changes.push got a classification catch in #6190, but changes.pull's catch only handled the upstream-missing case (and rethrew it as a plain Error, itself a reportable 500), rethrowing everything else raw. changes.sync called pushCurrentBranch / pushWithResolvedUpstream entirely outside any try, and its pull leg rethrew non-upstream-missing errors raw.

Fix

Classification only — no behavior change, no Sentry-side filtering (per the #6164 contract, classify at throw sites):

  • New isMergeConflictError matcher in git-utils.ts alongside the existing matchers.
  • Shared rethrowClassifiedGitError helper in git-operations.ts: passes TRPCError through, wraps expected git outcomes in TRPCError PRECONDITION_FAILED with a plain-object cause and git's original message text preserved for the UI, and rethrows anything unexpected raw so genuine bugs still report as 500s.
    • rebase/merge conflicts → cause: { kind: "MERGE_CONFLICT" }
    • rejected / non-fast-forward / rule-declined pushes (reuses isNonFastForwardPushError) → cause: { kind: "PUSH_REJECTED" }
    • missing upstream → cause: { kind: "UPSTREAM_MISSING" }, keeping pull's existing "No upstream branch to pull from…" message (previously a plain Error)
  • pull routes its catch through the helper; sync routes its pull leg and both push paths (pushCurrentBranch and the upstream-missing pushWithResolvedUpstream fallback) through it.
  • Sync's upstream-missing → push-with-set-upstream fallback behavior is unchanged.

Matchers are message-based (worker boundary strips prototypes, so no instanceof on git errors). Added unit tests for isMergeConflictError.

Verification

bunx biome check on the three changed files:

Checked 3 files in 11ms. No fixes applied.

cd apps/desktop && bun run typecheck:

$ tsc --noEmit
exit=0

bun test apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts:

 20 pass
 0 fail
 35 expect() calls
Ran 20 tests across 1 file. [67.00ms]

Refs DESKTOP-M7
Refs DESKTOP-MG
Refs DESKTOP-JK

https://claude.ai/code/session_0d966f97-3e8a-4239-ae1b-b1c2e539b359


Summary by cubic

Classifies expected git outcomes in the desktop changes router (merge conflicts, rejected pushes, missing upstream) as typed non-500 TRPCErrors to stop false 500s and avoid leaking git stderr details. No functional behavior changes to pull/push/sync behavior.

  • Bug Fixes
    • Added isMergeConflictError in git-utils.ts with unit tests.
    • Introduced rethrowClassifiedGitError to map expected failures to PRECONDITION_FAILED with cause kinds: MERGE_CONFLICT, PUSH_REJECTED, UPSTREAM_MISSING.
    • Routed error handling in pull, sync pull leg, pushCurrentBranch, and pushWithResolvedUpstream through the classifier; unexpected errors still surface as 500s.
    • Preserves git message text for the UI; keeps the existing upstream-missing message.