UNPKG

paqad-ai

Version:

Spec-driven development framework — AI agents that think before they type

149 lines (146 loc) • 5.4 kB
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url); import { readGitState } from "./chunk-BOEVU2BD.js"; import { detectTicketRefs, parseBranchTickets } from "./chunk-NJE7NS7E.js"; import { init_esm_shims } from "./chunk-IDFRWBMN.js"; // src/delivery/commit-split.ts init_esm_shims(); import { execFileSync } from "child_process"; function git(projectRoot, args) { try { return execFileSync("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 }).trim(); } catch { return void 0; } } function emptyGroup() { return { count: 0, tickets: [], merges: 0, untagged: 0 }; } function unknownSplit(reason) { return { state: "unknown", reason, base_commit: null, own: null, inherited: null }; } var FIELD_SEPARATOR = String.fromCharCode(31); function isPullRequestRef(ref) { return /^#\d+$/.test(ref.trim()); } function readBranchCommits(projectRoot, baseCommit) { const out = git(projectRoot, ["log", `${baseCommit}..HEAD`, "--format=%H%x1f%P%x1f%s"]) ?? ""; return out.split("\n").filter(Boolean).map((line) => { const [sha, parents = "", ...subject] = line.split(FIELD_SEPARATOR); return { sha, parents: parents.split(" ").filter(Boolean).length, tickets: detectTicketRefs(subject.join(FIELD_SEPARATOR), "generic").filter( (ref) => !isPullRequestRef(ref) ) }; }); } function commitsBeforeStart(projectRoot, baseCommit, startCommit) { if (!startCommit) return null; const out = git(projectRoot, ["rev-list", `${baseCommit}..${startCommit}`]); if (out === void 0) return null; return new Set(out.split("\n").filter(Boolean)); } function addTo(group, commit, tickets) { group.count += 1; if (commit.parents > 1) group.merges += 1; if (commit.tickets.length === 0) group.untagged += 1; for (const ticket of tickets) { if (!group.tickets.includes(ticket)) group.tickets.push(ticket); } } function changeOwnTickets(issue, branch) { const fromIssue = issue ? detectTicketRefs(issue, "generic") : []; return fromIssue.length > 0 ? fromIssue : parseBranchTickets(branch); } function computeCommitSplit(projectRoot, input = {}) { const state = readGitState(projectRoot, { baseBranch: input.baseBranch }); if (!state.head_commit) return unknownSplit("not a git repository"); if (!state.branch) return unknownSplit("detached HEAD"); if (!state.base_commit) return unknownSplit("no reachable base branch"); const own = new Set(input.ownTickets ?? []); const beforeStart = commitsBeforeStart(projectRoot, state.base_commit, input.startCommit); const split = { own: emptyGroup(), inherited: emptyGroup() }; const ownShas = []; const inheritedShas = []; for (const commit of readBranchCommits(projectRoot, state.base_commit)) { const ours = commit.tickets.some((ticket) => own.has(ticket)); const foreign = own.size > 0 && commit.tickets.length > 0 && !ours; const afterStart = beforeStart === null || !beforeStart.has(commit.sha); if (ours || !foreign && afterStart) { addTo(split.own, commit, commit.tickets); ownShas.push(commit.sha); } else { inheritedShas.push(commit.sha); addTo( split.inherited, commit, commit.tickets.filter((ticket) => !own.has(ticket)) ); } } for (const group of [split.own, split.inherited]) group.tickets.sort(byTicket); return { state: "known", reason: null, base_commit: state.base_commit, own: split.own, inherited: split.inherited, own_shas: ownShas, inherited_shas: inheritedShas }; } function branchCarriesOtherWork(split) { return split?.state === "known" && (split.inherited?.tickets.length ?? 0) > 0; } function commitCount(count) { return `${count} commit${count === 1 ? "" : "s"}`; } function byTicket(a, b) { return a.localeCompare(b, "en", { numeric: true }); } function describeEarlierWork(group) { if (group.count === 0) return "no commits from earlier work"; const tickets = group.tickets.length; const named = tickets === 0 ? "" : ` on ticket${tickets === 1 ? "" : "s"} ${group.tickets.join(", ")}`; return `${commitCount(group.count)} from earlier work${named}`; } function describeInherited(group) { const tickets = group.tickets.length; if (tickets === 0) return `${commitCount(group.count)} made before this change started`; const named = `ticket${tickets === 1 ? "" : "s"} ${group.tickets.join(", ")}`; return `${commitCount(group.count)} from ${named} made before this change started`; } function sharedBranchLine(split, branch) { if (!branchCarriesOtherWork(split) || !split.inherited) return null; return `\u{1F7E1} Branch \`${branch ?? "HEAD"}\` already carries ${describeInherited(split.inherited)}. They ride along in this PR unless you split the branch yourself; paqad never moves commits.`; } function intakeBranchLine(projectRoot, ticketRefs) { const state = readGitState(projectRoot); const split = computeCommitSplit(projectRoot, { startCommit: state.head_commit ?? null, ownTickets: ticketRefs }); return sharedBranchLine(split, state.branch ?? null); } export { changeOwnTickets, computeCommitSplit, branchCarriesOtherWork, describeEarlierWork, describeInherited, sharedBranchLine, intakeBranchLine };