Skip to content

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

Merged
saddlepaddle merged 1 commit into
mainfrom
sentry/pull-sync-conflict-typed
Aug 13, 2026
Merged

saddlepaddle merged 1 commit into
mainfrom
sentry/pull-sync-conflict-typed

Conversation

@Kitenite

@Kitenite Kitenite commented Aug 7, 2026 •

Copy link
Copy Markdown
Member

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.

Written for commit 7437c9c. Summary will update on new commits.

Review in cubic

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Git operations now classify expected merge, rebase, push, and upstream failures. Pull and sync convert recognized failures into typed PRECONDITION_FAILED TRPC errors while preserving unexpected errors.

Changes

Git error handling

Layer / File(s) Summary
Merge conflict classification
apps/desktop/src/lib/trpc/routers/changes/git-utils.ts, apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
Added isMergeConflictError and tests for merge, rebase, and unrelated Git messages.
Pull and sync error integration
apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
Centralized expected-failure conversion for pull and push operations. Sync retains upstream recovery and refreshes branch status and caches.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Sync
  participant GitOperations
  participant Git
  participant TRPC
  Sync->>GitOperations: Execute pull and push
  GitOperations->>Git: Run Git operation
  Git-->>GitOperations: Return success or expected failure
  GitOperations->>TRPC: Convert expected failure to PRECONDITION_FAILED
  GitOperations->>Sync: Refresh branch status and clear caches
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses conventional commit format and clearly summarizes the classification of pull, sync, and push errors.
Description check ✅ Passed The description explains the problem, root cause, fix, affected paths, preserved behavior, and verification results; the template checklist is omitted.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentry/pull-sync-conflict-typed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/changes/git-operations.ts`:
- Around line 65-70: In the isUpstreamMissingError(message) branch, keep kind
set to "UPSTREAM_MISSING" but return the original message value unchanged
instead of replacing it with fixed text, so Git’s message remains available to
the UI.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ba5b42c1-a601-4c80-b16e-673a1d237287

📥 Commits

Reviewing files that changed from the base of the PR and between ddd3d48 and 7437c9c.

📒 Files selected for processing (3)
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.test.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
  • apps/desktop/src/lib/trpc/routers/changes/git-utils.ts

Comment on lines +65 to +70
if (isUpstreamMissingError(message)) {
return {
kind: "UPSTREAM_MISSING",
message:
"No upstream branch to pull from. The remote branch may have been deleted.",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the original upstream error message.

Line 69 replaces Git's message with a fixed message. A direct pull with a missing upstream will no longer expose Git's original message to the UI. Keep message unchanged and use kind: "UPSTREAM_MISSING" for classification.

Based on PR objectives: “Git’s original message is preserved for the UI.”

Proposed fix
 	if (isUpstreamMissingError(message)) {
 		return {
 			kind: "UPSTREAM_MISSING",
-			message:
-				"No upstream branch to pull from. The remote branch may have been deleted.",
+			message,
 		};
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isUpstreamMissingError(message)) {
return {
kind: "UPSTREAM_MISSING",
message:
"No upstream branch to pull from. The remote branch may have been deleted.",
};
if (isUpstreamMissingError(message)) {
return {
kind: "UPSTREAM_MISSING",
message,
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/lib/trpc/routers/changes/git-operations.ts` around lines 65
- 70, In the isUpstreamMissingError(message) branch, keep kind set to
"UPSTREAM_MISSING" but return the original message value unchanged instead of
replacing it with fixed text, so Git’s message remains available to the UI.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/lib/trpc/routers/changes/git-operations.ts">

