138 lines
4.2 KiB
TypeScript
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();
|