| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { createLogger, getErrorMessage } from '@automaker/utils'; |
| import { execGitCommand } from '@automaker/git-utils'; |
| import { getCurrentBranch } from '../lib/git.js'; |
| import { performPull } from './pull-service.js'; |
|
|
| const logger = createLogger('PushService'); |
|
|
| |
| |
| |
|
|
| export interface PushOptions { |
| |
| remote?: string; |
| |
| force?: boolean; |
| |
| autoResolve?: boolean; |
| } |
|
|
| export interface PushResult { |
| success: boolean; |
| error?: string; |
| branch?: string; |
| pushed?: boolean; |
| |
| diverged?: boolean; |
| |
| autoResolved?: boolean; |
| |
| hasConflicts?: boolean; |
| |
| conflictFiles?: string[]; |
| message?: string; |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| function isDivergenceError(errorOutput: string): boolean { |
| const lower = errorOutput.toLowerCase(); |
| |
| |
| const hasNonFastForward = lower.includes('non-fast-forward'); |
| const hasFetchFirst = lower.includes('fetch first'); |
| const hasFailedToPush = lower.includes('failed to push some refs'); |
| const hasRejected = lower.includes('rejected'); |
| return hasNonFastForward || hasFetchFirst || (hasRejected && hasFailedToPush); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function performPush( |
| worktreePath: string, |
| options?: PushOptions |
| ): Promise<PushResult> { |
| const targetRemote = options?.remote || 'origin'; |
| const force = options?.force ?? false; |
| const autoResolve = options?.autoResolve ?? false; |
|
|
| |
| let branchName: string; |
| try { |
| branchName = await getCurrentBranch(worktreePath); |
| } catch (err) { |
| return { |
| success: false, |
| error: `Failed to get current branch: ${getErrorMessage(err)}`, |
| }; |
| } |
|
|
| |
| if (branchName === 'HEAD') { |
| return { |
| success: false, |
| error: 'Cannot push in detached HEAD state. Please checkout a branch first.', |
| }; |
| } |
|
|
| |
| const pushArgs = ['push', targetRemote, branchName]; |
| if (force) { |
| pushArgs.push('--force'); |
| } |
|
|
| |
| try { |
| await execGitCommand(pushArgs, worktreePath); |
|
|
| return { |
| success: true, |
| branch: branchName, |
| pushed: true, |
| message: `Successfully pushed ${branchName} to ${targetRemote}`, |
| }; |
| } catch (pushError: unknown) { |
| const err = pushError as { stderr?: string; stdout?: string; message?: string }; |
| const errorOutput = `${err.stderr || ''} ${err.stdout || ''} ${err.message || ''}`; |
|
|
| |
| if (isDivergenceError(errorOutput)) { |
| if (!autoResolve) { |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| diverged: true, |
| error: `Push rejected: remote has changes not present locally. Use sync or pull first, or enable auto-resolve.`, |
| message: `Push to ${targetRemote} was rejected because the remote branch has diverged.`, |
| }; |
| } |
|
|
| |
| logger.info('Push rejected due to divergence, attempting auto-resolve via pull', { |
| worktreePath, |
| remote: targetRemote, |
| branch: branchName, |
| }); |
|
|
| try { |
| const pullResult = await performPull(worktreePath, { |
| remote: targetRemote, |
| stashIfNeeded: true, |
| }); |
|
|
| if (!pullResult.success) { |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| diverged: true, |
| autoResolved: false, |
| error: `Auto-resolve failed during pull: ${pullResult.error}`, |
| }; |
| } |
|
|
| if (pullResult.hasConflicts) { |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| diverged: true, |
| autoResolved: false, |
| hasConflicts: true, |
| conflictFiles: pullResult.conflictFiles, |
| error: |
| 'Auto-resolve pull resulted in merge conflicts. Resolve conflicts and push again.', |
| }; |
| } |
|
|
| |
| try { |
| await execGitCommand(pushArgs, worktreePath); |
|
|
| return { |
| success: true, |
| branch: branchName, |
| pushed: true, |
| diverged: true, |
| autoResolved: true, |
| message: `Push succeeded after auto-resolving divergence (pulled from ${targetRemote} first).`, |
| }; |
| } catch (retryError: unknown) { |
| const retryErr = retryError as { stderr?: string; message?: string }; |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| diverged: true, |
| autoResolved: false, |
| error: `Push failed after auto-resolve pull: ${retryErr.stderr || retryErr.message || 'Unknown error'}`, |
| }; |
| } |
| } catch (pullError) { |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| diverged: true, |
| autoResolved: false, |
| error: `Auto-resolve pull failed: ${getErrorMessage(pullError)}`, |
| }; |
| } |
| } |
|
|
| |
| const isNoUpstreamError = |
| errorOutput.toLowerCase().includes('no upstream') || |
| errorOutput.toLowerCase().includes('has no upstream branch') || |
| errorOutput.toLowerCase().includes('set-upstream'); |
| if (isNoUpstreamError) { |
| try { |
| const setUpstreamArgs = ['push', '--set-upstream', targetRemote, branchName]; |
| if (force) { |
| setUpstreamArgs.push('--force'); |
| } |
| await execGitCommand(setUpstreamArgs, worktreePath); |
|
|
| return { |
| success: true, |
| branch: branchName, |
| pushed: true, |
| message: `Successfully pushed ${branchName} to ${targetRemote} (set upstream)`, |
| }; |
| } catch (upstreamError: unknown) { |
| const upstreamErr = upstreamError as { stderr?: string; message?: string }; |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| error: upstreamErr.stderr || upstreamErr.message || getErrorMessage(pushError), |
| }; |
| } |
| } |
|
|
| |
| return { |
| success: false, |
| branch: branchName, |
| pushed: false, |
| error: err.stderr || err.message || getErrorMessage(pushError), |
| }; |
| } |
| } |
|
|