Merge pull request #540 from stefandevo/fix/gh-not-in-git-folder

fix: stop repeated GitHub PR fetch warnings for non-GitHub repos
This commit is contained in:
Shirone
2026-01-17 13:08:28 +00:00
committed by GitHub
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 { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js';
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js'; import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import {
checkGitHubRemote,
type GitHubRemoteStatus,
} from '../../github/routes/check-github-remote.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const logger = createLogger('Worktree'); 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 { interface WorktreeInfo {
path: string; path: string;
branch: string; branch: string;
@@ -121,23 +138,63 @@ async function scanWorktreesDirectory(
return discovered; 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: Date.now(),
});
return status;
}
/** /**
* Fetch open PRs from GitHub and create a map of branch name to PR info. * 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. * 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>> { async function fetchGitHubPRs(projectPath: string): Promise<Map<string, WorktreePRInfo>> {
const prMap = new Map<string, WorktreePRInfo>(); const prMap = new Map<string, WorktreePRInfo>();
try { try {
// Check if gh CLI is available // Check GitHub remote status (uses cache to avoid repeated warnings)
const ghAvailable = await isGhCliAvailable(); const remoteStatus = await getGitHubRemoteStatus(projectPath);
if (!ghAvailable) {
// If gh CLI not available or no GitHub remote, return empty silently
if (!remoteStatus || !remoteStatus.hasGitHubRemote) {
return prMap; 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 // Fetch open PRs from GitHub
const { stdout } = await execAsync( 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 } { cwd: projectPath, env: execEnv, timeout: 15000 }
); );
@@ -170,9 +227,10 @@ async function fetchGitHubPRs(projectPath: string): Promise<Map<string, Worktree
export function createListHandler() { export function createListHandler() {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { projectPath, includeDetails } = req.body as { const { projectPath, includeDetails, forceRefreshGitHub } = req.body as {
projectPath: string; projectPath: string;
includeDetails?: boolean; includeDetails?: boolean;
forceRefreshGitHub?: boolean;
}; };
if (!projectPath) { if (!projectPath) {
@@ -180,6 +238,12 @@ export function createListHandler() {
return; 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))) { if (!(await isGitRepo(projectPath))) {
res.json({ success: true, worktrees: [] }); res.json({ success: true, worktrees: [] });
return; return;

View File

@@ -39,7 +39,10 @@ export function useWorktrees({
logger.warn('Worktree API not available'); logger.warn('Worktree API not available');
return; 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) { if (result.success && result.worktrees) {
setWorktrees(result.worktrees); setWorktrees(result.worktrees);
setWorktreesInStore(projectPath, result.worktrees); setWorktreesInStore(projectPath, result.worktrees);

View File

@@ -1596,10 +1596,15 @@ function createMockWorktreeAPI(): WorktreeAPI {
return { success: true, worktrees: [] }; return { success: true, worktrees: [] };
}, },
listAll: async (projectPath: string, includeDetails?: boolean) => { listAll: async (
projectPath: string,
includeDetails?: boolean,
forceRefreshGitHub?: boolean
) => {
console.log('[Mock] Listing all worktrees:', { console.log('[Mock] Listing all worktrees:', {
projectPath, projectPath,
includeDetails, includeDetails,
forceRefreshGitHub,
}); });
return { return {
success: true, success: true,

View File

@@ -1724,8 +1724,8 @@ export class HttpApiClient implements ElectronAPI {
getStatus: (projectPath: string, featureId: string) => getStatus: (projectPath: string, featureId: string) =>
this.post('/api/worktree/status', { projectPath, featureId }), this.post('/api/worktree/status', { projectPath, featureId }),
list: (projectPath: string) => this.post('/api/worktree/list', { projectPath }), list: (projectPath: string) => this.post('/api/worktree/list', { projectPath }),
listAll: (projectPath: string, includeDetails?: boolean) => listAll: (projectPath: string, includeDetails?: boolean, forceRefreshGitHub?: boolean) =>
this.post('/api/worktree/list', { projectPath, includeDetails }), this.post('/api/worktree/list', { projectPath, includeDetails, forceRefreshGitHub }),
create: (projectPath: string, branchName: string, baseBranch?: string) => create: (projectPath: string, branchName: string, baseBranch?: string) =>
this.post('/api/worktree/create', { this.post('/api/worktree/create', {
projectPath, projectPath,

View File

@@ -705,7 +705,8 @@ export interface WorktreeAPI {
// List all worktrees with details (for worktree selector) // List all worktrees with details (for worktree selector)
listAll: ( listAll: (
projectPath: string, projectPath: string,
includeDetails?: boolean includeDetails?: boolean,
forceRefreshGitHub?: boolean
) => Promise<{ ) => Promise<{
success: boolean; success: boolean;
worktrees?: Array<{ worktrees?: Array<{