rebase.ts•3.8 kB
/**
 * @fileoverview Git rebase operations
 * @module services/git/providers/cli/operations/branches/rebase
 */
import type { RequestContext } from '@/utils/index.js';
import type {
  GitOperationContext,
  GitRebaseOptions,
  GitRebaseResult,
} from '../../../../types.js';
import {
  buildGitCommand,
  mapGitError,
  shouldSignCommits,
} from '../../utils/index.js';
/**
 * Execute git rebase to reapply commits.
 */
export async function executeRebase(
  options: GitRebaseOptions,
  context: GitOperationContext,
  execGit: (
    args: string[],
    cwd: string,
    ctx: RequestContext,
  ) => Promise<{ stdout: string; stderr: string }>,
): Promise<GitRebaseResult> {
  try {
    const args: string[] = [];
    // Handle mode-based operations
    const mode = options.mode || 'start';
    if (mode === 'continue') {
      try {
        const continueCmd = buildGitCommand({
          command: 'rebase',
          args: ['--continue', '--no-edit'],
        });
        await execGit(
          continueCmd,
          context.workingDirectory,
          context.requestContext,
        );
      } catch (error) {
        if (
          error instanceof Error &&
          error.message.includes("unknown option `no-edit'")
        ) {
          const continueCmd = buildGitCommand({
            command: 'rebase',
            args: ['--continue'],
          });
          await execGit(
            continueCmd,
            context.workingDirectory,
            context.requestContext,
          );
        } else {
          // Re-throw other errors
          throw error;
        }
      }
      // Since the rebase is now handled within this block, we can return early
      return {
        success: true,
        conflicts: false,
        conflictedFiles: [],
        rebasedCommits: 1, // Assuming success
      };
    } else if (mode === 'abort') {
      args.push('--abort');
    } else if (mode === 'skip') {
      args.push('--skip');
    } else {
      // Start mode - requires upstream
      if (!options.upstream) {
        throw new Error('upstream is required for start mode');
      }
      if (options.onto) {
        args.push('--onto', options.onto, options.upstream);
        if (options.branch) {
          args.push(options.branch);
        }
      } else {
        args.push(options.upstream);
        if (options.branch) {
          args.push(options.branch);
        }
      }
      if (options.interactive) {
        args.push('--interactive');
      }
      if (options.preserve) {
        args.push('--preserve-merges');
      }
      // Add signing support for rebased commits - use explicit option or fall back to config default
      const shouldSign = options.sign ?? shouldSignCommits();
      if (shouldSign) {
        args.push('--gpg-sign');
      }
    }
    const cmd = buildGitCommand({ command: 'rebase', args });
    const result = await execGit(
      cmd,
      context.workingDirectory,
      context.requestContext,
    );
    const hasConflicts =
      result.stdout.includes('CONFLICT') || result.stderr.includes('CONFLICT');
    // Parse conflicted files
    const conflictedFiles = result.stdout
      .split('\n')
      .filter((line) => line.includes('CONFLICT'))
      .map((line) => {
        const match = line.match(/CONFLICT.*?in (.+)$/);
        return match?.[1] || '';
      })
      .filter((f) => f);
    // Count commits (simplified)
    const commitsMatch = result.stdout.match(/(\d+) commits? applied/);
    const rebasedCommits = commitsMatch ? parseInt(commitsMatch[1]!, 10) : 0;
    const rebaseResult = {
      success: !hasConflicts,
      conflicts: hasConflicts,
      conflictedFiles,
      rebasedCommits,
    };
    return rebaseResult;
  } catch (error) {
    throw mapGitError(error, 'rebase');
  }
}