Skip to content

Commit e306824

Browse files
Phlogistiqueclaude
andauthored
Add tests for rebase and mixed merge/rebase workflows (#30)
Verifies the action works regardless of how the child PR synced with its parent during development: pure rebase, or merge-then-rebase — including cases where the child is NOT fully up to date with the parent at squash-merge time. In all cases, the `git merge origin/$MERGED_BRANCH` step in the action correctly brings the child up to date with whatever commits it was missing, and the synthetic 3-parent merge produces a clean PR diff. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3046c27 commit e306824

4 files changed

Lines changed: 369 additions & 2 deletions

File tree

.github/workflows/tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ jobs:
1313
uses: actions/checkout@v4
1414

1515
- name: Run unit tests
16-
run: bash tests/test_update_pr_stack.sh
16+
run: |
17+
bash tests/test_update_pr_stack.sh
18+
bash tests/test_rebase_workflow.sh
19+
bash tests/test_mixed_workflows.sh
1720
1821
e2e-tests:
1922
name: E2E Tests

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ If you use squash & merge instead, your main branch history stays nice and clean
1010

1111
### The solution
1212

13-
This action tries to fix that in a transparent way. Install it, and hopefully the workflow of stacking + merge during dev + squash merge when landing works.
13+
This action tries to fix that in a transparent way. Install it, and hopefully the workflow of stacking + merge during dev + squash merge when landing works. It also works if you rebase during development instead of merging.
1414

1515
---
1616

tests/test_mixed_workflows.sh

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
#!/bin/bash
2+
#
3+
# Test mixed merge/rebase workflows where feature2 is NOT fully up to date
4+
# with feature1 at the time of squash-merge.
5+
#
6+
# Scenarios:
7+
# A) feature2 rebased onto an intermediate state of feature1, then feature1
8+
# got more commits that feature2 never picked up.
9+
# B) feature2 merged feature1 once, then rebased onto a later feature1 state,
10+
# but feature1 got yet more commits after that.
11+
12+
set -e
13+
14+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15+
source "$SCRIPT_DIR/../command_utils.sh"
16+
17+
simulate_push() {
18+
log_cmd git update-ref "refs/remotes/origin/$1" "$1"
19+
}
20+
21+
# Extract just the +/- lines from a diff (ignoring headers and context)
22+
diff_changes() {
23+
grep '^[+-]' | grep -v '^[+-][+-][+-]'
24+
}
25+
26+
run_scenario() {
27+
local SCENARIO_NAME="$1"
28+
echo ""
29+
echo "============================================"
30+
echo " Scenario $SCENARIO_NAME"
31+
echo "============================================"
32+
echo ""
33+
34+
TEST_REPO=$(mktemp -d)
35+
cd "$TEST_REPO"
36+
37+
log_cmd git init -b main
38+
log_cmd git config user.email "test@example.com"
39+
log_cmd git config user.name "Test User"
40+
41+
# 10-line file to keep changes well-separated
42+
for i in $(seq 1 10); do echo "line $i" >> file.txt; done
43+
log_cmd git add file.txt
44+
log_cmd git commit -m "Initial commit"
45+
simulate_push main
46+
}
47+
48+
assert_diff_is() {
49+
local LABEL="$1"
50+
local EXPECTED="$2"
51+
local ACTUAL
52+
ACTUAL=$(log_cmd git diff main...feature2 | diff_changes)
53+
if [[ "$ACTUAL" == "$EXPECTED" ]]; then
54+
echo "$LABEL"
55+
else
56+
echo "$LABEL"
57+
echo "Expected changed lines:"
58+
echo "$EXPECTED"
59+
echo "Actual changed lines:"
60+
echo "$ACTUAL"
61+
exit 1
62+
fi
63+
}
64+
65+
assert_idempotent() {
66+
local BEFORE AFTER
67+
BEFORE=$(git rev-parse feature2)
68+
run_update_pr_stack
69+
cd "$TEST_REPO"
70+
AFTER=$(git rev-parse feature2)
71+
if [[ "$BEFORE" == "$AFTER" ]]; then
72+
echo "✅ Idempotent"
73+
else
74+
echo "❌ Idempotence failed"
75+
exit 1
76+
fi
77+
}
78+
79+
squash_merge_feature1() {
80+
log_cmd git checkout main
81+
log_cmd git merge --squash feature1
82+
log_cmd git commit -m "Squash merge feature1"
83+
SQUASH_COMMIT=$(git rev-parse HEAD)
84+
simulate_push main
85+
echo "Squash commit: $SQUASH_COMMIT"
86+
}
87+
88+
run_update_pr_stack() {
89+
log_cmd \
90+
env \
91+
SQUASH_COMMIT=$SQUASH_COMMIT \
92+
MERGED_BRANCH=feature1 \
93+
TARGET_BRANCH=main \
94+
GH="$SCRIPT_DIR/mock_gh.sh" \
95+
GIT="$SCRIPT_DIR/mock_git.sh" \
96+
$SCRIPT_DIR/../update-pr-stack.sh
97+
}
98+
99+
########################################################################
100+
# Scenario A: rebase onto intermediate feature1, then feature1 advances
101+
########################################################################
102+
run_scenario "A: rebased onto intermediate feature1, then feature1 advances"
103+
104+
# feature1: two commits (lines 2 and 3)
105+
log_cmd git checkout -b feature1
106+
sed -i '2s/.*/f1 commit 1/' file.txt
107+
log_cmd git add file.txt
108+
log_cmd git commit -m "f1: commit 1"
109+
simulate_push feature1
110+
111+
# feature2: branched from feature1 after commit 1, changes line 9
112+
log_cmd git checkout -b feature2
113+
sed -i '9s/.*/f2 content/' file.txt
114+
log_cmd git add file.txt
115+
log_cmd git commit -m "f2: change line 9"
116+
simulate_push feature2
117+
118+
# feature1 gets commit 2
119+
log_cmd git checkout feature1
120+
sed -i '3s/.*/f1 commit 2/' file.txt
121+
log_cmd git add file.txt
122+
log_cmd git commit -m "f1: commit 2"
123+
simulate_push feature1
124+
125+
# Developer rebases feature2 onto feature1 (picks up commit 2)
126+
log_cmd git checkout feature2
127+
log_cmd git rebase feature1
128+
simulate_push feature2
129+
130+
# feature1 gets commit 3 — feature2 does NOT pick this up
131+
log_cmd git checkout feature1
132+
sed -i '4s/.*/f1 commit 3/' file.txt
133+
log_cmd git add file.txt
134+
log_cmd git commit -m "f1: commit 3"
135+
simulate_push feature1
136+
137+
echo ""
138+
echo "=== Graph before squash-merge ==="
139+
log_cmd git log --graph --oneline --all
140+
echo ""
141+
142+
squash_merge_feature1
143+
run_update_pr_stack
144+
cd "$TEST_REPO"
145+
146+
# The diff should show only feature2's unique change
147+
assert_diff_is "Scenario A: diff shows only f2's change" "$(cat <<'EOF'
148+
-line 9
149+
+f2 content
150+
EOF
151+
)"
152+
assert_idempotent
153+
154+
########################################################################
155+
# Scenario B: merge then rebase, then feature1 advances again
156+
########################################################################
157+
run_scenario "B: merge then rebase, then feature1 advances again"
158+
159+
# feature1: commit 1 (line 2)
160+
log_cmd git checkout -b feature1
161+
sed -i '2s/.*/f1 commit 1/' file.txt
162+
log_cmd git add file.txt
163+
log_cmd git commit -m "f1: commit 1"
164+
simulate_push feature1
165+
166+
# feature2 from feature1, changes line 9
167+
log_cmd git checkout -b feature2
168+
sed -i '9s/.*/f2 content/' file.txt
169+
log_cmd git add file.txt
170+
log_cmd git commit -m "f2: change line 9"
171+
simulate_push feature2
172+
173+
# feature1 gets commit 2
174+
log_cmd git checkout feature1
175+
sed -i '3s/.*/f1 commit 2/' file.txt
176+
log_cmd git add file.txt
177+
log_cmd git commit -m "f1: commit 2"
178+
simulate_push feature1
179+
180+
# Developer merges feature1 into feature2
181+
log_cmd git checkout feature2
182+
log_cmd git merge --no-edit feature1
183+
simulate_push feature2
184+
185+
# feature1 gets commit 3
186+
log_cmd git checkout feature1
187+
sed -i '4s/.*/f1 commit 3/' file.txt
188+
log_cmd git add file.txt
189+
log_cmd git commit -m "f1: commit 3"
190+
simulate_push feature1
191+
192+
# Developer rebases feature2 onto latest feature1 (rewrite away the merge)
193+
log_cmd git checkout feature2
194+
log_cmd git rebase feature1
195+
simulate_push feature2
196+
197+
# feature1 gets commit 4 — feature2 does NOT pick this up
198+
log_cmd git checkout feature1
199+
sed -i '5s/.*/f1 commit 4/' file.txt
200+
log_cmd git add file.txt
201+
log_cmd git commit -m "f1: commit 4"
202+
simulate_push feature1
203+
204+
echo ""
205+
echo "=== Graph before squash-merge ==="
206+
log_cmd git log --graph --oneline --all
207+
echo ""
208+
209+
squash_merge_feature1
210+
run_update_pr_stack
211+
cd "$TEST_REPO"
212+
213+
assert_diff_is "Scenario B: diff shows only f2's change" "$(cat <<'EOF'
214+
-line 9
215+
+f2 content
216+
EOF
217+
)"
218+
assert_idempotent
219+
220+
########################################################################
221+
echo ""
222+
echo "All mixed-workflow tests passed! 🎉"

