mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: remove obsolete commit helper
This commit is contained in:
@@ -253,7 +253,7 @@ every human `Thanks @...` attribution.
|
||||
generated checks are explicitly skipped
|
||||
- `git diff --check`
|
||||
- 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
|
||||
`git diff --name-only <code-sha>..<release-sha>` to print only
|
||||
`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.
|
||||
- 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.
|
||||
- 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.
|
||||
- Group related changes; avoid bundling unrelated refactors.
|
||||
- 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.
|
||||
Include before/after commands, artifacts, coverage notes, verification, and
|
||||
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.
|
||||
|
||||
## Plugin-Suite Workflow
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
- 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
|
||||
once, then report the first actionable error. Do not reconcile or reinstall a
|
||||
local Codex worktree merely to run validation.
|
||||
- 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/check-changed.mjs` for classify-first changed checks. Use
|
||||
`node scripts/run-vitest.mjs` for bounded focused local proof when the
|
||||
|
||||
@@ -61,4 +61,4 @@
|
||||
- **Tests**: `pnpm test`
|
||||
- **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.
|
||||
|
||||
@@ -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`.
|
||||
- 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.
|
||||
- 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.
|
||||
- Repo-native PR worktree may omit `node_modules`; prove remotely, then use `git commit --no-verify`, not `scripts/committer`.
|
||||
- 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 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.
|
||||
- Parallel agents share the checkout; never switch its branch while sibling work runs.
|
||||
- QA CLI `--output-dir` must be repo-relative.
|
||||
@@ -331,7 +331,7 @@ Mechanics only; policy lives above.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
|
||||
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### 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.
|
||||
- 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
@@ -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.
|
||||
- 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`
|
||||
- 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:
|
||||
- `pnpm test:extension <extension-name>`
|
||||
- `pnpm test:extension --list` to see valid extension ids
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ new directory taxonomy.
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
## Script Families
|
||||
|
||||
@@ -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[@]}"
|
||||
@@ -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" ...]',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1514,7 +1514,6 @@ describe("scripts/test-projects changed-target routing", () => {
|
||||
|
||||
it("keeps extensionless helper script edits on owner tests", () => {
|
||||
const expectedTargets = Object.entries({
|
||||
"scripts/committer": ["test/scripts/committer.test.ts"],
|
||||
"scripts/gh-read": ["test/scripts/gh-read.test.ts"],
|
||||
"scripts/pr": [
|
||||
"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",
|
||||
includePatterns: ["test/scripts/committer.test.ts"],
|
||||
includePatterns: ["test/scripts/gh-read.test.ts"],
|
||||
watchMode: false,
|
||||
},
|
||||
],
|
||||
@@ -3981,7 +3980,7 @@ describe("scripts/test-projects local heavy-check lock", () => {
|
||||
[
|
||||
{
|
||||
config: "test/vitest/vitest.tooling.config.ts",
|
||||
includePatterns: ["test/scripts/committer.test.ts"],
|
||||
includePatterns: ["test/scripts/gh-read.test.ts"],
|
||||
watchMode: false,
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user