55# this script will ensure that branches from the same MR will be rebased correctly, keeping the chain order
66
77def compute_default_branch
8- system ( *%w[ git show-ref -q --verify refs/heads/main ] ) ? "main" : "master"
8+ @compute_default_branch ||= system ( *%w[ git show-ref -q --verify refs/heads/main ] ) ? "main" : "master"
99end
1010
1111# compute_parent_branch will determine the closest parent branch,
1212# ignoring remotes that we're not tracking against, and with a preference
1313# for the local branch. In scenarios where the parent branch does not
1414# have a local tracking branch, then the remote is returned.
15+ def preload_parent_branches! ( branches )
16+ @compute_parent_branch_cache ||= { }
17+ @remote_names ||= `git remote` . lines . map ( &:chomp )
18+
19+ missing = branches . reject { |b | @compute_parent_branch_cache . key? ( b ) }
20+ return if missing . empty?
21+
22+ threads = missing . map do |branch_name |
23+ Thread . new do
24+ output = `git log --decorate --simplify-by-decoration --oneline #{ branch_name } ` . lines
25+ [ branch_name , output ]
26+ end
27+ end
28+
29+ threads . each do |t |
30+ branch_name , output = t . value
31+ resolve_parent_branch_from_log ( branch_name , output )
32+ end
33+ end
34+
1535def compute_parent_branch ( branch_name = nil )
36+ @compute_parent_branch_cache ||= { }
1637 branch_name ||= `git rev-parse --abbrev-ref HEAD`
17- remote_names = `git remote` . lines . map ( &:chomp )
18- active_remote_name = `git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null` . split ( "/" ) . first
19- other_remote_names = remote_names - [ active_remote_name ]
2038
21- parent_branch_line = `git log --decorate --simplify-by-decoration --oneline #{ branch_name } ` . lines [ 1 ] . rstrip
39+ return @compute_parent_branch_cache [ branch_name ] if @compute_parent_branch_cache . key? ( branch_name )
40+
41+ output = `git log --decorate --simplify-by-decoration --oneline #{ branch_name } ` . lines
42+ resolve_parent_branch_from_log ( branch_name , output )
43+ end
44+
45+ def resolve_parent_branch_from_log ( branch_name , log_lines )
46+ @compute_parent_branch_cache ||= { }
47+ @remote_names ||= `git remote` . lines . map ( &:chomp )
48+ @active_remote_name ||= `git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null` . split ( "/" ) . first
49+ other_remote_names = @remote_names - [ @active_remote_name ]
50+
51+ parent_branch_line = log_lines [ 1 ] &.rstrip || ""
2252 parent_branch_line = parent_branch_line
2353 . sub ( "HEAD -> " , "" )
2454 . gsub ( /tag: [^,)]+(, )?/ , "" )
25- return compute_default_branch if parent_branch_line . match? ( %r{.* \( (origin|security)/(.*)\) .*} )
26- return compute_default_branch unless parent_branch_line . match? ( /.* \( (.*)\) .*/ )
27-
28- parent_branch_line
29- . sub ( /.* \( (.*)\) .*/ , '\1' )
30- . split ( ", " )
31- . reject { |b | other_remote_names . any? { |remote_name | b . start_with? ( "#{ remote_name } /" ) } }
32- . min_by ( &:length ) || compute_default_branch
55+ return @compute_parent_branch_cache [ branch_name ] = compute_default_branch if parent_branch_line . match? ( %r{.* \( (origin|security)/(.*)\) .*} )
56+ return @compute_parent_branch_cache [ branch_name ] = compute_default_branch unless parent_branch_line . match? ( /.* \( (.*)\) .*/ )
57+
58+ @compute_parent_branch_cache [ branch_name ] =
59+ parent_branch_line
60+ . sub ( /.* \( (.*)\) .*/ , '\1' )
61+ . split ( ", " )
62+ . reject { |b | other_remote_names . any? { |remote_name | b . start_with? ( "#{ remote_name } /" ) } }
63+ . min_by ( &:length ) || compute_default_branch
3364end
3465
3566class String
@@ -155,16 +186,30 @@ def rebase_mappings
155186 user_name = ENV . fetch ( "USER" , nil )
156187 default_branch = compute_default_branch
157188
189+ preload_local_branches!
190+
158191 mr_pattern = %r{^(security[-/])?#{ user_name } /(?<mr_id>\d +)/[a-z0-9\- +_]+$}i
159192 seq_mr_pattern = %r{^(security[-/])?#{ user_name } /(?<mr_id>\d +)/(?<mr_seq_nr>\d +)-[a-z0-9\- +_]+$}i
160193 backport_pattern = %r{^(security[-/])?#{ user_name } /(?<mr_id>\d +)/[a-z0-9\- +_]+-(?<milestone>\d +[-.]\d +)$}i
161194
162195 local_branches =
163- `git branch --list`
164- . lines
165- . map { |line | line [ 2 ..] . rstrip }
196+ @branch_exists_cache . keys
166197 . select { |branch | branch . start_with? ( "#{ user_name } /" , "security-#{ user_name } /" , "security/#{ user_name } /" ) }
167- . sort_by do |branch |
198+
199+ non_seq_branches = local_branches . reject { |b | seq_mr_pattern . match? ( b ) || backport_pattern . match? ( b ) }
200+
201+ @remote_names ||= `git remote` . lines . map ( &:chomp )
202+ @active_remote_name ||= `git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null` . split ( "/" ) . first
203+
204+ preload_threads = [
205+ Thread . new { branch_distance_map ( default_branch , local_branches ) } ,
206+ Thread . new { preload_merge_bases! ( default_branch , local_branches ) } ,
207+ Thread . new { preload_fork_points! ( default_branch , local_branches ) } ,
208+ Thread . new { preload_parent_branches! ( non_seq_branches ) }
209+ ]
210+ preload_threads . each ( &:join )
211+
212+ local_branches = local_branches . sort_by do |branch |
168213 seq_mr_match_data = seq_mr_pattern . match ( branch )
169214 backport_match_data = backport_pattern . match ( branch )
170215 mr_match_data = mr_pattern . match ( branch )
@@ -238,7 +283,7 @@ def rebase_mappings
238283 prev_branch = seq_map [ prev_seq_nr ] . first
239284 if branch_exists? ( prev_branch )
240285 if branch_merged_into? ( prev_branch , default_branch )
241- mb = `git merge-base #{ default_branch } #{ branch } 2>/dev/null` . strip
286+ mb = merge_base ( default_branch , branch )
242287 fork_point = prev_branch unless branch_merged_into? ( prev_branch , mb )
243288 rebase_onto = default_branch
244289 else
@@ -251,7 +296,7 @@ def rebase_mappings
251296 prev_seq_pattern = "#{ user_name } /#{ current_mr_id } /#{ current_mr_seq_nr - 1 } -"
252297 old_tip = deleted_branch_tip_from_reflog ( prev_seq_pattern , branch )
253298 if old_tip
254- mb = `git merge-base #{ default_branch } #{ branch } 2>/dev/null` . strip
299+ mb = merge_base ( default_branch , branch )
255300 fork_point = old_tip unless branch_merged_into? ( old_tip , mb )
256301 end
257302
@@ -269,9 +314,11 @@ def rebase_mappings
269314 end
270315
271316 if fork_point . nil? && ( rebase_onto || parent_branch ) == default_branch
272- fp = `git merge-base --fork-point #{ default_branch } #{ branch } 2>/dev/null` . strip
273- mb = `git merge-base #{ default_branch } #{ branch } 2>/dev/null` . strip
274- if Process . last_status . success? && !fp . empty? && fp != `git rev-parse #{ default_branch } ` . strip && fp != mb
317+ fp_result = compute_fork_point ( default_branch , branch )
318+ fp = fp_result [ :value ]
319+ mb = merge_base ( default_branch , branch )
320+ default_branch_sha = resolve_ref ( default_branch )
321+ if fp_result [ :success ] && !fp . empty? && fp != default_branch_sha && fp != mb
275322 fork_point = fp
276323 rebase_onto = default_branch
277324 end
@@ -292,12 +339,89 @@ def branch_sort_key(branch_info)
292339 [ branch_info [ :chain_mr_id ] || "" , branch_info [ :chain_mr_seq_nr ] . nil? ? 0 : -branch_info [ :chain_mr_seq_nr ] ]
293340end
294341
342+ def preload_local_branches!
343+ @branch_exists_cache ||= { }
344+ return if @branch_exists_preloaded
345+
346+ `git for-each-ref --format='%(refname:short)' refs/heads/` . lines . each do |line |
347+ @branch_exists_cache [ line . strip ] = true
348+ end
349+ @branch_exists_preloaded = true
350+ end
351+
295352def branch_exists? ( branch )
296- system ( *%W[ git rev-parse --verify #{ branch } ] , out : File ::NULL , err : File ::NULL )
353+ @branch_exists_cache ||= { }
354+ return @branch_exists_cache [ branch ] if @branch_exists_cache . key? ( branch )
355+
356+ @branch_exists_cache [ branch ] =
357+ system ( *%W[ git rev-parse --verify #{ branch } ] , out : File ::NULL , err : File ::NULL )
358+ end
359+
360+ def resolve_ref ( ref )
361+ @resolve_ref_cache ||= { }
362+ @resolve_ref_cache [ ref ] ||= `git rev-parse #{ ref } ` . strip
363+ end
364+
365+ def merge_base ( a , b )
366+ @merge_base_cache ||= { }
367+ key = "#{ a } :#{ b } "
368+ return @merge_base_cache [ key ] if @merge_base_cache . key? ( key )
369+
370+ @merge_base_cache [ key ] = `git merge-base #{ a } #{ b } 2>/dev/null` . strip
371+ end
372+
373+ def preload_merge_bases! ( default_branch , branches )
374+ @merge_base_cache ||= { }
375+ missing = branches . reject { |b | @merge_base_cache . key? ( "#{ default_branch } :#{ b } " ) }
376+ return if missing . empty?
377+
378+ threads = missing . map do |branch |
379+ Thread . new do
380+ result = `git merge-base #{ default_branch } #{ branch } 2>/dev/null` . strip
381+ [ branch , result ]
382+ end
383+ end
384+
385+ threads . each do |t |
386+ branch , result = t . value
387+ @merge_base_cache [ "#{ default_branch } :#{ branch } " ] = result
388+ end
389+ end
390+
391+ def preload_fork_points! ( default_branch , branches )
392+ @fork_point_cache ||= { }
393+ missing = branches . reject { |b | @fork_point_cache . key? ( "#{ default_branch } :#{ b } " ) }
394+ return if missing . empty?
395+
396+ threads = missing . map do |branch |
397+ Thread . new do
398+ result = `git merge-base --fork-point #{ default_branch } #{ branch } 2>/dev/null` . strip
399+ success = Process . last_status . success?
400+ [ branch , result , success ]
401+ end
402+ end
403+
404+ threads . each do |t |
405+ branch , result , success = t . value
406+ @fork_point_cache [ "#{ default_branch } :#{ branch } " ] = { value : result , success : success }
407+ end
408+ end
409+
410+ def compute_fork_point ( default_branch , branch )
411+ @fork_point_cache ||= { }
412+ key = "#{ default_branch } :#{ branch } "
413+ return @fork_point_cache [ key ] if @fork_point_cache . key? ( key )
414+
415+ result = `git merge-base --fork-point #{ default_branch } #{ branch } 2>/dev/null` . strip
416+ @fork_point_cache [ key ] = { value : result , success : Process . last_status . success? }
297417end
298418
299419def branch_merged_into? ( branch , target )
300- system ( *%W[ git merge-base --is-ancestor #{ branch } #{ target } ] )
420+ @branch_merged_cache ||= { }
421+ key = "#{ branch } :#{ target } "
422+ return @branch_merged_cache [ key ] if @branch_merged_cache . key? ( key )
423+
424+ @branch_merged_cache [ key ] = system ( *%W[ git merge-base --is-ancestor #{ branch } #{ target } ] )
301425end
302426
303427def deleted_branch_tip_from_reflog ( branch_name_prefix , descendant_branch = nil )
@@ -307,28 +431,67 @@ def deleted_branch_tip_from_reflog(branch_name_prefix, descendant_branch = nil)
307431 /\b checkout: moving from .+ to #{ escaped } /
308432 ]
309433
310- `git reflog --format=%H\\ %gs` . lines . each do |line |
311- sha , description = line . strip . split ( " " , 2 )
312- next unless description
313- next unless patterns . any? { |p | description . match? ( p ) }
314- next if descendant_branch && !system ( *%W[ git merge-base --is-ancestor #{ sha } #{ descendant_branch } ] , out : File ::NULL , err : File ::NULL )
434+ reflog_cmd = %W[
435+ git reflog --format=%H\ %gs
436+ --grep-reflog=checkout.*#{ branch_name_prefix }
437+ --grep-reflog=rebase.*#{ branch_name_prefix }
438+ ]
439+
440+ IO . popen ( reflog_cmd ) do |io |
441+ io . each_line do |line |
442+ sha , description = line . strip . split ( " " , 2 )
443+ next unless description
444+ next unless patterns . any? { |p | description . match? ( p ) }
445+ next if descendant_branch && !branch_merged_into? ( sha , descendant_branch )
315446
316- return sha
447+ return sha
448+ end
317449 end
318450
319451 nil
320452end
321453
454+ def branch_distance_map ( parent_branch , branches )
455+ @branch_distance_cache ||= { }
456+
457+ missing = branches . uniq . reject { |branch | @branch_distance_cache . key? ( "#{ branch } :#{ parent_branch } " ) }
458+ return if missing . empty?
459+
460+ missing_set = missing . to_h { |branch | [ branch , true ] }
461+ distances = { }
462+
463+ output = `git for-each-ref --format='%(refname:short) %(ahead-behind:#{ parent_branch } )' refs/heads/`
464+ if Process . last_status . success?
465+ output . lines . each do |line |
466+ branch , ahead , behind = line . split
467+ next unless branch && ahead && behind
468+ next unless missing_set [ branch ]
469+
470+ distances [ branch ] = ahead . to_i
471+ end
472+ end
473+
474+ missing . each do |branch |
475+ key = "#{ branch } :#{ parent_branch } "
476+ @branch_distance_cache [ key ] = distances . fetch ( branch ) do
477+ `git rev-list --count #{ parent_branch } ..#{ branch } ` . strip . to_i
478+ end
479+ end
480+ end
481+
322482def branch_distance ( branch , parent_branch )
323- `git rev-list --count #{ parent_branch } ..#{ branch } ` . strip . to_i
483+ @branch_distance_cache ||= { }
484+ key = "#{ branch } :#{ parent_branch } "
485+ return @branch_distance_cache [ key ] if @branch_distance_cache . key? ( key )
486+
487+ @branch_distance_cache [ key ] = `git rev-list --count #{ parent_branch } ..#{ branch } ` . strip . to_i
324488end
325489
326490def rebase_all
327491 require "json"
328492 default_branch = compute_default_branch
329493 mappings = rebase_mappings
330- . sort { |b1 , b2 | branch_sort_key ( b1 ) <=> branch_sort_key ( b2 ) }
331- . sort { |b1 , b2 | branch_distance ( b1 [ :branch ] , default_branch ) <=> branch_distance ( b2 [ :branch ] , default_branch ) }
494+ . sort_by { |b | [ branch_distance ( b [ :branch ] , default_branch ) , branch_sort_key ( b ) ] }
332495 . to_h { |b | [ b [ :branch ] , { rebase_onto : b [ :rebase_onto ] , fork_point : b [ :fork_point ] } ] }
333496 rebase_all_per_capture_info ( mappings )
334497end
0 commit comments