tests/test_rebase_workflow.sh

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#!/bin/bash
2+
#
3+
# Test that the update-pr-stack mechanism works when PRs are rebased during
4+
# development (as opposed to using merge commits to sync with the parent).
5+
#
6+
# Scenario:
7+
# 1. main ← feature1 ← feature2 (linear stack)
8+
# 2. feature1 gets an additional commit AFTER feature2 was branched
9+
# 3. Developer rebases feature2 onto the updated feature1
10+
# 4. feature1 is squash-merged into main
11+
# 5. Action runs → feature2 should get a correct PR diff against main
12+
13+
set -e
14+
15+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16+
source "$SCRIPT_DIR/../command_utils.sh"
17+
18+
simulate_push() {
19+
local branch_name="$1"
20+
log_cmd git update-ref "refs/remotes/origin/$branch_name" "$branch_name"
21+
}
22+
23+
TEST_REPO=$(mktemp -d)
24+
cd "$TEST_REPO"
25+
echo "Created test repo at $TEST_REPO"
26+
27+
log_cmd git init -b main
28+
log_cmd git config user.email "test@example.com"
29+
log_cmd git config user.name "Test User"
30+
31+
# Initial commit on main — use enough lines to avoid adjacent-change conflicts
32+
for i in $(seq 1 10); do echo "line $i" >> file.txt; done
33+
log_cmd git add file.txt
34+
log_cmd git commit -m "Initial commit"
35+
simulate_push main
36+
37+
# feature1: modify line 2 (top region)
38+
log_cmd git checkout -b feature1
39+
sed -i '2s/.*/Feature 1 first change/' file.txt
40+
log_cmd git add file.txt
41+
log_cmd git commit -m "feature1: first commit"
42+
simulate_push feature1
43+
44+
# feature2: branched from feature1, modify line 9 (bottom region, far away)
45+
log_cmd git checkout -b feature2
46+
sed -i '9s/.*/Feature 2 content/' file.txt
47+
log_cmd git add file.txt
48+
log_cmd git commit -m "feature2: change line 9"
49+
simulate_push feature2
50+
51+
# Now feature1 gets an additional commit modifying line 3 (still top region)
52+
log_cmd git checkout feature1
53+
sed -i '3s/.*/Feature 1 second change/' file.txt
54+
log_cmd git add file.txt
55+
log_cmd git commit -m "feature1: second commit"
56+
simulate_push feature1
57+
58+
FEATURE1_TIP=$(git rev-parse HEAD)
59+
60+
# Developer rebases feature2 onto the updated feature1
61+
log_cmd git checkout feature2
62+
log_cmd git rebase feature1
63+
simulate_push feature2
64+
65+
echo ""
66+
echo "=== State before squash-merge ==="
67+
log_cmd git log --graph --oneline --all
68+
echo ""
69+
70+
# Squash-merge feature1 into main (simulated via cherry-pick --no-commit + commit)
71+
log_cmd git checkout main
72+
# Use merge --squash to properly squash all of feature1's commits
73+
log_cmd git merge --squash feature1
74+
log_cmd git commit -m "Squash merge feature1"
75+
SQUASH_COMMIT=$(git rev-parse HEAD)
76+
simulate_push main
77+
78+
echo ""
79+
echo "Squash commit: $SQUASH_COMMIT"
80+
echo ""
81+
82+
# Run the action
83+
run_update_pr_stack() {
84+
log_cmd \
85+
env \
86+
SQUASH_COMMIT=$SQUASH_COMMIT \
87+
MERGED_BRANCH=feature1 \
88+
TARGET_BRANCH=main \
89+
GH="$SCRIPT_DIR/mock_gh.sh" \
90+
GIT="$SCRIPT_DIR/mock_git.sh" \
91+
$SCRIPT_DIR/../update-pr-stack.sh
92+
}
93+
run_update_pr_stack
94+
95+
cd "$TEST_REPO"
96+
97+
# Verify: squash commit is an ancestor of feature2
98+
if log_cmd git merge-base --is-ancestor "$SQUASH_COMMIT" feature2; then
99+
echo "✅ feature2 includes the squash commit"
100+
else
101+
echo "❌ feature2 does not include the squash commit"
102+
log_cmd git log --graph --oneline --all
103+
exit 1
104+
fi
105+
106+
# Verify: triple-dot diff main...feature2 shows ONLY feature2's unique change
107+
# We expect the diff to show only feature2's unique change (line 9).
108+
# Strip index lines and hunk header function-name context for stable comparison.
109+
EXPECTED_DIFF=$(cat <<'EOF'
110+
-line 9
111+
+Feature 2 content
112+
EOF
113+
)
114+
ACTUAL_DIFF=$(log_cmd git diff main...feature2 | grep '^[+-]' | grep -v '^[+-][+-][+-]')
115+
if [[ "$ACTUAL_DIFF" == "$EXPECTED_DIFF" ]]; then
116+
echo "✅ Triple-dot diff main...feature2 shows only feature2's unique changes"
117+
else
118+
echo "❌ Triple-dot diff is wrong"
119+
echo "Expected:"
120+
echo "$EXPECTED_DIFF"
121+
echo "Actual:"
122+
echo "$ACTUAL_DIFF"
123+
diff <(echo "$EXPECTED_DIFF") <(echo "$ACTUAL_DIFF") || true
124+
exit 1
125+
fi
126+
127+
# Verify idempotence
128+
FEATURE2_BEFORE=$(git rev-parse feature2)
129+
run_update_pr_stack
130+
cd "$TEST_REPO"
131+
FEATURE2_AFTER=$(git rev-parse feature2)
132+
133+
if [[ "$FEATURE2_BEFORE" == "$FEATURE2_AFTER" ]]; then
134+
echo "✅ Idempotence: re-running produces no new commits"
135+
else
136+
echo "❌ Idempotence failed"
137+
exit 1
138+
fi
139+
140+
echo ""
141+
echo "All rebase-workflow tests passed! 🎉"
142+
echo "Test repository remains at: $TEST_REPO for inspection"

0 commit comments

Comments
 (0)