diff --git a/prompts/EXISTING_REPO_CHECKLIST.md b/prompts/EXISTING_REPO_CHECKLIST.md index 87197ab..8c3849d 100644 --- a/prompts/EXISTING_REPO_CHECKLIST.md +++ b/prompts/EXISTING_REPO_CHECKLIST.md @@ -215,10 +215,11 @@ with your task. `make update-policy` shims to it. Before its first use, the repo's own entries in `.gitignore`, `.editorconfig` and `.dockerignore` go below the comment that ends each canonical file. An entry above that comment which - the canonical file lacks makes the script stop before it writes anything, - naming the file and the entry. It stops the same way on a line in - `.golangci.yml` that the canonical file lacks, apart from comments and the - repo's entries in the `deny` list of the `test-support` rule. + the canonical file has never held makes the script stop before it writes + anything, naming the file and the entry. It stops the same way on a line + in `.golangci.yml` that the canonical file has never held, apart from + comments and the repo's entries in the `deny` list of the `test-support` + rule. - [ ] README has an **Entrypoints** section documenting the `script/` entrypoints and linking the standard - [ ] `script/lint` and `script/test` each run diff --git a/script/update-policy b/script/update-policy index 021ff4b..eaf4bea 100755 --- a/script/update-policy +++ b/script/update-policy @@ -11,10 +11,10 @@ # merged. A repo's own entries are kept: # - .gitignore, .editorconfig, .dockerignore: the lines below the # canonical file's last line. An entry above that line which the -# canonical file lacks stops the script. +# canonical file has never held stops the script. # - .golangci.yml: the entries the repo added to the deny list of the -# test-support rule. Any other line the canonical file lacks stops -# the script, unless it is blank or a comment. +# test-support rule. Any other line the canonical file has never +# held stops the script, unless it is blank or a comment. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" @@ -58,18 +58,19 @@ local_path() { } # local_only_lines [end]: print each line of the local -# file, above line end if given, that the fetched file lacks. Blank -# lines and comments do not count. +# file, above line end if given, that the canonical file has never held: +# not in the fetched file or in .former, the lines earlier +# commits removed from it. Blank lines and comments do not count. local_only_lines() { - awk -v end="${3:-}" ' - NR == FNR { fetched[$0]; next } - (end == "" || FNR < end) && !/^[ \t]*(#|$)/ && !($0 in fetched) - ' "$1" "$2" + cat "$1" "$1.former" | awk -v end="${3:-}" ' + NR == FNR { held[$0]; next } + (end == "" || FNR < end) && !/^[ \t]*(#|$)/ && !($0 in held) + ' - "$2" } # keep_lines_below_last : append to the fetched file -# whatever follows its last line in the local file. Stop if a line above -# that one is missing from the fetched file, unless it is blank or a +# whatever follows its last line in the local file. Stop if the canonical +# file has never held a line above that one, unless it is blank or a # comment: replacing the file would delete it. keep_lines_below_last() { last="$(tail -n 1 "$1")" @@ -88,8 +89,9 @@ $own" # keep_deny_entries : add to the deny list of the # test-support rule in the fetched file the entries only the local file # has. An entry is a "- pkg:" line, the lines under it and any comment -# above it. Stop if the local file still has a line the result lacks, -# unless it is blank or a comment: replacing the file would delete it. +# above it. Stop if the local file still has a line that neither the +# result nor the canonical file has ever held, unless it is blank or a +# comment: replacing the file would delete it. keep_deny_entries() { awk ' function indent(line) { return match(line, /[^ ]/) - 1 } @@ -143,7 +145,7 @@ keep_deny_entries() { mv "$1.merged" "$1" own="$(local_only_lines "$1" "$2")" if [ -n "$own" ]; then - die "$2 has lines the canonical file lacks; add each to the canonical file to keep it, or delete it: + die "$2 has lines the canonical file has never held; only the deny list of the test-support rule may differ from canonical: $own" fi } @@ -169,6 +171,9 @@ main() { tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT + git clone --quiet --bare "$PROMPTS_URL.git" "$tmp/prompts.git" || + die "cannot clone $PROMPTS_URL.git" + carried="" for file in $VENDORED_FILES; do path="$(local_path "$file")" @@ -181,6 +186,8 @@ main() { --output "$tmp/$path" \ "$PROMPTS_URL/raw/commit/$commit/$file" || die "cannot fetch $file at $commit" + git -C "$tmp/prompts.git" log -p --format= "$commit" -- "$file" | + sed -n 's/^-//p' > "$tmp/$path.former" carried="$carried $path" done [ -n "$carried" ] || die "this repo carries none of the vendored files" diff --git a/test/update-policy.sh b/test/update-policy.sh index 3573319..08bdee5 100755 --- a/test/update-policy.sh +++ b/test/update-policy.sh @@ -1,8 +1,9 @@ #!/bin/sh # test/update-policy.sh: test script/update-policy on throwaway repos. # Each scenario gets one temp directory holding a repo, its local bare -# origin, the canonical files and a stub curl that serves them, so -# nothing touches the network. +# origin, a local git repository of the canonical files in place of +# prompts, and a stub curl that serves them, so nothing touches the +# network. set -eu UPDATE_POLICY="$(cd "$(dirname "$0")/.." && pwd -P)/script/update-policy" @@ -82,6 +83,15 @@ EOF echo "new api policy" > "$dir/canonical/prompts/API_POLICIES.md" } +# 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 + git -C "$dir/canonical" commit -q -m "Canonical" + HASH="$(git -C "$dir/canonical" rev-parse HEAD)" +} + # write_outdated_files: this repo's files, one canonical version behind, # with the repo's own entries in the places the script keeps them. The # comment above the closing comment in .gitignore is replaced, not kept. @@ -131,18 +141,26 @@ EOF # the outdated files, not yet committed, and an empty origin. start_scenario() { scenario="$1" - # The one commit the stub curl serves; every other commit gets a 404. - export HASH=1111111111111111111111111111111111111111 rm -rf "$dir" dir="$(mktemp -d)" : > "$dir/output" write_stub_curl write_canonical_files + # git clone reads no repo's own config, so the setting that points + # git at the local canonical repository has to be in this file. + export GIT_CONFIG_GLOBAL="$dir/gitconfig" + git config --global user.name "Test" + git config --global user.email "test@example.com" + git config --global "url.$dir/canonical.insteadOf" \ + "https://git.eeqj.de/sneak/prompts.git" + + git init -q -b main "$dir/canonical" + export HASH + commit_canonical + git init -q --bare -b main "$dir/origin.git" git init -q -b main "$dir/repo" - git -C "$dir/repo" config user.name "Test" - git -C "$dir/repo" config user.email "test@example.com" git -C "$dir/repo" remote add origin "$dir/origin.git" mkdir "$dir/repo/script" @@ -476,16 +494,11 @@ test_short_commit_hash_refused() { expect_nothing_written } -# git is pointed at a local repository in place of the prompts remote, -# and the stub curl serves the head of that repository's main. +# git asks the local canonical repository for the head of main, and the +# stub curl serves only that commit. test_no_hash_uses_head_of_main() { start_scenario "with no hash the script uses the head of main" commit_and_push_fixture .prettierrc - git init -q --bare -b main "$dir/prompts.git" - git -C "$dir/repo" push -q "$dir/prompts.git" HEAD:refs/heads/main - git -C "$dir/repo" config "url.$dir/prompts.git.insteadOf" \ - "https://git.eeqj.de/sneak/prompts.git" - HASH="$before" run_update_policy expect_success expect_file .prettierrc << 'EOF' @@ -497,6 +510,39 @@ EOF fail "the commit was not pushed" } +# expect_former_canonical_line_replaced : an earlier +# canonical commit held the line and the newest does not. This repo's +# file still has it, and the script replaces it and pushes. +expect_former_canonical_line_replaced() { + cp "$dir/canonical/$1" "$dir/newest" + { echo "$2"; cat "$dir/newest"; } > "$dir/canonical/$1" + commit_canonical + cp "$dir/newest" "$dir/canonical/$1" + commit_canonical + { echo "$2"; cat "$dir/repo/$1"; } > "$dir/merged" + mv "$dir/merged" "$dir/repo/$1" + commit_and_push_fixture "$1" .prettierrc + + run_update_policy "$HASH" + expect_success + if grep -q -F -x -e "$2" "$dir/repo/$1"; then + fail "$1 still has: $2" + fi + [ "$(head_of_repo)" != "$before" ] || fail "no commit was made" + [ "$(head_of_origin)" = "$(head_of_repo)" ] || + fail "the commit was not pushed" +} + +test_former_canonical_golangci_line_replaced() { + start_scenario "a .golangci.yml line an earlier canonical commit held is replaced" + expect_former_canonical_line_replaced .golangci.yml "version: 1" +} + +test_former_canonical_gitignore_line_replaced() { + start_scenario "a .gitignore line above the closing comment that an earlier canonical commit held is replaced" + expect_former_canonical_line_replaced .gitignore "*.old" +} + main() { test_own_entries_kept test_files_not_carried_are_skipped @@ -513,6 +559,8 @@ main() { test_staged_file_stays_out_of_commit test_short_commit_hash_refused test_no_hash_uses_head_of_main + test_former_canonical_golangci_line_replaced + test_former_canonical_gitignore_line_replaced echo "update-policy tests passed" }