gergich 0.0.1

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