Skip to content

Commit f979ef1

Browse files
rheniumhsbt
authored andcommitted
sync_default_gems.rb: gracefully handle merge commits
Find interesting commits by following parents instead of relying on "git log". If we encounter a merge commit that may contain a conflict resolution, fall back to cherry-picking the merge commit as a whole rather than replaying each individual commit. The sync commit will include a shortlog for the squashed commits in that case.
1 parent 85e0f8c commit f979ef1

2 files changed

Lines changed: 86 additions & 31 deletions

File tree

tool/sync_default_gems.rb

Lines changed: 57 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ def check_prerelease_version(gem)
424424
puts "#{gem}-#{spec.version} is not latest version of rubygems.org" if spec.version.to_s != latest_version
425425
end
426426

427-
def message_filter(repo, sha, log)
427+
def message_filter(repo, sha, log, context: nil)
428428
unless repo.count("/") == 1 and /\A\S+\z/ =~ repo
429429
raise ArgumentError, "invalid repository: #{repo}"
430430
end
@@ -458,14 +458,15 @@ def message_filter(repo, sha, log)
458458
end
459459
end
460460
commit_url = "#{repo_url}/commit/#{sha[0,10]}\n"
461+
sync_note = context ? "#{commit_url}\n#{context}" : commit_url
461462
if log and !log.empty?
462463
log.sub!(/(?<=\n)\n+\z/, '') # drop empty lines at the last
463464
conv[log]
464465
log.sub!(/(?:(\A\s*)|\s*\n)(?=((?i:^Co-authored-by:.*\n?)+)?\Z)/) {
465-
($~.begin(1) ? "" : "\n\n") + commit_url + ($~.begin(2) ? "\n" : "")
466+
($~.begin(1) ? "" : "\n\n") + sync_note + ($~.begin(2) ? "\n" : "")
466467
}
467468
else
468-
log = commit_url
469+
log = sync_note
469470
end
470471
"#{subject}\n\n#{log}"
471472
end
@@ -475,27 +476,51 @@ def log_format(format, args, &block)
475476
log --no-show-signature --format=#{format}] + args, "rb", &block)
476477
end
477478

478-
# Returns commit list as array of [commit_hash, subject].
479-
def commits_in_ranges(gem, repo, default_branch, ranges)
480-
# If -a is given, discover all commits since the last picked commit
481-
if ranges == true
482-
pattern = "https://github\.com/#{Regexp.quote(repo)}/commit/([0-9a-f]+)$"
483-
log = log_format('%B', %W"-E --grep=#{pattern} -n1 --", &:read)
484-
ranges = ["#{log[%r[#{pattern}\n\s*(?i:co-authored-by:.*)*\s*\Z], 1]}..#{gem}/#{default_branch}"]
485-
end
479+
def commits_in_range(upto, exclude, toplevel:)
480+
args = [upto, *exclude.map {|s|"^#{s}"}]
481+
log_format('%H,%P,%s', %W"--first-parent" + args) do |f|
482+
f.read.split("\n").reverse.flat_map {|commit|
483+
hash, parents, subject = commit.split(',', 3)
484+
parents = parents.split
485+
486+
# Non-merge commit
487+
if parents.size <= 1
488+
puts "#{hash} #{subject}"
489+
next [[hash, subject]]
490+
end
486491

487-
# Parse a given range with git log
488-
ranges.flat_map do |range|
489-
unless range.include?("..")
490-
range = "#{range}~1..#{range}"
491-
end
492+
# Clean 2-parent merge commit: follow the other parent as long as it
493+
# contains no potentially-non-clean merges
494+
if parents.size == 2 &&
495+
IO.popen(%W"git diff-tree --remerge-diff #{hash}", "rb", &:read).empty?
496+
puts "\e[2mChecking the other parent of #{hash} #{subject}\e[0m"
497+
ret = catch(:quit) {
498+
commits_in_range(parents[1], exclude + [parents[0]], toplevel: false)
499+
}
500+
next ret if ret
501+
end
492502

493-
log_format('%H,%s', %W"#{range} --") do |f|
494-
f.read.split("\n").reverse.map{|commit| commit.split(',', 2)}
495-
end
503+
unless toplevel
504+
puts "\e[1mMerge commit with possible conflict resolution #{hash} #{subject}\e[0m"
505+
throw :quit
506+
end
507+
508+
puts "#{hash} #{subject} " \
509+
"\e[1m[merge commit with possible conflicts, will do a squash merge]\e[0m"
510+
[[hash, subject]]
511+
}
496512
end
497513
end
498514

