shiba 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +113 -63
- data/bin/review +25 -135
- data/lib/shiba.rb +4 -0
- data/lib/shiba/activerecord_integration.rb +21 -19
- data/lib/shiba/analyzer.rb +1 -1
- data/lib/shiba/configure.rb +12 -6
- data/lib/shiba/connection/mysql.rb +72 -3
- data/lib/shiba/connection/postgres.rb +4 -1
- data/lib/shiba/console.rb +165 -0
- data/lib/shiba/explain.rb +22 -7
- data/lib/shiba/fuzzer.rb +5 -0
- data/lib/shiba/index_stats.rb +20 -1
- data/lib/shiba/output/tags.yaml +1 -1
- data/lib/shiba/parsers/mysql_select_fields.rb +64 -0
- data/lib/shiba/parsers/shiba_string_scanner.rb +27 -0
- data/lib/shiba/review/cli.rb +196 -0
- data/lib/shiba/review/comment_renderer.rb +19 -2
- data/lib/shiba/review/diff.rb +227 -0
- data/lib/shiba/review/explain_diff.rb +117 -0
- data/lib/shiba/reviewer.rb +20 -19
- data/lib/shiba/table_stats.rb +4 -0
- data/lib/shiba/version.rb +1 -1
- data/web/results.html.erb +15 -1
- metadata +10 -5
- data/lib/shiba/checker.rb +0 -165
- data/lib/shiba/diff.rb +0 -129
data/lib/shiba/index_stats.rb
CHANGED
@@ -13,7 +13,7 @@ module Shiba
|
|
13
13
|
@tables.any?
|
14
14
|
end
|
15
15
|
|
16
|
-
Table = Struct.new(:name, :count, :indexes
|
16
|
+
Table = Struct.new(:name, :count, :indexes) do
|
17
17
|
def encode_with(coder)
|
18
18
|
coder.map = self.to_h.stringify_keys
|
19
19
|
coder.map.delete('name')
|
@@ -23,6 +23,7 @@ module Shiba
|
|
23
23
|
self.count = indexes.map { |i, parts| parts.columns.map { |v| v.raw_cardinality } }.flatten.max
|
24
24
|
end
|
25
25
|
|
26
|
+
coder.map['column_sizes'] = column_sizes
|
26
27
|
coder.tag = nil
|
27
28
|
end
|
28
29
|
|
@@ -39,6 +40,10 @@ module Shiba
|
|
39
40
|
self.count = cardinality
|
40
41
|
end
|
41
42
|
end
|
43
|
+
|
44
|
+
def column_sizes
|
45
|
+
@column_sizes ||= {}
|
46
|
+
end
|
42
47
|
end
|
43
48
|
|
44
49
|
Index = Struct.new(:table, :name, :columns, :unique) do
|
@@ -152,6 +157,20 @@ module Shiba
|
|
152
157
|
table.add_index_column(index_name, column_name, nil, cardinality, is_unique)
|
153
158
|
end
|
154
159
|
|
160
|
+
def get_column_size(table_name, column)
|
161
|
+
table = @tables[table_name]
|
162
|
+
return nil unless table
|
163
|
+
|
164
|
+
table.column_sizes[column]
|
165
|
+
end
|
166
|
+
|
167
|
+
def set_column_size(table_name, column, size)
|
168
|
+
table = @tables[table_name]
|
169
|
+
raise "couldn't find table: #{table_name}" unless table
|
170
|
+
|
171
|
+
table.column_sizes[column] = size
|
172
|
+
end
|
173
|
+
|
155
174
|
def estimate_key(table_name, key, parts)
|
156
175
|
index = fetch_index(table_name, key)
|
157
176
|
|
data/lib/shiba/output/tags.yaml
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'shiba/parsers/shiba_string_scanner'
|
2
|
+
|
3
|
+
module Shiba
|
4
|
+
module Parsers
|
5
|
+
class MysqlSelectFields
|
6
|
+
def initialize(sql)
|
7
|
+
@sql = sql
|
8
|
+
@sc = ShibaStringScanner.new(@sql)
|
9
|
+
end
|
10
|
+
attr_reader :sc
|
11
|
+
|
12
|
+
BACKTICK = "`"
|
13
|
+
|
14
|
+
def tick_match
|
15
|
+
sc.match_quoted_double_escape(BACKTICK)
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse_fields
|
19
|
+
tables = {}
|
20
|
+
|
21
|
+
sc.scan(%r{/\*.*?\*/ select })
|
22
|
+
|
23
|
+
while !sc.scan(/ from/i)
|
24
|
+
sc.scan(/distinct /)
|
25
|
+
|
26
|
+
if sc.scan(/\w+\(/)
|
27
|
+
parens = 1
|
28
|
+
while parens > 0
|
29
|
+
case sc.getch
|
30
|
+
when '('
|
31
|
+
parens += 1
|
32
|
+
when ')'
|
33
|
+
parens -= 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
sc.scan(/ AS /)
|
37
|
+
tick_match
|
38
|
+
# parse function
|
39
|
+
elsif sc.scan(/`(.*?)`\.`(.*?)`\.`(.*?)` AS `(.*?)`/)
|
40
|
+
db = sc[1]
|
41
|
+
table = sc[2]
|
42
|
+
col = sc[3]
|
43
|
+
|
44
|
+
tables[table] ||= []
|
45
|
+
tables[table] << col
|
46
|
+
|
47
|
+
elsif sc.scan(/`(.*?)`\.`(.*?)` AS `(.*?)`/)
|
48
|
+
table = sc[1]
|
49
|
+
col = sc[2]
|
50
|
+
|
51
|
+
tables[table] ||= []
|
52
|
+
tables[table] << col
|
53
|
+
elsif sc.scan(/(\d+|NULL|'.*?') AS `(.*?)`/m)
|
54
|
+
else
|
55
|
+
raise "unknown stuff: in #{@sql}: #{@sc.rest}"
|
56
|
+
end
|
57
|
+
|
58
|
+
sc.scan(/,/)
|
59
|
+
end
|
60
|
+
tables
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Shiba
|
4
|
+
module Parsers
|
5
|
+
class ShibaStringScanner < StringScanner
|
6
|
+
def match_quoted_double_escape(quote)
|
7
|
+
getch
|
8
|
+
|
9
|
+
str = ""
|
10
|
+
while ch = getch
|
11
|
+
if ch == quote
|
12
|
+
if peek(1) == quote
|
13
|
+
str += ch
|
14
|
+
str += getch
|
15
|
+
else
|
16
|
+
return str
|
17
|
+
end
|
18
|
+
else
|
19
|
+
str += ch
|
20
|
+
end
|
21
|
+
end
|
22
|
+
str
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Shiba
|
2
|
+
module Review
|
3
|
+
# Builds options for interacting with the reviewer via the command line.
|
4
|
+
# Automatically infers options from environment variables on CI.
|
5
|
+
#
|
6
|
+
# Example:
|
7
|
+
# cli = CLI.new
|
8
|
+
# cli.valid?
|
9
|
+
# => true
|
10
|
+
# cli.options
|
11
|
+
# => { "file" => 'path/to/explain_log.json' }
|
12
|
+
#
|
13
|
+
# or
|
14
|
+
#
|
15
|
+
# cli.valid?
|
16
|
+
# => false
|
17
|
+
# cli.failure
|
18
|
+
# => "An error message with command line help."
|
19
|
+
class CLI
|
20
|
+
|
21
|
+
attr_reader :errors
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@user_options = {}
|
25
|
+
@errors = []
|
26
|
+
parser.parse!
|
27
|
+
@options = default_options.merge(@user_options)
|
28
|
+
end
|
29
|
+
|
30
|
+
def options
|
31
|
+
@options
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid?
|
35
|
+
return false if @errors.any?
|
36
|
+
|
37
|
+
validate_log_path
|
38
|
+
#validate_git_repo if branch || options["submit"]
|
39
|
+
description = "Provide an explain log, or run 'shiba explain' to generate one."
|
40
|
+
|
41
|
+
require_option("file", description: description)
|
42
|
+
|
43
|
+
if options["submit"]
|
44
|
+
require_option("branch") if options["diff"].nil?
|
45
|
+
require_option("token")
|
46
|
+
require_option("pull_request")
|
47
|
+
end
|
48
|
+
|
49
|
+
@errors.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def failure
|
53
|
+
return nil if @errors.empty?
|
54
|
+
|
55
|
+
message, help = @errors.first
|
56
|
+
message += "\n"
|
57
|
+
if help
|
58
|
+
message += "\n#{parser}"
|
59
|
+
end
|
60
|
+
|
61
|
+
message
|
62
|
+
end
|
63
|
+
|
64
|
+
def report_options(*keys)
|
65
|
+
keys.each do |key|
|
66
|
+
report("#{key}: #{options[key]}")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def parser
|
73
|
+
@parser ||= OptionParser.new do |opts|
|
74
|
+
opts.banner = "Review changes for query problems. Optionally submit the comments to a Github pull request."
|
75
|
+
|
76
|
+
opts.separator "Required:"
|
77
|
+
|
78
|
+
opts.on("-f","--file FILE", "The explain output log to compare with. Automatically configured when $CI environment variable is set") do |f|
|
79
|
+
@user_options["file"] = f
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.separator ""
|
83
|
+
opts.separator "Git diff options:"
|
84
|
+
|
85
|
+
opts.on("-b", "--branch GIT_BRANCH", "Compare to changes between origin/HEAD and BRANCH. Attempts to read from CI environment when not set.") do |b|
|
86
|
+
@user_options["branch"] = b
|
87
|
+
end
|
88
|
+
|
89
|
+
opts.on("--staged", "Only check files that are staged for commit") do
|
90
|
+
@user_options["staged"] = true
|
91
|
+
end
|
92
|
+
|
93
|
+
opts.on("--unstaged", "Only check files that are not staged for commit") do
|
94
|
+
@user_options["unstaged"] = true
|
95
|
+
end
|
96
|
+
|
97
|
+
opts.separator ""
|
98
|
+
opts.separator "Github options:"
|
99
|
+
|
100
|
+
opts.on("--submit", "Submit comments to Github") do
|
101
|
+
@user_options["submit"] = true
|
102
|
+
end
|
103
|
+
|
104
|
+
opts.on("-p", "--pull-request PR_ID", "The ID of the pull request to comment on. Attempts to read from CI environment when not set.") do |p|
|
105
|
+
@user_options["pull_request"] = p
|
106
|
+
end
|
107
|
+
|
108
|
+
opts.on("-t", "--token TOKEN", "The Github API token to use for commenting. Defaults to $GITHUB_TOKEN.") do |t|
|
109
|
+
@user_options["token"] = t
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.separator ""
|
113
|
+
opts.separator "Common options:"
|
114
|
+
|
115
|
+
opts.on("--verbose", "Verbose/debug mode") do
|
116
|
+
@user_options["verbose"] = true
|
117
|
+
end
|
118
|
+
|
119
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
120
|
+
puts opts
|
121
|
+
exit
|
122
|
+
end
|
123
|
+
|
124
|
+
opts.on_tail("--version", "Show version") do
|
125
|
+
require 'shiba/version'
|
126
|
+
puts Shiba::VERSION
|
127
|
+
exit
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def default_options
|
133
|
+
defaults = {}
|
134
|
+
|
135
|
+
if ENV['DIFF']
|
136
|
+
defaults['diff'] = ENV['DIFF']
|
137
|
+
end
|
138
|
+
|
139
|
+
if Shiba::Configure.ci?
|
140
|
+
report("Finding default options from CI environment.")
|
141
|
+
|
142
|
+
defaults["file"] = ci_explain_log_path
|
143
|
+
defaults["pull_request"] = ci_pull_request
|
144
|
+
defaults["branch"] = ci_branch if !defaults['diff']
|
145
|
+
end
|
146
|
+
|
147
|
+
defaults["token"] = ENV['GITHUB_TOKEN'] if ENV['GITHUB_TOKEN']
|
148
|
+
|
149
|
+
defaults
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate_log_path
|
153
|
+
return if !options["file"]
|
154
|
+
if !File.exist?(options["file"])
|
155
|
+
error("File not found: '#{options["file"]}'")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def ci_explain_log_path
|
160
|
+
File.join(Shiba.path, 'ci.json')
|
161
|
+
end
|
162
|
+
|
163
|
+
def ci_branch
|
164
|
+
ENV['TRAVIS_PULL_REQUEST_SHA'] || ENV['CIRCLE_SHA1']
|
165
|
+
end
|
166
|
+
|
167
|
+
def ci_pull_request
|
168
|
+
ENV['TRAVIS_PULL_REQUEST'] || circle_pr_number
|
169
|
+
end
|
170
|
+
|
171
|
+
def circle_pr_number
|
172
|
+
return if ENV["CIRCLE_PULL_REQUEST"].nil?
|
173
|
+
number = URI.parse(ENV["CIRCLE_PULL_REQUEST"]).path.split("/").last
|
174
|
+
return if number !~ /\A[0-9]+\Z/
|
175
|
+
|
176
|
+
number
|
177
|
+
end
|
178
|
+
|
179
|
+
def require_option(name, description: nil)
|
180
|
+
return true if options.key?(name)
|
181
|
+
msg = "Required: '#{name}'"
|
182
|
+
msg << ". #{description}" if description
|
183
|
+
error(msg, help: true)
|
184
|
+
end
|
185
|
+
|
186
|
+
def report(message)
|
187
|
+
$stderr.puts message if @user_options["verbose"]
|
188
|
+
end
|
189
|
+
|
190
|
+
def error(message, help: false)
|
191
|
+
@errors << [ message, help ]
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -43,15 +43,32 @@ module Shiba
|
|
43
43
|
"fuzz_table_sizes" => fuzzed_sizes(message),
|
44
44
|
"table" => message["table"],
|
45
45
|
"table_size" => message["table_size"],
|
46
|
-
"result_size" => message["result_size"],
|
47
46
|
"index" => message["index"],
|
48
47
|
"join_to" => message["join_to"],
|
49
48
|
"key_parts" => (message["index_used"] || []).join(','),
|
50
49
|
"size" => message["size"],
|
51
|
-
"formatted_cost" => formatted_cost(message)
|
50
|
+
"formatted_cost" => formatted_cost(message),
|
51
|
+
"formatted_result" => formatted_result(message)
|
52
52
|
}
|
53
53
|
end
|
54
54
|
|
55
|
+
def formatted_result(explain)
|
56
|
+
return nil unless explain['result_bytes'] && explain['result_size']
|
57
|
+
|
58
|
+
bytes = explain['result_bytes']
|
59
|
+
return "%d rows" % explain['result_size'] if bytes == 0
|
60
|
+
|
61
|
+
if bytes < 1000
|
62
|
+
result = "%d bytes" % bytes
|
63
|
+
elsif bytes < 1000000
|
64
|
+
result = "%dkb" % (bytes / 1000)
|
65
|
+
else
|
66
|
+
result = "%.1fmb" % (bytes / 1000000.0 )
|
67
|
+
end
|
68
|
+
|
69
|
+
"%s (%d rows)" % [result, explain['result_size']]
|
70
|
+
end
|
71
|
+
|
55
72
|
def formatted_cost(explain)
|
56
73
|
return nil unless explain["rows_read"] && explain["table_size"]
|
57
74
|
percentage = (explain["rows_read"] / explain["table_size"]) * 100.0;
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module Shiba
|
2
|
+
module Review
|
3
|
+
module Diff
|
4
|
+
|
5
|
+
class Parser
|
6
|
+
# +++ b/config/environments/test.rb
|
7
|
+
FILE_PATTERN = /\A\+\+\+ b\/(.*?)\Z/
|
8
|
+
|
9
|
+
# @@ -177,0 +178 @@ ...
|
10
|
+
# @@ -177,0 +178,5 @@ ...
|
11
|
+
# @@ -21 +24 @@ ...
|
12
|
+
LINE_PATTERN = /\A@@ \-\d+,?\d+? \+(\d+),?(\d+)? @@/
|
13
|
+
|
14
|
+
# via https://developer.github.com/v3/pulls/comments/#create-a-comment
|
15
|
+
# The position value equals the number of lines down from the first "@@" hunk header
|
16
|
+
# in the file you want to add a comment.
|
17
|
+
|
18
|
+
attr_reader :status
|
19
|
+
|
20
|
+
def initialize(file)
|
21
|
+
# Fixme. seems like enumerables should work in general.
|
22
|
+
if !file.respond_to?(:pos)
|
23
|
+
raise StandardError.new("Diff file does not appear to be a seekable IO object.")
|
24
|
+
end
|
25
|
+
@diff = file
|
26
|
+
@status = :new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the file and line numbers that contain inserts. Deletions are ignored.
|
30
|
+
# For simplicity, the default output of git diff is not supported.
|
31
|
+
# The expected format is from 'git diff unified=0'
|
32
|
+
#
|
33
|
+
# Example:
|
34
|
+
# diff = `git diff --unified=0`
|
35
|
+
# Diff.new(StringIO.new(diff))
|
36
|
+
# => [ [ "hello.rb", 1..3 ]
|
37
|
+
# => [ "hello.rb", 7..7 ]
|
38
|
+
# => [ "test.rb", 23..23 ]
|
39
|
+
# => ]
|
40
|
+
def updated_lines
|
41
|
+
io = @diff.each_line
|
42
|
+
path = nil
|
43
|
+
|
44
|
+
found = []
|
45
|
+
|
46
|
+
while true
|
47
|
+
line = io.next
|
48
|
+
if line =~ FILE_PATTERN
|
49
|
+
path = $1
|
50
|
+
end
|
51
|
+
|
52
|
+
if hunk_header?(line)
|
53
|
+
line_numbers = line_numbers_for_destination(line)
|
54
|
+
found << [ path, line_numbers ]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue StopIteration
|
58
|
+
return found
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the position in the diff, after the relevant file header,
|
62
|
+
# that contains the specified file/lineno modification.
|
63
|
+
# Only supports finding the position in the destination / newest version of the file.
|
64
|
+
#
|
65
|
+
# Example:
|
66
|
+
# diff = Diff.new(`git diff`)
|
67
|
+
# diff.find_position("test.rb", 3)
|
68
|
+
# => 5
|
69
|
+
def find_position(path, line_number)
|
70
|
+
io = @diff.each_line # maybe redundant?
|
71
|
+
|
72
|
+
file_header = "+++ b/#{path}\n" # fixme
|
73
|
+
if !io.find_index(file_header)
|
74
|
+
@status = :file_not_found
|
75
|
+
return
|
76
|
+
end
|
77
|
+
|
78
|
+
line = io.peek
|
79
|
+
if !hunk_header?(line)
|
80
|
+
raise StandardError.new("Expected hunk header to be after file header, but got '#{line}'")
|
81
|
+
end
|
82
|
+
|
83
|
+
pos = 0
|
84
|
+
|
85
|
+
while true
|
86
|
+
line = io.next
|
87
|
+
pos += 1
|
88
|
+
|
89
|
+
if file_header?(line)
|
90
|
+
@status = :line_not_found
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
if !hunk_header?(line)
|
95
|
+
next
|
96
|
+
end
|
97
|
+
|
98
|
+
line_numbers = line_numbers_for_destination(line)
|
99
|
+
|
100
|
+
if destination_position = line_numbers.find_index(line_number)
|
101
|
+
@status = :found_position
|
102
|
+
return pos + find_hunk_index(io, destination_position)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
rescue StopIteration
|
106
|
+
@status = :line_not_found
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def find_hunk_index(hunk, pos)
|
112
|
+
line, idx = hunk.with_index.select { |l,idx| !l.start_with?('-') }.take(pos+1).last
|
113
|
+
idx
|
114
|
+
end
|
115
|
+
|
116
|
+
def file_header?(line)
|
117
|
+
line =~ FILE_PATTERN
|
118
|
+
end
|
119
|
+
|
120
|
+
def hunk_header?(line)
|
121
|
+
LINE_PATTERN =~ line
|
122
|
+
end
|
123
|
+
|
124
|
+
def line_numbers_for_destination(diff_line)
|
125
|
+
diff_line =~ LINE_PATTERN
|
126
|
+
line = $1.to_i
|
127
|
+
line_count = ($2 && $2.to_i) || 0
|
128
|
+
line..line+line_count
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class GitDiff
|
133
|
+
|
134
|
+
# Valid options are: "staged", "unstaged", "branch", and "verbose"
|
135
|
+
def initialize(options)
|
136
|
+
@options = options
|
137
|
+
end
|
138
|
+
|
139
|
+
def paths
|
140
|
+
if options['verbose']
|
141
|
+
puts cmd
|
142
|
+
end
|
143
|
+
|
144
|
+
run = "git diff#{cmd} --name-only --diff-filter=d"
|
145
|
+
|
146
|
+
if options[:verbose]
|
147
|
+
$stderr.puts run
|
148
|
+
end
|
149
|
+
result = `#{run}`
|
150
|
+
if $?.exitstatus != 0
|
151
|
+
$stderr.puts result
|
152
|
+
raise Shiba::Error.new "Failed to read changes"
|
153
|
+
end
|
154
|
+
|
155
|
+
result.split("\n")
|
156
|
+
end
|
157
|
+
|
158
|
+
# Differ expects context: 0, ignore_deletions: true
|
159
|
+
def file(context: nil, ignore_deletions: false)
|
160
|
+
differ = "git diff#{cmd}"
|
161
|
+
differ << " --unified=#{context}" if context
|
162
|
+
differ << " --diff-filter=d" if ignore_deletions
|
163
|
+
run(differ)
|
164
|
+
end
|
165
|
+
|
166
|
+
protected
|
167
|
+
|
168
|
+
attr_reader :options
|
169
|
+
|
170
|
+
def run(command)
|
171
|
+
if options[:verbose]
|
172
|
+
$stderr.puts command
|
173
|
+
end
|
174
|
+
|
175
|
+
_, out,_,_ = Open3.popen3(command)
|
176
|
+
out
|
177
|
+
end
|
178
|
+
|
179
|
+
def cmd
|
180
|
+
cmd = case
|
181
|
+
when options["staged"]
|
182
|
+
" --staged"
|
183
|
+
when options["unstaged"]
|
184
|
+
""
|
185
|
+
else
|
186
|
+
commit = " origin/HEAD"
|
187
|
+
commit << "...#{options["branch"]}" if options["branch"]
|
188
|
+
commit
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class FileDiff
|
194
|
+
# +++ b/test/app/app.rb
|
195
|
+
FILE_NAME_PATTERN = /^\+\+\+ b\/(.*?)$/
|
196
|
+
|
197
|
+
def initialize(path)
|
198
|
+
@path = path
|
199
|
+
end
|
200
|
+
|
201
|
+
# Extracts path names from the diff file
|
202
|
+
#
|
203
|
+
# Example:
|
204
|
+
# index ade9b24..661d522 100644
|
205
|
+
# --- a/test/app/app.rb
|
206
|
+
# +++ b/test/app/app.rb
|
207
|
+
# @@ -24,4 +24,4 @@ ActiveRecord::Base...
|
208
|
+
# org = Organization.create!(name: 'test')
|
209
|
+
#
|
210
|
+
# diff.paths
|
211
|
+
# => [ test/app/app.rb ]
|
212
|
+
def paths
|
213
|
+
f = File.open(@path)
|
214
|
+
f.grep(FILE_NAME_PATTERN) { $1 }
|
215
|
+
end
|
216
|
+
|
217
|
+
def file(context: nil, ignore_deletions: nil)
|
218
|
+
warn "Context not supported for file diffs" if context
|
219
|
+
warn "Ignore deletions not supported for file diffs" if ignore_deletions
|
220
|
+
File.open(@path)
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|