abtion-aid 0.2.0 → 0.3.1
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 +4 -4
- data/README.md +1 -1
- data/aid.gemspec +5 -1
- data/lib/aid/scripts/begin.rb +310 -0
- data/lib/aid/scripts/finish.rb +133 -0
- data/lib/aid/scripts/review.rb +346 -0
- data/lib/aid/scripts/shared/git_config.rb +39 -0
- data/lib/aid/scripts/shared/git_staged.rb +10 -0
- data/lib/aid/scripts/shared/team.rb +60 -0
- data/lib/aid/version.rb +1 -1
- metadata +52 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a777e8c76409c4bb71e1432027481e6887f6404b538ed81f324af12bcddd799
|
4
|
+
data.tar.gz: 16f94988582a60e12c00fed82c7b62f8a159bb4b4be61e8c4a6f5c8052ce222a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 174b5404a30f59b4b789f76b7095dffc304251c84f38374a8eea6e2d21be21b4b48a3419364fdf27568e0ab86a39d01f46ab2ce5b3b2ab8be3bb348e8bfb95d8
|
7
|
+
data.tar.gz: f4bdc737181cf232024863952dfb11255dbee8df42eadd2b9fb20db445125551799f2456a15fbe2dd4e299110d39437ef6b7ab2ca0318cbe621435adc9435606
|
data/README.md
CHANGED
@@ -34,7 +34,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
34
34
|
|
35
35
|
## Contributing
|
36
36
|
|
37
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
37
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/abtion/aid.
|
38
38
|
|
39
39
|
## License
|
40
40
|
|
data/aid.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.authors = ['Abtion', 'David Balatero']
|
11
11
|
spec.email = ['mail@abtion.com']
|
12
12
|
|
13
|
-
spec.summary = '
|
13
|
+
spec.summary = 'Abtion\'s development workflow tools'
|
14
14
|
spec.homepage = 'https://github.com/abtion/aid'
|
15
15
|
spec.license = 'MIT'
|
16
16
|
|
@@ -27,4 +27,8 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
28
28
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
29
|
spec.add_development_dependency 'rubocop'
|
30
|
+
|
31
|
+
spec.add_dependency 'http'
|
32
|
+
spec.add_dependency 'slack-notifier'
|
33
|
+
spec.add_dependency 'asana'
|
30
34
|
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
require "asana"
|
2
|
+
require "tempfile"
|
3
|
+
require_relative "./shared/git_config"
|
4
|
+
require_relative "./shared/git_staged"
|
5
|
+
|
6
|
+
ASANA_URL_PATTERN = %r{https?://app.asana.com/\d+/(?<project_id>\d+)/(?<task_id>\d+)}.freeze
|
7
|
+
|
8
|
+
|
9
|
+
module Aid
|
10
|
+
module Scripts
|
11
|
+
class Begin < Aid::Script
|
12
|
+
include Aid::GitConfig
|
13
|
+
include Aid::GitStaged
|
14
|
+
|
15
|
+
def self.description
|
16
|
+
"Starts a new Asana story"
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.help
|
20
|
+
<<~HELP
|
21
|
+
Fill me in.
|
22
|
+
aid begin:
|
23
|
+
Starts a story on Asana, moves it into the Work In Progress column,
|
24
|
+
and opens a pull request on GitHub for you.
|
25
|
+
Usage:
|
26
|
+
aid begin "https://app.asana.com/0/1135298954694367/1135903276459289/f"
|
27
|
+
HELP
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
check_for_hub!
|
32
|
+
check_for_hub_credentials!
|
33
|
+
|
34
|
+
prompt_for_config!(
|
35
|
+
"asana.personal-access-token",
|
36
|
+
"Enter your personal access token",
|
37
|
+
<<~EOF
|
38
|
+
1. Visit https://app.asana.com/0/developer-console
|
39
|
+
2. Click "+ New access token" under "Personal access tokens"
|
40
|
+
3. Give the token a name
|
41
|
+
4. Copy the token and paste it back here.
|
42
|
+
EOF
|
43
|
+
)
|
44
|
+
|
45
|
+
prompt_for_config!(
|
46
|
+
"github.user",
|
47
|
+
"Enter your GitHub username",
|
48
|
+
<<~EOF
|
49
|
+
1. Visit https://github.com and sign in
|
50
|
+
2. Find the GitHub username you use and paste it in here
|
51
|
+
EOF
|
52
|
+
)
|
53
|
+
|
54
|
+
set_asana_project_id_if_needed!
|
55
|
+
check_for_existing_branch!
|
56
|
+
ensure_base_branch_is_ok!
|
57
|
+
|
58
|
+
step "Starting '#{asana_task.name}' on Asana" do
|
59
|
+
start_task
|
60
|
+
end
|
61
|
+
|
62
|
+
step "Creating local branch off '#{base_branch}'" do
|
63
|
+
check_for_staged_files!
|
64
|
+
reset_hard_to_base_branch!
|
65
|
+
create_git_branch!
|
66
|
+
make_empty_commit!
|
67
|
+
end
|
68
|
+
|
69
|
+
step "Pushing to Github" do
|
70
|
+
push_branch_to_github!
|
71
|
+
end
|
72
|
+
|
73
|
+
step "Opening pull request on Github" do
|
74
|
+
open_pull_request_on_github!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def open_pull_request_on_github!
|
81
|
+
tempfile = Tempfile.new("begin_pull_request")
|
82
|
+
|
83
|
+
begin
|
84
|
+
tempfile.write(pull_request_description)
|
85
|
+
tempfile.close
|
86
|
+
|
87
|
+
labels = "WIP"
|
88
|
+
|
89
|
+
url = `hub pr list -h #{current_branch_name} -f '%U'`.strip
|
90
|
+
if url.empty?
|
91
|
+
url = `hub pull-request -F #{tempfile.path} -l "#{labels}" -d -a #{github_user}`.strip
|
92
|
+
end
|
93
|
+
puts colorize(:blue, url)
|
94
|
+
|
95
|
+
# Copy it to your clipboard
|
96
|
+
pbcopy_exist = system("command -v pbcopy >/dev/null 2>&1")
|
97
|
+
system("echo #{url.inspect} | pbcopy") if pbcopy_exist
|
98
|
+
|
99
|
+
# Update metadata in .git/config
|
100
|
+
result = url.match(%r{pull/(\d+)})
|
101
|
+
pull_request_id = result && result[1]
|
102
|
+
|
103
|
+
git_config("tasks.#{asana_task_id}.url", asana_url)
|
104
|
+
git_config("tasks.#{asana_task_id}.name", asana_task.name)
|
105
|
+
git_config("tasks.#{asana_task_id}.pull-request-id", pull_request_id)
|
106
|
+
ensure
|
107
|
+
tempfile.unlink
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def github_user
|
112
|
+
git_config("github.user")
|
113
|
+
end
|
114
|
+
|
115
|
+
def push_branch_to_github!
|
116
|
+
silent "git push --force-with-lease -u origin '#{current_branch_name}'"
|
117
|
+
end
|
118
|
+
|
119
|
+
def make_empty_commit!
|
120
|
+
msg = "Initial commit for task ##{asana_task_id} [ci skip]"
|
121
|
+
|
122
|
+
silent("git commit --allow-empty -m #{msg.inspect}")
|
123
|
+
end
|
124
|
+
|
125
|
+
def current_branch_name
|
126
|
+
`git rev-parse --abbrev-ref HEAD`.strip
|
127
|
+
end
|
128
|
+
|
129
|
+
def ensure_base_branch_is_ok!
|
130
|
+
base_branch
|
131
|
+
end
|
132
|
+
|
133
|
+
def base_branch
|
134
|
+
@base_branch ||= prompt_for_base_branch
|
135
|
+
end
|
136
|
+
|
137
|
+
def prompt_for_base_branch
|
138
|
+
current_branch = current_branch_name
|
139
|
+
|
140
|
+
if current_branch != "master"
|
141
|
+
print colorize(
|
142
|
+
:yellow,
|
143
|
+
"Current branch is #{colorize(:blue, current_branch)} - do you want "\
|
144
|
+
"to create a branch off that? (y/n) > "
|
145
|
+
)
|
146
|
+
|
147
|
+
answer = STDIN.gets.strip
|
148
|
+
|
149
|
+
exit if answer[0] != "y"
|
150
|
+
end
|
151
|
+
|
152
|
+
current_branch
|
153
|
+
end
|
154
|
+
|
155
|
+
def story_branch_name
|
156
|
+
name = asana_task
|
157
|
+
.name
|
158
|
+
.gsub(%r{[/.]}, "-")
|
159
|
+
.gsub(/\s+/, "-")
|
160
|
+
.gsub(/[^\w-]/, "")
|
161
|
+
.downcase
|
162
|
+
.slice(0, 50)
|
163
|
+
|
164
|
+
"#{name}-#{asana_task_id}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def start_task
|
168
|
+
# Assign task to runner of this script
|
169
|
+
user = asana.users.me
|
170
|
+
asana_task.update(assignee: user.gid)
|
171
|
+
|
172
|
+
# Move it to the WIP column
|
173
|
+
asana_task.add_project(
|
174
|
+
project: asana_project.gid,
|
175
|
+
section: asana_wip_section.gid
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
def asana_project
|
180
|
+
@asana_project ||= asana.projects.find_by_id(asana_project_id)
|
181
|
+
end
|
182
|
+
|
183
|
+
def asana_task
|
184
|
+
@asana_task ||= asana.tasks.find_by_id(asana_task_id)
|
185
|
+
end
|
186
|
+
|
187
|
+
def asana_sections
|
188
|
+
asana.sections.find_by_project(project: asana_project.gid)
|
189
|
+
end
|
190
|
+
|
191
|
+
def asana_wip_section
|
192
|
+
asana_sections.detect do |task|
|
193
|
+
task.name =~ /(Work In Progress|WIP)/i
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def asana_project_id
|
198
|
+
git_config("asana.project-id")&.to_i
|
199
|
+
end
|
200
|
+
|
201
|
+
def set_asana_project_id_if_needed!
|
202
|
+
return if asana_project_id
|
203
|
+
|
204
|
+
result = asana_url
|
205
|
+
.match(ASANA_URL_PATTERN)
|
206
|
+
|
207
|
+
abort help unless result
|
208
|
+
|
209
|
+
id = result[:project_id].to_i
|
210
|
+
|
211
|
+
puts "Setting asana.project-id to #{id}"
|
212
|
+
|
213
|
+
git_config("asana.project-id", id)
|
214
|
+
end
|
215
|
+
|
216
|
+
def asana_task_id
|
217
|
+
@asana_task_id ||=
|
218
|
+
begin
|
219
|
+
result = asana_url
|
220
|
+
.match(ASANA_URL_PATTERN)
|
221
|
+
|
222
|
+
abort help unless result
|
223
|
+
|
224
|
+
result[:task_id].to_i
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def asana_url
|
229
|
+
argv.first.to_s
|
230
|
+
end
|
231
|
+
|
232
|
+
def asana
|
233
|
+
@asana ||=
|
234
|
+
Asana::Client.new do |client|
|
235
|
+
client.default_headers "asana-enable" => "string_ids,new_sections"
|
236
|
+
client.authentication :access_token,
|
237
|
+
git_config("asana.personal-access-token")
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def pull_request_description
|
242
|
+
<<~EOF
|
243
|
+
#{asana_task.name}
|
244
|
+
|
245
|
+
#{asana_url}
|
246
|
+
EOF
|
247
|
+
end
|
248
|
+
|
249
|
+
def reset_hard_to_base_branch!
|
250
|
+
silent "git checkout #{base_branch}",
|
251
|
+
"git fetch origin",
|
252
|
+
"git reset --hard origin/#{base_branch}"
|
253
|
+
end
|
254
|
+
|
255
|
+
def check_for_existing_branch!
|
256
|
+
return unless system "git ls-remote --exit-code --heads origin #{story_branch_name}"
|
257
|
+
|
258
|
+
print colorize(
|
259
|
+
:yellow,
|
260
|
+
"The branch for this task (#{story_branch_name}) already exists on origin, \n"\
|
261
|
+
"if you continue, it will be reset to master. \n"\
|
262
|
+
"Do you want to continue? ? (y/n) > "
|
263
|
+
)
|
264
|
+
|
265
|
+
answer = STDIN.gets.strip.downcase
|
266
|
+
|
267
|
+
exit if answer[0] != "y"
|
268
|
+
end
|
269
|
+
|
270
|
+
def create_git_branch!
|
271
|
+
system! "git checkout -b '#{story_branch_name}'"
|
272
|
+
end
|
273
|
+
|
274
|
+
def check_for_hub_credentials!
|
275
|
+
config_file = "#{ENV['HOME']}/.config/hub"
|
276
|
+
credentials_exist = File.exist?(config_file) &&
|
277
|
+
File.read(config_file).match(/oauth_token/i)
|
278
|
+
|
279
|
+
unless credentials_exist
|
280
|
+
abort <<~EOF
|
281
|
+
Your GitHub credentials are not set. Run this command:
|
282
|
+
|
283
|
+
$ hub pull-request
|
284
|
+
|
285
|
+
and when prompted for login details, enter them. It will give you
|
286
|
+
an error at the end, but you can safely ignore it.
|
287
|
+
EOF
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def command?(name)
|
292
|
+
system("which #{name} 2>&1 >/dev/null")
|
293
|
+
end
|
294
|
+
|
295
|
+
def check_for_hub!
|
296
|
+
unless command?("hub")
|
297
|
+
abort <<~EOF
|
298
|
+
You need to install `hub` before you can use this program.
|
299
|
+
To fix:
|
300
|
+
$ brew install hub
|
301
|
+
EOF
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def silent(*cmds)
|
306
|
+
cmds.each { |cmd| system("#{cmd} >/dev/null") }
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
require_relative './shared/git_config'
|
4
|
+
require_relative './shared/git_staged'
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
module Aid
|
8
|
+
module Scripts
|
9
|
+
class Finish < Aid::Script
|
10
|
+
include Aid::GitConfig
|
11
|
+
include Aid::GitStaged
|
12
|
+
|
13
|
+
def self.description
|
14
|
+
"Commits what is currently staged with a [finishes] tag"
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
within_dir(project_root) do
|
19
|
+
Bundler.with_clean_env do
|
20
|
+
check_for_staged_files!
|
21
|
+
check_for_master!
|
22
|
+
|
23
|
+
clean_up_feature_branch!
|
24
|
+
amend_commit_with_finish_message!
|
25
|
+
check_for_linter_prehook!
|
26
|
+
force_push_to_github!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def check_for_master!
|
34
|
+
return unless `git rev-parse --abbrev-ref HEAD`.chomp == "master"
|
35
|
+
|
36
|
+
abort colorize(:red, "You are on master, can not aid finish")
|
37
|
+
end
|
38
|
+
|
39
|
+
def clean_up_feature_branch!
|
40
|
+
step "Removing any empty commits & pulling in any changes from master" do
|
41
|
+
system! "git fetch origin && git rebase -f origin/master"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def amend_commit_with_finish_message!
|
46
|
+
step "Amending your last commit with finish message" do
|
47
|
+
message = amend_message_with_tag(`git log -1 --pretty="format:%B"`.strip)
|
48
|
+
template_file = create_template_file
|
49
|
+
|
50
|
+
begin
|
51
|
+
template_file.write(message)
|
52
|
+
template_file.close
|
53
|
+
|
54
|
+
system! "git commit --amend -o -F '#{template_file.path}'"
|
55
|
+
ensure
|
56
|
+
template_file.close
|
57
|
+
template_file.unlink
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_for_linter_prehook!
|
63
|
+
step "checking for linter prehook" do
|
64
|
+
pre_commit_file_location = ".git/hooks/pre-commit"
|
65
|
+
pre_commit_exists = File.exist?(pre_commit_file_location)
|
66
|
+
|
67
|
+
linter_path = ".git/hooks/rubocop-linter"
|
68
|
+
if pre_commit_exists
|
69
|
+
if File.readlines(pre_commit_file_location).grep(/rubocop[-]linter/).empty?
|
70
|
+
open(pre_commit_file_location, "a") do |f|
|
71
|
+
f << "bash #{linter_path}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
else
|
75
|
+
add_rubocop_linter linter_path
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_rubocop_linter(linter_path)
|
81
|
+
dirname = File.dirname(linter_path)
|
82
|
+
FileUtils.mkdir_p(dirname) unless File.directory?(dirname)
|
83
|
+
linter_template = <<~RUBOCOP_LINTER
|
84
|
+
#!/bin/bash
|
85
|
+
rubocop -a
|
86
|
+
RUBOCOP_LINTER
|
87
|
+
file = File.open(linter_path, "w+")
|
88
|
+
file.puts linter_template
|
89
|
+
file.close
|
90
|
+
end
|
91
|
+
|
92
|
+
def force_push_to_github!
|
93
|
+
step "Force pushing your branch to origin" do
|
94
|
+
system! "git push --force-with-lease origin #{branch_name.inspect}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def amend_message_with_tag(message)
|
99
|
+
finish_message = asana_finish_message
|
100
|
+
message = message.gsub("\n\n#{finish_message}", "")
|
101
|
+
|
102
|
+
"#{message}\n\n#{finish_message}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def asana_finish_message
|
106
|
+
metadata = [asana_task_name, asana_task_url].compact
|
107
|
+
metadata_lines = metadata.join("\n ")
|
108
|
+
|
109
|
+
"Finishes:\n #{metadata_lines}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def asana_task_name
|
113
|
+
git_config("tasks.#{asana_task_id}.name")
|
114
|
+
end
|
115
|
+
|
116
|
+
def asana_task_url
|
117
|
+
git_config("tasks.#{asana_task_id}.url")
|
118
|
+
end
|
119
|
+
|
120
|
+
def asana_task_id
|
121
|
+
@asana_task_id ||= branch_name&.split("-")&.last
|
122
|
+
end
|
123
|
+
|
124
|
+
def branch_name
|
125
|
+
`git rev-parse --abbrev-ref HEAD`.strip
|
126
|
+
end
|
127
|
+
|
128
|
+
def create_template_file
|
129
|
+
Tempfile.new("git-commit-template")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
require "http"
|
2
|
+
require "json"
|
3
|
+
require "slack-notifier"
|
4
|
+
require_relative "./shared/git_config"
|
5
|
+
require_relative "./shared/team"
|
6
|
+
|
7
|
+
module Aid
|
8
|
+
module Scripts
|
9
|
+
class Review < Aid::Script
|
10
|
+
include Aid::GitConfig
|
11
|
+
|
12
|
+
def self.description
|
13
|
+
"Requests a review from one or more team members"
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.help
|
17
|
+
<<~HELP
|
18
|
+
Usage: $ aid review
|
19
|
+
Type aid review from your feature branch and follow the prompts
|
20
|
+
HELP
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
check_for_minimum_version_of_hub!
|
25
|
+
|
26
|
+
prompt_for_reviewers!
|
27
|
+
run_aid_finish_if_needed!
|
28
|
+
|
29
|
+
change_wip_to_reviewable_label!
|
30
|
+
move_asana_task_to_pull_request_column!
|
31
|
+
|
32
|
+
step "Marking PR ready for review" do
|
33
|
+
pull_request.mark_ready_for_review!
|
34
|
+
puts "✅ done"
|
35
|
+
end
|
36
|
+
|
37
|
+
add_reviewers_to_pr!
|
38
|
+
ping_reviewers_on_slack!
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def ping_reviewers_on_slack!
|
44
|
+
step "Pinging reviewers on slack" do
|
45
|
+
slack.ping(
|
46
|
+
slack_message,
|
47
|
+
username: "review-bot",
|
48
|
+
channel: slack_channel
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def github_username
|
54
|
+
git_config("github.user")
|
55
|
+
end
|
56
|
+
|
57
|
+
def current_user_slack_tag
|
58
|
+
member = team.members.find { |member| member.github_username == github_username }
|
59
|
+
member&.slack_tag || git_config("user.name")
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def slack_message
|
64
|
+
user_tags = @reviewers.map(&:slack_tag)
|
65
|
+
|
66
|
+
<<~MSG
|
67
|
+
:pray: *Review request from #{current_user_slack_tag}*
|
68
|
+
_#{pull_request.title}_
|
69
|
+
#{pull_request.url}
|
70
|
+
|
71
|
+
Requested: #{user_tags.join(', ')}
|
72
|
+
MSG
|
73
|
+
end
|
74
|
+
|
75
|
+
def slack
|
76
|
+
@slack ||= Slack::Notifier.new(slack_webhook)
|
77
|
+
end
|
78
|
+
|
79
|
+
def slack_webhook
|
80
|
+
"https://hooks.slack.com/services/T02A0DJMA/"\
|
81
|
+
"BN49T3ZC0/Q536v8O5pzn1NSqDJ9BpEV3s"
|
82
|
+
end
|
83
|
+
|
84
|
+
def slack_channel
|
85
|
+
"#verisure"
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_reviewers_to_pr!
|
89
|
+
step "Assigning reviewers on Github" do
|
90
|
+
github_client.post(
|
91
|
+
"#{api_prefix}/pulls/#{pull_request.id}/requested_reviewers",
|
92
|
+
json: {
|
93
|
+
reviewers: @reviewers.map(&:github_username)
|
94
|
+
}
|
95
|
+
)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def check_for_minimum_version_of_hub!
|
100
|
+
match = `hub --version`.strip.match(/hub version (.+)$/)
|
101
|
+
version = Gem::Version.new(match[1])
|
102
|
+
min_version = "2.8.4"
|
103
|
+
|
104
|
+
if version < Gem::Version.new(min_version)
|
105
|
+
abort "aid review requires hub at #{min_version} or greater. "\
|
106
|
+
"Run `brew upgrade hub` to upgrade."
|
107
|
+
end
|
108
|
+
rescue StandardError
|
109
|
+
abort "Error checking for hub version. Ensure you have it installed with "\
|
110
|
+
"$ brew install hub"
|
111
|
+
end
|
112
|
+
|
113
|
+
def run_aid_finish_if_needed!
|
114
|
+
branch_commit_logs = `git log --format=full master..`.strip
|
115
|
+
|
116
|
+
if branch_commit_logs =~ %r{Finishes:.+https://app.asana.com}mi
|
117
|
+
step "Doing one last push to GitHub" do
|
118
|
+
system! "git push --force-with-lease origin #{current_branch_name}"
|
119
|
+
end
|
120
|
+
else
|
121
|
+
step "Running aid finish..." do
|
122
|
+
Finish.run
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def move_asana_task_to_pull_request_column!
|
128
|
+
step "Moving Asana task to Pull Request column" do
|
129
|
+
asana_task.add_project(
|
130
|
+
project: asana_project.gid,
|
131
|
+
section: asana_pr_section.gid
|
132
|
+
)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def team
|
137
|
+
@team ||= Aid::Team.from_yml("#{project_root}/.team.yml")
|
138
|
+
end
|
139
|
+
|
140
|
+
def prompt_for_reviewers!
|
141
|
+
puts "Which reviewers do you want to review this PR?"
|
142
|
+
puts
|
143
|
+
|
144
|
+
@reviewers = team.prompt_for_members
|
145
|
+
puts
|
146
|
+
|
147
|
+
abort colorize(:red, "Please select a reviewer from the list") if @reviewers.empty?
|
148
|
+
end
|
149
|
+
|
150
|
+
def change_wip_to_reviewable_label!
|
151
|
+
step "Changing WIP to reviewable label" do
|
152
|
+
puts "Deleting WIP label..."
|
153
|
+
|
154
|
+
github_client.delete("#{api_prefix}/issues/#{pull_request.id}/labels/wip")
|
155
|
+
|
156
|
+
puts "Adding reviewable label..."
|
157
|
+
|
158
|
+
github_client.post(
|
159
|
+
"#{api_prefix}/issues/#{pull_request.id}/labels",
|
160
|
+
json: {
|
161
|
+
labels: ["reviewable"]
|
162
|
+
}
|
163
|
+
)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def api_prefix
|
168
|
+
"https://api.github.com/repos/#{repo_name}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def github_auth_token
|
172
|
+
@github_auth_token ||= load_github_auth_token
|
173
|
+
end
|
174
|
+
|
175
|
+
def load_github_auth_token
|
176
|
+
credential_file = "#{ENV['HOME']}/.config/hub"
|
177
|
+
|
178
|
+
abort "No hub credentials in #{credential_file}" unless File.exist?(credential_file)
|
179
|
+
|
180
|
+
credentials = YAML.safe_load(File.read(credential_file))
|
181
|
+
credentials["github.com"][0]["oauth_token"]
|
182
|
+
end
|
183
|
+
|
184
|
+
def github_client
|
185
|
+
HTTP.headers("Authorization" => "token #{github_auth_token}")
|
186
|
+
end
|
187
|
+
|
188
|
+
def asana_task_id
|
189
|
+
@asana_task_id ||=
|
190
|
+
begin
|
191
|
+
result = current_branch_name.strip.match(/\d+$/)
|
192
|
+
result && result[0]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def current_branch_name
|
197
|
+
`git rev-parse --abbrev-ref HEAD`.strip
|
198
|
+
end
|
199
|
+
|
200
|
+
def repo_name
|
201
|
+
remote_url = `git remote get-url origin`.strip
|
202
|
+
result = remote_url.match(%r{github.com[:/](?<repo_name>\w+/\w+)(?:\.git)?})
|
203
|
+
result && result[:repo_name]
|
204
|
+
end
|
205
|
+
|
206
|
+
def pull_request
|
207
|
+
@pull_request ||= local_github_pull_request ||
|
208
|
+
find_remote_github_pull_request
|
209
|
+
end
|
210
|
+
|
211
|
+
def local_github_pull_request
|
212
|
+
id = git_config("asana.#{asana_task_id}.pull-request-id")
|
213
|
+
title = git_config("asana.#{asana_task_id}.name")
|
214
|
+
|
215
|
+
return nil unless id && title
|
216
|
+
|
217
|
+
PullRequest.new(
|
218
|
+
id: id,
|
219
|
+
title: title,
|
220
|
+
repo_name: repo_name
|
221
|
+
)
|
222
|
+
end
|
223
|
+
|
224
|
+
def find_remote_github_pull_request
|
225
|
+
sep = "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="
|
226
|
+
cmd = %(hub pr list --format="#PR: %i\nTitle: %t\n\n%b\n#{sep}\n")
|
227
|
+
|
228
|
+
raw_pull_requests = `#{cmd}`.strip
|
229
|
+
pull_requests = raw_pull_requests.split(sep)
|
230
|
+
request = pull_requests.detect { |pr| pr.include?(asana_task_id) }
|
231
|
+
|
232
|
+
abort "Could not find a PR that matches story #{asana_task_id}" unless request
|
233
|
+
|
234
|
+
id = request.match(/PR: #(\d+)/)[1]
|
235
|
+
title = request.match(/Title: (.+?)\n/)[1]
|
236
|
+
|
237
|
+
PullRequest.new(
|
238
|
+
id: id,
|
239
|
+
title: title,
|
240
|
+
repo_name: repo_name
|
241
|
+
)
|
242
|
+
end
|
243
|
+
|
244
|
+
def asana_task
|
245
|
+
@asana_task ||= asana.tasks.find_by_id(asana_task_id)
|
246
|
+
end
|
247
|
+
|
248
|
+
def asana_project
|
249
|
+
@asana_project ||= asana.projects.find_by_id(asana_project_id)
|
250
|
+
end
|
251
|
+
|
252
|
+
def asana_project_id
|
253
|
+
git_config("asana.project-id").to_i
|
254
|
+
end
|
255
|
+
|
256
|
+
def asana_sections
|
257
|
+
asana.sections.find_by_project(project: asana_project.gid)
|
258
|
+
end
|
259
|
+
|
260
|
+
def asana_pr_section
|
261
|
+
asana_sections.detect do |task|
|
262
|
+
task.name =~ /Pull Request/i
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def asana
|
267
|
+
@asana ||=
|
268
|
+
Asana::Client.new do |client|
|
269
|
+
client.default_headers "asana-enable" => "string_ids,new_sections"
|
270
|
+
client.authentication :access_token,
|
271
|
+
git_config("asana.personal-access-token")
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
class PullRequest
|
276
|
+
attr_reader :id, :title, :url
|
277
|
+
|
278
|
+
def initialize(id:, title:, repo_name:)
|
279
|
+
@id = id
|
280
|
+
@title = title
|
281
|
+
@url = "https://github.com/#{repo_name}/pull/#{id}"
|
282
|
+
end
|
283
|
+
|
284
|
+
# rubocop:disable Metrics/MethodLength
|
285
|
+
def mark_ready_for_review!
|
286
|
+
cmd = <<~CMD
|
287
|
+
hub api graphql \
|
288
|
+
-H "Accept: application/vnd.github.shadow-cat-preview+json" \
|
289
|
+
-f query='
|
290
|
+
mutation MarkPullRequestReady {
|
291
|
+
markPullRequestReadyForReview(
|
292
|
+
input: {
|
293
|
+
pullRequestId:"#{graphql_id}"
|
294
|
+
}
|
295
|
+
) {
|
296
|
+
pullRequest {
|
297
|
+
isDraft
|
298
|
+
number
|
299
|
+
}
|
300
|
+
}
|
301
|
+
}
|
302
|
+
'
|
303
|
+
CMD
|
304
|
+
|
305
|
+
`#{cmd}`
|
306
|
+
end
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
def graphql_id
|
311
|
+
@graphql_id ||= find_graphql_id
|
312
|
+
end
|
313
|
+
|
314
|
+
def find_graphql_id
|
315
|
+
cmd = <<~CMD
|
316
|
+
hub api graphql -f query='
|
317
|
+
query FindPullRequestId {
|
318
|
+
repository(owner:"abtion", name:"verisure") {
|
319
|
+
pullRequests(states:OPEN, first: 25) {
|
320
|
+
nodes {
|
321
|
+
id
|
322
|
+
number
|
323
|
+
}
|
324
|
+
}
|
325
|
+
}
|
326
|
+
}
|
327
|
+
'
|
328
|
+
CMD
|
329
|
+
|
330
|
+
json = `#{cmd}`.strip
|
331
|
+
response = JSON.parse(json)
|
332
|
+
|
333
|
+
pull_requests = response.dig(
|
334
|
+
"data",
|
335
|
+
"repository",
|
336
|
+
"pullRequests",
|
337
|
+
"nodes"
|
338
|
+
)
|
339
|
+
|
340
|
+
request = pull_requests.find { |pr| pr["number"].to_s == id.to_s }
|
341
|
+
request["id"]
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Aid
|
2
|
+
module GitConfig
|
3
|
+
def git_config(key, value = nil)
|
4
|
+
if value
|
5
|
+
`git config --local --add #{key.inspect} #{value.inspect}`
|
6
|
+
else
|
7
|
+
git_value = `git config --get #{key.inspect}`.strip
|
8
|
+
git_value.empty? ? nil : git_value
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def prompt_for_config!(key, prompt_msg, remedy)
|
13
|
+
value = git_config(key)
|
14
|
+
|
15
|
+
if value == "" || value.nil?
|
16
|
+
puts <<~EOF
|
17
|
+
Missing git config "#{key}":
|
18
|
+
To find this value:
|
19
|
+
#{remedy}
|
20
|
+
EOF
|
21
|
+
|
22
|
+
new_value = prompt(prompt_msg)
|
23
|
+
|
24
|
+
if new_value.empty?
|
25
|
+
abort "Empty value, aborting"
|
26
|
+
else
|
27
|
+
git_config(key, new_value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def prompt(msg)
|
33
|
+
print "#{msg} > "
|
34
|
+
value = STDIN.gets.strip
|
35
|
+
puts
|
36
|
+
value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Aid
|
2
|
+
module GitStaged
|
3
|
+
def check_for_staged_files!
|
4
|
+
file_change_status_codes = "^\s*[MADRCU]"
|
5
|
+
return unless system("git status -s | grep #{file_change_status_codes.inspect} >/dev/null 2>&1")
|
6
|
+
|
7
|
+
abort colorize(:red, "You have modified/staged files, cannot aid")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Aid
|
2
|
+
class Team
|
3
|
+
attr_reader :members
|
4
|
+
|
5
|
+
def initialize(members)
|
6
|
+
@members =
|
7
|
+
members.map do |member|
|
8
|
+
Member.new(
|
9
|
+
member["name"],
|
10
|
+
member["github"],
|
11
|
+
member["slack"]
|
12
|
+
)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_yml(path)
|
17
|
+
members = YAML.safe_load(File.read(path))["team"]
|
18
|
+
|
19
|
+
Team.new(members)
|
20
|
+
end
|
21
|
+
|
22
|
+
def prompt_for_members
|
23
|
+
puts "Enter their number(s) below. For multiple team members, enter "\
|
24
|
+
"multiple numbers separated by spaces or commas."
|
25
|
+
puts
|
26
|
+
|
27
|
+
members.each.with_index do |member, index|
|
28
|
+
puts "#{index + 1}. #{member.name} (@#{member.github_username})"
|
29
|
+
end
|
30
|
+
|
31
|
+
puts
|
32
|
+
print "> "
|
33
|
+
|
34
|
+
numbers = $stdin.gets.strip.split(/[^\d]+/)
|
35
|
+
|
36
|
+
indexes =
|
37
|
+
numbers
|
38
|
+
.map { |num| num.to_i - 1 }
|
39
|
+
.reject { |num| num < 0 }
|
40
|
+
|
41
|
+
indexes.map { |index| members[index] }.compact
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
class Member
|
47
|
+
attr_reader :name, :github_username, :slack_user_id
|
48
|
+
|
49
|
+
def initialize(name, github_username, slack_user_id)
|
50
|
+
@name = name
|
51
|
+
@github_username = github_username
|
52
|
+
@slack_user_id = slack_user_id
|
53
|
+
end
|
54
|
+
|
55
|
+
def slack_tag
|
56
|
+
"<@#{slack_user_id}>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/aid/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abtion-aid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abtion
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-11-
|
12
|
+
date: 2019-11-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -81,6 +81,48 @@ dependencies:
|
|
81
81
|
- - ">="
|
82
82
|
- !ruby/object:Gem::Version
|
83
83
|
version: '0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: http
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: slack-notifier
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
type: :runtime
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: asana
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :runtime
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
84
126
|
description:
|
85
127
|
email:
|
86
128
|
- mail@abtion.com
|
@@ -123,10 +165,16 @@ files:
|
|
123
165
|
- lib/aid/plugins.rb
|
124
166
|
- lib/aid/script.rb
|
125
167
|
- lib/aid/scripts.rb
|
168
|
+
- lib/aid/scripts/begin.rb
|
126
169
|
- lib/aid/scripts/doctor.rb
|
170
|
+
- lib/aid/scripts/finish.rb
|
127
171
|
- lib/aid/scripts/help.rb
|
128
172
|
- lib/aid/scripts/init.rb
|
129
173
|
- lib/aid/scripts/new.rb
|
174
|
+
- lib/aid/scripts/review.rb
|
175
|
+
- lib/aid/scripts/shared/git_config.rb
|
176
|
+
- lib/aid/scripts/shared/git_staged.rb
|
177
|
+
- lib/aid/scripts/shared/team.rb
|
130
178
|
- lib/aid/version.rb
|
131
179
|
homepage: https://github.com/abtion/aid
|
132
180
|
licenses:
|
@@ -147,9 +195,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
195
|
- !ruby/object:Gem::Version
|
148
196
|
version: '0'
|
149
197
|
requirements: []
|
150
|
-
|
151
|
-
rubygems_version: 2.7.7
|
198
|
+
rubygems_version: 3.0.3
|
152
199
|
signing_key:
|
153
200
|
specification_version: 4
|
154
|
-
summary:
|
201
|
+
summary: Abtion's development workflow tools
|
155
202
|
test_files: []
|