ndr_dev_support 2.1.2 → 3.0.0

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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +23 -4
  3. data/README.md +24 -31
  4. data/code_safety.yml +99 -19
  5. data/lib/ndr_dev_support/daemon/ci_server.rb +94 -0
  6. data/lib/ndr_dev_support/daemon/stoppable.rb +111 -0
  7. data/lib/ndr_dev_support/integration_testing.rb +25 -3
  8. data/lib/ndr_dev_support/integration_testing/drivers/chrome.rb +9 -0
  9. data/lib/ndr_dev_support/integration_testing/drivers/chrome_headless.rb +17 -0
  10. data/lib/ndr_dev_support/integration_testing/drivers/firefox.rb +9 -0
  11. data/lib/ndr_dev_support/integration_testing/drivers/poltergeist.rb +15 -0
  12. data/lib/ndr_dev_support/integration_testing/drivers/switchable.rb +21 -0
  13. data/lib/ndr_dev_support/integration_testing/dsl.rb +62 -0
  14. data/lib/ndr_dev_support/rake_ci/brakeman_helper.rb +48 -0
  15. data/lib/ndr_dev_support/rake_ci/concerns/commit_metadata_persistable.rb +49 -0
  16. data/lib/ndr_dev_support/rake_ci/simple_cov_helper.rb +26 -0
  17. data/lib/ndr_dev_support/slack_message_publisher.rb +48 -0
  18. data/lib/ndr_dev_support/tasks.rb +12 -0
  19. data/lib/ndr_dev_support/version.rb +1 -1
  20. data/lib/tasks/audit_code.rake +94 -92
  21. data/lib/tasks/ci/brakeman.rake +54 -0
  22. data/lib/tasks/ci/bundle_audit.rake +45 -0
  23. data/lib/tasks/ci/bundle_install.rake +21 -0
  24. data/lib/tasks/ci/housekeep.rake +8 -0
  25. data/lib/tasks/ci/linguist.rake +42 -0
  26. data/lib/tasks/ci/notes.rake +30 -0
  27. data/lib/tasks/ci/prometheus.rake +50 -0
  28. data/lib/tasks/ci/rugged.rake +39 -0
  29. data/lib/tasks/ci/server.rake +12 -0
  30. data/lib/tasks/ci/simplecov.rake +37 -0
  31. data/lib/tasks/ci/slack.rake +30 -0
  32. data/lib/tasks/ci/stats.rake +24 -0
  33. data/ndr_dev_support.gemspec +15 -3
  34. metadata +164 -18
  35. data/lib/ndr_dev_support/integration_testing/capybara.rb +0 -3
  36. data/lib/ndr_dev_support/integration_testing/connection_sharing.rb +0 -47
  37. data/lib/ndr_dev_support/integration_testing/poltergeist.rb +0 -39
  38. data/lib/ndr_dev_support/integration_testing/screenshot.rb +0 -51
