refactor: remove obsolete commit helper

This commit is contained in:
Peter Steinberger
2026-08-06 18:09:59 -07:00
parent 179f5fa8ce
commit 00a5db443a
12 changed files with 14 additions and 582 deletions
@@ -253,7 +253,7 @@ every human `Thanks @...` attribution.
generated checks are explicitly skipped generated checks are explicitly skipped
- `git diff --check` - `git diff --check`
- for docs/changelog-only changes, no broad tests are required - for docs/changelog-only changes, no broad tests are required
- commit with `scripts/committer "docs(changelog): refresh YYYY.M.PATCH notes" CHANGELOG.md` - stage `CHANGELOG.md` and commit with `git commit -m "docs(changelog): refresh YYYY.M.PATCH notes"`
- record the new commit as the Release SHA and require - record the new commit as the Release SHA and require
`git diff --name-only <code-sha>..<release-sha>` to print only `git diff --name-only <code-sha>..<release-sha>` to print only
`CHANGELOG.md` `CHANGELOG.md`
@@ -356,7 +356,7 @@ gh search issues --repo openclaw/openclaw --match title,body --limit 50 \
- Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed. - Leave a review conversation unresolved only when reviewer or maintainer judgment is still needed.
- Before landing any PR with non-trivial code changes, run fresh `$autoreview` until no accepted/actionable findings remain; prior CI, ClawSweeper, or manual review is not a substitute. Skip only for truly trivial/docs-only changes or when the user explicitly opts out. - Before landing any PR with non-trivial code changes, run fresh `$autoreview` until no accepted/actionable findings remain; prior CI, ClawSweeper, or manual review is not a substitute. Skip only for truly trivial/docs-only changes or when the user explicitly opts out.
- When an agent is landing or merging a PR targeting `main`, use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `OPENCLAW_TESTBOX=1 scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`. The Testbox flag is mandatory for agents: it verifies hosted CI/Testbox on the current head or reuses a patch-identical pre-rebase run green within 24 hours instead of running full `pnpm` gates locally. Do not rebase only because `main` advanced; behind-main drift is advisory unless strict drift is explicitly enabled, while GitHub still blocks conflicts. - When an agent is landing or merging a PR targeting `main`, use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `OPENCLAW_TESTBOX=1 scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`. The Testbox flag is mandatory for agents: it verifies hosted CI/Testbox on the current head or reuses a patch-identical pre-rebase run green within 24 hours instead of running full `pnpm` gates locally. Do not rebase only because `main` advanced; behind-main drift is advisory unless strict drift is explicitly enabled, while GitHub still blocks conflicts.
- Use `scripts/committer "<msg>" <file...>` for scoped commits instead of manual `git add` and `git commit`. - Use standard Git commands and stage only the files intended for each commit.
- Keep commit messages concise and action-oriented. - Keep commit messages concise and action-oriented.
- Group related changes; avoid bundling unrelated refactors. - Group related changes; avoid bundling unrelated refactors.
- Use `.github/pull_request_template.md` for PR submissions and `.github/ISSUE_TEMPLATE/` for issues. - Use `.github/pull_request_template.md` for PR submissions and `.github/ISSUE_TEMPLATE/` for issues.
@@ -63,7 +63,7 @@ test:extensions:batch <plugin[,plugin...]>` or plugin-inspector command
8. Update the running report when requested or when this thread is tracking one. 8. Update the running report when requested or when this thread is tracking one.
Include before/after commands, artifacts, coverage notes, verification, and Include before/after commands, artifacts, coverage notes, verification, and
next attack order. next attack order.
9. Commit with `scripts/committer "<message>" <paths...>` and push when the 9. Stage the intended paths, commit with standard Git, and push when the
user asked for commits/pushes. Stage only files touched for this attack. user asked for commits/pushes. Stage only files touched for this attack.
## Plugin-Suite Workflow ## Plugin-Suite Workflow
+2 -2
View File
@@ -118,12 +118,12 @@ sync the current checkout on every run, and stop it before handoff.
static checks with ready dependencies. Untrusted repository tooling never static checks with ready dependencies. Untrusted repository tooling never
runs locally. Full suites and computationally intensive commands run remotely. runs locally. Full suites and computationally intensive commands run remotely.
- Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets. - Prefer GitHub Actions for release/Docker proof when the workflow already has the prepared image and secrets.
- Use `scripts/committer "<msg>" <paths...>` when committing; stage only your files. - Use standard Git commands when committing; stage only your files.
- If dependencies are missing on the selected remote box, run `pnpm install` there, retry - If dependencies are missing on the selected remote box, run `pnpm install` there, retry
once, then report the first actionable error. Do not reconcile or reinstall a once, then report the first actionable error. Do not reconcile or reinstall a
local Codex worktree merely to run validation. local Codex worktree merely to run validation.
- In a Codex worktree or linked/sparse checkout, do not run direct local - In a Codex worktree or linked/sparse checkout, do not run direct local
`pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, or `scripts/committer`. Use `pnpm test*`, `pnpm check*`, or `pnpm crabbox:run`. Use
`node scripts/crabbox-wrapper.mjs` for remote proof and `node scripts/crabbox-wrapper.mjs` for remote proof and
`node scripts/check-changed.mjs` for classify-first changed checks. Use `node scripts/check-changed.mjs` for classify-first changed checks. Use
`node scripts/run-vitest.mjs` for bounded focused local proof when the `node scripts/run-vitest.mjs` for bounded focused local proof when the
+1 -1
View File
@@ -61,4 +61,4 @@
- **Tests**: `pnpm test` - **Tests**: `pnpm test`
- **Build**: `pnpm build` - **Build**: `pnpm build`
If you are coding together with a human, do NOT use scripts/committer, but git directly and run the above commands manually to ensure quality. If you are coding together with a human, use Git directly and run the above commands manually to ensure quality.
+3 -3
View File
@@ -185,8 +185,8 @@ Skills own workflows; root owns hard policy and routing. Product direction and m
- Testbox mechanics: warm from the task checkout; ownership is checkout-path scoped; `--reclaim` only for intentional transfer, and it does not retarget the remote checkout — never cross repos. One lease, one active command; never sync/reclaim during a run; base/head changed means stop and rewarm — never override stale lease checks. Warmup must print a lease id; silent success is unusable — verify before reuse, else fall back to one-shot `run`. Wrapper reuse requires its local SSH key; missing after restart/handoff means warm fresh. Direct lease: `blacksmith testbox run`; Crabbox wrapper reuse needs a wrapper-created lease. Status/stop: `blacksmith testbox status|stop --id <tbx_id>` — id is not positional, no status `--json` flag. Delegated runs reject `--fresh-pr` and `--stop-after`; sync current checkout, workflow owns lifecycle. Compound commands: `bash -lc`, never `sh -lc`; job env uses Bash `declare`. Testbox owns Chromium; never pass Crabbox `--browser` to `provider=blacksmith-testbox`. - Testbox mechanics: warm from the task checkout; ownership is checkout-path scoped; `--reclaim` only for intentional transfer, and it does not retarget the remote checkout — never cross repos. One lease, one active command; never sync/reclaim during a run; base/head changed means stop and rewarm — never override stale lease checks. Warmup must print a lease id; silent success is unusable — verify before reuse, else fall back to one-shot `run`. Wrapper reuse requires its local SSH key; missing after restart/handoff means warm fresh. Direct lease: `blacksmith testbox run`; Crabbox wrapper reuse needs a wrapper-created lease. Status/stop: `blacksmith testbox status|stop --id <tbx_id>` — id is not positional, no status `--json` flag. Delegated runs reject `--fresh-pr` and `--stop-after`; sync current checkout, workflow owns lifecycle. Compound commands: `bash -lc`, never `sh -lc`; job env uses Bash `declare`. Testbox owns Chromium; never pass Crabbox `--browser` to `provider=blacksmith-testbox`.
- Crabbox mechanics: a Crabbox request means real scenario proof — install/update/call/repro the user path, not just copied tests run remotely. Final timing JSON = proof complete; if portal sync hangs after it, interrupt the wrapper only. Wrapper `stop` has no `--timing-json`; use `node scripts/crabbox-wrapper.mjs stop --provider <provider> --id <id>`. Sparse-sync temp checkout may claim a kept Testbox; repo-path reuse needs `--reclaim`. Dirty-sync generator proof: compare hashes before/after; `git diff` includes the synced patch. - Crabbox mechanics: a Crabbox request means real scenario proof — install/update/call/repro the user path, not just copied tests run remotely. Final timing JSON = proof complete; if portal sync hangs after it, interrupt the wrapper only. Wrapper `stop` has no `--timing-json`; use `node scripts/crabbox-wrapper.mjs stop --provider <provider> --id <id>`. Sparse-sync temp checkout may claim a kept Testbox; repo-path reuse needs `--reclaim`. Dirty-sync generator proof: compare hashes before/after; `git diff` includes the synced patch.
- Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked. - Visual proof: use Crabbox, set up like a user, then screenshot-verify. No harness/bypass/shortcut unless explicitly asked.
- In Codex or linked worktrees, direct local `pnpm test*`, `pnpm check*`, `pnpm crabbox:run`, and `scripts/committer` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof. - In Codex or linked worktrees, direct local `pnpm test*`, `pnpm check*`, and `pnpm crabbox:run` can trigger pnpm dependency reconciliation or install prompts. Prefer `node` wrappers locally and Crabbox/Testbox for pnpm-gated proof.
- Repo-native PR worktree may omit `node_modules`; prove remotely, then use `git commit --no-verify`, not `scripts/committer`. - Repo-native PR worktrees may omit `node_modules`; prove remotely, then use `git commit --no-verify`.
- Release-branch formatting: Testbox or existing binary; never local `pnpm exec` reconciliation. Targeted local format/lint: existing `./node_modules/.bin/*`; never `pnpm exec` reconciliation. - Release-branch formatting: Testbox or existing binary; never local `pnpm exec` reconciliation. Targeted local format/lint: existing `./node_modules/.bin/*`; never `pnpm exec` reconciliation.
- Parallel agents share the checkout; never switch its branch while sibling work runs. - Parallel agents share the checkout; never switch its branch while sibling work runs.
- QA CLI `--output-dir` must be repo-relative. - QA CLI `--output-dir` must be repo-relative.
@@ -331,7 +331,7 @@ Mechanics only; policy lives above.
## Git ## Git
- Commit via `scripts/committer "<msg>" <file...>`; stage intended files only. - Commit with standard Git commands; stage intended files only.
- Commits: conventional-ish, concise, grouped. - Commits: conventional-ish, concise, grouped.
- No manual stash/autostash unless explicit. Branch switches ok when useful; no new worktrees unless requested. - No manual stash/autostash unless explicit. Branch switches ok when useful; no new worktrees unless requested.
- `main`: no merge commits; rebase on latest `origin/main` before push. After one green run plus clean rebase sanity, do not chase moving `main` with repeated full gates. - `main`: no merge commits; rebase on latest `origin/main` before push. After one green run plus clean rebase sanity, do not chase moving `main` with repeated full gates.
+1
View File
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes ### Changes
- **Developer workflow:** remove the obsolete scoped-commit helper and use standard Git commands in isolated worktrees.
- **Plugin uninstall cleanup:** remove exact recorded install paths from `plugins.load.paths` for marketplace, npm, and other managed installs while preserving parent, child, prefix, and unrelated paths. - **Plugin uninstall cleanup:** remove exact recorded install paths from `plugins.load.paths` for marketplace, npm, and other managed installs while preserving parent, child, prefix, and unrelated paths.
- Fixed Crabbox hydration on unprivileged cloud sandboxes by falling back to a user-writable pnpm store when the shared `/var/cache/crabbox` cache is unavailable, preserving the hardlink import mode after hydration, and making Docker an explicit routed capability instead of an implicit install requirement. - Fixed Crabbox hydration on unprivileged cloud sandboxes by falling back to a user-writable pnpm store when the shared `/var/cache/crabbox` cache is unavailable, preserving the hardlink import mode after hydration, and making Docker an explicit routed capability instead of an implicit install requirement.
+1 -1
View File
@@ -54,7 +54,7 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
- Keep PRs takeover-ready: open them from a branch maintainers can push to. For fork PRs, leave GitHub's **Allow edits by maintainers** option enabled so maintainers can finish urgent fixes, changelog entries, or merge prep when needed. If GitHub shows **Allow edits and access to secrets by maintainers**, enable it only when that workflow/secrets access is acceptable and say so in the PR. - Keep PRs takeover-ready: open them from a branch maintainers can push to. For fork PRs, leave GitHub's **Allow edits by maintainers** option enabled so maintainers can finish urgent fixes, changelog entries, or merge prep when needed. If GitHub shows **Allow edits and access to secrets by maintainers**, enable it only when that workflow/secrets access is acceptable and say so in the PR.
- Do not edit `CHANGELOG.md` in contributor PRs. Maintainers or ClawSweeper add the changelog entry when landing user-facing changes. - Do not edit `CHANGELOG.md` in contributor PRs. Maintainers or ClawSweeper add the changelog entry when landing user-facing changes.
- Run tests: `pnpm build && pnpm check && pnpm test` - Run tests: `pnpm build && pnpm check && pnpm test`
- For iterative local commits, `scripts/committer --fast "message" <files...>` skips commit hooks. Only use it when you've already run equivalent targeted validation for the touched surface. - For iterative local commits after running equivalent targeted validation for the touched surface, `git commit --no-verify` skips commit hooks.
- For extension/plugin changes, run the fast local lane first: - For extension/plugin changes, run the fast local lane first:
- `pnpm test:extension <extension-name>` - `pnpm test:extension <extension-name>`
- `pnpm test:extension --list` to see valid extension ids - `pnpm test:extension --list` to see valid extension ids
+1 -1
View File
@@ -40,7 +40,7 @@ new directory taxonomy.
| Live proof | `pnpm test:live` | Live checks require the matching environment and credentials. | | Live proof | `pnpm test:live` | Live checks require the matching environment and credentials. |
| Release checks | `pnpm release:check`, `pnpm release:beta`, `pnpm release:candidate` | Release scripts are maintainer workflows; read release docs before use. | | Release checks | `pnpm release:check`, `pnpm release:beta`, `pnpm release:candidate` | Release scripts are maintainer workflows; read release docs before use. |
| GitHub reads | `scripts/gh-read` | Uses a GitHub App read token when configured, leaving normal `gh` login for writes. | | GitHub reads | `scripts/gh-read` | Uses a GitHub App read token when configured, leaving normal `gh` login for writes. |
| Commits | `scripts/committer "<message>" <files...>` | Preferred scoped commit helper for OpenClaw changes. | | Commits | `git add <files...>` then `git commit -m "<message>"` | Stage only the intended files for each commit. |
| Remote proof | `node scripts/crabbox-wrapper.mjs ...` | Agent default for tests and heavy work; pre-warm by source trust, sync each run, reuse the lease. | | Remote proof | `node scripts/crabbox-wrapper.mjs ...` | Agent default for tests and heavy work; pre-warm by source trust, sync each run, reuse the lease. |
## Script Families ## Script Families
-289
View File
@@ -1,289 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Disable glob expansion to handle brackets in file paths
set -f
usage() {
local exit_code=${1:-2}
if [ "$exit_code" -eq 0 ]; then
printf 'Usage: %s [--force] [--fast] [--no-verify-formatted] "commit message" "file" ["file" ...]\n' "$(basename "$0")"
else
printf 'Usage: %s [--force] [--fast] [--no-verify-formatted] "commit message" "file" ["file" ...]\n' "$(basename "$0")" >&2
fi
exit "$exit_code"
}
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
usage 0
fi
if [ "$#" -lt 2 ]; then
usage
fi
force_delete_lock=false
fast_commit=false
no_verify_formatted=false
while [[ "${1:-}" == --* ]]; do
case "${1:-}" in
--force)
force_delete_lock=true
shift
;;
--fast)
fast_commit=true
shift
;;
--no-verify-formatted)
no_verify_formatted=true
shift
;;
--help|-h)
usage 0
;;
*)
usage
;;
esac
done
if [ "$#" -lt 2 ]; then
usage
fi
commit_message=$1
shift
if [[ "$commit_message" != *[![:space:]]* ]]; then
printf 'Error: commit message must not be empty\n' >&2
exit 1
fi
if [ -e "$commit_message" ]; then
printf 'Error: first argument looks like a file path ("%s"); provide the commit message first\n' "$commit_message" >&2
exit 1
fi
if [ "$#" -eq 0 ]; then
usage
fi
path_exists_or_tracked() {
local candidate=$1
[ -e "$candidate" ] ||
git ls-files --error-unmatch -- "$candidate" >/dev/null 2>&1 ||
git cat-file -e "HEAD:$candidate" >/dev/null 2>&1
}
append_normalized_file_arg() {
local raw=$1
if path_exists_or_tracked "$raw"; then
files+=("$raw")
return
fi
if [[ "$raw" == *$'\n'* || "$raw" == *$'\r'* ]]; then
local normalized=${raw//$'\r'/}
while IFS= read -r line; do
if [[ "$line" == *[![:space:]]* ]]; then
files+=("$line")
fi
done <<< "$normalized"
return
fi
if [[ "$raw" == *[[:space:]]* ]]; then
local split_paths=()
# Intentional IFS split for callers that pass a single shell-expanded path blob.
# shellcheck disable=SC2206
split_paths=($raw)
if [ "${#split_paths[@]}" -gt 1 ]; then
files+=("${split_paths[@]}")
return
fi
fi
files+=("$raw")
}
files=()
for raw_arg in "$@"; do
append_normalized_file_arg "$raw_arg"
done
# Disallow "." because it stages the entire repository and defeats the helper's safety guardrails.
for file in "${files[@]}"; do
if [ "$file" = "." ]; then
printf 'Error: "." is not allowed; list specific paths instead\n' >&2
exit 1
fi
done
# Prevent staging node_modules even if a path is forced.
for file in "${files[@]}"; do
case "$file" in
*node_modules* | */node_modules | */node_modules/* | node_modules)
printf 'Error: node_modules paths are not allowed: %s\n' "$file" >&2
exit 1
;;
esac
done
last_commit_error=''
run_git_command() {
local stderr_log
stderr_log=$(mktemp)
if "$@" 2>"$stderr_log"; then
if [ -s "$stderr_log" ]; then
cat "$stderr_log" >&2
fi
rm -f "$stderr_log"
last_commit_error=''
return 0
fi
if [ -s "$stderr_log" ]; then
cat "$stderr_log" >&2
fi
last_commit_error=$(cat "$stderr_log")
rm -f "$stderr_log"
return 1
}
is_git_lock_error() {
printf '%s\n' "$last_commit_error" | grep -Eq \
"Another git process seems to be running|Unable to create '.*\\.git/[^']+\\.lock'"
}
extract_git_lock_path() {
printf '%s\n' "$last_commit_error" |
sed -n "s/.*'\(.*\.git\/[^']*\.lock\)'.*/\1/p" |
head -n 1
}
run_git_with_lock_retry() {
local label=$1
shift
local deadline=$((SECONDS + 5))
local announced_retry=false
while true; do
if run_git_command "$@"; then
return 0
fi
if ! is_git_lock_error; then
return 1
fi
if [ "$SECONDS" -ge "$deadline" ]; then
break
fi
if [ "$announced_retry" = false ]; then
printf 'Git lock during %s; retrying for up to 5 seconds...\n' "$label" >&2
announced_retry=true
fi
sleep 0.5
done
if [ "$force_delete_lock" = true ]; then
local lock_path
lock_path=$(extract_git_lock_path)
if [ -n "$lock_path" ] && [ -e "$lock_path" ]; then
rm -f "$lock_path"
printf 'Removed stale git lock: %s\n' "$lock_path" >&2
run_git_command "$@"
return $?
fi
fi
return 1
}
for file in "${files[@]}"; do
if ! path_exists_or_tracked "$file"; then
printf 'Error: file not found: %s\n' "$file" >&2
exit 1
fi
done
committer_requires_repo_formatter() {
local root="$1"
local filter="$root/scripts/pre-commit/filter-staged-files.mjs"
[ -f "$filter" ] || return 1
local filter_output selected
filter_output=$(mktemp "${TMPDIR:-/tmp}/openclaw-committer-filter.XXXXXX") || {
echo "Unable to create temporary output for formatter applicability check." >&2
return 2
}
if ! node "$filter" format -- "${files[@]}" >"$filter_output"; then
rm -f "$filter_output"
echo "Unable to determine formatter applicability: filter-staged-files.mjs failed." >&2
return 2
fi
if IFS= read -r -d '' selected <"$filter_output"; then
rm -f "$filter_output"
return 0
fi
rm -f "$filter_output"
return 1
}
repo_root=$(git rev-parse --show-toplevel)
if [ "$fast_commit" = false ] && [ "$no_verify_formatted" = false ] && \
[ -f "$repo_root/pnpm-lock.yaml" ] && \
command -v pnpm >/dev/null 2>&1 && \
[ ! -e "$repo_root/node_modules" ]
then
if committer_requires_repo_formatter "$repo_root"; then
formatter_requirement=0
else
formatter_requirement=$?
fi
if [ "$formatter_requirement" = 2 ]; then
exit 1
fi
if [ "$formatter_requirement" = 0 ]; then
echo "Missing repo dependencies: cannot run oxfmt without node_modules." >&2
echo "Run pnpm install in a normal checkout, or bypass the hook only after separate formatting proof." >&2
echo "After that proof, pass --no-verify-formatted to commit explicitly with --no-verify." >&2
exit 1
fi
fi
run_git_with_lock_retry "unstaging files" git restore --staged :/
run_git_with_lock_retry "staging files" git add --all --force -- "${files[@]}"
if git diff --staged --quiet; then
printf 'Warning: no staged changes detected for: %s\n' "${files[*]}" >&2
exit 1
fi
committed=false
if [ "$fast_commit" = true ]; then
declare -a commit_env=(FAST_COMMIT=1)
if run_git_with_lock_retry "commit" env "${commit_env[@]}" git commit --no-verify -m "$commit_message"; then
committed=true
fi
elif [ "$no_verify_formatted" = true ]; then
echo "Notice: --no-verify-formatted asserts separate formatting proof; committing with --no-verify."
if run_git_with_lock_retry "commit" git commit --no-verify -m "$commit_message"; then
committed=true
fi
else
if run_git_with_lock_retry "commit" git commit -m "$commit_message"; then
committed=true
fi
fi
if [ "$committed" = false ]; then
exit 1
fi
printf 'Committed "%s" with %d files\n' "$commit_message" "${#files[@]}"
-279
View File
@@ -1,279 +0,0 @@
// Committer tests cover committer script behavior.
import { execFileSync, spawnSync } from "node:child_process";
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createScriptTestHarness } from "./test-helpers.js";
const scriptPath = path.join(process.cwd(), "scripts", "committer");
const { createTempDir } = createScriptTestHarness();
let templateRepo: string;
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-");
cpSync(templateRepo, repo, { recursive: true });
return repo;
}
function createTemplateRepo() {
const repo = mkdtempSync(path.join(tmpdir(), "committer-template-"));
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: 0o755,
});
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 commitWithHelperFailure(repo: string, ...args: string[]) {
return spawnSync("bash", [scriptPath, ...args], { cwd: repo, encoding: "utf8" });
}
function committedPaths(repo: string) {
const output = git(repo, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD");
const paths: string[] = [];
for (const line of output.split("\n")) {
if (line) {
paths.push(line);
}
}
return paths.toSorted();
}
function committedFileContents(repo: string, relativePath: string) {
return git(repo, "show", `HEAD:${relativePath}`);
}
describe("scripts/committer", () => {
beforeAll(() => {
templateRepo = createTemplateRepo();
});
afterAll(() => {
rmSync(templateRepo, { recursive: true, force: true });
});
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("fails before staging when formatting dependencies are missing", () => {
const repo = createRepo();
writeRepoFile(repo, "pnpm-lock.yaml", "lockfileVersion: '9.0'\n");
writeRepoFile(
repo,
"scripts/pre-commit/filter-staged-files.mjs",
"for (const file of process.argv.slice(4)) { if (file.endsWith('.ts')) process.stdout.write(file + '\\0'); }\n",
);
writeRepoFile(repo, "note.ts", "export const note = true;\n");
const result = commitWithHelperFailure(repo, "test: missing formatter", "note.ts");
expect(result.status).toBe(1);
expect(result.stderr).toContain("cannot run oxfmt without node_modules");
expect(result.stderr).toContain("--no-verify-formatted");
expect(git(repo, "diff", "--cached", "--name-only")).toBe("");
expect(git(repo, "log", "-1", "--pretty=%s")).toBe("seed");
});
it("commits dependency-less formatted work only with the explicit assertion", () => {
const repo = createRepo();
writeRepoFile(repo, "pnpm-lock.yaml", "lockfileVersion: '9.0'\n");
writeRepoFile(
repo,
"scripts/pre-commit/filter-staged-files.mjs",
"for (const file of process.argv.slice(4)) { if (file.endsWith('.ts')) process.stdout.write(file + '\\0'); }\n",
);
writeRepoFile(repo, "note.ts", "export const note = true;\n");
const output = commitWithHelperArgs(
repo,
"--no-verify-formatted",
"test: formatted assertion",
"note.ts",
);
expect(output).toContain("asserts separate formatting proof; committing with --no-verify");
expect(committedPaths(repo)).toEqual(["note.ts"]);
});
it("fails before staging when formatter applicability cannot be determined", () => {
const repo = createRepo();
writeRepoFile(repo, "pnpm-lock.yaml", "lockfileVersion: '9.0'\n");
writeRepoFile(
repo,
"scripts/pre-commit/filter-staged-files.mjs",
"process.stderr.write('fixture filter failure\\n'); process.exit(7);\n",
);
writeRepoFile(repo, "note.ts", "export const note = true;\n");
const result = commitWithHelperFailure(repo, "test: failed formatter filter", "note.ts");
expect(result.status).toBe(1);
expect(result.stderr).toContain("Unable to determine formatter applicability");
expect(git(repo, "diff", "--cached", "--name-only")).toBe("");
expect(git(repo, "log", "-1", "--pretty=%s")).toBe("seed");
});
it("bypasses git hooks when using --fast", () => {
const repo = createRepo();
installHook(repo, ".githooks/pre-commit", "#!/usr/bin/env bash\nset -euo pipefail\nexit 91\n");
writeRepoFile(repo, "note.txt", "hello\n");
const output = commitWithHelperArgs(repo, "--fast", "test: fast no verify", "note.txt");
expect(output).toContain('Committed "test: fast no verify" 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] [--no-verify-formatted] "commit message" "file" ["file" ...]',
);
});
});
+2 -3
View File
@@ -1514,7 +1514,6 @@ describe("scripts/test-projects changed-target routing", () => {
it("keeps extensionless helper script edits on owner tests", () => { it("keeps extensionless helper script edits on owner tests", () => {
const expectedTargets = Object.entries({ const expectedTargets = Object.entries({
"scripts/committer": ["test/scripts/committer.test.ts"],
"scripts/gh-read": ["test/scripts/gh-read.test.ts"], "scripts/gh-read": ["test/scripts/gh-read.test.ts"],
"scripts/pr": [ "scripts/pr": [
"test/scripts/pr-merge.test.ts", "test/scripts/pr-merge.test.ts",
@@ -3933,7 +3932,7 @@ describe("scripts/test-projects local heavy-check lock", () => {
[ [
{ {
config: "test/vitest/vitest.tooling.config.ts", config: "test/vitest/vitest.tooling.config.ts",
includePatterns: ["test/scripts/committer.test.ts"], includePatterns: ["test/scripts/gh-read.test.ts"],
watchMode: false, watchMode: false,
}, },
], ],
@@ -3981,7 +3980,7 @@ describe("scripts/test-projects local heavy-check lock", () => {
[ [
{ {
config: "test/vitest/vitest.tooling.config.ts", config: "test/vitest/vitest.tooling.config.ts",
includePatterns: ["test/scripts/committer.test.ts"], includePatterns: ["test/scripts/gh-read.test.ts"],
watchMode: false, watchMode: false,
}, },
], ],