gergich 0.0.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 +7 -0
- data/LICENSE +20 -0
- data/README.md +154 -0
- data/bin/check_coverage +7 -0
- data/bin/gergich +8 -0
- data/bin/master_bouncer +4 -0
- data/lib/gergich.rb +455 -0
- data/lib/gergich/capture.rb +77 -0
- data/lib/gergich/capture/eslint_capture.rb +20 -0
- data/lib/gergich/capture/i18nliner_capture.rb +22 -0
- data/lib/gergich/capture/rubocop_capture.rb +19 -0
- data/lib/gergich/cli.rb +82 -0
- data/lib/gergich/cli/gergich.rb +247 -0
- data/lib/gergich/cli/master_bouncer.rb +109 -0
- data/spec/gergich/capture_spec.rb +59 -0
- data/spec/gergich_spec.rb +133 -0
- data/spec/spec_helper.rb +89 -0
- metadata +89 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module Gergich
|
2
|
+
module Capture
|
3
|
+
class BaseCapture
|
4
|
+
def self.inherited(subclass)
|
5
|
+
name = subclass.name
|
6
|
+
# borrowed from AS underscore, since we may not have it
|
7
|
+
name.gsub!(/.*::|Capture\z/, "")
|
8
|
+
name.gsub!(/([A-Z\d]+)([A-Z][a-z])/, "\\1_\\2")
|
9
|
+
name.gsub!(/([a-z\d])([A-Z])/, "\\1_\\2")
|
10
|
+
name.tr!("-", "_")
|
11
|
+
name.downcase!
|
12
|
+
Capture.captors[name] = subclass
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
def run(format, command)
|
18
|
+
captor = load_captor(format)
|
19
|
+
|
20
|
+
exit_code, output = run_command(command)
|
21
|
+
comments = captor.new.run(output.gsub(/\e\[\d+m/m, ""))
|
22
|
+
|
23
|
+
draft = Gergich::Draft.new
|
24
|
+
skip_paths = (ENV["SKIP_PATHS"] || "").split(",")
|
25
|
+
comments.each do |comment|
|
26
|
+
next if skip_paths.any? { |path| comment[:path].start_with?(path) }
|
27
|
+
draft.add_comment comment[:path], comment[:position],
|
28
|
+
comment[:message], comment[:severity]
|
29
|
+
end
|
30
|
+
|
31
|
+
exit_code
|
32
|
+
end
|
33
|
+
|
34
|
+
def run_command(command)
|
35
|
+
output = ""
|
36
|
+
IO.popen("#{command} 2>&1", "r+") do |io|
|
37
|
+
io.each do |line|
|
38
|
+
puts line
|
39
|
+
output << line
|
40
|
+
end
|
41
|
+
end
|
42
|
+
exit_code = $CHILD_STATUS && $CHILD_STATUS.exitstatus || 0
|
43
|
+
|
44
|
+
[exit_code, output]
|
45
|
+
end
|
46
|
+
|
47
|
+
def load_captor(format)
|
48
|
+
if (match = format.match(/\Acustom:(?<path>.+?):(?<class_name>.+)\z/))
|
49
|
+
load_custom_captor(match[:path], match[:class_name])
|
50
|
+
else
|
51
|
+
captor = captors[format]
|
52
|
+
raise GergichError, "Unrecognized format `#{format}`" unless captor
|
53
|
+
captor
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_custom_captor(path, class_name)
|
58
|
+
begin
|
59
|
+
require path
|
60
|
+
rescue LoadError
|
61
|
+
raise GergichError, "unable to load custom format from `#{path}`"
|
62
|
+
end
|
63
|
+
begin
|
64
|
+
const_get(class_name)
|
65
|
+
rescue NameError
|
66
|
+
raise GergichError, "unable to find custom format class `#{class_name}`"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def captors
|
71
|
+
@captors ||= {}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Dir[File.dirname(__FILE__) + "/capture/*.rb"].each { |file| require file }
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Gergich
|
2
|
+
module Capture
|
3
|
+
class EslintCapture < BaseCapture
|
4
|
+
def run(output)
|
5
|
+
# e.g. " 4:21 error Missing semicolon semi"
|
6
|
+
error_pattern = /\s\s+(\d+):\d+\s+\w+\s+(.*?)\s+[\w-]+\n/
|
7
|
+
pattern = %r{ # Example:
|
8
|
+
^([^\n]+)\n # jsapp/models/user.js
|
9
|
+
((#{error_pattern})+) # 4:21 error Missing semicolon semi
|
10
|
+
}mx
|
11
|
+
|
12
|
+
output.scan(pattern).map { |file, errors|
|
13
|
+
errors.scan(error_pattern).map { |line, error|
|
14
|
+
{ path: file, message: "[eslint] #{error}", position: line.to_i, severity: "error" }
|
15
|
+
}
|
16
|
+
}.compact.flatten
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Gergich
|
2
|
+
module Capture
|
3
|
+
class I18nlinerCapture < BaseCapture
|
4
|
+
def run(output)
|
5
|
+
pattern = %r{ # Example:
|
6
|
+
^\d+\)\n # 1)
|
7
|
+
(.*?)\n # invalid signature on line 4: <unsupported expression>
|
8
|
+
(.*?)\n # jsapp/models/user.js
|
9
|
+
}mx
|
10
|
+
|
11
|
+
output.scan(pattern).map { |error, file|
|
12
|
+
line = 1
|
13
|
+
error.sub!(/ on line (\d+)/) do
|
14
|
+
line = Regexp.last_match[1]
|
15
|
+
""
|
16
|
+
end
|
17
|
+
{ path: file, message: "[i18n] #{error}", position: line.to_i, severity: "error" }
|
18
|
+
}.compact
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Gergich
|
2
|
+
module Capture
|
3
|
+
class RubocopCapture < BaseCapture
|
4
|
+
def run(output)
|
5
|
+
pattern = %r{ # Example:
|
6
|
+
^([^:\n]+):(\d+):\d+:\s(.*?)\n # bin/gergich:47:8: C: Prefer double-quoted strings
|
7
|
+
([^\n]+\n # if ENV['DEBUG']
|
8
|
+
[^^\n]*\^+[^^\n]*\n)? # ^^^^^^^
|
9
|
+
}mx
|
10
|
+
|
11
|
+
output.scan(pattern).map { |file, line, error, context|
|
12
|
+
context = "\n\n" + context.gsub(/^/, " ") if context
|
13
|
+
{ path: file, message: "[rubocop] #{error}#{context}",
|
14
|
+
position: line.to_i, severity: "error" }
|
15
|
+
}.compact
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/gergich/cli.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require "shellwords"
|
2
|
+
require "English"
|
3
|
+
|
4
|
+
$stdout.sync = true
|
5
|
+
$stderr.sync = true
|
6
|
+
|
7
|
+
def info(text)
|
8
|
+
puts text
|
9
|
+
exit
|
10
|
+
end
|
11
|
+
|
12
|
+
def script_name
|
13
|
+
$PROGRAM_NAME.sub(%r{.*\/}, "")
|
14
|
+
end
|
15
|
+
|
16
|
+
def usage(content = nil)
|
17
|
+
"Usage: #{script_name} <command> [<args>...]\n" +
|
18
|
+
(content ? "\n#{content}\n\n" : "") +
|
19
|
+
"Tip: run `#{script_name} help <command>` for more info"
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(text)
|
23
|
+
$stderr.puts "\e[31mError:\e[0m #{text}"
|
24
|
+
$stderr.puts usage
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def help_command(commands)
|
29
|
+
{
|
30
|
+
action: ->(subcommand = "help") {
|
31
|
+
subcommand_info = commands[subcommand]
|
32
|
+
if !subcommand_info
|
33
|
+
error "Unrecognized command `#{subcommand}`"
|
34
|
+
elsif (help_text = subcommand_info[:help])
|
35
|
+
info help_text.respond_to?(:call) ? help_text.call : help_text
|
36
|
+
else
|
37
|
+
error "No help available for `#{subcommand}`"
|
38
|
+
end
|
39
|
+
},
|
40
|
+
help: -> {
|
41
|
+
indentation = commands.keys.map(&:size).sort.last
|
42
|
+
commands_help = commands
|
43
|
+
.to_a
|
44
|
+
.sort_by(&:first)
|
45
|
+
.map { |key, data|
|
46
|
+
"#{key.ljust(indentation)} - #{data[:summary]}" if data[:summary]
|
47
|
+
}
|
48
|
+
.compact
|
49
|
+
usage(commands_help.join("\n"))
|
50
|
+
}
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def run_command(action)
|
55
|
+
params = action.parameters
|
56
|
+
params.each_with_index do |(type, name), i|
|
57
|
+
error "No <#{name}> specified" if i >= ARGV.size && type == :req
|
58
|
+
end
|
59
|
+
if ARGV.size > params.size
|
60
|
+
extra_args = ARGV[params.size, ARGV.size].map { |a| "`#{Shellwords.escape(a)}`" }
|
61
|
+
error "Extra arg(s) #{extra_args.join(' ')}"
|
62
|
+
end
|
63
|
+
action.call(*ARGV)
|
64
|
+
end
|
65
|
+
|
66
|
+
def run_app(commands)
|
67
|
+
commands["help"] = help_command(commands)
|
68
|
+
command = ARGV.shift || "help"
|
69
|
+
|
70
|
+
if commands[command]
|
71
|
+
begin
|
72
|
+
action = commands[command][:action]
|
73
|
+
run_command(action)
|
74
|
+
rescue GergichError
|
75
|
+
error $ERROR_INFO.message
|
76
|
+
rescue
|
77
|
+
error "Unhandled exception: #{$ERROR_INFO}\n#{$ERROR_INFO.backtrace.join("\n")}"
|
78
|
+
end
|
79
|
+
else
|
80
|
+
error "Unrecognized command `#{command}`"
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# encoding=utf-8
|
2
|
+
|
3
|
+
require_relative "../cli"
|
4
|
+
require_relative "../../gergich"
|
5
|
+
|
6
|
+
CI_TEST_ARGS = {
|
7
|
+
"comment" => [
|
8
|
+
[
|
9
|
+
{ path: "foo.rb", position: 3, severity: "error", message: "ಠ_ಠ" },
|
10
|
+
{ path: "/COMMIT_MSG", position: 1, severity: "info", message: "cool story bro" },
|
11
|
+
{ path: "/COMMIT_MSG", severity: "info", message: "lol",
|
12
|
+
position: { start_line: 1, start_character: 1, end_line: 1, end_character: 2 } }
|
13
|
+
].to_json
|
14
|
+
],
|
15
|
+
"label" => ["Code-Review", 1],
|
16
|
+
"message" => ["this is a test"],
|
17
|
+
"capture" => ["rubocop", "echo #{Shellwords.escape(<<-OUTPUT)}"]
|
18
|
+
bin/gergich:47:8: C: Prefer double-quoted strings
|
19
|
+
if ENV['DEBUG']
|
20
|
+
^^^^^^^
|
21
|
+
OUTPUT
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
def run_ci_test!(all_commands)
|
25
|
+
commands_to_test = all_commands - %w[citest reset publish status]
|
26
|
+
commands_to_test << "status" # put it at the end, so we maximize the stuff it tests
|
27
|
+
|
28
|
+
commands = commands_to_test.map { |command| [command, CI_TEST_ARGS[command] || []] }
|
29
|
+
commands.concat all_commands.map { |command| ["help", [command]] }
|
30
|
+
|
31
|
+
# after running our test commands, reset and publish frd:
|
32
|
+
commands << ["reset"]
|
33
|
+
commands << ["label", ["QA-Review", 1]]
|
34
|
+
commands << ["label", ["Product-Review", 1]]
|
35
|
+
commands << ["label", ["Code-Review", 1]]
|
36
|
+
commands << ["message", ["\`gergich citest\` checks out :thumbsup: :mj:"]]
|
37
|
+
commands << ["publish"]
|
38
|
+
|
39
|
+
commands.each do |command, args = []|
|
40
|
+
arglist = args.map { |arg| Shellwords.escape(arg.to_s) }
|
41
|
+
output = `bin/gergich #{command} #{arglist.join(" ")} 2>&1`
|
42
|
+
unless $CHILD_STATUS.success?
|
43
|
+
error "`gergich citest` failed on step `#{command}`:\n\n#{output.gsub(/^/, ' ')}\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
commands = {}
|
49
|
+
|
50
|
+
commands["reset"] = {
|
51
|
+
summary: "Clear out pending comments/labels/messages for this patchset",
|
52
|
+
action: ->() {
|
53
|
+
Gergich::Draft.new.reset!
|
54
|
+
},
|
55
|
+
help: -> {
|
56
|
+
<<-TEXT
|
57
|
+
gergich reset
|
58
|
+
|
59
|
+
Clear out the draft for this patchset. Useful for testing.
|
60
|
+
TEXT
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
commands["publish"] = {
|
65
|
+
summary: "Publish the draft for this patchset",
|
66
|
+
action: ->() {
|
67
|
+
if (data = Gergich::Review.new.publish!)
|
68
|
+
puts "Published #{data[:total_comments]} comments, set score to #{data[:score]}"
|
69
|
+
else
|
70
|
+
puts "Nothing to publish"
|
71
|
+
end
|
72
|
+
},
|
73
|
+
help: -> {
|
74
|
+
<<-TEXT
|
75
|
+
gergich publish
|
76
|
+
|
77
|
+
Publish all draft comments/labels/messages for this patchset. no-op if
|
78
|
+
there are none.
|
79
|
+
|
80
|
+
The cover message and Code-Review label (e.g. -2) are inferred from the
|
81
|
+
comments, but labels and messages may be manually set (via `gergich
|
82
|
+
message` and `gergich labels`)
|
83
|
+
TEXT
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
commands["status"] = {
|
88
|
+
summary: "Show the current draft for this patchset",
|
89
|
+
action: ->() {
|
90
|
+
Gergich::Review.new.status
|
91
|
+
},
|
92
|
+
help: -> {
|
93
|
+
<<-TEXT
|
94
|
+
gergich status
|
95
|
+
|
96
|
+
Show the current draft for this patchset
|
97
|
+
|
98
|
+
Display any labels, cover messages and inline comments that will be set
|
99
|
+
as part of this review.
|
100
|
+
TEXT
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
commands["comment"] = {
|
105
|
+
summary: "Add one or more draft comments to this patchset",
|
106
|
+
action: ->(comment_data) {
|
107
|
+
comment_data = begin
|
108
|
+
JSON.parse(comment_data)
|
109
|
+
rescue JSON::ParserError
|
110
|
+
error("Unable to parse <comment_data> json", "comment")
|
111
|
+
end
|
112
|
+
comment_data = [comment_data] unless comment_data.is_a?(Array)
|
113
|
+
|
114
|
+
draft = Gergich::Draft.new
|
115
|
+
comment_data.each do |comment|
|
116
|
+
draft.add_comment comment["path"],
|
117
|
+
comment["position"],
|
118
|
+
comment["message"],
|
119
|
+
comment["severity"]
|
120
|
+
end
|
121
|
+
},
|
122
|
+
help: ->() {
|
123
|
+
<<-TEXT
|
124
|
+
gergich comment <comment_data>
|
125
|
+
|
126
|
+
<comment_data> is a JSON object (or array of objects). Each comment object
|
127
|
+
should have the following properties:
|
128
|
+
path - the relative file path, e.g. "app/models/user.rb"
|
129
|
+
position - either a number (line) or an object (range). If an object,
|
130
|
+
must have the following numeric properties:
|
131
|
+
* start_line
|
132
|
+
* start_character
|
133
|
+
* end_line
|
134
|
+
* end_character
|
135
|
+
message - the text of the comment
|
136
|
+
severity - "info"|"warn"|"error" - this will automatically prefix the
|
137
|
+
comment (e.g. "[ERROR] message here"), and the most severe
|
138
|
+
comment will be used to determine the overall Code-Review
|
139
|
+
score (0, -1, or -2 respectively)
|
140
|
+
|
141
|
+
Note that a cover message and Code-Review score will be inferred from the
|
142
|
+
most severe comment.
|
143
|
+
|
144
|
+
Examples
|
145
|
+
gergich comment '{"path":"foo.rb","position":3,"severity":"error",
|
146
|
+
"message":"ಠ_ಠ"}'
|
147
|
+
gergich comment '{"path":"bar.rb","severity":"warn",
|
148
|
+
"position":{"start_line":3,"start_character":5,...},
|
149
|
+
"message":"¯\\_(ツ)_/¯"}'
|
150
|
+
gergich comment '[{"path":"baz.rb",...}, {...}, {...}]'
|
151
|
+
TEXT
|
152
|
+
}
|
153
|
+
}
|
154
|
+
|
155
|
+
commands["message"] = {
|
156
|
+
summary: "Add a draft cover message to this patchset",
|
157
|
+
action: ->(message) {
|
158
|
+
draft = Gergich::Draft.new
|
159
|
+
draft.add_message message
|
160
|
+
},
|
161
|
+
help: ->() {
|
162
|
+
<<-TEXT
|
163
|
+
gergich message <message>
|
164
|
+
|
165
|
+
<message> will be appended to existing cover messages (inferred or manually
|
166
|
+
added) for this patchset.
|
167
|
+
TEXT
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
commands["label"] = {
|
172
|
+
summary: "Add a draft label (e.g. Code-Review -1) to this patchset",
|
173
|
+
action: ->(label, score) {
|
174
|
+
Gergich::Draft.new.add_label label, score
|
175
|
+
},
|
176
|
+
help: ->() {
|
177
|
+
<<-TEXT
|
178
|
+
gergich label <label> <score>
|
179
|
+
|
180
|
+
Add a draft label to this patchset. If the same label is set multiple
|
181
|
+
times, the lowest score will win.
|
182
|
+
|
183
|
+
<label> - a valid label (e.g. "Code-Review")
|
184
|
+
<score> - a valid score (e.g. -1)
|
185
|
+
TEXT
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
commands["capture"] = {
|
190
|
+
summary: "Run a command and translate its output into `gergich comment` calls",
|
191
|
+
action: ->(format, command) {
|
192
|
+
require_relative "../../gergich/capture"
|
193
|
+
exit Gergich::Capture.run(format, command)
|
194
|
+
},
|
195
|
+
help: ->() {
|
196
|
+
<<-TEXT
|
197
|
+
gergich capture <format> <command>
|
198
|
+
|
199
|
+
For common linting formats, `gergich capture` can be used to automatically
|
200
|
+
do `gergich comment` calls so you don't have to wire it up yourself.
|
201
|
+
|
202
|
+
<format> - One of the following:
|
203
|
+
* rubocop
|
204
|
+
* eslint
|
205
|
+
* i18nliner
|
206
|
+
* custom:<path>:<class_name> - file path and ruby
|
207
|
+
class_name of a custom formatter.
|
208
|
+
|
209
|
+
<command> - The command to run whose output conforms to <format>
|
210
|
+
|
211
|
+
Examples:
|
212
|
+
gergich capture rubocop "bundle exec rubocop"
|
213
|
+
|
214
|
+
gergich capture eslint eslint
|
215
|
+
|
216
|
+
gergich capture i18nliner "rake i18nliner:check"
|
217
|
+
|
218
|
+
gergich capture custom:./gergich/xss:Gergich::XSS "node script/xsslint"
|
219
|
+
|
220
|
+
TEXT
|
221
|
+
}
|
222
|
+
}
|
223
|
+
|
224
|
+
commands["citest"] = {
|
225
|
+
summary: "Do a full gergich test based on the current commit",
|
226
|
+
action: ->() {
|
227
|
+
# automagically test any new command that comes along
|
228
|
+
run_ci_test!(commands.keys)
|
229
|
+
},
|
230
|
+
help: ->() {
|
231
|
+
<<-TEXT
|
232
|
+
gergich citest
|
233
|
+
|
234
|
+
You shouldn't need to run this locally, it runs on jenkins. It does the
|
235
|
+
following:
|
236
|
+
|
237
|
+
1. runs all the gergich commands (w/ dummy data)
|
238
|
+
2. ensure all `help` commands work
|
239
|
+
3. ensures gergich status is correct
|
240
|
+
4. resets
|
241
|
+
5. posts an actual +1
|
242
|
+
6. publishes
|
243
|
+
TEXT
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
run_app commands
|