script/install: write every listed file the repo lacks
check / check (push) Successful in 48s

A listed file the repo lacks is now written whole, with no keep rule.
A prompts/NAME.md goes to docs/NAME.md unless the repo keeps it at
NAME.md, and any other file goes to its own path. A language's files
are skipped when git tracks no source file of that language.

A listed path git ignores now stops the script before it writes.

README bullets split into short sentences, and a false sentence about
template repos deleted.

Model: opus-5-5
This commit is contained in:
clawbot committed 2026-11-09 19:38:41 +00:00
1 parent f4275b9ac0
commit 76b75fa79f
4 files changed
+166 -45

No files matched your search

+4 -4
View File
@@ -142,11 +142,11 @@ alpine. We provide:
- `script/install-precommit` — installs the git pre-commit hook (our own
extension); `make hooks` shims to it
- `script/install` — fetch the vendored policy files from this repo over HTTP
and write them into the repo given as its argument, or its own repo with no
argument (our own extension); it stops when run here
and write them into a repo (our own extension). It writes into the repo given
as its argument, or its own repo with no argument. It stops when run here.
- `script/update-policy` — fetch the current `script/install`, run it on its own
repo and commit the result without pushing (our own extension); it is for the
repos that vendor the policy files and stops when run here
repo and commit the result without pushing (our own extension). It is for the
repos that vendor the policy files. It stops when run here.
`make hooks` installs the pre-commit hook that runs `script/precommit`.
+8 -6
View File
@@ -943,12 +943,14 @@ style conventions are in separate documents:
the files that changed and does not push, so a person reviews and pushes the
commit.
`script/install` fetches each vendored file the repo carries from `prompts`
over HTTP. It also installs `script/install` and `script/update-policy` in
the repo, so a repo updated once can update itself. Every repo made from a
template repo already has both. The list of vendored files is at the top of
`script/install`. The script replaces each file and keeps only the repo's
own entries:
`script/install` fetches each vendored file from `prompts` over HTTP and
writes it into the repo. It skips a language's files when git tracks no
source file of that language. A file the repo lacks is written whole. A
`prompts/NAME.md` goes to `docs/NAME.md`, unless the repo keeps it at
`NAME.md`. It also installs `script/install` and `script/update-policy` in
the repo, so a repo updated once can update itself. The list of vendored
files is at the top of `script/install`. The script replaces each file and
keeps only the repo's own entries:
- in `.gitignore`, `.editorconfig` and `.dockerignore`: the lines below the
comment that ends the canonical file
- in `.golangci.yml`: the entries the repo added to the `deny` list of the
+42 -20
View File
@@ -24,8 +24,9 @@ PROMPTS_URL="https://git.eeqj.de/sneak/prompts"
PROMPTS_API_URL="https://git.eeqj.de/api/v1/repos/sneak/prompts"
PROMPTS_FIRST_COMMIT="af257bef63f162a681bddc80d9b941c47a44b748"
# Paths in the prompts repo. A file the target repo does not carry is
# skipped, except the two scripts, which every repo gets.
# Paths in the prompts repo. Every repo gets each file, written whole if
# the repo lacks it. A language's files are skipped when git tracks no
# source file of that language.
VENDORED_FILES="
.dockerignore
.editorconfig
@@ -58,15 +59,36 @@ fetch() {
die "cannot fetch $1"
}
# local_path <path in prompts>: print where the target repo keeps the
# file, or nothing if git does not track it there. A prompts/NAME.md
# lands at docs/NAME.md or NAME.md.
# file_applies <path in prompts>: fail if the file is for a language of
# which git tracks no source file in the target repo.
file_applies() {
case "$1" in
.golangci.yml | prompts/CODE_STYLEGUIDE_GO.md | \
prompts/GO_HTTP_SERVER_CONVENTIONS.md)
[ -n "$(git ls-files -- '*.go')" ]
;;
prompts/CODE_STYLEGUIDE_JS.md)
[ -n "$(git ls-files -- '*.js' '*.mjs' '*.ts')" ]
;;
prompts/CODE_STYLEGUIDE_PYTHON.md)
[ -n "$(git ls-files -- '*.py')" ]
;;
esac
}
# local_path <path in prompts>: print where the file goes in the target
# repo. A prompts/NAME.md stays at NAME.md if git tracks it there and not
# at docs/NAME.md; otherwise it goes to docs/NAME.md. Any other file goes
# to its own path.
local_path() {
name="${1#prompts/}"
if [ "$name" != "$1" ] && [ -n "$(git ls-files -- "docs/$name")" ]; then
echo "docs/$name"
elif [ -n "$(git ls-files -- "$name")" ]; then
if [ "$name" = "$1" ]; then
echo "$1"
elif [ -z "$(git ls-files -- "docs/$name")" ] &&
[ -n "$(git ls-files -- "$name")" ]; then
echo "$name"
else
echo "docs/$name"
fi
}
@@ -177,9 +199,8 @@ $own"
# stop_on_own_script <fetched> <local>: stop if the local script has a
# line the canonical file has never held, unless it is blank or a
# comment: the repo wrote that script itself, and replacing it would
# delete it. A repo without the script is fine.
# delete it.
stop_on_own_script() {
[ -e "$2" ] || return 0
own="$(local_only_lines "$1" "$2")"
if [ -n "$own" ]; then
die "$2 has lines the canonical file has never held; rename the repo's own script to keep it:
@@ -210,23 +231,24 @@ main() {
fetch "$PROMPTS_URL/compare/$PROMPTS_FIRST_COMMIT...$commit.patch" \
"$tmp/history.patch"
carried=""
to_write=""
for file in $VENDORED_FILES; do
case "$file" in
script/*) path="$file" ;;
*) path="$(local_path "$file")" ;;
esac
[ -n "$path" ] || continue
file_applies "$file" || continue
path="$(local_path "$file")"
if git check-ignore -q -- "$path"; then
die "$path is ignored by git; stop ignoring it"
fi
if [ -n "$(git status --porcelain -- "$path")" ]; then
die "$path has uncommitted changes"
fi
mkdir -p "$tmp/$(dirname "$path")"
fetch "$PROMPTS_URL/raw/commit/$commit/$file" "$tmp/$path"
former_lines "$tmp/history.patch" "$file" > "$tmp/$path.former"
carried="$carried $path"
to_write="$to_write $path"
done
for path in $carried; do
for path in $to_write; do
[ -e "$path" ] || continue
case "$path" in
.gitignore | .editorconfig | .dockerignore)
keep_lines_below_last "$tmp/$path" "$path"
@@ -241,12 +263,12 @@ main() {
done
echo "$commit"
for path in $carried; do
for path in $to_write; do
mkdir -p "$(dirname "$path")"
case "$path" in
script/*)
# A new name and mv, because script/update-policy may
# be the script running.
mkdir -p script
cp "$tmp/$path" "$path.new"
chmod 755 "$path.new"
mv "$path.new" "$path"
+112 -15
View File
@@ -117,13 +117,20 @@ EOF
echo '{ "tabWidth": 4 }' > "$dir/canonical/.prettierrc"
echo "new policy" > "$dir/canonical/prompts/REPO_POLICIES.md"
echo "new api policy" > "$dir/canonical/prompts/API_POLICIES.md"
for name in CODE_STYLEGUIDE CODE_STYLEGUIDE_GO CODE_STYLEGUIDE_JS \
CODE_STYLEGUIDE_PYTHON EXISTING_REPO_CHECKLIST \
GO_HTTP_SERVER_CONVENTIONS LLM_PROSE_TELLS NEW_REPO_CHECKLIST; do
echo "new $name" > "$dir/canonical/prompts/$name.md"
done
mkdir -p "$dir/canonical/.gitea/workflows"
echo "name: check" > "$dir/canonical/.gitea/workflows/check.yml"
}
# commit_canonical: commit the canonical files as they stand. The stub
# curl serves only the newest commit; every other commit gets a 404.
commit_canonical() {
git -C "$dir/canonical" add .gitignore .dockerignore .editorconfig \
.golangci.yml .prettierrc prompts script
.golangci.yml .prettierrc .gitea prompts script
git -C "$dir/canonical" commit -q -m "Canonical"
HASH="$(git -C "$dir/canonical" rev-parse HEAD)"
}
@@ -174,7 +181,8 @@ EOF
}
# start_scenario <name>: make a new temp directory holding a repo with
# the outdated files, not yet committed, and an empty origin.
# a Go source file and the outdated files, not yet committed, and an
# empty origin.
start_scenario() {
scenario="$1"
rm -rf "$dir"
@@ -203,18 +211,46 @@ start_scenario() {
cp "$INSTALL" "$UPDATE_POLICY" "$dir/repo/script/"
printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname"
chmod +x "$dir/repo/script/projectname"
mkdir -p "$dir/repo/cmd/app"
echo "package main" > "$dir/repo/cmd/app/main.go"
write_outdated_files
}
# commit_and_push_fixture <paths...>: commit those paths, remove the
# rest of the outdated files, and record the commit as $before.
# commit_and_push_fixture <paths...>: commit those paths as they stand
# and every other shared file as the canonical file has it, so the
# script changes only those paths. Record the commit as $before.
commit_and_push_fixture() {
(
cd "$dir/repo"
git add script "$@"
git add script cmd "$@"
git clean -q -f -x
for file in $(git -C "$dir/canonical" ls-files); do
case "$file" in
script/*) continue ;;
prompts/REPO_POLICIES.md) path="REPO_POLICIES.md" ;;
prompts/*) path="docs/${file#prompts/}" ;;
*) path="$file" ;;
esac
if [ -z "$(git ls-files -- "$path")" ]; then
mkdir -p "$(dirname "$path")"
cp "$dir/canonical/$file" "$path"
git add "$path"
fi
done
git commit -q -m "Fixture"
git push -q origin HEAD
git clean -q -f -x
)
before="$(git -C "$dir/repo" rev-parse HEAD)"
}
# remove_from_fixture <paths...>: delete those paths from the repo and
# its origin, and record the commit as $before.
remove_from_fixture() {
(
cd "$dir/repo"
git rm -q -r "$@"
git commit -q -m "Remove from fixture"
git push -q origin HEAD
)
before="$(git -C "$dir/repo" rev-parse HEAD)"
}
@@ -365,25 +401,55 @@ EOF
[ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left"
}
test_files_not_carried_are_skipped() {
start_scenario "files the repo does not carry skipped"
test_files_the_repo_lacks_are_written_whole() {
start_scenario "files the repo lacks are written whole"
mkdir "$dir/repo/docs"
mv "$dir/repo/REPO_POLICIES.md" "$dir/repo/docs/REPO_POLICIES.md"
commit_and_push_fixture docs/REPO_POLICIES.md
echo "print()" > "$dir/repo/tool.py"
echo "export {}" > "$dir/repo/web.ts"
commit_and_push_fixture docs/REPO_POLICIES.md tool.py web.ts
remove_from_fixture REPO_POLICIES.md .gitignore .golangci.yml .gitea \
docs/API_POLICIES.md docs/CODE_STYLEGUIDE_GO.md \
docs/CODE_STYLEGUIDE_JS.md docs/CODE_STYLEGUIDE_PYTHON.md
run_update_policy
expect_success
expect_same_as_canonical .gitignore
expect_same_as_canonical .golangci.yml
expect_same_as_canonical .gitea/workflows/check.yml
expect_file docs/REPO_POLICIES.md << 'EOF'
new policy
EOF
expect_absent REPO_POLICIES.md
expect_file docs/API_POLICIES.md << 'EOF'
new api policy
EOF
expect_absent API_POLICIES.md
expect_absent docs/API_POLICIES.md
expect_absent .gitignore
expect_absent .dockerignore
expect_absent .editorconfig
expect_file docs/CODE_STYLEGUIDE_GO.md << 'EOF'
new CODE_STYLEGUIDE_GO
EOF
expect_file docs/CODE_STYLEGUIDE_JS.md << 'EOF'
new CODE_STYLEGUIDE_JS
EOF
expect_file docs/CODE_STYLEGUIDE_PYTHON.md << 'EOF'
new CODE_STYLEGUIDE_PYTHON
EOF
expect_new_commit_not_pushed
}
test_language_files_need_a_source_file() {
start_scenario "a repo with no source file of a language gets none of its files"
commit_and_push_fixture .prettierrc
remove_from_fixture cmd .golangci.yml docs/CODE_STYLEGUIDE_GO.md \
docs/GO_HTTP_SERVER_CONVENTIONS.md docs/CODE_STYLEGUIDE_JS.md \
docs/CODE_STYLEGUIDE_PYTHON.md
run_update_policy
expect_success
expect_absent .golangci.yml
expect_absent .prettierrc
expect_absent docs/CODE_STYLEGUIDE_GO.md
expect_absent docs/GO_HTTP_SERVER_CONVENTIONS.md
expect_absent docs/CODE_STYLEGUIDE_JS.md
expect_absent docs/CODE_STYLEGUIDE_PYTHON.md
expect_new_commit_not_pushed
}
@@ -463,6 +529,19 @@ EOF
[ -z "$(ls -A "$dir/tmp")" ] || fail "a temporary directory is left"
}
test_install_without_script_directory() {
start_scenario "script/install with a path writes both scripts into a clone with no script directory"
commit_and_push_fixture .prettierrc
remove_from_fixture script
run_install "$dir/repo"
expect_success
expect_same_as_canonical script/install
expect_same_as_canonical script/update-policy
expect_executable script/install
expect_executable script/update-policy
expect_no_commit
}
test_install_without_argument_updates_own_repo() {
start_scenario "script/install with no argument updates the repo it sits in"
commit_and_push_fixture .prettierrc
@@ -580,6 +659,21 @@ EOF
expect_no_commit
}
test_ignored_file_stops() {
start_scenario "an ignored file at a listed path is not overwritten"
commit_and_push_fixture .prettierrc
remove_from_fixture docs/API_POLICIES.md
echo "docs/API_POLICIES.md" >> "$dir/repo/.git/info/exclude"
echo "our own notes" > "$dir/repo/docs/API_POLICIES.md"
run_install "$dir/repo"
expect_failure
expect_output "docs/API_POLICIES.md is ignored by git"
expect_file docs/API_POLICIES.md << 'EOF'
our own notes
EOF
expect_no_commit
}
test_own_script_stops() {
start_scenario "a tracked script/install with a line the canonical file has never held stops"
echo "echo our own installer" >> "$dir/repo/script/install"
@@ -731,11 +825,13 @@ test_line_removed_from_another_file_still_stops() {
main() {
test_own_entries_kept
test_files_not_carried_are_skipped
test_files_the_repo_lacks_are_written_whole
test_language_files_need_a_source_file
test_nothing_changed
test_target_lacking_install_gets_both_scripts
test_update_policy_replaces_itself
test_install_with_path_commits_nothing
test_install_without_script_directory
test_install_without_argument_updates_own_repo
test_install_bad_target
test_failed_fetch_stops
@@ -744,6 +840,7 @@ main() {
test_own_entry_above_closing_comment
test_uncommitted_change_stops
test_untracked_script_stops
test_ignored_file_stops
test_own_script_stops
test_gitignore_lacks_canonical_last_line
test_fetched_golangci_without_deny_list