fix(desktop): classify pull/sync conflicts and rejected pushes as typed non-500s
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
isMergeConflictErrormatcher ingit-utils.tsalongside the existing matchers. - Shared
rethrowClassifiedGitErrorhelper ingit-operations.ts: passesTRPCErrorthrough, wraps expected git outcomes inTRPCErrorPRECONDITION_FAILEDwith 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 plainError)
- rebase/merge conflicts →
pullroutes its catch through the helper;syncroutes its pull leg and both push paths (pushCurrentBranchand the upstream-missingpushWithResolvedUpstreamfallback) 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
isMergeConflictErroringit-utils.tswith unit tests. - Introduced
rethrowClassifiedGitErrorto map expected failures toPRECONDITION_FAILEDwithcausekinds:MERGE_CONFLICT,PUSH_REJECTED,UPSTREAM_MISSING. - Routed error handling in
pull,syncpull leg,pushCurrentBranch, andpushWithResolvedUpstreamthrough the classifier; unexpected errors still surface as 500s. - Preserves git message text for the UI; keeps the existing upstream-missing message.
- Added