gitolemy 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/conglomerate.rb +145 -0
- data/bin/serve.rb +59 -0
- data/lib/cache.rb +92 -0
- data/lib/commit.rb +139 -0
- data/lib/commit_stats.rb +225 -0
- data/lib/diff.rb +65 -0
- data/lib/file_diff.rb +98 -0
- data/lib/file_helper.rb +58 -0
- data/lib/file_manager.rb +116 -0
- data/lib/function_trace/c_syntax_tracer.rb +111 -0
- data/lib/function_trace/python_tracer.rb +103 -0
- data/lib/function_trace/ruby_tracer.rb +64 -0
- data/lib/function_trace/tracer.rb +44 -0
- data/lib/gitolemy.rb +1 -0
- data/lib/integrations/airbrake_client.rb +134 -0
- data/lib/integrations/code_climate_client.rb +79 -0
- data/lib/integrations/covhura_client.rb +38 -0
- data/lib/integrations/error_client.rb +55 -0
- data/lib/integrations/git_client.rb +183 -0
- data/lib/integrations/jira_client.rb +145 -0
- data/lib/integrations/rollbar_client.rb +147 -0
- data/lib/line.rb +124 -0
- data/lib/line_tracker.rb +90 -0
- data/lib/loggr.rb +24 -0
- data/lib/notifier.rb +20 -0
- data/lib/project_cache.rb +13 -0
- data/lib/risk_analyzer.rb +53 -0
- data/lib/secure_file_store.rb +61 -0
- data/lib/source_tree.rb +23 -0
- data/lib/stack_tracer.rb +197 -0
- data/lib/store.rb +96 -0
- data/lib/util.rb +10 -0
- data/lib/virtual_file.rb +218 -0
- data/lib/virtual_file_system.rb +78 -0
- data/lib/virtual_function.rb +38 -0
- data/lib/virtual_tree.rb +233 -0
- metadata +223 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/conglomerate.rb
ADDED
@@ -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()
|
data/bin/serve.rb
ADDED
@@ -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
|
data/lib/cache.rb
ADDED
@@ -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
|
data/lib/commit.rb
ADDED
@@ -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
|
data/lib/commit_stats.rb
ADDED
@@ -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
|