-
# frozen_string_literal: true
-
-
1
require "zeitwerk"
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.inflector.inflect "cli" => "CLI",
-
"sha" => "SHA",
-
"circle_ci" => "CircleCI",
-
"netlify_ci" => "NetlifyCI",
-
"travis_ci" => "TravisCI"
-
1
loader.tag = "git-lint"
-
1
loader.ignore "#{__dir__}/lint/rake"
-
1
loader.push_dir "#{__dir__}/.."
-
1
loader.setup
-
end
-
-
1
module Git
-
# Main namespace.
-
1
module Lint
-
1
def self.loader registry = Zeitwerk::Registry
-
6
@loader ||= registry.loaders.each.find { |loader| loader.tag == "git-lint" }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
Gem::Specification.new do |spec|
-
1
spec.name = "git-lint"
-
1
spec.version = "9.7.0"
-
1
spec.authors = ["Brooke Kuhlmann"]
-
1
spec.email = ["brooke@alchemists.io"]
-
1
spec.homepage = "https://alchemists.io/projects/git-lint"
-
1
spec.summary = "A command line interface for linting Git commits."
-
1
spec.license = "Hippocratic-2.1"
-
-
1
spec.metadata = {
-
"bug_tracker_uri" => "https://github.com/bkuhlmann/git-lint/issues",
-
"changelog_uri" => "https://alchemists.io/projects/git-lint/versions",
-
"homepage_uri" => "https://alchemists.io/projects/git-lint",
-
"funding_uri" => "https://github.com/sponsors/bkuhlmann",
-
"label" => "Git Lint",
-
"rubygems_mfa_required" => "true",
-
"source_code_uri" => "https://github.com/bkuhlmann/git-lint"
-
}
-
-
1
spec.signing_key = Gem.default_key_path
-
1
spec.cert_chain = [Gem.default_cert_path]
-
-
1
spec.required_ruby_version = ">= 3.4"
-
1
spec.add_dependency "cogger", "~> 1.0"
-
1
spec.add_dependency "containable", "~> 1.1"
-
1
spec.add_dependency "core", "~> 2.5"
-
1
spec.add_dependency "dry-monads", "~> 1.9"
-
1
spec.add_dependency "dry-schema", "~> 1.13"
-
1
spec.add_dependency "etcher", "~> 3.0"
-
1
spec.add_dependency "gitt", "~> 4.1"
-
1
spec.add_dependency "infusible", "~> 4.0"
-
1
spec.add_dependency "refinements", "~> 13.6"
-
1
spec.add_dependency "runcom", "~> 12.0"
-
1
spec.add_dependency "sod", "~> 1.5"
-
1
spec.add_dependency "spek", "~> 4.0"
-
1
spec.add_dependency "tone", "~> 2.0"
-
1
spec.add_dependency "zeitwerk", "~> 2.7"
-
-
1
spec.bindir = "exe"
-
1
spec.executables << "git-lint"
-
1
spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
-
1
spec.files = Dir["*.gemspec", "lib/**/*"]
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
# Runs all analyzers.
-
1
class Analyzer
-
1
include Dependencies[:settings]
-
-
ANALYZERS = [
-
1
Analyzers::CommitAuthorCapitalization,
-
Analyzers::CommitAuthorEmail,
-
Analyzers::CommitAuthorName,
-
Analyzers::CommitBodyBulletCapitalization,
-
Analyzers::CommitBodyBulletDelimiter,
-
Analyzers::CommitBodyBulletOnly,
-
Analyzers::CommitBodyLeadingLine,
-
Analyzers::CommitBodyLineLength,
-
Analyzers::CommitBodyParagraphCapitalization,
-
Analyzers::CommitBodyPhrase,
-
Analyzers::CommitBodyPresence,
-
Analyzers::CommitBodyTrackerShorthand,
-
Analyzers::CommitBodyWordRepeat,
-
Analyzers::CommitSignature,
-
Analyzers::CommitSubjectLength,
-
Analyzers::CommitSubjectPrefix,
-
Analyzers::CommitSubjectSuffix,
-
Analyzers::CommitSubjectWordRepeat,
-
Analyzers::CommitTrailerCollaboratorCapitalization,
-
Analyzers::CommitTrailerCollaboratorEmail,
-
Analyzers::CommitTrailerCollaboratorKey,
-
Analyzers::CommitTrailerCollaboratorName,
-
Analyzers::CommitTrailerDuplicate,
-
Analyzers::CommitTrailerFormatKey,
-
Analyzers::CommitTrailerFormatValue,
-
Analyzers::CommitTrailerIssueKey,
-
Analyzers::CommitTrailerIssueValue,
-
Analyzers::CommitTrailerMilestoneKey,
-
Analyzers::CommitTrailerMilestoneValue,
-
Analyzers::CommitTrailerOrder,
-
Analyzers::CommitTrailerReviewerKey,
-
Analyzers::CommitTrailerReviewerValue,
-
Analyzers::CommitTrailerSignerCapitalization,
-
Analyzers::CommitTrailerSignerEmail,
-
Analyzers::CommitTrailerSignerKey,
-
Analyzers::CommitTrailerSignerName,
-
Analyzers::CommitTrailerTrackerKey,
-
Analyzers::CommitTrailerTrackerValue
-
].freeze
-
-
# rubocop:disable Metrics/ParameterLists
-
1
def initialize(
-
analyzers: ANALYZERS,
-
collector: Collector.new,
-
reporter: Reporters::Branch,
-
**
-
)
-
43
super(**)
-
43
@analyzers = analyzers
-
43
@collector = collector
-
43
@reporter = reporter
-
end
-
# rubocop:enable Metrics/ParameterLists
-
-
1
def call commits: Commits::Loader.new.call
-
22
process commits
-
22
a_reporter = reporter.new(collector:)
-
22
then: 17
else: 5
block_given? ? yield(collector, a_reporter) : [collector, a_reporter]
-
end
-
-
1
private
-
-
1
attr_reader :analyzers, :collector, :reporter
-
-
1
def process commits
-
22
collector.clear
-
43
commits.value_or([]).map { |commit| analyze commit }
-
end
-
-
735
def analyze(commit) = enabled.map { |id| collector.add load_analyzer(commit, id) }
-
-
# :reek:FeatureEnvy
-
1
def enabled
-
21
settings.to_h
-
2016
.select { |key, value| key.end_with?("enabled") && value == true }
-
.keys
-
734
.map { |key| key.to_s.sub("commits_", "commit_").delete_suffix! "_enabled" }
-
end
-
-
1
def load_analyzer commit, id
-
15586
analyzers.find { |analyzer| analyzer.id == id }
-
734
.then { |analyzer| analyzer.new commit }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/string"
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# An abstract class which provides basic functionality for all analyzers to inherit from.
-
1
class Abstract
-
1
include Dependencies[:settings, :environment]
-
-
1
using Refinements::String
-
-
1
LEVELS = %w[warn error].freeze
-
1
BODY_OFFSET = 3
-
-
1
def self.id = to_s.delete_prefix!("Git::Lint::Analyzers").snakecase
-
-
1
def self.label = to_s.delete_prefix("Git::Lint::Analyzers").titleize
-
-
1
def self.build_issue_line(index, line) = {number: index + BODY_OFFSET, content: line}
-
-
1
attr_reader :commit
-
-
1
def initialize(commit, **)
-
1059
super(**)
-
1059
@commit = commit
-
1059
@filter_list = load_filter_list
-
end
-
-
1
def severity
-
92
settings.public_send("#{self.class.id}_severity".sub("commit_", "commits_"))
-
92
else: 90
then: 2
.tap { |level| fail Errors::Severity, level unless LEVELS.include? level }
-
end
-
-
1
def valid? = fail NoMethodError, "The `##{__method__}` method must be implemented."
-
-
1
def invalid? = !valid?
-
-
1
def warning? = invalid? && severity == "warn"
-
-
1
def error? = invalid? && severity == "error"
-
-
1
def issue = fail NoMethodError, "The `##{__method__}` method must be implemented."
-
-
1
protected
-
-
1
attr_reader :filter_list
-
-
1
def load_filter_list = Core::EMPTY_ARRAY
-
-
1
def affected_commit_body_lines
-
138
commit.body_lines.each.with_object([]).with_index do |(line, lines), index|
-
94
then: 34
else: 60
lines << self.class.build_issue_line(index, line) if invalid_line? line
-
end
-
end
-
-
1
def affected_commit_trailers
-
1376
commit.trailers
-
.each
-
.with_object([])
-
.with_index(commit.body_lines.size) do |(trailer, trailers), index|
-
381
else: 71
then: 310
next unless invalid_line? trailer
-
-
71
trailers << self.class.build_issue_line(index, trailer.to_s)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes author name for proper capitalization.
-
1
class CommitAuthorCapitalization < Abstract
-
1
include Dependencies[validator: "validators.capitalization"]
-
-
1
def valid? = validator.call commit.author_name
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: %(Capitalize each part of name: "#{commit.author_name}".)}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes author email address for proper format.
-
1
class CommitAuthorEmail < Abstract
-
1
include Dependencies[validator: "validators.email"]
-
-
1
def valid? = validator.call commit.author_email
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: %(Use "<name>@<server>.<domain>" instead of "#{commit.author_email}".)}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes author name for minimum parts of name.
-
1
class CommitAuthorName < Abstract
-
1
include Dependencies[validator: "validators.name"]
-
-
1
def valid? = validator.call(commit.author_name, minimum:)
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: "Author name must consist of #{minimum} parts (minimum)."}
-
end
-
-
1
private
-
-
1
def minimum = settings.commits_author_name_minimum
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit body for proper capitalization of bullet sentences.
-
1
class CommitBodyBulletCapitalization < Abstract
-
1
def valid? = lowercased_bullets.empty?
-
-
1
def issue
-
3
then: 1
else: 2
return {} if valid?
-
-
2
{
-
hint: "Capitalize first word.",
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
31
Kit::FilterList.new settings.commits_body_bullet_capitalization_includes
-
end
-
-
1
def invalid_line? line
-
67
line.sub(/link:.+(?=\[)/, "").match?(/\A\s*#{Regexp.union filter_list}\s[[:lower:]]+/)
-
end
-
-
1
private
-
-
60
def lowercased_bullets = commit.body_lines.select { |line| invalid_line? line }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit body delimiter usage.
-
1
class CommitBodyBulletDelimiter < Abstract
-
60
def valid? = commit.body_lines.none? { |line| invalid_line? line }
-
-
1
def issue
-
3
then: 1
else: 2
return {} if valid?
-
-
2
{
-
hint: "Use space after bullet.",
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
31
Kit::FilterList.new settings.commits_body_bullet_delimiter_includes
-
end
-
-
1
def invalid_line?(line) = line.match?(/\A\s*#{pattern}(?!(#{pattern}|\s)).+\Z/)
-
-
1
def pattern = Regexp.union filter_list
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit bodies with only a single bullet point.
-
1
class CommitBodyBulletOnly < Abstract
-
1
def valid? = !affected_commit_body_lines.one?
-
-
1
def issue
-
5
then: 1
else: 4
return {} if valid?
-
-
4
{
-
hint: "Use paragraph instead of single bullet.",
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
32
Kit::FilterList.new settings.commits_body_bullet_only_includes
-
end
-
-
1
def invalid_line?(line) = line.match?(/\A#{Regexp.union filter_list}\s+/)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes leading line between commit subject and start of body.
-
1
class CommitBodyLeadingLine < Abstract
-
1
def valid?
-
119
raw = commit.raw
-
119
subject, body = raw.split "\n", 2
-
-
119
then: 99
else: 20
return true if !String(subject).empty? && String(body).strip.empty?
-
-
20
raw.match?(/\A.+(\n\n|\#).+/m)
-
end
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: "Use blank line between subject and body."}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit body line length to prevent unnecessary horizontal scrolling.
-
1
class CommitBodyLineLength < Abstract
-
10
def valid? = commit.body_lines.all? { |line| !invalid_line? line }
-
-
1
def issue
-
3
then: 1
else: 2
return {} if valid?
-
-
{
-
2
hint: "Use #{maximum} characters or less per line.",
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line?(line) = line.length > maximum
-
-
1
private
-
-
1
def maximum = settings.commits_body_line_length_maximum
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes proper capitalization of commit body paragraphs.
-
1
class CommitBodyParagraphCapitalization < Abstract
-
1
PATTERN = /
-
\A # Search start.
-
(?! # Negative lookahead start.
-
(?: # Non-capture group start.
-
audio # Ignore audio.
-
| # Or.
-
image # Ignore image.
-
| # Or.
-
video # Ignore video.
-
) # Non-capture group end.
-
:: # Suffix.
-
| # Or.
-
link: # Ignore link.
-
| # Or.
-
xref: # Ignore xref.
-
) # Negative lookahead end.
-
[[:lower:]] # Match lowercase letters.
-
.+ # Match one or more characters.
-
\Z # Search end.
-
/mx
-
-
1
def initialize(commit, pattern: PATTERN, **)
-
34
super(commit, **)
-
34
@pattern = pattern
-
end
-
-
1
def valid? = invalids.empty?
-
-
1
def issue
-
3
then: 1
else: 2
return {} if valid?
-
-
2
{
-
hint: "Capitalize first word.",
-
lines: affected_lines
-
}
-
end
-
-
1
private
-
-
1
attr_reader :pattern
-
-
1
def affected_lines
-
2
invalids.each.with_object [] do |line, lines|
-
2
lines << self.class.build_issue_line(commit.body_lines.index(line[/.+/]), line)
-
end
-
end
-
-
1
def invalids
-
125
@invalids ||= commit.body_paragraphs.grep pattern
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes use of commit body phrases that are not informative.
-
1
class CommitBodyPhrase < Abstract
-
85
def valid? = commit.body_lines.all? { |line| !invalid_line? line }
-
-
1
def issue
-
3
then: 1
else: 2
return {} if valid?
-
-
{
-
2
hint: %(Avoid: #{filter_list.to_usage}.),
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
62
Kit::FilterList.new settings.commits_body_phrase_excludes
-
end
-
-
1
def invalid_line? line
-
88
line.downcase.match? Regexp.new(Regexp.union(filter_list).source, Regexp::IGNORECASE)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes presence of commit body.
-
1
class CommitBodyPresence < Abstract
-
1
using Refinements::String
-
-
1
def valid?
-
7
then: 1
else: 6
return true if commit.fixup?
-
-
6
valid_lines = commit.body_lines.grep_v(/^\s*$/)
-
6
valid_lines.size >= minimum
-
end
-
-
1
def minimum = settings.commits_body_presence_minimum
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: %(Use minimum of #{"#{minimum} line".pluralize "s", minimum} (non-empty).)}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes body tracker shorthand usage.
-
1
class CommitBodyTrackerShorthand < Abstract
-
68
def valid? = commit.body_lines.none? { |line| invalid_line? line }
-
-
1
def issue
-
3
then: 1
else: 2
return {} if valid?
-
-
{
-
2
hint: "Explain issue instead of using shorthand. Avoid: #{filter_list.to_usage}.",
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
43
Kit::FilterList.new settings.commits_body_tracker_shorthand_excludes
-
end
-
-
1
def invalid_line?(line) = line.match?(/.*#{Regexp.union filter_list}.*/)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit body for repeated words.
-
1
class CommitBodyWordRepeat < Abstract
-
1
include Dependencies[validator: "validators.repeated_word"]
-
-
51
def valid? = commit.body_lines.all? { |line| !invalid_line? line }
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Avoid repeating these words: #{validator.call commit.body}.",
-
lines: affected_commit_body_lines
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? line
-
52
then: 7
else: 45
return false if line.start_with? "#"
-
-
45
!validator.call(line).empty?
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit signature validity.
-
1
class CommitSignature < Abstract
-
1
include Dependencies[sanitizer: "sanitizers.signature"]
-
-
1
def valid?
-
4
sanitizer.call(commit.signature).match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
-
3
then: 1
else: 1
def issue = valid? ? {} : {hint: %(Use: #{filter_list.to_usage "or"}.)}
-
-
1
protected
-
-
1
def load_filter_list
-
4
Kit::FilterList.new settings.commits_signature_includes
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit subject length is short and concise.
-
1
class CommitSubjectLength < Abstract
-
1
def valid? = commit.subject.sub(/(fixup!|squash!)\s{1}/, "").size <= maximum
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: "Use #{maximum} characters or less."}
-
end
-
-
1
private
-
-
1
def maximum = settings.commits_subject_length_maximum
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit subject uses standard prefix.
-
1
class CommitSubjectPrefix < Abstract
-
1
def valid?
-
133
then: 3
else: 130
return true if locally_prefixed?
-
130
then: 1
else: 129
return true if filter_list.empty?
-
-
129
commit.subject.match?(/\A#{Regexp.union filter_list}/)
-
end
-
-
1
def issue
-
11
then: 1
else: 10
return {} if valid?
-
-
10
{hint: %(Use: #{filter_list.to_usage "or"}.)}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
42
settings.commits_subject_prefix_includes
-
198
.map { |prefix| "#{prefix}#{delimiter}" }
-
42
.then { |list| Kit::FilterList.new list }
-
end
-
-
1
def locally_prefixed? = !ci? && commit.directive?
-
-
1
def ci? = environment["CI"] == "true"
-
-
1
def delimiter = settings.commits_subject_prefix_delimiter
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit subject suffix for punctuation.
-
1
class CommitSubjectSuffix < Abstract
-
1
def valid?
-
76
then: 1
else: 75
return true if filter_list.empty?
-
-
75
!commit.subject.match?(/#{Regexp.union filter_list}\Z/)
-
end
-
-
1
def issue
-
5
then: 1
else: 4
return {} if valid?
-
-
4
{hint: %(Avoid: #{filter_list.to_usage}.)}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
29
Kit::FilterList.new settings.commits_subject_suffix_excludes
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit subject for repeated words.
-
1
class CommitSubjectWordRepeat < Abstract
-
1
include Dependencies[validator: "validators.repeated_word"]
-
-
1
def valid? = validator.call(commit.subject).empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{hint: "Avoid repeating these words: #{validator.call commit.subject}."}
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer collaborator name capitalization.
-
1
class CommitTrailerCollaboratorCapitalization < Abstract
-
1
include Dependencies[
-
setting: "trailers.collaborator",
-
parser: "parsers.person",
-
validator: "validators.capitalization"
-
]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{
-
hint: "Name must be capitalized.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
19
parser.call(trailer.value).then do |person|
-
19
trailer.key.match?(setting.pattern) && !validator.call(person.name)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer collaborator email address format.
-
1
class CommitTrailerCollaboratorEmail < Abstract
-
1
include Dependencies[
-
setting: "trailers.collaborator",
-
parser: "parsers.person",
-
validator: "validators.email"
-
]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{
-
hint: %(Email must follow name and use format: "<name@server.domain>".),
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
18
email = parser.call(trailer.value).email
-
18
trailer.key.match?(setting.pattern) && !validator.call(email)
-
end
-
-
1
private
-
-
1
attr_reader :parser, :validator
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer collaborator key usage.
-
1
class CommitTrailerCollaboratorKey < Abstract
-
1
include Dependencies[setting: "trailers.collaborator"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list = Kit::FilterList.new setting.name
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer collaborator name construction.
-
1
class CommitTrailerCollaboratorName < Abstract
-
1
include Dependencies[
-
setting: "trailers.collaborator",
-
parser: "parsers.person",
-
validator: "validators.name"
-
]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Name must follow key and consist of #{minimum} parts (minimum).",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
20
parser.call(trailer.value).then do |person|
-
20
trailer.key.match?(setting.pattern) && !validator.call(person.name, minimum:)
-
end
-
end
-
-
1
private
-
-
1
def minimum = settings.commits_trailer_collaborator_name_minimum
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer duplicate.
-
1
class CommitTrailerDuplicate < Abstract
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{
-
hint: "Avoid duplicates.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line?(trailer) = commit.trailers.tally[trailer] != 1
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer format key usage.
-
1
class CommitTrailerFormatKey < Abstract
-
1
include Dependencies[setting: "trailers.format"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list = Kit::FilterList.new setting.name
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer format value.
-
1
class CommitTrailerFormatValue < Abstract
-
1
include Dependencies[setting: "trailers.format"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage "or"}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
27
Kit::FilterList.new settings.commits_trailer_format_value_includes
-
end
-
-
1
def invalid_line? trailer
-
19
trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern)
-
end
-
-
1
def value_pattern = /\A#{Regexp.union filter_list}\Z/
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer issue key usage.
-
1
class CommitTrailerIssueKey < Abstract
-
1
include Dependencies[setting: "trailers.issue"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list = Kit::FilterList.new setting.name
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer issue value.
-
1
class CommitTrailerIssueValue < Abstract
-
1
include Dependencies[setting: "trailers.issue"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
27
Kit::FilterList.new settings.commits_trailer_issue_value_includes
-
end
-
-
1
def invalid_line? trailer
-
19
trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern)
-
end
-
-
1
def value_pattern = /\A#{Regexp.union filter_list}\Z/
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer milestone key usage.
-
1
class CommitTrailerMilestoneKey < Abstract
-
1
include Dependencies[setting: "trailers.milestone"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list = Kit::FilterList.new setting.name
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer milestone value.
-
1
class CommitTrailerMilestoneValue < Abstract
-
1
include Dependencies[setting: "trailers.milestone"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use: #{filter_list.to_usage "or"}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
27
Kit::FilterList.new settings.commits_trailer_milestone_value_includes
-
end
-
-
1
def invalid_line? trailer
-
19
trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern)
-
end
-
-
1
def value_pattern = /\A#{Regexp.union filter_list}\Z/
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer order value.
-
1
class CommitTrailerOrder < Abstract
-
1
def initialize(...)
-
25
super
-
25
@original_order = commit.trailers.map(&:key)
-
25
@sorted_order = original_order.sort
-
end
-
-
1
def valid? = original_order == sorted_order
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{
-
hint: "Ensure keys are alphabetically sorted.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
3
key = trailer.key
-
3
original_order.index(key) != sorted_order.index(key)
-
end
-
-
1
private
-
-
1
attr_reader :original_order, :sorted_order
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer reviwer key usage.
-
1
class CommitTrailerReviewerKey < Abstract
-
1
include Dependencies[setting: "trailers.reviewer"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list = Kit::FilterList.new setting.name
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer reviewer value.
-
1
class CommitTrailerReviewerValue < Abstract
-
1
include Dependencies[setting: "trailers.reviewer"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use: #{filter_list.to_usage "or"}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
27
Kit::FilterList.new settings.commits_trailer_reviewer_value_includes
-
end
-
-
1
def invalid_line? trailer
-
19
trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern)
-
end
-
-
1
def value_pattern = /\A#{Regexp.union filter_list}\Z/
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer signer name capitalization.
-
1
class CommitTrailerSignerCapitalization < Abstract
-
1
include Dependencies[
-
setting: "trailers.signer",
-
parser: "parsers.person",
-
validator: "validators.capitalization"
-
]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{
-
hint: "Name must be capitalized.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
19
parser.call(trailer.value).then do |person|
-
19
trailer.key.match?(setting.pattern) && !validator.call(person.name)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer signer email address format.
-
1
class CommitTrailerSignerEmail < Abstract
-
1
include Dependencies[
-
setting: "trailers.signer",
-
parser: "parsers.person",
-
validator: "validators.email"
-
]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
1
{
-
hint: %(Email must follow name and use format: "<name@server.domain>".),
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
18
email = parser.call(trailer.value).email
-
18
trailer.key.match?(setting.pattern) && !validator.call(email)
-
end
-
-
1
private
-
-
1
attr_reader :parser, :validator
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer signer key usage.
-
1
class CommitTrailerSignerKey < Abstract
-
1
include Dependencies[setting: "trailers.signer"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
28
Kit::FilterList.new setting.name
-
end
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer signer name construction.
-
1
class CommitTrailerSignerName < Abstract
-
1
include Dependencies[
-
setting: "trailers.signer",
-
parser: "parsers.person",
-
validator: "validators.name"
-
]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Name must follow key and consist of #{minimum} parts (minimum).",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def invalid_line? trailer
-
20
parser.call(trailer.value).then do |person|
-
20
trailer.key.match?(setting.pattern) && !validator.call(person.name, minimum:)
-
end
-
end
-
-
1
private
-
-
1
def minimum = settings.commits_trailer_signer_name_minimum
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer tracker key usage.
-
1
class CommitTrailerTrackerKey < Abstract
-
1
include Dependencies[setting: "trailers.tracker"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list = Kit::FilterList.new setting.name
-
-
1
def invalid_line? trailer
-
20
trailer.key.then do |key|
-
20
key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Analyzers
-
# Analyzes commit trailer tracker value.
-
1
class CommitTrailerTrackerValue < Abstract
-
1
include Dependencies[setting: "trailers.tracker"]
-
-
1
def valid? = affected_commit_trailers.empty?
-
-
1
def issue
-
2
then: 1
else: 1
return {} if valid?
-
-
{
-
1
hint: "Use format: #{filter_list.to_usage}.",
-
lines: affected_commit_trailers
-
}
-
end
-
-
1
protected
-
-
1
def load_filter_list
-
27
Kit::FilterList.new settings.commits_trailer_tracker_value_includes
-
end
-
-
1
def invalid_line? trailer
-
19
trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern)
-
end
-
-
1
def value_pattern = /\A#{Regexp.union filter_list}\Z/
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Git
-
1
module Lint
-
1
module CLI
-
1
module Actions
-
1
module Analyze
-
# Handles analyze action for branch.
-
1
class Branch < Sod::Action
-
1
include Dependencies[:logger, :kernel, :io]
-
-
1
description "Analyze current branch."
-
-
1
on %w[-b --branch]
-
-
1
def initialize(analyzer: Analyzer.new, **)
-
14
super(**)
-
14
@analyzer = analyzer
-
end
-
-
1
def call(*)
-
7
parse
-
rescue Errors::Base => error
-
4
logger.error { error.message }
-
2
kernel.abort
-
end
-
-
1
private
-
-
1
attr_reader :analyzer
-
-
1
def parse
-
7
analyzer.call do |collector, reporter|
-
5
io.puts reporter
-
5
then: 3
else: 2
kernel.abort if collector.errors?
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Git
-
1
module Lint
-
1
module CLI
-
1
module Actions
-
1
module Analyze
-
# Handles analyze action for single commit SHA
-
1
class Commit < Sod::Action
-
1
include Dependencies[:git, :logger, :kernel, :io]
-
-
1
description "Analyze specific commits."
-
-
1
on %w[-c --commit], argument: "a,b,c"
-
-
1
def initialize(analyzer: Analyzer.new, **)
-
15
super(**)
-
15
@analyzer = analyzer
-
end
-
-
1
def call *arguments
-
8
process arguments.unshift "-1"
-
rescue Errors::Base => error
-
4
logger.error { error.message }
-
2
kernel.abort
-
end
-
-
1
private
-
-
1
attr_reader :analyzer
-
-
1
def process arguments
-
8
analyzer.call commits: git.commits(*arguments) do |collector, reporter|
-
6
io.puts reporter
-
6
then: 3
else: 3
kernel.abort if collector.errors?
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
1
require "sod/types/pathname"
-
-
1
module Git
-
1
module Lint
-
1
module CLI
-
1
module Actions
-
# Handles unsaved Git commit action.
-
1
class Hook < Sod::Action
-
1
include Dependencies[:git, :logger, :kernel, :io]
-
-
1
description "Hook for analyzing unsaved commits."
-
-
1
on "--hook", argument: "PATH", type: Pathname
-
-
1
def initialize(analyzer: Analyzer.new, **)
-
12
super(**)
-
12
@analyzer = analyzer
-
end
-
-
1
def call path
-
5
analyzer.call commits: commits(path) do |collector, reporter|
-
5
io.puts reporter
-
5
then: 3
else: 2
kernel.abort if collector.errors?
-
end
-
end
-
-
1
private
-
-
1
attr_reader :analyzer
-
-
6
def commits(path) = git.uncommitted(path).fmap { |commit| [commit] }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Git
-
1
module Lint
-
1
module CLI
-
# The main Command Line Interface (CLI) object.
-
1
class Shell
-
1
include Dependencies[:defaults_path, :xdg_config, :specification]
-
-
1
def initialize(context: Sod::Context, dsl: Sod, **)
-
10
super(**)
-
10
@context = context
-
10
@dsl = dsl
-
end
-
-
1
def call(...) = cli.call(...)
-
-
1
private
-
-
1
attr_reader :context, :dsl
-
-
1
def cli
-
9
context = build_context
-
-
9
dsl.new "git-lint", banner: specification.banner do
-
9
on(Sod::Prefabs::Commands::Config, context:)
-
-
9
on "analyze", "Analyze branch or commit(s)." do
-
9
on Actions::Analyze::Branch
-
9
on Actions::Analyze::Commit
-
end
-
-
9
on Actions::Hook
-
9
on(Sod::Prefabs::Actions::Version, context:)
-
9
on Sod::Prefabs::Actions::Help, self
-
end
-
end
-
-
1
def build_context
-
9
context[defaults_path:, xdg_config:, version_label: specification.labeled_version]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
# Collects and categorizes, by severity, all issues (if any).
-
1
class Collector
-
1
def initialize
-
114
@collection = Hash.new { |default, missing_id| default[missing_id] = [] }
-
end
-
-
1
def add analyzer
-
770
collection[analyzer.commit] << analyzer
-
770
analyzer
-
end
-
-
1
def retrieve(id) = collection[id]
-
-
1
def clear = collection.clear && self
-
-
1
def empty? = collection.empty?
-
-
1
def warnings? = collection.values.flatten.any?(&:warning?)
-
-
1
def errors? = collection.values.flatten.any?(&:error?)
-
-
1
def issues? = collection.values.flatten.any?(&:invalid?)
-
-
1
def total_warnings = collection.values.flatten.count(&:warning?)
-
-
1
def total_errors = collection.values.flatten.count(&:error?)
-
-
1
def total_issues = collection.values.flatten.count(&:invalid?)
-
-
1
def total_commits = collection.keys.size
-
-
1
def to_h = collection
-
-
1
private
-
-
1
attr_reader :collection
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Commits
-
1
module Hosts
-
# Provides Circle CI feature branch information.
-
1
class CircleCI
-
1
include Dependencies[:git]
-
-
1
def call = git.commits "origin/#{branch_default}..#{branch_name}"
-
-
1
private
-
-
1
def branch_default = git.branch_default.value_or nil
-
-
1
def branch_name = "origin/#{git.branch_name.value_or nil}"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Commits
-
1
module Hosts
-
# Provides GitHub Action feature branch information.
-
1
class GitHubAction
-
1
include Dependencies[:git]
-
-
1
def call = git.commits "origin/#{branch_default}..#{branch_name}"
-
-
1
private
-
-
1
def branch_default = git.branch_default.value_or nil
-
-
1
def branch_name = "origin/#{git.branch_name.value_or nil}"
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Commits
-
1
module Hosts
-
# Provides local feature branch information.
-
1
class Local
-
1
include Dependencies[:git]
-
-
1
def call = git.commits "#{branch_default}..#{branch_name}"
-
-
1
private
-
-
1
def branch_default = git.branch_default.value_or nil
-
-
1
def branch_name = git.branch_name.value_or nil
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Commits
-
1
module Hosts
-
# Provides Netlify CI feature branch information.
-
1
class NetlifyCI
-
1
include Dependencies[:git, :environment]
-
-
1
def call
-
3
git.call("remote", "add", "-f", "origin", environment["REPOSITORY_URL"])
-
3
.bind { git.call "fetch", "origin", "#{branch_name}:#{branch_name}" }
-
3
.bind { git.commits "origin/#{branch_default}..origin/#{branch_name}" }
-
end
-
-
1
private
-
-
1
def branch_default = git.branch_default.value_or nil
-
-
1
def branch_name = environment["HEAD"]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/string"
-
-
1
module Git
-
1
module Lint
-
1
module Commits
-
# Automatically detects and loads host.
-
1
class Loader
-
1
include Dependencies[
-
:git,
-
:environment,
-
circle_ci: "hosts.circle_ci",
-
git_hub_action: "hosts.git_hub_action",
-
netlify_ci: "hosts.git_hub_action",
-
local: "hosts.local"
-
]
-
-
1
using Refinements::String
-
-
1
def call
-
15
message = "Invalid repository. Are you within a Git repository?"
-
15
else: 14
then: 1
fail Errors::Base, message unless git.exist?
-
-
14
host.call
-
end
-
-
1
private
-
-
1
def host
-
14
then: 1
else: 13
if key? "CIRCLECI" then circle_ci
-
13
then: 1
else: 12
elsif key? "GITHUB_ACTIONS" then git_hub_action
-
12
then: 1
else: 11
elsif key? "NETLIFY" then netlify_ci
-
11
else local
-
end
-
end
-
-
1
def key?(key) = environment.fetch(key, "false").truthy?
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/schema"
-
1
require "etcher"
-
-
1
Dry::Schema.load_extensions :monads
-
-
1
module Git
-
1
module Lint
-
1
module Configuration
-
1
Contract = Dry::Schema.Params do
-
1
required(:commits_author_capitalization_enabled).filled :bool
-
1
required(:commits_author_capitalization_severity).filled :string
-
1
required(:commits_author_email_enabled).filled :bool
-
1
required(:commits_author_email_severity).filled :string
-
1
required(:commits_author_name_enabled).filled :bool
-
1
required(:commits_author_name_severity).filled :string
-
1
required(:commits_author_name_minimum).filled :integer
-
1
required(:commits_body_bullet_capitalization_enabled).filled :bool
-
1
required(:commits_body_bullet_capitalization_severity).filled :string
-
1
required(:commits_body_bullet_capitalization_includes).array :string
-
1
required(:commits_body_bullet_delimiter_enabled).filled :bool
-
1
required(:commits_body_bullet_delimiter_severity).filled :string
-
1
required(:commits_body_bullet_delimiter_includes).array :string
-
1
required(:commits_body_bullet_only_enabled).filled :bool
-
1
required(:commits_body_bullet_only_severity).filled :string
-
1
required(:commits_body_bullet_only_includes).array :string
-
1
required(:commits_body_leading_line_enabled).filled :bool
-
1
required(:commits_body_leading_line_severity).filled :string
-
1
required(:commits_body_line_length_enabled).filled :bool
-
1
required(:commits_body_line_length_severity).filled :string
-
1
required(:commits_body_line_length_maximum).filled :integer
-
1
required(:commits_body_paragraph_capitalization_enabled).filled :bool
-
1
required(:commits_body_paragraph_capitalization_severity).filled :string
-
1
required(:commits_body_phrase_enabled).filled :bool
-
1
required(:commits_body_phrase_severity).filled :string
-
1
required(:commits_body_phrase_excludes).array :string
-
1
required(:commits_body_presence_enabled).filled :bool
-
1
required(:commits_body_presence_severity).filled :string
-
1
required(:commits_body_presence_minimum).filled :integer
-
1
required(:commits_body_word_repeat_enabled).filled :bool
-
1
required(:commits_body_word_repeat_severity).filled :string
-
1
required(:commits_body_tracker_shorthand_enabled).filled :bool
-
1
required(:commits_body_tracker_shorthand_severity).filled :string
-
1
required(:commits_body_tracker_shorthand_excludes).array :string
-
1
required(:commits_signature_enabled).filled :bool
-
1
required(:commits_signature_severity).filled :string
-
1
required(:commits_signature_includes).array :string
-
1
required(:commits_subject_length_enabled).filled :bool
-
1
required(:commits_subject_length_severity).filled :string
-
1
required(:commits_subject_length_maximum).filled :integer
-
1
required(:commits_subject_prefix_enabled).filled :bool
-
1
required(:commits_subject_prefix_severity).filled :string
-
1
required(:commits_subject_prefix_delimiter).filled :string
-
1
required(:commits_subject_prefix_includes).array :string
-
1
required(:commits_subject_suffix_enabled).filled :bool
-
1
required(:commits_subject_suffix_severity).filled :string
-
1
required(:commits_subject_suffix_excludes).array :string
-
1
required(:commits_subject_word_repeat_enabled).filled :bool
-
1
required(:commits_subject_word_repeat_severity).filled :string
-
1
required(:commits_trailer_collaborator_capitalization_enabled).filled :bool
-
1
required(:commits_trailer_collaborator_capitalization_severity).filled :string
-
1
required(:commits_trailer_collaborator_email_enabled).filled :bool
-
1
required(:commits_trailer_collaborator_email_severity).filled :string
-
1
required(:commits_trailer_collaborator_key_enabled).filled :bool
-
1
required(:commits_trailer_collaborator_key_severity).filled :string
-
1
required(:commits_trailer_collaborator_name_enabled).filled :bool
-
1
required(:commits_trailer_collaborator_name_severity).filled :string
-
1
required(:commits_trailer_collaborator_name_minimum).filled :integer
-
1
required(:commits_trailer_duplicate_enabled).filled :bool
-
1
required(:commits_trailer_duplicate_severity).filled :string
-
1
required(:commits_trailer_format_key_enabled).filled :bool
-
1
required(:commits_trailer_format_key_severity).filled :string
-
1
required(:commits_trailer_format_value_enabled).filled :bool
-
1
required(:commits_trailer_format_value_severity).filled :string
-
1
required(:commits_trailer_format_value_includes).array :string
-
1
required(:commits_trailer_issue_key_enabled).filled :bool
-
1
required(:commits_trailer_issue_key_severity).filled :string
-
1
required(:commits_trailer_issue_value_enabled).filled :bool
-
1
required(:commits_trailer_issue_value_severity).filled :string
-
1
required(:commits_trailer_issue_value_includes).array :string
-
1
required(:commits_trailer_milestone_key_enabled).filled :bool
-
1
required(:commits_trailer_milestone_key_severity).filled :string
-
1
required(:commits_trailer_milestone_value_enabled).filled :bool
-
1
required(:commits_trailer_milestone_value_severity).filled :string
-
1
required(:commits_trailer_milestone_value_includes).array :string
-
1
required(:commits_trailer_order_enabled).filled :bool
-
1
required(:commits_trailer_order_severity).filled :string
-
1
required(:commits_trailer_reviewer_key_enabled).filled :bool
-
1
required(:commits_trailer_reviewer_key_severity).filled :string
-
1
required(:commits_trailer_reviewer_value_enabled).filled :bool
-
1
required(:commits_trailer_reviewer_value_severity).filled :string
-
1
required(:commits_trailer_reviewer_value_includes).array :string
-
1
required(:commits_trailer_signer_capitalization_enabled).filled :bool
-
1
required(:commits_trailer_signer_capitalization_severity).filled :string
-
1
required(:commits_trailer_signer_email_enabled).filled :bool
-
1
required(:commits_trailer_signer_email_severity).filled :string
-
1
required(:commits_trailer_signer_key_enabled).filled :bool
-
1
required(:commits_trailer_signer_key_severity).filled :string
-
1
required(:commits_trailer_signer_name_enabled).filled :bool
-
1
required(:commits_trailer_signer_name_severity).filled :string
-
1
required(:commits_trailer_signer_name_minimum).filled :integer
-
1
required(:commits_trailer_tracker_key_enabled).filled :bool
-
1
required(:commits_trailer_tracker_key_severity).filled :string
-
1
required(:commits_trailer_tracker_value_enabled).filled :bool
-
1
required(:commits_trailer_tracker_value_severity).filled :string
-
1
required(:commits_trailer_tracker_value_includes).array :string
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Configuration
-
# Defines configuration content as the primary source of truth for use throughout the gem.
-
1
Model = Struct.new :commits_author_capitalization_enabled,
-
:commits_author_capitalization_severity,
-
:commits_author_email_enabled,
-
:commits_author_email_severity,
-
:commits_author_name_enabled,
-
:commits_author_name_severity,
-
:commits_author_name_minimum,
-
:commits_body_bullet_capitalization_enabled,
-
:commits_body_bullet_capitalization_severity,
-
:commits_body_bullet_capitalization_includes,
-
:commits_body_bullet_delimiter_enabled,
-
:commits_body_bullet_delimiter_severity,
-
:commits_body_bullet_delimiter_includes,
-
:commits_body_bullet_only_enabled,
-
:commits_body_bullet_only_severity,
-
:commits_body_bullet_only_includes,
-
:commits_body_leading_line_enabled,
-
:commits_body_leading_line_severity,
-
:commits_body_line_length_enabled,
-
:commits_body_line_length_severity,
-
:commits_body_line_length_maximum,
-
:commits_body_paragraph_capitalization_enabled,
-
:commits_body_paragraph_capitalization_severity,
-
:commits_body_phrase_enabled,
-
:commits_body_phrase_severity,
-
:commits_body_phrase_excludes,
-
:commits_body_presence_enabled,
-
:commits_body_presence_severity,
-
:commits_body_presence_minimum,
-
:commits_body_word_repeat_enabled,
-
:commits_body_word_repeat_severity,
-
:commits_body_tracker_shorthand_enabled,
-
:commits_body_tracker_shorthand_severity,
-
:commits_body_tracker_shorthand_excludes,
-
:commits_signature_enabled,
-
:commits_signature_severity,
-
:commits_signature_includes,
-
:commits_subject_length_enabled,
-
:commits_subject_length_severity,
-
:commits_subject_length_maximum,
-
:commits_subject_prefix_enabled,
-
:commits_subject_prefix_severity,
-
:commits_subject_prefix_delimiter,
-
:commits_subject_prefix_includes,
-
:commits_subject_suffix_enabled,
-
:commits_subject_suffix_severity,
-
:commits_subject_suffix_excludes,
-
:commits_subject_word_repeat_enabled,
-
:commits_subject_word_repeat_severity,
-
:commits_trailer_collaborator_capitalization_enabled,
-
:commits_trailer_collaborator_capitalization_severity,
-
:commits_trailer_collaborator_email_enabled,
-
:commits_trailer_collaborator_email_severity,
-
:commits_trailer_collaborator_key_enabled,
-
:commits_trailer_collaborator_key_severity,
-
:commits_trailer_collaborator_name_enabled,
-
:commits_trailer_collaborator_name_severity,
-
:commits_trailer_collaborator_name_minimum,
-
:commits_trailer_duplicate_enabled,
-
:commits_trailer_duplicate_severity,
-
:commits_trailer_format_key_enabled,
-
:commits_trailer_format_key_severity,
-
:commits_trailer_format_value_enabled,
-
:commits_trailer_format_value_severity,
-
:commits_trailer_format_value_includes,
-
:commits_trailer_issue_key_enabled,
-
:commits_trailer_issue_key_severity,
-
:commits_trailer_issue_value_enabled,
-
:commits_trailer_issue_value_severity,
-
:commits_trailer_issue_value_includes,
-
:commits_trailer_milestone_key_enabled,
-
:commits_trailer_milestone_key_severity,
-
:commits_trailer_milestone_value_enabled,
-
:commits_trailer_milestone_value_severity,
-
:commits_trailer_milestone_value_includes,
-
:commits_trailer_order_enabled,
-
:commits_trailer_order_severity,
-
:commits_trailer_reviewer_key_enabled,
-
:commits_trailer_reviewer_key_severity,
-
:commits_trailer_reviewer_value_enabled,
-
:commits_trailer_reviewer_value_severity,
-
:commits_trailer_reviewer_value_includes,
-
:commits_trailer_signer_capitalization_enabled,
-
:commits_trailer_signer_capitalization_severity,
-
:commits_trailer_signer_email_enabled,
-
:commits_trailer_signer_email_severity,
-
:commits_trailer_signer_key_enabled,
-
:commits_trailer_signer_key_severity,
-
:commits_trailer_signer_name_enabled,
-
:commits_trailer_signer_name_severity,
-
:commits_trailer_signer_name_minimum,
-
:commits_trailer_tracker_key_enabled,
-
:commits_trailer_tracker_key_severity,
-
:commits_trailer_tracker_value_enabled,
-
:commits_trailer_tracker_value_severity,
-
:commits_trailer_tracker_value_includes
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Configuration
-
# Defines trailer configuration as a subset of the primary settings.
-
1
Trailer = Data.define :name, :pattern
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Git
-
1
module Lint
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Errors
-
# The root class of gem related errors.
-
1
class Base < StandardError
-
1
def initialize message = "Invalid Git Lint action."
-
10
super
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
-
1
module Git
-
1
module Lint
-
1
module Errors
-
# Categorizes severity errors.
-
1
class Severity < Base
-
1
using Refinements::Array
-
-
1
def initialize level
-
3
usage = Analyzers::Abstract::LEVELS.to_usage "or"
-
3
super %(Invalid severity level: #{level}. Use: #{usage}.)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Errors
-
# Categorizes commit SHA errors.
-
1
class SHA < Base
-
1
def initialize sha
-
1
super %(Invalid commit SHA: "#{sha}". Unable to obtain commit details.)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/array"
-
-
1
module Git
-
1
module Lint
-
1
module Kit
-
# Represents an regular expression list which may be used as an analyzer setting.
-
1
class FilterList
-
1
using Refinements::Array
-
-
1
def initialize list = Core::EMPTY_ARRAY
-
3382
@list = Array(list).map { |item| Regexp.new item }
-
end
-
-
1
def empty? = list.empty?
-
-
1
def to_a = list
-
-
1
alias to_ary to_a
-
-
1
def to_usage(...) = list.to_usage(...)
-
-
1
private
-
-
1
attr_reader :list
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "git/lint"
-
1
require "rake"
-
-
1
module Git
-
1
module Lint
-
1
module Rake
-
# Registers Rake tasks for use.
-
1
class Register
-
1
include ::Rake::DSL
-
-
1
def self.call = new.call
-
-
1
def initialize shell: CLI::Shell.new
-
2
@shell = shell
-
end
-
-
1
def call
-
2
desc "Run Git Lint"
-
3
task(:git_lint) { shell.call %w[analyze --branch] }
-
end
-
-
1
private
-
-
1
attr_reader :shell
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Reporters
-
# Reports issues related to a single branch.
-
1
class Branch
-
1
include Dependencies[:color]
-
-
1
using Refinements::String
-
-
1
def initialize(collector: Collector.new, **)
-
26
super(**)
-
26
@collector = collector
-
end
-
-
1
def to_s
-
20
"Running Git Lint...#{branch_report}\n" \
-
"#{commit_total}. #{issue_totals}.\n"
-
end
-
-
1
alias to_str to_s
-
-
1
private
-
-
1
attr_reader :collector
-
-
1
def branch_report
-
20
else: 11
then: 9
return "" unless collector.issues?
-
-
11
"\n\n#{commit_report}".chomp
-
end
-
-
1
def commit_report
-
11
collector.to_h.reduce "" do |details, (commit, analyzers)|
-
11
details + Commit.new(commit:, analyzers:)
-
end
-
end
-
-
1
def commit_total
-
20
total = collector.total_commits
-
20
%(#{total} #{"commit".pluralize "s", total} inspected)
-
end
-
-
1
def issue_totals
-
20
then: 11
if collector.issues?
-
11
"#{issue_total} detected (#{warning_total}, #{error_total})"
-
else: 9
else
-
9
color["0 issues", :green] + " detected"
-
end
-
end
-
-
1
def issue_total
-
11
then: 10
else: 1
style = collector.errors? ? :red : :yellow
-
11
total = collector.total_issues
-
11
color["#{total} issue".pluralize("s", total), style]
-
end
-
-
1
def warning_total
-
11
then: 1
else: 10
style = collector.warnings? ? :yellow : :green
-
11
total = collector.total_warnings
-
11
color["#{total} warning".pluralize("s", total), style]
-
end
-
-
1
def error_total
-
11
then: 10
else: 1
style = collector.errors? ? :red : :green
-
11
total = collector.total_errors
-
11
color["#{total} error".pluralize("s", total), style]
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Reporters
-
# Reports issues related to a single commit.
-
1
class Commit
-
1
def initialize commit:, analyzers: []
-
15
@commit = commit
-
15
@analyzers = analyzers.select(&:invalid?)
-
end
-
-
1
def to_s
-
15
then: 1
else: 14
return "" if analyzers.empty?
-
-
14
"#{commit.sha} (#{commit.author_name}, #{commit.authored_relative_at}): " \
-
"#{commit.subject}\n#{report}\n"
-
end
-
-
1
alias to_str to_s
-
-
1
private
-
-
1
attr_reader :commit, :analyzers
-
-
20
def report = analyzers.reduce("") { |report, analyzer| report + Style.new(analyzer) }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Git
-
1
module Lint
-
1
module Reporters
-
# Reports issues related to an invalid line within the commit body.
-
1
class Line
-
1
DEFAULT_INDENT = " "
-
-
1
def initialize data = Core::EMPTY_HASH
-
5
@data = data
-
end
-
-
1
def to_s
-
5
then: 1
else: 4
content.include?("\n") ? Lines::Paragraph.new(data).to_s : Lines::Sentence.new(data).to_s
-
end
-
-
1
alias to_str to_s
-
-
1
private
-
-
1
attr_reader :data
-
-
1
def content = data.fetch(__method__)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Git
-
1
module Lint
-
1
module Reporters
-
1
module Lines
-
# Reports paragraph details.
-
1
class Paragraph
-
1
def initialize data = Core::EMPTY_HASH
-
3
@data = data
-
end
-
-
1
def to_s
-
3
%(#{label}"#{paragraph}"\n)
-
end
-
-
1
alias to_str to_s
-
-
1
private
-
-
1
attr_reader :data
-
-
1
def label = "#{Line::DEFAULT_INDENT}Line #{number}: "
-
-
1
def paragraph = formatted_lines.join("\n")
-
-
1
def formatted_lines
-
3
content.split("\n").map.with_index do |line, index|
-
9
then: 3
else: 6
index.zero? ? line : "#{indent}#{line}"
-
end
-
end
-
-
7
def indent = " " * (label.length + 1)
-
-
1
def number = data.fetch(:number)
-
-
1
def content = data.fetch(:content)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Git
-
1
module Lint
-
1
module Reporters
-
1
module Lines
-
# Reports sentence details.
-
1
class Sentence
-
1
def initialize data = Core::EMPTY_HASH
-
6
@data = data
-
end
-
-
1
def to_s = %(#{Line::DEFAULT_INDENT}Line #{number}: "#{content}"\n)
-
-
1
alias to_str to_s
-
-
1
private
-
-
1
attr_reader :data
-
-
1
def number = data.fetch(:number)
-
-
1
def content = data.fetch(:content)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Reporters
-
# Reports issues related to a single style.
-
1
class Style
-
1
include Dependencies[:color]
-
-
1
def initialize(analyzer, **)
-
24
super(**)
-
24
@analyzer = analyzer
-
24
@issue = analyzer.issue
-
end
-
-
1
def to_s = color[message, style]
-
-
1
alias to_str to_s
-
-
1
private
-
-
1
attr_reader :analyzer, :issue
-
-
1
def message
-
24
" #{analyzer.class.label}#{severity_suffix}. " \
-
"#{issue.fetch :hint}\n" \
-
"#{affected_lines}"
-
end
-
-
1
def severity_suffix
-
24
when: 6
case analyzer.severity
-
6
when: 17
when "warn" then " Warning"
-
17
else: 1
when "error" then " Error"
-
1
else ""
-
end
-
end
-
-
1
def style
-
24
when: 6
case analyzer.severity
-
6
when: 17
when "warn" then :yellow
-
17
else: 1
when "error" then :red
-
1
else :white
-
end
-
end
-
-
1
def affected_lines
-
26
issue.fetch(:lines, []).reduce("") { |lines, line| lines + Line.new(line) }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Git
-
1
module Lint
-
1
module Validators
-
# Validates the capitalizationn of text.
-
1
class Capitalization
-
1
PATTERN = /\A[[:upper:]].*\Z/
-
-
1
def initialize delimiter: Name::DELIMITER, pattern: PATTERN
-
50
@delimiter = delimiter
-
50
@pattern = pattern
-
end
-
-
261
def call(content) = String(content).split(delimiter).all? { |name| name.match? pattern }
-
-
1
private
-
-
1
attr_reader :delimiter, :pattern
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "uri"
-
-
1
module Git
-
1
module Lint
-
1
module Validators
-
# Validates the format of email addresses.
-
1
class Email
-
1
def initialize pattern: URI::MailTo::EMAIL_REGEXP
-
40
@pattern = pattern
-
end
-
-
1
def call(content) = String(content).match? pattern
-
-
1
private
-
-
1
attr_reader :pattern
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/string"
-
-
1
module Git
-
1
module Lint
-
1
module Validators
-
# Validates the format of names.
-
1
class Name
-
1
using Refinements::String
-
-
1
DELIMITER = /\s{1}/
-
1
MINIMUM = 2
-
-
1
def initialize delimiter: DELIMITER
-
50
@delimiter = delimiter
-
end
-
-
1
def call content, minimum: MINIMUM
-
141
parts = String(content).split delimiter
-
397
parts.size >= minimum && parts.all? { |name| !name.blank? }
-
end
-
-
1
private
-
-
1
attr_reader :delimiter
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
-
1
module Git
-
1
module Lint
-
1
module Validators
-
# Validates content has no repeated words.
-
1
class RepeatedWord
-
1
PATTERNS = {
-
word: /
-
\w+(?=\s) # Match word with trailing space.
-
| # Or.
-
(?<=\s)\w+(?=\s) # Match word between two spaces.
-
| # Or.
-
(?<=\s)\w+ # Match word with leading space.
-
/x,
-
exclude: /
-
( # Conditional start.
-
`.+` # Code blocks.
-
| # Or.
-
\d+\. # Digits followed by periods.
-
) # Conditional end.
-
/x
-
}.freeze
-
-
1
def initialize patterns: PATTERNS
-
46
@patterns = patterns
-
end
-
-
131
then: 128
else: 2
def call(content) = content ? scan(content) : Core::EMPTY_ARRAY
-
-
1
private
-
-
1
attr_reader :patterns
-
-
1
def scan content
-
128
parse(content).each_cons(2).with_object [] do |(current, future), repeats|
-
289
then: 17
else: 272
repeats.append future if current.casecmp(future).zero?
-
end
-
end
-
-
1
def parse(content) = content.gsub(exclude_pattern, "").scan word_pattern
-
-
1
def word_pattern = patterns.fetch :word
-
-
1
def exclude_pattern = patterns.fetch :exclude
-
end
-
end
-
end
-
end