diff --git a/single-scripts/commit-bun.ts b/single-scripts/commit-bun.ts new file mode 100755 index 0000000..356f4b8 --- /dev/null +++ b/single-scripts/commit-bun.ts @@ -0,0 +1,337 @@ +#!/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 { + 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 { + 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 { + const ps = spawn(command, args, { + // shell: true, // when turn `shell` true will cause shell command line injection + 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 { + 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 { + const gitCurrentBranch = await execCommandRequiresSuccess("git", [ + "branch", + "--show-current", + ]); + return gitCurrentBranch.stdout.trim(); +} + +async function getGitLocalRev(): Promise { + 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 ..." 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 ..." to update what will be committed) +// (use "git restore ..." to discard changes in working directory) +// modified: README.md +// deleted: script-config.json +// +// Untracked files: +// (use "git add ..." 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 { + 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 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"; + } + async call(cb: () => Promise, clearLine?: boolean): Promise { + this.start(); + try { + return await cb(); + } finally { + this.stop(clearLine); + } + } + 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; + const time = `${Math.floor(costMs / 1000)}s`; + process.stderr.write(`\r${this.message} ${time} ${dots}\x1b[K`); + }, 500); + } + stop(clearLine?: boolean): void { + if (this.interval) { + clearInterval(this.interval); + process.stderr.write(clearLine ? "\r\x1b[K" : "\n"); + } + } +} + +function red(message: string): string { + return `\x1b[31m${message}\x1b[0m`; +} + +function green(message: string): string { + return `\x1b[32m${message}\x1b[0m`; +} + +async function checkRev(): Promise { + const localRev = await getGitLocalRev(); + const remoteRev = await new ProcessBar("Checking rev").call( + async (): Promise => { + return await getGitRemoteRev(); + }, + ); + if (localRev === remoteRev) { + console.log(green(`Check rev successfully, rev: ${localRev}`)); + } else { + throw `Check rev failed, local rev: ${localRev} vs remote rev: ${remoteRev}`; + } +} + +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); + + if (gitStatus.untracked.length > 0) { + const gitAddArgs = ["add"]; + for (const file of gitStatus.untracked) { + gitAddArgs.push(file); + } + console.log(">>>>>", ["git", gitAddArgs]); + await execCommandShell("git", gitAddArgs); + } + console.log(">>>>>", ["git", gitCommitArgs]); + await execCommandShell("git", gitCommitArgs); + console.log(">>>>>", ["git", "push"]); + await new ProcessBar("Git pushing").call(async (): Promise => { + await execCommandShell("git", ["push"]); + }); +} + +main().catch((err) => { + console.error(red(`[ERROR] ${err}`)); + process.exit(0); +}).then(() => process.exit(0)); + +// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260128T224430+08:00.MEYCIQCBwZf7i4TLaloGJMI2 +// 1lEGJACKU3pCRDUTXlU2AY5pVgIhANRpDg85DelQ5jyXlaIIDLQnrm9XRUFEQH/me/daYtTN diff --git a/single-scripts/commit.ts b/single-scripts/commit.ts index 356f4b8..a9aa8bb 100755 --- a/single-scripts/commit.ts +++ b/single-scripts/commit.ts @@ -1,117 +1,36 @@ -#!/usr/bin/env runts -- --runtime-bun +#!/usr/bin/env runts -- --allow-import --allow-run --allow-read -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 { - 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 { - 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 { - const ps = spawn(command, args, { - // shell: true, // when turn `shell` true will cause shell command line injection - 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); - }); - }); -} +import {execCommand, execCommandShell, log, ProcessBar, term,} from "../libraries/deno-commons-mod.ts"; async function getGitRemoteRev(): Promise { const currentBranch = await getGitCurrentBranch(); - const lsGitRemoteOrigin = await execCommandRequiresSuccess( - "git", - ["ls-remote", "origin", currentBranch], - ); + const lsGitRemoteOrigin = await execCommand("git", [ + "ls-remote", + "origin", + currentBranch, + ]); + lsGitRemoteOrigin.assertSuccess(); return lsGitRemoteOrigin.stdout.trim().split(/\s+/)[0].trim(); } async function getGitCurrentBranch(): Promise { - const gitCurrentBranch = await execCommandRequiresSuccess("git", [ + const gitCurrentBranch = await execCommand("git", [ "branch", "--show-current", ]); + gitCurrentBranch.assertSuccess(); return gitCurrentBranch.stdout.trim(); } async function getGitLocalRev(): Promise { - const gitLocalRev = await execCommandRequiresSuccess("git", [ + const gitLocalRev = await execCommand("git", [ "rev-parse", "HEAD", ]); + gitLocalRev.assertSuccess(); return gitLocalRev.stdout.trim(); } @@ -165,7 +84,8 @@ async function getGitStatus(): Promise { const deleted: string[] = []; const untracked: string[] = []; - const gitStatus = await execCommandRequiresSuccess("git", ["status"]); + const gitStatus = await execCommand("git", ["status"]); + gitStatus.assertSuccess(); const gitStatusStdout = gitStatus.stdout.trim(); if (gitStatusStdout.includes("nothing to commit, working tree clean")) { return null; @@ -207,7 +127,7 @@ async function getGitStatus(): Promise { } if (inUntrackedFiles) { const newFile = line.trim(); - if (existsSync(newFile)) { + if (newFile && existsSync(newFile)) { untracked.push(newFile); } } @@ -222,46 +142,6 @@ async function getGitStatus(): Promise { return new GitStatusResult(newfile, modified, deleted, untracked); } -class ProcessBar { - interval: number; - message: string; - constructor(message?: string) { - this.message = message || "Processing"; - } - async call(cb: () => Promise, clearLine?: boolean): Promise { - this.start(); - try { - return await cb(); - } finally { - this.stop(clearLine); - } - } - 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; - const time = `${Math.floor(costMs / 1000)}s`; - process.stderr.write(`\r${this.message} ${time} ${dots}\x1b[K`); - }, 500); - } - stop(clearLine?: boolean): void { - if (this.interval) { - clearInterval(this.interval); - process.stderr.write(clearLine ? "\r\x1b[K" : "\n"); - } - } -} - -function red(message: string): string { - return `\x1b[31m${message}\x1b[0m`; -} - -function green(message: string): string { - return `\x1b[32m${message}\x1b[0m`; -} - async function checkRev(): Promise { const localRev = await getGitLocalRev(); const remoteRev = await new ProcessBar("Checking rev").call( @@ -270,7 +150,7 @@ async function checkRev(): Promise { }, ); if (localRev === remoteRev) { - console.log(green(`Check rev successfully, rev: ${localRev}`)); + console.log(term.green(`Check rev successfully, rev: ${localRev}`)); } else { throw `Check rev failed, local rev: ${localRev} vs remote rev: ${remoteRev}`; } @@ -280,10 +160,10 @@ async function main() { await checkRev(); // check local rev <--> remote rev equals const gitStatus = await getGitStatus(); if (gitStatus === null) { - console.log("No git status found"); + log.warn("No git status found"); return; } - console.log("Git status:", gitStatus); + log.info("Git status:", gitStatus); let emptyCount = 0; let message = "empty message"; @@ -317,21 +197,21 @@ async function main() { for (const file of gitStatus.untracked) { gitAddArgs.push(file); } - console.log(">>>>>", ["git", gitAddArgs]); + log.info(">>>>>", ["git", gitAddArgs]); await execCommandShell("git", gitAddArgs); } - console.log(">>>>>", ["git", gitCommitArgs]); + log.info(">>>>>", ["git", gitCommitArgs]); await execCommandShell("git", gitCommitArgs); - console.log(">>>>>", ["git", "push"]); + log.info(">>>>>", ["git", "push"]); await new ProcessBar("Git pushing").call(async (): Promise => { await execCommandShell("git", ["push"]); }); } main().catch((err) => { - console.error(red(`[ERROR] ${err}`)); + log.error(err); process.exit(0); }).then(() => process.exit(0)); -// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260128T224430+08:00.MEYCIQCBwZf7i4TLaloGJMI2 -// 1lEGJACKU3pCRDUTXlU2AY5pVgIhANRpDg85DelQ5jyXlaIIDLQnrm9XRUFEQH/me/daYtTN +// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260207T012258+08:00.MEUCIEMd/yU+NpLUaEnUVl65 +// 6GG+ZETEZ/7Sqbj1bz4BUB6bAiEAiWQZCmeq80xCSnwownqPizH8NR3uu2/q0vWm3XicbOY=