← back to Letsbegin

lib/git.ts

123 lines

/**
 * Git integration for auto-committing on successful steps
 */

import { exec } from 'child_process'
import { promisify } from 'util'

const execAsync = promisify(exec)

interface CommitResult {
  success: boolean
  message: string
  sha?: string
}

export async function autoCommit(
  projectPath: string,
  message: string,
  files?: string[]
): Promise<CommitResult> {
  try {
    // Check if it's a git repo
    try {
      await execAsync(`cd "${projectPath}" && git rev-parse --is-inside-work-tree`)
    } catch {
      return { success: false, message: 'Not a git repository' }
    }

    // Stage files
    if (files && files.length > 0) {
      for (const file of files) {
        await execAsync(`cd "${projectPath}" && git add "${file}"`)
      }
    } else {
      // Stage all changes
      await execAsync(`cd "${projectPath}" && git add -A`)
    }

    // Check if there are changes to commit
    const { stdout: status } = await execAsync(`cd "${projectPath}" && git status --porcelain`)
    if (!status.trim()) {
      return { success: true, message: 'No changes to commit' }
    }

    // Create commit
    const commitMessage = `${message}\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>`
    const { stdout } = await execAsync(
      `cd "${projectPath}" && git commit -m "${commitMessage.replace(/"/g, '\\"')}"`,
      { maxBuffer: 10 * 1024 * 1024 }
    )

    // Get commit SHA
    const { stdout: sha } = await execAsync(`cd "${projectPath}" && git rev-parse --short HEAD`)

    return {
      success: true,
      message: 'Committed successfully',
      sha: sha.trim()
    }
  } catch (error) {
    console.error('Git commit error:', error)
    return {
      success: false,
      message: error instanceof Error ? error.message : 'Unknown error'
    }
  }
}

export async function createBranch(projectPath: string, branchName: string): Promise<CommitResult> {
  try {
    // Check if branch exists
    try {
      await execAsync(`cd "${projectPath}" && git show-ref --verify --quiet refs/heads/${branchName}`)
      // Branch exists, checkout
      await execAsync(`cd "${projectPath}" && git checkout ${branchName}`)
      return { success: true, message: `Switched to existing branch: ${branchName}` }
    } catch {
      // Branch doesn't exist, create it
      await execAsync(`cd "${projectPath}" && git checkout -b ${branchName}`)
      return { success: true, message: `Created and switched to branch: ${branchName}` }
    }
  } catch (error) {
    console.error('Git branch error:', error)
    return {
      success: false,
      message: error instanceof Error ? error.message : 'Unknown error'
    }
  }
}

export async function getGitStatus(projectPath: string): Promise<{
  isRepo: boolean
  branch: string | null
  hasChanges: boolean
  changedFiles: string[]
}> {
  try {
    // Check if it's a git repo
    await execAsync(`cd "${projectPath}" && git rev-parse --is-inside-work-tree`)

    // Get current branch
    const { stdout: branch } = await execAsync(`cd "${projectPath}" && git branch --show-current`)

    // Get changed files
    const { stdout: status } = await execAsync(`cd "${projectPath}" && git status --porcelain`)
    const changedFiles = status.trim().split('\n').filter(Boolean).map(line => line.slice(3))

    return {
      isRepo: true,
      branch: branch.trim() || 'detached',
      hasChanges: changedFiles.length > 0,
      changedFiles
    }
  } catch {
    return {
      isRepo: false,
      branch: null,
      hasChanges: false,
      changedFiles: []
    }
  }
}