OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / cli / git.js

102 lines · javascript ref:
/**
 * Shared git helpers for pgh commands
 */

import subprocess from 'bare-subprocess'

/**
 * Run a git command and capture its output
 * @param {string[]} args - Git arguments
 * @param {string} cwd - Working directory
 * @returns {Promise<string>} stdout trimmed
 */
export async function gitExec (args, cwd) {
  const { spawn } = subprocess
  return new Promise((resolve, reject) => {
    const proc = spawn('git', args, { cwd })
    const chunks = []
    const errChunks = []
    proc.stdout.on('data', (d) => chunks.push(d))
    proc.stderr.on('data', (d) => errChunks.push(d))
    proc.on('close', (code) => {
      const stdout = Buffer.concat(chunks).toString().trim()
      const stderr = Buffer.concat(errChunks).toString().trim()
      if (code === 0) resolve(stdout)
      else reject(new Error(stderr || `git exited with code ${code}`))
    })
  })
}

/**
 * Run a git command and stream stdout/stderr live
 * @param {string[]} args - Git arguments
 * @param {string} cwd - Working directory
 * @returns {Promise<void>} Resolves on success, rejects on non-zero exit
 */
export async function gitSpawn (args, cwd) {
  const proc = subprocess.spawn('git', args, { cwd })
  proc.stdout.on('data', (d) => process.stdout.write(d))
  proc.stderr.on('data', (d) => process.stderr.write(d))
  return new Promise((resolve, reject) => {
    proc.on('close', (code) => {
      if (code === 0) resolve()
      else reject(new Error(`git ${args[0]} exited with code ${code}`))
    })
  })
}

/**
 * Find a pear-related remote in the current repo
 * Checks for pear://, punch://, git+pear:// URLs, or a remote named 'pear'
 * @param {string} cwd - Working directory (git repo)
 * @returns {Promise<string|null>} Remote name or null
 */
export async function findPearRemote (cwd) {
  const output = await gitExec(['remote', '-v'], cwd)
  for (const line of output.split('\n')) {
    const [name, url] = line.split(/\s+/)
    if (url && (url.startsWith('pear://') || url.startsWith('punch://') || url.startsWith('git+pear://'))) return name
  }
  // Fall back to a remote named 'pear'
  try {
    await gitExec(['remote', 'get-url', 'pear'], cwd)
    return 'pear'
  } catch {
    return null
  }
}

/**
 * Parse .gitmodules content, extracting @pear markers
 * @param {string} content - Raw .gitmodules file content
 * @returns {Array<{name: string, path: string|null, url: string|null, pear: string|null}>}
 */
export function parseGitmodules (content) {
  const submodules = []
  let current = null

  for (const line of content.split('\n')) {
    const submoduleMatch = line.match(/^\[submodule\s+"([^"]+)"\]/)
    if (submoduleMatch) {
      if (current) submodules.push(current)
      current = { name: submoduleMatch[1], path: null, url: null, pear: null }
      continue
    }

    if (!current) continue

    const pathMatch = line.match(/^\s*path\s*=\s*(.+)$/)
    if (pathMatch) current.path = pathMatch[1].trim()

    const urlMatch = line.match(/^\s*url\s*=\s*(.+)$/)
    if (urlMatch) current.url = urlMatch[1].trim()

    // Look for @pear marker in comments
    const pearMatch = line.match(/^\s*#\s*@pear:\s*(.+)$/)
    if (pearMatch) current.pear = pearMatch[1].trim()
  }

  if (current) submodules.push(current)
  return submodules
}