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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +86 -0
- data/README.adoc +467 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cimas.gemspec +44 -0
- data/exe/cimas +208 -0
- data/lib/cimas.rb +5 -0
- data/lib/cimas/cli/command.rb +500 -0
- data/lib/cimas/version.rb +3 -0
- metadata +144 -0
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -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__)
|
data/bin/setup
ADDED
data/cimas.gemspec
ADDED
|
@@ -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
|
data/exe/cimas
ADDED
|
@@ -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
|
data/lib/cimas.rb
ADDED
|
@@ -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
|