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
data/lib/store.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require_relative "cache"
|
2
|
+
|
3
|
+
module Store
|
4
|
+
def Store.read(set, key)
|
5
|
+
set[key]
|
6
|
+
end
|
7
|
+
|
8
|
+
def Store.write(set, key, value)
|
9
|
+
set[key] = value
|
10
|
+
key
|
11
|
+
end
|
12
|
+
|
13
|
+
def Store.cache(cache_key, new, old)
|
14
|
+
Cache.write(cache_key, new, old)
|
15
|
+
end
|
16
|
+
|
17
|
+
module Commit
|
18
|
+
@@commits = {}
|
19
|
+
def Commit.[](commit_id)
|
20
|
+
::Store::read(@@commits, commit_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def Commit.index(commit)
|
24
|
+
::Store::write(@@commits, commit[:commit_id], commit)
|
25
|
+
end
|
26
|
+
|
27
|
+
def Commit.cache(old={})
|
28
|
+
::Store::cache("commits", @@commits, Cache.read("commits", {}))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module Line
|
33
|
+
@@lines = {}
|
34
|
+
def Line.[](line_id)
|
35
|
+
::Store::read(@@lines, line_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def Line.index(line)
|
39
|
+
::Store::write(@@lines, line[:line_id], line)
|
40
|
+
end
|
41
|
+
|
42
|
+
def Line.cache()
|
43
|
+
::Store::cache("lines", @@lines, Cache.read("lines", {}))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module Issue
|
48
|
+
@@issues = {}
|
49
|
+
def Issue.[](issue_id)
|
50
|
+
::Store::read(@@issues, issue_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def Issue.index(issue)
|
54
|
+
::Store::write(@@issues, issue[:issue_id], issue)
|
55
|
+
rescue
|
56
|
+
issue
|
57
|
+
end
|
58
|
+
|
59
|
+
def Issue.cache(old={})
|
60
|
+
::Store::cache("issues", @@issues, old)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Bug
|
65
|
+
@@bugs = {}
|
66
|
+
def Bug.[](bug_id)
|
67
|
+
::Store::read(@@bugs, bug_id)
|
68
|
+
end
|
69
|
+
|
70
|
+
def Bug.index(bug)
|
71
|
+
::Store::write(@@bugs, bug[:bug_id], bug)
|
72
|
+
rescue
|
73
|
+
bug
|
74
|
+
end
|
75
|
+
|
76
|
+
def Bug.cache(old={})
|
77
|
+
::Store::cache("bugs", @@bugs, old)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
module Error
|
83
|
+
@@errors = {}
|
84
|
+
def Error.[](error_id)
|
85
|
+
::Store::read(@@errors, error_id)
|
86
|
+
end
|
87
|
+
|
88
|
+
def Error.index(error)
|
89
|
+
::Store::write(@@errors, error[:error_id], error)
|
90
|
+
end
|
91
|
+
|
92
|
+
def Error.cache(old={})
|
93
|
+
::Store::cache("errors", @@errors, old)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/util.rb
ADDED
data/lib/virtual_file.rb
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
require "active_support/core_ext/object"
|
2
|
+
require "date"
|
3
|
+
|
4
|
+
require_relative "loggr"
|
5
|
+
require_relative "line"
|
6
|
+
require_relative "function_trace/tracer"
|
7
|
+
require_relative "virtual_function"
|
8
|
+
|
9
|
+
class VirtualFile
|
10
|
+
|
11
|
+
attr_reader :lines
|
12
|
+
attr_reader :path
|
13
|
+
attr_reader :file_id
|
14
|
+
attr_reader :issues
|
15
|
+
attr_reader :commits
|
16
|
+
attr_reader :business_value
|
17
|
+
|
18
|
+
def initialize(file_path, diff=nil)
|
19
|
+
@path = file_path
|
20
|
+
@file_id = nil
|
21
|
+
@previous_file_id = nil
|
22
|
+
@touched_at_times = []
|
23
|
+
@commits = []
|
24
|
+
@issues = []
|
25
|
+
@bugs = []
|
26
|
+
@lines = []
|
27
|
+
@functions = []
|
28
|
+
@business_value = 0
|
29
|
+
@is_binary = diff.blank? ? false : diff.operation == :binary
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.from_json(file_id, json)
|
33
|
+
file = VirtualFile.new(json["path"])
|
34
|
+
touched_at_times = (json["touched_at_times"] || []).map { |time| DateTime.rfc3339(time) }
|
35
|
+
lines = (json["lines"] || []).map { |line| line.deep_symbolize_keys }
|
36
|
+
functions = (json["functions"] || []).map { |function| function.deep_symbolize_keys }
|
37
|
+
|
38
|
+
file.instance_variable_set(:@file_id, file_id)
|
39
|
+
file.instance_variable_set(:@previous_file_id, json["previous_file_id"])
|
40
|
+
file.instance_variable_set(:@touched_at_times, touched_at_times)
|
41
|
+
file.instance_variable_set(:@lines, lines)
|
42
|
+
file.instance_variable_set(:@functions, functions)
|
43
|
+
file.instance_variable_set(:@commits, json["commits"] || [])
|
44
|
+
file.instance_variable_set(:@issues, json["issues"] || [])
|
45
|
+
file.instance_variable_set(:@bugs, json["bugs"] || [])
|
46
|
+
file.instance_variable_set(:@business_value, json["business_value"] || 0)
|
47
|
+
file.instance_variable_set(:@is_binary, false)
|
48
|
+
file
|
49
|
+
end
|
50
|
+
|
51
|
+
def touch(commit, file_diff)
|
52
|
+
@path = file_diff.b_file_name
|
53
|
+
@file_id = file_diff.b_file_id if not file_diff.b_file_id.nil?
|
54
|
+
@previous_file_id = file_diff.a_file_id
|
55
|
+
@touched_at_times << commit.date
|
56
|
+
@commits << commit.id
|
57
|
+
@issues << commit.issue_id if commit.issue_id.present?
|
58
|
+
@bugs << commit.bug_id if commit.bug_id.present?
|
59
|
+
@business_value += diff_business_value(commit, file_diff)
|
60
|
+
end
|
61
|
+
|
62
|
+
def save()
|
63
|
+
@functions = Tracer
|
64
|
+
.new
|
65
|
+
.trace(@path, as_text())
|
66
|
+
.map { |function| VirtualFunction.new(function, @path, @lines) }
|
67
|
+
|
68
|
+
Cache::write_object(@file_id, as_json())
|
69
|
+
end
|
70
|
+
|
71
|
+
def apply_file_diff!(commit, file_diff)
|
72
|
+
touch(commit, file_diff)
|
73
|
+
return if binary?
|
74
|
+
|
75
|
+
context = file_diff.diffs
|
76
|
+
.reduce(new_context(commit), &method(:apply_diff))
|
77
|
+
|
78
|
+
new_lines = context[:new_lines]
|
79
|
+
|
80
|
+
unchanged_lines = @lines[context[:position]..-1]
|
81
|
+
new_lines.concat(unchanged_lines) if unchanged_lines
|
82
|
+
|
83
|
+
@lines = new_lines
|
84
|
+
end
|
85
|
+
|
86
|
+
def binary?
|
87
|
+
@is_binary
|
88
|
+
end
|
89
|
+
|
90
|
+
def plain_text
|
91
|
+
@lines.map { |line| line[:text] }
|
92
|
+
end
|
93
|
+
|
94
|
+
def created_at
|
95
|
+
@touched_at_times.first
|
96
|
+
end
|
97
|
+
|
98
|
+
def last_updated_at
|
99
|
+
@touched_at_times.last
|
100
|
+
end
|
101
|
+
|
102
|
+
def change_line!(line_change, line_number)
|
103
|
+
@lines[line_number] = Line::change!(line_change, @lines[line_number])
|
104
|
+
end
|
105
|
+
|
106
|
+
def move_line!(line_movement, line_number)
|
107
|
+
line = @lines[line_number]
|
108
|
+
if line_movement[:text].strip() != line[:text].strip()
|
109
|
+
# TODO: raise reconstruction error.
|
110
|
+
Loggr.instance.warn("MISMATCH: #{line[:text]} VS #{line_movement[:text]}")
|
111
|
+
else
|
112
|
+
@lines[line_number] = Line::move!(line_movement, line)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def merge_coverage!(file_coverage)
|
117
|
+
@lines.each_with_index do |line, index|
|
118
|
+
line[:coverage] = file_coverage.dig("lines", (index + 1).to_s, "hits")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def merge_error!(error, trace, depth)
|
123
|
+
line = @lines[trace[:line] - 1]
|
124
|
+
Line::merge_error!(line, error, depth)
|
125
|
+
end
|
126
|
+
|
127
|
+
def as_json()
|
128
|
+
{
|
129
|
+
path: @path,
|
130
|
+
previous_file_id: @previous_file_id,
|
131
|
+
lines: @lines,
|
132
|
+
functions: @functions.map { |function| function.as_json() },
|
133
|
+
commits: @commits,
|
134
|
+
issues: @issues,
|
135
|
+
business_value: @business_value,
|
136
|
+
bugs: @bugs
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def revisions_total()
|
141
|
+
@lines
|
142
|
+
.map { |line| line[:revisions].count }
|
143
|
+
.reduce(0, &:+)
|
144
|
+
end
|
145
|
+
|
146
|
+
def errors_total()
|
147
|
+
@lines
|
148
|
+
.map do |line|
|
149
|
+
line[:revisions].flat_map { |revision| revision[:errors] }.count +
|
150
|
+
line[:errors].count
|
151
|
+
end
|
152
|
+
.reduce(0, &:+)
|
153
|
+
end
|
154
|
+
|
155
|
+
def buggy_lines_total()
|
156
|
+
@lines
|
157
|
+
.map do |line|
|
158
|
+
line[:revisions].flat_map { |revision| revision[:bugs] }.count +
|
159
|
+
line[:bugs].count
|
160
|
+
end
|
161
|
+
.reduce(0, &:+)
|
162
|
+
end
|
163
|
+
|
164
|
+
def covered_lines
|
165
|
+
@lines
|
166
|
+
.select { |line| line[:coverage] != nil && line[:coverage] > 0 }
|
167
|
+
.count
|
168
|
+
end
|
169
|
+
|
170
|
+
def uncovered_lines
|
171
|
+
@lines
|
172
|
+
.select { |line| line[:coverage] == 0 }
|
173
|
+
.count
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def as_text()
|
179
|
+
@lines
|
180
|
+
.map { |line| line[:text] }
|
181
|
+
.join("\n")
|
182
|
+
end
|
183
|
+
|
184
|
+
def apply_diff(acc, diff)
|
185
|
+
delete_start = diff.delete_start
|
186
|
+
delete_count = diff.delete_count
|
187
|
+
copy_end = delete_count == 0 ?
|
188
|
+
delete_start :
|
189
|
+
delete_start - 1
|
190
|
+
|
191
|
+
new_lines = diff.insertions
|
192
|
+
.each_with_index
|
193
|
+
.map do |line, index|
|
194
|
+
line_num = diff.insert_start + index
|
195
|
+
Line::new_line(line, @path, line_num, acc[:commit])
|
196
|
+
end
|
197
|
+
unchanged_lines = acc[:existing_lines][acc[:position]...copy_end]
|
198
|
+
|
199
|
+
acc[:new_lines].concat(unchanged_lines) if unchanged_lines
|
200
|
+
acc[:position] = copy_end + delete_count
|
201
|
+
|
202
|
+
acc[:new_lines].concat(new_lines)
|
203
|
+
acc
|
204
|
+
end
|
205
|
+
|
206
|
+
def diff_business_value(commit, file_diff)
|
207
|
+
(file_diff.changes_total / commit.changes_total.to_f) * commit.business_value
|
208
|
+
end
|
209
|
+
|
210
|
+
def new_context(commit)
|
211
|
+
{
|
212
|
+
position: 0,
|
213
|
+
new_lines: [],
|
214
|
+
existing_lines: @lines,
|
215
|
+
commit: commit
|
216
|
+
}
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative "virtual_tree"
|
2
|
+
require_relative "virtual_file"
|
3
|
+
require_relative "cache"
|
4
|
+
require_relative "store"
|
5
|
+
require "active_support/core_ext/module/delegation"
|
6
|
+
|
7
|
+
class VirtualFileSystem
|
8
|
+
|
9
|
+
attr_reader :root
|
10
|
+
|
11
|
+
def initialize()
|
12
|
+
@root = VirtualTree.new({path: ""}, {})
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](file_name)
|
16
|
+
@root.get_file(file_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def file_for_diff(commit, diff)
|
20
|
+
if diff.operation == :move
|
21
|
+
@root.move_file(diff.a_file_name, diff.b_file_name)
|
22
|
+
elsif diff.operation == :delete
|
23
|
+
@root.delete_file(diff.a_file_name)
|
24
|
+
else
|
25
|
+
file_name = diff.b_file_name
|
26
|
+
file = @root.get_file(file_name)
|
27
|
+
file = @root.set_file(file_name, VirtualFile.new(file_name, diff)) if file.nil?
|
28
|
+
file
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def load!(commits, meta)
|
33
|
+
commit = commits.first
|
34
|
+
return commits if commit.nil? || commit.cached_files.nil?
|
35
|
+
commit.cached_trees.each do |tree_object|
|
36
|
+
tree_id = tree_object[:object_id]
|
37
|
+
cached_tree = Cache.read_object(tree_id)
|
38
|
+
@root.add_tree(VirtualTree.from_json(cached_tree))
|
39
|
+
end
|
40
|
+
commit.cached_files.each do |file_object|
|
41
|
+
file_id = file_object[:object_id]
|
42
|
+
cached_file = Cache.read_object(file_id)
|
43
|
+
if not cached_file.nil?
|
44
|
+
@root.set_file(file_object[:path], VirtualFile.from_json(file_id, cached_file))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
sync_meta!(commit, meta)
|
48
|
+
commits
|
49
|
+
end
|
50
|
+
|
51
|
+
def touch_tree(diff_tree, commit)
|
52
|
+
@root.touch(diff_tree, commit)
|
53
|
+
end
|
54
|
+
|
55
|
+
def save(commit)
|
56
|
+
prune(commit)
|
57
|
+
@root.save(commit)
|
58
|
+
end
|
59
|
+
|
60
|
+
def file_paths
|
61
|
+
root
|
62
|
+
.all_files()
|
63
|
+
.map { |file| file.path }
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def prune(commit)
|
69
|
+
commit
|
70
|
+
.trees
|
71
|
+
.reverse()
|
72
|
+
.each { |diff_tree| @root.prune(diff_tree, commit) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def sync_meta!(commit, meta)
|
76
|
+
meta[:errors] && meta[:errors].sync!(commit)
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class VirtualFunction
|
2
|
+
def initialize(function, file, lines)
|
3
|
+
@name = function[:name]
|
4
|
+
@file = file
|
5
|
+
@start = function[:start]
|
6
|
+
@end = function[:end]
|
7
|
+
@lines = lines[(@start - 1)..@end]
|
8
|
+
end
|
9
|
+
|
10
|
+
def as_json()
|
11
|
+
commits = {}
|
12
|
+
issues = {}
|
13
|
+
errors = {}
|
14
|
+
bugs = {}
|
15
|
+
score = 0
|
16
|
+
|
17
|
+
@lines.each do |line|
|
18
|
+
commits[line[:commit]] = true
|
19
|
+
line[:revisions].each { |revision| commits[revision[:commit]] = true }
|
20
|
+
line[:issues].each { |issue| issues[issue] = true }
|
21
|
+
line[:errors].each { |error| errors[error] = true }
|
22
|
+
line[:bugs].each { |bug| bugs[bug] = true }
|
23
|
+
score += Line.score(line)
|
24
|
+
end
|
25
|
+
|
26
|
+
{
|
27
|
+
name: @name,
|
28
|
+
file: @file,
|
29
|
+
start: @start,
|
30
|
+
end: @end,
|
31
|
+
bugs: bugs.keys,
|
32
|
+
commits: commits.keys,
|
33
|
+
issues: issues.keys,
|
34
|
+
errors: errors.keys,
|
35
|
+
score: score
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
data/lib/virtual_tree.rb
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
require "active_support/core_ext/object"
|
2
|
+
|
3
|
+
require_relative "cache"
|
4
|
+
|
5
|
+
class VirtualTree
|
6
|
+
attr_reader :path
|
7
|
+
attr_reader :tree_id
|
8
|
+
attr_reader :previous_tree_id
|
9
|
+
attr_reader :issues
|
10
|
+
attr_reader :commits
|
11
|
+
attr_reader :files
|
12
|
+
attr_reader :trees
|
13
|
+
|
14
|
+
def initialize(diff_tree, commit)
|
15
|
+
@path = diff_tree[:path]
|
16
|
+
@tree_id = nil
|
17
|
+
@previous_tree_id = nil
|
18
|
+
@commits = []
|
19
|
+
@issues = []
|
20
|
+
@files = {}
|
21
|
+
@trees = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_json(tree_json)
|
25
|
+
new_tree = VirtualTree.new({path: tree_json["path"]}, {})
|
26
|
+
new_tree.instance_variable_set(:@commits, tree_json["commits"])
|
27
|
+
new_tree.instance_variable_set(:@issues, tree_json["issues"])
|
28
|
+
new_tree.instance_variable_set(:@tree_id, tree_json["tree_id"])
|
29
|
+
new_tree.instance_variable_set(:@previous_tree_id, tree_json["previous_tree_id"])
|
30
|
+
new_tree
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_file(file_path)
|
34
|
+
file_subtree(file_path).files[file_path]
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_file(file_path, file)
|
38
|
+
file_subtree(file_path).files[file_path] = file
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete_file(file_path)
|
42
|
+
file_subtree(file_path)
|
43
|
+
.files
|
44
|
+
.delete(file_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
def move_file(a_file_name, b_file_name)
|
48
|
+
file = get_file(a_file_name)
|
49
|
+
delete_file(a_file_name)
|
50
|
+
set_file(b_file_name, file)
|
51
|
+
end
|
52
|
+
|
53
|
+
# TODO: What happens when a tree moves??
|
54
|
+
def touch(diff_tree, commit)
|
55
|
+
if @path == diff_tree[:path]
|
56
|
+
@previous_tree_id = diff_tree[:a_tree_id]
|
57
|
+
@tree_id = diff_tree[:b_tree_id]
|
58
|
+
@commits << commit.id
|
59
|
+
@issues << commit.issue_id if commit.issue_id.present?
|
60
|
+
else
|
61
|
+
find_or_create_subtree!(diff_tree, commit).touch(diff_tree, commit)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def prune(diff_tree, commit)
|
66
|
+
return if diff_tree[:operation] != :delete
|
67
|
+
|
68
|
+
tree_name = tree_name(diff_tree[:path])
|
69
|
+
subtree(file_tree_path(diff_tree[:path]))
|
70
|
+
.trees
|
71
|
+
.delete(tree_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def all_files()
|
75
|
+
@trees
|
76
|
+
.values
|
77
|
+
.flat_map { |tree| tree.all_files() }
|
78
|
+
.concat(@files.values)
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO: Figure out how to save only when something changes.
|
82
|
+
def save(commit)
|
83
|
+
trees_json = @trees
|
84
|
+
.values
|
85
|
+
.map { |tree| tree.save(commit) }
|
86
|
+
|
87
|
+
json = trees_json
|
88
|
+
.reduce(as_json()) do |acc, tree_json|
|
89
|
+
[
|
90
|
+
:lines,
|
91
|
+
:covered_lines,
|
92
|
+
:uncovered_lines,
|
93
|
+
:changes,
|
94
|
+
:errors,
|
95
|
+
:buggy_lines,
|
96
|
+
:business_value
|
97
|
+
].each do |key|
|
98
|
+
acc[key] += tree_json[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
acc
|
102
|
+
end
|
103
|
+
|
104
|
+
relevant_lines = json[:covered_lines] + json[:uncovered_lines]
|
105
|
+
json[:coverage_percent] = relevant_lines > 0 ?
|
106
|
+
(json[:covered_lines] / relevant_lines.to_f * 100).round :
|
107
|
+
0
|
108
|
+
|
109
|
+
json[:trees] = trees_json.map(&method(:slim_tree_json))
|
110
|
+
|
111
|
+
tree_id = @tree_id.present? ? @tree_id : commit.id
|
112
|
+
Cache.write_object(tree_id, json)
|
113
|
+
|
114
|
+
json
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_tree(tree)
|
118
|
+
tree_name = tree_name(tree.path)
|
119
|
+
subtree = subtree(tree.path)
|
120
|
+
if subtree.path == tree.path
|
121
|
+
Loggr.instance.warn("Cannot add pre-existing tree: #{tree.path}")
|
122
|
+
else
|
123
|
+
subtree.trees[tree_name] = tree
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def as_json()
|
128
|
+
stats = @files
|
129
|
+
.values
|
130
|
+
.reduce(new_context()) do |acc, file|
|
131
|
+
acc[:business_value] += file.business_value
|
132
|
+
acc[:lines] += file.lines.count
|
133
|
+
acc[:covered_lines] += file.covered_lines
|
134
|
+
acc[:uncovered_lines] += file.uncovered_lines
|
135
|
+
acc[:buggy_lines] += file.buggy_lines_total
|
136
|
+
acc[:changes] += file.revisions_total
|
137
|
+
acc[:errors] += file.errors_total
|
138
|
+
acc
|
139
|
+
end
|
140
|
+
|
141
|
+
{
|
142
|
+
path: @path,
|
143
|
+
tree_id: @tree_id,
|
144
|
+
previous_tree_id: @previous_tree_id,
|
145
|
+
issues: @issues,
|
146
|
+
commits: @commits,
|
147
|
+
files: @files.values.map(&method(:file_json))
|
148
|
+
}.merge(stats)
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def subtree(path)
|
154
|
+
dirs = path.split(File::SEPARATOR)
|
155
|
+
subtree = self
|
156
|
+
while dirs.length > 0
|
157
|
+
path = dirs.shift()
|
158
|
+
subtree = subtree.trees[path] if subtree.trees.has_key?(path)
|
159
|
+
end
|
160
|
+
subtree
|
161
|
+
end
|
162
|
+
|
163
|
+
def file_tree_path(file_path)
|
164
|
+
file_path
|
165
|
+
.split(File::SEPARATOR)[0..-2]
|
166
|
+
.join(File::SEPARATOR)
|
167
|
+
end
|
168
|
+
|
169
|
+
def tree_name(tree_path)
|
170
|
+
tree_path
|
171
|
+
.split(File::SEPARATOR)
|
172
|
+
.last
|
173
|
+
end
|
174
|
+
|
175
|
+
def file_subtree(file_path)
|
176
|
+
subtree(file_tree_path(file_path))
|
177
|
+
end
|
178
|
+
|
179
|
+
def find_or_create_subtree!(diff_tree, commit)
|
180
|
+
subtree = subtree(diff_tree[:path])
|
181
|
+
if subtree.path != diff_tree[:path]
|
182
|
+
subtree_path = diff_tree[:path]
|
183
|
+
.split(File::SEPARATOR)
|
184
|
+
.last
|
185
|
+
subtree.trees[subtree_path] = subtree = VirtualTree.new(diff_tree, commit)
|
186
|
+
end
|
187
|
+
subtree
|
188
|
+
end
|
189
|
+
|
190
|
+
def slim_tree_json(tree_json)
|
191
|
+
{
|
192
|
+
path: tree_json[:path],
|
193
|
+
tree_id: tree_json[:tree_id],
|
194
|
+
commits: tree_json[:commits].length,
|
195
|
+
issues: tree_json[:commits].length,
|
196
|
+
business_value: tree_json[:business_value],
|
197
|
+
lines: tree_json[:lines],
|
198
|
+
changes: tree_json[:changes],
|
199
|
+
errors: tree_json[:errors],
|
200
|
+
buggy_lines: tree_json[:buggy_lines],
|
201
|
+
covered_lines: tree_json[:covered_lines],
|
202
|
+
uncovered_lines: tree_json[:uncovered_lines]
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
def file_json(file)
|
207
|
+
{
|
208
|
+
path: file.path,
|
209
|
+
file_id: file.file_id,
|
210
|
+
commits: file.commits.length,
|
211
|
+
issues: file.issues.length,
|
212
|
+
business_value: file.business_value,
|
213
|
+
lines: file.lines.count,
|
214
|
+
changes: file.revisions_total,
|
215
|
+
errors: file.errors_total,
|
216
|
+
buggy_lines: file.buggy_lines_total,
|
217
|
+
covered_lines: file.covered_lines,
|
218
|
+
uncovered_lines: file.uncovered_lines
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
def new_context()
|
223
|
+
{
|
224
|
+
business_value: 0,
|
225
|
+
lines: 0,
|
226
|
+
changes: 0,
|
227
|
+
errors: 0,
|
228
|
+
buggy_lines: 0,
|
229
|
+
covered_lines: 0,
|
230
|
+
uncovered_lines: 0
|
231
|
+
}
|
232
|
+
end
|
233
|
+
end
|