gitolemy 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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