summaryrefslogtreecommitdiff
path: root/tool/sync_default_gems.rb
diff options
context:
space:
mode:
Diffstat (limited to 'tool/sync_default_gems.rb')
-rwxr-xr-xtool/sync_default_gems.rb1124
1 files changed, 596 insertions, 528 deletions
diff --git a/tool/sync_default_gems.rb b/tool/sync_default_gems.rb
index cb4e0af50b..db64e20274 100755
--- a/tool/sync_default_gems.rb
+++ b/tool/sync_default_gems.rb
@@ -4,6 +4,8 @@
require 'fileutils'
require "rbconfig"
+require "find"
+require "tempfile"
module SyncDefaultGems
include FileUtils
@@ -11,78 +13,331 @@ module SyncDefaultGems
module_function
+ # upstream: "owner/repo"
+ # branch: "branch_name"
+ # mappings: [ ["path_in_upstream", "path_in_ruby"], ... ]
+ # NOTE: path_in_ruby is assumed to be "owned" by this gem, and the contents
+ # will be removed before sync
+ # exclude: [ "fnmatch_pattern_after_mapping", ... ]
+ Repository = Data.define(:upstream, :branch, :mappings, :exclude) do
+ def excluded?(newpath)
+ p = newpath
+ until p == "."
+ return true if exclude.any? {|pat| File.fnmatch?(pat, p, File::FNM_PATHNAME|File::FNM_EXTGLOB)}
+ p = File.dirname(p)
+ end
+ false
+ end
+
+ def rewrite_for_ruby(path)
+ newpath = mappings.find do |src, dst|
+ if path == src || path.start_with?(src + "/")
+ break path.sub(src, dst)
+ end
+ end
+ return nil unless newpath
+ return nil if excluded?(newpath)
+ newpath
+ end
+ end
+
+ CLASSICAL_DEFAULT_BRANCH = "master"
+
+ def repo((upstream, branch), mappings, exclude: [])
+ branch ||= CLASSICAL_DEFAULT_BRANCH
+ exclude += ["ext/**/depend"]
+ Repository.new(upstream:, branch:, mappings:, exclude:)
+ end
+
+ def lib((upstream, branch), gemspec_in_subdir: false)
+ _org, name = upstream.split("/")
+ gemspec_dst = gemspec_in_subdir ? "lib/#{name}/#{name}.gemspec" : "lib/#{name}.gemspec"
+ repo([upstream, branch], [
+ ["lib/#{name}.rb", "lib/#{name}.rb"],
+ ["lib/#{name}", "lib/#{name}"],
+ ["test/test_#{name}.rb", "test/test_#{name}.rb"],
+ ["test/#{name}", "test/#{name}"],
+ ["#{name}.gemspec", gemspec_dst],
+ ])
+ end
+
+ # Note: tool/auto_review_pr.rb also depends on these constants.
+ NO_UPSTREAM = [
+ "lib/unicode_normalize", # not to match with "lib/un"
+ ]
REPOSITORIES = {
- "io-console": 'ruby/io-console',
- "io-nonblock": 'ruby/io-nonblock',
- "io-wait": 'ruby/io-wait',
- "net-http": "ruby/net-http",
- "net-protocol": "ruby/net-protocol",
- "open-uri": "ruby/open-uri",
- "win32-registry": "ruby/win32-registry",
- English: "ruby/English",
- cgi: "ruby/cgi",
- date: 'ruby/date',
- delegate: "ruby/delegate",
- did_you_mean: "ruby/did_you_mean",
- digest: "ruby/digest",
- erb: "ruby/erb",
- error_highlight: "ruby/error_highlight",
- etc: 'ruby/etc',
- fcntl: 'ruby/fcntl',
- fileutils: 'ruby/fileutils',
- find: "ruby/find",
- forwardable: "ruby/forwardable",
- ipaddr: 'ruby/ipaddr',
- json: 'ruby/json',
- mmtk: ['ruby/mmtk', "main"],
- open3: "ruby/open3",
- openssl: "ruby/openssl",
- optparse: "ruby/optparse",
- pp: "ruby/pp",
- prettyprint: "ruby/prettyprint",
- prism: ["ruby/prism", "main"],
- psych: 'ruby/psych',
- resolv: "ruby/resolv",
- rubygems: 'rubygems/rubygems',
- securerandom: "ruby/securerandom",
- shellwords: "ruby/shellwords",
- singleton: "ruby/singleton",
- stringio: 'ruby/stringio',
- strscan: 'ruby/strscan',
- syntax_suggest: ["ruby/syntax_suggest", "main"],
- tempfile: "ruby/tempfile",
- time: "ruby/time",
- timeout: "ruby/timeout",
- tmpdir: "ruby/tmpdir",
- tsort: "ruby/tsort",
- un: "ruby/un",
- uri: "ruby/uri",
- weakref: "ruby/weakref",
- yaml: "ruby/yaml",
- zlib: 'ruby/zlib',
+ Onigmo: repo("k-takata/Onigmo", [
+ ["regcomp.c", "regcomp.c"],
+ ["regenc.c", "regenc.c"],
+ ["regenc.h", "regenc.h"],
+ ["regerror.c", "regerror.c"],
+ ["regexec.c", "regexec.c"],
+ ["regint.h", "regint.h"],
+ ["regparse.c", "regparse.c"],
+ ["regparse.h", "regparse.h"],
+ ["regsyntax.c", "regsyntax.c"],
+ ["onigmo.h", "include/ruby/onigmo.h"],
+ ["enc", "enc"],
+ ]),
+ "io-console": repo("ruby/io-console", [
+ ["ext/io/console", "ext/io/console"],
+ ["test/io/console", "test/io/console"],
+ ["lib/io/console", "ext/io/console/lib/console"],
+ ["io-console.gemspec", "ext/io/console/io-console.gemspec"],
+ ]),
+ "io-nonblock": repo("ruby/io-nonblock", [
+ ["ext/io/nonblock", "ext/io/nonblock"],
+ ["test/io/nonblock", "test/io/nonblock"],
+ ["io-nonblock.gemspec", "ext/io/nonblock/io-nonblock.gemspec"],
+ ]),
+ "io-wait": repo("ruby/io-wait", [
+ ["ext/io/wait", "ext/io/wait"],
+ ["test/io/wait", "test/io/wait"],
+ ["io-wait.gemspec", "ext/io/wait/io-wait.gemspec"],
+ ]),
+ "net-http": repo("ruby/net-http", [
+ ["lib/net/http.rb", "lib/net/http.rb"],
+ ["lib/net/http", "lib/net/http"],
+ ["test/net/http", "test/net/http"],
+ ["net-http.gemspec", "lib/net/http/net-http.gemspec"],
+ ]),
+ "net-protocol": repo("ruby/net-protocol", [
+ ["lib/net/protocol.rb", "lib/net/protocol.rb"],
+ ["test/net/protocol", "test/net/protocol"],
+ ["net-protocol.gemspec", "lib/net/net-protocol.gemspec"],
+ ]),
+ "open-uri": lib("ruby/open-uri"),
+ English: lib("ruby/English"),
+ date: repo("ruby/date", [
+ ["doc/date", "doc/date"],
+ ["ext/date", "ext/date"],
+ ["lib", "ext/date/lib"],
+ ["test/date", "test/date"],
+ ["date.gemspec", "ext/date/date.gemspec"],
+ ], exclude: [
+ "ext/date/lib/date_core.bundle",
+ ]),
+ delegate: lib("ruby/delegate"),
+ did_you_mean: repo("ruby/did_you_mean", [
+ ["lib/did_you_mean.rb", "lib/did_you_mean.rb"],
+ ["lib/did_you_mean", "lib/did_you_mean"],
+ ["test", "test/did_you_mean"],
+ ["did_you_mean.gemspec", "lib/did_you_mean/did_you_mean.gemspec"],
+ ], exclude: [
+ "test/did_you_mean/lib",
+ "test/did_you_mean/tree_spell/test_explore.rb",
+ ]),
+ digest: repo("ruby/digest", [
+ ["ext/digest/lib/digest/sha2", "ext/digest/sha2/lib/sha2"],
+ ["ext/digest", "ext/digest"],
+ ["lib/digest.rb", "ext/digest/lib/digest.rb"],
+ ["lib/digest/version.rb", "ext/digest/lib/digest/version.rb"],
+ ["lib/digest/sha2.rb", "ext/digest/sha2/lib/sha2.rb"],
+ ["test/digest", "test/digest"],
+ ["digest.gemspec", "ext/digest/digest.gemspec"],
+ ]),
+ erb: repo("ruby/erb", [
+ ["ext/erb", "ext/erb"],
+ ["lib/erb", "lib/erb"],
+ ["lib/erb.rb", "lib/erb.rb"],
+ ["test/erb", "test/erb"],
+ ["erb.gemspec", "lib/erb/erb.gemspec"],
+ ["libexec/erb", "libexec/erb"],
+ ]),
+ error_highlight: repo("ruby/error_highlight", [
+ ["lib/error_highlight.rb", "lib/error_highlight.rb"],
+ ["lib/error_highlight", "lib/error_highlight"],
+ ["test", "test/error_highlight"],
+ ["error_highlight.gemspec", "lib/error_highlight/error_highlight.gemspec"],
+ ]),
+ etc: repo("ruby/etc", [
+ ["ext/etc", "ext/etc"],
+ ["test/etc", "test/etc"],
+ ["etc.gemspec", "ext/etc/etc.gemspec"],
+ ]),
+ fcntl: repo("ruby/fcntl", [
+ ["ext/fcntl", "ext/fcntl"],
+ ["fcntl.gemspec", "ext/fcntl/fcntl.gemspec"],
+ ]),
+ fileutils: lib("ruby/fileutils"),
+ find: lib("ruby/find"),
+ forwardable: lib("ruby/forwardable", gemspec_in_subdir: true),
+ ipaddr: lib("ruby/ipaddr"),
+ json: repo("ruby/json", [
+ ["ext/json/ext", "ext/json"],
+ ["test/json", "test/json"],
+ ["lib", "ext/json/lib"],
+ ["json.gemspec", "ext/json/json.gemspec"],
+ ], exclude: [
+ "ext/json/lib/json/ext/.keep",
+ "ext/json/lib/json/pure.rb",
+ "ext/json/lib/json/pure",
+ "ext/json/lib/json/truffle_ruby",
+ "test/json/lib",
+ "ext/json/extconf.rb",
+ ]),
+ mmtk: repo(["ruby/mmtk", "main"], [
+ ["gc/mmtk", "gc/mmtk"],
+ ]),
+ open3: lib("ruby/open3", gemspec_in_subdir: true).tap {
+ it.exclude << "lib/open3/jruby_windows.rb"
+ },
+ openssl: repo("ruby/openssl", [
+ ["ext/openssl", "ext/openssl"],
+ ["lib", "ext/openssl/lib"],
+ ["test/openssl", "test/openssl"],
+ ["sample", "sample/openssl"],
+ ["openssl.gemspec", "ext/openssl/openssl.gemspec"],
+ ["History.md", "ext/openssl/History.md"],
+ ], exclude: [
+ "test/openssl/envutil.rb",
+ "ext/openssl/depend",
+ ]),
+ optparse: lib("ruby/optparse", gemspec_in_subdir: true).tap {
+ it.mappings << ["doc/optparse", "doc/optparse"]
+ },
+ pp: lib("ruby/pp"),
+ prettyprint: lib("ruby/prettyprint"),
+ prism: repo(["ruby/prism", "main"], [
+ ["ext/prism", "prism"],
+ ["lib/prism.rb", "lib/prism.rb"],
+ ["lib/prism", "lib/prism"],
+ ["test/prism", "test/prism"],
+ ["src", "prism"],
+ ["prism.gemspec", "lib/prism/prism.gemspec"],
+ ["include/prism", "prism"],
+ ["include/prism.h", "prism/prism.h"],
+ ["config.yml", "prism/config.yml"],
+ ["templates", "prism/templates"],
+ ], exclude: [
+ "prism/templates/{javascript,java,rbi,sig}",
+ "test/prism/snapshots_test.rb",
+ "test/prism/snapshots",
+ "prism/extconf.rb",
+ "prism/srcs.mk*",
+ ]),
+ psych: repo("ruby/psych", [
+ ["ext/psych", "ext/psych"],
+ ["lib", "ext/psych/lib"],
+ ["test/psych", "test/psych"],
+ ["psych.gemspec", "ext/psych/psych.gemspec"],
+ ], exclude: [
+ "ext/psych/lib/org",
+ "ext/psych/lib/psych.jar",
+ "ext/psych/lib/psych_jars.rb",
+ "ext/psych/lib/psych.{bundle,so}",
+ "ext/psych/lib/2.*",
+ "ext/psych/yaml/LICENSE",
+ "ext/psych/.gitignore",
+ ]),
+ resolv: repo("ruby/resolv", [
+ ["lib/resolv.rb", "lib/resolv.rb"],
+ ["test/resolv", "test/resolv"],
+ ["resolv.gemspec", "lib/resolv.gemspec"],
+ ["ext/win32/resolv/lib/resolv.rb", "ext/win32/lib/win32/resolv.rb"],
+ ["ext/win32/resolv", "ext/win32/resolv"],
+ ]),
+ rubygems: repo("ruby/rubygems", [
+ ["lib/rubygems.rb", "lib/rubygems.rb"],
+ ["lib/rubygems", "lib/rubygems"],
+ ["test/rubygems", "test/rubygems"],
+ ["bundler/lib/bundler.rb", "lib/bundler.rb"],
+ ["bundler/lib/bundler", "lib/bundler"],
+ ["bundler/exe/bundle", "libexec/bundle"],
+ ["bundler/exe/bundler", "libexec/bundler"],
+ ["bundler/bundler.gemspec", "lib/bundler/bundler.gemspec"],
+ ["spec", "spec/bundler"],
+ *["bundle", "parallel_rspec", "rspec"].map {|binstub|
+ ["bin/#{binstub}", "spec/bin/#{binstub}"]
+ },
+ *%w[dev_gems test_gems rubocop_gems standard_gems].flat_map {|gemfile|
+ ["rb.lock", "rb"].map do |ext|
+ ["tool/bundler/#{gemfile}.#{ext}", "tool/bundler/#{gemfile}.#{ext}"]
+ end
+ },
+ ], exclude: [
+ "spec/bundler/bin",
+ "spec/bundler/support/artifice/vcr_cassettes",
+ "spec/bundler/support/artifice/used_cassettes.txt",
+ "lib/{bundler,rubygems}/**/{COPYING,LICENSE,README}{,.{md,txt,rdoc}}",
+ ]),
+ securerandom: lib("ruby/securerandom"),
+ shellwords: lib("ruby/shellwords"),
+ singleton: lib("ruby/singleton"),
+ stringio: repo("ruby/stringio", [
+ ["ext/stringio", "ext/stringio"],
+ ["test/stringio", "test/stringio"],
+ ["stringio.gemspec", "ext/stringio/stringio.gemspec"],
+ ["doc/stringio", "doc/stringio"],
+ ], exclude: [
+ "ext/stringio/README.md",
+ ]),
+ strscan: repo("ruby/strscan", [
+ ["ext/strscan", "ext/strscan"],
+ ["lib", "ext/strscan/lib"],
+ ["test/strscan", "test/strscan"],
+ ["strscan.gemspec", "ext/strscan/strscan.gemspec"],
+ ["doc/strscan", "doc/strscan"],
+ ], exclude: [
+ "ext/strscan/regenc.h",
+ "ext/strscan/regint.h",
+ "ext/strscan/lib/strscan/truffleruby.rb",
+ ]),
+ syntax_suggest: repo(["ruby/syntax_suggest", "main"], [
+ ["lib/syntax_suggest.rb", "lib/syntax_suggest.rb"],
+ ["lib/syntax_suggest", "lib/syntax_suggest"],
+ ["syntax_suggest.gemspec", "lib/syntax_suggest/syntax_suggest.gemspec"],
+ ["exe/syntax_suggest", "libexec/syntax_suggest"],
+ ["spec", "spec/syntax_suggest"],
+ ]),
+ tempfile: lib("ruby/tempfile"),
+ time: lib("ruby/time"),
+ timeout: lib("ruby/timeout"),
+ tmpdir: lib("ruby/tmpdir"),
+ un: lib("ruby/un"),
+ uri: lib("ruby/uri", gemspec_in_subdir: true),
+ weakref: lib("ruby/weakref"),
+ yaml: lib("ruby/yaml", gemspec_in_subdir: true),
+ zlib: repo("ruby/zlib", [
+ ["ext/zlib", "ext/zlib"],
+ ["test/zlib", "test/zlib"],
+ ["zlib.gemspec", "ext/zlib/zlib.gemspec"],
+ ]),
}.transform_keys(&:to_s)
- CLASSICAL_DEFAULT_BRANCH = "master"
+ def REPOSITORIES.[](gem)
+ fetch(gem) {raise "unknown repository - #{gem}"}
+ end
- class << REPOSITORIES
- def [](gem)
- repo, branch = super(gem)
- return repo, branch || CLASSICAL_DEFAULT_BRANCH
+ class << Repository
+ def find_upstream(file)
+ return if NO_UPSTREAM.any? {|dst| file.start_with?(dst) }
+ REPOSITORIES.find do |repo_name, repository|
+ if repository.mappings.any? {|_src, dst| file.start_with?(dst) }
+ break repo_name
+ end
+ end
end
- def each_pair
- super do |gem, (repo, branch)|
- yield gem, [repo, branch || CLASSICAL_DEFAULT_BRANCH]
- end
+ def group(files)
+ files.group_by {|file| find_upstream(file)}
end
end
+ # Allow synchronizing commits up to this FETCH_DEPTH. We've historically merged PRs
+ # with about 250 commits to ruby/ruby, so we use this depth for ruby/ruby in general.
+ FETCH_DEPTH = 500
+
def pipe_readlines(args, rs: "\0", chomp: true)
IO.popen(args) do |f|
f.readlines(rs, chomp: chomp)
end
end
+ def porcelain_status(*pattern)
+ pipe_readlines(%W"git status --porcelain --no-renames -z --" + pattern)
+ end
+
def replace_rdoc_ref(file)
src = File.binread(file)
changed = false
@@ -101,7 +356,7 @@ module SyncDefaultGems
end
def replace_rdoc_ref_all
- result = pipe_readlines(%W"git status --porcelain -z -- *.c *.rb *.rdoc")
+ result = porcelain_status("*.c", "*.rb", "*.rdoc")
result.map! {|line| line[/\A.M (.*)/, 1]}
result.compact!
return if result.empty?
@@ -109,247 +364,69 @@ module SyncDefaultGems
result.inject(false) {|changed, file| changed | replace_rdoc_ref(file)}
end
+ def replace_rdoc_ref_all_full
+ Dir.glob("**/*.{c,rb,rdoc}").inject(false) {|changed, file| changed | replace_rdoc_ref(file)}
+ end
+
+ def rubygems_do_fixup
+ gemspec_content = File.readlines("lib/bundler/bundler.gemspec").map do |line|
+ next if line =~ /LICENSE\.md/
+
+ line.gsub("bundler.gemspec", "lib/bundler/bundler.gemspec")
+ end.compact.join
+ File.write("lib/bundler/bundler.gemspec", gemspec_content)
+
+ ["bundle", "parallel_rspec", "rspec"].each do |binstub|
+ path = "spec/bin/#{binstub}"
+ next unless File.exist?(path)
+ content = File.read(path).gsub("../spec", "../bundler")
+ File.write(path, content)
+ chmod("+x", path)
+ end
+ end
+
# We usually don't use this. Please consider using #sync_default_gems_with_commits instead.
def sync_default_gems(gem)
- repo, = REPOSITORIES[gem]
- puts "Sync #{repo}"
-
- upstream = File.join("..", "..", repo)
-
- case gem
- when "rubygems"
- rm_rf(%w[lib/rubygems lib/rubygems.rb test/rubygems])
- cp_r(Dir.glob("#{upstream}/lib/rubygems*"), "lib")
- cp_r("#{upstream}/test/rubygems", "test")
- rm_rf(%w[lib/bundler lib/bundler.rb libexec/bundler libexec/bundle spec/bundler tool/bundler/*])
- cp_r(Dir.glob("#{upstream}/bundler/lib/bundler*"), "lib")
- cp_r(Dir.glob("#{upstream}/bundler/exe/bundle*"), "libexec")
-
- gemspec_content = File.readlines("#{upstream}/bundler/bundler.gemspec").map do |line|
- next if line =~ /LICENSE\.md/
-
- line.gsub("bundler.gemspec", "lib/bundler/bundler.gemspec")
- end.compact.join
- File.write("lib/bundler/bundler.gemspec", gemspec_content)
-
- cp_r("#{upstream}/bundler/spec", "spec/bundler")
- rm_rf("spec/bundler/bin")
-
- ["bundle", "parallel_rspec", "rspec"].each do |binstub|
- content = File.read("#{upstream}/bundler/bin/#{binstub}").gsub("../spec", "../bundler")
- File.write("spec/bin/#{binstub}", content)
- chmod("+x", "spec/bin/#{binstub}")
- end
+ config = REPOSITORIES[gem]
+ puts "Sync #{config.upstream}"
+
+ upstream = File.join("..", "..", config.upstream)
- %w[dev_gems test_gems rubocop_gems standard_gems].each do |gemfile|
- ["rb.lock", "rb"].each do |ext|
- cp_r("#{upstream}/tool/bundler/#{gemfile}.#{ext}", "tool/bundler")
+ unless File.exist?(upstream)
+ abort %[Expected '#{upstream}' (#{File.expand_path("#{upstream}")}) to be a directory, but it didn't exist.]
+ end
+
+ config.mappings.each do |src, dst|
+ rm_rf(dst)
+ end
+
+ copied = Set.new
+ config.mappings.each do |src, dst|
+ prefix = File.join(upstream, src)
+ # Maybe mapping needs to be updated?
+ next unless File.exist?(prefix)
+ Find.find(prefix) do |path|
+ next if File.directory?(path)
+ if copied.add?(path)
+ newpath = config.rewrite_for_ruby(path.sub(%r{\A#{Regexp.escape(upstream)}/}, ""))
+ next unless newpath
+ mkdir_p(File.dirname(newpath))
+ cp(path, newpath)
end
end
- rm_rf Dir.glob("spec/bundler/support/artifice/{vcr_cassettes,used_cassettes.txt}")
- rm_rf Dir.glob("lib/{bundler,rubygems}/**/{COPYING,LICENSE,README}{,.{md,txt,rdoc}}")
- when "json"
- rm_rf(%w[ext/json lib/json test/json])
- cp_r("#{upstream}/ext/json/ext", "ext/json")
- cp_r("#{upstream}/test/json", "test/json")
- rm_rf("test/json/lib")
- cp_r("#{upstream}/lib", "ext/json")
- cp_r("#{upstream}/json.gemspec", "ext/json")
- rm_rf(%w[ext/json/lib/json/pure.rb ext/json/lib/json/pure ext/json/lib/json/truffle_ruby/])
- json_files = Dir.glob("ext/json/lib/json/ext/**/*", File::FNM_DOTMATCH).select { |f| File.file?(f) }
- rm_rf(json_files - Dir.glob("ext/json/lib/json/ext/**/*.rb") - Dir.glob("ext/json/lib/json/ext/**/depend"))
- `git checkout ext/json/extconf.rb ext/json/generator/depend ext/json/parser/depend ext/json/depend benchmark/`
- when "psych"
- rm_rf(%w[ext/psych test/psych])
- cp_r("#{upstream}/ext/psych", "ext")
- cp_r("#{upstream}/lib", "ext/psych")
- cp_r("#{upstream}/test/psych", "test")
- rm_rf(%w[ext/psych/lib/org ext/psych/lib/psych.jar ext/psych/lib/psych_jars.rb])
- rm_rf(%w[ext/psych/lib/psych.{bundle,so} ext/psych/lib/2.*])
- rm_rf(["ext/psych/yaml/LICENSE"])
- cp_r("#{upstream}/psych.gemspec", "ext/psych")
- `git checkout ext/psych/depend ext/psych/.gitignore`
- when "stringio"
- rm_rf(%w[ext/stringio test/stringio])
- cp_r("#{upstream}/ext/stringio", "ext")
- cp_r("#{upstream}/test/stringio", "test")
- cp_r("#{upstream}/stringio.gemspec", "ext/stringio")
- `git checkout ext/stringio/depend ext/stringio/README.md`
- when "io-console"
- rm_rf(%w[ext/io/console test/io/console])
- cp_r("#{upstream}/ext/io/console", "ext/io")
- cp_r("#{upstream}/test/io/console", "test/io")
- mkdir_p("ext/io/console/lib")
- cp_r("#{upstream}/lib/io/console", "ext/io/console/lib")
- rm_rf("ext/io/console/lib/console/ffi")
- cp_r("#{upstream}/io-console.gemspec", "ext/io/console")
- `git checkout ext/io/console/depend`
- when "io-nonblock"
- rm_rf(%w[ext/io/nonblock test/io/nonblock])
- cp_r("#{upstream}/ext/io/nonblock", "ext/io")
- cp_r("#{upstream}/test/io/nonblock", "test/io")
- cp_r("#{upstream}/io-nonblock.gemspec", "ext/io/nonblock")
- `git checkout ext/io/nonblock/depend`
- when "io-wait"
- rm_rf(%w[ext/io/wait test/io/wait])
- cp_r("#{upstream}/ext/io/wait", "ext/io")
- cp_r("#{upstream}/test/io/wait", "test/io")
- cp_r("#{upstream}/io-wait.gemspec", "ext/io/wait")
- `git checkout ext/io/wait/depend`
- when "etc"
- rm_rf(%w[ext/etc test/etc])
- cp_r("#{upstream}/ext/etc", "ext")
- cp_r("#{upstream}/test/etc", "test")
- cp_r("#{upstream}/etc.gemspec", "ext/etc")
- `git checkout ext/etc/depend`
- when "date"
- rm_rf(%w[ext/date test/date])
- cp_r("#{upstream}/doc/date", "doc")
- cp_r("#{upstream}/ext/date", "ext")
- cp_r("#{upstream}/lib", "ext/date")
- cp_r("#{upstream}/test/date", "test")
- cp_r("#{upstream}/date.gemspec", "ext/date")
- `git checkout ext/date/depend`
- rm_rf(["ext/date/lib/date_core.bundle"])
- when "zlib"
- rm_rf(%w[ext/zlib test/zlib])
- cp_r("#{upstream}/ext/zlib", "ext")
- cp_r("#{upstream}/test/zlib", "test")
- cp_r("#{upstream}/zlib.gemspec", "ext/zlib")
- `git checkout ext/zlib/depend`
- when "fcntl"
- rm_rf(%w[ext/fcntl])
- cp_r("#{upstream}/ext/fcntl", "ext")
- cp_r("#{upstream}/fcntl.gemspec", "ext/fcntl")
- `git checkout ext/fcntl/depend`
- when "strscan"
- rm_rf(%w[ext/strscan test/strscan])
- cp_r("#{upstream}/ext/strscan", "ext")
- cp_r("#{upstream}/lib", "ext/strscan")
- cp_r("#{upstream}/test/strscan", "test")
- cp_r("#{upstream}/strscan.gemspec", "ext/strscan")
- begin
- cp_r("#{upstream}/doc/strscan", "doc")
- rescue Errno::ENOENT
+ end
+
+ porcelain_status().each do |line|
+ /\A(?:.)(?:.) (?<path>.*)\z/ =~ line or raise
+ if config.excluded?(path)
+ puts "Restoring excluded file: #{path}"
+ IO.popen(%W"git checkout --" + [path], "rb", &:read)
end
- rm_rf(%w["ext/strscan/regenc.h ext/strscan/regint.h"])
- `git checkout ext/strscan/depend`
- when "cgi"
- rm_rf(%w[lib/cgi.rb lib/cgi ext/cgi test/cgi])
- cp_r("#{upstream}/ext/cgi", "ext")
- mkdir_p("lib/cgi")
- cp_r("#{upstream}/lib/cgi/escape.rb", "lib/cgi")
- mkdir_p("test/cgi")
- cp_r("#{upstream}/test/cgi/test_cgi_escape.rb", "test/cgi")
- cp_r("#{upstream}/test/cgi/update_env.rb", "test/cgi")
- rm_rf("lib/cgi/escape.jar")
- `git checkout lib/cgi.rb lib/cgi/util.rb ext/cgi/escape/depend`
- when "openssl"
- rm_rf(%w[ext/openssl test/openssl])
- cp_r("#{upstream}/ext/openssl", "ext")
- cp_r("#{upstream}/lib", "ext/openssl")
- cp_r("#{upstream}/test/openssl", "test")
- rm_rf("test/openssl/envutil.rb")
- cp_r("#{upstream}/openssl.gemspec", "ext/openssl")
- cp_r("#{upstream}/History.md", "ext/openssl")
- `git checkout ext/openssl/depend`
- when "net-protocol"
- rm_rf(%w[lib/net/protocol.rb lib/net/net-protocol.gemspec test/net/protocol])
- cp_r("#{upstream}/lib/net/protocol.rb", "lib/net")
- cp_r("#{upstream}/test/net/protocol", "test/net")
- cp_r("#{upstream}/net-protocol.gemspec", "lib/net")
- when "net-http"
- rm_rf(%w[lib/net/http.rb lib/net/http test/net/http])
- cp_r("#{upstream}/lib/net/http.rb", "lib/net")
- cp_r("#{upstream}/lib/net/http", "lib/net")
- cp_r("#{upstream}/test/net/http", "test/net")
- cp_r("#{upstream}/net-http.gemspec", "lib/net/http")
- when "did_you_mean"
- rm_rf(%w[lib/did_you_mean lib/did_you_mean.rb test/did_you_mean])
- cp_r(Dir.glob("#{upstream}/lib/did_you_mean*"), "lib")
- cp_r("#{upstream}/did_you_mean.gemspec", "lib/did_you_mean")
- cp_r("#{upstream}/test", "test/did_you_mean")
- rm_rf("test/did_you_mean/lib")
- rm_rf(%w[test/did_you_mean/tree_spell/test_explore.rb])
- when "erb"
- rm_rf(%w[lib/erb* test/erb libexec/erb])
- cp_r("#{upstream}/lib/erb.rb", "lib")
- cp_r("#{upstream}/test/erb", "test")
- cp_r("#{upstream}/erb.gemspec", "lib/erb")
- cp_r("#{upstream}/libexec/erb", "libexec")
- when "digest"
- rm_rf(%w[ext/digest test/digest])
- cp_r("#{upstream}/ext/digest", "ext")
- mkdir_p("ext/digest/lib/digest")
- cp_r("#{upstream}/lib/digest.rb", "ext/digest/lib/")
- cp_r("#{upstream}/lib/digest/version.rb", "ext/digest/lib/digest/")
- mkdir_p("ext/digest/sha2/lib")
- cp_r("#{upstream}/lib/digest/sha2.rb", "ext/digest/sha2/lib")
- move("ext/digest/lib/digest/sha2", "ext/digest/sha2/lib")
- cp_r("#{upstream}/test/digest", "test")
- cp_r("#{upstream}/digest.gemspec", "ext/digest")
- `git checkout ext/digest/depend ext/digest/*/depend`
- when "optparse"
- sync_lib gem, upstream
- rm_rf(%w[doc/optparse])
- mkdir_p("doc/optparse")
- cp_r("#{upstream}/doc/optparse", "doc")
- when "error_highlight"
- rm_rf(%w[lib/error_highlight lib/error_highlight.rb test/error_highlight])
- cp_r(Dir.glob("#{upstream}/lib/error_highlight*"), "lib")
- cp_r("#{upstream}/error_highlight.gemspec", "lib/error_highlight")
- cp_r("#{upstream}/test", "test/error_highlight")
- when "open3"
- sync_lib gem, upstream
- rm_rf("lib/open3/jruby_windows.rb")
- when "syntax_suggest"
- sync_lib gem, upstream
- rm_rf(%w[spec/syntax_suggest libexec/syntax_suggest])
- cp_r("#{upstream}/spec", "spec/syntax_suggest")
- cp_r("#{upstream}/exe/syntax_suggest", "libexec/syntax_suggest")
- when "prism"
- rm_rf(%w[test/prism prism])
-
- cp_r("#{upstream}/ext/prism", "prism")
- cp_r("#{upstream}/lib/.", "lib")
- cp_r("#{upstream}/test/prism", "test")
- cp_r("#{upstream}/src/.", "prism")
-
- cp_r("#{upstream}/prism.gemspec", "lib/prism")
- cp_r("#{upstream}/include/prism/.", "prism")
- cp_r("#{upstream}/include/prism.h", "prism")
-
- cp_r("#{upstream}/config.yml", "prism/")
- cp_r("#{upstream}/templates", "prism/")
- rm_rf("prism/templates/javascript")
- rm_rf("prism/templates/java")
- rm_rf("prism/templates/rbi")
- rm_rf("prism/templates/sig")
-
- rm("test/prism/snapshots_test.rb")
- rm_rf("test/prism/snapshots")
-
- rm("prism/extconf.rb")
- `git checkout prism/srcs.mk*`
- when "resolv"
- rm_rf(%w[lib/resolv.* ext/win32/resolv test/resolv ext/win32/lib/win32/resolv.rb])
- cp_r("#{upstream}/lib/resolv.rb", "lib")
- cp_r("#{upstream}/resolv.gemspec", "lib")
- cp_r("#{upstream}/ext/win32/resolv", "ext/win32")
- move("ext/win32/resolv/lib/resolv.rb", "ext/win32/lib/win32")
- rm_rf("ext/win32/resolv/lib") # Clean up empty directory
- cp_r("#{upstream}/test/resolv", "test")
- `git checkout ext/win32/resolv/depend`
- when "win32-registry"
- rm_rf(%w[ext/win32/lib/win32/registry.rb test/win32/test_registry.rb])
- cp_r("#{upstream}/lib/win32/registry.rb", "ext/win32/lib/win32")
- cp_r("#{upstream}/test/win32/test_registry.rb", "test/win32")
- cp_r("#{upstream}/win32-registry.gemspec", "ext/win32")
- when "mmtk"
- rm_rf("gc/mmtk")
- cp_r("#{upstream}/gc/mmtk", "gc")
- else
- sync_lib gem, upstream
+ end
+
+ # RubyGems/Bundler needs special care
+ if gem == "rubygems"
+ rubygems_do_fixup
end
check_prerelease_version(gem)
@@ -360,15 +437,13 @@ module SyncDefaultGems
end
def check_prerelease_version(gem)
- return if ["rubygems", "mmtk", "cgi"].include?(gem)
-
- gem = gem.downcase
+ return if ["rubygems", "mmtk", "Onigmo"].include?(gem)
require "net/https"
require "json"
require "uri"
- uri = URI("https://rubygems.org/api/v1/versions/#{gem}/latest.json")
+ uri = URI("https://rubygems.org/api/v1/versions/#{gem.downcase}/latest.json")
response = Net::HTTP.get(uri)
latest_version = JSON.parse(response)["version"]
@@ -385,42 +460,19 @@ module SyncDefaultGems
puts "#{gem}-#{spec.version} is not latest version of rubygems.org" if spec.version.to_s != latest_version
end
- def ignore_file_pattern_for(gem)
- patterns = []
-
- # Common patterns
- patterns << %r[\A(?:
- [^/]+ # top-level entries
- |\.git.*
- |bin/.*
- |ext/.*\.java
- |rakelib/.*
- |test/(?:lib|fixtures)/.*
- |tool/(?!bundler/).*
- )\z]mx
-
- # Gem-specific patterns
- case gem
- when nil
- end&.tap do |pattern|
- patterns << pattern
- end
-
- Regexp.union(*patterns)
- end
-
- def message_filter(repo, sha, input: ARGF)
+ def message_filter(repo, sha, log, context: nil)
unless repo.count("/") == 1 and /\A\S+\z/ =~ repo
raise ArgumentError, "invalid repository: #{repo}"
end
unless /\A\h{10,40}\z/ =~ sha
raise ArgumentError, "invalid commit-hash: #{sha}"
end
- log = input.read
- log.delete!("\r")
- log << "\n" if !log.end_with?("\n")
repo_url = "https://github.com/#{repo}"
+ # Log messages generated by GitHub web UI have inconsistent line endings
+ log = log.delete("\r")
+ log << "\n" if !log.end_with?("\n")
+
# Split the subject from the log message according to git conventions.
# SPECIAL TREAT: when the first line ends with a dot `.` (which is not
# obeying the conventions too), takes only that line.
@@ -442,41 +494,69 @@ module SyncDefaultGems
end
end
commit_url = "#{repo_url}/commit/#{sha[0,10]}\n"
+ sync_note = context ? "#{commit_url}\n#{context}" : commit_url
if log and !log.empty?
log.sub!(/(?<=\n)\n+\z/, '') # drop empty lines at the last
conv[log]
log.sub!(/(?:(\A\s*)|\s*\n)(?=((?i:^Co-authored-by:.*\n?)+)?\Z)/) {
- ($~.begin(1) ? "" : "\n\n") + commit_url + ($~.begin(2) ? "\n" : "")
+ ($~.begin(1) ? "" : "\n\n") + sync_note + ($~.begin(2) ? "\n" : "")
}
else
- log = commit_url
+ log = sync_note
end
- puts subject, "\n", log
+ "#{subject}\n\n#{log}"
end
- # Returns commit list as array of [commit_hash, subject].
- def commits_in_ranges(gem, repo, default_branch, ranges)
- # If -a is given, discover all commits since the last picked commit
- if ranges == true
- # \r? needed in the regex in case the commit has windows-style line endings (because e.g. we're running
- # tests on Windows)
- pattern = "https://github\.com/#{Regexp.quote(repo)}/commit/([0-9a-f]+)\r?$"
- log = IO.popen(%W"git log -E --grep=#{pattern} -n1 --format=%B", "rb", &:read)
- ranges = ["#{log[%r[#{pattern}\n\s*(?i:co-authored-by:.*)*\s*\Z], 1]}..#{gem}/#{default_branch}"]
- end
+ def log_format(format, args, &block)
+ IO.popen(%W[git -c core.autocrlf=false -c core.eol=lf
+ log --no-show-signature --format=#{format}] + args, "rb", &block)
+ end
- # Parse a given range with git log
- ranges.flat_map do |range|
- unless range.include?("..")
- range = "#{range}~1..#{range}"
- end
+ def commits_in_range(upto, exclude, toplevel:)
+ args = [upto, *exclude.map {|s|"^#{s}"}]
+ log_format('%H,%P,%s', %W"--first-parent" + args) do |f|
+ f.read.split("\n").reverse.flat_map {|commit|
+ hash, parents, subject = commit.split(',', 3)
+ parents = parents.split
+
+ # Non-merge commit
+ if parents.size <= 1
+ puts "#{hash} #{subject}"
+ next [[hash, subject]]
+ end
- IO.popen(%W"git log --format=%H,%s #{range} --", "rb") do |f|
- f.read.split("\n").reverse.map{|commit| commit.split(',', 2)}
- end
+ # Clean 2-parent merge commit: follow the other parent as long as it
+ # contains no potentially-non-clean merges
+ if parents.size == 2 &&
+ IO.popen(%W"git diff-tree --remerge-diff #{hash}", "rb", &:read).empty?
+ puts "\e[2mChecking the other parent of #{hash} #{subject}\e[0m"
+ ret = catch(:quit) {
+ commits_in_range(parents[1], exclude + [parents[0]], toplevel: false)
+ }
+ next ret if ret
+ end
+
+ unless toplevel
+ puts "\e[1mMerge commit with possible conflict resolution #{hash} #{subject}\e[0m"
+ throw :quit
+ end
+
+ puts "#{hash} #{subject} " \
+ "\e[1m[merge commit with possible conflicts, will do a squash merge]\e[0m"
+ [[hash, subject]]
+ }
end
end
+ # Returns commit list as array of [commit_hash, subject, sync_note].
+ def commits_in_ranges(ranges)
+ ranges.flat_map do |range|
+ exclude, upto = range.include?("..") ? range.split("..", 2) : ["#{range}~1", range]
+ puts "Looking for commits in range #{exclude}..#{upto}"
+ commits_in_range(upto, exclude.empty? ? [] : [exclude], toplevel: true)
+ end.uniq
+ end
+
#--
# Following methods used by sync_default_gems_with_commits return
# true: success
@@ -485,27 +565,9 @@ module SyncDefaultGems
#++
def resolve_conflicts(gem, sha, edit)
- # Skip this commit if everything has been removed as `ignored_paths`.
- changes = pipe_readlines(%W"git status --porcelain -z")
- if changes.empty?
- puts "Skip empty commit #{sha}"
- return false
- end
-
- # We want to skip DD: deleted by both.
- deleted = changes.grep(/^DD /) {$'}
- system(*%W"git rm -f --", *deleted) unless deleted.empty?
-
- # Import UA: added by them
- added = changes.grep(/^UA /) {$'}
- system(*%W"git add --", *added) unless added.empty?
-
- # Discover unmerged files
- # AU: unmerged, added by us
- # DU: unmerged, deleted by us
- # UU: unmerged, both modified
- # AA: unmerged, both added
- conflict = changes.grep(/\A(?:.U|AA) /) {$'}
+ # Discover unmerged files: any unstaged changes
+ changes = porcelain_status()
+ conflict = changes.grep(/\A(?:.[^ ?]) /) {$'}
# If -e option is given, open each conflicted file with an editor
unless conflict.empty?
if edit
@@ -526,123 +588,170 @@ module SyncDefaultGems
return true
end
- def preexisting?(base, file)
- system(*%w"git cat-file -e", "#{base}:#{file}", err: File::NULL)
+ def collect_cacheinfo(tree)
+ pipe_readlines(%W"git ls-tree -r -t -z #{tree}").filter_map do |line|
+ fields, path = line.split("\t", 2)
+ mode, type, object = fields.split(" ", 3)
+ next unless type == "blob"
+ [mode, type, object, path]
+ end
end
- def filter_pickup_files(changed, ignore_file_pattern, base)
- toplevels = {}
- remove = []
- ignore = []
- changed = changed.reject do |f|
- case
- when toplevels.fetch(top = f[%r[\A[^/]+(?=/|\z)]m]) {
- remove << top if toplevels[top] = !preexisting?(base, top)
- }
- # Remove any new top-level directories.
- true
- when ignore_file_pattern.match?(f)
- # Forcibly reset any changes matching ignore_file_pattern.
- (preexisting?(base, f) ? ignore : remove) << f
- end
+ def rewrite_cacheinfo(gem, blobs)
+ config = REPOSITORIES[gem]
+ rewritten = []
+ ignored = blobs.dup
+ ignored.delete_if do |mode, type, object, path|
+ newpath = config.rewrite_for_ruby(path)
+ next unless newpath
+ rewritten << [mode, type, object, newpath]
end
- return changed, remove, ignore
+ [rewritten, ignored]
end
- def pickup_files(gem, changed, picked)
- # Forcibly remove any files that we don't want to copy to this
- # repository.
-
- ignore_file_pattern = ignore_file_pattern_for(gem)
+ def make_commit_info(gem, sha)
+ config = REPOSITORIES[gem]
+ headers, orig = IO.popen(%W[git cat-file commit #{sha}], "rb", &:read).split("\n\n", 2)
+ /^author (?<author_name>.+?) <(?<author_email>.*?)> (?<author_date>.+?)$/ =~ headers or
+ raise "unable to parse author info for commit #{sha}"
+ author = {
+ "GIT_AUTHOR_NAME" => author_name,
+ "GIT_AUTHOR_EMAIL" => author_email,
+ "GIT_AUTHOR_DATE" => author_date,
+ }
+ context = nil
+ if /^parent (?<first_parent>.{40})\nparent .{40}$/ =~ headers
+ # Squashing a merge commit: keep authorship information
+ context = IO.popen(%W"git shortlog #{first_parent}..#{sha} --", "rb", &:read)
+ end
+ message = message_filter(config.upstream, sha, orig, context: context)
+ [author, message]
+ end
- base = picked ? "HEAD~" : "HEAD"
- changed, remove, ignore = filter_pickup_files(changed, ignore_file_pattern, base)
+ def fixup_commit(gem, commit)
+ wt = File.join("tmp", "sync_default_gems-fixup-worktree")
+ if File.directory?(wt)
+ IO.popen(%W"git -C #{wt} clean -xdf", "rb", &:read)
+ IO.popen(%W"git -C #{wt} reset --hard #{commit}", "rb", &:read)
+ else
+ IO.popen(%W"git worktree remove --force #{wt}", "rb", err: File::NULL, &:read)
+ IO.popen(%W"git worktree add --detach #{wt} #{commit}", "rb", &:read)
+ end
+ raise "git worktree prepare failed for commit #{commit}" unless $?.success?
- unless remove.empty?
- puts "Remove added files: #{remove.join(', ')}"
- system(*%w"git rm -fr --", *remove)
- if picked
- system(*%w"git commit --amend --no-edit --", *remove, %i[out err] => File::NULL)
+ Dir.chdir(wt) do
+ if gem == "rubygems"
+ rubygems_do_fixup
end
+ replace_rdoc_ref_all_full
end
- unless ignore.empty?
- puts "Reset ignored files: #{ignore.join(', ')}"
- system(*%W"git rm -r --", *ignore)
- ignore.each {|f| system(*%W"git checkout -f", base, "--", f)}
- end
+ IO.popen(%W"git -C #{wt} add -u", "rb", &:read)
+ IO.popen(%W"git -C #{wt} commit --amend --no-edit", "rb", &:read)
+ IO.popen(%W"git -C #{wt} rev-parse HEAD", "rb", &:read).chomp
+ end
+
+ def make_and_fixup_commit(gem, original_commit, cacheinfo, parent: nil, message: nil, author: nil)
+ tree = Tempfile.create("sync_default_gems-#{gem}-index") do |f|
+ File.unlink(f.path)
+ IO.popen({"GIT_INDEX_FILE" => f.path},
+ %W"git update-index --index-info", "wb", out: IO::NULL) do |io|
+ cacheinfo.each do |mode, type, object, path|
+ io.puts("#{mode} #{type} #{object}\t#{path}")
+ end
+ end
+ raise "git update-index failed" unless $?.success?
- if changed.empty?
- return nil
+ IO.popen({"GIT_INDEX_FILE" => f.path}, %W"git write-tree --missing-ok", "rb", &:read).chomp
end
- return changed
+ args = ["-m", message || "Rewriten commit for #{original_commit}"]
+ args += ["-p", parent] if parent
+ commit = IO.popen({**author}, %W"git commit-tree #{tree}" + args, "rb", &:read).chomp
+
+ # Apply changes that require a working tree
+ commit = fixup_commit(gem, commit)
+
+ commit
end
- def pickup_commit(gem, sha, edit)
- # Attempt to cherry-pick a commit
- result = IO.popen(%W"git cherry-pick #{sha}", "rb", &:read)
- picked = $?.success?
- if result =~ /nothing\ to\ commit/
- `git reset`
- puts "Skip empty commit #{sha}"
+ def rewrite_commit(gem, sha)
+ author, message = make_commit_info(gem, sha)
+ new_blobs = collect_cacheinfo("#{sha}")
+ new_rewritten, new_ignored = rewrite_cacheinfo(gem, new_blobs)
+
+ headers, _ = IO.popen(%W[git cat-file commit #{sha}], "rb", &:read).split("\n\n", 2)
+ first_parent = headers[/^parent (.{40})$/, 1]
+ unless first_parent
+ # Root commit, first time to sync this repo
+ return make_and_fixup_commit(gem, sha, new_rewritten, message: message, author: author)
+ end
+
+ old_blobs = collect_cacheinfo(first_parent)
+ old_rewritten, old_ignored = rewrite_cacheinfo(gem, old_blobs)
+ if old_ignored != new_ignored
+ paths = (old_ignored + new_ignored - (old_ignored & new_ignored))
+ .map {|*_, path| path}.uniq
+ puts "\e\[1mIgnoring file changes not in mappings: #{paths.join(" ")}\e\[0m"
+ end
+ changed_paths = (old_rewritten + new_rewritten - (old_rewritten & new_rewritten))
+ .map {|*_, path| path}.uniq
+ if changed_paths.empty?
+ puts "Skip commit only for tools or toplevel"
return false
end
- # Skip empty commits
- if result.empty?
- return false
- end
+ # Build commit objects from "cacheinfo"
+ new_parent = make_and_fixup_commit(gem, first_parent, old_rewritten)
+ new_commit = make_and_fixup_commit(gem, sha, new_rewritten, parent: new_parent, message: message, author: author)
+ puts "Created a temporary commit for cherry-pick: #{new_commit}"
+ new_commit
+ end
- if picked
- changed = pipe_readlines(%w"git diff-tree --name-only -r -z HEAD~..HEAD --")
- else
- changed = pipe_readlines(%w"git diff --name-only -r -z HEAD --")
- end
+ def pickup_commit(gem, sha, edit)
+ rewritten = rewrite_commit(gem, sha)
- # Pick up files to merge.
- unless changed = pickup_files(gem, changed, picked)
- puts "Skip commit #{sha} only for tools or toplevel"
- if picked
- `git reset --hard HEAD~`
- else
- `git cherry-pick --abort`
- end
- return false
- end
+ # No changes remaining after rewriting
+ return false unless rewritten
- # If the cherry-pick attempt failed, try to resolve conflicts.
- # Skip the commit, if it contains unresolved conflicts or no files to pick up.
- unless picked or resolve_conflicts(gem, sha, edit)
- `git reset` && `git checkout .` && `git clean -fd`
- return picked || nil # Fail unless cherry-picked
- end
+ # Attempt to cherry-pick a commit
+ result = IO.popen(%W"git cherry-pick #{rewritten}", "rb", err: [:child, :out], &:read)
+ unless $?.success?
+ if result =~ /The previous cherry-pick is now empty/
+ system(*%w"git cherry-pick --skip")
+ puts "Skip empty commit #{sha}"
+ return false
+ end
- # Commit cherry-picked commit
- if picked
- system(*%w"git commit --amend --no-edit")
- else
- system(*%w"git cherry-pick --continue --no-edit")
- end or return nil
+ # If the cherry-pick attempt failed, try to resolve conflicts.
+ # Skip the commit, if it contains unresolved conflicts or no files to pick up.
+ unless resolve_conflicts(gem, sha, edit)
+ system(*%w"git --no-pager diff") if !edit # If failed, show `git diff` unless editing
+ `git reset` && `git checkout .` && `git clean -fd` # Clean up un-committed diffs
+ return nil # Fail unless cherry-picked
+ end
- # Amend the commit if RDoc references need to be replaced
- head = `git log --format=%H -1 HEAD`.chomp
- system(*%w"git reset --quiet HEAD~ --")
- amend = replace_rdoc_ref_all
- system(*%W"git reset --quiet #{head} --")
- if amend
- `git commit --amend --no-edit --all`
+ # Commit cherry-picked commit
+ if porcelain_status().empty?
+ system(*%w"git cherry-pick --skip")
+ return false
+ else
+ system(*%w"git cherry-pick --continue --no-edit")
+ return nil unless $?.success?
+ end
end
+ new_head = IO.popen(%W"git rev-parse HEAD", "rb", &:read).chomp
+ puts "Committed cherry-pick as #{new_head}"
return true
end
- # NOTE: This method is also used by GitHub ruby/git.ruby-lang.org's bin/update-default-gem.sh
# @param gem [String] A gem name, also used as a git remote name. REPOSITORIES converts it to the appropriate GitHub repository.
- # @param ranges [Array<String>] "before..after". Note that it will NOT sync "before" (but commits after that).
+ # @param ranges [Array<String>, true] "commit", "before..after", or true. Note that it will NOT sync "before" (but commits after that).
# @param edit [TrueClass] Set true if you want to resolve conflicts. Obviously, update-default-gem.sh doesn't use this.
def sync_default_gems_with_commits(gem, ranges, edit: nil)
- repo, default_branch = REPOSITORIES[gem]
+ config = REPOSITORIES[gem]
+ repo, default_branch = config.upstream, config.branch
puts "Sync #{repo} with commit history."
# Fetch the repository to be synchronized
@@ -651,86 +760,46 @@ module SyncDefaultGems
`git remote add #{gem} https://github.com/#{repo}.git`
end
end
- system(*%W"git fetch --no-tags #{gem}")
-
- commits = commits_in_ranges(gem, repo, default_branch, ranges)
+ system(*%W"git fetch --no-tags --depth=#{FETCH_DEPTH} #{gem} #{default_branch}")
- # Ignore Merge commits and already-merged commits.
- commits.delete_if do |sha, subject|
- subject.start_with?("Merge", "Auto Merge")
+ # If -a is given, discover all commits since the last picked commit
+ if ranges == true
+ pattern = "https://github\.com/#{Regexp.quote(repo)}/commit/([0-9a-f]+)$"
+ log = log_format('%B', %W"-E --grep=#{pattern} -n1 --", &:read)
+ ranges = ["#{log[%r[#{pattern}\n\s*(?i:co-authored-by:.*)*\s*\Z], 1]}..#{gem}/#{default_branch}"]
end
-
+ commits = commits_in_ranges(ranges)
if commits.empty?
puts "No commits to pick"
return true
end
- puts "Try to pick these commits:"
- puts commits.map{|commit| commit.join(": ")}
- puts "----"
-
failed_commits = []
-
- require 'shellwords'
- filter = [
- ENV.fetch('RUBY', 'ruby').shellescape,
- File.realpath(__FILE__).shellescape,
- "--message-filter",
- ]
commits.each do |sha, subject|
- puts "Pick #{sha} from #{repo}."
+ puts "----"
+ puts "Pick #{sha} #{subject}"
case pickup_commit(gem, sha, edit)
when false
- next
+ # skipped
when nil
- failed_commits << sha
- next
- end
-
- puts "Update commit message: #{sha}"
-
- # Run this script itself (tool/sync_default_gems.rb --message-filter) as a message filter
- IO.popen({"FILTER_BRANCH_SQUELCH_WARNING" => "1"},
- %W[git filter-branch -f --msg-filter #{[filter, repo, sha].join(' ')} -- HEAD~1..HEAD],
- &:read)
- unless $?.success?
- puts "Failed to modify commit message of #{sha}"
- break
+ failed_commits << [sha, subject]
end
end
unless failed_commits.empty?
puts "---- failed commits ----"
- puts failed_commits
+ failed_commits.each do |sha, subject|
+ puts "#{sha} #{subject}"
+ end
return false
end
return true
end
- def sync_lib(repo, upstream = nil)
- unless upstream and File.directory?(upstream) or File.directory?(upstream = "../#{repo}")
- abort %[Expected '#{upstream}' \(#{File.expand_path("#{upstream}")}\) to be a directory, but it wasn't.]
- end
- rm_rf(["lib/#{repo}.rb", "lib/#{repo}/*", "test/test_#{repo}.rb"])
- cp_r(Dir.glob("#{upstream}/lib/*"), "lib")
- tests = if File.directory?("test/#{repo}")
- "test/#{repo}"
- else
- "test/test_#{repo}.rb"
- end
- cp_r("#{upstream}/#{tests}", "test") if File.exist?("#{upstream}/#{tests}")
- gemspec = if File.directory?("lib/#{repo}")
- "lib/#{repo}/#{repo}.gemspec"
- else
- "lib/#{repo}.gemspec"
- end
- cp_r("#{upstream}/#{repo}.gemspec", "#{gemspec}")
- end
-
def update_default_gems(gem, release: false)
-
- repository, default_branch = REPOSITORIES[gem]
- author, repository = repository.split('/')
+ config = REPOSITORIES[gem]
+ author, repository = config.upstream.split('/')
+ default_branch = config.branch
puts "Update #{author}/#{repository}"
@@ -770,28 +839,18 @@ module SyncDefaultGems
REPOSITORIES.each_key {|gem| update_default_gems(gem)}
end
when "all"
- if ARGV[1] == "release"
- REPOSITORIES.each_key do |gem|
- update_default_gems(gem, release: true)
- sync_default_gems(gem)
- end
- else
- REPOSITORIES.each_key {|gem| sync_default_gems(gem)}
+ REPOSITORIES.each_key do |gem|
+ next if ["Onigmo"].include?(gem)
+ update_default_gems(gem, release: true) if ARGV[1] == "release"
+ sync_default_gems(gem)
end
when "list"
ARGV.shift
pattern = Regexp.new(ARGV.join('|'))
- REPOSITORIES.each_pair do |name, (gem)|
- next unless pattern =~ name or pattern =~ gem
- printf "%-15s https://github.com/%s\n", name, gem
- end
- when "--message-filter"
- ARGV.shift
- if ARGV.size < 2
- abort "usage: #{$0} --message-filter repository commit-hash [input...]"
+ REPOSITORIES.each do |gem, config|
+ next unless pattern =~ gem or pattern =~ config.upstream
+ printf "%-15s https://github.com/%s\n", gem, config.upstream
end
- message_filter(*ARGV.shift(2))
- exit
when "rdoc-ref"
ARGV.shift
pattern = ARGV.empty? ? %w[*.c *.rb *.rdoc] : ARGV
@@ -807,7 +866,13 @@ module SyncDefaultGems
puts <<-HELP
\e[1mSync with upstream code of default libraries\e[0m
-\e[1mImport a default library through `git clone` and `cp -rf` (git commits are lost)\e[0m
+\e[1mImport all default gems through `git clone` and `cp -rf` (git commits are lost)\e[0m
+ ruby #$0 all
+
+\e[1mImport all released version of default gems\e[0m
+ ruby #$0 all release
+
+\e[1mImport a default gem with specific gem same as all command\e[0m
ruby #$0 rubygems
\e[1mPick a single commit from the upstream repository\e[0m
@@ -819,6 +884,9 @@ module SyncDefaultGems
\e[1mPick all commits since the last picked commit\e[0m
ruby #$0 -a rubygems
+\e[1mUpdate repositories of default gems\e[0m
+ ruby #$0 up
+
\e[1mList known libraries\e[0m
ruby #$0 list