@@ -1,3 +1,25 @@
1
- require 'ndr_dev_support/integration_testing/capybara'
2
- require 'ndr_dev_support/integration_testing/poltergeist'
3
- require 'ndr_dev_support/integration_testing/screenshot'
1
+ # Set up basic capybara:
2
+ require 'capybara/rails'
3
+ ActionDispatch::IntegrationTest.include(Capybara::DSL)
4
+
5
+ # Set up basic screenshotting capability:
6
+ require 'capybara-screenshot'
7
+ if defined?(MiniTest)
8
+ require 'capybara-screenshot/minitest'
9
+ ActionDispatch::IntegrationTest.include(Capybara::Screenshot::MiniTestPlugin)
10
+ else
11
+ require 'capybara-screenshot/testunit'
12
+ end
13
+
14
+ # Include our custom DSL extensions, that also cover screenshotting:
15
+ require 'ndr_dev_support/integration_testing/dsl'
16
+
17
+ # These are all the drivers we have capybara / screenshot support for:
18
+ require 'ndr_dev_support/integration_testing/drivers/chrome'
19
+ require 'ndr_dev_support/integration_testing/drivers/chrome_headless'
20
+ require 'ndr_dev_support/integration_testing/drivers/firefox'
21
+ require 'ndr_dev_support/integration_testing/drivers/poltergeist'
22
+ require 'ndr_dev_support/integration_testing/drivers/switchable'
23
+
24
+ Capybara.default_driver = :switchable
25
+ Capybara.javascript_driver = :switchable
@@ -0,0 +1,9 @@
1
+ require 'selenium-webdriver'
2
+
3
+ Capybara.register_driver(:chrome) do |app|
4
+ Capybara::Selenium::Driver.new app, browser: :chrome
5
+ end
6
+
7
+ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
8
+ driver.browser.save_screenshot(path)
9
+ end
@@ -0,0 +1,17 @@
1
+ require 'selenium-webdriver'
2
+
3
+ Capybara.register_driver(:chrome_headless) do |app|
4
+ capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
5
+ 'chromeOptions' => { args: %w[headless disable-gpu] }
6
+ )
7
+
8
+ Capybara::Selenium::Driver.new(
9
+ app,
10
+ browser: :chrome,
11
+ desired_capabilities: capabilities
12
+ )
13
+ end
14
+
15
+ Capybara::Screenshot.register_driver(:chrome_headless) do |driver, path|
16
+ driver.browser.save_screenshot(path)
17
+ end
@@ -0,0 +1,9 @@
1
+ require 'selenium-webdriver'
2
+
3
+ Capybara.register_driver(:firefox) do |app|
4
+ Capybara::Selenium::Driver.new app, browser: :firefox
5
+ end
6
+
7
+ Capybara::Screenshot.register_driver(:firefox) do |driver, path|
8
+ driver.browser.save_screenshot(path)
9
+ end
@@ -0,0 +1,15 @@
1
+ # The driver for PhantomJS is poltergeist:
2
+ require 'capybara/poltergeist'
3
+
4
+ Capybara.register_driver(:poltergeist) do |app|
5
+ options = { phantomjs_options: ['--proxy-type=none'], timeout: 60 }
6
+
7
+ options.merge!(debug: true, inspector: true) if ENV['DEBUG_PHANTOM_JS']
8
+
9
+ Capybara::Poltergeist::Driver.new(app, options)
10
+ end
11
+
12
+ Capybara::Screenshot.register_driver(:poltergeist) do |driver, path|
13
+ # Take full-height screenshots, rather than just capturing the viewport:
14
+ driver.render(path, full: true)
15
+ end
@@ -0,0 +1,21 @@
1
+ # A meta-driver that allows the driver to be set using the `INTEGRATION_DRIVER`
2
+ # environment variable (e.g. for a CI matrix), assuming that driver has been pre-registered
3
+ # with Capybara.
4
+
5
+ # Although the aim is to move to Chrome headless, we keep poltergeist as the default
6
+ # driver for now. For motivation behind not changing immediately, see the "Differences
7
+ # between Poltergeist and Selenium" section of:
8
+ #
9
+ # https://about.gitlab.com/2017/12/19/moving-to-headless-chrome/
10
+ #
11
+ Capybara.register_driver(:switchable) do |app|
12
+ choice = ENV.fetch('INTEGRATION_DRIVER', 'poltergeist').to_sym
13
+
14
+ Capybara.drivers.fetch(choice).call(app)
15
+ end
16
+
17
+ Capybara::Screenshot.register_driver(:switchable) do |driver, path|
18
+ choice = ENV.fetch('INTEGRATION_DRIVER', 'poltergeist').to_sym
19
+
20
+ Capybara::Screenshot.registered_drivers.fetch(choice).call(driver, path)
21
+ end
@@ -0,0 +1,62 @@
1
+ require 'capybara/rails'
2
+
3
+ module NdrDevSupport
4
+ module IntegrationTesting
5
+ # Additional integration testing DSL:
6
+ module DSL
7
+ # Instruct the headless browser to clear its session:
8
+ def clear_headless_session!
9
+ page.driver.reset!
10
+ end
11
+
12
+ # Get the headless browser to delete all of the cookies
13
+ # for the current page without resetting:
14
+ def delete_all_cookies!
15
+ page.driver.cookies.each_key do |name|
16
+ page.driver.remove_cookie(name)
17
+ end
18
+ end
19
+
20
+ # Wrap up interacting with modals. The assumption is that the modal
21
+ # should be gone one the interaction is complete (as this is a good
22
+ # proxy for a triggered AJAX request to have completed, and therefore
23
+ # a signal for capybara to wait for); if this is not the case, pass
24
+ # `remain: true` to signal that the modal should remain active.
25
+ def within_modal(selector: '#modal', remain: false)
26
+ within(selector) { yield }
27
+
28
+ message = "modal was #{'not ' unless remain} expected to remain visible!"
29
+ assert(remain ? has_selector?(selector) : has_no_selector?(selector), message)
30
+ end
31
+
32
+ # Adds variant of Capybara's #within_window method, that doesn't return
33
+ # to the preview window on an exception. This allows us to screenshot
34
+ # a popup automatically if a test errors/fails whilst it has focus.
35
+ module SessionExtensions
36
+ def within_screenshot_compatible_window(window_or_proc)
37
+ original = current_window
38
+
39
+ case window_or_proc
40
+ when Capybara::Window
41
+ switch_to_window(window_or_proc) unless original == window_or_proc
42
+ when Proc
43
+ switch_to_window { window_or_proc.call }
44
+ else
45
+ raise ArgumentError, 'Unsupported window type!'
46
+ end
47
+
48
+ scopes << nil
49
+ yield
50
+ @scopes.pop
51
+ switch_to_window(original)
52
+ end
53
+ end
54
+
55
+ Capybara::Session.include(SessionExtensions)
56
+
57
+ delegate :within_screenshot_compatible_window, to: :page
58
+ end
59
+ end
60
+ end
61
+
62
+ ActionDispatch::IntegrationTest.include(NdrDevSupport::IntegrationTesting::DSL)
@@ -0,0 +1,48 @@
1
+ module NdrDevSupport
2
+ module RakeCI
3
+ # Brakeman helper
4
+ class BrakemanHelper
5
+ require 'set'
6
+ require 'brakeman'
7
+ require_relative 'concerns/commit_metadata_persistable'
8
+
9
+ include CommitMetadataPersistable
10
+
11
+ attr_reader :new_fingerprints, :old_fingerprints
12
+
13
+ def run
14
+ @tracker = ::Brakeman.run(app_path: '.')
15
+
16
+ last_commit_fingerprints = load_last_commit_data
17
+ if last_commit_fingerprints
18
+ @new_fingerprints = current_fingerprints - last_commit_fingerprints
19
+ @old_fingerprints = last_commit_fingerprints - current_fingerprints
20
+ else
21
+ @new_fingerprints = @old_fingerprints = Set.new
22
+ end
23
+ end
24
+
25
+ def warnings
26
+ @tracker.warnings
27
+ end
28
+
29
+ def warning_counts_by_confidence
30
+ return @warning_counts_by_confidence if @warning_counts_by_confidence
31
+
32
+ @warning_counts_by_confidence = {}
33
+ warnings.group_by(&:confidence).each do |confidence, grouped_warnings|
34
+ @warning_counts_by_confidence[confidence] = grouped_warnings.count
35
+ end
36
+ @warning_counts_by_confidence
37
+ end
38
+
39
+ def current_fingerprints
40
+ @current_fingerprints ||= warnings.map(&:fingerprint).to_set
41
+ end
42
+
43
+ def save_current_fingerprints
44
+ save_current_commit_data(current_fingerprints)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/inflector'
3
+ require 'yaml'
4
+
5
+ # Provides methods relating to persisting commit metadata
6
+ module CommitMetadataPersistable
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ attr_accessor :commit
11
+ end
12
+
13
+ private
14
+
15
+ def load_last_commit_data
16
+ hash = YAML.load_file(filename)
17
+
18
+ if commit.parents.map(&:oid).include?(hash[:commit])
19
+ # payload from parent commit
20
+ hash[:payload]
21
+ end
22
+ rescue Errno::ENOENT
23
+ nil
24
+ end
25
+
26
+ def load_current_commit_data
27
+ hash = YAML.load_file(filename)
28
+
29
+ if commit.oid == hash[:commit]
30
+ # payload from parent commit
31
+ hash[:payload]
32
+ end
33
+ rescue Errno::ENOENT
34
+ nil
35
+ end
36
+
37
+ def save_current_commit_data(data)
38
+ hash = { commit: commit.oid, payload: data }
39
+ File.write(filename, YAML.dump(hash))
40
+ end
41
+
42
+ def filename
43
+ "rake_ci.#{name}.yml"
44
+ end
45
+
46
+ def name
47
+ self.class.name.demodulize.underscore.sub(/_helper\z/, '')
48
+ end
49
+ end
@@ -0,0 +1,26 @@
1
+ module NdrDevSupport
2
+ module RakeCI
3
+ # This helper persists the SimpleCov::Result
4
+ class SimpleCovHelper
5
+ require 'simplecov'
6
+ require_relative 'concerns/commit_metadata_persistable'
7
+
8
+ include CommitMetadataPersistable
9
+
10
+ def commit
11
+ return @commit if @commit
12
+
13
+ repo = Rugged::Repository.new('.')
14
+ @commit = repo.lookup(repo.head.target_id)
15
+ end
16
+
17
+ def load_current_result
18
+ load_current_commit_data
19
+ end
20
+
21
+ def save_current_result(result)
22
+ save_current_commit_data(result)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ module NdrDevSupport
2
+ # This Class publishes messages to Slack
3
+ class SlackMessagePublisher
4
+ def initialize(url, default_options = {})
5
+ @url = url
6
+ @default_options = default_options
7
+ end
8
+
9
+ def post(options = {})
10
+ request = json_request
11
+ request.body = message(options)
12
+
13
+ use_ssl = request.uri.scheme == 'https'
14
+ http =
15
+ if proxy
16
+ proxy.start(request.uri.host, use_ssl: use_ssl)
17
+ else
18
+ Net::HTTP.start(request.uri.host, request.uri.port, use_ssl: use_ssl)
19
+ end
20
+
21
+ http.request(request)
22
+ end
23
+
24
+ private
25
+
26
+ def json_request
27
+ uri = URI.parse(@url)
28
+ request = Net::HTTP::Post.new(uri)
29
+ # request.basic_auth(*@auth.split(':')) if @auth
30
+ request['Content-Type'] = 'application/json'
31
+ request
32
+ end
33
+
34
+ def message(options)
35
+ @default_options.merge(options).to_json
36
+ end
37
+
38
+ def proxy
39
+ return @proxy if @proxy
40
+
41
+ return if ENV['https_proxy'].nil?
42
+ host_and_port = ENV['https_proxy'].match(%r{\A(?:https?://)?([^:]+):(\d+)})[1, 2]
43
+
44
+ return if host_and_port.nil?
45
+ @proxy = Net::HTTP.Proxy(*host_and_port)
46
+ end
47
+ end
48
+ end
@@ -1,2 +1,14 @@
1
1
  load 'tasks/audit_code.rake'
2
+ load 'tasks/ci/brakeman.rake'
3
+ load 'tasks/ci/bundle_audit.rake'
4
+ load 'tasks/ci/bundle_install.rake'
5
+ load 'tasks/ci/housekeep.rake'
6
+ load 'tasks/ci/linguist.rake'
7
+ load 'tasks/ci/notes.rake'
8
+ load 'tasks/ci/prometheus.rake'
9
+ load 'tasks/ci/rugged.rake'
10
+ load 'tasks/ci/server.rake'
11
+ load 'tasks/ci/simplecov.rake'
12
+ load 'tasks/ci/slack.rake'
13
+ load 'tasks/ci/stats.rake'
2
14
  load 'tasks/rubocop.rake'
@@ -2,5 +2,5 @@
2
2
  # This defines the NdrDevSupport version. If you change it, rebuild and commit the gem.
3
3
  # Use "rake build" to build the gem, see rake -T for all bundler rake tasks (and our own).
4
4
  module NdrDevSupport
5
- VERSION = '2.1.2'.freeze
5
+ VERSION = '3.0.0'.freeze
6
6
  end
@@ -18,20 +18,44 @@ def rake_cmd
18
18
  ENV['BUNDLE_BIN_PATH'] ? 'bundle exec rake' : 'rake'
19
19
  end
20
20
 
21
- # Parameter max_print is number of entries to print before truncating output
22
- # (negative value => print all)
23
- def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, show_in_priority = false, user_name = 'usr')
24
- puts 'Running source code safety audit script.'
25
- puts
26
-
27
- max_print = 1_000_000 if max_print < 0
21
+ def load_file_safety
28
22
  safety_cfg = File.exist?(SAFETY_FILE) ? YAML.load_file(SAFETY_FILE) : {}
29
23
  file_safety = safety_cfg['file safety']
30
24
  if file_safety.nil?
31
25
  puts "Creating new 'file safety' block in #{SAFETY_FILE}"
32
- safety_cfg['file safety'] = file_safety = {}
26
+ file_safety = {}
33
27
  end
34
- file_safety.each do |_k, v|
28
+ file_safety
29
+ end
30
+
31
+ def update_safety_file(file_safety)
32
+ File.open(SAFETY_FILE, 'w') do |file|
33
+ # Consistent file diffs, as ruby preserves Hash insertion order since v1.9
34
+ list = {}
35
+ list['file safety'] = Hash[file_safety.sort]
36
+ YAML.dump(list, file) # Save changes before checking latest revisions
37
+ end
38
+ end
39
+
40
+ def add_new_file_to_file_safety(file_safety, f)
41
+ return if file_safety.key?(f)
42
+ file_safety[f] = {
43
+ 'comments' => nil,
44
+ 'reviewed_by' => nil,
45
+ 'safe_revision' => nil
46
+ }
47
+ end
48
+
49
+ # Parameter max_print is number of entries to print before truncating output
50
+ # (negative value => print all)
51
+ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, show_in_priority = false, usr = 'usr', interactive = false)
52
+ puts 'Running source code safety audit script.'
53
+ puts
54
+
55
+ max_print = 1_000_000 if max_print.negative?
56
+ show_diffs = true if interactive
57
+ file_safety = load_file_safety
58
+ file_safety.each_value do |v|
35
59
  rev = v['safe_revision']
36
60
  v['safe_revision'] = rev.to_s if rev.is_a?(Integer)
37
61
  end
@@ -57,19 +81,8 @@ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, sh
57
81
  new_files = get_new_files(safety_repo)
58
82
  # Ignore subdirectories, and exclude code_safety.yml by default.
59
83
  new_files.delete_if { |f| f =~ /[\/\\]$/ || Pathname.new(f).expand_path == SAFETY_FILE }
60
- new_files.each do |f|
61
- next if file_safety.key?(f)
62
- file_safety[f] = {
63
- 'comments' => nil,
64
- 'reviewed_by' => nil,
65
- 'safe_revision' => nil
66
- }
67
- end
68
- File.open(SAFETY_FILE, 'w') do |file|
69
- # Consistent file diffs, as ruby preserves Hash insertion order since v1.9
70
- safety_cfg['file safety'] = Hash[file_safety.sort]
71
- YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
72
- end
84
+ new_files.each { |f| add_new_file_to_file_safety(file_safety, f) }
85
+ update_safety_file(file_safety) # Save changes before checking latest revisions
73
86
  end
74
87
  puts "Updating latest revisions for #{file_safety.size} files"
75
88
  set_last_changed_revision(trunk_repo, file_safety, file_safety.keys)
@@ -79,7 +92,7 @@ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, sh
79
92
 
80
93
  missing_files = file_safety.keys.reject { |path| File.file?(path) }
81
94
 
82
- if missing_files.length > 0
95
+ unless missing_files.empty?
83
96
  puts "Number of files no longer in repository but in code_safety.yml: #{missing_files.length}"
84
97
  puts " Please run #{rake_cmd} audit:tidy_code_safety_file to remove redundant files"
85
98
  missing_files.each { |path| puts ' ' + path }
@@ -106,15 +119,13 @@ def audit_code_safety(max_print = 20, ignore_new = false, show_diffs = false, sh
106
119
  end
107
120
 
108
121
  file_list.each do |f|
109
- if print_file_safety(file_safety, trunk_repo, f, false, printed.size >= max_print)
110
- printed << f
111
- end
122
+ printed << f if print_file_safety(file_safety, f, false, printed.size >= max_print)
112
123
  end
113
124
  puts "... and #{printed.size - max_print} others" if printed.size > max_print
114
125
  if show_diffs
115
126
  puts
116
127
  printed.each do |f|
117
- print_file_diffs(file_safety, trunk_repo, f, user_name)
128
+ print_file_diffs(file_safety, trunk_repo, f, usr, interactive)
118
129
  end
119
130
  end
120
131
 
@@ -126,7 +137,7 @@ end
126
137
  # If not verbose, only prints details for unsafe files
127
138
  # Returns true if anything printed (or would have been printed if silent),
128
139
  # or false otherwise.
129
- def print_file_safety(file_safety, repo, fname, verbose = false, silent = false)
140
+ def print_file_safety(file_safety, fname, verbose = false, silent = false)
130
141
  msg = "#{fname}\n "
131
142
  entry = file_safety[fname]
132
143
  msg += 'File not in audit list' if entry.nil?
@@ -154,21 +165,12 @@ def print_file_safety(file_safety, repo, fname, verbose = false, silent = false)
154
165
  end
155
166
 
156
167
  def flag_file_as_safe(release, reviewed_by, comments, f)
157
- safety_cfg = YAML.load_file(SAFETY_FILE)
158
- file_safety = safety_cfg['file safety']
159
-
160
- unless File.exist?(f)
161
- abort("Error: Unable to flag non-existent file as safe: #{f}")
162
- end
163
- unless file_safety.key?(f)
164
- file_safety[f] = {
165
- 'comments' => nil,
166
- 'reviewed_by' => :dummy, # dummy value, will be overwritten
167
- 'safe_revision' => nil }
168
- end
168
+ abort("Error: Unable to flag non-existent file as safe: #{f}") unless File.exist?(f)
169
+ file_safety = load_file_safety
170
+ add_new_file_to_file_safety(file_safety, f)
169
171
  entry = file_safety[f]
170
172
  entry_orig = entry.dup
171
- if comments.to_s.length > 0 && entry['comments'] != comments
173
+ if !comments.to_s.empty? && entry['comments'] != comments
172
174
  entry['comments'] = if entry['comments'].to_s.empty?
173
175
  comments
174
176
  else
@@ -176,9 +178,7 @@ def flag_file_as_safe(release, reviewed_by, comments, f)
176
178
  end
177
179
  end
178
180
  if entry['safe_revision']
179
- unless release
180
- abort("Error: File already has safe revision #{entry['safe_revision']}: #{f}")
181
- end
181
+ abort("Error: File already has safe revision #{entry['safe_revision']}: #{f}") unless release
182
182
  if release.is_a?(Integer) && release < entry['safe_revision']
183
183
  puts("Warning: Rolling back safe revision from #{entry['safe_revision']} to #{release} for #{f}")
184
184
  end
@@ -188,11 +188,7 @@ def flag_file_as_safe(release, reviewed_by, comments, f)
188
188
  if entry == entry_orig
189
189
  puts "No changes when updating safe_revision to #{release || '[none]'} for #{f}"
190
190
  else
191
- File.open(SAFETY_FILE, 'w') do |file|
192
- # Consistent file diffs, as ruby preserves Hash insertion order since v1.9
193
- safety_cfg['file safety'] = Hash[file_safety.sort]
194
- YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
195
- end
191
+ update_safety_file(file_safety)
196
192
  puts "Updated safe_revision to #{release || '[none]'} for #{f}"
197
193
  end
198
194
  end
@@ -313,49 +309,38 @@ def get_last_changed_revision(repo, fname)
313
309
  end
314
310
  end
315
311
 
316
- # Get mime type. Note that Git does not have this information
317
- def get_mime_type(repo, fname)
318
- case repository_type
319
- when 'git'
320
- 'Git does not provide mime types'
321
- when 'git-svn', 'svn'
322
- %x[svn propget svn:mime-type "#{repo}/#{fname}"].chomp
323
- end
324
- end
325
-
326
312
  # # Print file diffs, for code review
327
- def print_file_diffs(file_safety, repo, fname, user_name)
313
+ def print_file_diffs(file_safety, repo, fname, usr, interactive)
328
314
  entry = file_safety[fname]
329
315
  repolatest = entry['last_changed_rev']
330
316
  safe_revision = entry['safe_revision']
331
317
 
332
- if safe_revision.nil?
333
- first_revision = set_safe_revision
334
- print_repo_file_diffs(repolatest, repo, fname, user_name, first_revision)
335
- else
336
-
337
- rev = get_last_changed_revision(repo, fname)
338
- if rev
339
- mime = get_mime_type(repo, fname)
340
- end
341
-
342
- print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision) if repolatest != safe_revision
343
- end
318
+ return unless safe_revision.nil? || repolatest != safe_revision
319
+ safe_revision ||= root_revision
320
+ print_repo_file_diffs(repolatest, repo, fname, usr, safe_revision, interactive)
344
321
  end
345
322
 
346
323
  # Returns first commit for git and 0 for svn in order to be used to display
347
324
  # new files. Called from print_file_diffs
348
- def set_safe_revision
325
+ def root_revision
349
326
  case repository_type
350
327
  when 'git'
351
- %x[git rev-list --max-parents=0 HEAD].chomp
328
+ `git rev-list --max-parents=0 HEAD`.chomp
352
329
  when 'git-svn', 'svn'
353
330
  0
354
331
  end
355
332
  end
356
333
 
357
- def print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision)
334
+ def print_repo_file_diffs(repolatest, repo, fname, usr, safe_revision, interactive)
358
335
  require 'open3'
