fix: Resolve git operation error handling and conflict detection issues

This commit is contained in:
gsxdsm
2026-02-18 23:03:39 -08:00
parent 205f662022
commit a144a63c51
8 changed files with 50 additions and 17 deletions

View File

@@ -15,7 +15,7 @@ import path from 'path';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import type { Feature, PlanningMode, ThinkingLevel } from '@automaker/types'; import type { Feature, PlanningMode, ThinkingLevel } from '@automaker/types';
import { DEFAULT_MAX_CONCURRENCY, stripProviderPrefix } from '@automaker/types'; import { DEFAULT_MAX_CONCURRENCY, DEFAULT_MODELS, stripProviderPrefix } from '@automaker/types';
import { resolveModelString } from '@automaker/model-resolver'; import { resolveModelString } from '@automaker/model-resolver';
import { createLogger, loadContextFiles, classifyError } from '@automaker/utils'; import { createLogger, loadContextFiles, classifyError } from '@automaker/utils';
import { getFeatureDir } from '@automaker/platform'; import { getFeatureDir } from '@automaker/platform';
@@ -213,7 +213,7 @@ export class AutoModeServiceFacade {
[key: string]: unknown; [key: string]: unknown;
} }
): Promise<void> => { ): Promise<void> => {
const resolvedModel = resolveModelString(model, 'claude-sonnet-4-6'); const resolvedModel = resolveModelString(model, DEFAULT_MODELS.claude);
const provider = ProviderFactory.getProviderForModel(resolvedModel); const provider = ProviderFactory.getProviderForModel(resolvedModel);
const effectiveBareModel = stripProviderPrefix(resolvedModel); const effectiveBareModel = stripProviderPrefix(resolvedModel);

View File

@@ -127,8 +127,8 @@ export async function popStash(
cwd: string cwd: string
): Promise<{ success: boolean; hasConflicts: boolean; error?: string }> { ): Promise<{ success: boolean; hasConflicts: boolean; error?: string }> {
try { try {
await execGitCommand(['stash', 'pop'], cwd); await execGitCommandWithLockRetry(['stash', 'pop'], cwd);
// If execGitCommand succeeds (zero exit code), there are no conflicts // If execGitCommandWithLockRetry succeeds (zero exit code), there are no conflicts
return { success: true, hasConflicts: false }; return { success: true, hasConflicts: false };
} catch (error) { } catch (error) {
const errorMsg = getErrorMessage(error); const errorMsg = getErrorMessage(error);

View File

@@ -296,6 +296,10 @@ export async function performCheckoutBranch(
branchName, branchName,
error: checkoutErrorMsg, error: checkoutErrorMsg,
}); });
throw checkoutError; return {
success: false,
error: checkoutErrorMsg,
stashPopConflicts: false,
};
} }
} }

View File

