cce workspace root: member list, lockfile and dependency pins
git clone https://git.lucas.co/cce.git
bump-revs.sh (8.3K)
1 #!/bin/sh
2 # Bump the pinned revs that let a single crate be installed on its own.
3 #
4 # Each app declares its workspace-internal dependencies as git dependencies on
5 # GitHub (the crates' origin), pinned to an exact rev, while the workspace root patches those
6 # sources back to the local crates. That split is what lets one app be cloned
7 # and built alone -- and it is also exactly why the pins rot silently: inside
8 # this workspace the [patch] block always wins, so a stale rev never fails a
9 # build here. It surfaces only as a standalone build of an app quietly
10 # compiling an old copy of the toolkit. Nothing warns you. Hence this script.
11 #
12 # Run it after pushing a shared crate (cce-ui, cce-window-manager).
13 #
14 # bump-revs.sh [--dry-run] [--commit] [dep...]
15 #
16 # With no dep named, every dependency that appears in a git pin is considered.
17 #
18 # WHAT IT PINS TO: the head of the dependency's branch on its `origin` remote
19 # (GitHub, since 2026-09-20 -- the bare repos under ~/git are gone) -- not the
20 # work tree's HEAD. A rev that exists only in a work tree is fetchable by
21 # nobody, so pinning it would write manifests that resolve on this machine and
22 # nowhere else. If the dependency's work tree has uncommitted changes or
23 # commits not yet on origin, that is reported and the run fails rather than
24 # pinning something stale; push it first (the post-commit hook normally has).
25 # An origin ahead of the work tree is fine and is pinned as-is -- it is what
26 # others can actually fetch.
27 #
28 # The pins name GitHub directly (since 2026-09-21; before that git.lucas.co,
29 # which only mirrors GitHub hourly, so a fresh pin was unfetchable for up to an
30 # hour), so a rev pinned right after a push resolves at once.
31 #
32 # A manifest that should have changed but did not fails the run. Silently
33 # skipping is what let 21 repos sit unpushed for a day; the same rule applies
34 # here.
35
36 set -eu
37
38 ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
39 DRY=""
40 COMMIT=""
41 WANT=""
42
43 for arg in "$@"; do
44 case "$arg" in
45 --dry-run) DRY=1 ;;
46 --commit) COMMIT=1 ;;
47 -h|--help) sed -n '2,34p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
48 -*) echo "unknown option: $arg" >&2; exit 2 ;;
49 *) WANT="$WANT $arg" ;;
50 esac
51 done
52
53 [ -f "$ROOT/Cargo.toml" ] || { echo "no Cargo.toml beside $0" >&2; exit 1; }
54 grep -q '^\[workspace\]' "$ROOT/Cargo.toml" || {
55 echo "$ROOT/Cargo.toml is not a workspace root" >&2; exit 1; }
56
57 say() { printf '%s\n' "$*"; }
58
59 # Every git pin in the workspace, as "crate dep rev". The manifests are written
60 # by this script and by hand in one uniform shape, so one pattern covers them;
61 # a line that drifts out of that shape stops matching and is reported below as
62 # a manifest that did not change, rather than being quietly left behind.
63 pins() {
64 for m in "$ROOT"/*/Cargo.toml; do
65 crate=$(basename "$(dirname "$m")")
66 sed -n 's|^\([a-z0-9-]*\) = { git = "https://github\.com/lsgalante/\1\.git", rev = "\([0-9a-f]\{40\}\)" }$|'"$crate"' \1 \2|p' "$m"
67 done
68 }
69
70 ALL_PINS=$(pins)
71 [ -n "$ALL_PINS" ] || { say "no git pins found under $ROOT"; exit 0; }
72
73 DEPS=$(printf '%s\n' "$ALL_PINS" | awk '{print $2}' | sort -u)
74 if [ -n "$WANT" ]; then
75 for w in $WANT; do
76 printf '%s\n' "$DEPS" | grep -qx "$w" || {
77 echo "$w is not pinned by any crate here" >&2; exit 1; }
78 done
79 DEPS=$(printf '%s' "$WANT" | tr ' ' '\n' | sed '/^$/d')
80 fi
81
82 [ -n "$DRY" ] && say "DRY RUN -- nothing will be written"
83
84 # Resolve each dependency to the rev that others can actually fetch, refusing
85 # to pin anything that is only local. Collected first, so a blocked dependency
86 # stops the run before any manifest is touched.
87 TARGETS=""
88 BLOCKED=""
89 for dep in $DEPS; do
90 wt="$ROOT/$dep"
91 [ -d "$wt/.git" ] || { say "!! $dep: no work tree at $wt"; BLOCKED="$BLOCKED $dep"; continue; }
92 branch=$(git -C "$wt" symbolic-ref --quiet --short HEAD) || {
93 say "!! $dep: detached HEAD -- check out its branch first"; BLOCKED="$BLOCKED $dep"; continue; }
94 url=$(git -C "$wt" remote get-url origin 2>/dev/null) || {
95 say "!! $dep: no origin remote"; BLOCKED="$BLOCKED $dep"; continue; }
96 # Asked of the remote itself, not a possibly stale remote-tracking ref:
97 # what others can fetch is what origin has right now.
98 new=$(git -C "$wt" ls-remote --quiet "$url" "refs/heads/$branch" 2>/dev/null | cut -f1)
99 [ -n "$new" ] || {
100 say "!! $dep: origin ($url) has no branch $branch -- push it first"; BLOCKED="$BLOCKED $dep"; continue; }
101
102 if [ -n "$(git -C "$wt" status --porcelain --untracked-files=no)" ]; then
103 say "!! $dep: uncommitted changes -- commit and push before pinning"
104 BLOCKED="$BLOCKED $dep"; continue
105 fi
106 wt_head=$(git -C "$wt" rev-parse HEAD)
107 if [ "$wt_head" != "$new" ]; then
108 # Make sure the local history knows the remote rev before comparing;
109 # a fetch is cheap and the ancestor test is meaningless without it.
110 git -C "$wt" fetch --quiet "$url" "refs/heads/$branch" 2>/dev/null || true
111 if git -C "$wt" merge-base --is-ancestor "$new" "$wt_head" 2>/dev/null; then
112 ahead=$(git -C "$wt" rev-list --count "$new..$wt_head")
113 say "!! $dep: work tree is $ahead commit(s) ahead of origin/$branch"
114 say " push it first, or the pin misses that work:"
115 say " git -C $wt push origin $branch"
116 BLOCKED="$BLOCKED $dep"; continue
117 elif ! git -C "$wt" merge-base --is-ancestor "$wt_head" "$new" 2>/dev/null; then
118 say "!! $dep: work tree and origin/$branch have diverged -- reconcile first"
119 BLOCKED="$BLOCKED $dep"; continue
120 fi
121 fi
122 TARGETS="$TARGETS $dep=$new"
123 done
124
125 if [ -n "$BLOCKED" ]; then
126 say ""
127 say "blocked:$BLOCKED -- nothing written"
128 say "name the other dependencies explicitly to bump them anyway, e.g."
129 say " $(basename "$0")$(printf '%s\n' "$DEPS" | grep -vx "$(printf '%s' "$BLOCKED" | tr -d ' ')" | tr '\n' ' ' | sed 's/ $//' | sed 's/^/ /')"
130 exit 1
131 fi
132
133 # Apply. A crate already at the target rev is left alone and reported as such,
134 # so the output distinguishes "nothing to do" from "did nothing".
135 CHANGED=""
136 UNCHANGED=0
137 for t in $TARGETS; do
138 dep=${t%%=*}
139 new=${t#*=}
140 say "$dep -> $(printf '%.8s' "$new")"
141 printf '%s\n' "$ALL_PINS" | while read -r crate d old; do
142 [ "$d" = "$dep" ] || continue
143 [ "$old" = "$new" ] && { echo "SAME $crate"; continue; }
144 echo "EDIT $crate $old"
145 done > "${TMPDIR:-/tmp}/bump-revs.$$"
146
147 while read -r verb crate old; do
148 case "$verb" in
149 SAME) UNCHANGED=$((UNCHANGED + 1)) ;;
150 EDIT)
151 m="$ROOT/$crate/Cargo.toml"
152 say " $crate"
153 if [ -z "$DRY" ]; then
154 sed -i "s|^$dep = { git = \"https://github.com/lsgalante/$dep.git\", rev = \"$old\" }$|$dep = { git = \"https://github.com/lsgalante/$dep.git\", rev = \"$new\" }|" "$m"
155 grep -q "rev = \"$new\"" "$m" || {
156 say "!! $crate: manifest did not change -- pin format drifted?"; exit 1; }
157 else
158 printf ' would: %s %s -> %s\n' "$crate" "$(printf '%.8s' "$old")" "$(printf '%.8s' "$new")"
159 fi
160 CHANGED="$CHANGED $crate"
161 ;;
162 esac
163 done < "${TMPDIR:-/tmp}/bump-revs.$$"
164 rm -f "${TMPDIR:-/tmp}/bump-revs.$$"
165 done
166
167 CHANGED=$(printf '%s' "$CHANGED" | tr ' ' '\n' | sed '/^$/d' | sort -u)
168 COUNT=$(printf '%s' "$CHANGED" | grep -c . || true)
169
170 say ""
171 if [ "$COUNT" = 0 ]; then
172 say "every pin already current ($UNCHANGED) -- nothing to do"
173 exit 0
174 fi
175 if [ "$UNCHANGED" -gt 0 ]; then
176 say "$COUNT crate(s) repinned, $UNCHANGED already current"
177 else
178 say "$COUNT crate(s) repinned"
179 fi
180
181 if [ -n "$COMMIT" ] && [ -z "$DRY" ]; then
182 say ""
183 say "committing:"
184 for crate in $CHANGED; do
185 ( cd "$ROOT/$crate" && git add Cargo.toml && git commit --quiet -m "Repin workspace dependencies to their published revs
186
187 The pinned revs only affect builds outside this workspace, where an app is
188 cloned on its own, so a stale pin never fails a build here. Bumped by
189 bump-revs.sh after the dependency was pushed." ) && say " $crate"
190 done
191 say ""
192 say "committed -- each crate's post-commit hook pushes it to origin (GitHub);"
193 say "a crate without the hook still needs: git -C <crate> push origin <branch>"
194 else
195 say "review, then commit in each crate (or re-run with --commit)"
196 fi