branch.ts•4.28 kB
/**
 * @fileoverview Git branch operations
 * @module services/git/providers/cli/operations/branches/branch
 */
import type { RequestContext } from '@/utils/index.js';
import type {
  GitBranchOptions,
  GitBranchResult,
  GitOperationContext,
} from '../../../../types.js';
import {
  buildGitCommand,
  GIT_FIELD_DELIMITER,
  mapGitError,
  parseBranchRef,
} from '../../utils/index.js';
/**
 * Execute git branch operations.
 */
export async function executeBranch(
  options: GitBranchOptions,
  context: GitOperationContext,
  execGit: (
    args: string[],
    cwd: string,
    ctx: RequestContext,
  ) => Promise<{ stdout: string; stderr: string }>,
): Promise<GitBranchResult> {
  try {
    const args: string[] = [];
    switch (options.mode) {
      case 'list': {
        // Build a custom format for git for-each-ref using field delimiters
        // This provides structured, machine-readable output that's more stable
        // than parsing the human-readable output of `git branch -v`
        const format = [
          '%(refname)', // Full ref name (e.g., refs/heads/main)
          '%(objectname)', // Commit hash
          '%(upstream:short)', // Upstream branch name (e.g., origin/main)
          '%(upstream:track)', // Tracking info (e.g., "ahead 1, behind 2")
          '%(HEAD)', // '*' for current branch
        ].join(GIT_FIELD_DELIMITER);
        // Choose the ref prefix based on whether we want remote or local branches
        const refPrefix = options.remote ? 'refs/remotes' : 'refs/heads';
        args.push(`--format=${format}`, refPrefix);
        // Add merge filtering if specified
        if (options.merged !== undefined) {
          const mergedRef =
            typeof options.merged === 'string' ? options.merged : 'HEAD';
          args.push(`--merged=${mergedRef}`);
        }
        if (options.noMerged !== undefined) {
          const noMergedRef =
            typeof options.noMerged === 'string' ? options.noMerged : 'HEAD';
          args.push(`--no-merged=${noMergedRef}`);
        }
        const cmd = buildGitCommand({ command: 'for-each-ref', args });
        const result = await execGit(
          cmd,
          context.workingDirectory,
          context.requestContext,
        );
        const branches = parseBranchRef(result.stdout);
        return {
          mode: 'list' as const,
          branches,
        };
      }
      case 'create': {
        if (!options.branchName) {
          throw new Error('Branch name is required for create operation');
        }
        args.push(options.branchName);
        if (options.startPoint) {
          args.push(options.startPoint);
        }
        if (options.force) {
          args.push('--force');
        }
        const cmd = buildGitCommand({ command: 'branch', args });
        await execGit(cmd, context.workingDirectory, context.requestContext);
        return {
          mode: 'create' as const,
          created: options.branchName,
        };
      }
      case 'delete': {
        if (!options.branchName) {
          throw new Error('Branch name is required for delete operation');
        }
        args.push(options.force ? '-D' : '-d', options.branchName);
        const cmd = buildGitCommand({ command: 'branch', args });
        await execGit(cmd, context.workingDirectory, context.requestContext);
        return {
          mode: 'delete' as const,
          deleted: options.branchName,
        };
      }
      case 'rename': {
        if (!options.branchName || !options.newBranchName) {
          throw new Error(
            'Both branch names are required for rename operation',
          );
        }
        args.push('-m', options.branchName, options.newBranchName);
        if (options.force) {
          args.push('--force');
        }
        const cmd = buildGitCommand({ command: 'branch', args });
        await execGit(cmd, context.workingDirectory, context.requestContext);
        return {
          mode: 'rename' as const,
          renamed: {
            from: options.branchName,
            to: options.newBranchName,
          },
        };
      }
      default:
        throw new Error('Unknown branch operation mode');
    }
  } catch (error) {
    throw mapGitError(error, 'branch');
  }
}