multi_repo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +16 -0
  3. data/.github/workflows/ci.yaml +32 -0
  4. data/.gitignore +6 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_cc.yml +4 -0
  8. data/.rubocop_local.yml +0 -0
  9. data/.whitesource +3 -0
  10. data/Gemfile +6 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +90 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +8 -0
  15. data/exe/multi_repo +29 -0
  16. data/lib/multi_repo/cli.rb +92 -0
  17. data/lib/multi_repo/helpers/git_mirror.rb +198 -0
  18. data/lib/multi_repo/helpers/license.rb +106 -0
  19. data/lib/multi_repo/helpers/pull_request_blaster_outer.rb +129 -0
  20. data/lib/multi_repo/helpers/readme_badges.rb +84 -0
  21. data/lib/multi_repo/helpers/rename_labels.rb +26 -0
  22. data/lib/multi_repo/helpers/update_branch_protection.rb +24 -0
  23. data/lib/multi_repo/helpers/update_labels.rb +34 -0
  24. data/lib/multi_repo/helpers/update_milestone.rb +33 -0
  25. data/lib/multi_repo/helpers/update_repo_settings.rb +23 -0
  26. data/lib/multi_repo/labels.rb +31 -0
  27. data/lib/multi_repo/repo.rb +56 -0
  28. data/lib/multi_repo/repo_set.rb +27 -0
  29. data/lib/multi_repo/service/artifactory.rb +122 -0
  30. data/lib/multi_repo/service/code_climate.rb +119 -0
  31. data/lib/multi_repo/service/docker.rb +178 -0
  32. data/lib/multi_repo/service/git/minigit_capturing_patch.rb +12 -0
  33. data/lib/multi_repo/service/git.rb +90 -0
  34. data/lib/multi_repo/service/github.rb +238 -0
  35. data/lib/multi_repo/service/rubygems_stub.rb +103 -0
  36. data/lib/multi_repo/service/travis.rb +68 -0
  37. data/lib/multi_repo/version.rb +3 -0
  38. data/lib/multi_repo.rb +44 -0
  39. data/multi_repo.gemspec +44 -0
  40. data/repos/.gitkeep +0 -0
  41. data/scripts/delete_labels +23 -0
  42. data/scripts/destroy_branch +23 -0
  43. data/scripts/destroy_remote +26 -0
  44. data/scripts/destroy_tag +31 -0
  45. data/scripts/each_repo +23 -0
  46. data/scripts/fetch_repos +18 -0
  47. data/scripts/git_mirror +9 -0
  48. data/scripts/github_rate_limit +10 -0
  49. data/scripts/hacktoberfest +138 -0
  50. data/scripts/make_alumni +50 -0
  51. data/scripts/new_rubygems_stub +17 -0
  52. data/scripts/pull_request_blaster_outer +24 -0
  53. data/scripts/pull_request_labeler +59 -0
  54. data/scripts/pull_request_merger +63 -0
  55. data/scripts/reenable_repo_workflows +33 -0
  56. data/scripts/rename_labels +22 -0
  57. data/scripts/restart_travis_builds +31 -0
  58. data/scripts/show_commit_history +86 -0
  59. data/scripts/show_org_members +19 -0
  60. data/scripts/show_org_repos +13 -0
  61. data/scripts/show_org_stats +82 -0
  62. data/scripts/show_project_cards +35 -0
  63. data/scripts/show_repo_set +13 -0
  64. data/scripts/show_tag +33 -0
  65. data/scripts/show_travis_status +63 -0
  66. data/scripts/update_branch_protection +22 -0
  67. data/scripts/update_labels +16 -0
  68. data/scripts/update_milestone +21 -0
  69. data/scripts/update_repo_settings +15 -0
  70. metadata +366 -0
