mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
fix: Resolve git operation error handling and conflict detection issues
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -296,6 +296,10 @@ export async function performCheckoutBranch(
|
|||||||
branchName,
|
branchName,
|
||||||
error: checkoutErrorMsg,
|
error: checkoutErrorMsg,
|
||||||
});
|
});
|
||||||
throw checkoutError;
|
return {
|
||||||
|
success: false,
|
||||||
|
error: checkoutErrorMsg,
|
||||||
|
stashPopConflicts: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
23
libs/git-utils/src/branch.ts
Normal file
23
libs/git-utils/src/branch.ts
Normal 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();
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user