import { spawn } from "node:child_process";

function run(cmd, args, opts = {}) {
  return new Promise((resolve, reject) => {
    let out = "";
    let err = "";
    const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...opts });
    child.stdout.on("data", (b) => (out += b));
    child.stderr.on("data", (b) => (err += b));
    child.on("error", reject);
    child.on("close", (code) =>
      code === 0
        ? resolve(out)
        : reject(new Error(`git ${args.join(" ")} exited ${code}: ${err.trim()}`))
    );
  });
}

/**
 * Stage the given paths (relative to projectRoot) and commit if anything changed.
 * Best-effort: never throws, so a git hiccup can't block a live publish. Returns
 * the new commit hash, or null if nothing was staged / git failed.
 * Keeping the working tree committed is what lets the console's git worktree +
 * `merge --ff-only` operations run against a clean tree.
 */
export async function gitCommitPaths(projectRoot, paths, message) {
  try {
    await run("git", ["-C", projectRoot, "add", "--", ...paths]);
    const staged = await run("git", ["-C", projectRoot, "diff", "--cached", "--name-only"]);
    if (!staged.trim()) return null;
    await run("git", ["-C", projectRoot, "commit", "-q", "-m", message]);
    return (await run("git", ["-C", projectRoot, "rev-parse", "HEAD"])).trim();
  } catch (e) {
    console.warn(`[git] commit skipped: ${e.message}`);
    return null;
  }
}