336
+ require 'highline/import'
337
+
338
+ if interactive
339
+ ask("\n<%= color('Press Enter to continue ...', :yellow) %>")
340
+ system('clear')
341
+ system("printf '\033[3J'") # clear the scrollback
342
+ end
343
+
359
344
  cmd = nil
360
345
  case repository_type
361
346
  when 'git'
@@ -366,15 +351,36 @@ def print_repo_file_diffs(repolatest, repo, fname, user_name, safe_revision)
366
351
  if cmd
367
352
  puts(cmd.join(' '))
368
353
  stdout_and_err_str, _status = Open3.capture2e(*cmd)
369
- puts 'Invalid commit ID in code_safety.yml ' + safe_revision if stdout_and_err_str.start_with?('fatal: Invalid revision range ')
354
+ puts 'Invalid commit ID in code_safety.yml ' + safe_revision if stdout_and_err_str.start_with?('fatal: Invalid revision range ')
370
355
  puts(stdout_and_err_str)
371
356
  else
372
357
  puts 'Unknown repo'
373
358
  end
374
359
 
375
- puts %(To flag the changes to this file as safe, run:)
376
- puts " #{rake_cmd} audit:safe release=#{repolatest} file=#{fname} reviewed_by=#{user_name}" \
377
- ' comments=""'
360
+ if interactive
361
+ response = ask("Flag #{fname} changes safe? [Yes|No|Abort]: ") { |q| q.case = :down }
362
+ if %w[yes y].include?(response)
363
+ puts 'Flagging as safe...'
364
+ release = get_release(repolatest)
365
+ if usr.to_s.strip.empty?
366
+ usr = ask('File reviewed by:') do |q|
367
+ q.whitespace = :strip_and_collapse
368
+ q.validate = /\A[\w \-]+\Z/
369
+ end
370
+ end
371
+ comment = ask('Please write your comments (optional):')
372
+ # use to_s to convert response from !ruby/string:HighLine::String to String
373
+ flag_file_as_safe(release, usr.to_s, comment.to_s, fname)
374
+ elsif %w[abort a].include?(response)
375
+ abort('Rake abort: user interrupt detected')
376
+ else
377
+ say("\n<%= color('Safey review for #{fname} skipped by user.', :magenta) %>")
378
+ end
379
+ else
380
+ puts 'To flag the changes to this file as safe, run:'
381
+ puts " #{rake_cmd} audit:safe release=#{repolatest} file=#{fname} reviewed_by=#{usr}" \
382
+ ' comments=""'
383
+ end
378
384
  puts
