Skip to content

Commit b50ee39

Browse files
committed
fix: fix trace fix to also lookup release branches
Fix trace fix to also lookup release branches. Signed-off-by: Noel Georgi <git@frezbo.dev>
1 parent 027c93d commit b50ee39

1 file changed

Lines changed: 164 additions & 73 deletions

File tree

hack/trace-fix.sh

Lines changed: 164 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
# tools --(TOOLS_REV in pkgs/Pkgfile)--------> pkgs
99
# pkgs --(PKGS in talos/Makefile)-----------> talos
1010
#
11-
# For each hop, finds the first dependent-repo commit whose pin resolves to a
12-
# ref that has the fix commit as an ancestor.
11+
# Walks origin/main AND every origin/release-* branch, since fixes are often
12+
# backported as a separate commit with the same subject. Equivalence on a
13+
# backport branch is determined by direct ancestry first, then by commit
14+
# subject (matches conventional cherry-pick workflow).
1315
#
1416
# Usage:
1517
# trace-fix.sh \
@@ -18,16 +20,18 @@
1820
# --pkgs /path/to/pkgs \
1921
# --talos /path/to/talos \
2022
# --from <toolchain|tools|pkgs> \
21-
# --commit <sha>
23+
# --commit <sha> \
24+
# [--branch <name>]
2225
#
23-
# Only the repos needed for the chain from --from down to talos must be
24-
# supplied. All supplied repos are fetched from origin/main before queries.
25-
# Working trees are not modified.
26+
# --branch limits the trace to a single branch (e.g. "main" or "release-1.13").
27+
# Default: walk main + every origin/release-* branch where the fix is present.
28+
# All supplied repos are fetched from origin before queries. Working trees are
29+
# not modified.
2630

2731
set -euo pipefail
2832

2933
usage() {
30-
{ sed -nE 's/^# ?//p' "$0" | head -30; } >&2 || true
34+
{ sed -nE 's/^# ?//p' "$0" | head -33; } >&2 || true
3135
exit 1
3236
}
3337

@@ -37,6 +41,7 @@ PKGS_PATH=""
3741
TALOS_PATH=""
3842
FROM=""
3943
COMMIT=""
44+
BRANCH_FILTER=""
4045

4146
while [ $# -gt 0 ]; do
4247
case "$1" in
@@ -46,6 +51,7 @@ while [ $# -gt 0 ]; do
4651
--talos) TALOS_PATH="$2"; shift 2;;
4752
--from) FROM="$2"; shift 2;;
4853
--commit) COMMIT="$2"; shift 2;;
54+
--branch) BRANCH_FILTER="$2"; shift 2;;
4955
-h|--help) usage;;
5056
*) echo "unknown arg: $1" >&2; usage;;
5157
esac
@@ -87,28 +93,66 @@ for r in $CHAIN; do
8793
|| { echo "$p is not a git repo" >&2; exit 1; }
8894
done
8995

