cimas 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.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ci/master"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,44 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "cimas"
5
+ require "cimas/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "cimas"
9
+ spec.version = Cimas::VERSION
10
+ spec.authors = ['Ribose Inc.']
11
+ spec.email = ['open.source@ribose.com']
12
+
13
+ spec.summary = %q{Automate and synchronize CI configuration across repositories.}
14
+ spec.description = %q{see --help}
15
+ spec.homepage = "https://github.com/metanorma/cimas"
16
+ spec.license = "BSD-2-Clause"
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/metanorma/cimas"
23
+ else
24
+ raise "RubyGems 2.0 or newer is required to protect against " \
25
+ "public gem pushes."
26
+ end
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
31
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ end
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_dependency "travis"
38
+ spec.add_dependency "octokit"
39
+ spec.add_dependency "git"
40
+
41
+ spec.add_development_dependency "bundler", "~> 2.0"
42
+ spec.add_development_dependency "rake", "~> 10.0"
43
+ spec.add_development_dependency "rspec", "~> 3.0"
44
+ end
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pathname'
4
+ require 'optparse'
5
+
6
+ require_relative '../lib/cimas/cli/command'
7
+
8
+ options = {
9
+ 'repos_path' => Pathname.getwd + 'repos',
10
+ 'config_file_path' => Pathname.getwd + 'ci.yml',
11
+ 'config_master_path' => Pathname.getwd + 'config'
12
+ }
13
+
14
+ top_help = <<HELP
15
+ Commonly used command are:
16
+ sync : do update ci configurations for all repos described in config
17
+ #lint : do lint for ci configurations
18
+ pull : do pull for repos
19
+ push : do push changes to repote server
20
+ open-prs : do PR for Github with hub utility
21
+ See 'cimas COMMAND --help' for more information on a specific command.
22
+ HELP
23
+
24
+ def validate_config_path(config_path)
25
+ if config_path.nil? || !config_path.exist?
26
+ raise OptionParser::MissingArgument, "-c/--config-path path is not set or does not exist"
27
+ end
28
+
29
+ config_path
30
+ end
31
+
32
+ global = OptionParser.new do |opts|
33
+ opts.banner = "Usage: cimas [options] [subcommand [options]]"
34
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
35
+ options['verbose'] = v
36
+ end
37
+ opts.on("-d", "--dry-run", "Run verbosely") do |v|
38
+ options['dry_run'] = v
39
+ end
40
+ opts.separator ""
41
+ opts.separator top_help
42
+ end
43
+
44
+ subcommands = {
45
+ 'setup' => OptionParser.new do |opts|
46
+ opts.banner = "Usage: cimas setup [options]"
47
+
48
+ opts.on("-r REPOS_PATH", "--repo-path=REPOS_PATH", "Repo root dir path") do |path|
49
+ options['repos_path'] = Pathname.getwd + path
50
+ end
51
+
52
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
53
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
54
+ end
55
+ end,
56
+ 'sync' => OptionParser.new do |opts|
57
+ opts.banner = "Usage: cimas sync [options]"
58
+
59
+ opts.on("-r REPOS_PATH", "--repo-path=REPOS_PATH", "Repo root dir path") do |path|
60
+ options['repos_path'] = Pathname.getwd + path
61
+ end
62
+
63
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
64
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
65
+ end
66
+
67
+ opts.on("-d CONFIG_MASTER_DIR_PATH", "--master-path=CONFIG_MASTER_DIR_PATH", "Config master path") do |path|
68
+ options['config_master_path'] = validate_config_path(Pathname.getwd + path)
69
+ end
70
+
71
+ opts.on("-g GROUP1,GROUP2,GROUPX", Array, "Groups to update") do |groups|
72
+ options['groups'] = groups
73
+ end
74
+ end,
75
+ 'diff' => OptionParser.new do |opts|
76
+ opts.banner = "Usage: cimas diff [options]"
77
+
78
+ opts.on("-r REPOS_PATH", "--repo-path=REPOS_PATH", "Repo root dir path") do |path|
79
+ options['repos_path'] = Pathname.getwd + path
80
+ end
81
+
82
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
83
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
84
+ end
85
+
86
+ opts.on("-d CONFIG_MASTER_DIR_PATH", "--master-path=CONFIG_MASTER_DIR_PATH", "Config master path") do |path|
87
+ options['config_master_path'] = validate_config_path(Pathname.getwd + path)
88
+ end
89
+
90
+ opts.on("-g GROUP1,GROUP2,GROUPX", Array, "Groups to update") do |groups|
91
+ options['groups'] = groups
92
+ end
93
+ end,
94
+ 'lint' => OptionParser.new do |opts|
95
+ opts.banner = "Usage: cimas lint [options]"
96
+
97
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
98
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
99
+ end
100
+
101
+ opts.on("-d CONFIG_MASTER_DIR_PATH", "--master-path=CONFIG_MASTER_DIR_PATH", "Config master path") do |path|
102
+ options['config_master_path'] = validate_config_path(Pathname.getwd + path)
103
+ end
104
+
105
+ opts.on("-a", "--appveyor-bearer=APPVEYOR_BEARER", "Appveyor API Bearer token") do |token|
106
+ options['appveyor_token'] = token
107
+ end
108
+ opts.on("-g GROUP1,GROUP2,GROUPX", Array, "Groups to update") do |groups|
109
+ options['groups'] = groups
110
+ end
111
+ end,
112
+ 'pull' => OptionParser.new do |opts|
113
+ opts.banner = "Usage: cimas pull [options]"
114
+
115
+ opts.on("-r REPOS_PATH", "--repo-path=REPOS_PATH", "Repo root dir path") do |path|
116
+ options['repos_path'] = Pathname.getwd + path
117
+ end
118
+
119
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
120
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
121
+ end
122
+
123
+ opts.on("-b BRANCH", "--pull-branch=BRANCH", "Branch to pull in all repos") do |branch|
124
+ options['pull_branch'] = branch
125
+ end
126
+
127
+ opts.on("-g GROUP1,GROUP2,GROUPX", Array, "Groups to update") do |groups|
128
+ options['groups'] = groups
129
+ end
130
+ end,
131
+ 'push' => OptionParser.new do |opts|
132
+ opts.banner = "Usage: cimas push [options]"
133
+
134
+ opts.on("-r REPOS_PATH", "--repo-path=REPOS_PATH", "Repo root dir path") do |path|
135
+ options['repos_path'] = Pathname.getwd + path
136
+ end
137
+
138
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
139
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
140
+ end
141
+
142
+ opts.on("-b BRANCH", "--push-branch=BRANCH", "Branch to push in all repos") do |branch|
143
+ options['push_to_branch'] = branch
144
+ end
145
+
146
+ opts.on("-m MESSAGE", "--message=MESSAGE", "Commit message") do |message|
147
+ options['commit_message'] = message
148
+ end
149
+
150
+ opts.on("-g GROUP1,GROUP2,GROUPX", Array, "Groups to update") do |groups|
151
+ options['groups'] = groups
152
+ end
153
+ # TODO: implement
154
+ # opts.on("--force", "Force push (with commit amend)") do |force|
155
+ # options['force_push'] = force
156
+ # end
157
+ end,
158
+ 'open-prs' => OptionParser.new do |opts|
159
+ opts.banner = "Usage: cimas open-pr [options]"
160
+
161
+ opts.on("-r REPOS_PATH", "--repo-path=REPOS_PATH", "Repo root dir path") do |path|
162
+ options['repos_path'] = Pathname.getwd + path
163
+ end
164
+
165
+ opts.on("-f CONFIG_FILE_PATH", "--config-path=CONFIG_FILE_PATH", "Config file path") do |path|
166
+ options['config_file_path'] = validate_config_path(Pathname.getwd + path)
167
+ end
168
+
169
+ opts.on("-w REVIEWERS", "--reviewers=REVIEWERS", "A comma-separated list (no spaces around the comma) of GitHub handles to request a review from") do |reviewers|
170
+ options['reviewers'] = reviewers
171
+ end
172
+
173
+ opts.on("-b BRANCH", "--merge-branch=BRANCH", "PR branch to merge into target") do |branch|
174
+ options['merge_branch'] = branch
175
+ end
176
+
177
+ opts.on("-m MESSAGE", "--message=MESSAGE", "Commit message") do |message|
178
+ options['pr_message'] = message
179
+ end
180
+
181
+ opts.on("-a ASSIGNMENTS", "--assign=ASSIGNMENTS", "A comma-separated list (no spaces around the comma) of GitHub handles to assign to this pull request.") do |assignees|
182
+ options['assignees'] = assignees
183
+ end
184
+
185
+ opts.on("-g GROUP1,GROUP2,GROUPX", Array, "Groups to update") do |groups|
186
+ options['groups'] = groups
187
+ end
188
+ end
189
+ }
190
+
191
+ global.order!
192
+ command = ARGV.shift
193
+
194
+ if command.nil?
195
+ command = 'sync'
196
+ end
197
+
198
+ subcommands[command].order!
199
+
200
+ begin
201
+ dispatch = Cimas::Cli::Command.new(options)
202
+ dispatch.send(command.gsub('-', '_'))
203
+ exit 0
204
+ rescue => e
205
+ puts "Error: #{e.message}"
206
+ puts e.backtrace
207
+ exit 1
208
+ end
@@ -0,0 +1,5 @@
1
+ require "cimas/cli/command"
2
+ require "cimas/version"
3
+
4
+ module Cimas
5
+ end
@@ -0,0 +1,500 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'net/http'
4
+ require 'git'
5
+ # require 'travis/client/session'
6
+
7
+ module Cimas
8
+ module Cli
9
+ class Command
10
+ attr_accessor :github_client, :config
11
+
12
+ DEFAULT_CONFIG = {
13
+ 'dry_run' => false,
14
+ 'verbose' => false,
15
+ 'groups' => ['all'],
16
+ 'pull_branch' => 'master',
17
+ 'force_push' => false,
18
+ 'assignees' => [],
19
+ 'reviewers' => []
20
+ }
21
+
22
+ def initialize(options)
23
+ unless options['config_file_path'].exist?
24
+ raise "[ERROR] config_file_path #{options['config_file_path']} does not exist, aborting."
25
+ end
26
+
27
+ @data = YAML.load(IO.read(options['config_file_path']))
28
+
29
+ @config = DEFAULT_CONFIG.merge(settings).merge(options)
30
+
31
+ unless repos_path.exist?
32
+ FileUtils.mkdir_p repos_path
33
+ end
34
+
35
+ if ENV["GITHUB_TOKEN"]
36
+ @config['github_token'] ||= ENV["GITHUB_TOKEN"]
37
+ end
38
+ end
39
+
40
+ def settings
41
+ data['settings']
42
+ end
43
+
44
+ def github_client
45
+ require 'octokit'
46
+ if config['github_token'].nil?
47
+ raise "[ERROR] Please set GITHUB_TOKEN environment variable to use GitHub functions."
48
+ end
49
+ @github_client ||= Octokit::Client.new(access_token: config['github_token'])
50
+ end
51
+
52
+ def config
53
+ @config
54
+ end
55
+
56
+ def data
57
+ @data
58
+ end
59
+
60
+ def setup
61
+ repositories.each_pair do |repo_name, attribs|
62
+ repo_dir = File.join(repos_path, repo_name)
63
+ # puts "attribs #{attribs.inspect}"
64
+ unless File.exist?(repo_dir) && File.exist?(File.join(repo_dir, '.git'))
65
+ puts "Git cloning #{repo_name} from #{attribs['remote']}..."
66
+ Git.clone(attribs['remote'], repo_name, path: repos_path)
67
+ else
68
+ puts "Skip cloning #{repo_name}, already exists."
69
+ end
70
+ end
71
+ end
72
+
73
+ def sanity_check
74
+ unsynced = []
75
+
76
+ repositories.each_pair do |repo_name, attribs|
77
+ repo_dir = File.join(repos_path, repo_name)
78
+ unless File.exist?(repo_dir) && File.exist?(File.join(repo_dir, '.git'))
79
+ unsynced << repo_name
80
+ end
81
+ end
82
+
83
+ unsynced.uniq!
84
+
85
+ return true if unsynced.empty?
86
+
87
+ raise "[ERROR] These repositories have not been setup, please run `setup` first: #{unsynced.inspect}"
88
+ end
89
+
90
+ def config_master_path
91
+ config['config_master_path']
92
+ end
93
+
94
+ def repos_path
95
+ config['repos_path']
96
+ end
97
+
98
+ def repositories
99
+ data['repositories']
100
+ end
101
+
102
+ def sync
103
+ sanity_check
104
+ unless config['config_master_path'].exist?
105
+ raise "[ERROR] config_master_path not set, aborting."
106
+ end
107
+
108
+ filtered_repo_names.each do |repo_name|
109
+
110
+ repo = repo_by_name(repo_name)
111
+ # puts "repo_name #{repo_name} #{repo.inspect}"
112
+
113
+ branch = repo['branch']
114
+ files = repo['files']
115
+
116
+ repo_dir = File.join(repos_path, repo_name)
117
+ unless File.exist?(repo_dir)
118
+ puts "[ERROR] #{repo_name} is missing in #{repos_path}, skipping sync for it."
119
+ next
120
+ end
121
+
122
+ dry_run("Copying files to #{repo_name} and staging them") do
123
+ g = Git.open(repo_dir)
124
+ g.checkout(branch)
125
+ g.reset_hard(branch)
126
+ g.clean(force: true)
127
+
128
+ puts "Syncing and staging files in #{repo_name}..."
129
+
130
+ files.each do |target, source|
131
+ # puts "file #{source} => #{target}"
132
+ source_path = File.join(config_master_path, source)
133
+ target_path = File.join(repos_path, repo_name, target)
134
+ # puts "file #{source_path} => #{target_path}"
135
+
136
+ copy_file(source_path, target_path)
137
+ g.add(target_path)
138
+ end
139
+
140
+ # Debugging to see if files have been changed
141
+ # g.status.changed.each do |file, status|
142
+ # puts "Updated files in #{repo_name}:"
143
+ # puts status.blob(:index).contents
144
+ # end
145
+ end
146
+ end
147
+ end
148
+
149
+ def diff
150
+ sanity_check
151
+ unless config['config_master_path'].exist?
152
+ raise "[ERROR] config_master_path not set, aborting."
153
+ end
154
+
155
+ filtered_repo_names.each do |repo_name|
156
+
157
+ repo = repo_by_name(repo_name)
158
+ # puts "repo_name #{repo_name} #{repo.inspect}"
159
+
160
+ branch = repo['branch']
161
+ files = repo['files']
162
+
163
+ repo_dir = File.join(repos_path, repo_name)
164
+ unless File.exist?(repo_dir)
165
+ puts "[ERROR] #{repo_name} is missing in #{repos_path}, skipping diff for it."
166
+ next
167
+ end
168
+
169
+ g = Git.open(repo_dir)
170
+ # g.checkout(branch)
171
+ # g.reset_hard(branch)
172
+ # g.clean(force: true)
173
+
174
+ # puts "Syncing files in #{repo_name}..."
175
+ #
176
+ # files.each do |target, source|
177
+ # # puts "file #{source} => #{target}"
178
+ # source_path = File.join(config_master_path, source)
179
+ # target_path = File.join(repos_path, repo_name, target)
180
+ # # puts "file #{source_path} => #{target_path}"
181
+ #
182
+ # copy_file(source_path, target_path)
183
+ # # g.add(target_path)
184
+ # end
185
+
186
+ puts "======================= DIFF FOR #{repo_name} ========================="
187
+ # Debugging to see if files have been changed
188
+ diff = g.diff
189
+ puts diff.patch
190
+
191
+ # g.status.changed.each do |file, status|
192
+ # puts "Updated files in #{repo_name}:"
193
+ # puts status.blob(:index).contents
194
+ # end
195
+ end
196
+ end
197
+
198
+ # def lint(options)
199
+ # config_master_path = options['config_master_path']
200
+ # appveyor_token = options['appveyor_token']
201
+ #
202
+ # config = YAML.load_file(File.join(config_master_path, 'ci.yml'))
203
+ #
204
+ # validated = []
205
+ #
206
+ # config['repos'].each do |_, repo_ci|
207
+ # travisci, appveyor = repo_ci.values_at('.travis.yml', 'appveyor.yml')
208
+ #
209
+ # if travisci && !validated.include?(travisci)
210
+ # valid = system("travis lint #{File.join(config_master_path, travisci)}", :out => :close)
211
+ # puts "#{travisci} valid: #{valid}"
212
+ # validated << travisci
213
+ # end
214
+ #
215
+ # if appveyor && !validated.include?(appveyor)
216
+ # uri = URI('https://ci.appveyor.com/api/projects/validate-yaml')
217
+ # http = Net::HTTP.new(uri.host, uri.port)
218
+ # http.use_ssl = true
219
+ #
220
+ # req = Net::HTTP::Post.new(uri.path, {
221
+ # "Content-Type" => "application/json",
222
+ # "Authorization" => "Bearer #{appveyor_token}"
223
+ # })
224
+ # req.body = File.read(File.join(config_master_path, appveyor))
225
+ #
226
+ # valid = http.request(req).kind_of? Net::HTTPSuccess
227
+ #
228
+ # puts "#{appveyor} valid: #{valid}"
229
+ # validated << appveyor
230
+ # end
231
+ # end
232
+ # end
233
+
234
+ def filtered_repo_names
235
+ return repositories unless config['groups']
236
+
237
+ # puts "config['groups'] #{config['groups'].inspect}"
238
+ config['groups'].inject([]) do |acc, group|
239
+ acc + group_repo_names(group)
240
+ end.uniq
241
+ end
242
+
243
+ def repo_by_name(name)
244
+ # puts "getting repository for #{name}"
245
+ data['repositories'][name]
246
+ end
247
+
248
+ def pull
249
+ sanity_check
250
+ filtered_repo_names.each do |repo_name|
251
+
252
+ repo = repo_by_name(repo_name)
253
+ branch = repo['branch']
254
+ files = repo['files']
255
+
256
+ repo_dir = File.join(repos_path, repo_name)
257
+ unless File.exist?(repo_dir)
258
+ puts "[ERROR] #{repo_name} is missing in #{repos_path}, skipping pull for it."
259
+ next
260
+ end
261
+
262
+ g = Git.open(repo_dir)
263
+
264
+ dry_run("Pulling from #{repo_name}...") do
265
+ puts "Pulling from #{repo_name}..."
266
+ g.reset_hard(branch)
267
+ g.checkout(branch)
268
+ g.pull
269
+ # g.fetch(g.remotes.first)
270
+ end
271
+ end
272
+
273
+ puts "Done!"
274
+ end
275
+
276
+ def commit_message
277
+ if config['commit_message'].nil?
278
+ raise OptionParser::MissingArgument, "Missing -m/--message value"
279
+ end
280
+ config['commit_message']
281
+ end
282
+
283
+ def pr_message
284
+ if config['pr_message'].nil?
285
+ raise OptionParser::MissingArgument, "Missing -m/--message value"
286
+ end
287
+ config['pr_message']
288
+ end
289
+
290
+ def push_to_branch
291
+ if config['push_to_branch'].nil?
292
+ raise OptionParser::MissingArgument, "Missing -b/--push-branch value"
293
+ end
294
+ config['push_to_branch']
295
+ end
296
+
297
+ def merge_branch
298
+ if config['merge_branch'].nil?
299
+ raise OptionParser::MissingArgument, "Missing -b/--merge-branch value"
300
+ end
301
+ config['merge_branch']
302
+ end
303
+
304
+
305
+ def force_push
306
+ config['force_push']
307
+ end
308
+
309
+ def push
310
+ sanity_check
311
+
312
+ filtered_repo_names.each do |repo_name|
313
+ repo = repo_by_name(repo_name)
314
+
315
+ repo_dir = File.join(repos_path, repo_name)
316
+ unless File.exist?(repo_dir)
317
+ puts "[ERROR] #{repo_name} is missing in #{repos_path}, skipping push for it."
318
+ next
319
+ end
320
+
321
+ g = Git.open(repo_dir)
322
+ # g.reset_hard(attribs['branch'])
323
+
324
+ dry_run("Pushing branch #{push_to_branch} (commit #{g.object('HEAD').sha}) to #{g.remotes.first}:#{repo_name}") do
325
+ g.branch(push_to_branch).checkout
326
+ g.add(all: true)
327
+
328
+ if g.status.added.empty?
329
+ puts "Skipping commit on #{repo_name}, no changes detected."
330
+ else
331
+ g.commit_all(commit_message)
332
+ end
333
+
334
+ if force_push
335
+ # TODO implement
336
+ raise "[ERROR] Force pushing with commit amend is not yet implemented."
337
+ else
338
+ puts "Pushing branch #{push_to_branch} (commit #{g.object('HEAD').sha}) to #{g.remotes.first}:#{repo_name}"
339
+ g.push(g.remotes.first, push_to_branch)
340
+ end
341
+ end
342
+ end
343
+
344
+ # do two separate `git add` because one of it may be missing
345
+ # run_cmd("git -C #{repos_path} multi -c add .travis.yml", dry_run)
346
+ # run_cmd("git -C #{repos_path} multi -c add appveyor.yml")
347
+ end
348
+
349
+ def git_remote_to_github_name(remote)
350
+ remote.match(/github.com\/(.*)/)[1]
351
+ end
352
+
353
+ def open_prs
354
+ sanity_check
355
+ branch = merge_branch
356
+ message = pr_message
357
+ assignees = config['assignees']
358
+ reviewers = config['reviewers']
359
+
360
+ filtered_repo_names.each do |repo_name|
361
+ repo = repo_by_name(repo_name)
362
+
363
+ repo_dir = File.join(repos_path, repo_name)
364
+ unless File.exist?(repo_dir)
365
+ puts "[ERROR] #{repo_name} is missing in #{repos_path}, skipping sync_and_commit for it."
366
+ next
367
+ end
368
+
369
+ g = Git.open(repo_dir)
370
+ github_slug = git_remote_to_github_name(repo['remote'])
371
+
372
+ dry_run("Opening GitHub PR: #{github_slug}, branch #{repo['branch']} <- #{branch}, message '#{message}'") do
373
+ puts "Opening GitHub PR: #{github_slug}, branch #{repo['branch']} <- #{branch}, message '#{message}'"
374
+
375
+ begin
376
+ pr = github_client.create_pull_request(
377
+ github_slug,
378
+ repo['branch'],
379
+ branch,
380
+ message,
381
+ "As title. \n\n _Generated by Cimas_."
382
+ )
383
+ number = pr['number']
384
+ puts "PR #{github_slug}\##{number} created"
385
+
386
+ rescue Octokit::Error => e
387
+ # puts e.inspect
388
+ # puts '------'
389
+ # puts "e.message #{e.message}"
390
+
391
+ case e.message
392
+ when /A pull request already exists/
393
+ puts "[WARNING] PR already exists for #{branch}."
394
+
395
+ when /field: head\s+code: invalid/
396
+ puts "[WARNING] Branch #{branch} does not exist on #{github_slug}. Did you run `push`? Skipping."
397
+ next
398
+ else
399
+ raise e
400
+ end
401
+ end
402
+
403
+ # puts pr.inspect
404
+
405
+ unless pr
406
+ puts "[WARNING] Detecting PR from GitHub..."
407
+ github_branch_owner = github_slug.match(/(.*)\/.*/)[1]
408
+ prs = github_client.pull_requests(github_slug, head: "#{github_branch_owner}:#{branch}")
409
+ pr = prs.first
410
+ puts "[WARNING] Detected PR to be #{github_slug}\##{pr['number']}, continue processing."
411
+ end
412
+
413
+ # TODO: Catch
414
+
415
+ number = pr['number']
416
+
417
+ unless reviewers.empty?
418
+ puts "Requesting #{github_slug}\##{number} review from: [#{reviewers.join(',')}]"
419
+ begin
420
+ github_client.request_pull_request_review(
421
+ github_slug,
422
+ number,
423
+ reviewers: reviewers
424
+ )
425
+
426
+ rescue Octokit::Error => e
427
+ # puts e.inspect
428
+ # puts '------'
429
+ # puts "e.message #{e.message}"
430
+
431
+ # TODO: When command is first run, should exclude the PR author from 'reviewers'
432
+ case e.message
433
+ when /Review cannot be requested from pull request author./
434
+ puts "[WARNING] #{e.message}, skipping."
435
+ next
436
+ else
437
+ raise e
438
+ end
439
+
440
+ end
441
+ end
442
+
443
+ unless assignees.empty?
444
+ puts "Assigning #{github_slug}\##{number} to: [#{assignees.join(',')}]"
445
+ github_client.add_assignees(
446
+ github_slug,
447
+ number,
448
+ assignees
449
+ )
450
+ end
451
+
452
+ end
453
+ end
454
+ end
455
+
456
+ private
457
+
458
+ def copy_file(from, to)
459
+ dry_run("copying file #{from} -> #{to}") do
460
+ to_dir = File.dirname(to)
461
+ unless File.directory?(to_dir)
462
+ FileUtils.mkdir_p(to_dir)
463
+ end
464
+
465
+ File.open(to, 'w+') do |fo|
466
+ fo.puts '# Auto-generated by Cimas: Do not edit it manually!'
467
+ fo.puts '# See https://github.com/metanorma/cimas'
468
+ File.foreach(from) do |li|
469
+ fo.puts li
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ def dry_run(description, &block)
476
+ if config['dry_run']
477
+ puts "dry run: #{description}"
478
+ else
479
+ yield
480
+ end
481
+ end
482
+
483
+ def groups
484
+ data['groups']
485
+ end
486
+
487
+ def group_repo_names(group)
488
+ # puts "group #{group}"
489
+ case group
490
+ when 'all'
491
+ repositories.keys
492
+ else
493
+ # puts "groups #{groups.inspect}"
494
+ groups[group]
495
+ end
496
+ end
497
+
498
+ end
499
+ end
500
+ end