lintron 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/app/models/file_like.rb +15 -0
- data/app/models/local_pr_alike.rb +84 -0
- data/app/models/patch.rb +51 -0
- data/app/models/stub_file.rb +40 -0
- data/bin/lintron +8 -0
- data/lib/lintron.rb +7 -0
- data/lib/lintron/api.rb +55 -0
- data/lib/lintron/cli.rb +101 -0
- data/lib/lintron/terminal_reporter.rb +74 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 395602bdec689110f8bdb9e08643197014a7db36
|
4
|
+
data.tar.gz: 9fb2bc909a2d0f52545538894f578a9ebf7a3863
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f853f0c7d1a2e9e0ceda044944dc49ee538e4ef95c3dfacf47ac8122bd3d471edccdfa45d4e25b45d71e3ccd0615e97ff2ff6a28f267489bdc7c83d0dd1b9ced
|
7
|
+
data.tar.gz: 1d4ebf5fa5e6617be217fe13f3072bd8eceed553ba944de6073afe5c1010ee7f18572ad71169d4d5ca6a0411b453d81088d2642ca6fa67cbcd4eccf3408ac8bf
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Base class for Github API files and mock files that perform basic operations
|
2
|
+
# on the blob and path
|
3
|
+
class FileLike
|
4
|
+
def extname
|
5
|
+
File.extname(path).gsub(/^\./, '') # without leading .
|
6
|
+
end
|
7
|
+
|
8
|
+
def basename(ext = nil)
|
9
|
+
File.basename path, ext
|
10
|
+
end
|
11
|
+
|
12
|
+
def first_line_of_patch
|
13
|
+
patch.changed_lines.first.try(:number)
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'git_diff_parser'
|
2
|
+
require_relative './stub_file'
|
3
|
+
require_relative './patch'
|
4
|
+
|
5
|
+
# An object that is similar enough to PullRequest to be linted. It can be
|
6
|
+
# constructed from the CLI tool (compares local working tree to base_branch) or
|
7
|
+
# from the JSON payload that the CLI tool sends to the API
|
8
|
+
class LocalPrAlike
|
9
|
+
attr_accessor :files
|
10
|
+
|
11
|
+
def self.from_json(json)
|
12
|
+
LocalPrAlike.new.tap do |pr|
|
13
|
+
pr.files = json.map do |file_json|
|
14
|
+
StubFile.from_json(file_json)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_branch(base_branch)
|
20
|
+
LocalPrAlike.new.tap do |pr|
|
21
|
+
pr.files = pr.stubs_for_existing(base_branch) + pr.stubs_for_new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def stubs_for_existing(base_branch)
|
26
|
+
patches = GitDiffParser.parse(raw_diff(base_branch))
|
27
|
+
|
28
|
+
patches.map do |patch|
|
29
|
+
StubFile.new(
|
30
|
+
path: patch.file,
|
31
|
+
blob: File.read(patch.file),
|
32
|
+
patch: Patch.new(patch.body),
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def raw_diff(base_branch)
|
38
|
+
diff = `git diff --no-ext-diff #{base_branch} .`
|
39
|
+
unless $CHILD_STATUS.success?
|
40
|
+
raise(
|
41
|
+
'git diff failed. You may need to set a default branch in .linty_rc.',
|
42
|
+
)
|
43
|
+
end
|
44
|
+
diff
|
45
|
+
end
|
46
|
+
|
47
|
+
def stubs_for_new
|
48
|
+
untracked_names = `git ls-files --others --exclude-standard`.split("\n")
|
49
|
+
untracked_names.map do |name|
|
50
|
+
body = File.read(name)
|
51
|
+
StubFile.new(
|
52
|
+
path: name,
|
53
|
+
blob: body,
|
54
|
+
patch: Patch.from_file_body(body),
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def changed_files
|
60
|
+
files
|
61
|
+
end
|
62
|
+
|
63
|
+
def expected_url_from_path(path)
|
64
|
+
path
|
65
|
+
end
|
66
|
+
|
67
|
+
def as_json(_opts = {})
|
68
|
+
@files.map(&:as_json)
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_json(_opts = {})
|
72
|
+
{
|
73
|
+
files: as_json.select do |file_json|
|
74
|
+
begin
|
75
|
+
JSON.dump(file_json)
|
76
|
+
file_json
|
77
|
+
rescue JSON::GeneratorError
|
78
|
+
puts "Ignoring #{file_json[:path]}, possible binary"
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
end,
|
82
|
+
}.to_json
|
83
|
+
end
|
84
|
+
end
|
data/app/models/patch.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
class Patch
|
2
|
+
RANGE_INFORMATION_LINE = /^@@ .+\+(?<line_number>\d+),/
|
3
|
+
MODIFIED_LINE = /^\+(?!\+|\+)/
|
4
|
+
NOT_REMOVED_LINE = /^[^-]/
|
5
|
+
|
6
|
+
attr_reader :body
|
7
|
+
|
8
|
+
def self.from_file_body(source)
|
9
|
+
lines = source.lines
|
10
|
+
header = "@@ -0,0 +1,#{lines.count} @@\n"
|
11
|
+
body = header + lines.map { |line| "+#{line}"}.join
|
12
|
+
new(body)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(body)
|
16
|
+
@body = body || ''
|
17
|
+
end
|
18
|
+
|
19
|
+
def changed_lines
|
20
|
+
line_number = 0
|
21
|
+
|
22
|
+
lines.each_with_index.inject([]) do |lines, (content, patch_position)|
|
23
|
+
case content
|
24
|
+
when RANGE_INFORMATION_LINE
|
25
|
+
line_number = Regexp.last_match[:line_number].to_i
|
26
|
+
when MODIFIED_LINE
|
27
|
+
line = Line.new(
|
28
|
+
content: content,
|
29
|
+
number: line_number,
|
30
|
+
patch_position: patch_position
|
31
|
+
)
|
32
|
+
lines << line
|
33
|
+
line_number += 1
|
34
|
+
when NOT_REMOVED_LINE
|
35
|
+
line_number += 1
|
36
|
+
end
|
37
|
+
|
38
|
+
lines
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def position_for_line_number(line)
|
43
|
+
changed_lines.find { |patch_line| patch_line.number == line }.patch_position
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def lines
|
49
|
+
@body.lines
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative './file_like'
|
2
|
+
|
3
|
+
# A simple object we can use in place of a GithubFile as a test mock or for
|
4
|
+
# debugging
|
5
|
+
class StubFile < FileLike
|
6
|
+
attr_accessor :path, :blob
|
7
|
+
|
8
|
+
def self.from_json(json)
|
9
|
+
json = json.symbolize_keys
|
10
|
+
StubFile.new(
|
11
|
+
path: json[:path],
|
12
|
+
blob: json[:blob],
|
13
|
+
patch: Patch.new(json[:patch]),
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(path:, blob:, patch: nil)
|
18
|
+
@path = path
|
19
|
+
@blob = blob
|
20
|
+
@patch = patch
|
21
|
+
end
|
22
|
+
|
23
|
+
def patch_from_blob
|
24
|
+
lines = @blob.lines
|
25
|
+
"@@ -0,0 +1,#{lines.length}\n" +
|
26
|
+
lines.map { |l| "+#{l}" }.join('')
|
27
|
+
end
|
28
|
+
|
29
|
+
def patch
|
30
|
+
@patch || Patch.new(patch_from_blob)
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_json(_opts = {})
|
34
|
+
{
|
35
|
+
path: @path,
|
36
|
+
blob: @blob,
|
37
|
+
patch: patch.body,
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
data/bin/lintron
ADDED
data/lib/lintron.rb
ADDED
data/lib/lintron/api.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'httparty'
|
3
|
+
require 'byebug'
|
4
|
+
|
5
|
+
module Lintron
|
6
|
+
# Makes requests to the lintron local lint API
|
7
|
+
class API
|
8
|
+
def initialize(base_url)
|
9
|
+
@base_url = base_url
|
10
|
+
end
|
11
|
+
|
12
|
+
def violations(pr)
|
13
|
+
response = post_lint_request(pr)
|
14
|
+
violations =
|
15
|
+
JSON
|
16
|
+
.parse(response.body)
|
17
|
+
.map { |json| Lintron::ViolationLine.new(json) }
|
18
|
+
|
19
|
+
violations.sort_by(&:file_and_line)
|
20
|
+
rescue JSON::ParserError
|
21
|
+
puts 'Error occurred while parsing response from Lintron'.colorize(:red)
|
22
|
+
puts 'Raw response body: '
|
23
|
+
puts response.body
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_lint_request(pr)
|
27
|
+
HTTParty.post(URI.join(@base_url, 'local_lints'), request_params(pr))
|
28
|
+
end
|
29
|
+
|
30
|
+
def request_params(pr)
|
31
|
+
{
|
32
|
+
body: pr.to_json,
|
33
|
+
headers: {
|
34
|
+
'Content-Type' => 'application/json',
|
35
|
+
'Accept' => 'application/json',
|
36
|
+
},
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Represents one line of the lint results. Exists so that
|
42
|
+
# (1) We can use dot syntax to access members instead of []
|
43
|
+
# (2) I can define file_and_line which is used for display and sorting
|
44
|
+
class ViolationLine < OpenStruct
|
45
|
+
def file_and_line(padTo = 0)
|
46
|
+
file_and_line = "#{path}:#{format '%03i', line} "
|
47
|
+
|
48
|
+
if padTo > file_and_line.length
|
49
|
+
file_and_line += ' ' * (padTo - file_and_line.length)
|
50
|
+
end
|
51
|
+
|
52
|
+
file_and_line
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/lintron/cli.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_support/all'
|
4
|
+
require 'filewatcher'
|
5
|
+
require_relative '../../app/models/local_pr_alike'
|
6
|
+
|
7
|
+
module Lintron
|
8
|
+
# Handles setting up flags for CLI runs based on defaults, linty_rc, and
|
9
|
+
# command line arguments
|
10
|
+
class CLI
|
11
|
+
def initialize
|
12
|
+
@options = {}
|
13
|
+
OptionParser.new do |opts|
|
14
|
+
opts.banner = 'Usage: linty [options]'
|
15
|
+
|
16
|
+
opts.on('--watch', 'Watch for changes') do |v|
|
17
|
+
@options[:watch] = v
|
18
|
+
end
|
19
|
+
end.parse!
|
20
|
+
end
|
21
|
+
|
22
|
+
def go
|
23
|
+
if config[:watch]
|
24
|
+
go_watch
|
25
|
+
else
|
26
|
+
go_once
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def go_watch
|
31
|
+
system('clear')
|
32
|
+
go_once
|
33
|
+
|
34
|
+
watcher.watch do |filename|
|
35
|
+
system('clear')
|
36
|
+
puts "Re-linting because of #{filename}\n\n"
|
37
|
+
go_once
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def watcher
|
42
|
+
FileWatcher.new(['*', '**/*'], exclude: config[:watch_exclude])
|
43
|
+
end
|
44
|
+
|
45
|
+
def go_once
|
46
|
+
violations = Lintron::API.new(config[:base_url]).violations(pr)
|
47
|
+
puts Lintron::TerminalReporter.new.format_violations(violations)
|
48
|
+
end
|
49
|
+
|
50
|
+
def pr
|
51
|
+
LocalPrAlike.from_branch(base_branch)
|
52
|
+
end
|
53
|
+
|
54
|
+
def base_branch
|
55
|
+
config[:base_branch]
|
56
|
+
end
|
57
|
+
|
58
|
+
def config
|
59
|
+
defaults
|
60
|
+
.merge(validated_config_from_file)
|
61
|
+
.merge(@options)
|
62
|
+
.merge(
|
63
|
+
{
|
64
|
+
base_branch: ARGV[0],
|
65
|
+
}.compact,
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def defaults
|
70
|
+
{
|
71
|
+
base_branch: 'origin/develop',
|
72
|
+
watch_exclude: [
|
73
|
+
'**/*.log',
|
74
|
+
'tmp/**/*',
|
75
|
+
],
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def validated_config_from_file
|
80
|
+
config = config_from_file
|
81
|
+
|
82
|
+
unless config.key?(:base_url)
|
83
|
+
raise('.linty_rc missing required key: base_url')
|
84
|
+
end
|
85
|
+
|
86
|
+
config
|
87
|
+
end
|
88
|
+
|
89
|
+
def config_from_file
|
90
|
+
file_path = File.join(`git rev-parse --show-toplevel`.strip, '.linty_rc')
|
91
|
+
|
92
|
+
raise('.linty_rc is missing.') unless File.exist?(file_path)
|
93
|
+
|
94
|
+
begin
|
95
|
+
JSON.parse(File.read(file_path)).symbolize_keys
|
96
|
+
rescue JSON::ParserError
|
97
|
+
raise('Malformed .linty_rc')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'colorize'
|
3
|
+
require 'terminfo'
|
4
|
+
|
5
|
+
module Lintron
|
6
|
+
# Outputs lint results on the command line
|
7
|
+
class TerminalReporter
|
8
|
+
def format_violations(violations)
|
9
|
+
row_header_width = violations.map { |v| v.file_and_line.length }.max
|
10
|
+
return no_violations if violations.empty?
|
11
|
+
last_file = violations.first.path
|
12
|
+
buffer = ''
|
13
|
+
|
14
|
+
violations.each do |violation|
|
15
|
+
buffer += do_line(violation, last_file, row_header_width)
|
16
|
+
last_file = violation.path
|
17
|
+
end
|
18
|
+
buffer += "\n\n"
|
19
|
+
buffer
|
20
|
+
end
|
21
|
+
|
22
|
+
def no_violations
|
23
|
+
'No violations found!'.colorize(:green)
|
24
|
+
end
|
25
|
+
|
26
|
+
def do_line(violation, last_file, row_header_width)
|
27
|
+
rule = ''
|
28
|
+
if last_file != violation.path
|
29
|
+
colors.rewind
|
30
|
+
rule += hr
|
31
|
+
end
|
32
|
+
|
33
|
+
file_and_line = violation.file_and_line(row_header_width)
|
34
|
+
rule + wrap_pretty(
|
35
|
+
"#{file_and_line}#{violation['message']}".colorize(colors.next),
|
36
|
+
file_and_line.length,
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def hr
|
41
|
+
'-' * TermInfo.screen_size[1] + "\n\n\n"
|
42
|
+
end
|
43
|
+
|
44
|
+
def colors
|
45
|
+
@_colors ||= [:magenta, :cyan].cycle.each
|
46
|
+
end
|
47
|
+
|
48
|
+
def wrap_pretty(string, indent_level)
|
49
|
+
width = TermInfo.screen_size[1] # Get the width of the term
|
50
|
+
buffer = ''
|
51
|
+
words = string.split(/\s/)
|
52
|
+
|
53
|
+
current_word = 0
|
54
|
+
line_length = 0
|
55
|
+
last_line_word = 0
|
56
|
+
while current_word < words.length
|
57
|
+
line_length += words[current_word].length
|
58
|
+
if line_length > width - 5 - indent_level &&
|
59
|
+
current_word > last_line_word
|
60
|
+
|
61
|
+
buffer += "\n"
|
62
|
+
buffer += ' ' * indent_level
|
63
|
+
line_length = indent_level
|
64
|
+
last_line_word = current_word
|
65
|
+
else
|
66
|
+
buffer += words[current_word]
|
67
|
+
buffer += ' '
|
68
|
+
current_word += 1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
buffer + "\n"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: lintron
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Prehn
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-09-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.2.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.2.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: git_diff_parser
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.3.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httparty
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.14.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.14.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: colorize
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.8.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.8.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: ruby-terminfo
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.1.1
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.1.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: filewatcher
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.5.3
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.5.3
|
97
|
+
description: A command line tool for using the lintron service against local changes
|
98
|
+
(requires a lintron server).
|
99
|
+
email: robert@revelry.co
|
100
|
+
executables:
|
101
|
+
- lintron
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- app/models/file_like.rb
|
106
|
+
- app/models/local_pr_alike.rb
|
107
|
+
- app/models/patch.rb
|
108
|
+
- app/models/stub_file.rb
|
109
|
+
- bin/lintron
|
110
|
+
- lib/lintron.rb
|
111
|
+
- lib/lintron/api.rb
|
112
|
+
- lib/lintron/cli.rb
|
113
|
+
- lib/lintron/terminal_reporter.rb
|
114
|
+
homepage: https://github.com/prehnra/lintron
|
115
|
+
licenses:
|
116
|
+
- MIT
|
117
|
+
metadata: {}
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 2.5.1
|
135
|
+
signing_key:
|
136
|
+
specification_version: 4
|
137
|
+
summary: A CLI for running lintron against your changes.
|
138
|
+
test_files: []
|