90-
# Fetch origin/main + tags for every repo. Queries run against origin/main so
91-
# local branch state is irrelevant and never modified.
96+
# Fetch origin's `main` plus every `release-*` branch (and all tags) for every
97+
# repo. Queries below run against refs/remotes/origin/{main,release-*} so local
98+
# branch state is irrelevant. Branches outside that set are intentionally not
99+
# fetched — `--branch <name>` is expected to target one of these refs.
92100
sync_repo() {
93101
name="$1"
94102
path=$(repo_path "$name")
95103
echo ">> fetch $name ($path)" >&2
96-
err=$(git -C "$path" fetch --tags --quiet origin main 2>&1) || {
97-
echo " WARN: fetch failed for $name (using last pulled origin/main; results may be stale):" >&2
104+
err=$(git -C "$path" fetch --tags --quiet origin '+refs/heads/main:refs/remotes/origin/main' '+refs/heads/release-*:refs/remotes/origin/release-*' 2>&1) || {
105+
echo " WARN: fetch failed for $name (using last pulled refs; results may be stale):" >&2
98106
printf ' %s\n' "$err" | sed 's/^/ /' >&2
99107
}
100-
local_branch=$(git -C "$path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "DETACHED")
101-
if [ "$local_branch" != "main" ]; then
102-
echo " note: $name working tree on '$local_branch' (queries use origin/main)" >&2
103-
fi
104108
}
105109

106110
for r in $CHAIN; do
107111
sync_repo "$r"
108112
done
109113

110-
# Resolve a pin string (e.g. "v1.14.0-alpha.0-2-gd6c7ac5" or "v1.14.0-alpha.0")
111-
# to a full SHA in the upstream repo. Empty on failure.
114+
# List branches to check in source repo: main + every origin/release-*
115+
# (sorted), respecting --branch filter.
116+
list_source_branches() {
117+
src="$1"
118+
{
119+
git -C "$src" show-ref --verify --quiet refs/remotes/origin/main && echo "main"
120+
git -C "$src" for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release-*' \
121+
| sed 's|^origin/||' \
122+
| sort -V
123+
}
124+
}
125+
126+
branch_exists() {
127+
repo="$1"; branch="$2"
128+
git -C "$repo" show-ref --verify --quiet "refs/remotes/origin/$branch"
129+
}
130+
131+
# Find equivalent of <target> on <branch> in <repo>.
132+
# 1. Direct ancestry: target itself.
133+
# 2. Exact-subject match: first commit on branch whose %s equals target's %s.
134+
# (Walks `git log --format='%H<TAB>%s'` rather than using `--grep`, which
135+
# matches the whole message and would surface unrelated commits with a
136+
# similar substring or body line. Tab is the separator because BSD awk
137+
# doesn't accept NUL as FS; commit subjects never contain tabs in
138+
# practice.)
139+
# Echoes full sha on success.
140+
equivalent_on_branch() {
141+
repo="$1"; branch="$2"; target="$3"
142+
if git -C "$repo" merge-base --is-ancestor "$target" "origin/$branch" 2>/dev/null; then
143+
echo "$target"
144+
return 0
145+
fi
146+
subj=$(git -C "$repo" log -1 --format='%s' "$target")
147+
eq=$(
148+
git -C "$repo" log --format='%H%x09%s' "origin/$branch" 2>/dev/null \
149+
| awk -v subj="$subj" -F '\t' '$2 == subj { print $1; exit }'
150+
)
151+
[ -n "$eq" ] && { echo "$eq"; return 0; }
152+
return 1
153+
}
154+
155+
# Resolve a pin string to a full SHA in the upstream repo. Empty on failure.
112156
resolve_ref() {
113157
repo="$1"; ref="$2"
114158
suffix=$(printf '%s\n' "$ref" | sed -nE 's/.*-g([0-9a-fA-F]+)$/\1/p')
@@ -119,25 +163,24 @@ resolve_ref() {
119163
fi
120164
}
121165

122-
# Walk commits on origin/main that touched <file>, oldest first, emitting
123-
# "<sha> <pin-value>" lines. <sed_extract> is a sed -nE script that prints the
124-
# pin value when applied to the file content.
166+
# Walk commits on origin/<branch> that touched <file>, oldest first.
125167
pin_history() {
126-
repo="$1"; file="$2"; sed_extract="$3"
127-
shas=$(git -C "$repo" log --reverse --format='%H' origin/main -- "$file")
168+
repo="$1"; branch="$2"; file="$3"; sed_extract="$4"
169+
shas=$(git -C "$repo" log --reverse --format='%H' "origin/$branch" -- "$file")
128170
for sha in $shas; do
129-
val=$(git -C "$repo" show "$sha:$file" 2>/dev/null | sed -nE "$sed_extract" | head -1 || true)
171+
val=$(git -C "$repo" show "$sha:$file" 2>/dev/null \
172+
| { sed -nE "$sed_extract" || true; } \
173+
| { head -1 || true; })
130174
[ -n "$val" ] && echo "$sha $val"
131175
done
132176
}
133177

134-
# Given dependent repo + pin location + upstream repo + target commit, find
135-
# the first dependent-repo commit whose pin resolves to a ref that has
136-
# <target> as an ancestor. Echoes "<dep-sha> <pin-value>" on success.
178+
# Find first commit on origin/<branch> in <dep_repo> whose pin resolves to a
179+
# ref that has <target> as an ancestor in <up_repo>. Echoes "<sha> <pin>".
137180
find_first_containing() {
138-
dep_repo="$1"; dep_file="$2"; sed_extract="$3"; up_repo="$4"; target="$5"
181+
dep_repo="$1"; branch="$2"; dep_file="$3"; sed_extract="$4"; up_repo="$5"; target="$6"
139182
tmp=$(mktemp)
140-
pin_history "$dep_repo" "$dep_file" "$sed_extract" > "$tmp"
183+
pin_history "$dep_repo" "$branch" "$dep_file" "$sed_extract" > "$tmp"
141184
prev_val=""
142185
rc=1
143186
while read -r sha val; do
@@ -155,7 +198,6 @@ find_first_containing() {
155198
return $rc
156199
}
157200

158-
# sed extractions for each pin location.
159201
SED_TOOLS_PIN='s|^[[:space:]]*TOOLCHAIN_IMAGE:[[:space:]]*ghcr\.io/siderolabs/toolchain:([^[:space:]]+).*$|\1|p'
160202
SED_PKGS_PIN='s|^[[:space:]]*TOOLS_REV:[[:space:]]*([^[:space:]]+).*$|\1|p'
161203
SED_TALOS_PIN='s|^PKGS[[:space:]]*\?=[[:space:]]*([^[:space:]]+).*$|\1|p'
@@ -174,62 +216,111 @@ print_commit_block() {
174216
return 0
175217
}
176218

177-
# Resolve full SHA of source commit in source repo.
178-
SRC_REPO="$FROM"
179-
SRC_PATH=$(repo_path "$SRC_REPO")
180-
if ! SRC_SHA=$(git -C "$SRC_PATH" rev-parse "$COMMIT" 2>/dev/null); then
181-
echo "commit $COMMIT not found in $SRC_REPO at $SRC_PATH" >&2
182-
exit 1
183-
fi
219+
# Trace one chain on a single branch. Args: branch, source-sha-on-branch.
220+
trace_chain() {
221+
branch="$1"; src_sha="$2"
184222

185-
print_commit_block "Source ($SRC_REPO)" "$SRC_REPO" "$SRC_SHA"
223+
echo
224+
echo "==========================================================="
225+
echo " branch: $branch"
226+
echo "==========================================================="
186227

187-
# Walk the chain. CARRY_REPO/CARRY_SHA is the upstream commit we're looking
188-
# for in the next-down repo's pin.
189-
CARRY_REPO="$SRC_REPO"
190-
CARRY_SHA="$SRC_SHA"
228+
print_commit_block "Source ($SRC_REPO @ $branch)" "$SRC_REPO" "$src_sha"
191229

192-
set -- $CHAIN
193-
shift # drop source repo; iterate dependents
194-
for DEP in "$@"; do
195-
DEP_PATH=$(repo_path "$DEP")
230+
CARRY_REPO="$SRC_REPO"
231+
CARRY_SHA="$src_sha"
196232

197-
case "$DEP" in
198-
tools) DEP_FILE="Pkgfile"; SED="$SED_TOOLS_PIN";;
199-
pkgs) DEP_FILE="Pkgfile"; SED="$SED_PKGS_PIN";;
200-
talos) DEP_FILE="Makefile"; SED="$SED_TALOS_PIN";;
201-
esac
233+
set -- $CHAIN
234+
shift
235+
for DEP in "$@"; do
236+
DEP_PATH=$(repo_path "$DEP")
202237

203-
CARRY_PATH=$(repo_path "$CARRY_REPO")
238+
if ! branch_exists "$DEP_PATH" "$branch"; then
239+
echo
240+
echo " $DEP: no origin/$branch branch — chain stops here."
241+
return 0
242+
fi
204243

205-
if ! result=$(find_first_containing "$DEP_PATH" "$DEP_FILE" "$SED" "$CARRY_PATH" "$CARRY_SHA"); then
206-
short=$(git -C "$CARRY_PATH" rev-parse --short "$CARRY_SHA")
207-
echo >&2
208-
echo "$DEP: no pin on origin/main yet contains $CARRY_REPO commit $short." >&2
209-
exit 2
210-
fi
244+
case "$DEP" in
245+
tools) DEP_FILE="Pkgfile"; SED="$SED_TOOLS_PIN";;
246+
pkgs) DEP_FILE="Pkgfile"; SED="$SED_PKGS_PIN";;
247+
talos) DEP_FILE="Makefile"; SED="$SED_TALOS_PIN";;
248+
esac
249+
250+
CARRY_PATH=$(repo_path "$CARRY_REPO")
251+
252+
if ! result=$(find_first_containing "$DEP_PATH" "$branch" "$DEP_FILE" "$SED" "$CARRY_PATH" "$CARRY_SHA"); then
253+
short=$(git -C "$CARRY_PATH" rev-parse --short "$CARRY_SHA")
254+
echo
255+
echo " $DEP @ $branch: no pin yet contains $CARRY_REPO commit $short."
256+
return 0
257+
fi
211258

