cce workspace root: member list, lockfile and dependency pins
git clone https://git.lucas.co/cce.git
bump-revs: commit only the repin, and fail when a commit fails
--commit ran `git add Cargo.toml` and then a bare `git commit`. Anything
else staged in a dependent went into the Repin commit and was pushed, and
so did uncommitted edits in that crate's manifest, which is often another
session's work in progress. A crate whose Cargo.toml or Cargo.lock is
already dirty now blocks the run before anything is written. The commit
names its two paths after `--`, so other staged files stay staged.
The commit ran as `( ... ) && say`, which set -e does not stop, so a
failed commit still printed "committed". Failures are now listed and the
run exits 1.
Also: with two or more blocked dependencies, the re-run hint glued their
names into one grep pattern and suggested re-running with them anyway.
With all blocked it suggested a bare re-run, meaning everything. The temp
file was a predictable /tmp/bump-revs.$$ left behind by an early exit; it
is now one mktemp -d removed by a single trap. A dependency checked out as
a git worktree (.git is a file) is no longer refused. A dry run now says
"would be repinned".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bump-revs.sh | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 79 insertions(+), 13 deletions(-)
diff --git a/bump-revs.sh b/bump-revs.sh
index 69fee29..dbe6f8c 100755
--- a/bump-revs.sh
+++ b/bump-revs.sh
@@ -53,6 +53,14 @@
# the pattern had already matched. Now every `git = "https://github.com/
# lsgalante/..."` line the pattern does not match is reported, and one naming
# a dependency being bumped fails the run before anything is written.
+#
+# A REPIN TOUCHES ONLY THE REPIN. A crate whose Cargo.toml or Cargo.lock
+# already has uncommitted changes blocks the run before anything is written,
+# and --commit commits exactly those two paths, leaving anything else staged
+# where it was. Until 2026-10-01 it ran `git add` then a bare `git commit`, so
+# whatever another session had staged, or half-edited in the manifest, went
+# out in the "Repin" commit and was pushed with it. A commit that fails now
+# fails the run.
set -eu
@@ -77,6 +85,12 @@ grep -q '^\[workspace\]' "$ROOT/Cargo.toml" || {
say() { printf '%s\n' "$*"; }
+# Scratch space for the whole run, removed however the run ends -- an early
+# `exit 1` included, which used to leave a predictable /tmp/bump-revs.$$
+# behind.
+SCRATCH=$(mktemp -d)
+trap 'rm -rf "$SCRATCH"' EXIT
+
# Every git pin in the workspace, as "crate dep rev". A pin is
# dep = { git = "https://github.com/lsgalante/dep.git", rev = "<sha>"[, more] }
# -- the git URL and rev first, then any other keys (features, optional, ...),
@@ -149,7 +163,8 @@ TARGETS=""
BLOCKED=""
for dep in $DEPS; do
wt="$ROOT/$dep"
- [ -d "$wt/.git" ] || { say "!! $dep: no work tree at $wt"; BLOCKED="$BLOCKED $dep"; continue; }
+ # -e, not -d: a checkout made with `git worktree add` has a .git FILE.
+ [ -e "$wt/.git" ] || { say "!! $dep: no work tree at $wt"; BLOCKED="$BLOCKED $dep"; continue; }
branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD) || {
say "!! $dep: detached HEAD -- check out its branch first"; BLOCKED="$BLOCKED $dep"; continue; }
url=$(git -C "$wt" remote get-url origin 2>/dev/null) || {
@@ -186,8 +201,38 @@ done
if [ -n "$BLOCKED" ]; then
say ""
say "blocked:$BLOCKED -- nothing written"
- say "name the other dependencies explicitly to bump them anyway, e.g."
- say " $(basename "$0")$(printf '%s\n' "$DEPS" | grep -vx "$(printf '%s' "$BLOCKED" | tr -d ' ')" | tr '\n' ' ' | sed 's/ $//' | sed 's/^/ /')"
+ # One blocked name per line: grep reads each line as its own pattern.
+ # (`tr -d ' '` used to glue two blocked names into one pattern that
+ # matched nothing, so the hint suggested re-running with them included.)
+ rest=$(printf '%s\n' "$DEPS" \
+ | grep -vxF "$(printf '%s' "$BLOCKED" | tr ' ' '\n' | sed '/^$/d')" \
+ | tr '\n' ' ' | sed 's/ $//')
+ if [ -n "$rest" ]; then
+ say "name the other dependencies explicitly to bump them anyway, e.g."
+ say " $(basename "$0") $rest"
+ fi
+ exit 1
+fi
+
+# A crate this run would repin must not already have uncommitted changes in
+# its manifest or lock: the repin would be committed on top of them -- often
+# another session's work in progress -- and pushed with it, and the lock
+# refresh would resolve against them too. Checked before anything is written.
+DIRTY=""
+for t in $TARGETS; do
+ dep=${t%%=*}
+ new=${t#*=}
+ for crate in $(printf '%s\n' "$ALL_PINS" | awk -v d="$dep" -v n="$new" '$2 == d && $3 != n { print $1 }'); do
+ case " $DIRTY " in *" $crate "*) continue ;; esac
+ if [ -n "$(git -C "$ROOT/$crate" status --porcelain --untracked-files=no -- Cargo.toml Cargo.lock 2>&1)" ]; then
+ say "!! $crate: Cargo.toml or Cargo.lock has uncommitted changes -- commit or stash them first"
+ DIRTY="$DIRTY $crate"
+ fi
+ done
+done
+if [ -n "$DIRTY" ]; then
+ say ""
+ say "blocked: would repin on top of uncommitted changes in$DIRTY -- nothing written"
exit 1
fi
@@ -203,7 +248,7 @@ for t in $TARGETS; do
[ "$d" = "$dep" ] || continue
[ "$old" = "$new" ] && { echo "SAME $crate"; continue; }
echo "EDIT $crate $old"
- done > "${TMPDIR:-/tmp}/bump-revs.$$"
+ done > "$SCRATCH/plan"
while read -r verb crate old; do
case "$verb" in
@@ -223,8 +268,7 @@ for t in $TARGETS; do
CHANGED="$CHANGED $crate"
;;
esac
- done < "${TMPDIR:-/tmp}/bump-revs.$$"
- rm -f "${TMPDIR:-/tmp}/bump-revs.$$"
+ done < "$SCRATCH/plan"
done
CHANGED=$(printf '%s' "$CHANGED" | tr ' ' '\n' | sed '/^$/d' | sort -u)
@@ -240,10 +284,10 @@ lock_tracked() {
# standalone clone would: the committed tree plus the work tree's manifest and
# lock, in a directory outside the workspace (inside it cargo would find the
# root and ignore this lock entirely).
-WORK=""
+WORK="$SCRATCH/locks"
refresh_lock() {
crate=$1
- [ -n "$WORK" ] || { WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT; }
+ mkdir -p "$WORK"
case "$WORK" in "$ROOT"/*)
say "!! temp dir $WORK is inside the workspace -- set TMPDIR elsewhere"; return 1 ;;
esac
@@ -291,25 +335,47 @@ if [ "$COUNT" = 0 ]; then
say "every pin already current ($UNCHANGED) -- nothing to do"
exit 0
fi
+if [ -n "$DRY" ]; then
+ repinned="would be repinned"
+else
+ repinned="repinned"
+fi
if [ "$UNCHANGED" -gt 0 ]; then
- say "$COUNT crate(s) repinned, $UNCHANGED already current"
+ say "$COUNT crate(s) $repinned, $UNCHANGED already current"
else
- say "$COUNT crate(s) repinned"
+ say "$COUNT crate(s) $repinned"
fi
-if [ -n "$COMMIT" ] && [ -z "$DRY" ]; then
+if [ -n "$DRY" ]; then
+ say "re-run without --dry-run to write them"
+elif [ -n "$COMMIT" ]; then
say ""
say "committing:"
+ FAILED=""
for crate in $CHANGED; do
files="Cargo.toml"
lock_tracked "$crate" && files="$files Cargo.lock"
- ( cd "$ROOT/$crate" && git add $files && git commit --quiet -m "Repin workspace dependencies to their published revs
+ # The paths after `--` are the whole commit: anything else already
+ # staged in the crate stays staged and out of it. A failure is
+ # counted, not swallowed -- `( ... ) && say` here once let a failed
+ # commit pass under set -e and still print "committed".
+ if git -C "$ROOT/$crate" commit --quiet -m "Repin workspace dependencies to their published revs
The pinned revs only affect builds outside this workspace, where an app is
cloned on its own, so a stale pin never fails a build here. Bumped by
bump-revs.sh after the dependency was pushed; Cargo.lock (when committed)
-is re-resolved to match." ) && say " $crate"
+is re-resolved to match." -- $files; then
+ say " $crate"
+ else
+ say "!! $crate: commit failed"
+ FAILED="$FAILED $crate"
+ fi
done
+ if [ -n "$FAILED" ]; then
+ say ""
+ say "not committed:$FAILED -- their manifests are edited in place; commit them by hand"
+ exit 1
+ fi
say ""
say "committed -- each crate's post-commit hook pushes it to origin (GitHub);"
say "a crate without the hook still needs: git -C <crate> push origin <branch>"