git.lucas.co / cce
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