@@ -16,8 +16,8 @@
*/ */
import { createLogger, getErrorMessage } from '@automaker/utils'; import { createLogger, getErrorMessage } from '@automaker/utils';
import { getConflictFiles } from '@automaker/git-utils'; import { execGitCommand, getConflictFiles } from '@automaker/git-utils';
import { execGitCommand, execGitCommandWithLockRetry, getCurrentBranch } from '../lib/git.js'; import { execGitCommandWithLockRetry, getCurrentBranch } from '../lib/git.js';
const logger = createLogger('PullService'); const logger = createLogger('PullService');
@@ -359,7 +359,7 @@ export async function performPull(
// 9. If pull had conflicts, return conflict info (don't try stash pop) // 9. If pull had conflicts, return conflict info (don't try stash pop)
if (pullConflict) { if (pullConflict) {
return { return {
success: true, success: false,
branch: branchName, branch: branchName,
pulled: true, pulled: true,
hasConflicts: true, hasConflicts: true,

View File

@@ -8,8 +8,7 @@
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
import { createLogger, getErrorMessage } from '@automaker/utils'; import { createLogger, getErrorMessage } from '@automaker/utils';
import { getConflictFiles } from '@automaker/git-utils'; import { execGitCommand, getCurrentBranch, getConflictFiles } from '@automaker/git-utils';
import { execGitCommand, getCurrentBranch } from '../lib/git.js';
const logger = createLogger('RebaseService'); const logger = createLogger('RebaseService');
@@ -64,13 +63,13 @@ export async function runRebase(worktreePath: string, ontoBranch: string): Promi
// Pass ontoBranch after '--' so git treats it as a ref, not an option. // Pass ontoBranch after '--' so git treats it as a ref, not an option.
// Set LC_ALL=C so git always emits English output regardless of the system // Set LC_ALL=C so git always emits English output regardless of the system
// locale, making text-based conflict detection reliable. // locale, making text-based conflict detection reliable.
await execGitCommand(['rebase', '--', ontoBranch], worktreePath, { LC_ALL: 'C' }); await execGitCommand(['rebase', '--', normalizedOntoBranch], worktreePath, { LC_ALL: 'C' });
return { return {
success: true, success: true,
branch: currentBranch, branch: currentBranch,
ontoBranch, ontoBranch: normalizedOntoBranch,
message: `Successfully rebased ${currentBranch} onto ${ontoBranch}`, message: `Successfully rebased ${currentBranch} onto ${normalizedOntoBranch}`,
}; };
} catch (rebaseError: unknown) { } catch (rebaseError: unknown) {
// Check if this is a rebase conflict. We use a multi-layer strategy so // Check if this is a rebase conflict. We use a multi-layer strategy so
@@ -165,13 +164,13 @@ export async function runRebase(worktreePath: string, ontoBranch: string): Promi
return { return {
success: false, success: false,
error: aborted error: aborted
? `Rebase of "${currentBranch}" onto "${ontoBranch}" aborted due to conflicts; no changes were applied.` ? `Rebase of "${currentBranch}" onto "${normalizedOntoBranch}" aborted due to conflicts; no changes were applied.`
: `Rebase of "${currentBranch}" onto "${ontoBranch}" failed due to conflicts and the abort also failed; repository may be in a dirty state.`, : `Rebase of "${currentBranch}" onto "${normalizedOntoBranch}" failed due to conflicts and the abort also failed; repository may be in a dirty state.`,
hasConflicts: true, hasConflicts: true,
conflictFiles, conflictFiles,
aborted, aborted,
branch: currentBranch, branch: currentBranch,
ontoBranch, ontoBranch: normalizedOntoBranch,
}; };
} }

View File

@@ -658,7 +658,11 @@ export function GitDiffPanel({
}, [worktreePath, projectPath, useWorktrees, enableStaging, files, executeStagingAction]); }, [worktreePath, projectPath, useWorktrees, enableStaging, files, executeStagingAction]);
const handleUnstageAll = useCallback(async () => { const handleUnstageAll = useCallback(async () => {
const allPaths = files.map((f) => f.path); const stagedFiles = files.filter((f) => {
const state = getStagingState(f);
return state === 'staged' || state === 'partial';
});
const allPaths = stagedFiles.map((f) => f.path);
if (allPaths.length === 0) return; if (allPaths.length === 0) return;
if (enableStaging && useWorktrees && !worktreePath) { if (enableStaging && useWorktrees && !worktreePath) {
toast.error('Failed to unstage all files', { toast.error('Failed to unstage all files', {

View File

@@ -0,0 +1,23 @@
/**
* Git branch utilities
*/
import { execGitCommand } from './exec.js';
/**
* Get the current branch name for a given worktree path.
*
* @param worktreePath - Path to the git worktree
* @returns Promise resolving to the current branch name (trimmed)
* @throws Error if the git command fails
*
* @example
* ```typescript
* const branch = await getCurrentBranch('/path/to/worktree');
* console.log(branch); // 'main'
* ```
*/
export async function getCurrentBranch(worktreePath: string): Promise<string> {
const branchOutput = await execGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], worktreePath);
return branchOutput.trim();
}

View File

@@ -23,3 +23,6 @@ export {
// Export conflict utilities // Export conflict utilities
export { getConflictFiles } from './conflict.js'; export { getConflictFiles } from './conflict.js';
// Export branch utilities
export { getCurrentBranch } from './branch.js';