UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

156 lines (155 loc) • 7.8 kB
import { resolveStoreConflicts } from "./conflict-resolve.js"; import { gitOperationRecovery, inProgressGitOperation, storeAutoCommitBlocker } from "./git-state.js"; import { storeCommitMessage } from "../machine-identity.js"; /** Pre-merge HEADs, one ref per diverged merge, named by the time it was taken (store-cleanup.ts prunes them). */ export const BACKUP_REF_PREFIX = "refs/phren/backup/"; /** The staged diff, added lines only, for `storeAutoCommitBlocker`. */ export const STAGED_DIFF = ["diff", "--cached", "--no-color", "--no-ext-diff", "-U0"]; const SENSITIVE_STORE_PATHSPECS = [".env", "**/.env", "*.pem", "*.key", ".config/auth-profiles.json"]; async function commitLocalStoreWrites(cwd, git, message) { const add = await git(cwd, ["add", "--sparse", "-A"]); if (!add.ok) return { committed: false, error: add.error || "git add failed" }; await git(cwd, ["reset", "HEAD", "--", ...SENSITIVE_STORE_PATHSPECS]); const staged = await git(cwd, ["diff", "--cached", "--name-only"]); if (!staged.ok) return { committed: false, error: staged.error || "git diff failed" }; if (!staged.output) return { committed: false }; const blocked = await storeAutoCommitBlocker(cwd, () => git(cwd, STAGED_DIFF)); if (blocked) return { committed: false, error: blocked }; const commit = await git(cwd, ["-c", "commit.gpgsign=false", "commit", "-m", storeCommitMessage(message)]); return commit.ok ? { committed: true } : { committed: false, error: commit.error || "git commit failed" }; } async function conflictedPaths(cwd, git) { const result = await git(cwd, ["diff", "--name-only", "--diff-filter=U"]); return result.ok ? result.output.split("\n").map((entry) => entry.trim()).filter(Boolean) : []; } async function abortMerge(cwd, git) { if (inProgressGitOperation(cwd) !== "merge") return true; const aborted = await git(cwd, ["merge", "--abort"]); if (aborted.ok) return true; const reset = await git(cwd, ["reset", "--merge", "ORIG_HEAD"]); return reset.ok && inProgressGitOperation(cwd) !== "merge"; } /** * Fetch and integrate a store's configured upstream without rebasing local * commits. Conflicts in tasks.md (per task id against the merge base), the * generated summary and topic blocks, findings and the task archive resolve * automatically (see conflict-resolve.ts). Any other conflict aborts the * merge and names its files. */ export async function mergeStoreUpstream(cwd, options) { const { git } = options; const operation = inProgressGitOperation(cwd); if (operation) { const recovery = gitOperationRecovery(operation); return { status: "busy", detail: `The store already has a Git operation in progress (${operation}). Run '${recovery}' in the store before syncing again.`, }; } const branch = await git(cwd, ["symbolic-ref", "--quiet", "HEAD"]); if (!branch.ok) return { status: "error", detail: "No branch is checked out." }; let committedLocalWrites = false; if (options.commitLocalWrites !== false) { const saved = await commitLocalStoreWrites(cwd, git, options.commitMessage ?? "auto-save phren (store sync)"); if (saved.error) return { status: "error", detail: `Cannot commit local store writes: ${saved.error}` }; committedLocalWrites = saved.committed; } const refs = await git(cwd, ["for-each-ref", "--format=%(upstream:remotename)%09%(upstream:remoteref)%09%(upstream)", branch.output]); const [remote, remoteRef, trackingRef] = refs.output.split("\t"); if (!refs.ok || !remote || !remoteRef || !trackingRef) { return { status: "unchanged", detail: committedLocalWrites ? "Committed local store writes; no tracking remote is configured." : "No tracking remote is configured.", committedLocalWrites, }; } const tracked = await git(cwd, ["rev-parse", "--verify", trackingRef]); if (!options.advertisedHead || !tracked.ok || tracked.output !== options.advertisedHead) { const fetched = await git(cwd, ["fetch", "--quiet", "--no-tags", "--no-recurse-submodules", "--", remote, `${remoteRef}:${trackingRef}`]); if (!fetched.ok) return { status: "error", detail: `Fetch failed: ${fetched.error || "unknown Git error"}`, committedLocalWrites }; } const target = await git(cwd, ["rev-parse", "--verify", trackingRef]); if (!target.ok || !target.output) return { status: "error", detail: "Cannot read the fetched tracking branch.", committedLocalWrites }; if ((await git(cwd, ["merge-base", "--is-ancestor", target.output, "HEAD"])).ok) { return { status: "unchanged", detail: committedLocalWrites ? "Committed local store writes; the store already contains the remote changes." : "The store already contains the remote changes.", committedLocalWrites, }; } const canFastForward = (await git(cwd, ["merge-base", "--is-ancestor", "HEAD", target.output])).ok; // Both sides moved: keep the local history reachable before merging into it. let backupRef; if (!canFastForward) { const ref = `${BACKUP_REF_PREFIX}${Date.now()}`; if (!(await git(cwd, ["update-ref", ref, "HEAD"])).ok) { return { status: "error", detail: "Cannot keep a backup of the store's history before merging.", committedLocalWrites }; } backupRef = ref; } const merged = await git(cwd, canFastForward ? ["merge", "--ff-only", target.output] : ["-c", "commit.gpgsign=false", "merge", "--no-edit", target.output]); if (merged.ok) { return { status: "updated", detail: canFastForward ? "Store fast-forwarded to the remote." : "Store merged with the remote.", committedLocalWrites, ...(backupRef ? { backupRef } : {}), }; } const initialConflicts = await conflictedPaths(cwd, git); if (initialConflicts.length === 0 && inProgressGitOperation(cwd) !== "merge") { return { status: "error", detail: `Merge failed before it started: ${merged.error || "unknown Git error"}`, committedLocalWrites, }; } if (initialConflicts.length > 0 && resolveStoreConflicts(cwd).unresolved.length === 0) { const commit = await git(cwd, ["-c", "commit.gpgsign=false", "commit", "--no-edit"]); if (commit.ok) { return { status: "updated", detail: `Store merged with the remote; resolved conflicts in ${initialConflicts.join(", ")}.`, conflicts: initialConflicts, committedLocalWrites, ...(backupRef ? { backupRef } : {}), }; } } const unresolvedConflicts = await conflictedPaths(cwd, git); const conflicts = unresolvedConflicts.length > 0 ? unresolvedConflicts : initialConflicts; const cleaned = await abortMerge(cwd, git); if (!cleaned) { return { status: "error", detail: `Merge cleanup failed${conflicts.length ? ` after conflicts in: ${conflicts.join(", ")}` : ""}. Run 'git merge --abort' in the store.`, conflicts, committedLocalWrites, }; } if (conflicts.length > 0) { return { status: "conflict", detail: `Merge aborted; manual resolution is required for: ${conflicts.join(", ")}`, conflicts, committedLocalWrites, }; } return { status: "error", detail: `Merge failed and was aborted: ${merged.error || "unknown Git error"}`, committedLocalWrites, }; }