git-lint 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/LICENSE.adoc +162 -0
- data/README.adoc +1068 -0
- data/bin/git-lint +9 -0
- data/lib/git/kit/repo.rb +30 -0
- data/lib/git/lint.rb +51 -0
- data/lib/git/lint/analyzers/abstract.rb +108 -0
- data/lib/git/lint/analyzers/commit_author_capitalization.rb +35 -0
- data/lib/git/lint/analyzers/commit_author_email.rb +35 -0
- data/lib/git/lint/analyzers/commit_author_name.rb +40 -0
- data/lib/git/lint/analyzers/commit_body_bullet.rb +43 -0
- data/lib/git/lint/analyzers/commit_body_bullet_capitalization.rb +46 -0
- data/lib/git/lint/analyzers/commit_body_bullet_delimiter.rb +40 -0
- data/lib/git/lint/analyzers/commit_body_issue_tracker_link.rb +45 -0
- data/lib/git/lint/analyzers/commit_body_leading_line.rb +30 -0
- data/lib/git/lint/analyzers/commit_body_line_length.rb +42 -0
- data/lib/git/lint/analyzers/commit_body_paragraph_capitalization.rb +47 -0
- data/lib/git/lint/analyzers/commit_body_phrase.rb +72 -0
- data/lib/git/lint/analyzers/commit_body_presence.rb +36 -0
- data/lib/git/lint/analyzers/commit_body_single_bullet.rb +40 -0
- data/lib/git/lint/analyzers/commit_subject_length.rb +33 -0
- data/lib/git/lint/analyzers/commit_subject_prefix.rb +42 -0
- data/lib/git/lint/analyzers/commit_subject_suffix.rb +39 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_capitalization.rb +51 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_duplication.rb +56 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_email.rb +52 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_key.rb +56 -0
- data/lib/git/lint/analyzers/commit_trailer_collaborator_name.rb +57 -0
- data/lib/git/lint/branches/environments/circle_ci.rb +28 -0
- data/lib/git/lint/branches/environments/local.rb +28 -0
- data/lib/git/lint/branches/environments/netlify_ci.rb +34 -0
- data/lib/git/lint/branches/environments/travis_ci.rb +57 -0
- data/lib/git/lint/branches/feature.rb +44 -0
- data/lib/git/lint/cli.rb +122 -0
- data/lib/git/lint/collector.rb +64 -0
- data/lib/git/lint/commits/saved.rb +104 -0
- data/lib/git/lint/commits/unsaved.rb +120 -0
- data/lib/git/lint/errors/base.rb +14 -0
- data/lib/git/lint/errors/severity.rb +13 -0
- data/lib/git/lint/errors/sha.rb +13 -0
- data/lib/git/lint/identity.rb +13 -0
- data/lib/git/lint/kit/filter_list.rb +30 -0
- data/lib/git/lint/parsers/trailers/collaborator.rb +57 -0
- data/lib/git/lint/rake/setup.rb +4 -0
- data/lib/git/lint/rake/tasks.rb +33 -0
- data/lib/git/lint/refinements/strings.rb +25 -0
- data/lib/git/lint/reporters/branch.rb +67 -0
- data/lib/git/lint/reporters/commit.rb +30 -0
- data/lib/git/lint/reporters/line.rb +32 -0
- data/lib/git/lint/reporters/lines/paragraph.rb +49 -0
- data/lib/git/lint/reporters/lines/sentence.rb +31 -0
- data/lib/git/lint/reporters/style.rb +52 -0
- data/lib/git/lint/runner.rb +34 -0
- data/lib/git/lint/validators/capitalization.rb +29 -0
- data/lib/git/lint/validators/email.rb +24 -0
- data/lib/git/lint/validators/name.rb +30 -0
- metadata +363 -0
- metadata.gz.sig +3 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "refinements/strings"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
class Collector
|
8
|
+
using Refinements::Strings
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@collection = Hash.new { |default, missing_id| default[missing_id] = [] }
|
12
|
+
end
|
13
|
+
|
14
|
+
def add analyzer
|
15
|
+
collection[analyzer.commit] << analyzer
|
16
|
+
analyzer
|
17
|
+
end
|
18
|
+
|
19
|
+
def retrieve id
|
20
|
+
collection[id]
|
21
|
+
end
|
22
|
+
|
23
|
+
def empty?
|
24
|
+
collection.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def warnings?
|
28
|
+
collection.values.flatten.any?(&:warning?)
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors?
|
32
|
+
collection.values.flatten.any?(&:error?)
|
33
|
+
end
|
34
|
+
|
35
|
+
def issues?
|
36
|
+
collection.values.flatten.any?(&:invalid?)
|
37
|
+
end
|
38
|
+
|
39
|
+
def total_warnings
|
40
|
+
collection.values.flatten.count(&:warning?)
|
41
|
+
end
|
42
|
+
|
43
|
+
def total_errors
|
44
|
+
collection.values.flatten.count(&:error?)
|
45
|
+
end
|
46
|
+
|
47
|
+
def total_issues
|
48
|
+
collection.values.flatten.count(&:invalid?)
|
49
|
+
end
|
50
|
+
|
51
|
+
def total_commits
|
52
|
+
collection.keys.size
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_h
|
56
|
+
collection
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_reader :collection
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Commits
|
8
|
+
# Represents an existing commit.
|
9
|
+
# :reek:TooManyMethods
|
10
|
+
class Saved
|
11
|
+
using Refinements::Strings
|
12
|
+
|
13
|
+
FORMATS = {
|
14
|
+
sha: "%H",
|
15
|
+
author_name: "%an",
|
16
|
+
author_email: "%ae",
|
17
|
+
author_date_relative: "%ar",
|
18
|
+
subject: "%s",
|
19
|
+
body: "%b",
|
20
|
+
raw_body: "%B",
|
21
|
+
trailers: "%(trailers)"
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
def self.pattern
|
25
|
+
FORMATS.reduce("") { |pattern, (key, value)| pattern + "<#{key}>#{value}</#{key}>%n" }
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize sha:, shell: Open3
|
29
|
+
data, status = shell.capture2e show_command(sha)
|
30
|
+
fail Errors::SHA, sha unless status.success?
|
31
|
+
|
32
|
+
@data = data.scrub "?"
|
33
|
+
end
|
34
|
+
|
35
|
+
def == other
|
36
|
+
other.is_a?(self.class) && sha == other.sha
|
37
|
+
end
|
38
|
+
alias eql? ==
|
39
|
+
|
40
|
+
def <=> other
|
41
|
+
sha <=> other.sha
|
42
|
+
end
|
43
|
+
|
44
|
+
def hash
|
45
|
+
sha.hash
|
46
|
+
end
|
47
|
+
|
48
|
+
def body_lines
|
49
|
+
body_without_trailing_spaces
|
50
|
+
end
|
51
|
+
|
52
|
+
def body_paragraphs
|
53
|
+
body_without_trailers.split("\n\n").map(&:chomp).reject { |line| line.start_with? "#" }
|
54
|
+
end
|
55
|
+
|
56
|
+
def trailer_lines
|
57
|
+
trailers.split "\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
def trailer_index
|
61
|
+
body.split("\n").index trailer_lines.first
|
62
|
+
end
|
63
|
+
|
64
|
+
def fixup?
|
65
|
+
subject.fixup?
|
66
|
+
end
|
67
|
+
|
68
|
+
def squash?
|
69
|
+
subject.squash?
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :data
|
75
|
+
|
76
|
+
def show_command sha
|
77
|
+
%(git show --stat --pretty=format:"#{self.class.pattern}" #{sha})
|
78
|
+
end
|
79
|
+
|
80
|
+
def body_without_trailing_spaces
|
81
|
+
body_without_comments.reverse.drop_while(&:empty?).reverse
|
82
|
+
end
|
83
|
+
|
84
|
+
def body_without_comments
|
85
|
+
body_without_trailers.split("\n").reject { |line| line.start_with? "#" }
|
86
|
+
end
|
87
|
+
|
88
|
+
def body_without_trailers
|
89
|
+
body.sub trailers, ""
|
90
|
+
end
|
91
|
+
|
92
|
+
def method_missing name, *arguments, &block
|
93
|
+
return super unless respond_to_missing? name
|
94
|
+
|
95
|
+
String data[%r(<#{name}>(?<content>.*?)</#{name}>)m, :content]
|
96
|
+
end
|
97
|
+
|
98
|
+
def respond_to_missing? name, include_private = false
|
99
|
+
FORMATS.key?(name.to_sym) || super
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "open3"
|
5
|
+
require "securerandom"
|
6
|
+
|
7
|
+
module Git
|
8
|
+
module Lint
|
9
|
+
module Commits
|
10
|
+
# Represents a partially formed, unsaved commit.
|
11
|
+
# :reek:TooManyMethods
|
12
|
+
class Unsaved
|
13
|
+
using Refinements::Strings
|
14
|
+
|
15
|
+
SUBJECT_LINE = 1
|
16
|
+
SCISSOR_PATTERN = /\#\s-+\s>8\s-+\n.+/m.freeze
|
17
|
+
|
18
|
+
attr_reader :sha, :raw_body
|
19
|
+
|
20
|
+
def initialize path:, sha: SecureRandom.hex(20), shell: Open3
|
21
|
+
fail Errors::Base, %(Invalid commit message path: "#{path}".) unless File.exist? path
|
22
|
+
|
23
|
+
@path = Pathname path
|
24
|
+
@sha = sha
|
25
|
+
@shell = shell
|
26
|
+
@raw_body = File.read(path).scrub "?"
|
27
|
+
end
|
28
|
+
|
29
|
+
def author_name
|
30
|
+
shell.capture2e("git config --get user.name").then { |result, _status| result.chomp }
|
31
|
+
end
|
32
|
+
|
33
|
+
def author_email
|
34
|
+
shell.capture2e("git config --get user.email").then { |result, _status| result.chomp }
|
35
|
+
end
|
36
|
+
|
37
|
+
def author_date_relative
|
38
|
+
"0 seconds ago"
|
39
|
+
end
|
40
|
+
|
41
|
+
def subject
|
42
|
+
String raw_body.split("\n").first
|
43
|
+
end
|
44
|
+
|
45
|
+
# :reek:FeatureEnvy
|
46
|
+
def body
|
47
|
+
raw_body.sub(SCISSOR_PATTERN, "").split("\n").drop(SUBJECT_LINE).then do |lines|
|
48
|
+
computed_body = lines.join "\n"
|
49
|
+
lines.empty? ? computed_body : "#{computed_body}\n"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def body_lines
|
54
|
+
body_without_trailing_spaces
|
55
|
+
end
|
56
|
+
|
57
|
+
# :reek:FeatureEnvy
|
58
|
+
def body_paragraphs
|
59
|
+
body_without_trailers.split("\n\n")
|
60
|
+
.map { |line| line.delete_prefix "\n" }
|
61
|
+
.map(&:chomp)
|
62
|
+
.reject { |line| line.start_with? "#" }
|
63
|
+
end
|
64
|
+
|
65
|
+
def trailers
|
66
|
+
trailers, status = shell.capture2e %(git interpret-trailers --only-trailers "#{path}")
|
67
|
+
|
68
|
+
return "" unless status.success?
|
69
|
+
|
70
|
+
trailers
|
71
|
+
end
|
72
|
+
|
73
|
+
def trailer_lines
|
74
|
+
trailers.split "\n"
|
75
|
+
end
|
76
|
+
|
77
|
+
def trailer_index
|
78
|
+
body.split("\n").index trailer_lines.first
|
79
|
+
end
|
80
|
+
|
81
|
+
def == other
|
82
|
+
other.is_a?(self.class) && raw_body == other.raw_body
|
83
|
+
end
|
84
|
+
alias eql? ==
|
85
|
+
|
86
|
+
def <=> other
|
87
|
+
raw_body <=> other.raw_body
|
88
|
+
end
|
89
|
+
|
90
|
+
def hash
|
91
|
+
raw_body.hash
|
92
|
+
end
|
93
|
+
|
94
|
+
def fixup?
|
95
|
+
subject.fixup?
|
96
|
+
end
|
97
|
+
|
98
|
+
def squash?
|
99
|
+
subject.squash?
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
attr_reader :path, :shell
|
105
|
+
|
106
|
+
def body_without_trailing_spaces
|
107
|
+
body_without_comments.reverse.drop_while(&:empty?).reverse
|
108
|
+
end
|
109
|
+
|
110
|
+
def body_without_comments
|
111
|
+
body_without_trailers.split("\n").reject { |line| line.start_with? "#" }
|
112
|
+
end
|
113
|
+
|
114
|
+
def body_without_trailers
|
115
|
+
body.sub trailers, ""
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Errors
|
6
|
+
# The root class of gem related errors.
|
7
|
+
class Base < StandardError
|
8
|
+
def initialize message = "Invalid #{Identity::LABEL} action."
|
9
|
+
super message
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Kit
|
6
|
+
class FilterList
|
7
|
+
# Represents an regular expression list which may be used as an analyzer setting.
|
8
|
+
def initialize list = []
|
9
|
+
@list = Array list
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hint
|
13
|
+
to_regexp.map(&:inspect).join ", "
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_regexp
|
17
|
+
list.map { |item| Regexp.new item }
|
18
|
+
end
|
19
|
+
|
20
|
+
def empty?
|
21
|
+
list.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :list
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Parsers
|
6
|
+
module Trailers
|
7
|
+
class Collaborator
|
8
|
+
DEFAULT_KEY_PATTERN = /\ACo.*Authored.*By.*\Z/i.freeze
|
9
|
+
|
10
|
+
DEFAULT_MATCH_PATTERN = /
|
11
|
+
(?<key>\A.+) # Key (anchored to start of line).
|
12
|
+
(?<delimiter>:) # Key delimiter.
|
13
|
+
(?<key_space>\s?) # Space delimiter (optional).
|
14
|
+
(?<name>.*?) # Collaborator name (smallest possible).
|
15
|
+
(?<name_space>\s?) # Space delimiter (optional).
|
16
|
+
(?<email><.+>)? # Collaborator email (optional).
|
17
|
+
\Z # End of line.
|
18
|
+
/x.freeze
|
19
|
+
|
20
|
+
def initialize text,
|
21
|
+
key_pattern: DEFAULT_KEY_PATTERN,
|
22
|
+
match_pattern: DEFAULT_MATCH_PATTERN
|
23
|
+
|
24
|
+
@text = String text
|
25
|
+
@key_pattern = key_pattern
|
26
|
+
@match_pattern = match_pattern
|
27
|
+
@matches = build_matches
|
28
|
+
end
|
29
|
+
|
30
|
+
def key
|
31
|
+
String matches["key"]
|
32
|
+
end
|
33
|
+
|
34
|
+
def name
|
35
|
+
String matches["name"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def email
|
39
|
+
String(matches["email"]).delete_prefix("<").delete_suffix(">")
|
40
|
+
end
|
41
|
+
|
42
|
+
def match?
|
43
|
+
text.match? key_pattern
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :text, :key_pattern, :match_pattern, :matches
|
49
|
+
|
50
|
+
def build_matches
|
51
|
+
text.match(match_pattern).then { |data| data ? data.named_captures : Hash.new }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "git/lint"
|
5
|
+
|
6
|
+
module Git
|
7
|
+
module Lint
|
8
|
+
module Rake
|
9
|
+
class Tasks
|
10
|
+
include ::Rake::DSL
|
11
|
+
|
12
|
+
def self.setup
|
13
|
+
new.install
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize cli: CLI
|
17
|
+
@cli = cli
|
18
|
+
end
|
19
|
+
|
20
|
+
def install
|
21
|
+
desc "Run Git Lint"
|
22
|
+
task :git_lint do
|
23
|
+
cli.start ["--analyze"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :cli
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|