add commit.ts
This commit is contained in:
316
single-scripts/commit.ts
Executable file
316
single-scripts/commit.ts
Executable file
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env runts -- --runtime-bun
|
||||
|
||||
import {spawn} from "node:child_process";
|
||||
import {existsSync} from "node:fs";
|
||||
import {createInterface} from "node:readline/promises";
|
||||
import {stdin, stdout} from "node:process";
|
||||
|
||||
class ProcessOutput {
|
||||
code: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
|
||||
constructor(code: number, stdout: string, stderr: string) {
|
||||
this.code = code;
|
||||
this.stdout = stdout;
|
||||
this.stderr = stderr;
|
||||
}
|
||||
}
|
||||
|
||||
async function execCommandRequiresSuccess(
|
||||
command: string,
|
||||
args: string[],
|
||||
timeout?: number,
|
||||
): Promise<ProcessOutput> {
|
||||
const result = await execCommand(command, args, timeout);
|
||||
if (result.code !== 0) {
|
||||
throw new Error(
|
||||
`Exec command ${command} ${
|
||||
args.join(" ")
|
||||
} failed: ${result.code}\n- stdout: ${result.stdout}\n - stderr: ${result.stderr}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function execCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
timeout?: number,
|
||||
): Promise<ProcessOutput> {
|
||||
const ps = spawn(command, args, { env: { "LANG": "en_US" } });
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(`Exec command ${command} ${args.join(" ")} timeout`);
|
||||
}, timeout || 10000);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
ps.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
ps.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
ps.on("close", (code) => {
|
||||
try {
|
||||
const output = new ProcessOutput(code, stdout, stderr);
|
||||
ps.stdin.end();
|
||||
resolve(output);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
ps.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function execCommandShell(
|
||||
command: string,
|
||||
args: string[],
|
||||
): Promise<void> {
|
||||
const ps = spawn(command, args, {
|
||||
shell: true,
|
||||
stdio: ["inherit", "inherit", "inherit"],
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
ps.on("close", (code) => {
|
||||
try {
|
||||
ps.stdin.end();
|
||||
resolve(void 0);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
});
|
||||
ps.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function getGitRemoteRev(): Promise<string> {
|
||||
const currentBranch = await getGitCurrentBranch();
|
||||
const lsGitRemoteOrigin = await execCommandRequiresSuccess(
|
||||
"git",
|
||||
["ls-remote", "origin", currentBranch],
|
||||
);
|
||||
return lsGitRemoteOrigin.stdout.trim().split(/\s+/)[0].trim();
|
||||
}
|
||||
|
||||
async function getGitCurrentBranch(): Promise<string> {
|
||||
const gitCurrentBranch = await execCommandRequiresSuccess("git", [
|
||||
"branch",
|
||||
"--show-current",
|
||||
]);
|
||||
return gitCurrentBranch.stdout.trim();
|
||||
}
|
||||
|
||||
async function getGitLocalRev(): Promise<string> {
|
||||
const gitLocalRev = await execCommandRequiresSuccess("git", [
|
||||
"rev-parse",
|
||||
"HEAD",
|
||||
]);
|
||||
return gitLocalRev.stdout.trim();
|
||||
}
|
||||
|
||||
class GitStatusResult {
|
||||
newfile: string[];
|
||||
modified: string[];
|
||||
deleted: string[];
|
||||
untracked: string[];
|
||||
constructor(
|
||||
newfile: string[],
|
||||
modified: string[],
|
||||
deleted: string[],
|
||||
untracked: string[],
|
||||
) {
|
||||
this.newfile = newfile;
|
||||
this.modified = modified;
|
||||
this.deleted = deleted;
|
||||
this.untracked = untracked;
|
||||
}
|
||||
}
|
||||
|
||||
// On branch main
|
||||
// Your branch is up to date with 'origin/main'.
|
||||
//
|
||||
// nothing to commit, working tree clean
|
||||
// ----------------------------------------------------------------------
|
||||
// On branch main
|
||||
// Your branch is up to date with 'origin/main'.
|
||||
//
|
||||
// Changes to be committed:
|
||||
// (use "git restore --staged <file>..." to unstage)
|
||||
// new file: single-scripts/commit.ts
|
||||
// ----------------------------------------------------------------------
|
||||
// On branch main
|
||||
// Your branch is up to date with 'origin/main'.
|
||||
//
|
||||
// Changes not staged for commit:
|
||||
// (use "git add/rm <file>..." to update what will be committed)
|
||||
// (use "git restore <file>..." to discard changes in working directory)
|
||||
// modified: README.md
|
||||
// deleted: script-config.json
|
||||
//
|
||||
// Untracked files:
|
||||
// (use "git add <file>..." to include in what will be committed)
|
||||
// single-scripts/commit.ts
|
||||
//
|
||||
// no changes added to commit (use "git add" and/or "git commit -a")
|
||||
async function getGitStatus(): Promise<GitStatusResult | null> {
|
||||
const newfile: string[] = [];
|
||||
const modified: string[] = [];
|
||||
const deleted: string[] = [];
|
||||
const untracked: string[] = [];
|
||||
|
||||
const gitStatus = await execCommandRequiresSuccess("git", ["status"]);
|
||||
const gitStatusStdout = gitStatus.stdout.trim();
|
||||
if (gitStatusStdout.includes("nothing to commit, working tree clean")) {
|
||||
return null;
|
||||
}
|
||||
let inChangesNotStaged = false;
|
||||
let inChangesToBeCommited = false;
|
||||
let inUntrackedFiles = false;
|
||||
gitStatusStdout.split("\n").forEach((line) => {
|
||||
if (
|
||||
line.startsWith("Changes to be committed:") ||
|
||||
line.startsWith("Changes not staged for commit:")
|
||||
) {
|
||||
inChangesNotStaged = true;
|
||||
inUntrackedFiles = false;
|
||||
return;
|
||||
}
|
||||
if (line.startsWith("Untracked files:")) {
|
||||
inChangesNotStaged = false;
|
||||
inUntrackedFiles = true;
|
||||
return;
|
||||
}
|
||||
if (inChangesNotStaged) {
|
||||
const modifiedOrDeletedLine = line.trim();
|
||||
if (modifiedOrDeletedLine.startsWith("new file:")) {
|
||||
const newFile = modifiedOrDeletedLine.substring(
|
||||
"new file:".length,
|
||||
).trim();
|
||||
newfile.push(newFile);
|
||||
} else if (modifiedOrDeletedLine.startsWith("modified:")) {
|
||||
const modifiedFile = modifiedOrDeletedLine.substring(
|
||||
"modified:".length,
|
||||
).trim();
|
||||
modified.push(modifiedFile);
|
||||
} else if (modifiedOrDeletedLine.startsWith("deleted:")) {
|
||||
const deletedFile = modifiedOrDeletedLine.substring(
|
||||
"deleted:".length,
|
||||
).trim();
|
||||
deleted.push(deletedFile);
|
||||
}
|
||||
}
|
||||
if (inUntrackedFiles) {
|
||||
const newFile = line.trim();
|
||||
if (existsSync(newFile)) {
|
||||
untracked.push(newFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (
|
||||
newfile.length === 0 && modified.length === 0 && deleted.length === 0 &&
|
||||
untracked.length === 0
|
||||
) {
|
||||
// WILL THIS HAPPEN?
|
||||
return null;
|
||||
}
|
||||
return new GitStatusResult(newfile, modified, deleted, untracked);
|
||||
}
|
||||
|
||||
class ProcessBar {
|
||||
interval: number;
|
||||
message: string;
|
||||
constructor(message?: string) {
|
||||
this.message = message || "Processing";
|
||||
}
|
||||
start(): void {
|
||||
const startMs = new Date().getTime();
|
||||
let count = 0;
|
||||
this.interval = setInterval(() => {
|
||||
const dots = ".".repeat(((count++) % 10) + 1);
|
||||
const costMs = new Date().getTime() - startMs;
|
||||
let time = `${costMs}ms`;
|
||||
if (costMs > 1000) {
|
||||
time = parseInt(costMs / 1000) + "s";
|
||||
}
|
||||
process.stdout.write(`\r${this.message} ${time} ${dots}\x1b[K`);
|
||||
}, 200);
|
||||
}
|
||||
stop(clearLine?: boolean): void {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
process.stdout.write(clearLine ? "\r" : "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkRev(): Promise<boolean> {
|
||||
const processBar = new ProcessBar("Checking rev");
|
||||
processBar.start();
|
||||
const localRev = await getGitLocalRev();
|
||||
const remoteRev = await getGitRemoteRev();
|
||||
processBar.stop();
|
||||
if (localRev === remoteRev) {
|
||||
console.log(`Check rev successfully, rev: ${localRev}`);
|
||||
} else {
|
||||
throw `Check rev failed, local rev: ${localRev} vs remote rev: ${remoteRev}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await checkRev(); // check local rev <--> remote rev equals
|
||||
const gitStatus = await getGitStatus();
|
||||
if (gitStatus === null) {
|
||||
console.log("No git status found");
|
||||
return;
|
||||
}
|
||||
console.log("Git status:", gitStatus);
|
||||
|
||||
let emptyCount = 0;
|
||||
let message = "empty message";
|
||||
const readline = createInterface({ input: stdin, output: stdout });
|
||||
do {
|
||||
if (emptyCount++ === 3) {
|
||||
readline.close();
|
||||
console.log("Too many empty messages, then exit");
|
||||
return;
|
||||
}
|
||||
message = await readline.question("Input your commit message> ");
|
||||
} while (message.length == 0);
|
||||
readline.close();
|
||||
|
||||
const gitCommitArgs: string[] = [];
|
||||
gitCommitArgs.push("commit");
|
||||
for (const file of gitStatus.modified) {
|
||||
gitCommitArgs.push(file);
|
||||
}
|
||||
for (const file of gitStatus.deleted) {
|
||||
gitCommitArgs.push(file);
|
||||
}
|
||||
for (const file of gitStatus.untracked) {
|
||||
gitCommitArgs.push(file);
|
||||
}
|
||||
gitCommitArgs.push("-m");
|
||||
gitCommitArgs.push(message);
|
||||
|
||||
console.log(gitCommitArgs);
|
||||
await execCommandShell("git", gitCommitArgs);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`[ERROR] ${err}`);
|
||||
process.exit(0);
|
||||
}).then(() => process.exit(0));
|
||||
|
||||
// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260125T125926+08:00.MEYCIQDyxV26TTjKJCjDH1Pb
|
||||
// s36IR6JZ4biD/G27wdrxtnQQYQIhAMVSIGkLWOd8dASIayCKeZ0UlfYJN3rercqJKqzRf0Cm
|
||||
Reference in New Issue
Block a user