<violation number="1" location="apps/desktop/src/lib/trpc/routers/changes/git-operations.ts:68">
P2: The UPSTREAM_MISSING classification in `classifyExpectedGitFailure` substitutes a hardcoded pull-specific message ("No upstream branch to pull from...") and drops the original git error text, and `rethrowClassifiedGitError` is now also applied to the sync push legs (`pushCurrentBranch` and the `pushWithResolvedUpstream` fallback). When a push fails with an upstream-missing signature (e.g. "couldn't find remote ref" / "cannot be resolved to branch" when the remote ref is gone), the user performing a push is shown a message that says "pull from" — the wrong operation — and the specific ref/branch context from the original error is discarded. Previously such push failures were rethrown with their raw git message. Consider using an operation-aware message for push legs (or preserving the original message/context, e.g. `cause: { kind, originalError/message }`) so the classification doesn't mislead users or hide which ref is missing.</violation>

<violation number="2" location="apps/desktop/src/lib/trpc/routers/changes/git-operations.ts:88">
P2: The new typed causes are dropped by the desktop changes error contract, so the renderer cannot obtain a typed conflict, rejection, or missing-upstream state from these errors. Extending `GitChangesErrorCause` and `GIT_CHANGES_CAUSE_KINDS` (plus the corresponding UI copy handling) would keep this classification usable.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant UI as Desktop UI
    participant TRPC as TRPC Router
    participant GitOp as git-operations.ts
    participant GitUtils as git-utils.ts
    participant SimpleGit as simple-git

    Note over UI,GitOp: Pull/Sync Flow with Error Classification

    UI->>TRPC: pull() or sync()
    TRPC->>GitOp: call handler

    alt Pull flow
        GitOp->>SimpleGit: git.pull(["--rebase"])
        alt Pull succeeds
            SimpleGit-->>GitOp: success
            GitOp-->>TRPC: { success: true }
            TRPC-->>UI: success response
        else Pull fails
            SimpleGit-->>GitOp: error message
            GitOp->>GitUtils: isMergeConflictError(message)
            alt Merge conflict detected
                GitUtils-->>GitOp: true
                GitOp->>GitOp: rethrowClassifiedGitError()
                GitOp-->>TRPC: TRPCError PRECONDITION_FAILED (MERGE_CONFLICT)
                TRPC-->>UI: typed error, not 500
            else Not merge conflict
                GitOp->>GitUtils: isNonFastForwardPushError(message)
                alt Push rejected
                    GitUtils-->>GitOp: true
                    GitOp->>GitOp: rethrowClassifiedGitError()
                    GitOp-->>TRPC: TRPCError PRECONDITION_FAILED (PUSH_REJECTED)
                    TRPC-->>UI: typed error, not 500
                else Not push rejected
                    GitOp->>GitUtils: isUpstreamMissingError(message)
                    alt Upstream missing
                        GitUtils-->>GitOp: true
                        GitOp->>GitOp: rethrowClassifiedGitError()
                        GitOp-->>TRPC: TRPCError PRECONDITION_FAILED (UPSTREAM_MISSING)
                        TRPC-->>UI: typed error, not 500
                    else Unknown git error
                        GitOp->>GitOp: throw error (unclassified)
                        GitOp-->>TRPC: 500 Internal Server Error
                        TRPC-->>UI: 500 error (reported to Sentry)
                    end
                end
            end
        end
    else Sync flow
        GitOp->>SimpleGit: pushCurrentBranch()
        alt Push succeeds
            SimpleGit-->>GitOp: success
            GitOp->>SimpleGit: fetchCurrentBranch()
            GitOp-->>TRPC: { success: true }
        else Push fails
            SimpleGit-->>GitOp: error message
            GitOp->>GitUtils: isUpstreamMissingError(message)
            alt Upstream missing
                GitUtils-->>GitOp: true
                GitOp->>SimpleGit: pushWithResolvedUpstream()
                alt Push succeeds
                    SimpleGit-->>GitOp: success
                    GitOp->>SimpleGit: fetchCurrentBranch()
                    GitOp-->>TRPC: { success: true }
                else Push fails
                    SimpleGit-->>GitOp: error message
                    GitOp->>GitOp: rethrowClassifiedGitError()
                    GitOp-->>TRPC: TRPCError PRECONDITION_FAILED
                    TRPC-->>UI: typed error, not 500
                end
            else Other error
                GitOp->>GitOp: rethrowClassifiedGitError()
                alt Classified as PUSH_REJECTED
                    GitOp-->>TRPC: TRPCError PRECONDITION_FAILED
                    TRPC-->>UI: typed error, not 500
                else Unclassified
                    GitOp-->>TRPC: 500 Internal Server Error
                    TRPC-->>UI: 500 error (reported to Sentry)
                end
            end
        end
    end
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

