wagemage 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,191 @@
1
+ module Wagemage
2
+ class CLI
3
+ include Wagemage::Helpers
4
+
5
+ def initialize(args)
6
+ @options = Slop.parse(args) do |option|
7
+ option.bool '-h', '--help', 'print this help'
8
+ option.on '-v', '--version', 'print the version'
9
+
10
+ option.string '-o', '--org', 'github org'
11
+ option.string '-r', '--repo', 'regex against which to match repo names'
12
+ option.string '-b', '--branch', 'regex against which to match branches'
13
+
14
+ option.path '-s', '--script', "the script to run on each repo's branch"
15
+
16
+ option.bool '--first-branch', 'operate only on the "oldest" branch'
17
+ option.array '--reviewers', 'array of github users to put on the PR'
18
+ option.string(
19
+ '--branch-prefix',
20
+ 'prefix of the new branch',
21
+ default: 'wagemage'
22
+ )
23
+
24
+ option.bool '--debug', "don't push or issue PR, keep the tmp directory"
25
+ end
26
+
27
+ validate_options!
28
+
29
+ token = ENV['WAGEMAGE_GITHUB_TOKEN'] || request_token
30
+ @okclient = Octokit::Client.new(access_token: token)
31
+
32
+ @tmpdir = Dir.mktmpdir
33
+ @script_path = @options[:script].expand_path.to_s
34
+ end
35
+
36
+ def run
37
+ raise Error, 'No repos found' if repos.empty?
38
+
39
+ say "#{repos.size} repo(s) found:", space: true
40
+ repos.each { |r| say "* #{r.name}", color: :green }
41
+
42
+ say 'Would you like to clone these repos? (Y/n)', space: true
43
+ abort if ask.casecmp?('n')
44
+
45
+ begin
46
+ clone_repos
47
+ display_branch_list
48
+
49
+ say <<~MESSAGE, space: true, color: :yellow
50
+ You're about to run this script against the aforementioned list:
51
+ => #{@script_path}
52
+ MESSAGE
53
+
54
+ if @options[:debug]
55
+ say "(--debug flag enabled. No code will be pushed.)", color: :green
56
+ else
57
+ say "(--debug flag NOT enabled. Code may be pushed.)", color: :red
58
+ end
59
+
60
+ say "Would you like to execute this script? (Y/n)"
61
+ abort if ask.casecmp?('n')
62
+
63
+ repos.each do |repo|
64
+ repo.branches.each_with_index do |branch, index|
65
+ if index > 0
66
+ say "=> Skipping #{repo.name}:#{branch}", color: :yellow
67
+ next
68
+ end
69
+
70
+ new_branch = [
71
+ @options[:branch_prefix],
72
+ branch,
73
+ Time.now.to_i
74
+ ].join('/')
75
+
76
+ repo.checkout! branch
77
+ repo.checkout! new_branch, create: true
78
+
79
+ say "=> Running script on #{repo.name}:#{new_branch}"
80
+ script_result = command(
81
+ [@script_path, repo.clone_dir, repo.name, branch].join(' ')
82
+ )
83
+
84
+ unless script_result[:stderr].empty?
85
+ say script_result[:stderr], color: :red
86
+ end
87
+
88
+ if script_result[:status].success? && !repo.has_changed?
89
+ say 'SCRIPT SUCCEEDED BUT NO CHANGES TO COMMIT!', color: :yellow
90
+ next
91
+ elsif !script_result[:status].success?
92
+ say 'SCRIPT FAILED!', color: :red
93
+ next
94
+ end
95
+
96
+ say 'SCRIPT SUCCEEDED! COMMITTING CHANGES!', color: :green
97
+
98
+ repo.add_all!
99
+ repo.commit! script_result[:stdout]
100
+
101
+ if @options[:debug]
102
+ say 'DEBUG ENABLED! SKIPPING PUSH & PULL REQUEST!', color: :yellow
103
+ next
104
+ end
105
+
106
+ repo.push!
107
+
108
+ pr_result = repo.pull_request!(branch, @options[:reviewers])
109
+
110
+ if pr_result[:status].success?
111
+ say "=> PULL REQUEST URL: #{pr_result[:stdout]}", color: :green
112
+ else
113
+ say "=> PULL REQUEST FAILED!", color: :red
114
+ end
115
+ end
116
+ end
117
+ ensure
118
+ if @options[:debug]
119
+ say <<~MESSAGE, space: :true, color: :yellow
120
+ The temporary directory has been retained because you have specified
121
+ the --debug flag. You can view it here:
122
+ => #{@tmpdir}
123
+ MESSAGE
124
+ else
125
+ FileUtils.remove_entry(@tmpdir)
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def repos
133
+ @repos ||= begin
134
+ repos = @options[:org].nil? ?
135
+ @okclient.repos :
136
+ @okclient.org_repos(@options[:org])
137
+
138
+ last_response = @okclient.last_response
139
+ while last_response.rels[:next] do
140
+ repos.concat(last_response.rels[:next].get.data)
141
+ last_response = last_response.rels[:next].get
142
+ end
143
+
144
+ repos.map! { |r| Wagemage::Repo.new(r, @tmpdir, @options[:branch]) }
145
+
146
+ return repos if @options[:repo].nil?
147
+
148
+ repo_name_regex = Regexp.new(@options[:repo])
149
+ repos.select { |r| r.name =~ repo_name_regex }
150
+ end
151
+ end
152
+
153
+ def clone_repos
154
+ repos.each do |repo|
155
+ say "=> Cloning #{repo.name} to #{repo.clone_dir}", color: :light_blue
156
+ repo.clone!
157
+ end
158
+ end
159
+
160
+ def display_branch_list
161
+ repos.each do |repo|
162
+ say "* #{repo.name}:", space: true
163
+
164
+ if repo.branches.empty?
165
+ say ' - NONE FOUND', color: :red
166
+ next
167
+ end
168
+
169
+ if @options[:first_branch]
170
+ say " - #{repo.branches.first}", color: :green
171
+ repo.branches[1..-1].each { |b| say " - #{b}" }
172
+ else
173
+ repo.branches.each { |b| say " - #{b}", color: :green }
174
+ end
175
+ end
176
+ end
177
+
178
+ def request_token
179
+ say 'Github Personal Access Token missing', color: :red
180
+ say 'Please supply it now:'
181
+ ask
182
+ end
183
+
184
+ def validate_options!
185
+ abort(@options.to_s) if @options.help?
186
+ abort("Wagemage v#{Wagemage::VERSION}") if @options.version?
187
+
188
+ raise OptionError if @options[:script].nil?
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,30 @@
1
+ module Wagemage
2
+ module Helpers
3
+ def say(message, space: false, color: :white)
4
+ puts if space
5
+ puts message.colorize(color.to_sym)
6
+ end
7
+
8
+ def ask
9
+ STDIN.gets.chomp
10
+ end
11
+
12
+ def warning(message)
13
+ say(message, color: :red)
14
+ end
15
+
16
+ def command(cmd, chdir: Dir.pwd, error: false)
17
+ stdout, stderr, status = Open3.capture3(cmd, chdir: chdir)
18
+
19
+ unless status.success?
20
+ error ? (raise Error, stderr) : warning(stderr)
21
+ end
22
+
23
+ {
24
+ stdout: stdout,
25
+ stderr: stderr,
26
+ status: status
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,78 @@
1
+ module Wagemage
2
+ class Repo
3
+ attr_reader :clone_dir
4
+
5
+ def initialize(info, dir, branch_pattern)
6
+ @info = info
7
+ @clone_dir = [dir, info[:full_name]].join('/')
8
+ @branch_pattern = branch_pattern
9
+ end
10
+
11
+ def name
12
+ @info[:full_name]
13
+ end
14
+
15
+ def url
16
+ @info[:ssh_url]
17
+ end
18
+
19
+ def clone!
20
+ Wagemage.command("git clone #{url} #{clone_dir}", error: true)
21
+ end
22
+
23
+ def branches
24
+ @branches ||= begin
25
+ result = Wagemage.command('git branch -a', chdir: @clone_dir)
26
+
27
+ return [] unless result[:status].success?
28
+
29
+ branch_list =
30
+ result[:stdout]
31
+ .split("\n")
32
+ .select { |b| b.include?('remotes/origin/') }
33
+ .reject { |b| b.include?('->') }
34
+ .map { |b| b.split('/')[2..-1].join('/') }
35
+
36
+ if branch_list.include?('master')
37
+ branch_list
38
+ .reject! { |b| b == 'master' }
39
+ .push('master')
40
+ end
41
+
42
+ return branch_list if @branch_pattern.nil?
43
+
44
+ branch_name_regex = Regexp.new(@branch_pattern)
45
+ branch_list.select { |b| b =~ branch_name_regex }
46
+ end
47
+ end
48
+
49
+ def checkout!(ref, create: false)
50
+ cmd = create ? "git checkout -b #{ref}" : "git checkout #{ref}"
51
+ Wagemage.command(cmd, chdir: @clone_dir)
52
+ end
53
+
54
+ def add_all!
55
+ Wagemage.command('git add .', chdir: @clone_dir)
56
+ end
57
+
58
+ def commit!(message)
59
+ Wagemage.command(%Q[git commit -m "#{message}"], chdir: @clone_dir)
60
+ end
61
+
62
+ def push!
63
+ Wagemage.command('git push origin HEAD', chdir: @clone_dir)
64
+ end
65
+
66
+ def pull_request!(base_branch, reviewers = [])
67
+ cmd = "hub pull-request --no-edit -b #{base_branch}"
68
+ cmd = [cmd, '-r', reviewers.join(',')].join(' ') unless reviewers.empty?
69
+
70
+ Wagemage.command(cmd, chdir: @clone_dir)
71
+ end
72
+
73
+ def has_changed?
74
+ result = Wagemage.command('git status -s', chdir: @clone_dir)
75
+ !result[:stdout].empty?
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module Wagemage
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "wagemage/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "wagemage"
7
+ spec.version = Wagemage::VERSION
8
+ spec.authors = ["Curt Howard"]
9
+ spec.email = ["curt@portugly.com"]
10
+
11
+ spec.summary = "A CLI for making changes to many Github repos"
12
+ spec.description = "A CLI for making changes to many Github repos"
13
+ spec.homepage = "https://github.com/meowsus/wagemage"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/meowsus/wagemage"
18
+
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`
21
+ .split("\x0")
22
+ .reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_development_dependency "bundler", "~> 2.0"
30
+ spec.add_development_dependency "rake", "~> 12.3.3"
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+
33
+ spec.add_dependency "slop", "~> 4.7"
34
+ spec.add_dependency "colorize", "~> 0.8"
35
+ spec.add_dependency "octokit", "~> 4.14"
36
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wagemage
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Curt Howard
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: slop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.7'
69
+ - !ruby/object:Gem::Dependency
70
+ name: colorize
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.8'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: octokit
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '4.14'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '4.14'
97
+ description: A CLI for making changes to many Github repos
98
+ email:
99
+ - curt@portugly.com
100
+ executables:
101
+ - wagemage
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".github/workflows/tests.yml"
106
+ - ".gitignore"
107
+ - ".rubocop.yml"
108
+ - ".travis.yml"
109
+ - CODE_OF_CONDUCT.md
110
+ - Gemfile
111
+ - Gemfile.lock
112
+ - LICENSE.txt
113
+ - README.md
114
+ - Rakefile
115
+ - bin/console
116
+ - bin/rake
117
+ - bin/setup
118
+ - examples/add_test_file
119
+ - examples/consolidate_pushes_during_release
120
+ - examples/enable_teaspoon_tests
121
+ - examples/find_decorated_test
122
+ - examples/find_teaspoon_tests
123
+ - examples/ignore_remote_rubocop_config
124
+ - examples/nextyear
125
+ - exe/wagemage
126
+ - lib/wagemage.rb
127
+ - lib/wagemage/cli.rb
128
+ - lib/wagemage/helpers.rb
129
+ - lib/wagemage/repo.rb
130
+ - lib/wagemage/version.rb
131
+ - wagemage.gemspec
132
+ homepage: https://github.com/meowsus/wagemage
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ homepage_uri: https://github.com/meowsus/wagemage
137
+ source_code_uri: https://github.com/meowsus/wagemage
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubygems_version: 3.1.2
154
+ signing_key:
155
+ specification_version: 4
156
+ summary: A CLI for making changes to many Github repos
157
+ test_files: []