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/diff.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "active_support/core_ext/string"
|
2
|
+
|
3
|
+
class Diff
|
4
|
+
|
5
|
+
attr_accessor :insertions
|
6
|
+
attr_accessor :deletions
|
7
|
+
attr_accessor :delete_start
|
8
|
+
attr_accessor :delete_count
|
9
|
+
attr_accessor :insert_start
|
10
|
+
attr_accessor :insert_count
|
11
|
+
|
12
|
+
def initialize(attrs={})
|
13
|
+
attrs.each do |key, val|
|
14
|
+
instance_variable_set("@#{key}".to_sym, val)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def from_git(diff_lines)
|
22
|
+
Diff.new(parse_diff(diff_lines))
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_diff(diff_lines)
|
26
|
+
stats = diff_lines
|
27
|
+
.first
|
28
|
+
.split("@@")[1]
|
29
|
+
.split(" ")
|
30
|
+
|
31
|
+
delete = stats.first.split(",")
|
32
|
+
add = stats.last.split(",")
|
33
|
+
|
34
|
+
delete_start = delete.first[1..-1].to_i
|
35
|
+
delete_count = delete.length > 1 ? delete.last.to_i : 1
|
36
|
+
add_start = add.first[1..-1].to_i
|
37
|
+
add_count = add.length > 1 ? add.last.to_i : 1
|
38
|
+
|
39
|
+
diffs = diff_lines[1..-1]
|
40
|
+
patch = {
|
41
|
+
insertions: [],
|
42
|
+
deletions: [],
|
43
|
+
delete_start: delete_start,
|
44
|
+
delete_count: delete_count,
|
45
|
+
insert_start: add_start,
|
46
|
+
insert_count: add_count
|
47
|
+
}
|
48
|
+
|
49
|
+
diffs.reduce(patch, &method(:fold_lines_standard))
|
50
|
+
end
|
51
|
+
|
52
|
+
def fold_lines_standard(acc, diff_line)
|
53
|
+
diff = diff_line
|
54
|
+
line = diff_line[1..-1]
|
55
|
+
|
56
|
+
if diff.first == "+"
|
57
|
+
acc[:insertions] << line
|
58
|
+
elsif diff.first == "-"
|
59
|
+
acc[:deletions] << line
|
60
|
+
end
|
61
|
+
|
62
|
+
acc
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/file_diff.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative "util"
|
2
|
+
require_relative "diff"
|
3
|
+
|
4
|
+
class FileDiff
|
5
|
+
DIFF_REGEX = /^@@ -\d+(,\d+)? \+\d+(,\d+)? @@/
|
6
|
+
FILE_INDEX_REGEX = /index [0-9a-f]{7,40}/
|
7
|
+
BINARY_REGEX = /^Binary files \/dev\/null and/
|
8
|
+
DELETION_REGEX = /\[-.*?-\]/
|
9
|
+
INSERTION_REGEX = /\{\+.*?\+\}/
|
10
|
+
DIFF_STAT_REGEX = /^\d+\t\d+\t/
|
11
|
+
DELETE_REGEX = /$\[-.*?-\]$/
|
12
|
+
DELETE_OP_REGEX = /^deleted file/
|
13
|
+
|
14
|
+
attr_accessor :operation
|
15
|
+
attr_accessor :a_file_name
|
16
|
+
attr_accessor :b_file_name
|
17
|
+
attr_accessor :a_file_id
|
18
|
+
attr_accessor :b_file_id
|
19
|
+
attr_accessor :diffs
|
20
|
+
attr_accessor :insertions_total
|
21
|
+
attr_accessor :deletions_total
|
22
|
+
attr_accessor :changes_total
|
23
|
+
|
24
|
+
def initialize(attrs={})
|
25
|
+
attrs.each do |key, val|
|
26
|
+
instance_variable_set("@#{key}".to_sym, val)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def from_git(file_diff_lines)
|
32
|
+
FileDiff.new(parse_file_diff(file_diff_lines))
|
33
|
+
end
|
34
|
+
|
35
|
+
# TODO: Handle different file modes like new, move, and delete.
|
36
|
+
def parse_file_diff(file_diff_lines)
|
37
|
+
file_names = file_diff_lines
|
38
|
+
.first
|
39
|
+
.sub("diff --git a/", "")
|
40
|
+
.split(" b/")
|
41
|
+
|
42
|
+
a_file_name = file_names.first.strip()
|
43
|
+
b_file_name = file_names.last.strip()
|
44
|
+
|
45
|
+
file_index = file_diff_lines
|
46
|
+
.each_index
|
47
|
+
.detect { |i| file_diff_lines[i].match(FILE_INDEX_REGEX) }
|
48
|
+
|
49
|
+
if file_index.nil?
|
50
|
+
a_file_id = nil
|
51
|
+
b_file_id = nil
|
52
|
+
else
|
53
|
+
a_file_id, b_file_id = file_diff_lines[file_index]
|
54
|
+
.sub("index ", "")
|
55
|
+
.split(" ")
|
56
|
+
.first
|
57
|
+
.split("..")
|
58
|
+
end
|
59
|
+
|
60
|
+
if file_diff_lines.last.match(BINARY_REGEX)
|
61
|
+
operation = :binary
|
62
|
+
elsif (a_file_name != b_file_name)
|
63
|
+
operation = :move
|
64
|
+
elsif (file_diff_lines[1].match(DELETE_OP_REGEX))
|
65
|
+
operation = :delete
|
66
|
+
else
|
67
|
+
operation = :change
|
68
|
+
end
|
69
|
+
|
70
|
+
diff_index = file_diff_lines
|
71
|
+
.each_index
|
72
|
+
.detect { |i| file_diff_lines[i].match(DIFF_REGEX) }
|
73
|
+
|
74
|
+
if diff_index.nil?
|
75
|
+
diffs = []
|
76
|
+
else
|
77
|
+
diffs = file_diff_lines[diff_index..-1]
|
78
|
+
.reduce([], &fold_reducer(DIFF_REGEX))
|
79
|
+
.flat_map { |diff_lines| Diff.from_git(diff_lines) }
|
80
|
+
end
|
81
|
+
|
82
|
+
insertions = diffs.map { |diff| diff.insert_count }.reduce(0, :+)
|
83
|
+
deletions = diffs.map { |diff| diff.delete_count }.reduce(0, :+)
|
84
|
+
|
85
|
+
{
|
86
|
+
operation: operation,
|
87
|
+
a_file_name: a_file_name,
|
88
|
+
b_file_name: b_file_name,
|
89
|
+
a_file_id: a_file_id,
|
90
|
+
b_file_id: b_file_id,
|
91
|
+
diffs: diffs,
|
92
|
+
insertions_total: insertions,
|
93
|
+
deletions_total: deletions,
|
94
|
+
changes_total: insertions + deletions
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/file_helper.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "byebug"
|
2
|
+
|
3
|
+
require_relative "loggr"
|
4
|
+
|
5
|
+
module FileHelper
|
6
|
+
def fdiff_stat(diff)
|
7
|
+
"@@ -#{diff.delete_start},#{diff.delete_count} +#{diff.insert_start},#{diff.insert_count} @@"
|
8
|
+
end
|
9
|
+
|
10
|
+
def print_context(lines, from, to=nil)
|
11
|
+
if to.nil?
|
12
|
+
to = from + 3
|
13
|
+
from -= 4
|
14
|
+
end
|
15
|
+
(from...to).each do |i|
|
16
|
+
line = lines[i]
|
17
|
+
line = "nil" if line.nil?
|
18
|
+
puts "#{i + 1}: #{line}"
|
19
|
+
end
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def compare(commit, file_name, lines, context=nil, diff=nil)
|
24
|
+
if diff.nil?
|
25
|
+
Loggr.instance.info("COMPARE COMMIT: #{commit.id}")
|
26
|
+
else
|
27
|
+
Loggr.instance.info("COMPARE fdiff: #{fdiff_stat(diff)}")
|
28
|
+
end
|
29
|
+
|
30
|
+
real_file = `git show #{commit.id}:#{file_name}`
|
31
|
+
.encode("UTF-8", invalid: :replace)
|
32
|
+
.lines
|
33
|
+
.map(&:chomp)
|
34
|
+
|
35
|
+
return if real_file.length == 0
|
36
|
+
if real_file.length != lines.length && diff.nil?
|
37
|
+
puts "FILE SIZE MISMATCH:"
|
38
|
+
puts "REAL: #{real_file.length}"
|
39
|
+
puts "RECONSTRUCT: #{lines.length}"
|
40
|
+
byebug
|
41
|
+
1
|
42
|
+
end
|
43
|
+
|
44
|
+
lines
|
45
|
+
.each_with_index
|
46
|
+
.map do |line, line_number|
|
47
|
+
if real_file[line_number] != line.gsub(/\r$/, "")
|
48
|
+
puts "MISMATCH: #{commit.id}:#{file_name}"
|
49
|
+
puts "REAL:"
|
50
|
+
print_context(real_file, line_number)
|
51
|
+
puts "RECONSTRUCT"
|
52
|
+
print_context(lines, line_number)
|
53
|
+
byebug
|
54
|
+
1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/file_manager.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require "active_support/core_ext/hash"
|
2
|
+
|
3
|
+
require_relative "virtual_file_system"
|
4
|
+
require_relative "file_helper"
|
5
|
+
require_relative "loggr"
|
6
|
+
require_relative "store"
|
7
|
+
|
8
|
+
class FileManager
|
9
|
+
include FileHelper
|
10
|
+
|
11
|
+
attr_reader :vfs
|
12
|
+
|
13
|
+
def initialize(branch)
|
14
|
+
@branch = branch
|
15
|
+
@vfs = VirtualFileSystem.new()
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply!(commits, meta)
|
19
|
+
vfs.load!(commits, meta)
|
20
|
+
.each do |commit|
|
21
|
+
# TODO: Won't work for first commit...
|
22
|
+
coverage = meta[:coverage].get_report(commit.id, @vfs.file_paths)
|
23
|
+
errors = meta[:errors]&.get_errors!(commit) || []
|
24
|
+
apply_commit!(commit, errors, coverage)
|
25
|
+
Cache.index_commit(@branch, commit.id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def apply_commit!(commit, errors, coverage)
|
32
|
+
Loggr.instance.info("APPLY COMMIT: #{commit.id}")
|
33
|
+
|
34
|
+
movements = extract_changes(commit.movements)
|
35
|
+
changes = extract_changes(commit.changes)
|
36
|
+
|
37
|
+
commit.trees.each { |diff_tree| @vfs.touch_tree(diff_tree, commit) }
|
38
|
+
|
39
|
+
files = commit
|
40
|
+
.file_diffs
|
41
|
+
.map { |file_name, file_diff| apply_file_diff!(commit, file_diff) }
|
42
|
+
|
43
|
+
changes.each(&method(:merge_change!))
|
44
|
+
movements.each(&method(:merge_movement!))
|
45
|
+
|
46
|
+
merge_coverage!(coverage) if not coverage.nil?
|
47
|
+
errors.each { |error| apply_error!(error) }
|
48
|
+
|
49
|
+
@vfs.save(commit)
|
50
|
+
files.each(&:save)
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_file_diff!(commit, file_diff)
|
54
|
+
file = @vfs.file_for_diff(commit, file_diff)
|
55
|
+
file.apply_file_diff!(commit, file_diff)
|
56
|
+
compare(commit, file.path, file.plain_text(), nil, nil) if compare?(file)
|
57
|
+
file
|
58
|
+
end
|
59
|
+
|
60
|
+
def apply_error!(error)
|
61
|
+
error[:stack_trace].each_with_index do |trace, i|
|
62
|
+
file = trace_file(trace[:file])
|
63
|
+
if file.nil?
|
64
|
+
Loggr.instance.error("Trace Not Found: #{trace[:file]}:#{trace[:line]} - #{trace[:message]}")
|
65
|
+
else
|
66
|
+
file.merge_error!(error, trace, i) if not file.nil?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
rescue
|
70
|
+
Loggr.instance.error("CANNOT APPLLY ERROR: #{error[:error_id]}")
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_changes(changes)
|
74
|
+
changes.reduce({}) do |acc, (dest_file, changes)|
|
75
|
+
changes.each do |dest_line, change|
|
76
|
+
acc[dest_file] ||= {}
|
77
|
+
acc[dest_file][dest_line] = @vfs[change[:from]].lines[change[:line]]
|
78
|
+
end
|
79
|
+
acc
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def merge_change!(file, file_line_changes)
|
84
|
+
file_line_changes.each do |(line_number, line_change)|
|
85
|
+
@vfs[file].change_line!(line_change, line_number)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def merge_movement!(file, file_line_movements)
|
90
|
+
file_line_movements.each do |(line_number, line_movement)|
|
91
|
+
@vfs[file].move_line!(line_movement, line_number)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# TODO: Technically, file lines aren't covered. Lines at commits are.
|
96
|
+
# 1: To avoid ambiguity, maybe only view trees by commit.
|
97
|
+
def merge_coverage!(coverage)
|
98
|
+
coverage.each do |file, file_coverage|
|
99
|
+
begin
|
100
|
+
@vfs[file].merge_coverage!(file_coverage)
|
101
|
+
@vfs[file].save()
|
102
|
+
rescue => ex
|
103
|
+
Loggr.instance.error("Coverage File Not Found: #{file}")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def trace_file(trace_file)
|
109
|
+
match = @vfs.file_paths.detect { |app_file| trace_file.include?(app_file) }
|
110
|
+
match.nil? ? nil : @vfs[match]
|
111
|
+
end
|
112
|
+
|
113
|
+
def compare?(file)
|
114
|
+
ENV["GITOLEMY_COMPARE"] == "true" && !file.binary?
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
class CSyntaxTracer
|
2
|
+
def trace(code)
|
3
|
+
code = strip_comments(code)
|
4
|
+
find_funcs(code)
|
5
|
+
.map { |func| define_func(func, code) }
|
6
|
+
rescue
|
7
|
+
[]
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def find_funcs(code)
|
13
|
+
funcs = code.scan(/[a-zA-Z \t]*function\s+[A-Za-z_][A-Za-z0-9_]*/)
|
14
|
+
funcs += code.scan(/[a-zA-Z0-9_. \t]*\s*=\s*function/)
|
15
|
+
|
16
|
+
funcs = funcs
|
17
|
+
.each_with_index
|
18
|
+
.map do |func, index|
|
19
|
+
if func.match(/[a-zA-Z0-9_]*\s+=/)
|
20
|
+
name = func
|
21
|
+
.split("=")
|
22
|
+
.last(2)
|
23
|
+
.first
|
24
|
+
.split(/[ \t.]/)
|
25
|
+
.last
|
26
|
+
else
|
27
|
+
name = func.split(" ").last
|
28
|
+
end
|
29
|
+
|
30
|
+
[name] + code
|
31
|
+
.lines
|
32
|
+
.each_with_index
|
33
|
+
.select { |line, line_num| line.match(func) }
|
34
|
+
.first
|
35
|
+
end
|
36
|
+
|
37
|
+
funcs
|
38
|
+
.each_with_index
|
39
|
+
.map do |func, index|
|
40
|
+
if funcs.count > index + 2
|
41
|
+
func << funcs[index + 1].last
|
42
|
+
else
|
43
|
+
func << code.lines.count
|
44
|
+
end
|
45
|
+
func
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def define_func(func, code)
|
50
|
+
function_code = code.lines[func[2]...func[3]]
|
51
|
+
indent = function_code
|
52
|
+
.first
|
53
|
+
.match(/^\s*/)[0]
|
54
|
+
|
55
|
+
close = function_code
|
56
|
+
.each_with_index
|
57
|
+
.select { |line, line_num| line.match(Regexp.compile("^#{indent}}")) }
|
58
|
+
.first
|
59
|
+
|
60
|
+
close ||= function_code
|
61
|
+
.each_with_index
|
62
|
+
.map { |line, line_num| [line.match(/}/), line_num] }
|
63
|
+
.select { |line| line.first }
|
64
|
+
.reverse
|
65
|
+
.first
|
66
|
+
|
67
|
+
end_line = close.nil? ? func[3] : func[2] + close.last + 1
|
68
|
+
|
69
|
+
{
|
70
|
+
name: func.first,
|
71
|
+
start: func[2] + 1,
|
72
|
+
end: end_line
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def strip_comments(code)
|
77
|
+
block_captures.each do |block_capture|
|
78
|
+
matches = code.scan(block_capture[:regex])
|
79
|
+
matches.each do |match|
|
80
|
+
code = code.gsub(match, "#{block_capture[:start]} #{block_capture[:type]}" + "\n" * (match.lines.count - 1) + block_capture[:end])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
line_captures.each do |line_capture|
|
85
|
+
code = code.gsub(line_capture[:regex], "#{line_capture[:start]} #{line_capture[:type]}")
|
86
|
+
end
|
87
|
+
|
88
|
+
code
|
89
|
+
end
|
90
|
+
|
91
|
+
def line_captures()
|
92
|
+
[
|
93
|
+
{
|
94
|
+
regex: /\/\/.*/,
|
95
|
+
type: "LINE COMMENT",
|
96
|
+
start: "//"
|
97
|
+
}
|
98
|
+
]
|
99
|
+
end
|
100
|
+
|
101
|
+
def block_captures()
|
102
|
+
[
|
103
|
+
{
|
104
|
+
regex: /\/\*.*?\*\//m,
|
105
|
+
type: "BLOCK QUOTE",
|
106
|
+
start: "/*",
|
107
|
+
end: "*/"
|
108
|
+
}
|
109
|
+
]
|
110
|
+
end
|
111
|
+
end
|