|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Diff vendored packages against a pristine upstream checkout at the SHA |
| 3 | +# recorded in docs/VENDORING.md. Prints only local divergences (the lines |
| 4 | +# that are ours). Markers in vendor/ are navigation; this diff is proof. |
| 5 | +# |
| 6 | +# Usage: |
| 7 | +# bin/vendor-patch-diff [--upstream PATH] [package...] |
| 8 | +# |
| 9 | +# package is a vendor directory basename (e.g. intx-inference) or an |
| 10 | +# @intx/* name. With no packages, diffs every row in VENDORING.md that |
| 11 | +# carries local patches. |
| 12 | +# |
| 13 | +# Upstream clone resolution (first hit wins): |
| 14 | +# 1. --upstream PATH |
| 15 | +# 2. $INTERCHANGE_UPSTREAM |
| 16 | +# 3. ../interchange relative to the repo root |
| 17 | +# 4. ../../interchange relative to the repo root |
| 18 | +set -euo pipefail |
| 19 | + |
| 20 | +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
| 21 | +VENDORING="$REPO_ROOT/docs/VENDORING.md" |
| 22 | + |
| 23 | +usage() { |
| 24 | + sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' |
| 25 | + exit 2 |
| 26 | +} |
| 27 | + |
| 28 | +UPSTREAM="" |
| 29 | +PACKAGES=() |
| 30 | + |
| 31 | +while [[ $# -gt 0 ]]; do |
| 32 | + case "$1" in |
| 33 | + --upstream) |
| 34 | + [[ $# -ge 2 ]] || usage |
| 35 | + UPSTREAM="$2" |
| 36 | + shift 2 |
| 37 | + ;; |
| 38 | + -h|--help) |
| 39 | + usage |
| 40 | + ;; |
| 41 | + --) |
| 42 | + shift |
| 43 | + PACKAGES+=("$@") |
| 44 | + break |
| 45 | + ;; |
| 46 | + -*) |
| 47 | + echo "unknown flag: $1" >&2 |
| 48 | + usage |
| 49 | + ;; |
| 50 | + *) |
| 51 | + PACKAGES+=("$1") |
| 52 | + shift |
| 53 | + ;; |
| 54 | + esac |
| 55 | +done |
| 56 | + |
| 57 | +if [[ -z "$UPSTREAM" ]]; then |
| 58 | + if [[ -n "${INTERCHANGE_UPSTREAM:-}" ]]; then |
| 59 | + UPSTREAM="$INTERCHANGE_UPSTREAM" |
| 60 | + elif [[ -d "$REPO_ROOT/../interchange/.git" || -f "$REPO_ROOT/../interchange/.git" ]]; then |
| 61 | + UPSTREAM="$(cd "$REPO_ROOT/../interchange" && pwd)" |
| 62 | + elif [[ -d "$REPO_ROOT/../../interchange/.git" || -f "$REPO_ROOT/../../interchange/.git" ]]; then |
| 63 | + UPSTREAM="$(cd "$REPO_ROOT/../../interchange" && pwd)" |
| 64 | + else |
| 65 | + echo "error: no upstream Interchange clone found." >&2 |
| 66 | + echo "Pass --upstream PATH or set INTERCHANGE_UPSTREAM." >&2 |
| 67 | + exit 1 |
| 68 | + fi |
| 69 | +fi |
| 70 | + |
| 71 | +if [[ ! -d "$UPSTREAM" ]]; then |
| 72 | + echo "error: upstream path is not a directory: $UPSTREAM" >&2 |
| 73 | + exit 1 |
| 74 | +fi |
| 75 | +if ! git -C "$UPSTREAM" rev-parse --git-dir >/dev/null 2>&1; then |
| 76 | + echo "error: upstream path is not a git repository: $UPSTREAM" >&2 |
| 77 | + exit 1 |
| 78 | +fi |
| 79 | + |
| 80 | +# Rows look like: |
| 81 | +# | `@intx/inference` | `vendor/intx-inference/` | LGPL-2.1-only | `SHA` | 2026-08-08 | Yes — see … | |
| 82 | +# Capture: name, vendor path, sha, local-patches cell. |
| 83 | +mapfile -t ROWS < <( |
| 84 | + awk -F'|' ' |
| 85 | + /^\| `@intx\// { |
| 86 | + name=$2; vendor=$3; sha=$5; patches=$7 |
| 87 | + gsub(/^ +| +$/, "", name) |
| 88 | + gsub(/^ +| +$/, "", vendor) |
| 89 | + gsub(/^ +| +$/, "", sha) |
| 90 | + gsub(/^ +| +$/, "", patches) |
| 91 | + gsub(/`/, "", name) |
| 92 | + gsub(/`/, "", vendor) |
| 93 | + gsub(/`/, "", sha) |
| 94 | + gsub(/\/$/, "", vendor) |
| 95 | + print name "\t" vendor "\t" sha "\t" patches |
| 96 | + } |
| 97 | + ' "$VENDORING" |
| 98 | +) |
| 99 | + |
| 100 | +if [[ ${#ROWS[@]} -eq 0 ]]; then |
| 101 | + echo "error: no vendored packages parsed from $VENDORING" >&2 |
| 102 | + exit 1 |
| 103 | +fi |
| 104 | + |
| 105 | +normalize_pkg() { |
| 106 | + local raw="$1" |
| 107 | + raw="${raw#@intx/}" |
| 108 | + raw="${raw#vendor/}" |
| 109 | + raw="${raw%/}" |
| 110 | + if [[ "$raw" == intx-* ]]; then |
| 111 | + echo "$raw" |
| 112 | + else |
| 113 | + echo "intx-$raw" |
| 114 | + fi |
| 115 | +} |
| 116 | + |
| 117 | +# Build the work list: either explicit packages, or every "Yes" local-patches row. |
| 118 | +declare -a WORK_NAMES=() |
| 119 | +declare -a WORK_VENDORS=() |
| 120 | +declare -a WORK_SHAS=() |
| 121 | + |
| 122 | +if [[ ${#PACKAGES[@]} -eq 0 ]]; then |
| 123 | + for row in "${ROWS[@]}"; do |
| 124 | + IFS=$'\t' read -r name vendor sha patches <<<"$row" |
| 125 | + if [[ "$patches" == Yes* ]]; then |
| 126 | + WORK_NAMES+=("$name") |
| 127 | + WORK_VENDORS+=("$vendor") |
| 128 | + WORK_SHAS+=("$sha") |
| 129 | + fi |
| 130 | + done |
| 131 | +else |
| 132 | + for want in "${PACKAGES[@]}"; do |
| 133 | + want_norm="$(normalize_pkg "$want")" |
| 134 | + found=0 |
| 135 | + for row in "${ROWS[@]}"; do |
| 136 | + IFS=$'\t' read -r name vendor sha patches <<<"$row" |
| 137 | + vendor_base="${vendor#vendor/}" |
| 138 | + if [[ "$(normalize_pkg "$name")" == "$want_norm" || "$vendor_base" == "$want_norm" ]]; then |
| 139 | + WORK_NAMES+=("$name") |
| 140 | + WORK_VENDORS+=("$vendor") |
| 141 | + WORK_SHAS+=("$sha") |
| 142 | + found=1 |
| 143 | + break |
| 144 | + fi |
| 145 | + done |
| 146 | + if [[ $found -eq 0 ]]; then |
| 147 | + echo "error: package not listed in $VENDORING: $want" >&2 |
| 148 | + exit 1 |
| 149 | + fi |
| 150 | + done |
| 151 | +fi |
| 152 | + |
| 153 | +if [[ ${#WORK_NAMES[@]} -eq 0 ]]; then |
| 154 | + echo "error: no packages selected (no local-patches rows, or empty filter)" >&2 |
| 155 | + exit 1 |
| 156 | +fi |
| 157 | + |
| 158 | +# Map @intx/<x> → packages/<x> in the upstream monorepo. |
| 159 | +upstream_pkg_path() { |
| 160 | + local name="$1" |
| 161 | + echo "packages/${name#@intx/}" |
| 162 | +} |
| 163 | + |
| 164 | +TMP="$(mktemp -d "${TMPDIR:-/tmp}/vendor-patch-diff.XXXXXX")" |
| 165 | +cleanup() { rm -rf "$TMP"; } |
| 166 | +trap cleanup EXIT |
| 167 | + |
| 168 | +OVERALL=0 |
| 169 | + |
| 170 | +for i in "${!WORK_NAMES[@]}"; do |
| 171 | + name="${WORK_NAMES[$i]}" |
| 172 | + vendor="${WORK_VENDORS[$i]}" |
| 173 | + sha="${WORK_SHAS[$i]}" |
| 174 | + up_path="$(upstream_pkg_path "$name")" |
| 175 | + vendor_src="$REPO_ROOT/$vendor/src" |
| 176 | + |
| 177 | + if [[ ! -d "$vendor_src" ]]; then |
| 178 | + echo "error: missing vendored src: $vendor_src" >&2 |
| 179 | + exit 1 |
| 180 | + fi |
| 181 | + |
| 182 | + if ! git -C "$UPSTREAM" cat-file -e "${sha}^{commit}" 2>/dev/null; then |
| 183 | + echo "error: upstream $UPSTREAM has no commit $sha" >&2 |
| 184 | + echo "Fetch that commit into the clone, then re-run." >&2 |
| 185 | + exit 1 |
| 186 | + fi |
| 187 | + |
| 188 | + if ! git -C "$UPSTREAM" cat-file -e "${sha}:${up_path}" 2>/dev/null; then |
| 189 | + echo "error: $up_path does not exist at $sha in $UPSTREAM" >&2 |
| 190 | + exit 1 |
| 191 | + fi |
| 192 | + |
| 193 | + pristine="$TMP/pristine-$i" |
| 194 | + mkdir -p "$pristine" |
| 195 | + # Extract only the package tree at the recorded SHA — read-only on upstream. |
| 196 | + git -C "$UPSTREAM" archive "$sha" "$up_path" | tar -x -C "$pristine" |
| 197 | + |
| 198 | + pristine_src="$pristine/$up_path/src" |
| 199 | + if [[ ! -d "$pristine_src" ]]; then |
| 200 | + echo "error: archived tree has no src/: $pristine_src" >&2 |
| 201 | + exit 1 |
| 202 | + fi |
| 203 | + |
| 204 | + echo "### $name upstream=$sha vendor=$vendor" |
| 205 | + echo "# pristine: $up_path/src vs $vendor/src" |
| 206 | + echo |
| 207 | + |
| 208 | + # Unified diff of source only. diff exits 1 on differences — that is success |
| 209 | + # for this tool (we expect patches). Exit 2 is a real error. |
| 210 | + set +e |
| 211 | + diff -ruN "$pristine_src" "$vendor_src" |
| 212 | + code=$? |
| 213 | + set -e |
| 214 | + if [[ $code -eq 0 ]]; then |
| 215 | + echo "(no source differences)" |
| 216 | + elif [[ $code -eq 1 ]]; then |
| 217 | + OVERALL=1 |
| 218 | + else |
| 219 | + echo "error: diff failed for $name (exit $code)" >&2 |
| 220 | + exit 1 |
| 221 | + fi |
| 222 | + echo |
| 223 | +done |
| 224 | + |
| 225 | +# Exit 0 when there are differences (the usual patched case) so pipelines |
| 226 | +# treat a successful run as success. Exit 3 when everything is verbatim — |
| 227 | +# surprising for a "show our patches" tool, so callers can detect it. |
| 228 | +if [[ $OVERALL -eq 0 ]]; then |
| 229 | + echo "note: no local source patches found for selected packages." >&2 |
| 230 | + exit 3 |
| 231 | +fi |
| 232 | +exit 0 |
0 commit comments