212-
DEP_SHA=$(echo "$result" | awk '{print $1}')
213-
PIN_VAL=$(echo "$result" | awk '{print $2}')
214-
215-
EXTRA=""
216-
if [ "$DEP" = "talos" ]; then
217-
DESC=$(git -C "$DEP_PATH" describe --tags "$DEP_SHA" 2>/dev/null || true)
218-
CONTAIN=$(git -C "$DEP_PATH" tag --contains "$DEP_SHA" 2>/dev/null \
219-
| { paste -sd ',' - || true; } \
220-
| sed 's/,/, /g')
221-
REL=$(git -C "$DEP_PATH" describe --contains "$DEP_SHA" 2>/dev/null || true)
222-
N_BACK=$(printf '%s\n' "$DESC" | sed -nE 's/.*-([0-9]+)-g[0-9a-fA-F]+$/\1/p')
223-
N_FWD=$(printf '%s\n' "$REL" | sed -nE 's/.*~([0-9]+).*/\1/p')
224-
EXTRA=" describe: $DESC (${N_BACK:-?} commits AFTER the nearest ancestor tag — that tag does NOT ship this commit)
259+
DEP_SHA=$(echo "$result" | awk '{print $1}')
260+
PIN_VAL=$(echo "$result" | awk '{print $2}')
261+
262+
EXTRA=""
263+
if [ "$DEP" = "talos" ]; then
264+
DESC=$(git -C "$DEP_PATH" describe --tags "$DEP_SHA" 2>/dev/null || true)
265+
CONTAIN=$(git -C "$DEP_PATH" tag --contains "$DEP_SHA" 2>/dev/null \
266+
| { paste -sd ',' - || true; } \
267+
| sed 's/,/, /g')
268+
REL=$(git -C "$DEP_PATH" describe --contains "$DEP_SHA" 2>/dev/null || true)
269+
N_BACK=$(printf '%s\n' "$DESC" | sed -nE 's/.*-([0-9]+)-g[0-9a-fA-F]+$/\1/p')
270+
N_FWD=$(printf '%s\n' "$REL" | sed -nE 's/.*~([0-9]+).*/\1/p')
271+
EXTRA=" describe: $DESC (${N_BACK:-?} commits AFTER the nearest ancestor tag — that tag does NOT ship this commit)
225272
containing tags: ${CONTAIN:-<none>}
226273
position: $REL (${N_FWD:-?} first-parent commits BEFORE the containing tag — that release SHIPS this commit)"
274+
fi
275+
276+
print_commit_block "$DEP @ $branch (first bump containing fix)" "$DEP" "$DEP_SHA" "$PIN_VAL" "$EXTRA"
277+
278+
CARRY_REPO="$DEP"
279+
CARRY_SHA="$DEP_SHA"
280+
done
281+
}
282+
283+
SRC_REPO="$FROM"
284+
SRC_PATH=$(repo_path "$SRC_REPO")
285+
if ! SRC_SHA=$(git -C "$SRC_PATH" rev-parse "$COMMIT" 2>/dev/null); then
286+
echo "commit $COMMIT not found in $SRC_REPO at $SRC_PATH" >&2
287+
exit 1
288+
fi
289+
290+
# Determine which branches to check.
291+
if [ -n "$BRANCH_FILTER" ]; then
292+
BRANCHES_TO_CHECK="$BRANCH_FILTER"
293+
else
294+
BRANCHES_TO_CHECK=$(list_source_branches "$SRC_PATH")
295+
fi
296+
297+
# For each branch, compute equivalent commit. Print summary, then traces.
298+
PRESENT_BRANCHES=""
299+
declare -a TRACES_BRANCH TRACES_SHA
300+
for b in $BRANCHES_TO_CHECK; do
301+
if ! branch_exists "$SRC_PATH" "$b"; then
302+
echo "WARN: origin/$b not in $SRC_REPO — skipping." >&2
303+
continue
227304
fi
305+
if eq=$(equivalent_on_branch "$SRC_PATH" "$b" "$SRC_SHA"); then
306+
PRESENT_BRANCHES="$PRESENT_BRANCHES $b"
307+
TRACES_BRANCH+=("$b")
308+
TRACES_SHA+=("$eq")
309+
fi
310+
done
228311

229-
print_commit_block "$DEP (first bump containing fix)" "$DEP" "$DEP_SHA" "$PIN_VAL" "$EXTRA"
312+
if [ ${#TRACES_BRANCH[@]} -eq 0 ]; then
313+
echo "fix not found on any checked branch in $SRC_REPO." >&2
314+
exit 2
315+
fi
316+
317+
echo
318+
echo "Fix present on branches:$PRESENT_BRANCHES"
230319

231-
CARRY_REPO="$DEP"
232-
CARRY_SHA="$DEP_SHA"
320+
i=0
321+
while [ $i -lt ${#TRACES_BRANCH[@]} ]; do
322+
trace_chain "${TRACES_BRANCH[$i]}" "${TRACES_SHA[$i]}"
323+
i=$((i + 1))
233324
done
234325

235326
echo

0 commit comments

Comments
 (0)