throw new TRPCError({
code: "PRECONDITION_FAILED",
message: expected.message,
cause: { kind: expected.kind },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new typed causes are dropped by the desktop changes error contract, so the renderer cannot obtain a typed conflict, rejection, or missing-upstream state from these errors. Extending GitChangesErrorCause and GIT_CHANGES_CAUSE_KINDS (plus the corresponding UI copy handling) would keep this classification usable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/changes/git-operations.ts, line 88:

<comment>The new typed causes are dropped by the desktop changes error contract, so the renderer cannot obtain a typed conflict, rejection, or missing-upstream state from these errors. Extending `GitChangesErrorCause` and `GIT_CHANGES_CAUSE_KINDS` (plus the corresponding UI copy handling) would keep this classification usable.</comment>

<file context>
@@ -47,6 +48,49 @@ async function getLocalBranchOrThrow({
+		throw new TRPCError({
+			code: "PRECONDITION_FAILED",
+			message: expected.message,
+			cause: { kind: expected.kind },
+		});
+	}
</file context>

if (isUpstreamMissingError(message)) {
return {
kind: "UPSTREAM_MISSING",
message:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The UPSTREAM_MISSING classification in classifyExpectedGitFailure substitutes a hardcoded pull-specific message ("No upstream branch to pull from...") and drops the original git error text, and rethrowClassifiedGitError is now also applied to the sync push legs (pushCurrentBranch and the pushWithResolvedUpstream fallback). When a push fails with an upstream-missing signature (e.g. "couldn't find remote ref" / "cannot be resolved to branch" when the remote ref is gone), the user performing a push is shown a message that says "pull from" — the wrong operation — and the specific ref/branch context from the original error is discarded. Previously such push failures were rethrown with their raw git message. Consider using an operation-aware message for push legs (or preserving the original message/context, e.g. cause: { kind, originalError/message }) so the classification doesn't mislead users or hide which ref is missing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/changes/git-operations.ts, line 68:

<comment>The UPSTREAM_MISSING classification in `classifyExpectedGitFailure` substitutes a hardcoded pull-specific message ("No upstream branch to pull from...") and drops the original git error text, and `rethrowClassifiedGitError` is now also applied to the sync push legs (`pushCurrentBranch` and the `pushWithResolvedUpstream` fallback). When a push fails with an upstream-missing signature (e.g. "couldn't find remote ref" / "cannot be resolved to branch" when the remote ref is gone), the user performing a push is shown a message that says "pull from" — the wrong operation — and the specific ref/branch context from the original error is discarded. Previously such push failures were rethrown with their raw git message. Consider using an operation-aware message for push legs (or preserving the original message/context, e.g. `cause: { kind, originalError/message }`) so the classification doesn't mislead users or hide which ref is missing.</comment>

<file context>
@@ -47,6 +48,49 @@ async function getLocalBranchOrThrow({
+	if (isUpstreamMissingError(message)) {
+		return {
+			kind: "UPSTREAM_MISSING",
+			message:
+				"No upstream branch to pull from. The remote branch may have been deleted.",
+		};
</file context>

@saddlepaddle
saddlepaddle merged commit 9b73ff0 into main Aug 13, 2026
12 checks passed
@saddlepaddle
saddlepaddle deleted the sentry/pull-sync-conflict-typed branch August 13, 2026 18:42
@github-actions

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch

Thank you for your contribution! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants