@@ -37,24 +37,29 @@ usage () {
3737 exit 1
3838}
3939
40- # Function to check if patch was trivially upstreamed (exact range-diff match)
41- check_trivial_skip () {
42- # Try range-diff with default creation factor to find exact matches
43- # range-diff may fail if there's no common base, which is fine - just means no trivial match
44- if ! local range_diff_output=$( git range-diff --no-color REBASE_HEAD^! REBASE_HEAD..) ; then
45- # range-diff failed (no common ancestor, etc.) - not a trivial skip
46- return 1
47- fi
40+ # Function to generate a correspondence map between two commit ranges
41+ # Usage: generate_correspondence_map <our-range> <their-range> <output-file>
42+ # The map file contains range-diff output that can be searched for correspondences
43+ generate_correspondence_map () {
44+ local our_range=$1 their_range=$2 output_file=$3
45+ git -c core.abbrev=false range-diff --no-color " $our_range " " $their_range " > " $output_file " 2> /dev/null || :
46+ }
4847
49- # Look for exact match: "1: abc123 = 1: def456"
50- # The "=" means identical patches
51- if echo " $range_diff_output " | grep -qE ' ^[0-9]+: [0-9a-f]+ = [0-9]+: [0-9a-f]+' ; then
52- # Extract the upstream OID (right side of =)
53- echo " $range_diff_output " | grep -oE ' ^[0-9]+: [0-9a-f]+ = [0-9]+: ([0-9a-f]+)' | \
54- sed ' s/.*= [0-9]*: //' | head -1
55- return 0
56- fi
57- return 1
48+ # Function to find a corresponding commit in a map file
49+ # Usage: find_correspondence <oid> <map-file>
50+ # Returns: corresponding OID on stdout, exit 0 if found (= or ! match), 1 if not
51+ # Sets CORRESPONDENCE_TYPE to "=" (identical) or "!" (modified)
52+ find_correspondence () {
53+ local oid=$1 map_file=$2
54+ test -s " $map_file " || return 1
55+
56+ # Look for this OID in the left side of range-diff output
57+ # Format: "N: <oid> = M: <oid>" (identical) or "N: <oid> ! M: <oid>" (modified)
58+ local match=$( sed -n " s/^[0-9]*: $oid \([!=]\) [0-9]*: \([0-9a-f]*\).*/\1 \2/p" " $map_file " | head -1)
59+ test -n " $match " || return 1
60+
61+ CORRESPONDENCE_TYPE=${match% * }
62+ echo " ${match#* } "
5863}
5964
6065# Generate git log -L commands for conflicting hunks
@@ -82,7 +87,10 @@ generate_log_l_commands () {
8287}
8388
8489# Function to resolve a single conflict with AI
90+ # Usage: resolve_conflict_with_ai [<tried-correspondences>]
8591resolve_conflict_with_ai () {
92+ local tried_correspondences=$1
93+
8694 # Get REBASE_HEAD info once
8795 local rebase_head_oid rebase_head_ref rebase_head_oneline
8896 rebase_head_oid=$( git rev-parse REBASE_HEAD)
@@ -101,11 +109,20 @@ resolve_conflict_with_ai () {
101109 log_l_commands=" ${log_l_commands} $( generate_log_l_commands " $file " ) "
102110 done
103111
112+ # Add context about tried correspondences
113+ local correspondence_context=" "
114+ if test -n " $tried_correspondences " ; then
115+ correspondence_context="
116+ Note: We found corresponding commits from previous/sibling rebases but they did not apply cleanly:
117+ $tried_correspondences
118+ You may want to examine these with 'git show <oid>' for hints on how to resolve."
119+ fi
120+
104121 # Build the prompt - minimal, letting LLM discover context
105122 local prompt=" Resolve merge conflict during rebase of commit REBASE_HEAD.
106123
107124Conflicting files: $conflicting_files
108-
125+ $correspondence_context
109126Investigation commands:
110127- See the patch: git show REBASE_HEAD
111128- See conflict markers: view <file>
@@ -297,15 +314,16 @@ run_rebase_with_ai () {
297314 rebase_head_ref=$( git show --no-patch --format=reference REBASE_HEAD)
298315 rebase_head_oneline=$( git show --no-patch --format=' %h %s' REBASE_HEAD)
299316
300- # First, try trivial skip check (exact range-diff match)
301- if local trivial_upstream_oid=$( check_trivial_skip) ; then
302- echo " $rebase_head_oid $trivial_upstream_oid " >> " $SKIPPED_MAP_FILE "
303- echo " ::notice::Trivial skip (upstream: $trivial_upstream_oid ): $rebase_head_oneline "
317+ # Check upstream correspondence (= means identical, skip it)
318+ if corresponding_oid=$( find_correspondence " $rebase_head_oid " " $UPSTREAM_MAP " ) &&
319+ test " $CORRESPONDENCE_TYPE " = " =" ; then
320+ echo " $rebase_head_oid $corresponding_oid " >> " $SKIPPED_MAP_FILE "
321+ echo " ::notice::Trivial skip (upstream: $corresponding_oid ): $rebase_head_oneline "
304322 cat >> " $REPORT_FILE " << TRIVIAL_SKIP_EOF
305323
306324### Skipped (trivial): $rebase_head_ref
307325
308- Upstream equivalent: $( git show --no-patch --format=reference " $trivial_upstream_oid " || echo " $trivial_upstream_oid " )
326+ Upstream equivalent: $( git show --no-patch --format=reference " $corresponding_oid " 2> /dev/null || echo " $corresponding_oid " )
309327
310328Detected via exact range-diff match (no AI needed).
311329
@@ -315,8 +333,34 @@ TRIVIAL_SKIP_EOF
315333 continue
316334 fi
317335
318- # Non-trivial conflict - invoke AI
319- resolve_conflict_with_ai
336+ # Try previous/sibling correspondences (reuse their resolution via merge-tree)
337+ local tried_correspondences=" "
338+ for map_file in " $PREVIOUS_MAP " " $SIBLING_MAP " ; do
339+ test -s " $map_file " || continue
340+ corresponding_oid=$( find_correspondence " $rebase_head_oid " " $map_file " ) || continue
341+
342+ echo " ::notice::Found correspondence: $corresponding_oid for $rebase_head_oneline "
343+ tried_correspondences=" ${tried_correspondences: +$tried_correspondences } $corresponding_oid "
344+ # Try merge-tree: if clean, use the resulting tree
345+ if result_tree=$( git merge-tree --write-tree HEAD^ REBASE_HEAD " $corresponding_oid " 2> /dev/null) &&
346+ git read-tree --reset -u " $result_tree " &&
347+ git commit -C REBASE_HEAD; then
348+ echo " ::notice::Used resolution from: $corresponding_oid "
349+ cat >> " $REPORT_FILE " << RESOLVED_EOF
350+
351+ ### Resolved via correspondence: $rebase_head_ref
352+
353+ Used resolution from: $( git show --no-patch --format=reference " $corresponding_oid " 2> /dev/null || echo " $corresponding_oid " )
354+
355+ RESOLVED_EOF
356+ CONFLICTS_RESOLVED=$(( CONFLICTS_RESOLVED + 1 ))
357+ git rebase --continue
358+ continue 2
359+ fi
360+ done
361+
362+ # Non-trivial conflict - invoke AI (pass tried correspondences as context)
363+ resolve_conflict_with_ai " $tried_correspondences "
320364 done
321365}
322366
@@ -369,14 +413,28 @@ echo "::notice::Current tip: $TIP_OID"
369413# The second parent of the marker points to the GfW branch tip at marker creation time
370414GFW_MAIN_BRANCH=" origin/main"
371415BEHIND_COUNT=$( git rev-list --count " $TIP_OID ..$GFW_MAIN_BRANCH " || echo " 0" )
416+ PREVIOUS_MAP=" "
372417
373418if test " $BEHIND_COUNT " -gt 0; then
374- echo " ::notice::Syncing $BEHIND_COUNT commits from $GFW_MAIN_BRANCH "
375- echo " ::group::Sync rebase from $GFW_MAIN_BRANCH "
376- run_rebase_with_ai -r HEAD " $GFW_MAIN_BRANCH "
377- git checkout -B " $SHEARS_BRANCH "
378- TIP_OID=$( git rev-parse HEAD)
379- echo " ::endgroup::"
419+ if git rev-list --grep=' ^Start the merging-rebase' " $TIP_OID ..$GFW_MAIN_BRANCH " | grep -q . ; then
420+ # origin/main was rebased - generate correspondence before adopting
421+ PREVIOUS_MAP=" $WORKTREE_DIR /previous-correspondence.map"
422+ MAIN_MARKER=$( git rev-parse " $GFW_MAIN_BRANCH ^{/Start.the.merging-rebase}" )
423+ generate_correspondence_map " $MAIN_MARKER ..$GFW_MAIN_BRANCH " " $OLD_MARKER ..$TIP_OID " " $PREVIOUS_MAP "
424+
425+ echo " ::notice::origin/main was rebased, adopting its $BEHIND_COUNT commits"
426+ git checkout -B " $SHEARS_BRANCH " " $GFW_MAIN_BRANCH "
427+ TIP_OID=$( git rev-parse HEAD)
428+ OLD_MARKER=$( git rev-parse " HEAD^{/Start.the.merging-rebase}" )
429+ OLD_UPSTREAM=$( git rev-parse " $OLD_MARKER ^1" )
430+ else
431+ echo " ::notice::Syncing $BEHIND_COUNT commits from $GFW_MAIN_BRANCH "
432+ echo " ::group::Rebasing $BEHIND_COUNT commits from $GFW_MAIN_BRANCH on top of $SHEARS_BRANCH "
433+ run_rebase_with_ai -r HEAD " $GFW_MAIN_BRANCH "
434+ git checkout -B " $SHEARS_BRANCH "
435+ TIP_OID=$( git rev-parse HEAD)
436+ echo " ::endgroup::"
437+ fi
380438fi
381439
382440# Initialize report
@@ -396,21 +454,41 @@ CONFLICTS_RESOLVED=0
396454SKIPPED_MAP_FILE=" $WORKTREE_DIR /skipped-commits.map"
397455: > " $SKIPPED_MAP_FILE "
398456
399- # Create new marker with two parents: upstream + current tip
457+ # Generate correspondence maps for conflict resolution
458+ UPSTREAM_MAP=" $WORKTREE_DIR /upstream-correspondence.map"
459+ SIBLING_MAP=" $WORKTREE_DIR /sibling-correspondence.map"
460+
461+ # Map 1: Our commits vs upstream (for trivial skips)
462+ generate_correspondence_map " $OLD_MARKER ..$TIP_OID " " $OLD_UPSTREAM ..$NEW_UPSTREAM " " $UPSTREAM_MAP "
463+
464+ # Map 2: Our commits vs sibling branch (seen→next→main hierarchy)
465+ case " ${SHEARS_BRANCH##*/ } " in
466+ main) SIBLING_BRANCH=" origin/shears/next" ;;
467+ next) SIBLING_BRANCH=" origin/shears/seen" ;;
468+ * ) SIBLING_BRANCH=" " ;;
469+ esac
470+ if test -n " $SIBLING_BRANCH " && git rev-parse --verify " $SIBLING_BRANCH " > /dev/null 2>&1 ; then
471+ SIBLING_MARKER=$( git rev-parse " $SIBLING_BRANCH ^{/Start.the.merging-rebase}" 2> /dev/null) || SIBLING_MARKER=" "
472+ if test -n " $SIBLING_MARKER " ; then
473+ generate_correspondence_map " $OLD_MARKER ..$TIP_OID " " $SIBLING_MARKER ..$SIBLING_BRANCH " " $SIBLING_MAP "
474+ fi
475+ fi
476+
477+ # Create new marker with two parents: upstream + origin/main
400478echo " ::group::Creating marker and running rebase"
401479MARKER_OID=$( git commit-tree " $UPSTREAM_BRANCH ^{tree}" \
402480 -p " $UPSTREAM_BRANCH " \
403- -p " $TIP_OID " \
481+ -p " $GFW_MAIN_BRANCH " \
404482 -m " Start the merging-rebase to $UPSTREAM_BRANCH
405483
406484This commit starts the rebase of $OLD_MARKER to $NEW_UPSTREAM " )
407485
408486git replace --graft " $MARKER_OID " " $UPSTREAM_BRANCH "
409487REBASE_TODO_COUNT=$( git rev-list --count " $OLD_MARKER ..$TIP_OID " )
410488echo " Rebasing $REBASE_TODO_COUNT commits onto $MARKER_OID "
411- echo " ::endgroup::"
412489
413490run_rebase_with_ai -r --onto " $MARKER_OID " " $OLD_MARKER "
491+ echo " ::endgroup::"
414492
415493# Clean up graft and verify
416494git replace -d " $MARKER_OID "
0 commit comments