The trash name has the time to the second, so two removals of one repo in the same second gave the same name, and mv put the second repo inside the first one's directory, where restore and gc never saw it. A name taken gets .2, .3 and so on, and restore picks the newest by the time and that suffix alone, since repo names may hold dots.
- commit
- 6f5e766c14b0996f54a268d452da1ffe91bc977d
- parent
- 6d291bcbe3a9c42c35ae132b88f6083a0e0ccfa7
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 66s, signed by cat
- kind
- fix risk score 1.2 of 3, Jev's estimate
- review
- #2 Deploy a commit, not the working tree
ci attestation
forge-attestation 1 type ci commit 6f5e766c14b0996f54a268d452da1ffe91bc977d tree de1db1c000249dbdde1adaaf8612d2b9c8d67df9 script .forge/ci 05d612a331a46d602928c28846e036e53299df48 result pass exit 0 started 2026-10-18T22:05:42Z duration 66s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:0e0a9f19b1928a8b7bd8d3441ff123b8169bedb9fe1043bbde0fc816902ae400 234 lines |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |1630ea7 running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |1630ea7 pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |46aea39 running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |46aea39 fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |a63a51e running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |a63a51e pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed 000ba55 -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |8c84a34 running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |8c84a34 pass in 0s, signed |pushed main |42eb374 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |42eb374 pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: deployed 1aa7db4 -> /sites/dami/forge/guide/ |(node:98994) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:99117) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:99253) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:99382) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |(node:150) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 0s -> /Users/dami/Library/Caches/forge/ci/35f7a65d0235/demo/root/.forge/www |226 of 226 pages render the same without 11ty |guide: 4 files, links ok |ok
Signature: good, cat
| M | bin/ | 11 | +++++++++-- |
| M | test/ | 13 | +++++++++++++ |
@@ -1288,6 +1288,10 @@ repo_rm() {
12881288 trash=$FORGE_STATE/trash/$owner12891289 mkdir -p "$trash"12901290 t=$(date -u +%Y%m%dT%H%M%SZ)1291+ # Two removals in one second get .2, .3, ...: mv onto an existing1292+ # directory would put the repo inside it.1293+ n=11294+ while [ -e "$trash/$name.git.$t" ]; do n=$((n + 1)) t=${t%%.*}.$n; done12911295 mv "$FORGE_ROOT/$1.git" "$trash/$name.git.$t"12921296 # The time it was removed, which gc counts from.12931297 touch "$trash/$name.git.$t"@@ -1308,8 +1312,11 @@ repo_restore() {
13081312 owner=${1%%/*} name=${1#*/}13091313 valid_name "$owner" && valid_name "$name" && [ "$owner/$name" = "$1" ] || die "invalid repo: $1"13101314 [ ! -e "$FORGE_ROOT/$1.git" ] || die "$1 exists; remove or rename it first"1311- from=$(ls -d "$FORGE_STATE/trash/$owner/$name.git."* 2>/dev/null | sort | tail -n 1)1312- [ -n "$from" ] || die "no removed $1 in the trash"1315+ # Newest last: by the time after <name>.git., then by the .N of a second1316+ # removal within that second (names may hold dots, times don't).1317+ when=$(cd "$FORGE_STATE/trash/$owner" 2>/dev/null && for x in "$name.git."*; do [ -e "$x" ] && printf '%s\n' "${x#"$name.git."}"; done | sort -t . -k 1,1 -k 2,2n | tail -n 1)1318+ [ -n "$when" ] || die "no removed $1 in the trash"1319+ from=$FORGE_STATE/trash/$owner/$name.git.$when13131320 mkdir -p "$FORGE_ROOT/$owner"13141321 mv "$from" "$FORGE_ROOT/$1.git"13151322 repo_setup "$FORGE_ROOT/$1.git"@@ -55,3 +55,16 @@ eq "$(ls "$FORGE_ROOT/.forge/trash/dami" | grep -c '^greet\.git\.')" 1
5555 ok env FORGE_TRASH_DAYS=0 "$FORGE" gc --age 05656 output | grep -q '^removed trash dami/greet\.git\.' || fail "gc removed no trash"5757 eq "$(ls "$FORGE_ROOT/.forge/trash/dami" | grep -c '^greet\.git\.' || true)" 058+59+# A dotted name removed twice, likely within one second: two entries side60+# by side, never one inside the other, and restore takes the newer.61+repo dami/klot.ski62+git --git-dir="$FORGE_ROOT/dami/klot.ski.git" config forge.mark first63+ok forge repo rm dami/klot.ski64+repo dami/klot.ski65+git --git-dir="$FORGE_ROOT/dami/klot.ski.git" config forge.mark second66+ok forge repo rm dami/klot.ski67+eq "$(ls "$FORGE_ROOT/.forge/trash/dami" | grep -c '^klot\.ski\.git\.')" 268+eq "$(find "$FORGE_ROOT/.forge/trash" -mindepth 3 -maxdepth 3 -name '*.git.*' | wc -l | tr -d ' ')" 069+ok forge repo restore dami/klot.ski70+eq "$(git --git-dir="$FORGE_ROOT/dami/klot.ski.git" config forge.mark)" second