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