Files
blog/write-server/src/lib/git.ts
T
2026-06-21 20:42:44 +08:00

138 lines
4.2 KiB
TypeScript

import { execFile } from "child_process";
import { promisify } from "util";
import { BLOG_ROOT } from "./config";
const execFileAsync = promisify(execFile);
type GitExecError = Error & {
stdout?: string;
stderr?: string;
code?: number;
};
export interface DeployResult {
success: boolean;
output?: string;
error?: string;
conflict?: boolean;
}
interface QueueItem {
message: string;
resolve: (result: DeployResult) => void;
reject: (err: Error) => void;
}
class GitDeployQueue {
private queue: QueueItem[] = [];
private running = false;
async enqueue(commitMessage: string): Promise<DeployResult> {
return new Promise<DeployResult>((resolve, reject) => {
this.queue.push({ message: commitMessage, resolve, reject });
if (!this.running) this.next();
});
}
async sync(): Promise<DeployResult> {
try {
const result = await this.runGit(["pull", "--rebase", "--autostash", "origin", "main"]);
return { success: true, output: result.stdout + result.stderr };
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (this.isRebaseConflict(msg)) {
await this.runGit(["rebase", "--abort"], true);
return { success: false, conflict: true, error: "远程冲突,已放弃合并" };
}
return { success: false, error: msg };
}
}
private async next(): Promise<void> {
const item = this.queue.shift();
if (!item) {
this.running = false;
return;
}
this.running = true;
try {
const result = await this.execute(item.message);
item.resolve(result);
} catch (e) {
item.reject(e instanceof Error ? e : new Error(String(e)));
}
this.next();
}
private async runGit(args: string[], allowFailure = false) {
try {
const { stdout, stderr } = await execFileAsync("git", args, {
cwd: BLOG_ROOT,
windowsHide: true,
maxBuffer: 1024 * 1024,
});
return { stdout, stderr, code: 0 };
} catch (error) {
const err = error as GitExecError;
const result = {
stdout: err.stdout || "",
stderr: err.stderr || err.message,
code: typeof err.code === "number" ? err.code : 1,
};
if (allowFailure) return result;
throw new Error((result.stderr || result.stdout || err.message).trim());
}
}
private isRebaseConflict(message: string): boolean {
return /conflict|CONFLICT|Resolve all conflicts|Automatic merge failed|could not apply|rebase/i.test(message);
}
private async execute(message: string): Promise<DeployResult> {
let output = "";
// git add
const addResult = await this.runGit(["add", "content"]);
output += addResult.stdout + addResult.stderr;
// git diff --cached --quiet (exit 1 = staged changes exist)
const diffResult = await this.runGit(["diff", "--cached", "--quiet"], true);
if (diffResult.code === 1) {
const commitResult = await this.runGit(["commit", "-m", message]);
output += commitResult.stdout + commitResult.stderr;
} else if (diffResult.code !== 0) {
return { success: false, error: (diffResult.stderr || diffResult.stdout || "检查暂存区失败").trim() };
}
// git pull --rebase --autostash
try {
const pullResult = await this.runGit(["pull", "--rebase", "--autostash", "origin", "main"]);
output += pullResult.stdout + pullResult.stderr;
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (this.isRebaseConflict(msg)) {
await this.runGit(["rebase", "--abort"], true);
return {
success: false,
conflict: true,
error: "远程有冲突,已自动放弃合并。请先 /sync 同步,手动解决后再 deploy。",
output: msg.slice(0, 1000),
};
}
return { success: false, error: msg };
}
// git push
try {
const pushResult = await this.runGit(["push"]);
output += pushResult.stdout + pushResult.stderr;
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
return { success: true, output: output.slice(0, 1000) };
}
}
export const deployQueue = new GitDeployQueue();