@@ -0,0 +1,129 @@
1
+ require 'pathname'
2
+
3
+ module MultiRepo::Helpers
4
+ class PullRequestBlasterOuter
5
+ attr_reader :repo, :base, :head, :script, :dry_run, :message, :title
6
+
7
+ def initialize(repo, base:, head:, script:, dry_run:, message:, title: nil, **)
8
+ @repo = repo
9
+ @base = base
10
+ @head = head
11
+ @script = begin
12
+ s = Pathname.new(script)
13
+ s = Pathname.new(Dir.pwd).join(script) if s.relative?
14
+ raise "File not found #{s}" unless s.exist?
15
+ s.to_s
16
+ end
17
+ @dry_run = dry_run
18
+ @message = message
19
+ @title = (title || message)[0, 72]
20
+ end
21
+
22
+ def blast
23
+ puts "+++ blasting #{repo.name}..."
24
+
25
+ repo.git.fetch
26
+
27
+ unless repo.git.remote_branch?("origin", base)
28
+ puts "!!! Skipping #{repo.name}: 'origin/#{base}' not found"
29
+ return
30
+ end
31
+
32
+ repo.git.hard_checkout(head, "origin/#{base}")
33
+ run_script
34
+
35
+ result = false
36
+ if !commit_changes
37
+ puts "!!! Failed to commit changes. Perhaps the script is wrong or #{repo.name} is already updated."
38
+ elsif dry_run
39
+ result = "Committed but is dry run"
40
+ else
41
+ puts "Do you want to open a pull request on #{repo.name} with the above changes? (Y/N)"
42
+ answer = $stdin.gets.chomp
43
+ if answer.upcase.start_with?("Y")
44
+ fork_repo unless forked?
45
+ push_branch
46
+ result = open_pull_request
47
+ end
48
+ end
49
+ puts "--- blasting #{repo.name} complete"
50
+ result
51
+ end
52
+
53
+ private
54
+
55
+ def github
56
+ @github ||= MultiRepo::Service::Github.new(dry_run: dry_run)
57
+ end
58
+
59
+ def forked?
60
+ github.client.repos(github.client.login).any? { |m| m.name == repo.name }
61
+ end
62
+
63
+ def fork_repo
64
+ github.client.fork(repo.name)
65
+ until forked?
66
+ print "."
67
+ sleep 3
68
+ end
69
+ end
70
+
71
+ def run_script
72
+ repo.chdir do
73
+ parts = []
74
+ parts << "GITHUB_REPO=#{repo.name}"
75
+ parts << "DRY_RUN=true" if dry_run
76
+ parts << script
77
+ cmd = parts.join(" ")
78
+
79
+ unless system(cmd)
80
+ puts "!!! Script execution failed."
81
+ exit $?.exitstatus
82
+ end
83
+ end
84
+ end
85
+
86
+ def commit_changes
87
+ repo.chdir do
88
+ begin
89
+ repo.git.client.add("-v", ".")
90
+ repo.git.client.commit("-m", message)
91
+ repo.git.client.show
92
+ if dry_run
93
+ puts "!!! --dry-run enabled: If the above commit in #{repo.path} looks good, run again without dry run to fork the repo, push the branch and open a pull request."
94
+ end
95
+ true
96
+ rescue MiniGit::GitError => e
97
+ e.status.exitstatus == 0
98
+ end
99
+ end
100
+ end
101
+
102
+ def origin_remote
103
+ "pr_blaster_outer"
104
+ end
105
+
106
+ def origin_url
107
+ "git@github.com:#{github.client.login}/#{repo.name}.git"
108
+ end
109
+
110
+ def pr_head
111
+ "#{github.client.login}:#{head}"
112
+ end
113
+
114
+ def push_branch
115
+ repo.chdir do
116
+ repo.git.client.remote("add", origin_remote, origin_url) unless repo.git.remote?(origin_remote)
117
+ repo.git.client.push("-f", origin_remote, "#{head}:#{head}")
118
+ end
119
+ end
120
+
121
+ def open_pull_request
122
+ pr = github.client.create_pull_request(repo.name, base, pr_head, title, title)
123
+ pr.html_url
124
+ rescue => err
125
+ raise unless err.message.include?("A pull request already exists")
126
+ puts "!!! Skipping. #{err.message}"
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,84 @@
1
+ require "active_support/core_ext/object/deep_dup"
2
+
3
+ module MultiRepo::Helpers
4
+ class ReadmeBadges
5
+ attr_reader :repo, :dry_run
6
+ attr_accessor :badges
7
+
8
+ def initialize(repo, dry_run: false, **)
9
+ @repo = repo
10
+ @dry_run = dry_run
11
+
12
+ if repo.dry_run != dry_run
13
+ raise ArgumentError, "expected repo.dry_run (#{repo.dry_run}) to match dry_run (#{dry_run})"
14
+ end
15
+
16
+ reload
17
+ end
18
+
19
+ def save!
20
+ lines = content.lines
21
+
22
+ apply_badges!(lines)
23
+ save_contents!(lines.join)
24
+
25
+ if dry_run
26
+ reload(lines)
27
+ else
28
+ reload
29
+ end
30
+
31
+ true
32
+ end
33
+
34
+ def content
35
+ return "" unless @file
36
+ File.read(repo.path.join(@file))
37
+ end
38
+
39
+ private
40
+
41
+ def reload(lines = nil)
42
+ @file = repo.detect_readme_file if lines.nil?
43
+
44
+ reload_badges(lines)
45
+ end
46
+
47
+ def reload_badges(lines)
48
+ lines ||= content.lines
49
+ @badges = extract_badges(lines)
50
+ @original_badges = @badges.deep_dup
51
+ @original_badge_indexes = @badges.map { |b| b["index"] }
52
+ end
53
+
54
+ def extract_badges(lines)
55
+ lines.each.with_index.select do |l, _i|
56
+ l.to_s.start_with?("[![")
57
+ end.map do |l, i|
58
+ match = l.match(/\A\[!\[(?<description>[^\]]+)\]\((?<image>[^\)]+)\)\]\((?<url>[^\)]+)\)/)
59
+ match.named_captures.merge("index" => i)
60
+ end
61
+ end
62
+
63
+ def build_badge_string(badge)
64
+ "[![#{badge["description"]}](#{badge["image"]})](#{badge["url"]})\n"
65
+ end
66
+
67
+ def apply_badges!(lines)
68
+ return if badges == @original_badges
69
+
70
+ lines.reject!.with_index { |_l, i| @original_badge_indexes.include?(i) }
71
+
72
+ start_index = @original_badge_indexes[0] || 2
73
+ @badges.reverse_each do |b|
74
+ lines.insert(start_index, build_badge_string(b))
75
+ end
76
+ end
77
+
78
+ def save_contents!(contents)
79
+ repo.rm_file("README")
80
+ repo.rm_file("README.txt")
81
+ repo.write_file("README.md", contents)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,26 @@
1
+ module MultiRepo::Helpers
2
+ class RenameLabels
3
+ attr_reader :repo_name, :rename_hash, :github
4
+
5
+ def initialize(repo_name, rename_hash, dry_run: false, **)
6
+ @repo_name = repo_name
7
+ @rename_hash = rename_hash
8
+ @github = MultiRepo::Service::Github.new(dry_run: dry_run)
9
+ end
10
+
11
+ def run
12
+ rename_hash.each do |old_name, new_name|
13
+ github_label = existing_labels.detect { |l| l.name == old_name }
14
+
15
+ if github_label
16
+ puts "Renaming label #{old_name.inspect} to #{new_name.inspect}"
17
+ github.update_label(repo_name, old_name, name: new_name)
18
+ end
19
+ end
20
+ end
21
+
22
+ private def existing_labels
23
+ @existing_labels ||= github.labels(repo_name)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ module MultiRepo::Helpers
2
+ class UpdateBranchProtection
3
+ attr_reader :repo_name, :branch, :dry_run, :github
4
+
5
+ def initialize(repo_name, branch:, dry_run: false, **)
6
+ @repo_name = repo_name
7
+ @branch = branch
8
+ @github = MultiRepo::Service::Github.new(dry_run: dry_run)
9
+ end
10
+
11
+ def run
12
+ puts "Protecting #{branch} branch"
13
+
14
+ settings = {
15
+ :enforce_admins => nil,
16
+ :required_status_checks => nil,
17
+ :required_pull_request_reviews => nil,
18
+ :restrictions => nil
19
+ }
20
+
21
+ github.protect_branch(repo_name, branch, settings)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ module MultiRepo::Helpers
2
+ class UpdateLabels
3
+ attr_reader :repo_name, :expected_labels, :github
4
+
5
+ def initialize(repo_name, dry_run: false, **)
6
+ @repo_name = repo_name
7
+ @expected_labels = MultiRepo::Labels[repo_name]
8
+ @github = MultiRepo::Service::Github.new(dry_run: dry_run)
9
+ end
10
+
11
+ def run
12
+ if expected_labels.nil?
13
+ puts "!! No labels defined for #{repo_name}"
14
+ return
15
+ end
16
+
17
+ expected_labels.each do |label, color|
18
+ github_label = existing_labels.detect { |l| l.name == label }
19
+
20
+ if !github_label
21
+ puts "Creating #{label.inspect} with #{color.inspect}"
22
+ github.create_label(repo_name, label, color)
23
+ elsif github_label.color.downcase != color.downcase
24
+ puts "Updating #{label.inspect} to #{color.inspect}"
25
+ github.update_label(repo_name, label, color: color)
26
+ end
27
+ end
28
+ end
29
+
30
+ private def existing_labels
31
+ @existing_labels ||= github.client.labels(repo_name)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+
2
+
3
+ module MultiRepo::Helpers
4
+ class UpdateMilestone
5
+ attr_reader :repo_name, :title, :due_on, :close, :github
6
+
7
+ def initialize(repo_name, title:, due_on:, close:, dry_run: false, **)
8
+ raise ArgumentError, "due_on must be specified" if due_on.nil? && !close
9
+
10
+ @repo_name = repo_name
11
+ @title = title
12
+ @due_on = MultiRepo::Service::Github.parse_milestone_date(due_on) if due_on
13
+ @close = close
14
+ @github = MultiRepo::Service::Github.new(dry_run: dry_run)
15
+ end
16
+
17
+ def run
18
+ existing = github.find_milestone_by_title(repo_name, title)
19
+ if close
20
+ if existing
21
+ puts "Closing milestone #{title.inspect} (#{existing.number})"
22
+ github.close_milestone(repo.name, title, existing.number)
23
+ end
24
+ elsif existing
25
+ puts "Updating milestone #{title.inspect} (#{existing.number}) with due date #{due_on_str.inspect}"
26
+ github.update_milestone(repo.name, existing.number, due_on)
27
+ else
28
+ puts "Creating milestone #{title.inspect} with due date #{due_on_str.inspect}"
29
+ github.create_milestone(repo.name, title, due_on)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module MultiRepo::Helpers
2
+ class UpdateRepoSettings
3
+ attr_reader :repo_name, :github
4
+
5
+ def initialize(repo_name, dry_run: false, **)
6
+ @repo_name = repo_name
7
+ @github = MultiRepo::Service::Github.new(dry_run: dry_run)
8
+ end
9
+
10
+ def run
11
+ settings = {
12
+ :has_wiki => false,
13
+ :has_projects => false,
14
+ :allow_merge_commit => true,
15
+ :allow_rebase_merge => false,
16
+ :allow_squash_merge => false,
17
+ }
18
+
19
+ puts "Editing #{repo_name}"
20
+ github.edit_repository(repo_name, settings)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ module MultiRepo
2
+ class Labels
3
+ def self.config_file
4
+ MultiRepo.config_dir.join("labels.yml")
5
+ end
6
+
7
+ def self.config
8
+ @config ||= YAML.unsafe_load_file(config_file)
9
+ end
10
+
11
+ def self.[](repo)
12
+ all[repo]
13
+ end
14
+
15
+ def self.all
16
+ @all ||= begin
17
+ require "more_core_extensions/core_ext/hash/nested"
18
+
19
+ Array(config["orgs"]).each do |org, options|
20
+ MultiRepo::Service::Github.org_repo_names(org).each do |repo_name|
21
+ next if config.key_path?("repos", repo_name)
22
+ next if options["except"].include?(repo_name)
23
+
24
+ config.store_path("repos", repo_name, options["labels"])
25
+ end
26
+ end
27
+ config["repos"].sort.to_h
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ require 'ostruct'
2
+
3
+ module MultiRepo
4
+ class Repo
5
+ attr_reader :name, :config, :path
6
+ attr_accessor :dry_run
7
+
8
+ def initialize(name, config: nil, dry_run: false)
9
+ @name = name
10
+ @dry_run = dry_run
11
+ @config = OpenStruct.new(config || {})
12
+ @path = MultiRepo.repos_dir.join(name)
13
+ end
14
+
15
+ alias to_s inspect
16
+
17
+ def git
18
+ @git ||= MultiRepo::Service::Git.new(path: path, clone_source: config.clone_source || "git@github.com:#{name}.git", dry_run: dry_run)
19
+ end
20
+
21
+ def chdir
22
+ git # Ensures the clone exists
23
+ Dir.chdir(path) { yield }
24
+ end
25
+
26
+ def short_name
27
+ name.split("/").last
28
+ end
29
+
30
+ def write_file(file, content, **kwargs)
31
+ if dry_run
32
+ puts "** dry-run: Writing #{path.join(file).expand_path}".light_black
33
+ else
34
+ chdir { File.write(file, content) }
35
+ end
36
+ end
37
+
38
+ def rm_file(file)
39
+ return unless path.join(file).exist?
40
+
41
+ if dry_run
42
+ puts "** dry-run: Removing #{path.join(file).expand_path}".light_black
43
+ else
44
+ chdir { FileUtils.rm_f(file) }
45
+ end
46
+ end
47
+
48
+ def detect_readme_file
49
+ chdir do
50
+ %w[README.md README README.txt].detect do |f|
51
+ File.exist?(f)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ require 'yaml'
2
+
3
+ module MultiRepo
4
+ class RepoSet
5
+ def self.config_files
6
+ Dir.glob(MultiRepo.config_dir.join("repos*.yml")).sort
7
+ end
8
+
9
+ def self.config
10
+ @config ||= config_files.each_with_object({}) do |f, h|
11
+ h.merge!(YAML.unsafe_load_file(f))
12
+ end
13
+ end
14
+
15
+ def self.[](set_name)
16
+ all[set_name]
17
+ end
18
+
19
+ def self.all
20
+ @all ||= config.transform_values do |repo_set|
21
+ repo_set.map do |repo, config|
22
+ Repo.new(repo, config: config)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,122 @@
1
+ module MultiRepo::Service
2
+ class Artifactory
3
+ def self.api_token
4
+ @api_token ||= ENV.fetch("ARTIFACTORY_API_TOKEN")
5
+ end
6
+
7
+ def self.api_token=(token)
8
+ @api_token = token
9
+ end
10
+
11
+ def self.api_endpoint
12
+ @api_endpoint ||= ENV.fetch("ARTIFACTORY_API_ENDPOINT")
13
+ end
14
+
15
+ def self.api_endpoint=(endpoint)
16
+ @api_endpoint = endpoint
17
+ end
18
+
19
+ def self.auth_header
20
+ {"X-JFrog-Art-Api" => api_token}
21
+ end
22
+
23
+ attr_accessor :dry_run, :cache
24
+
25
+ def initialize(dry_run: false, cache: true)
26
+ require "rest-client"
27
+ require "json"
28
+
29
+ @dry_run = dry_run
30
+ @cache = cache
31
+ end
32
+
33
+ def clear_cache
34
+ FileUtils.rm_f(Dir.glob("/tmp/artifactory-*"))
35
+ end
36
+
37
+ # https://www.jfrog.com/confluence/display/JFROG/RPM+Repositories
38
+ def get(path, **kwargs)
39
+ path = path.to_s
40
+ request(:get, path, **kwargs)
41
+ end
42
+
43
+ def list(folder, cache: @cache, **kwargs)
44
+ folder = folder.to_s
45
+ cache_file = "/tmp/artifactory-#{folder.tr("/", "_")}-#{Date.today}.txt"
46
+ if cache && File.exist?(cache_file)
47
+ File.readlines(cache_file, :chomp => true)
48
+ else
49
+ data = raw_list(folder, cache: cache, **kwargs)
50
+ uri = data["uri"]
51
+
52
+ data["files"].map { |d| File.join(uri, d["uri"]) }.tap do |d|
53
+ File.write(cache_file, d.join("\n")) if cache
54
+ end
55
+ end
56
+ end
57
+
58
+ def raw_list(folder, cache: @cache, **kwargs)
59
+ folder = folder.to_s
60
+ cache_file = "/tmp/artifactory-#{folder.tr("/", "_")}-raw-#{Date.today}.json"
61
+ if cache && File.exist?(cache_file)
62
+ JSON.parse(File.read(cache_file))
63
+ else
64
+ get("/api/storage/#{folder}?list&deep=1", **kwargs).tap do |d|
65
+ File.write(cache_file, JSON.pretty_generate(d)) if cache
66
+ end
67
+ end
68
+ end
69
+
70
+ def delete(file, **kwargs)
71
+ file = file.to_s
72
+ request(:delete, strip_api_prefix(file), **kwargs)
73
+ rescue RestClient::NotFound => err
74
+ # Ignore deletes on a 404 because it's already deleted
75
+ raise unless err.http_code == 404
76
+ end
77
+
78
+ def move(file, to, **kwargs)
79
+ file = file.to_s
80
+ to = to.to_s
81
+ request(:post, File.join("/api/move", "#{strip_api_prefix(file)}?to=/#{strip_api_prefix(to)}"), **kwargs)
82
+ end
83
+
84
+ def copy(file, to, **kwargs)
85
+ file = file.to_s
86
+ to = to.to_s
87
+ request(:post, File.join("/api/copy", "#{strip_api_prefix(file)}?to=/#{strip_api_prefix(to)}"), **kwargs)
88
+ end
89
+
90
+ private
91
+
92
+ def request(verb, path, body: nil, headers: nil, verbose: true)
93
+ headers ||= self.class.auth_header.merge(
94
+ "Accept" => "application/json",
95
+ "Content-Type" => "application/json"
96
+ )
97
+ path = File.join(self.class.api_endpoint, path)
98
+
99
+ puts "+ #{verb.to_s.upcase} #{path}".light_black if verbose
100
+ if dry_run && %i[delete put post patch].include?(verb)
101
+ puts "+ dry_run: #{verb.to_s.upcase} #{path}".light_black if verbose
102
+ {}
103
+ else
104
+ response =
105
+ if %i[put post patch].include?(verb)
106
+ RestClient.send(verb, path, body, headers)
107
+ else
108
+ RestClient.send(verb, path, headers)
109
+ end
110
+ response.empty? ? {} : JSON.parse(response)
111
+ end
112
+ end
113
+
114
+ def api_file_prefix
115
+ File.join(self.class.api_endpoint, "api/storage")
116
+ end
117
+
118
+ def strip_api_prefix(path)
119
+ path.start_with?(api_file_prefix) ? path.sub(api_file_prefix, "") : path
120
+ end
121
+ end
122
+ end