379
385
  end
380
386
 
@@ -389,8 +395,8 @@ def release_valid?(release)
389
395
  end
390
396
  end
391
397
 
392
- def get_release
393
- release = ENV['release']
398
+ def get_release(release = nil)
399
+ release ||= ENV['release']
394
400
  release = nil if release == '0'
395
401
  case repository_type
396
402
  when 'svn', 'git-svn'
@@ -413,29 +419,24 @@ def clean_working_copy?
413
419
  end
414
420
 
415
421
  def remove_non_existent_files_from_code_safety
416
- safety_cfg = YAML.load_file(SAFETY_FILE)
417
- file_safety = safety_cfg['file safety']
422
+ file_safety = load_file_safety
418
423
  files_no_longer_in_repo = file_safety.keys.reject { |ff| File.file?(ff) }
419
424
  files_no_longer_in_repo.each do |f|
420
425
  puts 'No longer in repository ' + f
421
426
  file_safety.delete f
422
427
  end
423
- File.open(SAFETY_FILE, 'w') do |file|
424
- # Consistent file diffs, as ruby preserves Hash insertion order since v1.9
425
- safety_cfg['file safety'] = Hash[file_safety.sort]
426
- YAML.dump(safety_cfg, file) # Save changes before checking latest revisions
427
- end
428
+ update_safety_file(file_safety)
428
429
  end
