From 97519fb0f11c4e2207a80b4ec0db732071e7b0d0 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Wed, 26 Aug 2026 17:39:52 -0400 Subject: [PATCH] pick: detect backported PRs by message reference, not just ancestry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes reach a release branch as backports — cherry-picks with new SHAs — so the original quattro merge commit is never an ancestor of a patch branch and the ancestry filter let already-applied PRs through. Candidates are now also matched against the branch's own commit messages since it left quattro: 'backport of #N' / squash '(#N)' references and 'cherry picked from commit ' trailers (which pick -x itself writes). Explicitly named PRs/commits that are already on the branch are skipped with a note instead of re-picked. Verified against the live v4-0-2 branch: the five backported PRs it carries filter out; un-backported ones are still offered. --- bin/omarchy-release | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/bin/omarchy-release b/bin/omarchy-release index 2436b2e..382ee1b 100755 --- a/bin/omarchy-release +++ b/bin/omarchy-release @@ -462,6 +462,28 @@ cmd_pick() { ensure_work_clone git -C "$WORK_CLONE" checkout --quiet -B "$branch" "origin/$branch" + # Changes usually reach a release branch as BACKPORTS — cherry-picks with + # new SHAs — so ancestry of the original quattro merge commit proves nothing + # on a patch branch. Detect equivalence the way it is actually recorded: + # the branch's own commit messages since it left quattro name the PR + # ("backport of #N", squash titles "(#N)") or the source commit + # ("cherry picked from commit ", which pick -x writes). + local base_commit branch_log + base_commit=$(git -C "$WORK_CLONE" merge-base "origin/$branch" "origin/$DEV_BRANCH" 2>/dev/null) || base_commit="" + branch_log=$(git -C "$WORK_CLONE" log --format='%s %b' "origin/$branch" ${base_commit:+--not "$base_commit"} 2>/dev/null) + + already_on_branch() { # already_on_branch + local sha="$1" number="$2" + if [[ -n "$sha" ]]; then + git -C "$WORK_CLONE" merge-base --is-ancestor "$sha" "origin/$branch" 2>/dev/null && return 0 + grep -q "cherry picked from commit $sha" <<<"$branch_log" && return 0 + fi + if [[ -n "$number" ]]; then + grep -qE "#$number([^0-9]|$)" <<<"$branch_log" && return 0 + fi + return 1 + } + local -a shas=() labels=() if [[ $# -gt 0 ]]; then local arg sha title @@ -473,23 +495,32 @@ cmd_pick() { print_error "PR #$arg has no merge commit (not merged?)" exit 1 fi + if already_on_branch "$sha" "$arg"; then + print_info "PR #$arg is already on $branch — skipping" + continue + fi shas+=("$sha") labels+=("PR #$arg: $title") else + if already_on_branch "$arg" ""; then + print_info "commit $arg is already on $branch — skipping" + continue + fi shas+=("$arg") labels+=("commit $arg") fi done else - # Interactive: merged dev-branch PRs whose merge commits are not on the - # release branch yet, oldest first so picks apply in merge order. + # Interactive: merged dev-branch PRs not already on the release branch + # (by ancestry, backport reference, or cherry-pick trailer), oldest first + # so picks apply in merge order. print_info "Loading merged $DEV_BRANCH PRs not yet on $branch..." local list list=$(gh pr list --repo "$UPSTREAM_REPO" --state merged --base "$DEV_BRANCH" \ --limit 30 --json number,title,mergeCommit,mergedAt \ - --jq 'sort_by(.mergedAt) | .[] | "\(.mergeCommit.oid)\t#\(.number) \(.title)"') + --jq 'sort_by(.mergedAt) | .[] | "\(.mergeCommit.oid)\t\(.number)\t#\(.number) \(.title)"') local -a cand_shas=() cand_labels=() - while IFS=$'\t' read -r sha label; do + while IFS=$'\t' read -r sha number label; do [[ -z "$sha" || "$sha" == "null" ]] && continue - git -C "$WORK_CLONE" merge-base --is-ancestor "$sha" "origin/$branch" 2>/dev/null && continue + already_on_branch "$sha" "$number" && continue cand_shas+=("$sha") cand_labels+=("$label") done <<<"$list" if [[ ${#cand_shas[@]} -eq 0 ]]; then