From a144a63c5120e5e3f560e587507588e366d22bd8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 18 Feb 2026 23:03:39 -0800 Subject: [PATCH] fix: Resolve git operation error handling and conflict detection issues --- apps/server/src/services/auto-mode/facade.ts | 4 ++-- apps/server/src/services/branch-utils.ts | 4 ++-- .../src/services/checkout-branch-service.ts | 6 ++++- apps/server/src/services/pull-service.ts | 6 ++--- apps/server/src/services/rebase-service.ts | 15 ++++++------ apps/ui/src/components/ui/git-diff-panel.tsx | 6 ++++- libs/git-utils/src/branch.ts | 23 +++++++++++++++++++ libs/git-utils/src/index.ts | 3 +++ 8 files changed, 50 insertions(+), 17 deletions(-) create mode 100644 libs/git-utils/src/branch.ts diff --git a/apps/server/src/services/auto-mode/facade.ts b/apps/server/src/services/auto-mode/facade.ts index 5b606e36..c63d9889 100644 --- a/apps/server/src/services/auto-mode/facade.ts +++ b/apps/server/src/services/auto-mode/facade.ts @@ -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 => { - const resolvedModel = resolveModelString(model, 'claude-sonnet-4-6'); + const resolvedModel = resolveModelString(model, DEFAULT_MODELS.claude); const provider = ProviderFactory.getProviderForModel(resolvedModel); const effectiveBareModel = stripProviderPrefix(resolvedModel); diff --git a/apps/server/src/services/branch-utils.ts b/apps/server/src/services/branch-utils.ts index 66510d28..3526c5ab 100644 --- a/apps/server/src/services/branch-utils.ts +++ b/apps/server/src/services/branch-utils.ts @@ -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); diff --git a/apps/server/src/services/checkout-branch-service.ts b/apps/server/src/services/checkout-branch-service.ts index 521d375a..f4b9c817 100644 --- a/apps/server/src/services/checkout-branch-service.ts +++ b/apps/server/src/services/checkout-branch-service.ts @@ -296,6 +296,10 @@ export async function performCheckoutBranch( branchName, error: checkoutErrorMsg, }); - throw checkoutError; + return { + success: false, + error: checkoutErrorMsg, + stashPopConflicts: false, + }; } } diff --git a/apps/server/src/services/pull-service.ts b/apps/server/src/services/pull-service.ts index 9222162a..ab217c2b 100644 --- a/apps/server/src/services/pull-service.ts +++ b/apps/server/src/services/pull-service.ts @@ -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, diff --git a/apps/server/src/services/rebase-service.ts b/apps/server/src/services/rebase-service.ts index e10cecc0..05c2f33e 100644 --- a/apps/server/src/services/rebase-service.ts +++ b/apps/server/src/services/rebase-service.ts @@ -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, }; } diff --git a/apps/ui/src/components/ui/git-diff-panel.tsx b/apps/ui/src/components/ui/git-diff-panel.tsx index 84fe7cac..f39c39da 100644 --- a/apps/ui/src/components/ui/git-diff-panel.tsx +++ b/apps/ui/src/components/ui/git-diff-panel.tsx @@ -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', { diff --git a/libs/git-utils/src/branch.ts b/libs/git-utils/src/branch.ts new file mode 100644 index 00000000..80377404 --- /dev/null +++ b/libs/git-utils/src/branch.ts @@ -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 { + const branchOutput = await execGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], worktreePath); + return branchOutput.trim(); +} diff --git a/libs/git-utils/src/index.ts b/libs/git-utils/src/index.ts index cdcadffc..b9cea86e 100644 --- a/libs/git-utils/src/index.ts +++ b/libs/git-utils/src/index.ts @@ -23,3 +23,6 @@ export { // Export conflict utilities export { getConflictFiles } from './conflict.js'; + +// Export branch utilities +export { getCurrentBranch } from './branch.js';