gitolemy 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,10 @@
1
+ def fold_reducer(regex)
2
+ lambda do |acc, line|
3
+ if line.match(regex)
4
+ acc << [line]
5
+ else
6
+ acc.last << line
7
+ end
8
+ acc
9
+ end
10
+ end
@@ -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
@@ -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