diff options
Diffstat (limited to 'spec/mspec/tool/sync')
| -rw-r--r-- | spec/mspec/tool/sync/.gitignore | 4 | ||||
| -rw-r--r-- | spec/mspec/tool/sync/sync-rubyspec.rb | 254 |
2 files changed, 258 insertions, 0 deletions
diff --git a/spec/mspec/tool/sync/.gitignore b/spec/mspec/tool/sync/.gitignore new file mode 100644 index 0000000000..e64f1e8542 --- /dev/null +++ b/spec/mspec/tool/sync/.gitignore @@ -0,0 +1,4 @@ +/jruby +/rubinius +/ruby +/truffleruby diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb new file mode 100644 index 0000000000..86c43d0dc8 --- /dev/null +++ b/spec/mspec/tool/sync/sync-rubyspec.rb @@ -0,0 +1,254 @@ +# This script is based on commands from the wiki: +# https://github.com/ruby/spec/wiki/Merging-specs-from-JRuby-and-other-sources + +IMPLS = { + truffleruby: { + git: "https://github.com/truffleruby/truffleruby.git", + from_commit: "f10ab6988d", + }, + jruby: { + git: "https://github.com/jruby/jruby.git", + from_commit: "f10ab6988d", + }, + rbx: { + git: "https://github.com/rubinius/rubinius.git", + }, + mri: { + git: "https://github.com/ruby/ruby.git", + }, +} + +MSPEC = ARGV.delete('--mspec') + +CHECK_LAST_MERGE = !MSPEC && ENV['CHECK_LAST_MERGE'] != 'false' +TEST_MASTER = ENV['TEST_MASTER'] != 'false' + +ONLY_FILTER = ENV['ONLY_FILTER'] == 'true' + +MSPEC_REPO = File.expand_path("../../..", __FILE__) +raise MSPEC_REPO if !Dir.exist?(MSPEC_REPO) or !Dir.exist?("#{MSPEC_REPO}/.git") + +# Assuming the rubyspec repo is a sibling of the mspec repo +RUBYSPEC_REPO = File.expand_path("../rubyspec", MSPEC_REPO) +raise RUBYSPEC_REPO unless Dir.exist?(RUBYSPEC_REPO) + +SOURCE_REPO = MSPEC ? MSPEC_REPO : RUBYSPEC_REPO + +# LAST_MERGE is a commit of ruby/spec or ruby/mspec +# which is the spec/mspec commit that was last imported in the Ruby implementation +# (i.e. the commit in "Update to ruby/spec@commit"). +# It is normally automatically computed, but can be manually set when +# e.g. the last update of specs wasn't merged in the Ruby implementation. +LAST_MERGE = ENV["LAST_MERGE"] + +NOW = Time.now + +BRIGHT_RED = "\e[31;1m" +BRIGHT_YELLOW = "\e[33;1m" +RESET = "\e[0m" + +# git filter-branch --subdirectory-filter works fine for our use case +ENV['FILTER_BRANCH_SQUELCH_WARNING'] = '1' + +class RubyImplementation + attr_reader :name + + def initialize(name, data) + @name = name.to_s + @data = data + end + + def git_url + @data[:git] + end + + def repo_name + File.basename(git_url, ".git") + end + + def repo_path + "#{__dir__}/#{repo_name}" + end + + def repo_org + File.basename(File.dirname(git_url)) + end + + def from_commit + from = @data[:from_commit] + "#{from}..." if from + end + + def last_merge_message + message = @data[:merge_message] || "Update to ruby/spec@" + message.gsub!("ruby/spec", "ruby/mspec") if MSPEC + message + end + + def prefix + MSPEC ? "spec/mspec" : "spec/ruby" + end + + def rebased_branch + "#{@name}-rebased" + end +end + +def sh(*args) + puts args.join(' ') + system(*args) + raise unless $?.success? +end + +def branch?(name) + branches = `git branch`.sub('*', '').lines.map(&:strip) + branches.include?(name) +end + +def update_repo(impl) + unless File.directory? impl.repo_name + sh "git", "clone", impl.git_url + end + + Dir.chdir(impl.repo_name) do + puts Dir.pwd + + sh "git", "checkout", "master" + sh "git", "pull" + end +end + +def filter_commits(impl) + Dir.chdir(impl.repo_name) do + date = NOW.strftime("%F") + branch = "#{MSPEC ? :mspec : :specs}-#{date}" + + unless branch?(branch) + sh "git", "checkout", "-b", branch + sh "git", "filter-branch", "-f", "--subdirectory-filter", impl.prefix, *impl.from_commit + sh "git", "push", "-f", SOURCE_REPO, "#{branch}:#{impl.name}" + end + end +end + +def rebase_commits(impl) + Dir.chdir(SOURCE_REPO) do + sh "git", "checkout", "master" + sh "git", "pull" + + rebased = impl.rebased_branch + if branch?(rebased) + last_commit = Time.at(Integer(`git log -n 1 --format='%ct' #{rebased}`)) + days_since_last_commit = (NOW-last_commit) / 86400 + if days_since_last_commit > 7 + abort "#{BRIGHT_RED}#{rebased} exists but last commit is old (#{last_commit}), delete the branch if it was merged#{RESET}" + else + puts "#{BRIGHT_YELLOW}#{rebased} already exists, last commit on #{last_commit}, assuming it correct#{RESET}" + sh "git", "checkout", rebased + end + else + sh "git", "checkout", impl.name + + if LAST_MERGE + last_merge = `git log -n 1 --format='%H %ct' #{LAST_MERGE}` + else + last_merge = `git log --grep='^#{impl.last_merge_message}' -n 1 --format='%H %ct'` + end + last_merge, commit_timestamp = last_merge.split(' ') + + raise "Could not find last merge" unless last_merge + puts "Last merge is #{last_merge}" + + commit_date = Time.at(Integer(commit_timestamp)) + days_since_last_merge = (NOW-commit_date) / 86400 + if CHECK_LAST_MERGE and days_since_last_merge > 60 + raise "#{days_since_last_merge.floor} days since last merge, probably wrong commit" + end + + puts "Checking if the last merge is consistent with upstream files" + rubyspec_commit = `git log -n 1 --format='%s' #{last_merge}`.chomp.split('@', 2)[-1] + sh "git", "checkout", last_merge + sh "git", "diff", "--exit-code", rubyspec_commit, "--", ":!.github" + + puts "Rebasing..." + sh "git", "branch", "-D", rebased if branch?(rebased) + sh "git", "checkout", "-b", rebased, impl.name + sh "git", "rebase", "--onto", "master", last_merge + end + end +end + +def new_commits?(impl) + Dir.chdir(SOURCE_REPO) do + diff = `git diff master #{impl.rebased_branch}` + !diff.empty? + end +end + +def test_new_specs + require "yaml" + Dir.chdir(SOURCE_REPO) do + workflow = YAML.load_file(".github/workflows/ci.yml") + job_name = MSPEC ? "test" : "specs" + versions = workflow.dig("jobs", job_name, "strategy", "matrix", "ruby").map(&:to_s) + versions = versions.grep(/^\d+\./) # Test on MRI + min_version, max_version = versions.minmax + + test_command = MSPEC ? "bundle install && bundle exec rspec" : "../mspec/bin/mspec -j" + + run_test = -> version { + command = "chruby ruby-#{version} && #{test_command}" + sh ENV["SHELL"], "-c", command + } + + run_test[min_version] + run_test[max_version] + run_test["master"] if TEST_MASTER + end +end + +def fast_forward_master(impl) + Dir.chdir(SOURCE_REPO) do + sh "git", "checkout", "master" + sh "git", "merge", "--ff-only", impl.rebased_branch + sh "git", "branch", "--delete", impl.rebased_branch + end +end + +def check_ci + puts + puts <<-EOS + Push to master, and check that the CI passes: + https://github.com/ruby/#{:m if MSPEC}spec/commits/master + + EOS +end + +def main(impls) + impls.each_pair do |impl, data| + impl = RubyImplementation.new(impl, data) + update_repo(impl) + filter_commits(impl) + unless ONLY_FILTER + rebase_commits(impl) + if new_commits?(impl) + test_new_specs + fast_forward_master(impl) + check_ci + else + STDERR.puts "#{BRIGHT_YELLOW}No new commits#{RESET}" + fast_forward_master(impl) + end + end + end +end + +if ARGV == ["all"] + impls = IMPLS +else + args = ARGV.map { |arg| arg.to_sym } + raise ARGV.to_s unless (args - IMPLS.keys).empty? + impls = IMPLS.select { |impl| args.include?(impl) } +end + +main(impls) |
