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.
@@ -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