ndr_dev_support 1.1.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/.gitignore +9 -0
- data/.rubocop.yml +19 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +12 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/code_safety.yml +91 -0
- data/lib/ndr_dev_support.rb +5 -0
- data/lib/ndr_dev_support/rubocop/executor.rb +28 -0
- data/lib/ndr_dev_support/rubocop/range_augmenter.rb +82 -0
- data/lib/ndr_dev_support/rubocop/range_finder.rb +82 -0
- data/lib/ndr_dev_support/rubocop/reporter.rb +76 -0
- data/lib/ndr_dev_support/tasks.rb +2 -0
- data/lib/ndr_dev_support/version.rb +5 -0
- data/lib/tasks/audit_code.rake +475 -0
- data/lib/tasks/rubocop.rake +63 -0
- data/ndr_dev_support.gemspec +29 -0
- metadata +162 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'English'
|
2
|
+
require 'rubocop'
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
module NdrDevSupport
|
6
|
+
module Rubocop
|
7
|
+
# Produces diffs, and parses from them the file/hunk boundaries
|
8
|
+
class RangeFinder
|
9
|
+
class << self
|
10
|
+
# Use RuboCop to produce a list of all files that should be scanned.
|
11
|
+
def target_files
|
12
|
+
@target_files ||= begin
|
13
|
+
defaults = ::RuboCop::ConfigStore.new
|
14
|
+
::RuboCop::TargetFinder.new(defaults, {}).target_files_in_dir
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def diff_files(files)
|
20
|
+
diff = Array(files).map { |file| git_diff(%(-- "#{file}")) }.join
|
21
|
+
file_change_locations_from diff
|
22
|
+
end
|
23
|
+
|
24
|
+
def diff_head
|
25
|
+
file_change_locations_from git_diff('HEAD')
|
26
|
+
end
|
27
|
+
|
28
|
+
def diff_staged
|
29
|
+
file_change_locations_from git_diff('--staged')
|
30
|
+
end
|
31
|
+
|
32
|
+
def diff_unstaged
|
33
|
+
file_change_locations_from git_diff('')
|
34
|
+
end
|
35
|
+
|
36
|
+
def diff_expr(expr)
|
37
|
+
file_change_locations_from git_diff(expr)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def git_diff(args)
|
43
|
+
diff = `git diff --no-prefix --unified=0 #{Shellwords.escape(args)}`
|
44
|
+
fail "Failed to diff: '#{args}'" unless $CHILD_STATUS.exitstatus.zero?
|
45
|
+
diff
|
46
|
+
end
|
47
|
+
|
48
|
+
def file_changes_hash
|
49
|
+
Hash.new { |hash, file| hash[file] = [] }
|
50
|
+
end
|
51
|
+
|
52
|
+
def file_change_locations_from(diff, changes = file_changes_hash)
|
53
|
+
current_file = nil
|
54
|
+
diff.each_line do |line|
|
55
|
+
if line.start_with?('+++')
|
56
|
+
current_file = line[4..-1].strip
|
57
|
+
elsif line.start_with?('@@')
|
58
|
+
range = hunk_range_from(line)
|
59
|
+
changes[current_file].push(range) unless range.end.zero?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
ruby_files_from changes
|
64
|
+
end
|
65
|
+
|
66
|
+
# Don't report on changes in files that RuboCop won't understand:
|
67
|
+
def ruby_files_from(changes)
|
68
|
+
whitelist = RangeFinder.target_files
|
69
|
+
changes.reject { |file, _ranges| !whitelist.include? File.join(Dir.pwd, file) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def hunk_range_from(line)
|
73
|
+
match_data = line.match(/\A@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/)
|
74
|
+
start_line = match_data[1].to_i
|
75
|
+
new_lines = match_data[2].to_i
|
76
|
+
end_line = new_lines.zero? ? start_line : start_line + new_lines - 1
|
77
|
+
|
78
|
+
start_line..end_line
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
|
3
|
+
module NdrDevSupport
|
4
|
+
module Rubocop
|
5
|
+
# Handles the display of any rubocop output
|
6
|
+
class Reporter
|
7
|
+
HEADER = (('=' * 34) << ' Summary: ' << ('=' * 34)).freeze
|
8
|
+
FOOTER = ('=' * HEADER.length).freeze
|
9
|
+
|
10
|
+
COLOURS = {
|
11
|
+
'refactor' => :yellow,
|
12
|
+
'convention' => :yellow,
|
13
|
+
'warning' => :magenta,
|
14
|
+
'error' => :red,
|
15
|
+
'fatal' => :red
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(offenses)
|
19
|
+
@offenses = offenses
|
20
|
+
end
|
21
|
+
|
22
|
+
# Prints out a report, and returns an appriopriate
|
23
|
+
# exit status for the rake task to terminate with.
|
24
|
+
def report
|
25
|
+
if @offenses.any?
|
26
|
+
print_summary
|
27
|
+
puts
|
28
|
+
print_offenses
|
29
|
+
return @offenses.values.all?(&:empty?)
|
30
|
+
else
|
31
|
+
puts Rainbow('No files scanned.').red
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def colour_severity(severity, initial_only = false)
|
39
|
+
colour = COLOURS[severity]
|
40
|
+
message = initial_only ? severity[0, 1].upcase : severity
|
41
|
+
colour ? Rainbow(message).color(colour) : message
|
42
|
+
end
|
43
|
+
|
44
|
+
def print_summary
|
45
|
+
puts HEADER
|
46
|
+
@offenses.each do |filename, file_offenses|
|
47
|
+
puts format(' * %s has %d relevant offence%s.',
|
48
|
+
Rainbow(filename).fg(file_offenses.empty? ? :green : :red),
|
49
|
+
file_offenses.length,
|
50
|
+
file_offenses.one? ? '' : 's'
|
51
|
+
)
|
52
|
+
end
|
53
|
+
puts FOOTER
|
54
|
+
end
|
55
|
+
|
56
|
+
def print_offenses
|
57
|
+
@offenses.each do |filename, file_offenses|
|
58
|
+
file_offenses.each do |offense|
|
59
|
+
puts formatted_offense(filename, offense)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def formatted_offense(filename, offense)
|
65
|
+
format('%s:%d:%d: %s: %s %s',
|
66
|
+
Rainbow(filename).cyan,
|
67
|
+
offense['location']['line'],
|
68
|
+
offense['location']['column'],
|
69
|
+
colour_severity(offense['severity'], true),
|
70
|
+
offense['message'],
|
71
|
+
colour_severity(offense['severity'])
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,475 @@
|
|
1
|
+
SAFETY_FILE =
|
2
|
+
if File.exist?('code_safety.yml')
|
3
|
+
'code_safety.yml'
|
4
|
+
elsif defined?(Rails)
|
5
|
+
Rails.root.join('config', 'code_safety.yml')
|
6
|
+
else
|
7
|
+
'code_safety.yml'
|
8
|
+
end
|
9
|
+
|
10
|
+
# Temporary overrides to only audit external access files
|
11
|
+
SAFETY_REPOS = [['/svn/era', '/svn/extra/era/external-access']]
|
12
|
+
|
13
|
+
require 'yaml'
|
14
|
+
|
15
|
+
# Parameter max_print is number of entries to print before truncating output
|
16
|
+
# (negative value => print all)
|
17
|
+
def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, show_in_priority = false, user_name = 'usr')
|
18
|
+
puts 'Running source code safety audit script.'
|
19
|
+
puts
|
20
|
+
|
21
|
+
max_print = 1_000_000 if max_print < 0
|
22
|
+
safety_cfg = File.exist?(SAFETY_FILE) ? YAML.load_file(SAFETY_FILE) : {}
|
23
|
+
file_safety = safety_cfg['file safety']
|
24
|
+
if file_safety.nil?
|
25
|
+
puts "Creating new 'file safety' block in #{SAFETY_FILE}"
|
26
|
+
safety_cfg['file safety'] = file_safety = {}
|
27
|
+
end
|
28
|
+
file_safety.each do |_k, v|
|
29
|
+
rev = v['safe_revision']
|
30
|
+
v['safe_revision'] = rev.to_s if rev.is_a?(Integer)
|
31
|
+
end
|
32
|
+
orig_count = file_safety.size
|
33
|
+
|
34
|
+
safety_repo = trunk_repo = get_trunk_repo
|
35
|
+
|
36
|
+
# TODO: below is broken for git-svn
|
37
|
+
# Is it needed?
|
38
|
+
|
39
|
+
SAFETY_REPOS.each do |suffix, alt|
|
40
|
+
# Temporarily override to only audit a different file list
|
41
|
+
if safety_repo.end_with?(suffix)
|
42
|
+
safety_repo = safety_repo[0...-suffix.length] + alt
|
43
|
+
break
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
if ignore_new
|
48
|
+
puts "Not checking for new files in #{safety_repo}"
|
49
|
+
else
|
50
|
+
puts "Checking for new files in #{safety_repo}"
|
51
|
+
new_files = get_new_files(safety_repo)
|
52
|
+
# Ignore subdirectories, and exclude code_safety.yml by default.
|
53
|
+
new_files.delete_if { |f| f =~ /[\/\\]$/ || f == SAFETY_FILE }
|
54
|
+
new_files.each do |f|
|
55
|
+
next if file_safety.key?(f)
|
56
|
+
file_safety[f] = {
|
57
|
+
'comments' => nil,
|
58
|
+
'reviewed_by' => nil,
|
59
|
+
'safe_revision' => nil }
|
60
|
+
end
|
61
|
+
File.open(SAFETY_FILE, 'w') do |file|
|
62
|
+
# Consistent file diffs, as ruby preserves Hash insertion order since v1.9
|
63
|
+
safety_cfg['file safety'] = Hash[file_safety.sort]
|
64
|
+
YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
|
65
|
+
end
|
66
|
+
end
|
67
|
+
puts "Updating latest revisions for #{file_safety.size} files"
|
68
|
+
set_last_changed_revision(trunk_repo, file_safety, file_safety.keys)
|
69
|
+
puts "\nSummary:"
|
70
|
+
puts "Number of files originally in #{SAFETY_FILE}: #{orig_count}"
|
71
|
+
puts "Number of new files added: #{file_safety.size - orig_count}"
|
72
|
+
|
73
|
+
# Now generate statistics:
|
74
|
+
unknown = file_safety.values.select { |x| x['safe_revision'].nil? }
|
75
|
+
unsafe = file_safety.values.select do |x|
|
76
|
+
!x['safe_revision'].nil? && x['safe_revision'] != -1 &&
|
77
|
+
x['last_changed_rev'] != x['safe_revision'] &&
|
78
|
+
!(x['last_changed_rev'] =~ /^[0-9]+$/ && x['safe_revision'] =~ /^[0-9]+$/ &&
|
79
|
+
x['last_changed_rev'].to_i < x['safe_revision'].to_i)
|
80
|
+
end
|
81
|
+
puts "Number of files with no safe version: #{unknown.size}"
|
82
|
+
puts "Number of files which are no longer safe: #{unsafe.size}"
|
83
|
+
puts
|
84
|
+
printed = []
|
85
|
+
# We also print a third category: ones which are no longer in the repository
|
86
|
+
file_list =
|
87
|
+
if show_in_priority
|
88
|
+
file_safety.sort_by { |_k, v| v.nil? ? -100 : v['last_changed_rev'].to_i }.map(&:first)
|
89
|
+
else
|
90
|
+
file_safety.keys.sort
|
91
|
+
end
|
92
|
+
|
93
|
+
file_list.each do |f|
|
94
|
+
if print_file_safety(file_safety, trunk_repo, f, false, printed.size >= max_print)
|
95
|
+
printed << f
|
96
|
+
end
|
97
|
+
end
|
98
|
+
puts "... and #{printed.size - max_print} others" if printed.size > max_print
|
99
|
+
if show_diffs
|
100
|
+
puts
|
101
|
+
printed.each do |f|
|
102
|
+
print_file_diffs(file_safety, trunk_repo, f, user_name)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns `true` unless there are pending reviews:
|
107
|
+
unsafe.length.zero? && unknown.length.zero?
|
108
|
+
end
|
109
|
+
|
110
|
+
# Print summary details of a file's known safety
|
111
|
+
# If not verbose, only prints details for unsafe files
|
112
|
+
# Returns true if anything printed (or would have been printed if silent),
|
113
|
+
# or false otherwise.
|
114
|
+
def print_file_safety(file_safety, repo, fname, verbose = false, silent = false)
|
115
|
+
msg = "#{fname}\n "
|
116
|
+
entry = file_safety[fname]
|
117
|
+
msg += 'File not in audit list' if entry.nil?
|
118
|
+
|
119
|
+
if entry['safe_revision'].nil?
|
120
|
+
msg += 'No safe revision known'
|
121
|
+
msg += ", last changed #{entry['last_changed_rev']}" unless entry['last_changed_rev'].nil?
|
122
|
+
else
|
123
|
+
repolatest = entry['last_changed_rev'] # May have been prepopulated en mass
|
124
|
+
msg += 'Not in repository: ' if entry['last_changed_rev'] == -1
|
125
|
+
if (repolatest != entry['safe_revision']) &&
|
126
|
+
!(repolatest =~ /^[0-9]+$/ && entry['safe_revision'] =~ /^[0-9]+$/ &&
|
127
|
+
repolatest.to_i < entry['safe_revision'].to_i)
|
128
|
+
# (Allow later revisions to be treated as safe for svn)
|
129
|
+
msg += "No longer safe since revision #{repolatest}: "
|
130
|
+
else
|
131
|
+
return false unless verbose
|
132
|
+
msg += 'Safe: '
|
133
|
+
end
|
134
|
+
msg += "revision #{entry['safe_revision']} reviewed by #{entry['reviewed_by']}"
|
135
|
+
end
|
136
|
+
msg += "\n Comments: #{entry['comments']}" if entry['comments']
|
137
|
+
puts msg unless silent
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def flag_file_as_safe(release, reviewed_by, comments, f)
|
142
|
+
safety_cfg = YAML.load_file(SAFETY_FILE)
|
143
|
+
file_safety = safety_cfg['file safety']
|
144
|
+
|
145
|
+
unless File.exist?(f)
|
146
|
+
abort("Error: Unable to flag non-existent file as safe: #{f}")
|
147
|
+
end
|
148
|
+
unless file_safety.key?(f)
|
149
|
+
file_safety[f] = {
|
150
|
+
'comments' => nil,
|
151
|
+
'reviewed_by' => :dummy, # dummy value, will be overwritten
|
152
|
+
'safe_revision' => nil }
|
153
|
+
end
|
154
|
+
entry = file_safety[f]
|
155
|
+
entry_orig = entry.dup
|
156
|
+
if comments.to_s.length > 0 && entry['comments'] != comments
|
157
|
+
entry['comments'] = if entry['comments'].to_s.empty?
|
158
|
+
comments
|
159
|
+
else
|
160
|
+
"#{entry['comments']}#{'.' unless entry['comments'].end_with?('.')} Revision #{release}: #{comments}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
if entry['safe_revision']
|
164
|
+
unless release
|
165
|
+
abort("Error: File already has safe revision #{entry['safe_revision']}: #{f}")
|
166
|
+
end
|
167
|
+
if release.is_a?(Integer) && release < entry['safe_revision']
|
168
|
+
puts("Warning: Rolling back safe revision from #{entry['safe_revision']} to #{release} for #{f}")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
entry['safe_revision'] = release
|
172
|
+
entry['reviewed_by'] = reviewed_by
|
173
|
+
if entry == entry_orig
|
174
|
+
puts "No changes when updating safe_revision to #{release || '[none]'} for #{f}"
|
175
|
+
else
|
176
|
+
File.open(SAFETY_FILE, 'w') do |file|
|
177
|
+
# Consistent file diffs, as ruby preserves Hash insertion order since v1.9
|
178
|
+
safety_cfg['file safety'] = Hash[file_safety.sort]
|
179
|
+
YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
|
180
|
+
end
|
181
|
+
puts "Updated safe_revision to #{release || '[none]'} for #{f}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Determine the type of repository
|
186
|
+
def repository_type
|
187
|
+
@repository_type ||= if Dir.exist?('.svn') || system("svn info . > /dev/null 2>&1")
|
188
|
+
'svn'
|
189
|
+
elsif Dir.exist?('.git') && open('.git/config').grep(/svn/).any?
|
190
|
+
'git-svn'
|
191
|
+
elsif Dir.exist?('.git') && open('.git/config').grep(/git/).any?
|
192
|
+
'git'
|
193
|
+
else
|
194
|
+
'not known'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def get_trunk_repo
|
199
|
+
case repository_type
|
200
|
+
when 'svn'
|
201
|
+
repo_info = %x[svn info]
|
202
|
+
puts 'svn case'
|
203
|
+
return repo_info.split("\n").select { |x| x =~ /^URL: / }.collect { |x| x[5..-1] }.first
|
204
|
+
when 'git-svn'
|
205
|
+
puts 'git-svn case'
|
206
|
+
repo_info = %x[git svn info]
|
207
|
+
return repo_info.split("\n").select { |x| x =~ /^URL: / }.collect { |x| x[5..-1] }.first
|
208
|
+
when 'git'
|
209
|
+
puts 'git case'
|
210
|
+
repo_info = %x[git remote -v]
|
211
|
+
return repo_info.split("\n").first[7..-9]
|
212
|
+
else
|
213
|
+
return 'Information not available. Unknown repository type'
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def get_new_files(safety_repo)
|
218
|
+
case repository_type
|
219
|
+
when 'svn', 'git-svn'
|
220
|
+
%x[svn ls -R "#{safety_repo}"].split("\n")
|
221
|
+
when 'git'
|
222
|
+
#%x[git ls-files --modified].split("\n")
|
223
|
+
%x[git ls-files].split("\n")
|
224
|
+
|
225
|
+
# TODO: Below is for remote repository - for testing use local files
|
226
|
+
#new_files = %x[git ls-files --modified #{safety_repo}].split("\n")
|
227
|
+
# TODO: Do we need the --modified option?
|
228
|
+
#new_files = %x[git ls-files --modified].split("\n")
|
229
|
+
else
|
230
|
+
[]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Fill in the latest changed revisions in a file safety map.
|
235
|
+
# (Don't write this data to the YAML file, as it is intrinsic to the SVN
|
236
|
+
# repository.)
|
237
|
+
def set_last_changed_revision(repo, file_safety, fnames)
|
238
|
+
dot_freq = (file_safety.size / 40.0).ceil # Print up to 40 progress dots
|
239
|
+
case repository_type
|
240
|
+
when 'git'
|
241
|
+
fnames = file_safety.keys if fnames.nil?
|
242
|
+
|
243
|
+
fnames.each_with_index do |f, i|
|
244
|
+
info = %x[git log -n 1 #{f}].split("\n").first[7..-1]
|
245
|
+
if info.nil? || info.empty?
|
246
|
+
file_safety[f]['last_changed_rev'] = -1
|
247
|
+
else
|
248
|
+
file_safety[f]['last_changed_rev'] = info
|
249
|
+
end
|
250
|
+
# Show progress
|
251
|
+
print '.' if (i % dot_freq) == 0
|
252
|
+
end
|
253
|
+
puts
|
254
|
+
when 'git-svn', 'svn'
|
255
|
+
fnames = file_safety.keys if fnames.nil?
|
256
|
+
|
257
|
+
fnames.each_with_index do |f, i|
|
258
|
+
last_revision = get_last_changed_revision(repo, f)
|
259
|
+
if last_revision.nil? || last_revision.empty?
|
260
|
+
file_safety[f]['last_changed_rev'] = -1
|
261
|
+
else
|
262
|
+
file_safety[f]['last_changed_rev'] = last_revision
|
263
|
+
end
|
264
|
+
# Show progress
|
265
|
+
print '.' if (i % dot_freq) == 0
|
266
|
+
end
|
267
|
+
puts
|
268
|
+
# NOTE: Do we need the following for retries?
|
269
|
+
# if retries && result.size != fnames.size && fnames.size > 1
|
270
|
+
# # At least one invalid (deleted file --> subsequent arguments ignored)
|
271
|
+
# # Try each file individually
|
272
|
+
# # (It would probably be safe to continue from the extra_info.size argument)
|
273
|
+
# puts "Retrying (got #{result.size}, expected #{fnames.size})" if debug >= 2
|
274
|
+
# result = []
|
275
|
+
# fnames.each{ |f|
|
276
|
+
# result += svn_info_entries([f], repo, false, debug)
|
277
|
+
# }
|
278
|
+
# end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Return the last changed revision
|
283
|
+
def get_last_changed_revision(repo, fname)
|
284
|
+
case repository_type
|
285
|
+
when 'git'
|
286
|
+
%x[git log -n 1 "#{fname}"].split("\n").first[7..-1]
|
287
|
+
when 'git-svn', 'svn'
|
288
|
+
begin
|
289
|
+
svn_info = %x[svn info -r head "#{repo}/#{fname}"]
|
290
|
+
rescue
|
291
|
+
puts 'we have an error in the svn info line'
|
292
|
+
end
|
293
|
+
begin
|
294
|
+
svn_info.match('Last Changed Rev: ([0-9]*)').to_a[1]
|
295
|
+
rescue
|
296
|
+
puts 'We have an error in getting the revision'
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Get mime type. Note that Git does not have this information
|
302
|
+
def get_mime_type(repo, fname)
|
303
|
+
case repository_type
|
304
|
+
when 'git'
|
305
|
+
'Git does not provide mime types'
|
306
|
+
when 'git-svn', 'svn'
|
307
|
+
%x[svn propget svn:mime-type "#{repo}/#{fname}"].chomp
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# # Print file diffs, for code review
|
312
|
+
def print_file_diffs(file_safety, repo, fname, user_name)
|
313
|
+
entry = file_safety[fname]
|
314
|
+
repolatest = entry['last_changed_rev']
|
315
|
+
safe_revision = entry['safe_revision']
|
316
|
+
|
317
|
+
if safe_revision.nil?
|
318
|
+
first_revision = set_safe_revision
|
319
|
+
print_repo_file_diffs(repolatest, repo, fname, user_name, first_revision)
|
320
|
+
else
|
321
|
+
|
322
|
+
rev = get_last_changed_revision(repo, fname)
|
323
|
+
if rev
|
324
|
+
mime = get_mime_type(repo, fname)
|
325
|
+
end
|
326
|
+
|
327
|
+
print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision) if repolatest != safe_revision
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Returns first commit for git and 0 for svn in order to be used to display
|
332
|
+
# new files. Called from print_file_diffs
|
333
|
+
def set_safe_revision
|
334
|
+
case repository_type
|
335
|
+
when 'git'
|
336
|
+
%x[git rev-list --max-parents=0 HEAD].chomp
|
337
|
+
when 'git-svn', 'svn'
|
338
|
+
0
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision)
|
343
|
+
require 'open3'
|
344
|
+
cmd = nil
|
345
|
+
case repository_type
|
346
|
+
when 'git'
|
347
|
+
cmd = ['git', '--no-pager', 'diff', '-b', "#{safe_revision}..#{repolatest}", fname]
|
348
|
+
when 'git-svn', 'svn'
|
349
|
+
cmd = ['svn', 'diff', '-r', "#{safe_revision.to_i}:#{repolatest.to_i}", '-x', '-b', "#{repo}/#{fname}"]
|
350
|
+
end
|
351
|
+
if cmd
|
352
|
+
puts(cmd.join(' '))
|
353
|
+
stdout_and_err_str, status = Open3.capture2e(*cmd)
|
354
|
+
puts(stdout_and_err_str)
|
355
|
+
else
|
356
|
+
puts 'Unknown repo'
|
357
|
+
end
|
358
|
+
|
359
|
+
puts %(To flag the changes to this file as safe, run:)
|
360
|
+
puts %( rake audit:safe release=#{repolatest} file=#{fname} reviewed_by=#{user_name} comments="")
|
361
|
+
puts
|
362
|
+
end
|
363
|
+
|
364
|
+
def release_valid?(release)
|
365
|
+
case repository_type
|
366
|
+
when 'svn', 'git-svn'
|
367
|
+
release =~ /\A[0-9][0-9]*\Z/
|
368
|
+
when 'git'
|
369
|
+
release =~ /\A[0-9a-f]{40}\Z/
|
370
|
+
else
|
371
|
+
false
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def get_release
|
376
|
+
release = ENV['release']
|
377
|
+
release = nil if release == '0'
|
378
|
+
case repository_type
|
379
|
+
when 'svn', 'git-svn'
|
380
|
+
release.to_i
|
381
|
+
when 'git'
|
382
|
+
release
|
383
|
+
else
|
384
|
+
''
|
385
|
+
end
|
386
|
+
release
|
387
|
+
end
|
388
|
+
|
389
|
+
def clean_working_copy?
|
390
|
+
case repository_type
|
391
|
+
when 'svn'
|
392
|
+
system('svn status | grep -q [^AMCDG]')
|
393
|
+
when 'git', 'git-svn'
|
394
|
+
system('git diff --quiet HEAD')
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
namespace :audit do
|
399
|
+
desc "Audit safety of source code.
|
400
|
+
Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [reviewed_by=usr]
|
401
|
+
|
402
|
+
File #{SAFETY_FILE} lists the safety and revision information
|
403
|
+
of the era source code. This task updates the list, and [TODO] warns about
|
404
|
+
files which have changed since they were last verified as safe."
|
405
|
+
task(:code) do
|
406
|
+
puts 'Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [show_in_priority=false|true] [reviewed_by=usr]'
|
407
|
+
puts "This is a #{repository_type} repository"
|
408
|
+
|
409
|
+
ignore_new = (ENV['ignore_new'].to_s =~ /\Atrue\Z/i)
|
410
|
+
show_diffs = (ENV['show_diffs'].to_s =~ /\Atrue\Z/i)
|
411
|
+
show_in_priority = (ENV['show_in_priority'].to_s =~ /\Atrue\Z/i)
|
412
|
+
max_print = ENV['max_print'] =~ /\A-?[0-9][0-9]*\Z/ ? ENV['max_print'].to_i : 20
|
413
|
+
reviewer = ENV['reviewed_by']
|
414
|
+
|
415
|
+
all_safe = audit_code_safety(max_print, ignore_new, show_diffs, show_in_priority, reviewer)
|
416
|
+
|
417
|
+
unless show_diffs
|
418
|
+
puts 'To show file diffs, run: rake audit:code max_print=-1 show_diffs=true'
|
419
|
+
end
|
420
|
+
|
421
|
+
exit(1) unless all_safe
|
422
|
+
end
|
423
|
+
|
424
|
+
desc "Flag a source file as safe.
|
425
|
+
|
426
|
+
Usage:
|
427
|
+
Flag as safe: rake audit:safe release=revision reviewed_by=usr [comments=...] file=f
|
428
|
+
Needs review: rake audit:safe release=0 [comments=...] file=f"
|
429
|
+
task(:safe) do
|
430
|
+
release = get_release
|
431
|
+
|
432
|
+
required_fields = %w(release file)
|
433
|
+
required_fields << 'reviewed_by' if release # 'Needs review' doesn't need a reviewer
|
434
|
+
missing = required_fields.collect { |f| (f if ENV[f].to_s.empty? || (f == 'reviewed_by' && ENV[f] == 'usr')) }.compact # Avoid accidental missing username
|
435
|
+
unless missing.empty?
|
436
|
+
puts 'Usage: rake audit:safe release=revision reviewed_by=usr [comments=...] file=f'
|
437
|
+
puts 'or, to flag a file for review: rake audit:safe release=0 [comments=...] file=f'
|
438
|
+
abort("Error: Missing required argument(s): #{missing.join(', ')}")
|
439
|
+
end
|
440
|
+
|
441
|
+
unless release.nil? || release_valid?(release)
|
442
|
+
puts 'Usage: rake audit:safe release=revision reviewed_by=usr [comments=...] file=f'
|
443
|
+
puts 'or, to flag a file for review: rake audit:safe release=0 [comments=...] file=f'
|
444
|
+
abort("Error: Invalid release: #{ENV['release']}")
|
445
|
+
end
|
446
|
+
|
447
|
+
flag_file_as_safe(release, ENV['reviewed_by'], ENV['comments'], ENV['file'])
|
448
|
+
end
|
449
|
+
|
450
|
+
desc 'Wraps audit:code, and stops if any review is pending/stale.'
|
451
|
+
task(:ensure_safe) do
|
452
|
+
abort('You have local changes, cannot verify code safety!') unless clean_working_copy?
|
453
|
+
|
454
|
+
puts 'Checking code safety...'
|
455
|
+
|
456
|
+
begin
|
457
|
+
begin
|
458
|
+
$stdout = $stderr = StringIO.new
|
459
|
+
Rake::Task['audit:code'].invoke
|
460
|
+
ensure
|
461
|
+
$stdout, $stderr = STDOUT, STDERR
|
462
|
+
end
|
463
|
+
rescue SystemExit => ex
|
464
|
+
puts '=============================================================='
|
465
|
+
puts 'Code safety review of some files are not up-to-date; aborting!'
|
466
|
+
puts ' - to review the files in question, run: rake audit:code'
|
467
|
+
puts '=============================================================='
|
468
|
+
|
469
|
+
raise ex
|
470
|
+
end
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# Prevent building of un-reviewed gems:
|
475
|
+
task build: :'audit:ensure_safe'
|