mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Fix concurrency limits and remote branch fetching issues (#788)
* Changes from fix/bug-fixes * feat: Refactor worktree iteration and improve error logging across services * feat: Extract URL/port patterns to module level and fix abort condition * fix: Improve IPv6 loopback handling, select component layout, and terminal UI * feat: Add thinking level defaults and adjust list row padding * Update apps/ui/src/store/app-store.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit * feat: Add tracked remote detection to pull dialog flow * feat: Add merge state tracking to git operations * feat: Improve merge detection and add post-merge action preferences * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Pass merge detection info to stash reapplication and handle merge state consistently * fix: Call onPulled callback in merge handlers and add validation checks --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -26,23 +26,9 @@ export function createRunFeatureHandler(autoModeService: AutoModeServiceCompat)
|
||||
return;
|
||||
}
|
||||
|
||||
// Check per-worktree capacity before starting
|
||||
const capacity = await autoModeService.checkWorktreeCapacity(projectPath, featureId);
|
||||
if (!capacity.hasCapacity) {
|
||||
const worktreeDesc = capacity.branchName
|
||||
? `worktree "${capacity.branchName}"`
|
||||
: 'main worktree';
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: `Agent limit reached for ${worktreeDesc} (${capacity.currentAgents}/${capacity.maxAgents}). Wait for running tasks to complete or increase the limit.`,
|
||||
details: {
|
||||
currentAgents: capacity.currentAgents,
|
||||
maxAgents: capacity.maxAgents,
|
||||
branchName: capacity.branchName,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Note: No concurrency limit check here. Manual feature starts always run
|
||||
// immediately and bypass the concurrency limit. Their presence IS counted
|
||||
// by the auto-loop coordinator when deciding whether to dispatch new auto-mode tasks.
|
||||
|
||||
// Start execution in background
|
||||
// executeFeature derives workDir from feature.branchName
|
||||
|
||||
@@ -23,6 +23,7 @@ export function createDiffsHandler() {
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
...(result.mergeState ? { mergeState: result.mergeState } : {}),
|
||||
});
|
||||
} catch (innerError) {
|
||||
logError(innerError, 'Git diff failed');
|
||||
|
||||
@@ -22,6 +22,36 @@ import { getErrorMessage, logError, isValidBranchName } from '../common.js';
|
||||
import { execGitCommand } from '../../../lib/git.js';
|
||||
import type { EventEmitter } from '../../../lib/events.js';
|
||||
import { performCheckoutBranch } from '../../../services/checkout-branch-service.js';
|
||||
import { createLogger } from '@automaker/utils';
|
||||
|
||||
const logger = createLogger('CheckoutBranchRoute');
|
||||
|
||||
/** Timeout for git fetch operations (30 seconds) */
|
||||
const FETCH_TIMEOUT_MS = 30_000;
|
||||
|
||||
/**
|
||||
* Fetch latest from all remotes (silently, with timeout).
|
||||
* Non-fatal: fetch errors are logged and swallowed so the workflow continues.
|
||||
*/
|
||||
async function fetchRemotes(cwd: string): Promise<void> {
|
||||
const controller = new AbortController();
|
||||
const timerId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
await execGitCommand(['fetch', '--all', '--quiet'], cwd, undefined, controller);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Process aborted') {
|
||||
logger.warn(
|
||||
`fetchRemotes timed out after ${FETCH_TIMEOUT_MS}ms - continuing without latest remote refs`
|
||||
);
|
||||
} else {
|
||||
logger.warn(`fetchRemotes failed: ${getErrorMessage(error)} - continuing with local refs`);
|
||||
}
|
||||
// Non-fatal: continue with locally available refs
|
||||
} finally {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
}
|
||||
|
||||
export function createCheckoutBranchHandler(events?: EventEmitter) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -127,6 +157,10 @@ export function createCheckoutBranchHandler(events?: EventEmitter) {
|
||||
}
|
||||
|
||||
// Original simple flow (no stash handling)
|
||||
// Fetch latest remote refs before creating the branch so that
|
||||
// base branch validation works for remote references like "origin/main"
|
||||
await fetchRemotes(resolvedPath);
|
||||
|
||||
const currentBranchOutput = await execGitCommand(
|
||||
['rev-parse', '--abbrev-ref', 'HEAD'],
|
||||
resolvedPath
|
||||
|
||||
@@ -30,6 +30,9 @@ import { runInitScript } from '../../../services/init-script-service.js';
|
||||
|
||||
const logger = createLogger('Worktree');
|
||||
|
||||
/** Timeout for git fetch operations (30 seconds) */
|
||||
const FETCH_TIMEOUT_MS = 30_000;
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
@@ -83,41 +86,6 @@ async function findExistingWorktreeForBranch(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether a base branch reference is a remote branch (e.g. "origin/main").
|
||||
* Returns the remote name if it matches a known remote, otherwise null.
|
||||
*/
|
||||
async function detectRemoteBranch(
|
||||
projectPath: string,
|
||||
baseBranch: string
|
||||
): Promise<{ remote: string; branch: string } | null> {
|
||||
const slashIndex = baseBranch.indexOf('/');
|
||||
if (slashIndex <= 0) return null;
|
||||
|
||||
const possibleRemote = baseBranch.substring(0, slashIndex);
|
||||
|
||||
try {
|
||||
// Check if this is actually a remote name by listing remotes
|
||||
const stdout = await execGitCommand(['remote'], projectPath);
|
||||
const remotes = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((r: string) => r.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (remotes.includes(possibleRemote)) {
|
||||
return {
|
||||
remote: possibleRemote,
|
||||
branch: baseBranch.substring(slashIndex + 1),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Not a git repo or no remotes — fall through
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createCreateHandler(events: EventEmitter, settingsService?: SettingsService) {
|
||||
const worktreeService = new WorktreeService();
|
||||
|
||||
@@ -206,26 +174,23 @@ export function createCreateHandler(events: EventEmitter, settingsService?: Sett
|
||||
// Create worktrees directory if it doesn't exist
|
||||
await secureFs.mkdir(worktreesDir, { recursive: true });
|
||||
|
||||
// If a base branch is specified and it's a remote branch, fetch from that remote first
|
||||
// This ensures we have the latest refs before creating the worktree
|
||||
if (baseBranch && baseBranch !== 'HEAD') {
|
||||
const remoteBranchInfo = await detectRemoteBranch(projectPath, baseBranch);
|
||||
if (remoteBranchInfo) {
|
||||
logger.info(
|
||||
`Fetching from remote "${remoteBranchInfo.remote}" before creating worktree (base: ${baseBranch})`
|
||||
);
|
||||
try {
|
||||
await execGitCommand(
|
||||
['fetch', remoteBranchInfo.remote, remoteBranchInfo.branch],
|
||||
projectPath
|
||||
);
|
||||
} catch (fetchErr) {
|
||||
// Non-fatal: log but continue — the ref might already be cached locally
|
||||
logger.warn(
|
||||
`Failed to fetch from remote "${remoteBranchInfo.remote}": ${getErrorMessage(fetchErr)}`
|
||||
);
|
||||
}
|
||||
// Fetch latest from all remotes before creating the worktree.
|
||||
// This ensures remote refs are up-to-date for:
|
||||
// - Remote base branches (e.g. "origin/main")
|
||||
// - Existing remote branches being checked out as worktrees
|
||||
// - Branch existence checks against fresh remote state
|
||||
logger.info('Fetching from all remotes before creating worktree');
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timerId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
await execGitCommand(['fetch', '--all', '--quiet'], projectPath, undefined, controller);
|
||||
} finally {
|
||||
clearTimeout(timerId);
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
// Non-fatal: log but continue — refs might already be cached locally
|
||||
logger.warn(`Failed to fetch from remotes: ${getErrorMessage(fetchErr)}`);
|
||||
}
|
||||
|
||||
// Check if branch exists (using array arguments to prevent injection)
|
||||
|
||||
@@ -34,6 +34,7 @@ export function createDiffsHandler() {
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
...(result.mergeState ? { mergeState: result.mergeState } : {}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -55,6 +56,7 @@ export function createDiffsHandler() {
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
...(result.mergeState ? { mergeState: result.mergeState } : {}),
|
||||
});
|
||||
} catch (innerError) {
|
||||
// Worktree doesn't exist - fallback to main project path
|
||||
@@ -71,6 +73,7 @@ export function createDiffsHandler() {
|
||||
diff: result.diff,
|
||||
files: result.files,
|
||||
hasChanges: result.hasChanges,
|
||||
...(result.mergeState ? { mergeState: result.mergeState } : {}),
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
logError(fallbackError, 'Fallback to main project also failed');
|
||||
|
||||
@@ -83,6 +83,9 @@ function mapResultToResponse(res: Response, result: PullResult): void {
|
||||
stashed: result.stashed,
|
||||
stashRestored: result.stashRestored,
|
||||
message: result.message,
|
||||
isMerge: result.isMerge,
|
||||
isFastForward: result.isFastForward,
|
||||
mergeAffectedFiles: result.mergeAffectedFiles,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* For remote branches (e.g., "origin/feature"), automatically creates a
|
||||
* local tracking branch and checks it out.
|
||||
*
|
||||
* Also fetches the latest remote refs after switching.
|
||||
* Also fetches the latest remote refs before switching to ensure accurate branch detection.
|
||||
*
|
||||
* Git business logic is delegated to worktree-branch-service.ts.
|
||||
* Events are emitted at key lifecycle points for WebSocket subscribers.
|
||||
|
||||
Reference in New Issue
Block a user