Skip to content

Commit a24a802

Browse files
taminomaraclaude
andcommitted
feat(code-review, test-planning): detect the diff base across multiple remotes and git-flow
The code-review and test-planning git-context detectors chose the branch to diff changes against from origin/HEAD alone. On a fork whose origin default is stale, on a git-flow branch cut from an integration branch, or on a branch that merged another branch in, that base was wrong, so the review or test plan was scoped to an inflated, empty, or mis-attributed diff. Port review-skill-or-agent's base selection to both detectors: the base is now the candidate whose fork point is nearest the current commit, measured along the branch's own first-parent line, chosen across a multi-remote candidate pool. - Candidate pool: every remote's declared default branch plus well-known trunk/integration names (main, master, trunk, mainline, next, develop, devel, development, default, dev), looked up as local and per-remote branches; nearest own-line coincidence wins, declared defaults break ties. - A candidate the branch merely merged in cannot win on its absorbed commits. - Output contract shape unchanged; fail-open to `default-branch: none` preserved. - Adds test/detect-review-context.bats and test/detect-test-context.bats (16 tests each) under test/, which these detectors previously lacked. Completes the OI-1 follow-up flagged in review-skill-or-agent (PR testdouble#119), which intentionally ran ahead of these copies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfTexc8iv7XAqt3ksH9zNP
1 parent a90cb09 commit a24a802

4 files changed

Lines changed: 674 additions & 10 deletions

File tree

han-coding/skills/code-review/scripts/detect-review-context.sh

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,60 @@ echo "git-available: true"
2424
BRANCH=$(git branch --show-current)
2525
echo "branch: ${BRANCH:-none}"
2626

27-
# Check for remote and default branch
28-
if git symbolic-ref --short refs/remotes/origin/HEAD &>/dev/null; then
29-
DEFAULT=$(git symbolic-ref --short refs/remotes/origin/HEAD)
30-
echo "default-branch: $DEFAULT"
27+
# Select the base branch to diff against: the candidate whose fork point is
28+
# nearest the current commit, measured along the branch's own line of work so a
29+
# candidate the branch merely merged in cannot win on absorbed commits.
3130

31+
# The branch's own line of work (first-parent history), newest first: a candidate
32+
# reachable only through a merge's second parent is not on it.
33+
OWNLINE=$(git rev-list --first-parent HEAD 2>/dev/null)
34+
35+
DEFAULT=none
36+
best_distance=""
37+
seen=" "
38+
39+
# Fold one candidate ref into the running nearest-wins selection. Its distance is
40+
# the number of own-line commits above the newest own-line commit the candidate
41+
# contains; a ref already seen, or one sharing no history with the own line, is
42+
# skipped.
43+
consider() { # candidate-ref
44+
case "$seen" in *" $1 "*) return ;; esac
45+
seen="$seen$1 "
46+
local commit count=0 distance=""
47+
while IFS= read -r commit; do
48+
if git merge-base --is-ancestor "$commit" "$1" 2>/dev/null; then
49+
distance=$count
50+
break
51+
fi
52+
count=$((count + 1))
53+
done <<<"$OWNLINE"
54+
[ -n "$distance" ] || return
55+
if [ -z "$best_distance" ] || [ "$distance" -lt "$best_distance" ]; then
56+
best_distance=$distance
57+
DEFAULT=$1
58+
fi
59+
}
60+
61+
# Candidate pool, enumerated declared-defaults-first so that under the
62+
# nearest-wins rule a declared default beats a name-guessed candidate at an
63+
# equal-distance tie. First, every remote's declared default branch (HEAD).
64+
while IFS= read -r headref; do
65+
ref=$(git symbolic-ref --short "$headref" 2>/dev/null) || continue
66+
consider "$ref"
67+
done < <(git for-each-ref --sort=refname --format='%(refname)' refs/remotes/ 2>/dev/null | grep -E '/HEAD$')
68+
69+
# Then well-known trunk/integration names as local branches, then as each
70+
# remote's tracking branch.
71+
for n in main master trunk mainline next develop devel development default dev; do
72+
git show-ref --verify -q "refs/heads/$n" && consider "$n"
73+
while IFS= read -r ref; do
74+
[ -n "$ref" ] && consider "$ref"
75+
done < <(git for-each-ref --sort=refname --format='%(refname:short)' "refs/remotes/*/$n" 2>/dev/null)
76+
done
77+
78+
echo "default-branch: $DEFAULT"
79+
80+
if [ "$DEFAULT" != none ]; then
3281
CHANGED=$(git diff --name-only "$DEFAULT...HEAD" 2>/dev/null)
3382
if [ -n "$CHANGED" ]; then
3483
echo "changed-files-start"
@@ -38,6 +87,5 @@ if git symbolic-ref --short refs/remotes/origin/HEAD &>/dev/null; then
3887
echo "changed-files: none"
3988
fi
4089
else
41-
echo "default-branch: none"
4290
echo "changed-files: none"
4391
fi

