gitolemy 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: '083b930c641ed21a49484825dcf23d74097bb07b'
4
+ data.tar.gz: c89bc22ac8076d07a01e7d95b86be3c0924aaef8
5
+ SHA512:
6
+ metadata.gz: 88c97b2e09ae85864329201c1f91732c2c22aeb49bd25162e3556a8e1b751268c60260ea82a0e3abf3c3b7fcd2b252fefce0d81dcf0282fbfb431e327701cc71
7
+ data.tar.gz: 84cdb5564782d60da58bd49b12089edb283765e65e8e93ae6d5ed3b0204c62e026099798c3a3711d264ba38a0a8066bcb8145d9ab2cf75ea4274eef0650accd5
@@ -0,0 +1,145 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "rubygems"
4
+ require "json"
5
+ require "date"
6
+ require "optparse"
7
+ require "dotenv"
8
+ require "active_support/core_ext/string"
9
+
10
+ require "rollbar"
11
+ require "airbrake-ruby"
12
+
13
+ require_relative "../lib/file_manager"
14
+ require_relative "../lib/notifier"
15
+ require_relative "../lib/risk_analyzer"
16
+ require_relative "../lib/secure_file_store"
17
+
18
+
19
+ Dotenv.load()
20
+
21
+ def merge_files_hash!(output, file_hash)
22
+ changes = output[:summary][:file_changes]
23
+ file_hash.each do |key, value|
24
+ changes[key] ||= {}
25
+ changes[key].merge!(value)
26
+ end
27
+ end
28
+
29
+ def client_factory(config, keys, additions={})
30
+ key = keys.detect { |key| config.has_key?(key) }
31
+ return nil if key.nil?
32
+ require_relative "../lib/integrations/#{key}_client"
33
+ data = config[key].merge(additions)
34
+ "#{key.camelize}Client".constantize.new(data)
35
+ end
36
+
37
+ def scm_client(config)
38
+ client_factory(config, ["git"])
39
+ end
40
+
41
+ def issues(config, commits)
42
+ client_factory(config, ["jira"])
43
+ .try(:merge_and_fetch_issues!, commits) || {}
44
+ end
45
+
46
+ def errors(config)
47
+ client_factory(config, ["airbrake", "rollbar"])
48
+ end
49
+
50
+ def coverage(config)
51
+ client_factory(config, ["covhura"])
52
+ end
53
+
54
+ def bugs(config, commits)
55
+ client_factory(config, ["jira"])
56
+ .try(:merge_and_fetch_bugs!, commits) || {}
57
+ end
58
+
59
+
60
+ def index(branches)
61
+ config = SecureFileStore.new(ENV["SETTING_KEY"]).read_settings()
62
+ scm_client = scm_client(config)
63
+
64
+ branches = branches.count == 0 ?
65
+ scm_client.remote_branches() :
66
+ branches
67
+
68
+ branches.each do |branch|
69
+ commits = scm_client.commits(branch)
70
+ file_manager = FileManager.new(branch)
71
+ file_manager.apply!(commits, {
72
+ issues: issues(config, commits),
73
+ errors: errors(config),
74
+ bugs: bugs(config, commits),
75
+ coverage: coverage(config)
76
+ })
77
+
78
+ risk = RiskAnalyzer
79
+ .new
80
+ .analyze(file_manager, commits.last)
81
+
82
+ Notifier
83
+ .new
84
+ .notify(scm_client.notification_url, commits.last, risk)
85
+ end
86
+ end
87
+
88
+ def get_opts()
89
+ options = {}
90
+ OptionParser.new do |opts|
91
+ opts.banner = "Usage: conglomerate.rb [options]"
92
+
93
+ opts.on("-p", "--repo-path=val", String, "Repository Path") do |path|
94
+ Dir.chdir(path)
95
+ end
96
+
97
+ opts.on("-v", "--verbose", "Run Verbosely") do |verbose|
98
+ ENV["GITOLEMY_VERBOSE"] = "true"
99
+ end
100
+
101
+ opts.on("-c", "--compare", "Compare virtual files to Git objects") do |compare|
102
+ ENV["GITOLEMY_COMPARE"] = "true"
103
+ end
104
+
105
+ opts.on("-t", "--test", "Run as test: do not store results") do |test|
106
+ ENV["GITOLEMY_PERSIST"] = "false"
107
+ end
108
+
109
+ opts.on("-s", "--sync", "Sync branch even if last commit indexed") do |sync|
110
+ ENV["GITOLEMY_SYNC"] = "true"
111
+ end
112
+ end.parse!
113
+ options[:branches] = []
114
+ options[:branches] << ARGV.first if ARGV.count > 0
115
+ options
116
+ end
117
+
118
+
119
+ def main()
120
+ index(get_opts()[:branches])
121
+ rescue => ex
122
+ #handler = proc do |options|
123
+ # payload = options[:payload]
124
+ # payload["data"]["environment"] = ENV["ROLLBAR_ENV"]
125
+ #end
126
+
127
+ #Rollbar.configure do |config|
128
+ # config.access_token = ENV["ROLLBAR_API_KEY"]
129
+ # config.transform << handler
130
+ #end
131
+
132
+ #Rollbar.error(ex) if ENV["ROLLBAR_ENV"] == "production"
133
+
134
+ #Airbrake.configure do |c|
135
+ # c.project_id = ENV["AIRBRAKE_PROJECT_ID"]
136
+ # c.project_key = ENV["AIRBRAKE_PROJECT_KEY"]
137
+ # c.environment = ENV["AIRBRAKE_ENV"].to_sym
138
+ #end
139
+
140
+ #Airbrake.notify_sync(ex) if ENV["AIRBRAKE_ENV"] == "production"
141
+
142
+ raise ex
143
+ end
144
+
145
+ main()
@@ -0,0 +1,59 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require "webrick"
4
+ require "webrick/httpproxy"
5
+ require "json"
6
+
7
+ server = WEBrick::HTTPProxyServer.new(:BindAddress => "0.0.0.0", :Port => 8180)
8
+
9
+ server.mount_proc "/sync" do |req, res|
10
+ user = req.query["user"].gsub(" ", "+")
11
+ repo = req.query["repo"]
12
+ branch = req.query["branch"]
13
+ is_sync = req.query["sync"] == "true"
14
+
15
+ path = File.join(ENV.fetch("PROJECT_ROOT"), user, repo)
16
+ if Dir.exist?(path)
17
+ bin_path = File.join(File.dirname(__FILE__), "conglomerate.rb")
18
+ pid = spawn("#{bin_path} #{'-s' if is_sync} #{branch} -p #{path}")
19
+ Process.detach(pid)
20
+ res.status = 200
21
+ else
22
+ res.status = 400
23
+ end
24
+ end
25
+
26
+ server.mount_proc "/progress" do |req, res|
27
+ user = req.query["user"].gsub(" ", "+")
28
+ repo = req.query["repo"]
29
+ branch = req.query["branch"]
30
+
31
+ path = File.join(ENV.fetch("PROJECT_ROOT"), user, repo)
32
+ if Dir.exist?(path)
33
+ resp = {
34
+ commits: commit_count(path),
35
+ indexed: indexed_count(path, branch)
36
+ }
37
+ res.status = 200
38
+ res.body = resp.to_json
39
+ else
40
+ res.status = 400
41
+ end
42
+ end
43
+
44
+ def commit_count(project_root)
45
+ git_dir = File.join(project_root, ".git")
46
+ `git --git-dir=#{git_dir} log --graph --oneline | grep '^\*' | wc -l`.to_i
47
+ end
48
+
49
+ def indexed_count(project_root, branch)
50
+ branch = branch.gsub(/^remotes\/origin\//, "")
51
+ branch_lock_path = File.join(project_root, ".gitolemy", "branches", "#{branch}.lock")
52
+ `wc -l #{branch_lock_path}`
53
+ .split(" ")
54
+ .first
55
+ .to_i
56
+ end
57
+
58
+ trap("INT") { server.shutdown }
59
+ server.start
@@ -0,0 +1,92 @@
1
+ require "json"
2
+ require "zlib"
3
+ require "fileutils"
4
+ require "active_support/json"
5
+
6
+ module Cache
7
+ extend self
8
+
9
+ CACHE_BASE_PATH = ".gitolemy"
10
+ OBJECT_CACHE_BASE = "objects"
11
+ BRANCH_CACHE_BASE = "branches"
12
+ REMOTES_REGEX = /^remotes\/origin\//
13
+
14
+ def cache_path(key)
15
+ File.join(CACHE_BASE_PATH, "#{key}.json.gz")
16
+ end
17
+
18
+ def write(key, data, existing_data=nil)
19
+ return data if not persist?
20
+ filename = cache_path(key)
21
+ dirname = File.dirname(filename)
22
+ ensure_directory(dirname)
23
+
24
+ store_data = existing_data.nil? ? data : existing_data.merge(data)
25
+ Zlib::GzipWriter.open(filename) { |gz| gz.write(store_data.to_json) }
26
+ data
27
+ end
28
+
29
+ def read(key, default_val=nil)
30
+ filename = cache_path(key)
31
+ return default_val if not File.exist?(filename)
32
+ JSON.parse(Zlib::GzipReader.open(filename) { |gz| gz.read })
33
+ rescue JSON::ParserError, Zlib::GzipFile::Error
34
+ default_val
35
+ end
36
+
37
+ def index_commit(branch, commit_id)
38
+ return if not persist?
39
+ branch_path = branch_path(branch)
40
+ ensure_directory(File.dirname(branch_path))
41
+ FileUtils.touch(branch_path) if not File.exist?(branch_path)
42
+ File.open(branch_path, "a") do |file|
43
+ file << "#{commit_id.to_s}\n"
44
+ end
45
+ end
46
+
47
+ def index_commits(branch, commits)
48
+ return if not persist?
49
+ File.write(branch_path(branch), commits.join("\n") + "\n")
50
+ end
51
+
52
+ def last_indexed_commit(branch, commit_ids)
53
+ cached_commit_ids = File
54
+ .read(branch_path(branch))
55
+ .lines
56
+ .map(&:chomp)
57
+ .reduce({}) do |acc, commit_id|
58
+ acc[commit_id.to_sym] = true
59
+ acc
60
+ end
61
+
62
+ commit_ids.detect { |commit_id| cached_commit_ids[commit_id] }
63
+ rescue Errno::ENOENT
64
+ nil
65
+ end
66
+
67
+ def ensure_directory(dirname)
68
+ return if File.directory?(dirname)
69
+ FileUtils.makedirs(dirname)
70
+ end
71
+
72
+ def read_object(key, default_val=nil)
73
+ read(object_rel_path(key.to_s), default_val)
74
+ end
75
+
76
+ def write_object(key, data)
77
+ write(object_rel_path(key.to_s), data)
78
+ end
79
+
80
+ def object_rel_path(object_id)
81
+ File.join(OBJECT_CACHE_BASE, object_id)
82
+ end
83
+
84
+ def branch_path(branch)
85
+ branch_name = branch.gsub(REMOTES_REGEX, "")
86
+ File.join(".gitolemy", BRANCH_CACHE_BASE, "#{branch_name}.lock")
87
+ end
88
+
89
+ def persist?
90
+ ENV["GITOLEMY_PERSIST"] != "false"
91
+ end
92
+ end
@@ -0,0 +1,139 @@
1
+ require "mail"
2
+ require "iconv"
3
+
4
+ require_relative "util"
5
+ require_relative "loggr"
6
+ require_relative "file_diff"
7
+ require_relative "line_tracker"
8
+
9
+ class Commit
10
+ FILE_DIFF_REGEX = /^diff --git a\/.* b\//
11
+
12
+ attr_accessor :commit_id
13
+ attr_accessor :children
14
+ attr_accessor :author
15
+ attr_accessor :date
16
+ attr_accessor :subject
17
+ attr_accessor :trees
18
+ attr_accessor :file_diffs
19
+ attr_accessor :movements
20
+ attr_accessor :changes
21
+ attr_accessor :insertions_total
22
+ attr_accessor :deletions_total
23
+ attr_accessor :changes_total
24
+ attr_accessor :cached_files
25
+ attr_accessor :cached_trees
26
+ attr_accessor :issue_id
27
+ attr_accessor :bug_id
28
+
29
+ alias_method :id, :commit_id
30
+ alias_method :id=, :commit_id=
31
+
32
+ def initialize(attrs={})
33
+ attrs.each do |key, val|
34
+ instance_variable_set("@#{key}".to_sym, val)
35
+ end
36
+ end
37
+
38
+ def set_cached(tree)
39
+ @cached_files = tree.select { |obj| obj[:type] == :file }
40
+ @cached_trees = tree.select { |obj| obj[:type] == :tree }
41
+ end
42
+
43
+ def business_value
44
+ issue = ::Store::Issue[issue_id]
45
+ issue.present? ?
46
+ issue[:business_value] :
47
+ 0
48
+ end
49
+
50
+ def as_json
51
+ {
52
+ commit_id: commit_id,
53
+ children: children,
54
+ author: author,
55
+ date: date,
56
+ subject: subject,
57
+ insertions_total: insertions_total,
58
+ deletions_total: deletions_total,
59
+ changes_total: changes.count,
60
+ movements_total: movements.count,
61
+ issue_id: issue_id,
62
+ bug_id: bug_id
63
+ }
64
+ end
65
+
66
+ class << self
67
+ def from_git(commit_lines, git_client)
68
+ Commit.new(parse_commit(commit_lines, git_client))
69
+ end
70
+
71
+ def parse_commit(commit_lines, git_client)
72
+ commit_id, author, date, children, subject = parse_commit_header(commit_lines.shift())
73
+ Loggr.instance.info("PARSE COMMIT: #{commit_id}")
74
+ if children.length > 1
75
+ commit_lines = git_client.diff(children.first, commit_id)
76
+ end
77
+
78
+ file_diffs = commit_lines
79
+ .reduce([], &fold_reducer(FILE_DIFF_REGEX))
80
+ .map { |file_diff_lines| FileDiff.from_git(file_diff_lines) }
81
+
82
+ insertions = file_diffs.reduce(0) { |sum, file_diff| sum + file_diff.insertions_total }
83
+ deletions = file_diffs.reduce(0) { |sum, file_diff| sum + file_diff.deletions_total }
84
+
85
+ file_diffs = file_diffs.reduce({}) do |obj, file_diff|
86
+ obj[file_diff.b_file_name] = file_diff
87
+ obj
88
+ end
89
+
90
+ if children.length > 1
91
+ diff_id = "#{children.first}..#{commit_id}"
92
+ trees = git_client.parse_diff_tree(git_client.diff_tree(diff_id))
93
+ elsif children.length == 1
94
+ trees = git_client.parse_diff_tree(git_client.diff_tree(commit_id))
95
+ else
96
+ trees = git_client.parse_ls_tree(git_client.ls_tree(commit_id))
97
+ end
98
+
99
+ mutations = LineTracker.new.track_mutations!(file_diffs)
100
+
101
+ {
102
+ commit_id: commit_id,
103
+ children: children,
104
+ author: {
105
+ name: author.display_name,
106
+ email: author.address
107
+ },
108
+ date: date,
109
+ subject: subject,
110
+ trees: trees,
111
+ file_diffs: file_diffs,
112
+ movements: mutations[:movements],
113
+ changes: mutations[:changes],
114
+ insertions_total: insertions,
115
+ deletions_total: deletions,
116
+ changes_total: insertions + deletions
117
+ }
118
+ end
119
+
120
+ def parse_commit_header(header)
121
+ commit_id, children, author, date, subject = header.split("|||")
122
+ commit_id = commit_id.to_sym
123
+ subject ||= ""
124
+ children = children
125
+ .split(" ")
126
+ .map { |child_id| child_id.to_sym }
127
+ author = parse_author(author)
128
+ date = DateTime.rfc2822(date)
129
+ [commit_id, author, date, children, subject]
130
+ end
131
+
132
+ def parse_author(author)
133
+ author = Iconv
134
+ .conv("ascii//translit", "UTF-8", author)
135
+ .tr("[]", "()")
136
+ Mail::Address.new(author)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,225 @@
1
+ require "diff_match_patch_native"
2
+
3
+ module CommitStats
4
+ def self.link_mutations!(files, commit_mutations, type)
5
+ dmp = DiffMatchPatch.new()
6
+
7
+ commit_mutations.each do |dest_file, file_mutations|
8
+ file_mutations.each do |dest_line, mutation|
9
+ src_file = mutation[:from]
10
+ src_line = mutation[:line]
11
+
12
+ b_file = files[:b][dest_file][:b_file]
13
+ a_file = files[:a][src_file][:a_file]
14
+
15
+ b_line = b_file.is_a?(Hash) ? b_file[:lines][dest_line] : b_file.lines[dest_line]
16
+ a_line = a_file.is_a?(Hash) ? a_file[:lines][src_line] : a_file.lines[src_line]
17
+
18
+ if type == :change
19
+ diff = dmp.diff_main(a_line[:text], b_line[:text], false)
20
+
21
+ b_line[:diff_text] = diff.reduce("") do |acc, section|
22
+ if section.first == 0
23
+ acc += section.last
24
+ elsif section.first == 1
25
+ acc += "<ins>#{section.last}</ins>"
26
+ end
27
+ acc
28
+ end
29
+
30
+ a_line[:diff_text] = diff.reduce("") do |acc, section|
31
+ if section.first == 0
32
+ acc += section.last
33
+ elsif section.first == -1
34
+ acc += "<del>#{section.last}</del>"
35
+ end
36
+ acc
37
+ end
38
+ end
39
+
40
+ if Line.trailing?(b_line, a_line)
41
+ change_type = :trailing
42
+ elsif Line.beauty?(b_line, a_line)
43
+ change_type = :beauty
44
+ else
45
+ change_type = :normal
46
+ end
47
+
48
+ b_line[type] = {
49
+ link: "#/a/#{src_line + 1}/#{src_file}",
50
+ type: change_type,
51
+ change_text: a_line[:text]
52
+ }
53
+
54
+ a_line[type] = {
55
+ link: "#/b/#{dest_line + 1}/#{dest_file}",
56
+ type: change_type,
57
+ change_text: b_line[:text]
58
+ }
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.link_diffs!(files, file_diffs)
64
+ file_diffs.reduce(diff_context()) do |acc, (file_name, file_diff)|
65
+ file_diff.diffs.each do |diff|
66
+ diff.insertions.each_with_index do |insertion, index|
67
+ line_num = diff.insert_start + index - 1
68
+ file = files[:b][file_diff.b_file_name][:b_file]
69
+ line = file.is_a?(Hash) ? file[:lines][line_num] : file.lines[line_num]
70
+ line[:insertion] = true
71
+ acc[:insertions] << line
72
+ end
73
+
74
+ del_index = diff.insert_count > 0 ? 1 : 0
75
+ diff.deletions.each_with_index do |deletion, index|
76
+ line_num = diff.delete_start + index - 1
77
+ file = files[:a][file_diff.a_file_name][:a_file]
78
+ line = file.is_a?(Hash) ? file[:lines][line_num] : file.lines[line_num]
79
+ line.merge!({
80
+ deletion: true,
81
+ a_pos: line_num + 1,
82
+ b_pos: diff.insert_start + index - del_index
83
+ })
84
+
85
+ del_index += 1 if line[:change].blank?
86
+ acc[:deletions] << line
87
+ end
88
+ end
89
+ acc
90
+ end
91
+ end
92
+
93
+ def self.stats(commit, line_diffs, files)
94
+ changes_total = commit
95
+ .changes
96
+ .reduce(0) { |acc, (filename, changeset)| acc += changeset.count } * 2
97
+
98
+ movements_total = commit
99
+ .movements
100
+ .reduce(0) { |acc, (filename, moveset)| acc += moveset.count } * 2
101
+
102
+ uncovered_deletions_total = 0
103
+ uncovered_insertions_total = 0
104
+ uncovered_changes_total = 0
105
+ uncovered_error_changes_total = 0
106
+ uncovered_buggy_changes_total = 0
107
+ error_deletions_total = 0
108
+ error_changes_total = 0
109
+ buggy_insertions_total = 0
110
+ buggy_deletions_total = 0
111
+ buggy_changes_total = 0
112
+ whitespace_insertions_total = 0
113
+ whitespace_deletions_total = 0
114
+ total_files_modified = files[:a].count
115
+ total_lines_modified = commit.changes_total
116
+ trailingspace_total = 0
117
+ beautyspace_total = 0
118
+
119
+ line_diffs[:insertions].each do |insertion|
120
+ if insertion[:coverage] == 0
121
+ uncovered_insertions_total += 1
122
+ if insertion[:change]
123
+ uncovered_changes_total += 1
124
+ uncovered_error_changes_total += 1 if insertion[:errors].count > 0
125
+ uncovered_buggy_changes_total += 1 if insertion[:bugs].count > 0
126
+ end
127
+ end
128
+ if insertion[:change]
129
+ buggy_changes_total += 1 if insertion[:bugs].count > 0
130
+ error_changes_total += 1 if insertion[:errors].count > 0
131
+ beautyspace_total += 2 if insertion[:change][:type] == :beauty # + deletion
132
+ trailingspace_total += 2 if insertion[:change][:type] == :trailing # + deletion
133
+ end
134
+ whitespace_insertions_total += 1 if insertion[:text].strip().blank?
135
+ buggy_insertions_total += 1 if insertion[:bugs].count > 0
136
+ end
137
+
138
+ line_diffs[:deletions].each do |deletion|
139
+ if deletion[:coverage] == 0
140
+ uncovered_deletions_total += 1
141
+ end
142
+ error_deletions_total += 1 if deletion[:errors].count > 0
143
+ whitespace_deletions_total += 1 if deletion[:text].strip().blank?
144
+ buggy_deletions_total += 1 if deletion[:bugs].count > 0
145
+ end
146
+
147
+ relevant_insertions_total = commit.insertions_total - whitespace_insertions_total
148
+ covered_insertions_total = relevant_insertions_total - uncovered_insertions_total
149
+ covered_insertions_percent = change_percent(covered_insertions_total, relevant_insertions_total)
150
+ uncovered_insertions_percent = change_percent(uncovered_insertions_total, relevant_insertions_total)
151
+ buggy_insertions_percent = change_percent(buggy_insertions_total, relevant_insertions_total)
152
+
153
+ relevant_deletions_total = commit.insertions_total - whitespace_insertions_total
154
+ covered_deletions_total = relevant_deletions_total - uncovered_deletions_total
155
+ covered_deletions_percent = change_percent(covered_deletions_total, relevant_deletions_total)
156
+ uncovered_deletions_percent = change_percent(uncovered_deletions_total, relevant_deletions_total)
157
+ buggy_deletions_percent = change_percent(buggy_deletions_total, relevant_deletions_total)
158
+ error_deletions_percent = change_percent(error_deletions_total, relevant_deletions_total)
159
+
160
+ covered_changes_total = changes_total - uncovered_changes_total
161
+ covered_changes_percent = change_percent(covered_changes_total, changes_total)
162
+ uncovered_changes_percent = change_percent(uncovered_changes_total, changes_total)
163
+ uncovered_error_changes_percent = change_percent(uncovered_error_changes_total, changes_total)
164
+ uncovered_buggy_changes_percent = change_percent(uncovered_buggy_changes_total, changes_total)
165
+
166
+ diffs_total = commit.deletions_total + commit.insertions_total
167
+ movements_percent = change_percent(movements_total, diffs_total)
168
+
169
+ whitespace_total = whitespace_insertions_total + whitespace_deletions_total
170
+ whitespace_percent = change_percent(whitespace_total, diffs_total)
171
+ trailingspace_percent = change_percent(trailingspace_total, diffs_total)
172
+ beautyspace_percent = change_percent(beautyspace_total, diffs_total)
173
+
174
+ {
175
+ changes: commit.changes,
176
+ movements: commit.movements,
177
+ deletions_total: commit.deletions_total,
178
+ insertions_total: commit.insertions_total,
179
+ changes_total: changes_total,
180
+ movements_total: movements_total,
181
+ whitespace_total: whitespace_total,
182
+ trailingspace_total: trailingspace_total,
183
+ beautyspace_total: beautyspace_total,
184
+ covered_insertions_percent: covered_insertions_percent,
185
+ covered_deletions_percent: covered_deletions_percent,
186
+ covered_changes_percent: covered_changes_percent,
187
+ uncovered_insertions_percent: uncovered_insertions_percent,
188
+ uncovered_deletions_percent: uncovered_deletions_percent,
189
+ uncovered_changes_percent: uncovered_changes_percent,
190
+ uncovered_error_changes_percent: uncovered_error_changes_percent,
191
+ uncovered_buggy_changes_percent: uncovered_buggy_changes_percent,
192
+ buggy_insertions_percent: buggy_insertions_percent,
193
+ buggy_deletions_percent: buggy_deletions_percent,
194
+ error_deletions_percent: error_deletions_percent,
195
+ movements_percent: movements_percent,
196
+ whitespace_percent: whitespace_percent,
197
+ trailingspace_percent: trailingspace_percent,
198
+ beautyspace_percent: beautyspace_percent,
199
+ uncovered_deletions_total: uncovered_deletions_total,
200
+ uncovered_insertions_total: uncovered_insertions_total,
201
+ uncovered_changes_total: uncovered_changes_total,
202
+ uncovered_error_changes_total: uncovered_error_changes_total,
203
+ buggy_insertions_total: buggy_insertions_total,
204
+ buggy_deletions_total: buggy_deletions_total,
205
+ buggy_changes_total: buggy_changes_total,
206
+ uncovered_buggy_changes_total: uncovered_buggy_changes_total,
207
+ error_deletions_total: error_deletions_total,
208
+ error_changes_total: error_changes_total,
209
+ total_files_modified: total_files_modified,
210
+ total_lines_modified: total_lines_modified
211
+ }
212
+ end
213
+
214
+ def self.change_percent(numerator, denominator)
215
+ denominator > 0 ?
216
+ (numerator / denominator.to_f * 100) :
217
+ 0
218
+ end
219
+
220
+ def self.diff_context()
221
+ {
222
+ insertions: [],
223
+ deletions: [] }
224
+ end
225
+ end