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 { promisify } from 'util';
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 { createLogger, loadContextFiles, classifyError } from '@automaker/utils';
import { getFeatureDir } from '@automaker/platform';
@@ -213,7 +213,7 @@ export class AutoModeServiceFacade {
[key: string]: unknown;
}
): Promise<void> => {
const resolvedModel = resolveModelString(model, 'claude-sonnet-4-6');
const resolvedModel = resolveModelString(model, DEFAULT_MODELS.claude);
const provider = ProviderFactory.getProviderForModel(resolvedModel);
const effectiveBareModel = stripProviderPrefix(resolvedModel);

View File

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

View File

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

View File

@@ -16,8 +16,8 @@
*/
import { createLogger, getErrorMessage } from '@automaker/utils';
import { getConflictFiles } from '@automaker/git-utils';
import { execGitCommand, execGitCommandWithLockRetry, getCurrentBranch } from '../lib/git.js';
import { execGitCommand, getConflictFiles } from '@automaker/git-utils';
import { execGitCommandWithLockRetry, getCurrentBranch } from '../lib/git.js';
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)
if (pullConflict) {
return {
success: true,
success: false,
branch: branchName,
pulled: true,
hasConflicts: true,

View File

@@ -8,8 +8,7 @@
import fs from 'fs/promises';
import path from 'path';
import { createLogger, getErrorMessage } from '@automaker/utils';
import { getConflictFiles } from '@automaker/git-utils';
import { execGitCommand, getCurrentBranch } from '../lib/git.js';
import { execGitCommand, getCurrentBranch, getConflictFiles } from '@automaker/git-utils';
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.
// Set LC_ALL=C so git always emits English output regardless of the system
// locale, making text-based conflict detection reliable.
await execGitCommand(['rebase', '--', ontoBranch], worktreePath, { LC_ALL: 'C' });
await execGitCommand(['rebase', '--', normalizedOntoBranch], worktreePath, { LC_ALL: 'C' });
return {
success: true,
branch: currentBranch,
ontoBranch,
message: `Successfully rebased ${currentBranch} onto ${ontoBranch}`,
ontoBranch: normalizedOntoBranch,
message: `Successfully rebased ${currentBranch} onto ${normalizedOntoBranch}`,
};
} catch (rebaseError: unknown) {
// 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 {
success: false,
error: aborted
? `Rebase of "${currentBranch}" onto "${ontoBranch}" 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}" aborted due to conflicts; no changes were applied.`
: `Rebase of "${currentBranch}" onto "${normalizedOntoBranch}" failed due to conflicts and the abort also failed; repository may be in a dirty state.`,
hasConflicts: true,
conflictFiles,
aborted,
branch: currentBranch,
ontoBranch,
ontoBranch: normalizedOntoBranch,
};
}

View File

@@ -658,7 +658,11 @@ export function GitDiffPanel({
}, [worktreePath, projectPath, useWorktrees, enableStaging, files, executeStagingAction]);
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 (enableStaging && useWorktrees && !worktreePath) {
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 { getConflictFiles } from './conflict.js';
// Export branch utilities
export { getCurrentBranch } from './branch.js';