han-coding/skills/test-planning/scripts/detect-test-context.sh

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,60 @@ echo "git-available: true"
2424
BRANCH=$(git branch --show-current)
2525
echo "branch: ${BRANCH:-none}"
2626

27-
# Check for remote and default branch
28-
if git symbolic-ref --short refs/remotes/origin/HEAD &>/dev/null; then
29-
DEFAULT=$(git symbolic-ref --short refs/remotes/origin/HEAD)
30-
echo "default-branch: $DEFAULT"
27+
# Select the base branch to diff against: the candidate whose fork point is
28+
# nearest the current commit, measured along the branch's own line of work so a
29+
# candidate the branch merely merged in cannot win on absorbed commits.
3130

31+
# The branch's own line of work (first-parent history), newest first: a candidate
32+
# reachable only through a merge's second parent is not on it.
33+
OWNLINE=$(git rev-list --first-parent HEAD 2>/dev/null)
34+
35+
DEFAULT=none
36+
best_distance=""
37+
seen=" "
38+
39+
# Fold one candidate ref into the running nearest-wins selection. Its distance is
40+
# the number of own-line commits above the newest own-line commit the candidate
41+
# contains; a ref already seen, or one sharing no history with the own line, is
42+
# skipped.
43+
consider() { # candidate-ref
44+
case "$seen" in *" $1 "*) return ;; esac
45+
seen="$seen$1 "
46+
local commit count=0 distance=""
47+
while IFS= read -r commit; do
48+
if git merge-base --is-ancestor "$commit" "$1" 2>/dev/null; then
49+
distance=$count
50+
break
51+
fi
52+
count=$((count + 1))
53+
done <<<"$OWNLINE"
54+
[ -n "$distance" ] || return
55+
if [ -z "$best_distance" ] || [ "$distance" -lt "$best_distance" ]; then
56+
best_distance=$distance
57+
DEFAULT=$1
58+
fi
59+
}
60+
61+
# Candidate pool, enumerated declared-defaults-first so that under the
62+
# nearest-wins rule a declared default beats a name-guessed candidate at an
63+
# equal-distance tie. First, every remote's declared default branch (HEAD).
64+
while IFS= read -r headref; do
65+
ref=$(git symbolic-ref --short "$headref" 2>/dev/null) || continue
66+
consider "$ref"
67+
done < <(git for-each-ref --sort=refname --format='%(refname)' refs/remotes/ 2>/dev/null | grep -E '/HEAD$')
68+
69+
# Then well-known trunk/integration names as local branches, then as each
70+
# remote's tracking branch.
71+
for n in main master trunk mainline next develop devel development default dev; do
72+
git show-ref --verify -q "refs/heads/$n" && consider "$n"
73+
while IFS= read -r ref; do
74+
[ -n "$ref" ] && consider "$ref"
75+
done < <(git for-each-ref --sort=refname --format='%(refname:short)' "refs/remotes/*/$n" 2>/dev/null)
76+
done
77+
78+
echo "default-branch: $DEFAULT"
79+
80+
if [ "$DEFAULT" != none ]; then
3281
CHANGED=$(git diff --name-only "$DEFAULT...HEAD" 2>/dev/null)
3382
if [ -n "$CHANGED" ]; then
3483
echo "changed-files-start"
@@ -38,6 +87,5 @@ if git symbolic-ref --short refs/remotes/origin/HEAD &>/dev/null; then
3887
echo "changed-files: none"
3988
fi
4089
else
41-
echo "default-branch: none"
4290
echo "changed-files: none"
4391
fi

0 commit comments

Comments
 (0)