From 2d9e38ad99b88d5a6af39b671e7de3b3f71fad89 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Sat, 17 Jan 2026 12:32:42 +0100 Subject: [PATCH] fix: stop repeated GitHub PR fetch warnings for non-GitHub repos When opening a git repository without a GitHub remote, the server logs were spammed with warnings every 5 seconds during worktree polling: WARN [Worktree] Failed to fetch GitHub PRs: Command failed: gh pr list ... no git remotes found This happened because fetchGitHubPRs() ran `gh pr list` without first checking if the project has a GitHub remote configured. Changes: - Add per-project cache for GitHub remote status with 5-minute TTL - Check cache before attempting to fetch PRs, skip silently if no remote - Add forceRefreshGitHub parameter to clear cache on manual refresh - Pass forceRefreshGitHub when user clicks the refresh worktrees button This allows users to add a GitHub remote and immediately detect it by clicking the refresh button, while preventing log spam during normal polling for projects without GitHub remotes. --- .../server/src/routes/worktree/routes/list.ts | 74 +++++++++++++++++-- .../worktree-panel/hooks/use-worktrees.ts | 5 +- apps/ui/src/lib/electron.ts | 7 +- apps/ui/src/lib/http-api-client.ts | 4 +- apps/ui/src/types/electron.d.ts | 3 +- 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index a7c12f98..4d623414 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -16,10 +16,27 @@ import { isGitRepo } from '@automaker/git-utils'; import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js'; import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js'; import { createLogger } from '@automaker/utils'; +import { + checkGitHubRemote, + type GitHubRemoteStatus, +} from '../../github/routes/check-github-remote.js'; const execAsync = promisify(exec); const logger = createLogger('Worktree'); +/** + * Cache for GitHub remote status per project path. + * This prevents repeated "no git remotes found" warnings when polling + * projects that don't have a GitHub remote configured. + */ +interface GitHubRemoteCacheEntry { + status: GitHubRemoteStatus; + checkedAt: number; +} + +const githubRemoteCache = new Map(); +const GITHUB_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes + interface WorktreeInfo { path: string; branch: string; @@ -121,23 +138,63 @@ async function scanWorktreesDirectory( return discovered; } +/** + * Get cached GitHub remote status for a project, or check and cache it. + * Returns null if gh CLI is not available. + */ +async function getGitHubRemoteStatus(projectPath: string): Promise { + // Check if gh CLI is available first + const ghAvailable = await isGhCliAvailable(); + if (!ghAvailable) { + return null; + } + + const now = Date.now(); + const cached = githubRemoteCache.get(projectPath); + + // Return cached result if still valid + if (cached && now - cached.checkedAt < GITHUB_REMOTE_CACHE_TTL_MS) { + return cached.status; + } + + // Check GitHub remote and cache the result + const status = await checkGitHubRemote(projectPath); + githubRemoteCache.set(projectPath, { + status, + checkedAt: now, + }); + + return status; +} + /** * Fetch open PRs from GitHub and create a map of branch name to PR info. * This allows detecting PRs that were created outside the app. + * + * Uses cached GitHub remote status to avoid repeated warnings when the + * project doesn't have a GitHub remote configured. */ async function fetchGitHubPRs(projectPath: string): Promise> { const prMap = new Map(); try { - // Check if gh CLI is available - const ghAvailable = await isGhCliAvailable(); - if (!ghAvailable) { + // Check GitHub remote status (uses cache to avoid repeated warnings) + const remoteStatus = await getGitHubRemoteStatus(projectPath); + + // If gh CLI not available or no GitHub remote, return empty silently + if (!remoteStatus || !remoteStatus.hasGitHubRemote) { return prMap; } + // Use -R flag with owner/repo for more reliable PR fetching + const repoFlag = + remoteStatus.owner && remoteStatus.repo + ? `-R ${remoteStatus.owner}/${remoteStatus.repo}` + : ''; + // Fetch open PRs from GitHub const { stdout } = await execAsync( - 'gh pr list --state open --json number,title,url,state,headRefName,createdAt --limit 1000', + `gh pr list ${repoFlag} --state open --json number,title,url,state,headRefName,createdAt --limit 1000`, { cwd: projectPath, env: execEnv, timeout: 15000 } ); @@ -170,9 +227,10 @@ async function fetchGitHubPRs(projectPath: string): Promise => { try { - const { projectPath, includeDetails } = req.body as { + const { projectPath, includeDetails, forceRefreshGitHub } = req.body as { projectPath: string; includeDetails?: boolean; + forceRefreshGitHub?: boolean; }; if (!projectPath) { @@ -180,6 +238,12 @@ export function createListHandler() { return; } + // Clear GitHub remote cache if force refresh requested + // This allows users to re-check for GitHub remote after adding one + if (forceRefreshGitHub) { + githubRemoteCache.delete(projectPath); + } + if (!(await isGitRepo(projectPath))) { res.json({ success: true, worktrees: [] }); return; diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts index 1575f38a..95589f4b 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts @@ -39,7 +39,10 @@ export function useWorktrees({ logger.warn('Worktree API not available'); return; } - const result = await api.worktree.listAll(projectPath, true); + // Pass forceRefreshGitHub when this is a manual refresh (not silent polling) + // This clears the GitHub remote cache so users can re-detect after adding a remote + const forceRefreshGitHub = !silent; + const result = await api.worktree.listAll(projectPath, true, forceRefreshGitHub); if (result.success && result.worktrees) { setWorktrees(result.worktrees); setWorktreesInStore(projectPath, result.worktrees); diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 97167e89..66bbd537 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -1596,10 +1596,15 @@ function createMockWorktreeAPI(): WorktreeAPI { return { success: true, worktrees: [] }; }, - listAll: async (projectPath: string, includeDetails?: boolean) => { + listAll: async ( + projectPath: string, + includeDetails?: boolean, + forceRefreshGitHub?: boolean + ) => { console.log('[Mock] Listing all worktrees:', { projectPath, includeDetails, + forceRefreshGitHub, }); return { success: true, diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index 547fee7f..d7ac5280 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1724,8 +1724,8 @@ export class HttpApiClient implements ElectronAPI { getStatus: (projectPath: string, featureId: string) => this.post('/api/worktree/status', { projectPath, featureId }), list: (projectPath: string) => this.post('/api/worktree/list', { projectPath }), - listAll: (projectPath: string, includeDetails?: boolean) => - this.post('/api/worktree/list', { projectPath, includeDetails }), + listAll: (projectPath: string, includeDetails?: boolean, forceRefreshGitHub?: boolean) => + this.post('/api/worktree/list', { projectPath, includeDetails, forceRefreshGitHub }), create: (projectPath: string, branchName: string, baseBranch?: string) => this.post('/api/worktree/create', { projectPath, diff --git a/apps/ui/src/types/electron.d.ts b/apps/ui/src/types/electron.d.ts index 42e4200d..49c1c4ad 100644 --- a/apps/ui/src/types/electron.d.ts +++ b/apps/ui/src/types/electron.d.ts @@ -705,7 +705,8 @@ export interface WorktreeAPI { // List all worktrees with details (for worktree selector) listAll: ( projectPath: string, - includeDetails?: boolean + includeDetails?: boolean, + forceRefreshGitHub?: boolean ) => Promise<{ success: boolean; worktrees?: Array<{