gitolemy 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,103 @@
1
+ require "byebug"
2
+
3
+ class PythonTracer
4
+ def trace(code)
5
+ code = strip_comments(code)
6
+ find_funcs(code)
7
+ .map { |func| define_func(func, code) }
8
+ rescue => e
9
+ byebug
10
+ []
11
+ end
12
+
13
+ private
14
+
15
+ def find_funcs(code)
16
+ funcs = code.scan(/[ \t]*def\s+[A-Za-z_][A-Za-z0-9_]*.*?\)\:/m)
17
+
18
+ funcs = funcs
19
+ .each_with_index
20
+ .map do |func, index|
21
+ name = func.split("(").first.split(" ").last
22
+
23
+ [name, func.lines.count] + code
24
+ .lines
25
+ .each_with_index
26
+ .select { |line, line_num| line.match(func.split("(").first) }
27
+ .first
28
+ end
29
+
30
+ funcs
31
+ .each_with_index
32
+ .map do |func, index|
33
+ if funcs.count > index + 2
34
+ func << funcs[index + 1].last
35
+ else
36
+ func << code.lines.count
37
+ end
38
+ func
39
+ end
40
+ end
41
+
42
+ def define_func(func, code)
43
+ function_code = code.lines[func[3]...func[4]]
44
+ indent = function_code
45
+ .first[/^\s*/]
46
+ .size
47
+
48
+ close = function_code[func[1]..-1]
49
+ .each_with_index
50
+ .select do |line, line_num|
51
+ line_indent = line[/^\s*/].size
52
+ line.match(/[^\s]/) && line_indent <= indent
53
+ end
54
+ .first
55
+
56
+ end_line = close.nil? ? func[4] : func[3] + close.last + func[1]
57
+ while code.lines[end_line - 1].match(/^\s+$/) do
58
+ end_line -= 1
59
+ end
60
+
61
+ {
62
+ name: func.first,
63
+ start: func[3] + 1,
64
+ end: end_line
65
+ }
66
+ end
67
+
68
+ def strip_comments(code)
69
+ block_captures.each do |block_capture|
70
+ matches = code.scan(block_capture[:regex])
71
+ matches.each do |match|
72
+ code = code.gsub(match, "#{block_capture[:start]} #{block_capture[:type]}" + "\n" * (match.lines.count - 1) + block_capture[:end])
73
+ end
74
+ end
75
+
76
+ line_captures.each do |line_capture|
77
+ code = code.gsub(line_capture[:regex], "#{line_capture[:start]} #{line_capture[:type]}")
78
+ end
79
+
80
+ code.tr("\t", " ")
81
+ end
82
+
83
+ def line_captures()
84
+ [
85
+ {
86
+ regex: /#.*/,
87
+ type: "LINE COMMENT",
88
+ start: "#"
89
+ }
90
+ ]
91
+ end
92
+
93
+ def block_captures()
94
+ [
95
+ {
96
+ regex: /\/\'''.*?\'''\//m,
97
+ type: "BLOCK QUOTE",
98
+ start: "'''",
99
+ end: "'''"
100
+ }
101
+ ]
102
+ end
103
+ end
@@ -0,0 +1,64 @@
1
+ require "ripper"
2
+
3
+ class RubyTracer
4
+ # TODO: Handle other languages...
5
+ def trace(file)
6
+ out = Ripper.sexp(file)
7
+
8
+ funcs = find_funcs(out.last)
9
+ .map { |func| define_func(func, file.lines) }
10
+ rescue
11
+ []
12
+ end
13
+
14
+ private
15
+
16
+ def find_funcs(ast)
17
+ ast.reduce([]) do |acc, ast|
18
+ if ast.is_a?(Array)
19
+ if ast.first == :def
20
+ acc << ast
21
+ else
22
+ ast
23
+ .select { |tree| tree.is_a?(Array) }
24
+ .each do |sub_ast|
25
+ acc << sub_ast if sub_ast.first == :def
26
+ acc.concat(find_funcs(sub_ast)) if sub_ast.is_a?(Array)
27
+ end
28
+ end
29
+ end
30
+ acc
31
+ end
32
+ end
33
+
34
+ def trace_func(ast)
35
+ ast
36
+ .reduce([]) do |acc, ast|
37
+ if ast.is_a?(Array)
38
+ lines = ast.last
39
+ if lines.is_a?(Array) && lines.count == 2 && lines.first.is_a?(Integer) && lines.last.is_a?(Integer)
40
+ acc << lines.first
41
+ else
42
+ acc.concat(trace_func(ast))
43
+ end
44
+ end
45
+ acc
46
+ end
47
+ .uniq()
48
+ end
49
+
50
+ def define_func(ast, file_lines)
51
+ name = ast[1][1]
52
+ lines = trace_func(ast)
53
+
54
+ if file_lines[lines.last].strip() == "end"
55
+ lines << lines.last + 1
56
+ end
57
+
58
+ {
59
+ name: name,
60
+ start: lines.first,
61
+ end: lines.last
62
+ }
63
+ end
64
+ end
@@ -0,0 +1,44 @@
1
+ require "linguist"
2
+
3
+ require_relative "ruby_tracer"
4
+ require_relative "c_syntax_tracer"
5
+ require_relative "python_tracer"
6
+
7
+ class VirtualBlob < Linguist::FileBlob
8
+ def initialize(path, data)
9
+ @path = path
10
+ @data = data
11
+ end
12
+
13
+ def data
14
+ @data
15
+ end
16
+
17
+ def size
18
+ @size ||= data.length
19
+ end
20
+ end
21
+
22
+ class Tracer
23
+ def trace(path, code)
24
+ case recognize(path, code)
25
+ when "ruby"
26
+ RubyTracer.new.trace(code)
27
+ when "php", "javascript"
28
+ CSyntaxTracer.new.trace(code)
29
+ when "python"
30
+ PythonTracer.new.trace(code)
31
+ else
32
+ []
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def recognize(path, code)
39
+ Linguist
40
+ .detect(VirtualBlob.new(path, code))
41
+ &.name
42
+ &.downcase
43
+ end
44
+ end
@@ -0,0 +1 @@
1
+ Dir[File.dirname(__FILE__) + "/**/*.rb"].each {|file| require file }
@@ -0,0 +1,134 @@
1
+ require "json"
2
+ require "date"
3
+ require "net/http"
4
+ require "active_support/core_ext/hash"
5
+
6
+ require_relative "../cache"
7
+ require_relative "../store"
8
+ require_relative "error_client"
9
+
10
+ class AirbrakeClient < ErrorClient
11
+ PROJECT_ROOT_RGX = /^\[PROJECT_ROOT\]/
12
+
13
+ def initialize(config)
14
+ @key = config["api_key"]
15
+ @project_id = config["project_id"]
16
+ @environment = config["environment"]
17
+ @errors = @key.nil? ? [] : errors()
18
+ @deploys = @key.nil? ? [] : deploys()
19
+ end
20
+
21
+ private
22
+
23
+ def deploys()
24
+ fetch_deploys()["deploys"]
25
+ .map do |deploy|
26
+ {
27
+ commit_id: deploy["version"].to_sym,
28
+ environment: deploy["environment"],
29
+ timestamp: DateTime.parse(deploy["createdAt"])
30
+ }
31
+ end
32
+ .sort_by { |deploy| deploy[:timestamp] }
33
+ .select { |deploy| deploy[:environment] == @environment }
34
+ end
35
+
36
+ def errors()
37
+ cached_errors = load_from_cache()
38
+
39
+ errors = fetch_groups(cached_errors)["groups"]
40
+ .flat_map(&method(:select_traces))
41
+ .reject(&:nil?)
42
+ .map do |trace|
43
+ stack_trace = (trace["backtrace"] || [])
44
+ .reject { |trace| trace["file"].nil? }
45
+ .map do |trace|
46
+ file = trace["file"].gsub(PROJECT_ROOT_RGX, "")
47
+ line = trace["line"]
48
+ function = trace["function"]
49
+
50
+ {
51
+ file: file,
52
+ line: line,
53
+ function: function
54
+ }
55
+ end
56
+
57
+ {
58
+ error_id: trace["id"].to_sym,
59
+ first_time: DateTime.rfc3339(trace["createdAt"]),
60
+ last_time: DateTime.rfc3339(trace["lastNoticeAt"]),
61
+ link: "https://airbrake.io/projects/#{@project_id}/groups/#{trace["id"]}",
62
+ environment: trace["context"]["environment"],
63
+ type: trace["type"],
64
+ message: trace["message"],
65
+ total_occurrences: trace["noticeTotalCount"],
66
+ stack_trace: stack_trace
67
+ }
68
+ end
69
+ .concat(cached_errors)
70
+ .select { |error| error[:environment] == @environment }
71
+ .sort_by { |error| error[:last_time] }
72
+
73
+ errors.each { |error| Store::Error::index(error) }
74
+ Store::Error::cache()
75
+
76
+ errors
77
+ end
78
+
79
+ def select_traces(group)
80
+ notices = fetch_error(group["id"])["notices"]
81
+ error = notices.first["errors"].first
82
+ group["backtrace"] = error["backtrace"]
83
+ group["type"] = error["type"]
84
+ group["message"] = error["message"]
85
+ [group]
86
+ rescue
87
+ []
88
+ end
89
+
90
+ def fetch_error(group_id)
91
+ JSON.parse(Net::HTTP.get(
92
+ URI("https://airbrake.io/api/v4/projects/#{@project_id}/groups/#{group_id}/notices?key=#{@key}")
93
+ ))
94
+ end
95
+
96
+ def fetch_groups(cached_errors, page=1)
97
+ errors = {"groups" => [], "count" => 1}
98
+ while errors["groups"].length < errors["count"] do
99
+ resp = JSON.parse(Net::HTTP.get(
100
+ URI("https://airbrake.io/api/v4/projects/#{@project_id}/groups?key=#{@key}&page=#{page}")
101
+ ))
102
+
103
+ errors["count"] = resp["count"]
104
+
105
+ while group = resp["groups"].shift() do
106
+ if cached_errors.detect { |cached_item| cached_item[:error_id] == group["id"].to_sym }
107
+ return errors
108
+ else
109
+ errors["groups"] << group
110
+ end
111
+ end
112
+
113
+ page += 1
114
+ end
115
+ errors
116
+ rescue
117
+ errors
118
+ end
119
+
120
+ def fetch_deploys(page=1)
121
+ deploys = {"deploys" => [], "count" => 1}
122
+ while deploys["deploys"].length < deploys["count"] do
123
+ resp = JSON.parse(Net::HTTP.get(
124
+ URI("https://airbrake.io/api/v4/projects/#{@project_id}/deploys?key=#{@key}&page=#{page}")
125
+ ))
126
+
127
+ deploys["count"] = resp["count"]
128
+ deploys["deploys"].concat(resp["deploys"])
129
+
130
+ page += 1
131
+ end
132
+ deploys
133
+ end
134
+ end
@@ -0,0 +1,79 @@
1
+ # Consider getting every commit and scoring climate
2
+
3
+ require "uri"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ class CodeClimateClient
8
+ def initialize(config)
9
+ @api_key = config["api_key"]
10
+ @org_id = config["org_id"]
11
+ @repo_id = config["repo_id"]
12
+ end
13
+
14
+ def issues()
15
+ snapshot = last_snapshot_id()
16
+ request("repos/#{@repo_id}/snapshots/#{snapshot}/issues")["data"]
17
+ .reduce({}) do |obj, issue|
18
+ file = issue.dig("attributes", "location", "path")
19
+
20
+ obj[file] ||= {
21
+ issues: []
22
+ }
23
+
24
+ obj[file][:issues] << {
25
+ categories: issue.dig("attributes", "categories"),
26
+ description: issue.dig("attributes", "description"),
27
+ severity: issue.dig("attributes", "severity"),
28
+ remediation_points: issue.dig("attributes", "remediation_points"),
29
+ start_line: issue.dig("attributes", "location", "start_line"),
30
+ end_line: issue.dig("attributes", "location", "end_line"),
31
+ }
32
+
33
+ obj
34
+ end
35
+ end
36
+
37
+ def scores()
38
+ snapshot = last_snapshot_id()
39
+ request("repos/#{@repo_id}/snapshots/#{snapshot}/files")["data"]
40
+ .reduce({}) do |obj, file|
41
+ path = file.dig("attributes", "path")
42
+ obj[path] ||= {}
43
+ obj[path][:gpa] ||= file.dig("attributes", "rating")
44
+ obj
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def last_snapshot_id()
51
+ @last_snapshot_id ||= ref_points()["data"]
52
+ .map { |ref_point| ref_point["relationships"]["snapshot"] }
53
+ .select { |snap_shot| snap_shot }
54
+ .first["data"]["id"]
55
+ end
56
+
57
+ def ref_points()
58
+ request("repos/#{@repo_id}/ref_points")
59
+ end
60
+
61
+ def request(path)
62
+ uri = URI.parse("https://api.codeclimate.com/v1/#{path}")
63
+
64
+ http = Net::HTTP.new(uri.host, uri.port)
65
+ http.use_ssl = true
66
+
67
+ request = Net::HTTP::Get.new(uri.request_uri)
68
+ request.initialize_http_header(headers())
69
+
70
+ JSON.parse(http.request(request).body)
71
+ end
72
+
73
+ def headers()
74
+ {
75
+ "Accept" => "application/vnd.api+json",
76
+ "Authorization" => "Token token=#{@api_key}"
77
+ }
78
+ end
79
+ end
@@ -0,0 +1,38 @@
1
+ require "zlib"
2
+ require "json"
3
+ require "active_support/core_ext/object"
4
+
5
+ class CovhuraClient
6
+ ROOT_PATH = Dir.pwd
7
+
8
+ def initialize(config)
9
+ end
10
+
11
+ def get_report(commit_id, app_files)
12
+ filename = File.join(Dir.pwd, ".gitolemy", "coverage", "#{commit_id}.covhura.json.gz")
13
+ return {} if not File.exist?(filename)
14
+
15
+ coverage = JSON.parse(Zlib::GzipReader.open(filename) { |gz| gz.read })
16
+
17
+ root_path = coverage_root_path(coverage.keys, app_files)
18
+ coverage.reduce({}) do |acc, (file_path, file_coverage)|
19
+ begin
20
+ acc[file_path.sub(root_path, "")] = file_coverage
21
+ acc
22
+ rescue => e
23
+ byebug
24
+ 1
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def coverage_root_path(coverage_files, app_files)
32
+ coverage_files.each do |coverage_file|
33
+ match = app_files.detect { |app_file| coverage_file.include?(app_file) }
34
+ return coverage_file.sub(match, "") if match
35
+ end
36
+ return nil
37
+ end
38
+ end