515+
# Returns commit list as array of [commit_hash, subject, sync_note].
516+
def commits_in_ranges(ranges)
517+
ranges.flat_map do |range|
518+
exclude, upto = range.include?("..") ? range.split("..", 2) : ["#{range}~1", range]
519+
puts "Looking for commits in range #{exclude}..#{upto}"
520+
commits_in_range(upto, exclude.empty? ? [] : [exclude], toplevel: true)
521+
end.uniq
522+
end
523+
499524
#--
500525
# Following methods used by sync_default_gems_with_commits return
501526
# true: success
@@ -558,7 +583,12 @@ def make_commit_info(gem, sha)
558583
"GIT_AUTHOR_EMAIL" => author_email,
559584
"GIT_AUTHOR_DATE" => author_date,
560585
}
561-
message = message_filter(config.upstream, sha, orig)
586+
context = nil
587+
if /^parent (?<first_parent>.{40})\nparent .{40}$/ =~ headers
588+
# Squashing a merge commit: keep authorship information
589+
context = IO.popen(%W"git shortlog #{first_parent}..#{sha} --", "rb", &:read)
590+
end
591+
message = message_filter(config.upstream, sha, orig, context: context)
562592
[author, message]
563593
end
564594

@@ -683,9 +713,8 @@ def pickup_commit(gem, sha, edit)
683713
return true
684714
end
685715

686-
# NOTE: This method is also used by GitHub ruby/git.ruby-lang.org's bin/update-default-gem.sh
687716
# @param gem [String] A gem name, also used as a git remote name. REPOSITORIES converts it to the appropriate GitHub repository.
688-
# @param ranges [Array<String>] "before..after". Note that it will NOT sync "before" (but commits after that).
717+
# @param ranges [Array<String>, true] "commit", "before..after", or true. Note that it will NOT sync "before" (but commits after that).
689718
# @param edit [TrueClass] Set true if you want to resolve conflicts. Obviously, update-default-gem.sh doesn't use this.
690719
def sync_default_gems_with_commits(gem, ranges, edit: nil)
691720
config = REPOSITORIES[gem]
@@ -700,21 +729,18 @@ def sync_default_gems_with_commits(gem, ranges, edit: nil)
700729
end
701730
system(*%W"git fetch --no-tags --depth=#{FETCH_DEPTH} #{gem} #{default_branch}")
702731

703-
commits = commits_in_ranges(gem, repo, default_branch, ranges)
704-
705-
# Ignore Merge commits and already-merged commits.
706-
commits.delete_if do |sha, subject|
707-
subject.start_with?("Merge", "Auto Merge")
732+
# If -a is given, discover all commits since the last picked commit
733+
if ranges == true
734+
pattern = "https://github\.com/#{Regexp.quote(repo)}/commit/([0-9a-f]+)$"
735+
log = log_format('%B', %W"-E --grep=#{pattern} -n1 --", &:read)
736+
ranges = ["#{log[%r[#{pattern}\n\s*(?i:co-authored-by:.*)*\s*\Z], 1]}..#{gem}/#{default_branch}"]
708737
end
709-
738+
commits = commits_in_ranges(ranges)
710739
if commits.empty?
711740
puts "No commits to pick"
712741
return true
713742
end
714743

715-
puts "Try to pick these commits:"
716-
puts commits.map{|commit| commit.join(": ")}
717-
718744
failed_commits = []
719745
commits.each do |sha, subject|
720746
puts "----"

tool/test/test_sync_default_gems.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,5 +317,34 @@ def test_delete_after_conflict
317317
assert_equal(":ok\n""Should.be_merged\n", File.read("src/lib/common.rb"), out)
318318
assert_not_operator(File, :exist?, "src/lib/bad.rb", out)
319319
end
320+
321+
def test_squash_merge
322+
# 2---. <- branch
323+
# / \
324+
# 1---3---3'<- merge commit with conflict resolution
325+
File.write("#@target/lib/conflict.rb", "# 1\n")
326+
git(*%W"add lib/conflict.rb", chdir: @target)
327+
git(*%W"commit -q -m", "Add conflict.rb", chdir: @target)
328+
329+
git(*%W"checkout -q -b branch", chdir: @target)
330+
File.write("#@target/lib/conflict.rb", "# 2\n")
331+
File.write("#@target/lib/new.rb", "# new\n")
332+
git(*%W"add lib/conflict.rb lib/new.rb", chdir: @target)
333+
git(*%W"commit -q -m", "Commit in branch", chdir: @target)
334+
335+
git(*%W"checkout -q default", chdir: @target)
336+
File.write("#@target/lib/conflict.rb", "# 3\n")
337+
git(*%W"add lib/conflict.rb", chdir: @target)
338+
git(*%W"commit -q -m", "Commit in default", chdir: @target)
339+
340+
# How can I suppress "Auto-merging ..." message from git merge?
341+
git(*%W"merge -X ours -m", "Merge commit", "branch", chdir: @target, out: IO::NULL)
342+
343+
out = assert_sync()
344+
assert_equal("# 3\n", File.read("src/lib/conflict.rb"), out)
345+
subject, body = top_commit("src", format: "%B").split("\n\n", 2)
346+
assert_equal("[ruby/#@target] Merge commit", subject, out)
347+
assert_includes(body, "Commit in branch", out)
348+
end
320349
end if /darwin|linux/ =~ RUBY_PLATFORM
321350
end

0 commit comments

Comments
 (0)