import { execFileSync } from "node:child_process" ;
import { mkdirSync, writeFileSync } from "node:fs" ;
import path from "node:path" ;
import { describe, expect, it } from "vitest" ;
import { createScriptTestHarness } from "./test-helpers.js" ;
const scriptPath = path.join(process.cwd(), "scripts" , "committer" );
const { createTempDir } = createScriptTestHarness();
function run(cwd: string, command: string, args: string[]) {
return execFileSync(command, args, {
cwd,
encoding: "utf8" ,
}).trim();
}
function git(cwd: string, ...args: string[]) {
return run(cwd, "git" , args);
}
function createRepo() {
const repo = createTempDir("committer-test-" );
git(repo, "init" , "-q" );
git(repo, "config" , "user.email" , "test@example.com" );
git(repo, "config" , "user.name" , "Test User" );
writeFileSync(path.join(repo, "seed.txt" ), "seed\n" );
git(repo, "add" , "seed.txt" );
git(repo, "commit" , "-qm" , "seed" );
return repo;
}
function writeRepoFile(repo: string, relativePath: string, contents: string) {
const fullPath = path.join(repo, relativePath);
mkdirSync(path.dirname(fullPath), { recursive: true });
writeFileSync(fullPath, contents);
}
function installHook(repo: string, relativePath: string, contents: string) {
const fullPath = path.join(repo, relativePath);
mkdirSync(path.dirname(fullPath), { recursive: true });
writeFileSync(fullPath, contents, {
encoding: "utf8" ,
mode: 0 o755,
});
git(repo, "config" , "core.hooksPath" , path.dirname(relativePath));
}
function commitWithHelper(repo: string, commitMessage: string, ...args: string[]) {
return run(repo, "bash" , [scriptPath, commitMessage, ...args]);
}
function commitWithHelperArgs(repo: string, ...args: string[]) {
return run(repo, "bash" , [scriptPath, ...args]);
}
function committedPaths(repo: string) {
const output = git(repo, "diff-tree" , "--no-commit-id" , "--name-only" , "-r" , "HEAD" );
return output.split("\n" ).filter(Boolean ).toSorted();
}
function committedFileContents(repo: string, relativePath: string) {
return git(repo, "show" , `HEAD:${relativePath}`);
}
describe("scripts/committer" , () => {
it("accepts supported path argument shapes" , () => {
const cases = [
{
commitMessage: "test: plain argv" ,
files: [
["alpha.txt" , "alpha\n" ],
["nested/file with spaces.txt" , "beta\n" ],
] as const ,
args: ["alpha.txt" , "nested/file with spaces.txt" ],
expected: ["alpha.txt" , "nested/file with spaces.txt" ],
},
{
commitMessage: "test: space blob" ,
files: [
["alpha.txt" , "alpha\n" ],
["beta.txt" , "beta\n" ],
] as const ,
args: ["alpha.txt beta.txt" ],
expected: ["alpha.txt" , "beta.txt" ],
},
{
commitMessage: "test: newline blob" ,
files: [
["alpha.txt" , "alpha\n" ],
["nested/file with spaces.txt" , "beta\n" ],
] as const ,
args: ["alpha.txt\nnested/file with spaces.txt" ],
expected: ["alpha.txt" , "nested/file with spaces.txt" ],
},
] as const ;
for (const testCase of cases) {
const repo = createRepo();
for (const [file, contents] of testCase.files) {
writeRepoFile(repo, file, contents);
}
commitWithHelper(repo, testCase.commitMessage, ...testCase.args);
expect(committedPaths(repo)).toEqual(testCase.expected);
}
});
it("commits changelog-only changes without pulling in unrelated dirty files" , () => {
const repo = createRepo();
writeRepoFile(repo, "CHANGELOG.md" , "initial\n" );
writeRepoFile(repo, "unrelated.ts" , "export const ok = true;\n" );
git(repo, "add" , "CHANGELOG.md" , "unrelated.ts" );
git(repo, "commit" , "-qm" , "seed extra files" );
writeRepoFile(repo, "CHANGELOG.md" , "breaking note\n" );
writeRepoFile(repo, "unrelated.ts" , "<<<<<<< HEAD\nleft\n=======\nright\n>>>>>>> branch\n" );
commitWithHelper(repo, "docs(changelog): note breaking change" , "CHANGELOG.md" );
expect(committedPaths(repo)).toEqual(["CHANGELOG.md" ]);
expect(git(repo, "status" , "--short" )).toContain("M unrelated.ts" );
});
it("supports --fast before the commit message" , () => {
const repo = createRepo();
writeRepoFile(repo, "note.txt" , "hello\n" );
const output = commitWithHelperArgs(repo, "--fast" , "test: fast helper" , "note.txt" );
expect(output).toContain('Committed "test: fast helper" with 1 files' );
expect(committedPaths(repo)).toEqual(["note.txt" ]);
});
it("supports combining --force and --fast" , () => {
const repo = createRepo();
writeRepoFile(repo, "note.txt" , "hello\n" );
const output = commitWithHelperArgs(
repo,
"--force" ,
"--fast" ,
"test: fast forced helper" ,
"note.txt" ,
);
expect(output).toContain('Committed "test: fast forced helper" with 1 files' );
expect(committedPaths(repo)).toEqual(["note.txt" ]);
});
it("passes FAST_COMMIT through to git hooks when using --fast" , () => {
const repo = createRepo();
installHook(
repo,
".githooks/pre-commit" ,
'#!/usr/bin/env bash\nset -euo pipefail\n[ "${FAST_COMMIT:-}" = "1" ] || exit 91\n' ,
);
writeRepoFile(repo, "note.txt" , "hello\n" );
const output = commitWithHelperArgs(repo, "--fast" , "test: fast hook env" , "note.txt" );
expect(output).toContain('Committed "test: fast hook env" with 1 files' );
expect(committedPaths(repo)).toEqual(["note.txt" ]);
});
it("commits the hook-restaged file contents and leaves the tree clean" , () => {
const repo = createRepo();
installHook(
repo,
".githooks/pre-commit" ,
[
"#!/usr/bin/env bash" ,
"set -euo pipefail" ,
"printf 'formatted\\n' > note.txt" ,
"git add note.txt" ,
].join("\n" ) + "\n" ,
);
writeRepoFile(repo, "note.txt" , "raw\n" );
const output = commitWithHelperArgs(repo, "test: hook rewrite" , "note.txt" );
expect(output).toContain('Committed "test: hook rewrite" with 1 files' );
expect(committedPaths(repo)).toEqual(["note.txt" ]);
expect(committedFileContents(repo, "note.txt" )).toBe("formatted" );
expect(git(repo, "status" , "--short" , "--untracked-files=no" )).toBe("" );
});
it("prints usage for --help" , () => {
const repo = createRepo();
const output = commitWithHelperArgs(repo, "--help" );
expect(output).toContain(
'Usage: committer [--force] [--fast] "commit message" "file" ["file" ...]' ,
);
});
});
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland