dudity 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/download_service.rb +10 -0
- data/lib/dudes.rb +82 -0
- data/lib/dudity.rb +155 -0
- data/lib/git_diff_service.rb +119 -0
- data/lib/process_code_service.rb +87 -0
- data/lib/scan_app.rb +42 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2f4f733d0f1d5e557bf7a80436baa2a66a3fe8d689a313f0eed01b728ac3d65d
|
4
|
+
data.tar.gz: 945da3f26fcaa3656824de52f471731fe091d5fe7c991ff81d45af46cfb0b8db
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 272d70947283eb90ebcd0248d7a2813b5fcb26b88e750d735c788cb6176ca41acf9cdd209dfc732dc3e3d723b2f2d7dd9f3eba5bbe54dea90033917f9b2b83cb
|
7
|
+
data.tar.gz: 33a6432c3575142ecea4d6de618748f3c94762b838e105e0037ed6a775138f6d0ac2707c35f2fc04ce795ec1eb9a72306ca3b23c5714de5da1750f3020d074c6
|
data/lib/dudes.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# this module was created by Victor Shepelev (zverok) https://github.com/zverok
|
2
|
+
require 'fast'
|
3
|
+
|
4
|
+
module Dudes
|
5
|
+
class Calculator
|
6
|
+
using(Module.new do
|
7
|
+
refine Astrolabe::Node do
|
8
|
+
def fast(path)
|
9
|
+
Fast.search(path, self)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Lot of chunks of code parsed as (:foo, ...) if it is a single statement,
|
13
|
+
# or (:begin, (:foo, ...), (:bar, ...)) if it is several independent statements.
|
14
|
+
# Robustly make it an array of nodes
|
15
|
+
def arrayify
|
16
|
+
type == :begin ? children : [self]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end)
|
20
|
+
|
21
|
+
def initialize(code)
|
22
|
+
@ast = Fast.ast(code)
|
23
|
+
end
|
24
|
+
|
25
|
+
def call
|
26
|
+
return [] unless @ast
|
27
|
+
@ast.arrayify.map(&method(:calc_class)).compact
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def calc_class(node)
|
33
|
+
return unless node.type == :class
|
34
|
+
|
35
|
+
name, parent, body = *node
|
36
|
+
{
|
37
|
+
name: name.children.compact.join(':'),
|
38
|
+
methods: body&.arrayify&.map(&method(:calc_method))&.compact || [],
|
39
|
+
references: extract_references(body)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def extract_references(node)
|
44
|
+
return [] unless node
|
45
|
+
|
46
|
+
node.fast('(const {nil _} )').map { |n| n.children.compact.join('::') }.sort.uniq
|
47
|
+
end
|
48
|
+
|
49
|
+
def calc_method(node)
|
50
|
+
return unless node.type == :def
|
51
|
+
|
52
|
+
name, args, body = *node
|
53
|
+
|
54
|
+
return empty_body(name, args) if body.nil?
|
55
|
+
|
56
|
+
{
|
57
|
+
name: name,
|
58
|
+
args: args.children.count,
|
59
|
+
length: count_statements(body.arrayify),
|
60
|
+
conditions: count_conditions(body),
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def empty_body(name, args)
|
65
|
+
{
|
66
|
+
name: name,
|
67
|
+
args: args.children.count,
|
68
|
+
length: 0,
|
69
|
+
conditions: 0,
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# FIXME: This is kinda naive... But maybe appropriate enough
|
74
|
+
def count_statements(nodes)
|
75
|
+
nodes.sum { |n| n.each_node.count }
|
76
|
+
end
|
77
|
+
|
78
|
+
def count_conditions(node)
|
79
|
+
node.fast('({if case} _)').count
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/dudity.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'dudegl'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
Dir[File.dirname(__FILE__) + '/**/*.rb'].each {|file| require_relative file }
|
5
|
+
|
6
|
+
class Dudity
|
7
|
+
class << self
|
8
|
+
def visualise(path, opt = {})
|
9
|
+
@path = path
|
10
|
+
opt.key?(:except) ? except = opt[:except] : except = nil
|
11
|
+
opt.key?(:only) ? only = opt[:only] : only = nil
|
12
|
+
opt.key?(:ignore_classes) ? ignore_classes = opt[:ignore_classes] : ignore_classes = nil
|
13
|
+
opt.key?(:only_classes) ? only_classes = opt[:only_classes] : only_classes = nil
|
14
|
+
opt.key?(:filename_suffix) ? filename_suffix = opt[:filename_suffix] : filename_suffix = ''
|
15
|
+
|
16
|
+
project_files = ScanApp.call(@path, except, only)
|
17
|
+
app_name = @path.split('/').last
|
18
|
+
|
19
|
+
@params_list = []
|
20
|
+
project_files.each { |project_file| process_item(project_file) }
|
21
|
+
|
22
|
+
@params_list = @params_list.flatten.compact
|
23
|
+
exclude_classes(ignore_classes) if ignore_classes
|
24
|
+
include_classes(only_classes) if only_classes
|
25
|
+
|
26
|
+
dudes = DudeGl.new @params_list, dudes_per_row_max: 4
|
27
|
+
dudes.render
|
28
|
+
dudes.save "#{app_name}#{filename_suffix}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def visualise_diff(path_to_diff, opt = {})
|
32
|
+
@params1 = []
|
33
|
+
@params2 = []
|
34
|
+
|
35
|
+
# path to the dir where diff file is stored
|
36
|
+
@path = path_to_diff.split('/').take(path_to_diff.split('/').size - 1).join('/')
|
37
|
+
|
38
|
+
opt.key?(:as) ? file_type = opt[:as] : file_type = :svg
|
39
|
+
opt.key?(:pull_branch) ? @pull_branch = opt[:pull_branch] : return
|
40
|
+
|
41
|
+
diff = open(path_to_diff).readlines
|
42
|
+
@diff_data = GitDiffService.call(diff)
|
43
|
+
|
44
|
+
return generate_svg if file_type == :svg
|
45
|
+
return generate_html_report if file_type == :html
|
46
|
+
end
|
47
|
+
|
48
|
+
def visualise_pr(public_pr_link, opt = {})
|
49
|
+
@params1 = []
|
50
|
+
@params2 = []
|
51
|
+
|
52
|
+
@path = public_pr_link
|
53
|
+
diff_url = "#{public_pr_link}.diff"
|
54
|
+
|
55
|
+
opt.key?(:as) ? file_type = opt[:as] : file_type = :svg
|
56
|
+
opt.key?(:pull_branch) ? @pull_branch = opt[:pull_branch] : return
|
57
|
+
|
58
|
+
diff = DownloadService.call(diff_url, :read_by_line)
|
59
|
+
@diff_data = GitDiffService.call(diff)
|
60
|
+
|
61
|
+
return generate_svg if file_type == :svg
|
62
|
+
return generate_html_report if file_type == :html
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# generate name based on pull request data. Example: DudesHub_pull_5
|
68
|
+
def fname
|
69
|
+
@path.split('/')[-3, 3].join('_')
|
70
|
+
end
|
71
|
+
|
72
|
+
# generate html report title based on repo data, make each word capitalized
|
73
|
+
def report_title
|
74
|
+
@path.split('/')[-3, 3].map(&:capitalize).join(' ')
|
75
|
+
end
|
76
|
+
|
77
|
+
def generate_svg
|
78
|
+
analyze_code(@diff_data)
|
79
|
+
end
|
80
|
+
|
81
|
+
def generate_html_report
|
82
|
+
separate_code
|
83
|
+
analyze_by_category
|
84
|
+
|
85
|
+
html = open('templates/dudes_report.html').read
|
86
|
+
html = html.sub('[dudes_report_title]', report_title)
|
87
|
+
@report_file_path = "#{fname}.html"
|
88
|
+
File.open(@report_file_path, 'w') { |file| file.write(html) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def separate_code
|
92
|
+
@diff_data_controllers = []
|
93
|
+
@diff_data_models = []
|
94
|
+
@diff_data_others = []
|
95
|
+
|
96
|
+
@diff_data.each do |item|
|
97
|
+
if item[:old_name]&.start_with?("app/controllers") || item[:new_name]&.start_with?("app/controllers")
|
98
|
+
@diff_data_controllers << item
|
99
|
+
elsif item[:old_name]&.start_with?("app/models") || item[:new_name]&.start_with?("app/models")
|
100
|
+
@diff_data_models << item
|
101
|
+
else
|
102
|
+
@diff_data_others << item
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def analyze_by_category
|
108
|
+
analyze_code(@diff_data_controllers, :controllers) if !@diff_data_controllers.empty?
|
109
|
+
analyze_code(@diff_data_models, :models) if !@diff_data_models.empty?
|
110
|
+
analyze_code(@diff_data_others, :others) if !@diff_data_others.empty?
|
111
|
+
end
|
112
|
+
|
113
|
+
def analyze_code(diff_data, label = nil)
|
114
|
+
@params1 = []
|
115
|
+
@params2 = []
|
116
|
+
local = !@path.start_with?('http')
|
117
|
+
local ? suffix = '_local' : suffix = ''
|
118
|
+
|
119
|
+
diff_data.map { |item| process_item_for_diff(item, local) }
|
120
|
+
|
121
|
+
renamed = diff_data.select { |item| item[:status] == :renamed_class }
|
122
|
+
|
123
|
+
return false if params_empty?
|
124
|
+
|
125
|
+
dudes = DudeGl.new [@params1.flatten.compact, @params2.flatten.compact],
|
126
|
+
dudes_per_row_max: 4, renamed: renamed, diff: true
|
127
|
+
dudes.render
|
128
|
+
|
129
|
+
label ? dudes.save("#{label}#{suffix}") : dudes.save("#{fname}#{suffix}")
|
130
|
+
end
|
131
|
+
|
132
|
+
def process_item(project_file)
|
133
|
+
code = open(project_file).read
|
134
|
+
@params_list << Dudes::Calculator.new(code).call
|
135
|
+
end
|
136
|
+
|
137
|
+
def process_item_for_diff(item, local = false)
|
138
|
+
processed_code = ProcessCodeService.new(@path, @pull_branch, item, local = local).call
|
139
|
+
@params1 << processed_code.first
|
140
|
+
@params2 << processed_code.last
|
141
|
+
end
|
142
|
+
|
143
|
+
def params_empty?
|
144
|
+
@params1.empty? || @params2.empty?
|
145
|
+
end
|
146
|
+
|
147
|
+
def exclude_classes(classes)
|
148
|
+
@params_list = @params_list.reject { |param| classes.any? { |item| param[:name] == item } }
|
149
|
+
end
|
150
|
+
|
151
|
+
def include_classes(classes)
|
152
|
+
@params_list = @params_list.select { |param| classes.any? { |item| param[:name] == item } }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
class GitDiffService
|
2
|
+
class << self
|
3
|
+
PATTERN_MATCH = /^diff --git\s/
|
4
|
+
# restrict scope: for Rails app exclude autogenerated code and omit any files except .rb
|
5
|
+
EXCLUDE_ROOT_FOLDERS = ['db/', 'spec/', 'config/']
|
6
|
+
ALLOWED_EXTENSION = '.rb'
|
7
|
+
|
8
|
+
def call(diff_lines, exclude_folders = [])
|
9
|
+
@diff_lines = diff_lines
|
10
|
+
# TO-DO: support @only_folders
|
11
|
+
@exclude_folders = exclude_folders + EXCLUDE_ROOT_FOLDERS
|
12
|
+
|
13
|
+
patch_pos_start = lines_with_file_paths.map { |line| patch_start_pos(line) }
|
14
|
+
# array with start and end positions for each patch
|
15
|
+
@patches_pos = patch_pos_start.zip(patch_end_pos(patch_pos_start))
|
16
|
+
slice_diff
|
17
|
+
@patches = @patches.select { |patch| allowed_patch?(patch) }
|
18
|
+
@diff_data = @patches.map { |patch| fname_diff(patch) }
|
19
|
+
@diff_data += @patches.map { |patch| class_diff(patch) }
|
20
|
+
@diff_data.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# split diff lines into separate patches for each file
|
26
|
+
def slice_diff
|
27
|
+
@patches = @patches_pos.map { |pos| @diff_lines[pos.first..pos.last] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def allowed_patch?(patch)
|
31
|
+
fname_string = patch.first
|
32
|
+
extension_allowed = extract_file_paths(fname_string).last.end_with?(ALLOWED_EXTENSION)
|
33
|
+
folder_allowed = !extract_file_paths(fname_string).last.start_with?(*@exclude_folders)
|
34
|
+
|
35
|
+
extension_allowed && folder_allowed
|
36
|
+
end
|
37
|
+
|
38
|
+
def new_file?(patch)
|
39
|
+
patch.select { |line| line.start_with?('new file mode ') }.any?
|
40
|
+
end
|
41
|
+
|
42
|
+
def deleted_file?(patch)
|
43
|
+
patch.select { |line| line.start_with?('deleted file mode ') }.any?
|
44
|
+
end
|
45
|
+
|
46
|
+
def renamed_file?(patch)
|
47
|
+
patch.select { |line| line.start_with?('rename from ') }.any?
|
48
|
+
end
|
49
|
+
|
50
|
+
def class_renamed?(patch)
|
51
|
+
patch.select { |line| line.start_with?('-class') }.any? &&
|
52
|
+
patch.select { |line| line.start_with?('+class') }.any?
|
53
|
+
end
|
54
|
+
|
55
|
+
def old_class_name(patch)
|
56
|
+
res = patch.select { |line| line.start_with?('-class') }
|
57
|
+
return if res.empty?
|
58
|
+
line = res.first
|
59
|
+
line.split(' ').last.strip
|
60
|
+
end
|
61
|
+
|
62
|
+
def new_class_name(patch)
|
63
|
+
res = patch.select { |line| line.start_with?('+class') }
|
64
|
+
return if res.empty?
|
65
|
+
line = res.first
|
66
|
+
line.split(' ').last.strip
|
67
|
+
end
|
68
|
+
|
69
|
+
def fname_diff(patch)
|
70
|
+
# first line in patch contains info about file path
|
71
|
+
file_path_line = patch.first
|
72
|
+
return { old_name: extract_file_paths(file_path_line).first,
|
73
|
+
new_name: extract_file_paths(file_path_line).last, status: :renamed } if renamed_file?(patch)
|
74
|
+
return { old_name: nil, new_name: extract_file_paths(file_path_line).last,
|
75
|
+
status: :new } if new_file?(patch)
|
76
|
+
return { old_name: extract_file_paths(file_path_line).first,
|
77
|
+
new_name: nil, status: :deleted } if deleted_file?(patch)
|
78
|
+
# changed file without renaming
|
79
|
+
return { old_name: extract_file_paths(file_path_line).first,
|
80
|
+
new_name: extract_file_paths(file_path_line).last, status: :changed }
|
81
|
+
end
|
82
|
+
|
83
|
+
def class_diff(patch)
|
84
|
+
return { old_name: old_class_name(patch),
|
85
|
+
new_name: new_class_name(patch), status: :renamed_class } if class_renamed?(patch)
|
86
|
+
end
|
87
|
+
|
88
|
+
# end line index for current patch
|
89
|
+
def patch_end_pos(patch_pos_start)
|
90
|
+
end_line_pos = patch_pos_start.size - 1
|
91
|
+
pos_end = patch_pos_start.map.with_index do |pos, i|
|
92
|
+
i == patch_pos_start.size - 1 ? @diff_lines.size - 1 : patch_pos_start[i + 1] - 1
|
93
|
+
end
|
94
|
+
|
95
|
+
pos_end
|
96
|
+
end
|
97
|
+
|
98
|
+
def lines_with_file_paths
|
99
|
+
@diff_lines.select { |line| line.scan(PATTERN_MATCH).any? }
|
100
|
+
end
|
101
|
+
|
102
|
+
def patch_start_pos(line)
|
103
|
+
@diff_lines.index(line)
|
104
|
+
end
|
105
|
+
|
106
|
+
def extract_file_paths(line)
|
107
|
+
# diff --git a/project_v1/README.md b/project_v2/README2.md
|
108
|
+
line = line.delete_prefix('diff --git ')
|
109
|
+
path1, path2 = line.split(' ')
|
110
|
+
[path1, path2].map! { |path| file_path(path) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def file_path(str)
|
114
|
+
# a/project_v1/README.md -> project_v1/README.md
|
115
|
+
prefix, *path_parts = str.split('/')
|
116
|
+
path_parts.join('/')
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# use source code of gems to simplify their code editing (if improvements are needed or bugs are found)
|
2
|
+
# in the final version of app gems will be used
|
3
|
+
require '/Users/dmkp/Documents/code/ruby/dudes/zverok_dudes_fork2/dudes/lib/dudes.rb'
|
4
|
+
|
5
|
+
class ProcessCodeService
|
6
|
+
# path is link to pr or treated as local path if local = true
|
7
|
+
def initialize(path, pull_branch, file_data, local = false)
|
8
|
+
@path = path
|
9
|
+
@pull_branch = pull_branch
|
10
|
+
@file_data = file_data
|
11
|
+
@local = local
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
process_code_file
|
16
|
+
|
17
|
+
[@params1, @params2]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def process_code_file
|
23
|
+
download_master_code
|
24
|
+
download_pull_request_code
|
25
|
+
end
|
26
|
+
|
27
|
+
def download_master_code
|
28
|
+
return unless [:deleted, :changed, :renamed].include?(@file_data[:status])
|
29
|
+
code_master = get_code(code_master_branch_path)
|
30
|
+
# code_master = DownloadService.call(code_master_branch_path)
|
31
|
+
code_hash = Dudes::Calculator.new(code_master).call
|
32
|
+
@params1 = code_hash.first unless code_hash.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
def download_pull_request_code
|
36
|
+
return unless [:new, :changed, :renamed].include?(@file_data[:status])
|
37
|
+
code_pull_request = get_code(code_pull_request_branch_path)
|
38
|
+
# code_pull_request = DownloadService.call(code_pull_request_branch_path)
|
39
|
+
code_hash = Dudes::Calculator.new(code_pull_request).call
|
40
|
+
@params2 = code_hash.first unless code_hash.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_code(path)
|
44
|
+
return open(path).read if @local
|
45
|
+
return DownloadService.call(path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def code_master_branch_path
|
49
|
+
code_path = extract_code_path(:master)
|
50
|
+
return "#{@path}/#{repo_full_name}-master/#{code_path}" if @local
|
51
|
+
return "https://raw.githubusercontent.com/#{repo_full_name}/master/#{code_path}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def code_pull_request_branch_path
|
55
|
+
code_path = extract_code_path(:pull)
|
56
|
+
return "#{@path}/#{repo_full_name}-#{@pull_branch}/#{code_path}" if @local
|
57
|
+
return "https://raw.githubusercontent.com/#{repo_full_name}/#{@pull_branch}/#{code_path}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def repo_full_name
|
61
|
+
return @path.split('/').last if @local
|
62
|
+
return @path.split('/')[-4, 2].join('/')
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_code_path(branch)
|
66
|
+
return @file_data[:old_name] if deleted?
|
67
|
+
return @file_data[:new_name] if new? || changed?
|
68
|
+
return renamed?(branch)
|
69
|
+
end
|
70
|
+
|
71
|
+
def deleted?
|
72
|
+
@file_data[:status] == :deleted
|
73
|
+
end
|
74
|
+
|
75
|
+
def new?
|
76
|
+
@file_data[:status] == :new
|
77
|
+
end
|
78
|
+
|
79
|
+
def changed?
|
80
|
+
@file_data[:status] == :changed
|
81
|
+
end
|
82
|
+
|
83
|
+
def renamed?(branch)
|
84
|
+
return @file_data[:old_name] if branch == :master
|
85
|
+
return @file_data[:new_name] if branch == :pull
|
86
|
+
end
|
87
|
+
end
|
data/lib/scan_app.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
class ScanApp
|
2
|
+
class << self
|
3
|
+
EXCLUDE_FOLDERS = ['db/', 'spec/', 'config/']
|
4
|
+
ALLOWED_EXTENSIONS = ['.rb']
|
5
|
+
|
6
|
+
def call(path, except, only)
|
7
|
+
@except = except
|
8
|
+
@only = only
|
9
|
+
# list of all files from app dir recursively
|
10
|
+
@project_files = Dir.glob("#{path}/**/*")
|
11
|
+
|
12
|
+
filter_by_extension
|
13
|
+
exclude_default
|
14
|
+
|
15
|
+
exclude_files if @except
|
16
|
+
include_only_files if @only
|
17
|
+
@project_files
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def exclude_default
|
23
|
+
@project_files = @project_files.reject { |project_file| match?(project_file, EXCLUDE_FOLDERS) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def filter_by_extension
|
27
|
+
@project_files = @project_files.select { |project_file| project_file.end_with?(*ALLOWED_EXTENSIONS) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def exclude_files
|
31
|
+
@project_files = @project_files.reject { |project_file| match?(project_file, @except) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def include_only_files
|
35
|
+
@project_files = @project_files.select { |project_file| match?(project_file, @only) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def match?(string, matches)
|
39
|
+
matches.any? { |item| string.include?(item) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dudity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Dmitry Khramtsov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Try DudeGL code visualization in your Rails projects
|
14
|
+
email:
|
15
|
+
- dp@khramtsov.net
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/download_service.rb
|
21
|
+
- lib/dudes.rb
|
22
|
+
- lib/dudity.rb
|
23
|
+
- lib/git_diff_service.rb
|
24
|
+
- lib/process_code_service.rb
|
25
|
+
- lib/scan_app.rb
|
26
|
+
homepage: https://github.com/dmikhr/Dudity
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.0.3
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: Analyze Rails code with stick dudes
|
49
|
+
test_files: []
|