cimas 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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