429
430
 
430
431
  namespace :audit do
431
432
  desc "Audit safety of source code.
432
- Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [reviewed_by=usr]
433
+ Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [reviewed_by=usr] [interactive=false|true]
433
434
 
434
435
  File #{SAFETY_FILE} lists the safety and revision information
435
436
  of the era source code. This task updates the list, and [TODO] warns about
436
437
  files which have changed since they were last verified as safe."
437
438
  task(:code) do
438
- puts 'Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [show_in_priority=false|true] [reviewed_by=usr]'
439
+ puts 'Usage: audit:code [max_print=n] [ignore_new=false|true] [show_diffs=false|true] [show_in_priority=false|true] [reviewed_by=usr] [interactive=false|true]'
439
440
  puts "This is a #{repository_type} repository"
440
441
 
441
442
  ignore_new = (ENV['ignore_new'].to_s =~ /\Atrue\Z/i)
@@ -443,10 +444,11 @@ files which have changed since they were last verified as safe."
443
444
  show_in_priority = (ENV['show_in_priority'].to_s =~ /\Atrue\Z/i)
444
445
  max_print = ENV['max_print'] =~ /\A-?[0-9][0-9]*\Z/ ? ENV['max_print'].to_i : 20
445
446
  reviewer = ENV['reviewed_by']
447
+ interactive = (ENV['interactive'].to_s =~ /\Atrue\Z/i)
446
448
 
447
- all_safe = audit_code_safety(max_print, ignore_new, show_diffs, show_in_priority, reviewer)
449
+ all_safe = audit_code_safety(max_print, ignore_new, show_diffs, show_in_priority, reviewer, interactive)
448
450
 
449
- unless show_diffs
451
+ unless show_diffs || interactive
450
452
  puts "To show file diffs, run: #{rake_cmd} audit:code max_print=-1 show_diffs=true"
451
453
  end
452
454