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,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorDuplication < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize commit:,
|
15
|
+
settings: self.class.defaults,
|
16
|
+
parser: Parsers::Trailers::Collaborator
|
17
|
+
super commit: commit, settings: settings
|
18
|
+
@parser = parser
|
19
|
+
@tally = build_tally
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
affected_commit_trailer_lines.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def issue
|
27
|
+
return {} if valid?
|
28
|
+
|
29
|
+
{
|
30
|
+
hint: "Avoid duplication.",
|
31
|
+
lines: affected_commit_trailer_lines
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def invalid_line? line
|
38
|
+
collaborator = parser.new line
|
39
|
+
collaborator.match? && tally[line] != 1
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :parser, :tally
|
45
|
+
|
46
|
+
def build_tally
|
47
|
+
zeros = Hash.new { |new_hash, missing_key| new_hash[missing_key] = 0 }
|
48
|
+
|
49
|
+
zeros.tap do |collection|
|
50
|
+
commit.trailer_lines.each { |line| collection[line] += 1 if parser.new(line).match? }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorEmail < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/ParameterLists
|
15
|
+
def initialize commit:,
|
16
|
+
settings: self.class.defaults,
|
17
|
+
parser: Parsers::Trailers::Collaborator,
|
18
|
+
validator: Validators::Email
|
19
|
+
|
20
|
+
super commit: commit, settings: settings
|
21
|
+
@parser = parser
|
22
|
+
@validator = validator
|
23
|
+
end
|
24
|
+
# rubocop:enable Metrics/ParameterLists
|
25
|
+
|
26
|
+
def valid?
|
27
|
+
affected_commit_trailer_lines.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def issue
|
31
|
+
return {} if valid?
|
32
|
+
|
33
|
+
{
|
34
|
+
hint: %(Email must follow name and use format: "<name@server.domain>".),
|
35
|
+
lines: affected_commit_trailer_lines
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
def invalid_line? line
|
42
|
+
collaborator = parser.new line
|
43
|
+
collaborator.match? && !validator.new(collaborator.email).valid?
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :parser, :validator
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorKey < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error,
|
11
|
+
includes: ["Co-Authored-By"]
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize commit:,
|
16
|
+
settings: self.class.defaults,
|
17
|
+
parser: Parsers::Trailers::Collaborator
|
18
|
+
super commit: commit, settings: settings
|
19
|
+
@parser = parser
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid?
|
23
|
+
affected_commit_trailer_lines.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def issue
|
27
|
+
return {} if valid?
|
28
|
+
|
29
|
+
{
|
30
|
+
hint: "Use format: #{filter_list.to_hint}.",
|
31
|
+
lines: affected_commit_trailer_lines
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def load_filter_list
|
38
|
+
Kit::FilterList.new settings.fetch :includes
|
39
|
+
end
|
40
|
+
|
41
|
+
def invalid_line? line
|
42
|
+
collaborator = parser.new line
|
43
|
+
key = collaborator.key
|
44
|
+
|
45
|
+
collaborator.match? && !key.empty? && !key.match?(
|
46
|
+
/\A#{Regexp.union filter_list.to_regexp}\Z/
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :parser
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Analyzers
|
6
|
+
class CommitTrailerCollaboratorName < Abstract
|
7
|
+
def self.defaults
|
8
|
+
{
|
9
|
+
enabled: true,
|
10
|
+
severity: :error,
|
11
|
+
minimum: 2
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
# rubocop:disable Metrics/ParameterLists
|
16
|
+
def initialize commit:,
|
17
|
+
settings: self.class.defaults,
|
18
|
+
parser: Parsers::Trailers::Collaborator,
|
19
|
+
validator: Validators::Name
|
20
|
+
|
21
|
+
super commit: commit, settings: settings
|
22
|
+
@parser = parser
|
23
|
+
@validator = validator
|
24
|
+
end
|
25
|
+
# rubocop:enable Metrics/ParameterLists
|
26
|
+
|
27
|
+
def valid?
|
28
|
+
affected_commit_trailer_lines.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def issue
|
32
|
+
return {} if valid?
|
33
|
+
|
34
|
+
{
|
35
|
+
hint: "Name must follow key and consist of #{minimum} parts (minimum).",
|
36
|
+
lines: affected_commit_trailer_lines
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def invalid_line? line
|
43
|
+
collaborator = parser.new line
|
44
|
+
collaborator.match? && !validator.new(collaborator.name.strip, minimum: minimum).valid?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :parser, :validator
|
50
|
+
|
51
|
+
def minimum
|
52
|
+
settings.fetch :minimum
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Branches
|
6
|
+
module Environments
|
7
|
+
# Provides Circle CI build environment feature branch information.
|
8
|
+
class CircleCI
|
9
|
+
def initialize repo: Git::Kit::Repo.new
|
10
|
+
@repo = repo
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
"origin/#{repo.branch_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def shas
|
18
|
+
repo.shas start: "origin/master", finish: name
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :repo
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Git
|
4
|
+
module Lint
|
5
|
+
module Branches
|
6
|
+
module Environments
|
7
|
+
# Provides local build environment feature branch information.
|
8
|
+
class Local
|
9
|
+
def initialize repo: Git::Kit::Repo.new
|
10
|
+
@repo = repo
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
repo.branch_name
|
15
|
+
end
|
16
|
+
|
17
|
+
def shas
|
18
|
+
repo.shas start: "master", finish: name
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :repo
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Branches
|
8
|
+
module Environments
|
9
|
+
# Provides Netlify CI build environment feature branch information.
|
10
|
+
class NetlifyCI
|
11
|
+
def initialize environment: ENV, repo: Git::Kit::Repo.new, shell: Open3
|
12
|
+
@environment = environment
|
13
|
+
@repo = repo
|
14
|
+
@shell = shell
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
environment["HEAD"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def shas
|
22
|
+
shell.capture2e "git remote add -f origin #{environment["REPOSITORY_URL"]}"
|
23
|
+
shell.capture2e "git fetch origin #{name}:#{name}"
|
24
|
+
repo.shas start: "origin/master", finish: "origin/#{name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :environment, :repo, :shell
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Branches
|
8
|
+
module Environments
|
9
|
+
# Provides Travis CI build environment feature branch information.
|
10
|
+
class TravisCI
|
11
|
+
def initialize environment: ENV, repo: Git::Kit::Repo.new, shell: Open3
|
12
|
+
@environment = environment
|
13
|
+
@repo = repo
|
14
|
+
@shell = shell
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
pull_request_branch.empty? ? ci_branch : pull_request_branch
|
19
|
+
end
|
20
|
+
|
21
|
+
def shas
|
22
|
+
prepare_project
|
23
|
+
repo.shas start: "origin/master", finish: name
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :environment, :repo, :shell
|
29
|
+
|
30
|
+
def prepare_project
|
31
|
+
slug = pull_request_slug
|
32
|
+
|
33
|
+
unless slug.empty?
|
34
|
+
shell.capture2e "git remote add -f original_branch https://github.com/#{slug}.git"
|
35
|
+
shell.capture2e "git fetch original_branch #{name}:#{name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
shell.capture2e "git remote set-branches --add origin master"
|
39
|
+
shell.capture2e "git fetch"
|
40
|
+
end
|
41
|
+
|
42
|
+
def ci_branch
|
43
|
+
environment["TRAVIS_BRANCH"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def pull_request_branch
|
47
|
+
environment["TRAVIS_PULL_REQUEST_BRANCH"]
|
48
|
+
end
|
49
|
+
|
50
|
+
def pull_request_slug
|
51
|
+
environment["TRAVIS_PULL_REQUEST_SLUG"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Git
|
6
|
+
module Lint
|
7
|
+
module Branches
|
8
|
+
# Represents a feature branch.
|
9
|
+
class Feature
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :selected_environment, :name, :shas
|
13
|
+
|
14
|
+
def initialize environment: ENV, git_repo: Git::Kit::Repo.new
|
15
|
+
message = "Invalid repository. Are you within a Git-enabled project?"
|
16
|
+
fail Errors::Base, message unless git_repo.exist?
|
17
|
+
|
18
|
+
@current_environment = environment
|
19
|
+
@selected_environment = load_environment
|
20
|
+
end
|
21
|
+
|
22
|
+
def commits
|
23
|
+
shas.map { |sha| Commits::Saved.new sha: sha }
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :current_environment, :selected_environment
|
29
|
+
|
30
|
+
def load_environment
|
31
|
+
if key? "CIRCLECI" then Environments::CircleCI.new
|
32
|
+
elsif key? "NETLIFY" then Environments::NetlifyCI.new environment: current_environment
|
33
|
+
elsif key? "TRAVIS" then Environments::TravisCI.new environment: current_environment
|
34
|
+
else Environments::Local.new
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def key? key
|
39
|
+
current_environment[key] == "true"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/git/lint/cli.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require "thor/actions"
|
5
|
+
require "runcom"
|
6
|
+
require "pastel"
|
7
|
+
|
8
|
+
module Git
|
9
|
+
module Lint
|
10
|
+
# The Command Line Interface (CLI) for the gem.
|
11
|
+
class CLI < Thor
|
12
|
+
include Thor::Actions
|
13
|
+
|
14
|
+
package_name Identity::VERSION_LABEL
|
15
|
+
|
16
|
+
def self.configuration
|
17
|
+
defaults = Analyzers::Abstract.descendants.reduce({}) do |settings, analyzer|
|
18
|
+
settings.merge analyzer.id => analyzer.defaults
|
19
|
+
end
|
20
|
+
|
21
|
+
Runcom::Config.new "#{Identity::NAME}/configuration.yml", defaults: defaults
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize args = [], options = {}, config = {}
|
25
|
+
super args, options, config
|
26
|
+
@configuration = self.class.configuration
|
27
|
+
@runner = Runner.new configuration: @configuration.to_h
|
28
|
+
@colorizer = Pastel.new
|
29
|
+
rescue Runcom::Errors::Base => error
|
30
|
+
abort error.message
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "-c, [--config]", "Manage gem configuration."
|
34
|
+
map %w[-c --config] => :config
|
35
|
+
method_option :edit,
|
36
|
+
aliases: "-e",
|
37
|
+
desc: "Edit gem configuration.",
|
38
|
+
type: :boolean,
|
39
|
+
default: false
|
40
|
+
method_option :info,
|
41
|
+
aliases: "-i",
|
42
|
+
desc: "Print gem configuration.",
|
43
|
+
type: :boolean,
|
44
|
+
default: false
|
45
|
+
def config
|
46
|
+
path = configuration.current
|
47
|
+
|
48
|
+
if options.edit? then `#{ENV["EDITOR"]} #{path}`
|
49
|
+
elsif options.info?
|
50
|
+
path ? say(path) : say("Configuration doesn't exist.")
|
51
|
+
else help :config
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "-a, [--analyze]", "Analyze feature branch for issues."
|
56
|
+
map %w[-a --analyze] => :analyze
|
57
|
+
method_option :commits,
|
58
|
+
aliases: "-c",
|
59
|
+
desc: "Analyze specific commit SHA(s).",
|
60
|
+
type: :array,
|
61
|
+
default: []
|
62
|
+
def analyze
|
63
|
+
collector = analyze_commits options.commits
|
64
|
+
abort if collector.errors?
|
65
|
+
rescue Errors::Base => error
|
66
|
+
abort colorizer.red("#{Identity::LABEL}: #{error.message}")
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "--hook", "Add Git Hook support."
|
70
|
+
map "--hook" => :hook
|
71
|
+
method_option :commit_message,
|
72
|
+
desc: "Analyze commit message.",
|
73
|
+
banner: "PATH",
|
74
|
+
type: :string
|
75
|
+
def hook
|
76
|
+
if options.commit_message?
|
77
|
+
check_commit_message options.commit_message
|
78
|
+
else
|
79
|
+
help "--hook"
|
80
|
+
end
|
81
|
+
rescue Errors::Base => error
|
82
|
+
abort colorizer.red("#{Identity::LABEL}: #{error.message}")
|
83
|
+
end
|
84
|
+
|
85
|
+
desc "-v, [--version]", "Show gem version."
|
86
|
+
map %w[-v --version] => :version
|
87
|
+
def version
|
88
|
+
say Identity::VERSION_LABEL
|
89
|
+
end
|
90
|
+
|
91
|
+
desc "-h, [--help=COMMAND]", "Show this message or get help for a command."
|
92
|
+
map %w[-h --help] => :help
|
93
|
+
def help task = nil
|
94
|
+
say and super
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
attr_reader :configuration, :runner, :colorizer
|
100
|
+
|
101
|
+
def load_collector shas
|
102
|
+
commits = shas.map { |sha| Commits::Saved.new sha: sha }
|
103
|
+
commits.empty? ? runner.call : runner.call(commits: commits)
|
104
|
+
end
|
105
|
+
|
106
|
+
def analyze_commits shas
|
107
|
+
load_collector(shas).tap do |collector|
|
108
|
+
reporter = Reporters::Branch.new collector: collector
|
109
|
+
say reporter.to_s
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def check_commit_message path
|
114
|
+
commit = Commits::Unsaved.new path: path
|
115
|
+
collector = runner.call commits: commit
|
116
|
+
reporter = Reporters::Branch.new collector: collector
|
117
|
+
say reporter.to_s
|
118
|
+
abort if collector.errors?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|