git-lint 1.0.0
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.
- 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
|