From dcdb9ac92320d62028fd2d12be26167406666920 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Feb 2026 14:28:03 -0800 Subject: [PATCH] Skip local-branch PR checks and dedupe sidebar git status - server: only query open PRs when the current branch has an upstream; still fall back to merged PR lookup - tests: cover status behavior for branches without upstream tracking - web: fetch git status once per cwd, validate query data with schema parsing, and map merged PR badges by thread branch match --- apps/server/src/gitManager.test.ts | 22 +++++++++++++++++ apps/server/src/gitManager.ts | 11 ++++++--- apps/web/src/components/Sidebar.tsx | 38 ++++++++++++++++++++++------- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/apps/server/src/gitManager.test.ts b/apps/server/src/gitManager.test.ts index 961f74fb9de082485d83fc712b57785f8fb161c3..67aaaf978b5a6ef39ab5bc9c14d94d47850f4aa8 100644 --- a/apps/server/src/gitManager.test.ts +++ b/apps/server/src/gitManager.test.ts @@ -242,6 +242,28 @@ describe("GitManager", () => { expect(ghCalls.some((call) => call.includes("--state merged"))).toBe(true); }); + it("status skips open PR lookup for branches without upstream", async () => { + const repoDir = makeTempDir("t3code-git-manager-"); + await initRepo(repoDir); + await runGit(repoDir, ["checkout", "-b", "feature/local-only"]); + + const { runner, ghCalls } = createRunnerWithFakeGh({ + prListSequence: ["[]"], + }); + + const manager = new GitManager({ + runProcess: runner, + textGenerator: createTextGenerator(), + }); + + const status = await manager.status({ cwd: repoDir }); + expect(status.branch).toBe("feature/local-only"); + expect(status.openPr).toBeNull(); + expect(status.mergedPr).toBeNull(); + expect(ghCalls.some((call) => call.includes("--state open"))).toBe(false); + expect(ghCalls.some((call) => call.includes("--state merged"))).toBe(true); + }); + it("status is resilient to gh lookup failures and returns openPr/mergedPr null", async () => { const repoDir = makeTempDir("t3code-git-manager-"); await initRepo(repoDir); diff --git a/apps/server/src/gitManager.ts b/apps/server/src/gitManager.ts index d163b8ee9dd5f37cfd29aac77bf24d1c114fa15e..c8668d1cb8f111c48601c7d73a87d043e650a514 100644 --- a/apps/server/src/gitManager.ts +++ b/apps/server/src/gitManager.ts @@ -195,10 +195,13 @@ export class GitManager { let mergedPr: ReturnType | null = null; if (details.branch) { try { - const existing = await this.findOpenPr(input.cwd, details.branch); - if (existing) { - openPr = toStatusOpenPr(existing); - } else { + if (details.hasUpstream) { + const existing = await this.findOpenPr(input.cwd, details.branch); + if (existing) { + openPr = toStatusOpenPr(existing); + } + } + if (!openPr) { const merged = await this.findMergedPr(input.cwd, details.branch); if (merged) { mergedPr = toStatusOpenPr(merged); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index a0bd90740e910ca052183216c6e5c600ffccc12b..5f18ccb42eee41b554ea38e1b0e5b610ce7a9cc6 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query"; -import type { GitStatusResult } from "@t3tools/contracts"; +import { gitStatusResultSchema, type GitStatusResult } from "@t3tools/contracts"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; import { DEFAULT_MODEL } from "../model-logic"; @@ -153,29 +153,49 @@ export default function Sidebar() { })), [projectCwdById, state.threads], ); + const threadGitStatusCwds = useMemo( + () => + [ + ...new Set( + threadGitTargets + .filter((target) => target.branch !== null) + .map((target) => target.cwd) + .filter((cwd): cwd is string => cwd !== null), + ), + ], + [threadGitTargets], + ); const threadGitStatusQueries = useQueries({ - queries: threadGitTargets.map((target) => { - const base = gitStatusQueryOptions(api, target.cwd); + queries: threadGitStatusCwds.map((cwd) => { + const base = gitStatusQueryOptions(api, cwd); return { ...base, - enabled: Boolean(base.enabled && target.branch !== null), + enabled: Boolean(base.enabled), staleTime: 30_000, refetchInterval: 60_000, }; }), }); const mergedPrByThreadId = useMemo(() => { + const statusByCwd = new Map(); + for (let index = 0; index < threadGitStatusCwds.length; index += 1) { + const cwd = threadGitStatusCwds[index]; + if (!cwd) continue; + const parsed = gitStatusResultSchema.safeParse(threadGitStatusQueries[index]?.data); + if (parsed.success) { + statusByCwd.set(cwd, parsed.data); + } + } + const map = new Map(); - for (let index = 0; index < threadGitTargets.length; index += 1) { - const target = threadGitTargets[index]; - if (!target) continue; - const status = threadGitStatusQueries[index]?.data as GitStatusResult | undefined; + for (const target of threadGitTargets) { + const status = target.cwd ? statusByCwd.get(target.cwd) : undefined; const branchMatches = target.branch !== null && status?.branch !== null && status?.branch === target.branch; map.set(target.threadId, branchMatches && status?.openPr === null && status?.mergedPr !== null); } return map; - }, [threadGitStatusQueries, threadGitTargets]); + }, [threadGitStatusCwds, threadGitStatusQueries, threadGitTargets]); const handleNewThread = useCallback( (projectId: string) => { -- 2.54.0