/**
* 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
}