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,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