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.
This commit is contained in:
Stefan de Vogelaere
2026-01-17 12:32:42 +01:00
parent ef6b9ac2d2
commit 2d9e38ad99
5 changed files with 83 additions and 10 deletions

View File

@@ -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<string, GitHubRemoteCacheEntry>();
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<GitHubRemoteStatus | null> {
// 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<Map<string, WorktreePRInfo>> {
const prMap = new Map<string, WorktreePRInfo>();
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<Map<string, Worktree
export function createListHandler() {
return async (req: Request, res: Response): Promise<void> => {
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;