import type { GitChangeContext, GitCommitResult } from "../types.ts"; import { collectRepositoryFingerprint } from "./collect-changes.ts"; import { GitClient } from "./git-client.ts"; /** 提交前只重新计算轻量指纹,并确认生成依据没有发生变化。 */ async function assertRepositoryUnchanged( git: GitClient, original: GitChangeContext, signal?: AbortSignal, ): Promise { const latest = await collectRepositoryFingerprint( git, original.root, original.requestedSource, signal, ); if ( latest.resolvedSource !== original.resolvedSource || latest.fingerprint !== original.fingerprint ) { throw new Error( "The repository changed after the commit message was generated. To avoid committing unreviewed changes, the commit was cancelled. Please regenerate the message.", ); } } /** 确保暂存区确实包含可提交变化。 */ async function assertIndexHasChanges( git: GitClient, root: string, signal?: AbortSignal, ): Promise { const result = await git.run(["diff", "--cached", "--quiet", "--exit-code", "--"], { cwd: root, signal, acceptedExitCodes: [0, 1], }); if (result.code === 0) { throw new Error("The staging area has no changes to commit."); } } /** 校验待提交消息的基本 Git 兼容性。 */ function validateCommitMessage(message: string): string { const normalized = message.trim(); if (!normalized) { throw new Error("The commit message must not be empty."); } if (normalized.includes("\0")) { throw new Error("The commit message must not contain NUL characters."); } return normalized; } /** 在用户确认后安全地暂存所需内容并创建本地提交。 */ export async function createGitCommit( git: GitClient, original: GitChangeContext, message: string, options?: { signoff?: boolean; signal?: AbortSignal }, ): Promise { const signal = options?.signal; const normalizedMessage = validateCommitMessage(message); await assertRepositoryUnchanged(git, original, signal); let stagedByExtension = false; if (original.resolvedSource === "all") { await git.run(["add", "-A", "--", "."], { cwd: original.root, signal, timeout: 30_000, }); stagedByExtension = true; } await assertIndexHasChanges(git, original.root, signal); const commitArgs = ["commit", "--message", normalizedMessage]; if (options?.signoff) { commitArgs.push("--signoff"); } let commitResult; try { commitResult = await git.run(commitArgs, { cwd: original.root, signal, timeout: 120_000, }); } catch (error) { if (stagedByExtension) { const detail = error instanceof Error ? error.message : String(error); throw new Error( `${detail} The extension staged changes in all mode, but the commit failed. The staged state was preserved to avoid altering your work.`, ); } throw error; } const hashResult = await git.run(["rev-parse", "--short", "HEAD"], { cwd: original.root, signal, }); return { hash: hashResult.stdout.trim(), output: commitResult.stdout.trim(), }; }