From edb5940150717ff72ffbab612bc39ee90f3ace85 Mon Sep 17 00:00:00 2001 From: Justin Michaels Date: Tue, 2 Jun 2026 20:33:05 -0400 Subject: [PATCH] fix(rebase): apply committed patch series directly onto upstream MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rebase script (both before and after the 4-submodule generalization) snapshotted the *working tree* to build the replay series, assuming apply-patches.sh had run first. In CI the submodules are checked out at their pinned SHAs with no patches applied, so the snapshot was empty: the script reset to upstream and then 'git am'-ed an empty series, producing a spurious conflict on every run. The original PR #14 'conflict' was this bug, not real upstream drift. Apply patches//*.patch directly onto the freshly-reset upstream via git am -3 — the committed patch files are the source of truth, no working-tree dependency. Preserve the curated patch filenames on regeneration (positional, since series order is stable) so doc references and the 0001/0002 numbering don't churn. Validated locally: clean apply onto the pinned base, filename preserved, content identical. --- scripts/rebase-upstream.sh | 83 +++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/scripts/rebase-upstream.sh b/scripts/rebase-upstream.sh index c112840..beb2243 100755 --- a/scripts/rebase-upstream.sh +++ b/scripts/rebase-upstream.sh @@ -77,62 +77,69 @@ rebase_one() { cd "${submod_dir}" - if (( ! REGEN_ONLY )); then - echo "==> [${submod}] fetching origin (upstream)" - git fetch origin --tags --prune - - local target="${REF}" - if [[ -z "${target}" ]]; then - local db; db="$(default_branch)" - if [[ -z "${db}" ]]; then - echo " [error] ${submod}: could not resolve origin default branch" >&2 - return 1 - fi - target="origin/${db}" + # Resolve the upstream target for this submodule. + local target="${REF}" + if [[ -z "${target}" ]]; then + if (( ! REGEN_ONLY )); then + echo "==> [${submod}] fetching origin (upstream)" + git fetch origin --tags --prune fi - - echo "==> [${submod}] snapshotting working-tree patches as commits" - local pre_ref commits_ref - pre_ref="$(git rev-parse HEAD)" - # apply-patches.sh leaves our patches present-but-uncommitted in the - # working tree. Materialize them as commits so `git am -3` can replay them. - if ! git diff --quiet || ! git diff --cached --quiet; then - git add -A - git commit -q -m "WIP: quenchforge patches (pre-rebase snapshot)" --allow-empty + local db; db="$(default_branch)" + if [[ -z "${db}" ]]; then + echo " [error] ${submod}: could not resolve origin default branch" >&2 + return 1 fi - commits_ref="$(git rev-parse HEAD)" - - echo "==> [${submod}] generating replay series from ${pre_ref}..${commits_ref}" - local tmp_series; tmp_series="$(mktemp -d)" - git format-patch --zero-commit -N -o "${tmp_series}" "${pre_ref}..${commits_ref}" >/dev/null + target="origin/${db}" + elif (( ! REGEN_ONLY )); then + echo "==> [${submod}] fetching origin (upstream)" + git fetch origin --tags --prune + fi + if (( ! REGEN_ONLY )); then + # Apply the committed patch series directly onto the fresh upstream. This + # does NOT depend on the working tree being pre-patched — the canonical + # source of truth is patches//*.patch. + git am --abort >/dev/null 2>&1 || true echo "==> [${submod}] resetting to ${target}" git reset --hard "${target}" - echo "==> [${submod}] replaying patches with three-way merge" - if ! git am -3 "${tmp_series}"/*.patch; then + echo "==> [${submod}] applying patches/${submod}/*.patch with three-way merge" + if ! git am -3 "${patches[@]}"; then echo echo "rebase stopped on conflict in ${submod}. Resolve hunks, then:" echo " git -C ${submod} add " echo " git -C ${submod} am --continue" echo " scripts/rebase-upstream.sh --only ${submod} --regenerate-only" - rm -rf "${tmp_series}" return 1 fi - rm -rf "${tmp_series}" fi echo "==> [${submod}] regenerating canonical series into patches/${submod}/" - local base - base="$(git rev-list --max-parents=0 HEAD | tail -1)" - if [[ -n "${REF}" ]]; then - base="$(git merge-base HEAD "${REF}")" + # After `git am`, HEAD = target + N quenchforge commits; regenerate the + # series off that base so the on-disk patches are refreshed against upstream + # (3-way merge may have adjusted context lines). + local base; base="$(git merge-base HEAD "${target}")" + local tmp_out; tmp_out="$(mktemp -d)" + git format-patch --zero-commit -N -o "${tmp_out}" "${base}..HEAD" >/dev/null + + shopt -s nullglob + local gen=( "${tmp_out}"/*.patch ) + shopt -u nullglob + # git format-patch names files from the commit subject; preserve the curated + # filenames (positional — series order is stable) so doc references and the + # 0001/0002 numbering don't churn on every rebase. + if (( ${#gen[@]} == ${#patches[@]} )); then + rm -f "${sub_patch_dir}"/*.patch + local i + for i in "${!gen[@]}"; do + cp "${gen[$i]}" "${sub_patch_dir}/$(basename "${patches[$i]}")" + done else - local db; db="$(default_branch)" - [[ -n "${db}" ]] && base="$(git merge-base HEAD "origin/${db}")" + echo " [warn] ${submod}: regenerated ${#gen[@]} patches vs ${#patches[@]} curated — keeping generated names" >&2 + rm -f "${sub_patch_dir}"/*.patch + cp "${gen[@]}" "${sub_patch_dir}/" fi - rm -f "${sub_patch_dir}"/*.patch - git format-patch --zero-commit -N -o "${sub_patch_dir}" "${base}..HEAD" >/dev/null + rm -rf "${tmp_out}" echo " updated: $(ls -1 "${sub_patch_dir}"/*.patch 2>/dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ')" }