From 07912c269db9b72fea7d8a7064d25ec57d2068dd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Dec 2025 13:24:25 +0000 Subject: [PATCH 1/2] Fix branch ancestry check to use local branches The is_base_ancestor function was checking remote-tracking branches (origin/$BASE and origin/$BRANCH), but merge operations performed by the script haven't been pushed yet. This caused incorrect ancestry detection for child branches. The fix leverages control flow analysis: git checkout creates the local branch from origin if needed, so we checkout first then compare local refs. This eliminates the need for conditional branch creation and ensures idempotence even when pushes haven't occurred yet. Changes: - Remove ensure_local_branch() and is_base_ancestor() helpers - Modify has_squash_commit() to use local refs (caller ensures they exist) - Move git checkout before ancestry checks in update_direct_target() - Inline merge-base check in update_indirect_target() after checkout - Add comments explaining the control flow reasoning --- update-pr-stack.sh | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/update-pr-stack.sh b/update-pr-stack.sh index a08be11..5f48e60 100755 --- a/update-pr-stack.sh +++ b/update-pr-stack.sh @@ -33,20 +33,16 @@ check_env_var() { fi } -# Check if BASE is already an ancestor of BRANCH (simple merge check) -is_base_ancestor() { - local BRANCH="$1" - local BASE="$2" - git merge-base --is-ancestor "origin/$BASE" "origin/$BRANCH" -} - # Check if a branch already has the squash commit merged (squash-merge mode only) # Requires SQUASH_COMMIT ref to be set via git update-ref +# +# Note: This uses local branch refs. The caller must ensure both branches +# exist locally before calling (e.g., via git checkout). has_squash_commit() { local BRANCH="$1" local BASE="$2" - is_base_ancestor "$BRANCH" "$BASE" \ - && git merge-base --is-ancestor SQUASH_COMMIT "origin/$BRANCH" + git merge-base --is-ancestor "$BASE" "$BRANCH" \ + && git merge-base --is-ancestor SQUASH_COMMIT "$BRANCH" } format_branch_list_for_text() { @@ -64,13 +60,17 @@ update_direct_target() { local BRANCH="$1" local BASE_BRANCH="$2" + # Checkout first to ensure the local branch exists (created from origin if + # needed). This allows has_squash_commit to compare local refs, which matters + # for testing where the script may run multiple times in the same repo. + log_cmd git checkout "$BRANCH" + if has_squash_commit "$BRANCH" "$TARGET_BRANCH"; then echo "✓ $BRANCH already up-to-date; skipping" return fi echo "Updating direct target $BRANCH (from $MERGED_BRANCH to $BASE_BRANCH)" - log_cmd git checkout "$BRANCH" CONFLICTS=() log_cmd git update-ref BEFORE_MERGE HEAD @@ -122,16 +122,21 @@ update_indirect_target() { local BRANCH="$1" local BASE_BRANCH="$2" - # For indirect targets, we only need to check if the parent is already - # an ancestor. If so, the branch already has all updates from the parent. - # (No SQUASH_COMMIT check needed - that's only for direct targets) - if is_base_ancestor "$BRANCH" "$BASE_BRANCH"; then + # At this point in the control flow: + # - BASE_BRANCH has already been checked out and potentially modified locally + # - BRANCH has not been touched yet + # + # We checkout BRANCH first (which creates the local branch from origin if + # needed), then compare against the local BASE_BRANCH to check if it's + # already an ancestor. + log_cmd git checkout "$BRANCH" + + if git merge-base --is-ancestor "$BASE_BRANCH" "$BRANCH"; then echo "✓ $BRANCH already up-to-date with $BASE_BRANCH; skipping" return fi echo "Updating indirect target $BRANCH (based on $BASE_BRANCH)" - log_cmd git checkout "$BRANCH" log_cmd git merge --no-edit "$BASE_BRANCH" } From de48836835f0bd19c4e69fe93d8e45ff67beb413 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Dec 2025 14:32:49 +0000 Subject: [PATCH 2/2] Fix e2e test to checkout feature3 before verifying its content The test was checking file.txt content after checking out feature4, so it was reading feature4's content instead of feature3's. --- tests/test_e2e.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_e2e.sh b/tests/test_e2e.sh index fe3872e..b007a0b 100755 --- a/tests/test_e2e.sh +++ b/tests/test_e2e.sh @@ -831,6 +831,7 @@ fi # Line 1: Original base # Line 2: From feature 3 commit ("Feature 3 content line 2") # Line 7: From feature 3 conflict commit, kept during resolution ("Feature 3 conflicting change line 7") +log_cmd git checkout feature3 EXPECTED_CONTENT_LINE1="Base file content line 1" EXPECTED_CONTENT_LINE2="Feature 3 content line 2" EXPECTED_CONTENT_LINE7="Feature 3 conflicting change line 7"