├── .circleci └── config.yml ├── .config └── rubocop │ └── config.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── git_lint.yml ├── .gitignore ├── .reek.yml ├── .ruby-version ├── CITATION.cff ├── Gemfile ├── LICENSE.adoc ├── README.adoc ├── Rakefile ├── bin ├── console ├── rake ├── rspec ├── rubocop └── setup ├── exe └── git-lint ├── git-lint.gemspec ├── lib └── git │ ├── lint.rb │ └── lint │ ├── analyzer.rb │ ├── analyzers │ ├── abstract.rb │ ├── commit_author_capitalization.rb │ ├── commit_author_email.rb │ ├── commit_author_name.rb │ ├── commit_body_bullet_capitalization.rb │ ├── commit_body_bullet_delimiter.rb │ ├── commit_body_bullet_only.rb │ ├── commit_body_leading_line.rb │ ├── commit_body_line_length.rb │ ├── commit_body_paragraph_capitalization.rb │ ├── commit_body_phrase.rb │ ├── commit_body_presence.rb │ ├── commit_body_tracker_shorthand.rb │ ├── commit_body_word_repeat.rb │ ├── commit_signature.rb │ ├── commit_subject_length.rb │ ├── commit_subject_prefix.rb │ ├── commit_subject_suffix.rb │ ├── commit_subject_word_repeat.rb │ ├── commit_trailer_collaborator_capitalization.rb │ ├── commit_trailer_collaborator_email.rb │ ├── commit_trailer_collaborator_key.rb │ ├── commit_trailer_collaborator_name.rb │ ├── commit_trailer_duplicate.rb │ ├── commit_trailer_format_key.rb │ ├── commit_trailer_format_value.rb │ ├── commit_trailer_issue_key.rb │ ├── commit_trailer_issue_value.rb │ ├── commit_trailer_milestone_key.rb │ ├── commit_trailer_milestone_value.rb │ ├── commit_trailer_order.rb │ ├── commit_trailer_reviewer_key.rb │ ├── commit_trailer_reviewer_value.rb │ ├── commit_trailer_signer_capitalization.rb │ ├── commit_trailer_signer_email.rb │ ├── commit_trailer_signer_key.rb │ ├── commit_trailer_signer_name.rb │ ├── commit_trailer_tracker_key.rb │ └── commit_trailer_tracker_value.rb │ ├── cli │ ├── actions │ │ ├── analyze │ │ │ ├── branch.rb │ │ │ └── commit.rb │ │ └── hook.rb │ └── shell.rb │ ├── collector.rb │ ├── commits │ ├── hosts │ │ ├── circle_ci.rb │ │ ├── git_hub_action.rb │ │ ├── local.rb │ │ └── netlify_ci.rb │ └── loader.rb │ ├── configuration │ ├── contract.rb │ ├── defaults.yml │ ├── model.rb │ └── trailer.rb │ ├── container.rb │ ├── dependencies.rb │ ├── errors │ ├── base.rb │ ├── severity.rb │ └── sha.rb │ ├── kit │ └── filter_list.rb │ ├── rake │ └── register.rb │ ├── reporters │ ├── branch.rb │ ├── commit.rb │ ├── line.rb │ ├── lines │ │ ├── paragraph.rb │ │ └── sentence.rb │ └── style.rb │ └── validators │ ├── capitalization.rb │ ├── email.rb │ ├── name.rb │ └── repeated_word.rb └── spec ├── lib └── git │ ├── lint │ ├── analyzer_spec.rb │ ├── analyzers │ │ ├── abstract_spec.rb │ │ ├── commit_author_capitalization_spec.rb │ │ ├── commit_author_email_spec.rb │ │ ├── commit_author_name_spec.rb │ │ ├── commit_body_bullet_capitalization_spec.rb │ │ ├── commit_body_bullet_delimiter_spec.rb │ │ ├── commit_body_bullet_only_spec.rb │ │ ├── commit_body_leading_line_spec.rb │ │ ├── commit_body_line_length_spec.rb │ │ ├── commit_body_paragraph_capitalization_spec.rb │ │ ├── commit_body_phrase_spec.rb │ │ ├── commit_body_presence_spec.rb │ │ ├── commit_body_tracker_shorthand_spec.rb │ │ ├── commit_body_word_repeat_spec.rb │ │ ├── commit_signature_spec.rb │ │ ├── commit_subject_length_spec.rb │ │ ├── commit_subject_prefix_spec.rb │ │ ├── commit_subject_suffix_spec.rb │ │ ├── commit_subject_word_repeat_spec.rb │ │ ├── commit_trailer_collaborator_capitalization_spec.rb │ │ ├── commit_trailer_collaborator_email_spec.rb │ │ ├── commit_trailer_collaborator_key_spec.rb │ │ ├── commit_trailer_collaborator_name_spec.rb │ │ ├── commit_trailer_duplicate_spec.rb │ │ ├── commit_trailer_format_key_spec.rb │ │ ├── commit_trailer_format_value_spec.rb │ │ ├── commit_trailer_issue_key_spec.rb │ │ ├── commit_trailer_issue_value_spec.rb │ │ ├── commit_trailer_milestone_key_spec.rb │ │ ├── commit_trailer_milestone_value_spec.rb │ │ ├── commit_trailer_order_spec.rb │ │ ├── commit_trailer_reviewer_key_spec.rb │ │ ├── commit_trailer_reviewer_value_spec.rb │ │ ├── commit_trailer_signer_capitalization_spec.rb │ │ ├── commit_trailer_signer_email_spec.rb │ │ ├── commit_trailer_signer_key_spec.rb │ │ ├── commit_trailer_signer_name_spec.rb │ │ ├── commit_trailer_tracker_key_spec.rb │ │ └── commit_trailer_tracker_value_spec.rb │ ├── cli │ │ ├── actions │ │ │ ├── analyze │ │ │ │ ├── branch_spec.rb │ │ │ │ └── commit_spec.rb │ │ │ └── hook_spec.rb │ │ └── shell_spec.rb │ ├── collector_spec.rb │ ├── commits │ │ ├── hosts │ │ │ ├── circle_ci_spec.rb │ │ │ ├── git_hub_action_spec.rb │ │ │ ├── local_spec.rb │ │ │ └── netlify_ci_spec.rb │ │ └── loader_spec.rb │ ├── errors │ │ ├── base_spec.rb │ │ ├── severity_spec.rb │ │ └── sha_spec.rb │ ├── kit │ │ └── filter_list_spec.rb │ ├── rake │ │ └── register_spec.rb │ ├── reporters │ │ ├── branch_spec.rb │ │ ├── commit_spec.rb │ │ ├── line_spec.rb │ │ ├── lines │ │ │ ├── paragraph_spec.rb │ │ │ └── sentence_spec.rb │ │ └── style_spec.rb │ └── validators │ │ ├── capitalization_spec.rb │ │ ├── email_spec.rb │ │ ├── name_spec.rb │ │ └── repeated_word_spec.rb │ └── lint_spec.rb ├── spec_helper.rb └── support ├── fixtures ├── commit-invalid.txt ├── commit-scissors.txt ├── commit-valid.txt └── invalid_phrases.txt └── shared_contexts ├── application_dependencies.rb └── host_dependencies.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | working_directory: ~/project 5 | docker: 6 | - image: bkuhlmann/alpine-ruby:latest 7 | steps: 8 | - checkout 9 | 10 | - restore_cache: 11 | name: Gems Restore 12 | keys: 13 | - gem-cache-{{.Branch}}-{{checksum "Gemfile"}}-{{checksum "git-lint.gemspec"}} 14 | - gem-cache- 15 | 16 | - run: 17 | name: Gems Install 18 | command: | 19 | gem update --system 20 | bundle config set path "vendor/bundle" 21 | bundle install 22 | 23 | - save_cache: 24 | name: Gems Store 25 | key: gem-cache-{{.Branch}}-{{checksum "Gemfile"}}-{{checksum "git-lint.gemspec"}} 26 | paths: 27 | - vendor/bundle 28 | 29 | - run: 30 | name: Rake 31 | command: bundle exec rake 32 | 33 | - store_artifacts: 34 | name: SimpleCov Report 35 | path: ~/project/coverage 36 | destination: coverage 37 | -------------------------------------------------------------------------------- /.config/rubocop/config.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | caliber: config/all.yml 3 | 4 | Metrics/CollectionLiteralLength: 5 | Exclude: 6 | - "lib/git/lint/analyzer.rb" 7 | 8 | Metrics/MethodLength: 9 | Exclude: 10 | - "lib/git/lint/cli/shell.rb" 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bkuhlmann] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why 2 | 3 | 4 | ## How 5 | 6 | 7 | ## Notes 8 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | 4 | ## Screenshots/Screencasts 5 | 6 | 7 | ## Details 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/git_lint.yml: -------------------------------------------------------------------------------- 1 | name: Git Lint 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | container: 9 | image: ruby:latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: '0' 15 | ref: ${{github.head_ref}} 16 | - name: Install 17 | run: gem install git-lint 18 | - name: Analyze 19 | run: git-lint analyze --branch 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg 5 | tmp 6 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - tmp 3 | - vendor 4 | 5 | detectors: 6 | TooManyStatements: 7 | exclude: 8 | - "Git::Lint::CLI::Shell#cli" 9 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.4 2 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: Please use the following metadata when citing this project in your work. 3 | title: Git Lint 4 | abstract: A command line interface for linting Git commits. 5 | version: 9.3.1 6 | license: Hippocratic-2.1 7 | date-released: 2025-05-21 8 | authors: 9 | - family-names: Kuhlmann 10 | given-names: Brooke 11 | affiliation: Alchemists 12 | orcid: https://orcid.org/0000-0002-5810-6268 13 | keywords: 14 | - ruby 15 | - command line interface 16 | - git 17 | - linter 18 | - code quality 19 | - consistency 20 | repository-code: https://github.com/bkuhlmann/git-lint 21 | repository-artifact: https://rubygems.org/gems/git-lint 22 | url: https://alchemists.io/projects/git-lint 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ruby file: ".ruby-version" 4 | 5 | source "https://rubygems.org" 6 | 7 | gemspec 8 | 9 | group :quality do 10 | gem "caliber", "~> 0.79" 11 | gem "reek", "~> 6.5", require: false 12 | gem "simplecov", "~> 0.22", require: false 13 | end 14 | 15 | group :development do 16 | gem "rake", "~> 13.2" 17 | end 18 | 19 | group :test do 20 | gem "rspec", "~> 3.13" 21 | end 22 | 23 | group :tools do 24 | gem "amazing_print", "~> 1.8" 25 | gem "debug", "~> 1.10" 26 | gem "irb-kit", "~> 1.1" 27 | gem "repl_type_completor", "~> 0.1" 28 | end 29 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "git/lint/rake/register" 5 | require "reek/rake/task" 6 | require "rspec/core/rake_task" 7 | require "rubocop/rake_task" 8 | 9 | Git::Lint::Rake::Register.call 10 | Reek::Rake::Task.new 11 | RSpec::Core::RakeTask.new { |task| task.verbose = false } 12 | RuboCop::RakeTask.new 13 | 14 | desc "Run code quality checks" 15 | task quality: %i[git_lint reek rubocop] 16 | 17 | task default: %i[quality spec] 18 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | Bundler.require :tools 6 | 7 | require "git/lint" 8 | require "irb" 9 | 10 | IRB.start __FILE__ 11 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rake", "rake" 7 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rspec-core", "rspec" 7 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rubocop", "rubocop" 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "debug" 5 | require "fileutils" 6 | require "pathname" 7 | 8 | APP_ROOT = Pathname(__dir__).join("..").expand_path 9 | 10 | Runner = lambda do |*arguments, kernel: Kernel| 11 | kernel.system(*arguments) || kernel.abort("\nERROR: Command #{arguments.inspect} failed.") 12 | end 13 | 14 | FileUtils.chdir APP_ROOT do 15 | puts "Installing dependencies..." 16 | Runner.call "bundle install" 17 | end 18 | -------------------------------------------------------------------------------- /exe/git-lint: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "git/lint" 5 | 6 | Git::Lint::CLI::Shell.new.call 7 | -------------------------------------------------------------------------------- /git-lint.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "git-lint" 5 | spec.version = "9.3.1" 6 | spec.authors = ["Brooke Kuhlmann"] 7 | spec.email = ["brooke@alchemists.io"] 8 | spec.homepage = "https://alchemists.io/projects/git-lint" 9 | spec.summary = "A command line interface for linting Git commits." 10 | spec.license = "Hippocratic-2.1" 11 | 12 | spec.metadata = { 13 | "bug_tracker_uri" => "https://github.com/bkuhlmann/git-lint/issues", 14 | "changelog_uri" => "https://alchemists.io/projects/git-lint/versions", 15 | "homepage_uri" => "https://alchemists.io/projects/git-lint", 16 | "funding_uri" => "https://github.com/sponsors/bkuhlmann", 17 | "label" => "Git Lint", 18 | "rubygems_mfa_required" => "true", 19 | "source_code_uri" => "https://github.com/bkuhlmann/git-lint" 20 | } 21 | 22 | spec.signing_key = Gem.default_key_path 23 | spec.cert_chain = [Gem.default_cert_path] 24 | 25 | spec.required_ruby_version = "~> 3.4" 26 | spec.add_dependency "cogger", "~> 1.0" 27 | spec.add_dependency "containable", "~> 1.1" 28 | spec.add_dependency "core", "~> 2.0" 29 | spec.add_dependency "dry-monads", "~> 1.8" 30 | spec.add_dependency "dry-schema", "~> 1.13" 31 | spec.add_dependency "etcher", "~> 3.0" 32 | spec.add_dependency "gitt", "~> 4.1" 33 | spec.add_dependency "infusible", "~> 4.0" 34 | spec.add_dependency "refinements", "~> 13.0" 35 | spec.add_dependency "runcom", "~> 12.0" 36 | spec.add_dependency "sod", "~> 1.0" 37 | spec.add_dependency "spek", "~> 4.0" 38 | spec.add_dependency "tone", "~> 2.0" 39 | spec.add_dependency "zeitwerk", "~> 2.7" 40 | 41 | spec.bindir = "exe" 42 | spec.executables << "git-lint" 43 | spec.extra_rdoc_files = Dir["README*", "LICENSE*"] 44 | spec.files = Dir["*.gemspec", "lib/**/*"] 45 | end 46 | -------------------------------------------------------------------------------- /lib/git/lint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "zeitwerk" 4 | 5 | Zeitwerk::Loader.new.then do |loader| 6 | loader.inflector.inflect "cli" => "CLI", 7 | "sha" => "SHA", 8 | "circle_ci" => "CircleCI", 9 | "netlify_ci" => "NetlifyCI", 10 | "travis_ci" => "TravisCI" 11 | loader.tag = "git-lint" 12 | loader.ignore "#{__dir__}/lint/rake" 13 | loader.push_dir "#{__dir__}/.." 14 | loader.setup 15 | end 16 | 17 | module Git 18 | # Main namespace. 19 | module Lint 20 | def self.loader registry = Zeitwerk::Registry 21 | @loader ||= registry.loaders.each.find { |loader| loader.tag == "git-lint" } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/git/lint/analyzer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | # Runs all analyzers. 6 | class Analyzer 7 | include Dependencies[:settings] 8 | 9 | ANALYZERS = [ 10 | Analyzers::CommitAuthorCapitalization, 11 | Analyzers::CommitAuthorEmail, 12 | Analyzers::CommitAuthorName, 13 | Analyzers::CommitBodyBulletCapitalization, 14 | Analyzers::CommitBodyBulletDelimiter, 15 | Analyzers::CommitBodyBulletOnly, 16 | Analyzers::CommitBodyLeadingLine, 17 | Analyzers::CommitBodyLineLength, 18 | Analyzers::CommitBodyParagraphCapitalization, 19 | Analyzers::CommitBodyPhrase, 20 | Analyzers::CommitBodyPresence, 21 | Analyzers::CommitBodyTrackerShorthand, 22 | Analyzers::CommitBodyWordRepeat, 23 | Analyzers::CommitSignature, 24 | Analyzers::CommitSubjectLength, 25 | Analyzers::CommitSubjectPrefix, 26 | Analyzers::CommitSubjectSuffix, 27 | Analyzers::CommitSubjectWordRepeat, 28 | Analyzers::CommitTrailerCollaboratorCapitalization, 29 | Analyzers::CommitTrailerCollaboratorEmail, 30 | Analyzers::CommitTrailerCollaboratorKey, 31 | Analyzers::CommitTrailerCollaboratorName, 32 | Analyzers::CommitTrailerDuplicate, 33 | Analyzers::CommitTrailerFormatKey, 34 | Analyzers::CommitTrailerFormatValue, 35 | Analyzers::CommitTrailerIssueKey, 36 | Analyzers::CommitTrailerIssueValue, 37 | Analyzers::CommitTrailerMilestoneKey, 38 | Analyzers::CommitTrailerMilestoneValue, 39 | Analyzers::CommitTrailerOrder, 40 | Analyzers::CommitTrailerReviewerKey, 41 | Analyzers::CommitTrailerReviewerValue, 42 | Analyzers::CommitTrailerSignerCapitalization, 43 | Analyzers::CommitTrailerSignerEmail, 44 | Analyzers::CommitTrailerSignerKey, 45 | Analyzers::CommitTrailerSignerName, 46 | Analyzers::CommitTrailerTrackerKey, 47 | Analyzers::CommitTrailerTrackerValue 48 | ].freeze 49 | 50 | # rubocop:disable Metrics/ParameterLists 51 | def initialize( 52 | analyzers: ANALYZERS, 53 | collector: Collector.new, 54 | reporter: Reporters::Branch, 55 | ** 56 | ) 57 | super(**) 58 | @analyzers = analyzers 59 | @collector = collector 60 | @reporter = reporter 61 | end 62 | # rubocop:enable Metrics/ParameterLists 63 | 64 | def call commits: Commits::Loader.new.call 65 | process commits 66 | a_reporter = reporter.new(collector:) 67 | block_given? ? yield(collector, a_reporter) : [collector, a_reporter] 68 | end 69 | 70 | private 71 | 72 | attr_reader :analyzers, :collector, :reporter 73 | 74 | def process commits 75 | collector.clear 76 | commits.value_or([]).map { |commit| analyze commit } 77 | end 78 | 79 | def analyze(commit) = enabled.map { |id| collector.add load_analyzer(commit, id) } 80 | 81 | # :reek:FeatureEnvy 82 | def enabled 83 | settings.to_h 84 | .select { |key, value| key.end_with?("enabled") && value == true } 85 | .keys 86 | .map { |key| key.to_s.sub("commits_", "commit_").delete_suffix! "_enabled" } 87 | end 88 | 89 | def load_analyzer commit, id 90 | analyzers.find { |analyzer| analyzer.id == id } 91 | .then { |analyzer| analyzer.new commit } 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/abstract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core" 4 | require "refinements/string" 5 | 6 | module Git 7 | module Lint 8 | module Analyzers 9 | # An abstract class which provides basic functionality for all analyzers to inherit from. 10 | class Abstract 11 | include Dependencies[:settings, :environment] 12 | 13 | using Refinements::String 14 | 15 | LEVELS = %w[warn error].freeze 16 | BODY_OFFSET = 3 17 | 18 | def self.id = to_s.delete_prefix!("Git::Lint::Analyzers").snakecase 19 | 20 | def self.label = to_s.delete_prefix("Git::Lint::Analyzers").titleize 21 | 22 | def self.build_issue_line(index, line) = {number: index + BODY_OFFSET, content: line} 23 | 24 | attr_reader :commit 25 | 26 | def initialize(commit, **) 27 | super(**) 28 | @commit = commit 29 | @filter_list = load_filter_list 30 | end 31 | 32 | def severity 33 | settings.public_send("#{self.class.id}_severity".sub("commit_", "commits_")) 34 | .tap { |level| fail Errors::Severity, level unless LEVELS.include? level } 35 | end 36 | 37 | def valid? = fail NoMethodError, "The `##{__method__}` method must be implemented." 38 | 39 | def invalid? = !valid? 40 | 41 | def warning? = invalid? && severity == "warn" 42 | 43 | def error? = invalid? && severity == "error" 44 | 45 | def issue = fail NoMethodError, "The `##{__method__}` method must be implemented." 46 | 47 | protected 48 | 49 | attr_reader :filter_list 50 | 51 | def load_filter_list = Core::EMPTY_ARRAY 52 | 53 | def affected_commit_body_lines 54 | commit.body_lines.each.with_object([]).with_index do |(line, lines), index| 55 | lines << self.class.build_issue_line(index, line) if invalid_line? line 56 | end 57 | end 58 | 59 | def affected_commit_trailers 60 | commit.trailers 61 | .each 62 | .with_object([]) 63 | .with_index(commit.body_lines.size) do |(trailer, trailers), index| 64 | next unless invalid_line? trailer 65 | 66 | trailers << self.class.build_issue_line(index, trailer.to_s) 67 | end 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_author_capitalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes author name for proper capitalization. 7 | class CommitAuthorCapitalization < Abstract 8 | include Dependencies[validator: "validators.capitalization"] 9 | 10 | def valid? = validator.call commit.author_name 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | {hint: %(Capitalize each part of name: "#{commit.author_name}".)} 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_author_email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes author email address for proper format. 7 | class CommitAuthorEmail < Abstract 8 | include Dependencies[validator: "validators.email"] 9 | 10 | def valid? = validator.call commit.author_email 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | {hint: %(Use "@." instead of "#{commit.author_email}".)} 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_author_name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes author name for minimum parts of name. 7 | class CommitAuthorName < Abstract 8 | include Dependencies[validator: "validators.name"] 9 | 10 | def valid? = validator.call(commit.author_name, minimum:) 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | {hint: "Author name must consist of #{minimum} parts (minimum)."} 16 | end 17 | 18 | private 19 | 20 | def minimum = settings.commits_author_name_minimum 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_bullet_capitalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit body for proper capitalization of bullet sentences. 7 | class CommitBodyBulletCapitalization < Abstract 8 | def valid? = lowercased_bullets.empty? 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: "Capitalize first word.", 15 | lines: affected_commit_body_lines 16 | } 17 | end 18 | 19 | protected 20 | 21 | def load_filter_list 22 | Kit::FilterList.new settings.commits_body_bullet_capitalization_includes 23 | end 24 | 25 | def invalid_line? line 26 | line.sub(/link:.+(?=\[)/, "").match?(/\A\s*#{Regexp.union filter_list}\s[[:lower:]]+/) 27 | end 28 | 29 | private 30 | 31 | def lowercased_bullets = commit.body_lines.select { |line| invalid_line? line } 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_bullet_delimiter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit body delimiter usage. 7 | class CommitBodyBulletDelimiter < Abstract 8 | def valid? = commit.body_lines.none? { |line| invalid_line? line } 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: "Use space after bullet.", 15 | lines: affected_commit_body_lines 16 | } 17 | end 18 | 19 | protected 20 | 21 | def load_filter_list 22 | Kit::FilterList.new settings.commits_body_bullet_delimiter_includes 23 | end 24 | 25 | def invalid_line?(line) = line.match?(/\A\s*#{pattern}(?!(#{pattern}|\s)).+\Z/) 26 | 27 | def pattern = Regexp.union filter_list 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_bullet_only.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit bodies with only a single bullet point. 7 | class CommitBodyBulletOnly < Abstract 8 | def valid? = !affected_commit_body_lines.one? 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: "Use paragraph instead of single bullet.", 15 | lines: affected_commit_body_lines 16 | } 17 | end 18 | 19 | protected 20 | 21 | def load_filter_list 22 | Kit::FilterList.new settings.commits_body_bullet_only_includes 23 | end 24 | 25 | def invalid_line?(line) = line.match?(/\A#{Regexp.union filter_list}\s+/) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_leading_line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes leading line between commit subject and start of body. 7 | class CommitBodyLeadingLine < Abstract 8 | def valid? 9 | raw = commit.raw 10 | subject, body = raw.split "\n", 2 11 | 12 | return true if !String(subject).empty? && String(body).strip.empty? 13 | 14 | raw.match?(/\A.+(\n\n|\#).+/m) 15 | end 16 | 17 | def issue 18 | return {} if valid? 19 | 20 | {hint: "Use blank line between subject and body."} 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_line_length.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit body line length to prevent unnecessary horizontal scrolling. 7 | class CommitBodyLineLength < Abstract 8 | def valid? = commit.body_lines.all? { |line| !invalid_line? line } 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: "Use #{maximum} characters or less per line.", 15 | lines: affected_commit_body_lines 16 | } 17 | end 18 | 19 | protected 20 | 21 | def invalid_line?(line) = line.length > maximum 22 | 23 | private 24 | 25 | def maximum = settings.commits_body_line_length_maximum 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_paragraph_capitalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes proper capitalization of commit body paragraphs. 7 | class CommitBodyParagraphCapitalization < Abstract 8 | PATTERN = / 9 | \A # Search start. 10 | (?! # Negative lookahead start. 11 | (?: # Non-capture group start. 12 | audio # Ignore audio. 13 | | # Or. 14 | image # Ignore image. 15 | | # Or. 16 | video # Ignore video. 17 | ) # Non-capture group end. 18 | :: # Suffix. 19 | | # Or. 20 | link: # Ignore link. 21 | | # Or. 22 | xref: # Ignore xref. 23 | ) # Negative lookahead end. 24 | [[:lower:]] # Match lowercase letters. 25 | .+ # Match one or more characters. 26 | \Z # Search end. 27 | /mx 28 | 29 | def initialize(commit, pattern: PATTERN, **) 30 | super(commit, **) 31 | @pattern = pattern 32 | end 33 | 34 | def valid? = invalids.empty? 35 | 36 | def issue 37 | return {} if valid? 38 | 39 | { 40 | hint: "Capitalize first word.", 41 | lines: affected_lines 42 | } 43 | end 44 | 45 | private 46 | 47 | attr_reader :pattern 48 | 49 | def affected_lines 50 | invalids.each.with_object [] do |line, lines| 51 | lines << self.class.build_issue_line(commit.body_lines.index(line[/.+/]), line) 52 | end 53 | end 54 | 55 | def invalids 56 | @invalids ||= commit.body_paragraphs.grep pattern 57 | end 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_phrase.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes use of commit body phrases that are not informative. 7 | class CommitBodyPhrase < Abstract 8 | def valid? = commit.body_lines.all? { |line| !invalid_line? line } 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: %(Avoid: #{filter_list.to_usage}.), 15 | lines: affected_commit_body_lines 16 | } 17 | end 18 | 19 | protected 20 | 21 | def load_filter_list 22 | Kit::FilterList.new settings.commits_body_phrase_excludes 23 | end 24 | 25 | def invalid_line? line 26 | line.downcase.match? Regexp.new(Regexp.union(filter_list).source, Regexp::IGNORECASE) 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_presence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes presence of commit body. 7 | class CommitBodyPresence < Abstract 8 | using Refinements::String 9 | 10 | def valid? 11 | return true if commit.fixup? 12 | 13 | valid_lines = commit.body_lines.grep_v(/^\s*$/) 14 | valid_lines.size >= minimum 15 | end 16 | 17 | def minimum = settings.commits_body_presence_minimum 18 | 19 | def issue 20 | return {} if valid? 21 | 22 | {hint: %(Use minimum of #{"#{minimum} line".pluralize "s", minimum} (non-empty).)} 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_tracker_shorthand.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes body tracker shorthand usage. 7 | class CommitBodyTrackerShorthand < Abstract 8 | def valid? = commit.body_lines.none? { |line| invalid_line? line } 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: "Explain issue instead of using shorthand. Avoid: #{filter_list.to_usage}.", 15 | lines: affected_commit_body_lines 16 | } 17 | end 18 | 19 | protected 20 | 21 | def load_filter_list 22 | Kit::FilterList.new settings.commits_body_tracker_shorthand_excludes 23 | end 24 | 25 | def invalid_line?(line) = line.match?(/.*#{Regexp.union filter_list}.*/) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_body_word_repeat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit body for repeated words. 7 | class CommitBodyWordRepeat < Abstract 8 | include Dependencies[validator: "validators.repeated_word"] 9 | 10 | def valid? = commit.body_lines.all? { |line| !invalid_line? line } 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Avoid repeating these words: #{validator.call commit.body}.", 17 | lines: affected_commit_body_lines 18 | } 19 | end 20 | 21 | protected 22 | 23 | def invalid_line? line 24 | return false if line.start_with? "#" 25 | 26 | !validator.call(line).empty? 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_signature.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit signature validity. 7 | class CommitSignature < Abstract 8 | include Dependencies[sanitizer: "sanitizers.signature"] 9 | 10 | def valid? 11 | sanitizer.call(commit.signature).match?(/\A#{Regexp.union filter_list}\Z/) 12 | end 13 | 14 | def issue = valid? ? {} : {hint: %(Use: #{filter_list.to_usage "or"}.)} 15 | 16 | protected 17 | 18 | def load_filter_list 19 | Kit::FilterList.new settings.commits_signature_includes 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_subject_length.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit subject length is short and concise. 7 | class CommitSubjectLength < Abstract 8 | def valid? = commit.subject.sub(/(fixup!|squash!)\s{1}/, "").size <= maximum 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | {hint: "Use #{maximum} characters or less."} 14 | end 15 | 16 | private 17 | 18 | def maximum = settings.commits_subject_length_maximum 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_subject_prefix.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit subject uses standard prefix. 7 | class CommitSubjectPrefix < Abstract 8 | def valid? 9 | return true if locally_prefixed? 10 | return true if filter_list.empty? 11 | 12 | commit.subject.match?(/\A#{Regexp.union filter_list}/) 13 | end 14 | 15 | def issue 16 | return {} if valid? 17 | 18 | {hint: %(Use: #{filter_list.to_usage "or"}.)} 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | settings.commits_subject_prefix_includes 25 | .map { |prefix| "#{prefix}#{delimiter}" } 26 | .then { |list| Kit::FilterList.new list } 27 | end 28 | 29 | def locally_prefixed? = !ci? && commit.directive? 30 | 31 | def ci? = environment["CI"] == "true" 32 | 33 | def delimiter = settings.commits_subject_prefix_delimiter 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_subject_suffix.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit subject suffix for punctuation. 7 | class CommitSubjectSuffix < Abstract 8 | def valid? 9 | return true if filter_list.empty? 10 | 11 | !commit.subject.match?(/#{Regexp.union filter_list}\Z/) 12 | end 13 | 14 | def issue 15 | return {} if valid? 16 | 17 | {hint: %(Avoid: #{filter_list.to_usage}.)} 18 | end 19 | 20 | protected 21 | 22 | def load_filter_list 23 | Kit::FilterList.new settings.commits_subject_suffix_excludes 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_subject_word_repeat.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit subject for repeated words. 7 | class CommitSubjectWordRepeat < Abstract 8 | include Dependencies[validator: "validators.repeated_word"] 9 | 10 | def valid? = validator.call(commit.subject).empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | {hint: "Avoid repeating these words: #{validator.call commit.subject}."} 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_collaborator_capitalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer collaborator name capitalization. 7 | class CommitTrailerCollaboratorCapitalization < Abstract 8 | include Dependencies[ 9 | setting: "trailers.collaborator", 10 | parser: "parsers.person", 11 | validator: "validators.capitalization" 12 | ] 13 | 14 | def valid? = affected_commit_trailers.empty? 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: "Name must be capitalized.", 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | parser.call(trailer.value).then do |person| 29 | trailer.key.match?(setting.pattern) && !validator.call(person.name) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_collaborator_email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer collaborator email address format. 7 | class CommitTrailerCollaboratorEmail < Abstract 8 | include Dependencies[ 9 | setting: "trailers.collaborator", 10 | parser: "parsers.person", 11 | validator: "validators.email" 12 | ] 13 | 14 | def valid? = affected_commit_trailers.empty? 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: %(Email must follow name and use format: "".), 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | email = parser.call(trailer.value).email 29 | trailer.key.match?(setting.pattern) && !validator.call(email) 30 | end 31 | 32 | private 33 | 34 | attr_reader :parser, :validator 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_collaborator_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer collaborator key usage. 7 | class CommitTrailerCollaboratorKey < Abstract 8 | include Dependencies[setting: "trailers.collaborator"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list = Kit::FilterList.new setting.name 24 | 25 | def invalid_line? trailer 26 | trailer.key.then do |key| 27 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_collaborator_name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer collaborator name construction. 7 | class CommitTrailerCollaboratorName < Abstract 8 | include Dependencies[ 9 | setting: "trailers.collaborator", 10 | parser: "parsers.person", 11 | validator: "validators.name" 12 | ] 13 | 14 | def valid? = affected_commit_trailers.empty? 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: "Name must follow key and consist of #{minimum} parts (minimum).", 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | parser.call(trailer.value).then do |person| 29 | trailer.key.match?(setting.pattern) && !validator.call(person.name, minimum:) 30 | end 31 | end 32 | 33 | private 34 | 35 | def minimum = settings.commits_trailer_collaborator_name_minimum 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_duplicate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer duplicate. 7 | class CommitTrailerDuplicate < Abstract 8 | def valid? = affected_commit_trailers.empty? 9 | 10 | def issue 11 | return {} if valid? 12 | 13 | { 14 | hint: "Avoid duplicates.", 15 | lines: affected_commit_trailers 16 | } 17 | end 18 | 19 | protected 20 | 21 | def invalid_line?(trailer) = commit.trailers.tally[trailer] != 1 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_format_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer format key usage. 7 | class CommitTrailerFormatKey < Abstract 8 | include Dependencies[setting: "trailers.format"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list = Kit::FilterList.new setting.name 24 | 25 | def invalid_line? trailer 26 | trailer.key.then do |key| 27 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_format_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer format value. 7 | class CommitTrailerFormatValue < Abstract 8 | include Dependencies[setting: "trailers.format"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage "or"}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | Kit::FilterList.new settings.commits_trailer_format_value_includes 25 | end 26 | 27 | def invalid_line? trailer 28 | trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern) 29 | end 30 | 31 | def value_pattern = /\A#{Regexp.union filter_list}\Z/ 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_issue_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer issue key usage. 7 | class CommitTrailerIssueKey < Abstract 8 | include Dependencies[setting: "trailers.issue"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list = Kit::FilterList.new setting.name 24 | 25 | def invalid_line? trailer 26 | trailer.key.then do |key| 27 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_issue_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer issue value. 7 | class CommitTrailerIssueValue < Abstract 8 | include Dependencies[setting: "trailers.issue"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | Kit::FilterList.new settings.commits_trailer_issue_value_includes 25 | end 26 | 27 | def invalid_line? trailer 28 | trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern) 29 | end 30 | 31 | def value_pattern = /\A#{Regexp.union filter_list}\Z/ 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_milestone_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer milestone key usage. 7 | class CommitTrailerMilestoneKey < Abstract 8 | include Dependencies[setting: "trailers.milestone"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list = Kit::FilterList.new setting.name 24 | 25 | def invalid_line? trailer 26 | trailer.key.then do |key| 27 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_milestone_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer milestone value. 7 | class CommitTrailerMilestoneValue < Abstract 8 | include Dependencies[setting: "trailers.milestone"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use: #{filter_list.to_usage "or"}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | Kit::FilterList.new settings.commits_trailer_milestone_value_includes 25 | end 26 | 27 | def invalid_line? trailer 28 | trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern) 29 | end 30 | 31 | def value_pattern = /\A#{Regexp.union filter_list}\Z/ 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_order.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer order value. 7 | class CommitTrailerOrder < Abstract 8 | def initialize(...) 9 | super 10 | @original_order = commit.trailers.map(&:key) 11 | @sorted_order = original_order.sort 12 | end 13 | 14 | def valid? = original_order == sorted_order 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: "Ensure keys are alphabetically sorted.", 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | key = trailer.key 29 | original_order.index(key) != sorted_order.index(key) 30 | end 31 | 32 | private 33 | 34 | attr_reader :original_order, :sorted_order 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_reviewer_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer reviwer key usage. 7 | class CommitTrailerReviewerKey < Abstract 8 | include Dependencies[setting: "trailers.reviewer"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list = Kit::FilterList.new setting.name 24 | 25 | def invalid_line? trailer 26 | trailer.key.then do |key| 27 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_reviewer_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer reviewer value. 7 | class CommitTrailerReviewerValue < Abstract 8 | include Dependencies[setting: "trailers.reviewer"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use: #{filter_list.to_usage "or"}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | Kit::FilterList.new settings.commits_trailer_reviewer_value_includes 25 | end 26 | 27 | def invalid_line? trailer 28 | trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern) 29 | end 30 | 31 | def value_pattern = /\A#{Regexp.union filter_list}\Z/ 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_signer_capitalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer signer name capitalization. 7 | class CommitTrailerSignerCapitalization < Abstract 8 | include Dependencies[ 9 | setting: "trailers.signer", 10 | parser: "parsers.person", 11 | validator: "validators.capitalization" 12 | ] 13 | 14 | def valid? = affected_commit_trailers.empty? 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: "Name must be capitalized.", 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | parser.call(trailer.value).then do |person| 29 | trailer.key.match?(setting.pattern) && !validator.call(person.name) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_signer_email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer signer email address format. 7 | class CommitTrailerSignerEmail < Abstract 8 | include Dependencies[ 9 | setting: "trailers.signer", 10 | parser: "parsers.person", 11 | validator: "validators.email" 12 | ] 13 | 14 | def valid? = affected_commit_trailers.empty? 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: %(Email must follow name and use format: "".), 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | email = parser.call(trailer.value).email 29 | trailer.key.match?(setting.pattern) && !validator.call(email) 30 | end 31 | 32 | private 33 | 34 | attr_reader :parser, :validator 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_signer_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer signer key usage. 7 | class CommitTrailerSignerKey < Abstract 8 | include Dependencies[setting: "trailers.signer"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | Kit::FilterList.new setting.name 25 | end 26 | 27 | def invalid_line? trailer 28 | trailer.key.then do |key| 29 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_signer_name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer signer name construction. 7 | class CommitTrailerSignerName < Abstract 8 | include Dependencies[ 9 | setting: "trailers.signer", 10 | parser: "parsers.person", 11 | validator: "validators.name" 12 | ] 13 | 14 | def valid? = affected_commit_trailers.empty? 15 | 16 | def issue 17 | return {} if valid? 18 | 19 | { 20 | hint: "Name must follow key and consist of #{minimum} parts (minimum).", 21 | lines: affected_commit_trailers 22 | } 23 | end 24 | 25 | protected 26 | 27 | def invalid_line? trailer 28 | parser.call(trailer.value).then do |person| 29 | trailer.key.match?(setting.pattern) && !validator.call(person.name, minimum:) 30 | end 31 | end 32 | 33 | private 34 | 35 | def minimum = settings.commits_trailer_signer_name_minimum 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_tracker_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer tracker key usage. 7 | class CommitTrailerTrackerKey < Abstract 8 | include Dependencies[setting: "trailers.tracker"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list = Kit::FilterList.new setting.name 24 | 25 | def invalid_line? trailer 26 | trailer.key.then do |key| 27 | key.match?(setting.pattern) && !key.match?(/\A#{Regexp.union filter_list}\Z/) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/git/lint/analyzers/commit_trailer_tracker_value.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Analyzers 6 | # Analyzes commit trailer tracker value. 7 | class CommitTrailerTrackerValue < Abstract 8 | include Dependencies[setting: "trailers.tracker"] 9 | 10 | def valid? = affected_commit_trailers.empty? 11 | 12 | def issue 13 | return {} if valid? 14 | 15 | { 16 | hint: "Use format: #{filter_list.to_usage}.", 17 | lines: affected_commit_trailers 18 | } 19 | end 20 | 21 | protected 22 | 23 | def load_filter_list 24 | Kit::FilterList.new settings.commits_trailer_tracker_value_includes 25 | end 26 | 27 | def invalid_line? trailer 28 | trailer.key.match?(setting.pattern) && !trailer.value.match?(value_pattern) 29 | end 30 | 31 | def value_pattern = /\A#{Regexp.union filter_list}\Z/ 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git/lint/cli/actions/analyze/branch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | 5 | module Git 6 | module Lint 7 | module CLI 8 | module Actions 9 | module Analyze 10 | # Handles analyze action for branch. 11 | class Branch < Sod::Action 12 | include Dependencies[:logger, :kernel, :io] 13 | 14 | description "Analyze current branch." 15 | 16 | on %w[-b --branch] 17 | 18 | def initialize(analyzer: Analyzer.new, **) 19 | super(**) 20 | @analyzer = analyzer 21 | end 22 | 23 | def call(*) 24 | parse 25 | rescue Errors::Base => error 26 | logger.error { error.message } 27 | kernel.abort 28 | end 29 | 30 | private 31 | 32 | attr_reader :analyzer 33 | 34 | def parse 35 | analyzer.call do |collector, reporter| 36 | io.puts reporter 37 | kernel.abort if collector.errors? 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/git/lint/cli/actions/analyze/commit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | 5 | module Git 6 | module Lint 7 | module CLI 8 | module Actions 9 | module Analyze 10 | # Handles analyze action for single commit SHA 11 | class Commit < Sod::Action 12 | include Dependencies[:git, :logger, :kernel, :io] 13 | 14 | description "Analyze specific commits." 15 | 16 | on %w[-c --commit], argument: "a,b,c" 17 | 18 | def initialize(analyzer: Analyzer.new, **) 19 | super(**) 20 | @analyzer = analyzer 21 | end 22 | 23 | def call *arguments 24 | process arguments.unshift "-1" 25 | rescue Errors::Base => error 26 | logger.error { error.message } 27 | kernel.abort 28 | end 29 | 30 | private 31 | 32 | attr_reader :analyzer 33 | 34 | def process arguments 35 | analyzer.call commits: git.commits(*arguments) do |collector, reporter| 36 | io.puts reporter 37 | kernel.abort if collector.errors? 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/git/lint/cli/actions/hook.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | require "sod/types/pathname" 5 | 6 | module Git 7 | module Lint 8 | module CLI 9 | module Actions 10 | # Handles unsaved Git commit action. 11 | class Hook < Sod::Action 12 | include Dependencies[:git, :logger, :kernel, :io] 13 | 14 | description "Hook for analyzing unsaved commits." 15 | 16 | on "--hook", argument: "PATH", type: Pathname 17 | 18 | def initialize(analyzer: Analyzer.new, **) 19 | super(**) 20 | @analyzer = analyzer 21 | end 22 | 23 | def call path 24 | analyzer.call commits: commits(path) do |collector, reporter| 25 | io.puts reporter 26 | kernel.abort if collector.errors? 27 | end 28 | end 29 | 30 | private 31 | 32 | attr_reader :analyzer 33 | 34 | def commits(path) = git.uncommitted(path).fmap { |commit| [commit] } 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/git/lint/cli/shell.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "sod" 4 | 5 | module Git 6 | module Lint 7 | module CLI 8 | # The main Command Line Interface (CLI) object. 9 | class Shell 10 | include Dependencies[:defaults_path, :xdg_config, :specification] 11 | 12 | def initialize(context: Sod::Context, dsl: Sod, **) 13 | super(**) 14 | @context = context 15 | @dsl = dsl 16 | end 17 | 18 | def call(...) = cli.call(...) 19 | 20 | private 21 | 22 | attr_reader :context, :dsl 23 | 24 | def cli 25 | context = build_context 26 | 27 | dsl.new "git-lint", banner: specification.banner do 28 | on(Sod::Prefabs::Commands::Config, context:) 29 | 30 | on "analyze", "Analyze branch or commit(s)." do 31 | on Actions::Analyze::Branch 32 | on Actions::Analyze::Commit 33 | end 34 | 35 | on Actions::Hook 36 | on(Sod::Prefabs::Actions::Version, context:) 37 | on Sod::Prefabs::Actions::Help, self 38 | end 39 | end 40 | 41 | def build_context 42 | context[defaults_path:, xdg_config:, version_label: specification.labeled_version] 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/git/lint/collector.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | # Collects and categorizes, by severity, all issues (if any). 6 | class Collector 7 | def initialize 8 | @collection = Hash.new { |default, missing_id| default[missing_id] = [] } 9 | end 10 | 11 | def add analyzer 12 | collection[analyzer.commit] << analyzer 13 | analyzer 14 | end 15 | 16 | def retrieve(id) = collection[id] 17 | 18 | def clear = collection.clear && self 19 | 20 | def empty? = collection.empty? 21 | 22 | def warnings? = collection.values.flatten.any?(&:warning?) 23 | 24 | def errors? = collection.values.flatten.any?(&:error?) 25 | 26 | def issues? = collection.values.flatten.any?(&:invalid?) 27 | 28 | def total_warnings = collection.values.flatten.count(&:warning?) 29 | 30 | def total_errors = collection.values.flatten.count(&:error?) 31 | 32 | def total_issues = collection.values.flatten.count(&:invalid?) 33 | 34 | def total_commits = collection.keys.size 35 | 36 | def to_h = collection 37 | 38 | private 39 | 40 | attr_reader :collection 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/git/lint/commits/hosts/circle_ci.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Commits 6 | module Hosts 7 | # Provides Circle CI feature branch information. 8 | class CircleCI 9 | include Dependencies[:git] 10 | 11 | def call = git.commits "origin/#{branch_default}..#{branch_name}" 12 | 13 | private 14 | 15 | def branch_default = git.branch_default.value_or nil 16 | 17 | def branch_name = "origin/#{git.branch_name.value_or nil}" 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/git/lint/commits/hosts/git_hub_action.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Commits 6 | module Hosts 7 | # Provides GitHub Action feature branch information. 8 | class GitHubAction 9 | include Dependencies[:git] 10 | 11 | def call = git.commits "origin/#{branch_default}..#{branch_name}" 12 | 13 | private 14 | 15 | def branch_default = git.branch_default.value_or nil 16 | 17 | def branch_name = "origin/#{git.branch_name.value_or nil}" 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/git/lint/commits/hosts/local.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Commits 6 | module Hosts 7 | # Provides local feature branch information. 8 | class Local 9 | include Dependencies[:git] 10 | 11 | def call = git.commits "#{branch_default}..#{branch_name}" 12 | 13 | private 14 | 15 | def branch_default = git.branch_default.value_or nil 16 | 17 | def branch_name = git.branch_name.value_or nil 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/git/lint/commits/hosts/netlify_ci.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Commits 6 | module Hosts 7 | # Provides Netlify CI feature branch information. 8 | class NetlifyCI 9 | include Dependencies[:git, :environment] 10 | 11 | def call 12 | git.call("remote", "add", "-f", "origin", environment["REPOSITORY_URL"]) 13 | .bind { git.call "fetch", "origin", "#{branch_name}:#{branch_name}" } 14 | .bind { git.commits "origin/#{branch_default}..origin/#{branch_name}" } 15 | end 16 | 17 | private 18 | 19 | def branch_default = git.branch_default.value_or nil 20 | 21 | def branch_name = environment["HEAD"] 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/git/lint/commits/loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/string" 4 | 5 | module Git 6 | module Lint 7 | module Commits 8 | # Automatically detects and loads host. 9 | class Loader 10 | include Dependencies[ 11 | :git, 12 | :environment, 13 | circle_ci: "hosts.circle_ci", 14 | git_hub_action: "hosts.git_hub_action", 15 | netlify_ci: "hosts.git_hub_action", 16 | local: "hosts.local" 17 | ] 18 | 19 | using Refinements::String 20 | 21 | def call 22 | message = "Invalid repository. Are you within a Git repository?" 23 | fail Errors::Base, message unless git.exist? 24 | 25 | host.call 26 | end 27 | 28 | private 29 | 30 | def host 31 | if key? "CIRCLECI" then circle_ci 32 | elsif key? "GITHUB_ACTIONS" then git_hub_action 33 | elsif key? "NETLIFY" then netlify_ci 34 | else local 35 | end 36 | end 37 | 38 | def key?(key) = environment.fetch(key, "false").to_bool 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/git/lint/configuration/trailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Configuration 6 | # Defines trailer configuration as a subset of the primary settings. 7 | Trailer = Data.define :name, :pattern 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/git/lint/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cogger" 4 | require "containable" 5 | require "etcher" 6 | require "gitt" 7 | require "runcom" 8 | require "spek" 9 | require "tone" 10 | 11 | module Git 12 | module Lint 13 | # Provides a global gem container for injection into other objects. 14 | module Container 15 | extend Containable 16 | 17 | namespace :trailers do 18 | register :collaborator do 19 | Configuration::Trailer[name: "Co-authored-by", pattern: /\ACo.*authored.*by.*\Z/i] 20 | end 21 | 22 | register :format do 23 | Configuration::Trailer[name: "Format", pattern: /\AFormat.*\Z/i] 24 | end 25 | 26 | register :issue do 27 | Configuration::Trailer[name: "Issue", pattern: /\AIssue.*\Z/i] 28 | end 29 | 30 | register :milestone do 31 | Configuration::Trailer[name: "Milestone", pattern: /\AMilestone.*\Z/i] 32 | end 33 | 34 | register :reviewer do 35 | Configuration::Trailer[name: "Reviewer", pattern: /\AReviewer.*\Z/i] 36 | end 37 | 38 | register :signer do 39 | Configuration::Trailer[name: "Signed-off-by", pattern: /\ASigned.*off.*by.*\Z/i] 40 | end 41 | 42 | register :tracker do 43 | Configuration::Trailer[name: "Tracker", pattern: /\ATracker.*\Z/i] 44 | end 45 | end 46 | 47 | namespace :parsers do 48 | register(:person) { Gitt::Parsers::Person.new } 49 | end 50 | 51 | namespace :sanitizers do 52 | register :signature, Gitt::Sanitizers::Signature 53 | end 54 | 55 | namespace :validators do 56 | register(:capitalization) { Validators::Capitalization.new } 57 | register(:email) { Validators::Email.new } 58 | register(:name) { Validators::Name.new } 59 | register(:repeated_word) { Validators::RepeatedWord.new } 60 | end 61 | 62 | namespace :hosts do 63 | register(:circle_ci) { Commits::Hosts::CircleCI.new } 64 | register(:git_hub_action) { Commits::Hosts::GitHubAction.new } 65 | register(:netlify_ci) { Commits::Hosts::NetlifyCI.new } 66 | register(:local) { Commits::Hosts::Local.new } 67 | end 68 | 69 | register :registry, as: :fresh do 70 | Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model) 71 | .add_loader(:yaml, self[:defaults_path]) 72 | .add_loader(:yaml, self[:xdg_config].active) 73 | end 74 | 75 | register(:settings) { Etcher.call(self[:registry]).dup } 76 | register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") } 77 | register(:specification) { Spek::Loader.call "#{__dir__}/../../../git-lint.gemspec" } 78 | register(:color) { Tone.new } 79 | register :environment, ENV 80 | register(:git) { Gitt::Repository.new } 81 | register(:xdg_config) { Runcom::Config.new "git-lint/configuration.yml" } 82 | register(:logger) { Cogger.new id: "git-lint" } 83 | register :kernel, Kernel 84 | register :io, STDOUT 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/git/lint/dependencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "infusible" 4 | 5 | module Git 6 | module Lint 7 | Dependencies = Infusible[Container] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/git/lint/errors/base.rb: -------------------------------------------------------------------------------- 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 Git Lint action." 9 | super 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/git/lint/errors/severity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/array" 4 | 5 | module Git 6 | module Lint 7 | module Errors 8 | # Categorizes severity errors. 9 | class Severity < Base 10 | using Refinements::Array 11 | 12 | def initialize level 13 | usage = Analyzers::Abstract::LEVELS.to_usage "or" 14 | super %(Invalid severity level: #{level}. Use: #{usage}.) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/git/lint/errors/sha.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Errors 6 | # Categorizes commit SHA errors. 7 | class SHA < Base 8 | def initialize sha 9 | super %(Invalid commit SHA: "#{sha}". Unable to obtain commit details.) 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/git/lint/kit/filter_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core" 4 | require "refinements/array" 5 | 6 | module Git 7 | module Lint 8 | module Kit 9 | # Represents an regular expression list which may be used as an analyzer setting. 10 | class FilterList 11 | using Refinements::Array 12 | 13 | def initialize list = Core::EMPTY_ARRAY 14 | @list = Array(list).map { |item| Regexp.new item } 15 | end 16 | 17 | def empty? = list.empty? 18 | 19 | def to_a = list 20 | 21 | alias to_ary to_a 22 | 23 | def to_usage(...) = list.to_usage(...) 24 | 25 | private 26 | 27 | attr_reader :list 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/git/lint/rake/register.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "git/lint" 4 | require "rake" 5 | 6 | module Git 7 | module Lint 8 | module Rake 9 | # Registers Rake tasks for use. 10 | class Register 11 | include ::Rake::DSL 12 | 13 | def self.call = new.call 14 | 15 | def initialize shell: CLI::Shell.new 16 | @shell = shell 17 | end 18 | 19 | def call 20 | desc "Run Git Lint" 21 | task(:git_lint) { shell.call %w[analyze --branch] } 22 | end 23 | 24 | private 25 | 26 | attr_reader :shell 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/git/lint/reporters/branch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Reporters 6 | # Reports issues related to a single branch. 7 | class Branch 8 | include Dependencies[:color] 9 | using Refinements::String 10 | 11 | def initialize(collector: Collector.new, **) 12 | super(**) 13 | @collector = collector 14 | end 15 | 16 | def to_s 17 | "Running Git Lint...#{branch_report}\n" \ 18 | "#{commit_total}. #{issue_totals}.\n" 19 | end 20 | 21 | alias to_str to_s 22 | 23 | private 24 | 25 | attr_reader :collector 26 | 27 | def branch_report 28 | return "" unless collector.issues? 29 | 30 | "\n\n#{commit_report}".chomp 31 | end 32 | 33 | def commit_report 34 | collector.to_h.reduce "" do |details, (commit, analyzers)| 35 | details + Commit.new(commit:, analyzers:) 36 | end 37 | end 38 | 39 | def commit_total 40 | total = collector.total_commits 41 | %(#{total} #{"commit".pluralize "s", total} inspected) 42 | end 43 | 44 | def issue_totals 45 | if collector.issues? 46 | "#{issue_total} detected (#{warning_total}, #{error_total})" 47 | else 48 | color["0 issues", :green] + " detected" 49 | end 50 | end 51 | 52 | def issue_total 53 | style = collector.errors? ? :red : :yellow 54 | total = collector.total_issues 55 | color["#{total} issue".pluralize("s", total), style] 56 | end 57 | 58 | def warning_total 59 | style = collector.warnings? ? :yellow : :green 60 | total = collector.total_warnings 61 | color["#{total} warning".pluralize("s", total), style] 62 | end 63 | 64 | def error_total 65 | style = collector.errors? ? :red : :green 66 | total = collector.total_errors 67 | color["#{total} error".pluralize("s", total), style] 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/git/lint/reporters/commit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Reporters 6 | # Reports issues related to a single commit. 7 | class Commit 8 | def initialize commit:, analyzers: [] 9 | @commit = commit 10 | @analyzers = analyzers.select(&:invalid?) 11 | end 12 | 13 | def to_s 14 | return "" if analyzers.empty? 15 | 16 | "#{commit.sha} (#{commit.author_name}, #{commit.authored_relative_at}): " \ 17 | "#{commit.subject}\n#{report}\n" 18 | end 19 | 20 | alias to_str to_s 21 | 22 | private 23 | 24 | attr_reader :commit, :analyzers 25 | 26 | def report = analyzers.reduce("") { |report, analyzer| report + Style.new(analyzer) } 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/git/lint/reporters/line.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core" 4 | 5 | module Git 6 | module Lint 7 | module Reporters 8 | # Reports issues related to an invalid line within the commit body. 9 | class Line 10 | DEFAULT_INDENT = " " 11 | 12 | def initialize data = Core::EMPTY_HASH 13 | @data = data 14 | end 15 | 16 | def to_s 17 | content.include?("\n") ? Lines::Paragraph.new(data).to_s : Lines::Sentence.new(data).to_s 18 | end 19 | 20 | alias to_str to_s 21 | 22 | private 23 | 24 | attr_reader :data 25 | 26 | def content = data.fetch(__method__) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/git/lint/reporters/lines/paragraph.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core" 4 | 5 | module Git 6 | module Lint 7 | module Reporters 8 | module Lines 9 | # Reports paragraph details. 10 | class Paragraph 11 | def initialize data = Core::EMPTY_HASH 12 | @data = data 13 | end 14 | 15 | def to_s 16 | %(#{label}"#{paragraph}"\n) 17 | end 18 | 19 | alias to_str to_s 20 | 21 | private 22 | 23 | attr_reader :data 24 | 25 | def label = "#{Line::DEFAULT_INDENT}Line #{number}: " 26 | 27 | def paragraph = formatted_lines.join("\n") 28 | 29 | def formatted_lines 30 | content.split("\n").map.with_index do |line, index| 31 | index.zero? ? line : "#{indent}#{line}" 32 | end 33 | end 34 | 35 | def indent = " " * (label.length + 1) 36 | 37 | def number = data.fetch(:number) 38 | 39 | def content = data.fetch(:content) 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/git/lint/reporters/lines/sentence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core" 4 | 5 | module Git 6 | module Lint 7 | module Reporters 8 | module Lines 9 | # Reports sentence details. 10 | class Sentence 11 | def initialize data = Core::EMPTY_HASH 12 | @data = data 13 | end 14 | 15 | def to_s = %(#{Line::DEFAULT_INDENT}Line #{number}: "#{content}"\n) 16 | 17 | alias to_str to_s 18 | 19 | private 20 | 21 | attr_reader :data 22 | 23 | def number = data.fetch(:number) 24 | 25 | def content = data.fetch(:content) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/git/lint/reporters/style.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Reporters 6 | # Reports issues related to a single style. 7 | class Style 8 | include Dependencies[:color] 9 | 10 | def initialize(analyzer, **) 11 | super(**) 12 | @analyzer = analyzer 13 | @issue = analyzer.issue 14 | end 15 | 16 | def to_s = color[message, style] 17 | 18 | alias to_str to_s 19 | 20 | private 21 | 22 | attr_reader :analyzer, :issue 23 | 24 | def message 25 | " #{analyzer.class.label}#{severity_suffix}. " \ 26 | "#{issue.fetch :hint}\n" \ 27 | "#{affected_lines}" 28 | end 29 | 30 | def severity_suffix 31 | case analyzer.severity 32 | when "warn" then " Warning" 33 | when "error" then " Error" 34 | else "" 35 | end 36 | end 37 | 38 | def style 39 | case analyzer.severity 40 | when "warn" then :yellow 41 | when "error" then :red 42 | else :white 43 | end 44 | end 45 | 46 | def affected_lines 47 | issue.fetch(:lines, []).reduce("") { |lines, line| lines + Line.new(line) } 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/git/lint/validators/capitalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Git 4 | module Lint 5 | module Validators 6 | # Validates the capitalizationn of text. 7 | class Capitalization 8 | PATTERN = /\A[[:upper:]].*\Z/ 9 | 10 | def initialize delimiter: Name::DELIMITER, pattern: PATTERN 11 | @delimiter = delimiter 12 | @pattern = pattern 13 | end 14 | 15 | def call(content) = String(content).split(delimiter).all? { |name| name.match? pattern } 16 | 17 | private 18 | 19 | attr_reader :delimiter, :pattern 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/git/lint/validators/email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "uri" 4 | 5 | module Git 6 | module Lint 7 | module Validators 8 | # Validates the format of email addresses. 9 | class Email 10 | def initialize pattern: URI::MailTo::EMAIL_REGEXP 11 | @pattern = pattern 12 | end 13 | 14 | def call(content) = String(content).match? pattern 15 | 16 | private 17 | 18 | attr_reader :pattern 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/git/lint/validators/name.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/string" 4 | 5 | module Git 6 | module Lint 7 | module Validators 8 | # Validates the format of names. 9 | class Name 10 | using Refinements::String 11 | 12 | DELIMITER = /\s{1}/ 13 | MINIMUM = 2 14 | 15 | def initialize delimiter: DELIMITER 16 | @delimiter = delimiter 17 | end 18 | 19 | def call content, minimum: MINIMUM 20 | parts = String(content).split delimiter 21 | parts.size >= minimum && parts.all? { |name| !name.blank? } 22 | end 23 | 24 | private 25 | 26 | attr_reader :delimiter 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/git/lint/validators/repeated_word.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "core" 4 | 5 | module Git 6 | module Lint 7 | module Validators 8 | # Validates content has no repeated words. 9 | class RepeatedWord 10 | PATTERNS = { 11 | word: / 12 | \w+(?=\s) # Match word with trailing space. 13 | | # Or. 14 | (?<=\s)\w+(?=\s) # Match word between two spaces. 15 | | # Or. 16 | (?<=\s)\w+ # Match word with leading space. 17 | /x, 18 | exclude: / 19 | ( # Conditional start. 20 | `.+` # Code blocks. 21 | | # Or. 22 | \d+\. # Digits followed by periods. 23 | ) # Conditional end. 24 | /x 25 | }.freeze 26 | 27 | def initialize patterns: PATTERNS 28 | @patterns = patterns 29 | end 30 | 31 | def call(content) = content ? scan(content) : Core::EMPTY_ARRAY 32 | 33 | private 34 | 35 | attr_reader :patterns 36 | 37 | def scan content 38 | parse(content).each_cons(2).with_object [] do |(current, future), repeats| 39 | repeats.append future if current.casecmp(future).zero? 40 | end 41 | end 42 | 43 | def parse(content) = content.gsub(exclude_pattern, "").scan word_pattern 44 | 45 | def word_pattern = patterns.fetch :word 46 | 47 | def exclude_pattern = patterns.fetch :exclude 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "spec_helper" 5 | 6 | RSpec.describe Git::Lint::Analyzer do 7 | include Dry::Monads[:result] 8 | 9 | using Refinements::Pathname 10 | using Refinements::Struct 11 | 12 | subject(:analyzer) { described_class.new } 13 | 14 | include_context "with Git repository" 15 | include_context "with application dependencies" 16 | 17 | let(:branch) { "test" } 18 | 19 | before do 20 | git_repo_dir.change_dir do 21 | `git switch --quiet --create test --track` 22 | `printf "%s\n" "Test content" > one.txt` 23 | `git add --all .` 24 | end 25 | end 26 | 27 | describe "#call" do 28 | it "answers collector and reporter without block" do 29 | git_repo_dir.change_dir do 30 | `git commit --no-verify --message "Added one.txt" --message "- For testing purposes"` 31 | 32 | expect(analyzer.call).to contain_exactly( 33 | kind_of(Git::Lint::Collector), 34 | kind_of(Git::Lint::Reporters::Branch) 35 | ) 36 | end 37 | end 38 | 39 | it "yields collector and reporter with block" do 40 | git_repo_dir.change_dir do 41 | `git commit --no-verify --message "Added one.txt" --message "- For testing purposes"` 42 | 43 | analyzer.call do |collector, reporter| 44 | expect([collector, reporter]).to contain_exactly( 45 | kind_of(Git::Lint::Collector), 46 | kind_of(Git::Lint::Reporters::Branch) 47 | ) 48 | end 49 | end 50 | end 51 | 52 | it "reports no issues with valid commits" do 53 | git_repo_dir.change_dir do 54 | `git commit --no-verify --message "Added one.txt" --message "For testing purposes"` 55 | collector, _reporter = analyzer.call 56 | 57 | expect(collector.issues?).to be(false) 58 | end 59 | end 60 | 61 | it "reports issues with invalid commits" do 62 | git_repo_dir.change_dir do 63 | `git commit --no-verify --message "Add one.txt" --message "- A test bullet"` 64 | collector, _reporter = analyzer.call 65 | 66 | expect(collector.issues?).to be(true) 67 | end 68 | end 69 | 70 | it "reports no issues with disabled analyzer" do 71 | analyzer = described_class.new( 72 | settings: settings.merge(commits_subject_prefix_enabled: false) 73 | ) 74 | 75 | git_repo_dir.change_dir do 76 | `git commit --no-verify --message "Bogus commit message"` 77 | collector, _reporter = analyzer.call 78 | 79 | expect(collector.issues?).to be(false) 80 | end 81 | end 82 | 83 | context "with single commit" do 84 | include_context "with Git commit" 85 | 86 | it "processes commit" do 87 | collector, _reporter = analyzer.call commits: Success([git_commit]) 88 | expect(collector.issues?).to be(true) 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_author_capitalization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitAuthorCapitalization do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_author_capitalization") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Author Capitalization") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with capitalization" do 24 | let(:commit) { Gitt::Models::Commit[author_name: "Example Test"] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "without capitalization" do 32 | let(:commit) { Gitt::Models::Commit[author_name: "example Test"] } 33 | 34 | it "answers false" do 35 | expect(analyzer.valid?).to be(false) 36 | end 37 | end 38 | end 39 | 40 | describe "#issue" do 41 | let(:issue) { analyzer.issue } 42 | 43 | context "when valid" do 44 | let(:commit) { Gitt::Models::Commit[author_name: "Example Test"] } 45 | 46 | it "answers empty hash" do 47 | expect(issue).to eq({}) 48 | end 49 | end 50 | 51 | context "when invalid" do 52 | let(:commit) { Gitt::Models::Commit[author_name: "example"] } 53 | 54 | it "answers issue hint" do 55 | expect(issue[:hint]).to eq(%(Capitalize each part of name: "example".)) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_author_email_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitAuthorEmail do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_author_email") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Author Email") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid email" do 24 | let(:commit) { Gitt::Models::Commit[author_email: "test@example.com"] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with invalid email" do 32 | let(:commit) { Gitt::Models::Commit[author_email: "bogus"] } 33 | 34 | it "answers false" do 35 | expect(analyzer.valid?).to be(false) 36 | end 37 | end 38 | end 39 | 40 | describe "#issue" do 41 | let(:issue) { analyzer.issue } 42 | 43 | context "when valid" do 44 | let(:commit) { Gitt::Models::Commit[author_email: "test@example.com"] } 45 | 46 | it "answers empty hash" do 47 | expect(issue).to eq({}) 48 | end 49 | end 50 | 51 | context "when invalid" do 52 | let(:commit) { Gitt::Models::Commit[author_email: "bogus"] } 53 | 54 | it "answers issue hint" do 55 | expect(issue[:hint]).to eq(%(Use "@." instead of "bogus".)) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_author_name_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitAuthorName do 6 | using Refinements::Struct 7 | 8 | subject(:analyzer) { described_class.new commit } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe ".id" do 13 | it "answers class ID" do 14 | expect(described_class.id).to eq("commit_author_name") 15 | end 16 | end 17 | 18 | describe ".label" do 19 | it "answers class label" do 20 | expect(described_class.label).to eq("Commit Author Name") 21 | end 22 | end 23 | 24 | describe "#valid?" do 25 | context "when valid" do 26 | let(:commit) { Gitt::Models::Commit[author_name: "Test Example"] } 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with invalid name" do 34 | let(:commit) { Gitt::Models::Commit[author_name: "Bogus"] } 35 | 36 | it "answers false" do 37 | expect(analyzer.valid?).to be(false) 38 | end 39 | end 40 | 41 | it "answers true with custom minimum" do 42 | analyzer = described_class.new( 43 | Gitt::Models::Commit[author_name: "Example"], 44 | settings: settings.merge(commits_author_name_minimum: 1) 45 | ) 46 | 47 | expect(analyzer.valid?).to be(true) 48 | end 49 | end 50 | 51 | describe "#issue" do 52 | let(:issue) { analyzer.issue } 53 | 54 | context "when valid" do 55 | let(:commit) { Gitt::Models::Commit[author_name: "Example Tester"] } 56 | 57 | it "answers empty hash" do 58 | expect(issue).to eq({}) 59 | end 60 | end 61 | 62 | context "when invalid" do 63 | let(:commit) { Gitt::Models::Commit[author_name: "Bogus"] } 64 | 65 | it "answers issue hint" do 66 | hint = "Author name must consist of 2 parts (minimum)." 67 | expect(issue[:hint]).to eq(hint) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_bullet_capitalization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyBulletCapitalization do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_body_bullet_capitalization") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Body Bullet Capitalization") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with uppercase bullet" do 24 | let(:commit) { Gitt::Models::Commit[body_lines: ["- Test."]] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with no bullet lines" do 32 | let(:commit) { Gitt::Models::Commit[body_lines: ["test."]] } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "with empty lines" do 40 | let(:commit) { Gitt::Models::Commit[body_lines: []] } 41 | 42 | it "answers true" do 43 | expect(analyzer.valid?).to be(true) 44 | end 45 | end 46 | 47 | context "with lowercase bullet (no trailing space)" do 48 | let(:commit) { Gitt::Models::Commit[body_lines: ["-test."]] } 49 | 50 | it "answers true" do 51 | expect(analyzer.valid?).to be(true) 52 | end 53 | end 54 | 55 | context "with capitalized ASCII Doc link" do 56 | let(:commit) { Gitt::Models::Commit[body_lines: ["- link:https://test.com[Test]"]] } 57 | 58 | it "answers true" do 59 | expect(analyzer.valid?).to be(true) 60 | end 61 | end 62 | 63 | context "with lowercase bullet" do 64 | let(:commit) { Gitt::Models::Commit[body_lines: ["- test."]] } 65 | 66 | it "answers false" do 67 | expect(analyzer.valid?).to be(false) 68 | end 69 | end 70 | 71 | context "with lowercase bullet (indented)" do 72 | let(:commit) { Gitt::Models::Commit[body_lines: [" - test."]] } 73 | 74 | it "answers false" do 75 | expect(analyzer.valid?).to be(false) 76 | end 77 | end 78 | end 79 | 80 | describe "#issue" do 81 | let(:issue) { analyzer.issue } 82 | 83 | context "when valid" do 84 | let(:commit) { Gitt::Models::Commit[body_lines: ["- Test."]] } 85 | 86 | it "answers empty hash" do 87 | expect(issue).to eq({}) 88 | end 89 | end 90 | 91 | context "when invalid" do 92 | let :commit do 93 | Gitt::Models::Commit[body_lines: ["Examples:", "- one.", "- Two.", "* three."]] 94 | end 95 | 96 | it "answers issue hint" do 97 | expect(issue[:hint]).to eq("Capitalize first word.") 98 | end 99 | 100 | it "answers issue affected lines" do 101 | expect(issue[:lines]).to contain_exactly( 102 | {number: 4, content: "- one."}, 103 | {number: 6, content: "* three."} 104 | ) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_bullet_delimiter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyBulletDelimiter do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_body_bullet_delimiter") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Body Bullet Delimiter") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with space after bullet" do 24 | let(:commit) { Gitt::Models::Commit[body_lines: ["- Test."]] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with indented bullet and trailing space" do 32 | let(:commit) { Gitt::Models::Commit[body_lines: [" - test."]] } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "with repeated bullet" do 40 | let :commit do 41 | Gitt::Models::Commit[body_lines: ["--", "--test", " --test", "-- test"]] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "without space after bullet" do 50 | let(:commit) { Gitt::Models::Commit[body_lines: ["-Test."]] } 51 | 52 | it "answers false" do 53 | expect(analyzer.valid?).to be(false) 54 | end 55 | end 56 | 57 | context "with indented bullet without trailing space" do 58 | let(:commit) { Gitt::Models::Commit[body_lines: [" -test."]] } 59 | 60 | it "answers false" do 61 | expect(analyzer.valid?).to be(false) 62 | end 63 | end 64 | 65 | context "with no bullet lines" do 66 | let(:commit) { Gitt::Models::Commit[body_lines: ["test."]] } 67 | 68 | it "answers true" do 69 | expect(analyzer.valid?).to be(true) 70 | end 71 | end 72 | 73 | context "with empty lines" do 74 | let(:commit) { Gitt::Models::Commit[body_lines: []] } 75 | 76 | it "answers true" do 77 | expect(analyzer.valid?).to be(true) 78 | end 79 | end 80 | end 81 | 82 | describe "#issue" do 83 | let(:issue) { analyzer.issue } 84 | 85 | context "when valid" do 86 | let(:commit) { Gitt::Models::Commit[body_lines: []] } 87 | 88 | it "answers empty hash" do 89 | expect(issue).to eq({}) 90 | end 91 | end 92 | 93 | context "when invalid" do 94 | let(:commit) { Gitt::Models::Commit[body_lines: ["One.", "- Two.", "-three.", "*four."]] } 95 | 96 | it "answers issue hint" do 97 | expect(issue[:hint]).to eq("Use space after bullet.") 98 | end 99 | 100 | it "answers issue affected lines" do 101 | expect(issue[:lines]).to contain_exactly( 102 | {number: 5, content: "-three."}, 103 | {number: 6, content: "*four."} 104 | ) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_bullet_only_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyBulletOnly do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_body_bullet_only") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Body Bullet Only") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with multiple bullets" do 24 | let(:commit) { Gitt::Models::Commit[body_lines: ["- One.", "- Two."]] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with no bullets" do 32 | let(:commit) { Gitt::Models::Commit[body_lines: ["Test."]] } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "with empty lines" do 40 | let(:commit) { Gitt::Models::Commit[body_lines: []] } 41 | 42 | it "answers true" do 43 | expect(analyzer.valid?).to be(true) 44 | end 45 | end 46 | 47 | context "with single bullet (indented)" do 48 | let(:commit) { Gitt::Models::Commit[body_lines: [" - Test."]] } 49 | 50 | it "answers true" do 51 | expect(analyzer.valid?).to be(true) 52 | end 53 | end 54 | 55 | context "with single bullet (no trailing space)" do 56 | let(:commit) { Gitt::Models::Commit[body_lines: ["-Test."]] } 57 | 58 | it "answers true" do 59 | expect(analyzer.valid?).to be(true) 60 | end 61 | end 62 | 63 | context "with single bullet" do 64 | let(:commit) { Gitt::Models::Commit[body_lines: ["- Test."]] } 65 | 66 | it "answers false" do 67 | expect(analyzer.valid?).to be(false) 68 | end 69 | end 70 | end 71 | 72 | describe "#issue" do 73 | let(:issue) { analyzer.issue } 74 | 75 | context "when valid" do 76 | let(:commit) { Gitt::Models::Commit[body_lines: ["- One.", "- Two."]] } 77 | 78 | it "answers empty hash" do 79 | expect(issue).to eq({}) 80 | end 81 | end 82 | 83 | context "with invalid ASCII Doc" do 84 | let(:commit) { Gitt::Models::Commit[body_lines: ["* Test."]] } 85 | 86 | it "answers issue hint" do 87 | expect(issue[:hint]).to eq("Use paragraph instead of single bullet.") 88 | end 89 | 90 | it "answers issue affected lines" do 91 | expect(issue[:lines]).to contain_exactly(number: 3, content: "* Test.") 92 | end 93 | end 94 | 95 | context "with invalid Markdown" do 96 | let(:commit) { Gitt::Models::Commit[body_lines: ["- Test."]] } 97 | 98 | it "answers issue hint" do 99 | expect(issue[:hint]).to eq("Use paragraph instead of single bullet.") 100 | end 101 | 102 | it "answers issue affected lines" do 103 | expect(issue[:lines]).to contain_exactly(number: 3, content: "- Test.") 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_leading_line_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyLeadingLine do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_body_leading_line") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Body Leading Line") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "when valid" do 24 | let(:commit) { Gitt::Models::Commit[raw: "Subject\n\nBody.\n"] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with subject only" do 32 | let(:commit) { Gitt::Models::Commit[raw: "Subject"] } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "with subject and no body" do 40 | let(:commit) { Gitt::Models::Commit[raw: "Subject\n\n"] } 41 | 42 | it "answers true" do 43 | expect(analyzer.valid?).to be(true) 44 | end 45 | end 46 | 47 | context "with subject and comments" do 48 | let(:commit) { Gitt::Models::Commit[raw: "Subject\n\n# Comment.\n"] } 49 | 50 | it "answers true" do 51 | expect(analyzer.valid?).to be(true) 52 | end 53 | end 54 | 55 | context "with no space between subject and body" do 56 | let(:commit) { Gitt::Models::Commit[raw: "Subject\nBody\n"] } 57 | 58 | it "answers false" do 59 | expect(analyzer.valid?).to be(false) 60 | end 61 | end 62 | 63 | context "with no content" do 64 | let(:commit) { Gitt::Models::Commit[raw: ""] } 65 | 66 | it "answers false" do 67 | expect(analyzer.valid?).to be(false) 68 | end 69 | end 70 | end 71 | 72 | describe "#issue" do 73 | let(:issue) { analyzer.issue } 74 | 75 | context "when valid" do 76 | let(:commit) { Gitt::Models::Commit[raw: "Subject\n\nBody.\n"] } 77 | 78 | it "answers empty hash" do 79 | expect(issue).to eq({}) 80 | end 81 | end 82 | 83 | context "when invalid" do 84 | let(:commit) { Gitt::Models::Commit[raw: "Subject\nBody."] } 85 | 86 | it "answers issue hint" do 87 | expect(issue[:hint]).to eq("Use blank line between subject and body.") 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_line_length_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyLineLength do 6 | using Refinements::Struct 7 | 8 | subject(:analyzer) { described_class.new commit } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe ".id" do 13 | it "answers class ID" do 14 | expect(described_class.id).to eq("commit_body_line_length") 15 | end 16 | end 17 | 18 | describe ".label" do 19 | it "answers class label" do 20 | expect(described_class.label).to eq("Commit Body Line Length") 21 | end 22 | end 23 | 24 | describe "#valid?" do 25 | context "when valid" do 26 | let(:commit) { Gitt::Models::Commit[body_lines: ["Test."]] } 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "when invalid (single line)" do 34 | let :commit do 35 | Gitt::Models::Commit[ 36 | body_lines: ["Pellentque morbi-trist sentus et netus et malesuada fames ac turpis egest."] 37 | ] 38 | end 39 | 40 | it "answers false" do 41 | expect(analyzer.valid?).to be(false) 42 | end 43 | end 44 | 45 | context "when invalid (multiple lines)" do 46 | let :commit do 47 | Gitt::Models::Commit[ 48 | body_lines: [ 49 | "- Curabitur eleifend wisi iaculis ipsum.", 50 | "- Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante.", 51 | "- Donec eu_libero sit amet quam egestas semper. Aenean ultricies mi vitae est." 52 | ] 53 | ] 54 | end 55 | 56 | it "answers false" do 57 | expect(analyzer.valid?).to be(false) 58 | end 59 | end 60 | end 61 | 62 | describe "#issue" do 63 | let(:issue) { analyzer.issue } 64 | 65 | context "when valid" do 66 | let(:commit) { Gitt::Models::Commit[body_lines: ["Test."]] } 67 | 68 | it "answers empty hash" do 69 | expect(issue).to eq({}) 70 | end 71 | end 72 | 73 | context "when invalid" do 74 | subject :analyzer do 75 | described_class.new Gitt::Models::Commit[ 76 | body_lines: [ 77 | "- Curabitur eleifend wisi iaculis ipsum.", 78 | "- Vestibulum tortor quam, feugiat vitae, ultricies eget bon.", 79 | "- Donec eu_libero sit amet quam egestas semper. Aenean ultr." 80 | ] 81 | ], 82 | settings: settings.merge(commits_body_line_length_maximum: 55) 83 | end 84 | 85 | it "answers issue hint" do 86 | expect(issue[:hint]).to eq("Use 55 characters or less per line.") 87 | end 88 | 89 | it "answers issue lines" do 90 | expect(issue[:lines]).to eq( 91 | [ 92 | {number: 4, content: "- Vestibulum tortor quam, feugiat vitae, ultricies eget bon."}, 93 | {number: 5, content: "- Donec eu_libero sit amet quam egestas semper. Aenean ultr."} 94 | ] 95 | ) 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_presence_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyPresence do 6 | using Refinements::Struct 7 | 8 | subject(:analyzer) { described_class.new commit } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe ".id" do 13 | it "answers class ID" do 14 | expect(described_class.id).to eq("commit_body_presence") 15 | end 16 | end 17 | 18 | describe ".label" do 19 | it "answers class label" do 20 | expect(described_class.label).to eq("Commit Body Presence") 21 | end 22 | end 23 | 24 | describe "#valid?" do 25 | context "when valid" do 26 | let(:commit) { Gitt::Models::Commit[subject: "Test", body_lines: ["Test."]] } 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | it "answers true when valid (custom minimum)" do 34 | analyzer = described_class.new( 35 | Gitt::Models::Commit[subject: "Test", body_lines: ["One.", "Two.", "Three."]], 36 | settings: settings.merge(commits_body_presence_minimum: 3) 37 | ) 38 | 39 | expect(analyzer.valid?).to be(true) 40 | end 41 | 42 | context "when valid (fixup!)" do 43 | let(:commit) { Gitt::Models::Commit[subject: "fixup! Test", body_lines: ["Test."]] } 44 | 45 | it "answers true" do 46 | expect(analyzer.valid?).to be(true) 47 | end 48 | end 49 | 50 | context "when invalid (empty)" do 51 | let(:commit) { Gitt::Models::Commit[subject: "Test", body_lines: [""]] } 52 | 53 | it "answers false" do 54 | expect(analyzer.valid?).to be(false) 55 | end 56 | end 57 | 58 | it "answers false when invalid (custom minimum and not enough non-empty lines)" do 59 | analyzer = described_class.new( 60 | Gitt::Models::Commit[subject: "Test", body_lines: ["One.", "\r", "", "\t", "Two."]], 61 | settings: settings.merge(commits_body_presence_minimum: 3) 62 | ) 63 | 64 | expect(analyzer.valid?).to be(false) 65 | end 66 | end 67 | 68 | describe "#issue" do 69 | let(:issue) { analyzer.issue } 70 | 71 | context "when valid" do 72 | let(:commit) { Gitt::Models::Commit[subject: "Test", body_lines: ["Test."]] } 73 | 74 | it "answers empty hash" do 75 | expect(issue).to eq({}) 76 | end 77 | end 78 | 79 | context "when invalid" do 80 | let :analyzer do 81 | described_class.new( 82 | Gitt::Models::Commit[subject: "Test", body_lines: ["One.", "\r", " ", "\t", "Two."]], 83 | settings: settings.merge(commits_body_presence_minimum: 3) 84 | ) 85 | end 86 | 87 | it "answers issue hint" do 88 | expect(issue[:hint]).to eq("Use minimum of 3 lines (non-empty).") 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_tracker_shorthand_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyTrackerShorthand do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_body_tracker_shorthand") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Body Tracker Shorthand") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with no links" do 24 | let(:commit) { Gitt::Models::Commit[body_lines: ["Test."]] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | ["fix #1", "Fix #12", "fixes #3", "Fixes #4", "fixed #5", "Fixed #6"].each do |line| 32 | context %(with "#{line}" link) do 33 | let(:commit) { Gitt::Models::Commit[body_lines: [line]] } 34 | 35 | it "answers false" do 36 | expect(analyzer.valid?).to be(false) 37 | end 38 | end 39 | end 40 | 41 | ["close #1", "Close #12", "closes #3", "Closes #4", "closed #5", "Closed #6"].each do |line| 42 | context %(with "#{line}" link) do 43 | let(:commit) { Gitt::Models::Commit[body_lines: [line]] } 44 | 45 | it "answers false" do 46 | expect(analyzer.valid?).to be(false) 47 | end 48 | end 49 | end 50 | 51 | [ 52 | "resolve #1", 53 | "Resolve #12", 54 | "resolves #3", 55 | "Resolves #4", 56 | "resolved #5", 57 | "Resolved #6" 58 | ].each do |line| 59 | context %(with "#{line}" link) do 60 | let(:commit) { Gitt::Models::Commit[body_lines: [line]] } 61 | 62 | it "answers false" do 63 | expect(analyzer.valid?).to be(false) 64 | end 65 | end 66 | end 67 | end 68 | 69 | describe "#issue" do 70 | let(:issue) { analyzer.issue } 71 | 72 | context "when valid" do 73 | let(:commit) { Gitt::Models::Commit[body_lines: ["Test."]] } 74 | 75 | it "answers empty hash" do 76 | expect(issue).to eq({}) 77 | end 78 | end 79 | 80 | context "when invalid" do 81 | let :commit do 82 | Gitt::Models::Commit[ 83 | body_lines: [ 84 | "An example paragraph.", 85 | "This work fixes #22 using suggestions from team.", 86 | "See [Issue 22](https://github.com/test/project/issues/22)." 87 | ] 88 | ] 89 | end 90 | 91 | it "answers issue hint" do 92 | expect(issue[:hint]).to eq( 93 | "Explain issue instead of using shorthand. Avoid: /(c|C)lose(s|d)?\\s\\#\\d+/, " \ 94 | "/(f|F)ix(es|ed)?\\s\\#\\d+/, " \ 95 | "and " \ 96 | "/(r|R)esolve(s|d)?\\s\\#\\d+/." 97 | ) 98 | end 99 | 100 | it "answers issue affected lines" do 101 | expect(issue[:lines]).to eq( 102 | [ 103 | {number: 4, content: "This work fixes #22 using suggestions from team."} 104 | ] 105 | ) 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_body_word_repeat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitBodyWordRepeat do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_body_word_repeat") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Body Word Repeat") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid body" do 24 | let(:commit) { Gitt::Models::Commit[body_lines: ["This is a test."]] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with comments only" do 32 | let(:commit) { Gitt::Models::Commit[body_lines: ["# This is is a test."]] } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "when invalid with single line" do 40 | let(:commit) { Gitt::Models::Commit[body_lines: ["This is is a test."]] } 41 | 42 | it "answers true" do 43 | expect(analyzer.valid?).to be(false) 44 | end 45 | end 46 | 47 | context "when invalid with multiple lines" do 48 | let :commit do 49 | Gitt::Models::Commit[ 50 | body_lines: ["This is a test.", "This is invalid invalid.", "End end."] 51 | ] 52 | end 53 | 54 | it "answers true" do 55 | expect(analyzer.valid?).to be(false) 56 | end 57 | end 58 | 59 | context "with empty body" do 60 | let(:commit) { Gitt::Models::Commit[body_lines: []] } 61 | 62 | it "answers true" do 63 | expect(analyzer.valid?).to be(true) 64 | end 65 | end 66 | end 67 | 68 | describe "#issue" do 69 | let(:issue) { analyzer.issue } 70 | 71 | context "when valid" do 72 | let(:commit) { Gitt::Models::Commit[body_lines: ["This is a test."]] } 73 | 74 | it "answers empty string" do 75 | expect(issue).to eq({}) 76 | end 77 | end 78 | 79 | context "when invalid" do 80 | let :commit do 81 | Gitt::Models::Commit[ 82 | body: "This is is a test.\nOne one more time.", 83 | body_lines: ["This is is a test.", "One one more time."] 84 | ] 85 | end 86 | 87 | it "answres issue hint" do 88 | expect(issue).to eq( 89 | hint: %(Avoid repeating these words: ["is", "one"].), 90 | lines: [ 91 | {content: "This is is a test.", number: 3}, 92 | {content: "One one more time.", number: 4} 93 | ] 94 | ) 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_signature_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitSignature do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_signature") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Signature") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "when valid" do 24 | let(:commit) { Gitt::Models::Commit[signature: "G"] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "when invalid" do 32 | let(:commit) { Gitt::Models::Commit[signature: "B"] } 33 | 34 | it "answers false" do 35 | expect(analyzer.valid?).to be(false) 36 | end 37 | end 38 | end 39 | 40 | describe "#issue" do 41 | let(:issue) { analyzer.issue } 42 | 43 | context "when valid" do 44 | let(:commit) { Gitt::Models::Commit[signature: "G"] } 45 | 46 | it "answers empty hash" do 47 | expect(issue).to eq({}) 48 | end 49 | end 50 | 51 | context "when invalid" do 52 | let(:commit) { Gitt::Models::Commit[signature: "B"] } 53 | 54 | it "answers issue hint" do 55 | expect(issue[:hint]).to eq("Use: /Good/ or /Invalid/.") 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_subject_length_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitSubjectLength do 6 | using Refinements::Struct 7 | 8 | subject(:analyzer) { described_class.new commit } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe ".id" do 13 | it "answers class ID" do 14 | expect(described_class.id).to eq("commit_subject_length") 15 | end 16 | end 17 | 18 | describe ".label" do 19 | it "answers class label" do 20 | expect(described_class.label).to eq("Commit Subject Length") 21 | end 22 | end 23 | 24 | describe "#valid?" do 25 | context "when valid" do 26 | let(:commit) { Gitt::Models::Commit[subject: "Added specs"] } 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with fixup prefix and max subject length" do 34 | let :commit do 35 | Gitt::Models::Commit[ 36 | subject: "fixup! Added Curabitur eleifend wisi iaculis ipsum iaculis inia wazlouth tik mx" 37 | ] 38 | end 39 | 40 | it "answers true" do 41 | expect(analyzer.valid?).to be(true) 42 | end 43 | end 44 | 45 | context "with squash prefix and max subject length" do 46 | let :commit do 47 | Gitt::Models::Commit[ 48 | subject: "squash! Added Curabitur eleifend wisi iaculis ipsum iaculis inia wazlouth " \ 49 | "tik mx" 50 | ] 51 | end 52 | 53 | it "answers true" do 54 | expect(analyzer.valid?).to be(true) 55 | end 56 | end 57 | 58 | it "answers false with invalid length" do 59 | analyzer = described_class.new( 60 | Gitt::Models::Commit[subject: "Added specs"], 61 | settings: settings.merge(commits_subject_length_maximum: 10) 62 | ) 63 | 64 | expect(analyzer.valid?).to be(false) 65 | end 66 | end 67 | 68 | describe "#issue" do 69 | let(:issue) { analyzer.issue } 70 | 71 | context "when valid" do 72 | let(:commit) { Gitt::Models::Commit[subject: "Added specs"] } 73 | 74 | it "answers empty hash" do 75 | expect(issue).to eq({}) 76 | end 77 | end 78 | 79 | context "when invalid" do 80 | subject :analyzer do 81 | described_class.new( 82 | Gitt::Models::Commit[subject: "Added specs"], 83 | settings: settings.merge(commits_subject_length_maximum: 10) 84 | ) 85 | end 86 | 87 | it "answers issue hint" do 88 | expect(issue[:hint]).to eq("Use 10 characters or less.") 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_subject_suffix_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitSubjectSuffix do 6 | using Refinements::Struct 7 | 8 | subject(:analyzer) { described_class.new commit } 9 | 10 | include_context "with application dependencies" 11 | 12 | describe ".id" do 13 | it "answers class ID" do 14 | expect(described_class.id).to eq("commit_subject_suffix") 15 | end 16 | end 17 | 18 | describe ".label" do 19 | it "answers class label" do 20 | expect(described_class.label).to eq("Commit Subject Suffix") 21 | end 22 | end 23 | 24 | describe "#valid?" do 25 | context "when valid" do 26 | let(:commit) { Gitt::Models::Commit[subject: "Added specs"] } 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with period" do 34 | let(:commit) { Gitt::Models::Commit[subject: "Added specs."] } 35 | 36 | it "answers false" do 37 | expect(analyzer.valid?).to be(false) 38 | end 39 | end 40 | 41 | context "with question mark" do 42 | let(:commit) { Gitt::Models::Commit[subject: "Added specs?"] } 43 | 44 | it "answers false" do 45 | expect(analyzer.valid?).to be(false) 46 | end 47 | end 48 | 49 | context "with exclamation mark" do 50 | let(:commit) { Gitt::Models::Commit[subject: "Added specs!"] } 51 | 52 | it "answers false" do 53 | expect(analyzer.valid?).to be(false) 54 | end 55 | end 56 | 57 | it "answers false with custom exclude list" do 58 | analyzer = described_class.new( 59 | Gitt::Models::Commit[subject: "Added specs 😅"], 60 | settings: settings.merge(commits_subject_suffix_excludes: ["😅"]) 61 | ) 62 | 63 | expect(analyzer.valid?).to be(false) 64 | end 65 | 66 | it "answers true with empty exclude list" do 67 | analyzer = described_class.new( 68 | Gitt::Models::Commit[subject: "Added specs?"], 69 | settings: settings.merge(commits_subject_suffix_excludes: []) 70 | ) 71 | 72 | expect(analyzer.valid?).to be(true) 73 | end 74 | end 75 | 76 | describe "#issue" do 77 | let(:issue) { analyzer.issue } 78 | 79 | context "when valid" do 80 | let(:commit) { Gitt::Models::Commit[subject: "Added specs"] } 81 | 82 | it "answers empty hash" do 83 | expect(issue).to eq({}) 84 | end 85 | end 86 | 87 | context "when invalid" do 88 | let(:commit) { Gitt::Models::Commit[subject: "Added specs?"] } 89 | 90 | it "answers issue hint" do 91 | expect(issue[:hint]).to eq("Avoid: /\\!/, /\\./, and /\\?/.") 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_subject_word_repeat_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitSubjectWordRepeat do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_subject_word_repeat") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Subject Word Repeat") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid subject" do 24 | let(:commit) { Gitt::Models::Commit[subject: "Added specs"] } 25 | 26 | it "answers true" do 27 | expect(analyzer.valid?).to be(true) 28 | end 29 | end 30 | 31 | context "with empty subject" do 32 | let(:commit) { Gitt::Models::Commit.new subject: "" } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "with no subject" do 40 | let(:commit) { Gitt::Models::Commit.new } 41 | 42 | it "answers true" do 43 | expect(analyzer.valid?).to be(true) 44 | end 45 | end 46 | end 47 | 48 | describe "#issue" do 49 | let(:issue) { analyzer.issue } 50 | 51 | context "when valid" do 52 | let(:commit) { Gitt::Models::Commit[subject: "Added specs"] } 53 | 54 | it "answers empty string" do 55 | expect(issue).to eq({}) 56 | end 57 | end 58 | 59 | context "when invalid" do 60 | let(:commit) { Gitt::Models::Commit[subject: "Added specs specs"] } 61 | 62 | it "answres issue hint" do 63 | expect(issue[:hint]).to eq(%(Avoid repeating these words: ["specs"].)) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_collaborator_capitalization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerCollaboratorCapitalization do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_collaborator_capitalization") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Collaborator Capitalization") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with no matching key" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Unknown: value")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid capitalization" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: Test Example ")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with invalid capitalization" do 50 | let :commit do 51 | Gitt::Models::Commit[ 52 | body_lines: [], 53 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: test ")] 54 | ] 55 | end 56 | 57 | it "answers false" do 58 | expect(analyzer.valid?).to be(false) 59 | end 60 | end 61 | 62 | context "with missing name" do 63 | let :commit do 64 | Gitt::Models::Commit[ 65 | body_lines: [], 66 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: ")] 67 | ] 68 | end 69 | 70 | it "answers true" do 71 | expect(analyzer.valid?).to be(true) 72 | end 73 | end 74 | end 75 | 76 | describe "#issue" do 77 | let(:issue) { analyzer.issue } 78 | 79 | context "when valid" do 80 | let :commit do 81 | Gitt::Models::Commit[ 82 | body_lines: [], 83 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: Test Example ")] 84 | ] 85 | end 86 | 87 | it "answers empty hash" do 88 | expect(issue).to eq({}) 89 | end 90 | end 91 | 92 | context "when invalid" do 93 | let :commit do 94 | Gitt::Models::Commit[ 95 | body_lines: [], 96 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: test Example ")] 97 | ] 98 | end 99 | 100 | it "answers issue" do 101 | expect(issue).to eq( 102 | hint: "Name must be capitalized.", 103 | lines: [ 104 | { 105 | content: "Co-Authored-By: test Example ", 106 | number: 3 107 | } 108 | ] 109 | ) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_collaborator_email_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerCollaboratorEmail do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_collaborator_email") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Collaborator Email") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "when valid" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: Test Example ")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with no matching key" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Unknown: value")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with malformed email address" do 50 | let :commit do 51 | Gitt::Models::Commit[ 52 | body_lines: [], 53 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: Test Example ")] 54 | ] 55 | end 56 | 57 | it "answers false" do 58 | expect(analyzer.valid?).to be(false) 59 | end 60 | end 61 | end 62 | 63 | describe "#issue" do 64 | let(:issue) { analyzer.issue } 65 | 66 | context "when valid" do 67 | let :commit do 68 | Gitt::Models::Commit[ 69 | body_lines: [], 70 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: Test Example ")] 71 | ] 72 | end 73 | 74 | it "answers empty hash" do 75 | expect(issue).to eq({}) 76 | end 77 | end 78 | 79 | context "when invalid" do 80 | let :commit do 81 | Gitt::Models::Commit[ 82 | body_lines: [], 83 | trailers: [Gitt::Models::Trailer.for("Co-Authored-By: Test Example ")] 84 | ] 85 | end 86 | 87 | it "answers issue" do 88 | expect(issue).to eq( 89 | hint: %(Email must follow name and use format: "".), 90 | lines: [ 91 | { 92 | content: "Co-Authored-By: Test Example ", 93 | number: 3 94 | } 95 | ] 96 | ) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_duplicate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerDuplicate do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_duplicate") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Duplicate") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "when valid" do 24 | let :commit do 25 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Issue: 123")]] 26 | end 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with no matching key" do 34 | let :commit do 35 | Gitt::Models::Commit[ 36 | body_lines: [], 37 | trailers: [Gitt::Models::Trailer.for("Unknown: value")] 38 | ] 39 | end 40 | 41 | it "answers true" do 42 | expect(analyzer.valid?).to be(true) 43 | end 44 | end 45 | 46 | context "when unique" do 47 | let :commit do 48 | Gitt::Models::Commit[ 49 | body_lines: [], 50 | trailers: [ 51 | Gitt::Models::Trailer.for("Issue: 123"), 52 | Gitt::Models::Trailer.for("Issue: 456") 53 | ] 54 | ] 55 | end 56 | 57 | it "answers true" do 58 | expect(analyzer.valid?).to be(true) 59 | end 60 | end 61 | 62 | context "when duplicated" do 63 | let :commit do 64 | Gitt::Models::Commit[ 65 | body_lines: [], 66 | trailers: [ 67 | Gitt::Models::Trailer.for("Issue: 123"), 68 | Gitt::Models::Trailer.for("Issue: 123") 69 | ] 70 | ] 71 | end 72 | 73 | it "answers false" do 74 | expect(analyzer.valid?).to be(false) 75 | end 76 | end 77 | end 78 | 79 | describe "#issue" do 80 | let(:issue) { analyzer.issue } 81 | 82 | context "when valid" do 83 | let :commit do 84 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Issue: 123")]] 85 | end 86 | 87 | it "answers empty hash" do 88 | expect(issue).to eq({}) 89 | end 90 | end 91 | 92 | context "when invalid" do 93 | let :commit do 94 | Gitt::Models::Commit[ 95 | body_lines: [], 96 | trailers: [ 97 | Gitt::Models::Trailer.for("Tracker: Linear"), 98 | Gitt::Models::Trailer.for("Tracker: Linear"), 99 | Gitt::Models::Trailer.for("Issue: 123"), 100 | Gitt::Models::Trailer.for("Issue: 456"), 101 | Gitt::Models::Trailer.for("Issue: 456") 102 | ] 103 | ] 104 | end 105 | 106 | it "answers issue" do 107 | expect(issue).to eq( 108 | hint: "Avoid duplicates.", 109 | lines: [ 110 | {number: 3, content: "Tracker: Linear"}, 111 | {number: 4, content: "Tracker: Linear"}, 112 | {number: 6, content: "Issue: 456"}, 113 | {number: 7, content: "Issue: 456"} 114 | ] 115 | ) 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_format_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerFormatKey do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_format_key") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Format Key") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Format: ASCII")]] 26 | end 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with valid key and invalid value" do 34 | let :commit do 35 | Gitt::Models::Commit[ 36 | body_lines: [], 37 | trailers: [Gitt::Models::Trailer.for("Format: #\{$%}")] 38 | ] 39 | end 40 | 41 | it "answers true" do 42 | expect(analyzer.valid?).to be(true) 43 | end 44 | end 45 | 46 | context "with valid key only" do 47 | let :commit do 48 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Format:")]] 49 | end 50 | 51 | it "answers true" do 52 | expect(analyzer.valid?).to be(true) 53 | end 54 | end 55 | 56 | context "with invalid key only" do 57 | let :commit do 58 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("format:")]] 59 | end 60 | 61 | it "answers false" do 62 | expect(analyzer.valid?).to be(false) 63 | end 64 | end 65 | 66 | context "with no matching key" do 67 | let :commit do 68 | Gitt::Models::Commit[ 69 | body_lines: [], 70 | trailers: [ 71 | Gitt::Models::Trailer.for("unknown: value") 72 | ] 73 | ] 74 | end 75 | 76 | it "answers true" do 77 | expect(analyzer.valid?).to be(true) 78 | end 79 | end 80 | end 81 | 82 | describe "#issue" do 83 | let(:issue) { analyzer.issue } 84 | 85 | context "when valid" do 86 | let :commit do 87 | Gitt::Models::Commit[ 88 | body_lines: [], 89 | trailers: [ 90 | Gitt::Models::Trailer.for("Format: asciidoc") 91 | ] 92 | ] 93 | end 94 | 95 | it "answers empty hash" do 96 | expect(issue).to eq({}) 97 | end 98 | end 99 | 100 | context "when invalid" do 101 | let :commit do 102 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("format: no")]] 103 | end 104 | 105 | it "answers issue" do 106 | expect(issue).to eq( 107 | hint: "Use format: /Format/.", 108 | lines: [ 109 | { 110 | content: "format: no", 111 | number: 3 112 | } 113 | ] 114 | ) 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_format_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerFormatValue do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_format_value") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Format Value") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Format: asciidoc")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Format: Bogus")] 41 | ] 42 | end 43 | 44 | it "answers false" do 45 | expect(analyzer.valid?).to be(false) 46 | end 47 | end 48 | 49 | context "with valid key and no value" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Format:")]] 52 | end 53 | 54 | it "answers false" do 55 | expect(analyzer.valid?).to be(false) 56 | end 57 | end 58 | 59 | context "with no matching key" do 60 | let :commit do 61 | Gitt::Models::Commit[ 62 | body_lines: [], 63 | trailers: [ 64 | Gitt::Models::Trailer.for("Unknown: value") 65 | ] 66 | ] 67 | end 68 | 69 | it "answers true" do 70 | expect(analyzer.valid?).to be(true) 71 | end 72 | end 73 | end 74 | 75 | describe "#issue" do 76 | let(:issue) { analyzer.issue } 77 | 78 | context "when valid" do 79 | let :commit do 80 | Gitt::Models::Commit[ 81 | body_lines: [], 82 | trailers: [ 83 | Gitt::Models::Trailer.for("Format: asciidoc") 84 | ] 85 | ] 86 | end 87 | 88 | it "answers empty hash" do 89 | expect(issue).to eq({}) 90 | end 91 | end 92 | 93 | context "when invalid" do 94 | let :commit do 95 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Format: +*&")]] 96 | end 97 | 98 | it "answers issue" do 99 | expect(issue).to eq( 100 | hint: "Use format: /asciidoc/ or /markdown/.", 101 | lines: [ 102 | { 103 | content: "Format: +*&", 104 | number: 3 105 | } 106 | ] 107 | ) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_issue_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerIssueKey do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_issue_key") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Issue Key") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Issue: 123")]] 26 | end 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with valid key and invalid value" do 34 | let :commit do 35 | Gitt::Models::Commit[ 36 | body_lines: [], 37 | trailers: [Gitt::Models::Trailer.for("Issue: #\{$%}")] 38 | ] 39 | end 40 | 41 | it "answers true" do 42 | expect(analyzer.valid?).to be(true) 43 | end 44 | end 45 | 46 | context "with valid key only" do 47 | let :commit do 48 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Issue:")]] 49 | end 50 | 51 | it "answers true" do 52 | expect(analyzer.valid?).to be(true) 53 | end 54 | end 55 | 56 | context "with invalid key only" do 57 | let :commit do 58 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("issue:")]] 59 | end 60 | 61 | it "answers false" do 62 | expect(analyzer.valid?).to be(false) 63 | end 64 | end 65 | 66 | context "with no matching key" do 67 | let :commit do 68 | Gitt::Models::Commit[ 69 | body_lines: [], 70 | trailers: [ 71 | Gitt::Models::Trailer.for("unknown: value") 72 | ] 73 | ] 74 | end 75 | 76 | it "answers true" do 77 | expect(analyzer.valid?).to be(true) 78 | end 79 | end 80 | end 81 | 82 | describe "#issue" do 83 | let(:issue) { analyzer.issue } 84 | 85 | context "when valid" do 86 | let :commit do 87 | Gitt::Models::Commit[ 88 | body_lines: [], 89 | trailers: [ 90 | Gitt::Models::Trailer.for("Issue: 123") 91 | ] 92 | ] 93 | end 94 | 95 | it "answers empty hash" do 96 | expect(issue).to eq({}) 97 | end 98 | end 99 | 100 | context "when invalid" do 101 | let :commit do 102 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("issue: no")]] 103 | end 104 | 105 | it "answers issue" do 106 | expect(issue).to eq( 107 | hint: "Use format: /Issue/.", 108 | lines: [ 109 | { 110 | content: "issue: no", 111 | number: 3 112 | } 113 | ] 114 | ) 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_issue_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerIssueValue do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_issue_value") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Issue Value") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Issue: 123_abc-DEF")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Issue: #\{$%}")] 41 | ] 42 | end 43 | 44 | it "answers false" do 45 | expect(analyzer.valid?).to be(false) 46 | end 47 | end 48 | 49 | context "with valid key and no value" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Issue:")]] 52 | end 53 | 54 | it "answers false" do 55 | expect(analyzer.valid?).to be(false) 56 | end 57 | end 58 | 59 | context "with no matching key" do 60 | let :commit do 61 | Gitt::Models::Commit[ 62 | body_lines: [], 63 | trailers: [ 64 | Gitt::Models::Trailer.for("Unknown: value") 65 | ] 66 | ] 67 | end 68 | 69 | it "answers true" do 70 | expect(analyzer.valid?).to be(true) 71 | end 72 | end 73 | end 74 | 75 | describe "#issue" do 76 | let(:issue) { analyzer.issue } 77 | 78 | context "when valid" do 79 | let :commit do 80 | Gitt::Models::Commit[ 81 | body_lines: [], 82 | trailers: [ 83 | Gitt::Models::Trailer.for("Issue: 123") 84 | ] 85 | ] 86 | end 87 | 88 | it "answers empty hash" do 89 | expect(issue).to eq({}) 90 | end 91 | end 92 | 93 | context "when invalid" do 94 | let :commit do 95 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Issue: +*&")]] 96 | end 97 | 98 | it "answers issue" do 99 | expect(issue).to eq( 100 | hint: "Use format: /[\\w-]+/.", 101 | lines: [ 102 | { 103 | content: "Issue: +*&", 104 | number: 3 105 | } 106 | ] 107 | ) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_milestone_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerMilestoneKey do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_milestone_key") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Milestone Key") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Milestone: patch")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Milestone: #\{$%}")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with valid key only" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Milestone:")]] 52 | end 53 | 54 | it "answers true" do 55 | expect(analyzer.valid?).to be(true) 56 | end 57 | end 58 | 59 | context "with invalid key only" do 60 | let :commit do 61 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("milestone:")]] 62 | end 63 | 64 | it "answers false" do 65 | expect(analyzer.valid?).to be(false) 66 | end 67 | end 68 | 69 | context "with no matching key" do 70 | let :commit do 71 | Gitt::Models::Commit[ 72 | body_lines: [], 73 | trailers: [ 74 | Gitt::Models::Trailer.for("unknown: value") 75 | ] 76 | ] 77 | end 78 | 79 | it "answers true" do 80 | expect(analyzer.valid?).to be(true) 81 | end 82 | end 83 | end 84 | 85 | describe "#issue" do 86 | let(:issue) { analyzer.issue } 87 | 88 | context "when valid" do 89 | let :commit do 90 | Gitt::Models::Commit[ 91 | body_lines: [], 92 | trailers: [ 93 | Gitt::Models::Trailer.for("Milestone: patch") 94 | ] 95 | ] 96 | end 97 | 98 | it "answers empty hash" do 99 | expect(issue).to eq({}) 100 | end 101 | end 102 | 103 | context "when invalid" do 104 | let :commit do 105 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("milestone: no")]] 106 | end 107 | 108 | it "answers issue" do 109 | expect(issue).to eq( 110 | hint: "Use: /Milestone/.", 111 | lines: [ 112 | { 113 | content: "milestone: no", 114 | number: 3 115 | } 116 | ] 117 | ) 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_milestone_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerMilestoneValue do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_milestone_value") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Milestone Value") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Milestone: patch")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Milestone: bogus")] 41 | ] 42 | end 43 | 44 | it "answers false" do 45 | expect(analyzer.valid?).to be(false) 46 | end 47 | end 48 | 49 | context "with valid key and no value" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Milestone:")]] 52 | end 53 | 54 | it "answers false" do 55 | expect(analyzer.valid?).to be(false) 56 | end 57 | end 58 | 59 | context "with no matching key" do 60 | let :commit do 61 | Gitt::Models::Commit[ 62 | body_lines: [], 63 | trailers: [ 64 | Gitt::Models::Trailer.for("unknown: value") 65 | ] 66 | ] 67 | end 68 | 69 | it "answers true" do 70 | expect(analyzer.valid?).to be(true) 71 | end 72 | end 73 | end 74 | 75 | describe "#issue" do 76 | let(:issue) { analyzer.issue } 77 | 78 | context "when valid" do 79 | let :commit do 80 | Gitt::Models::Commit[ 81 | body_lines: [], 82 | trailers: [ 83 | Gitt::Models::Trailer.for("Milestone: patch") 84 | ] 85 | ] 86 | end 87 | 88 | it "answers empty hash" do 89 | expect(issue).to eq({}) 90 | end 91 | end 92 | 93 | context "when invalid" do 94 | let :commit do 95 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("milestone: no")]] 96 | end 97 | 98 | it "answers issue" do 99 | expect(issue).to eq( 100 | hint: "Use: /major/, /minor/, or /patch/.", 101 | lines: [ 102 | { 103 | content: "milestone: no", 104 | number: 3 105 | } 106 | ] 107 | ) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_order_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerOrder do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | let :trailers do 11 | [ 12 | Gitt::Models::Trailer.for("Co-authored-by: River Tam "), 13 | Gitt::Models::Trailer.for("Issue: 123"), 14 | Gitt::Models::Trailer.for("Milestone: patch") 15 | ] 16 | end 17 | 18 | describe ".id" do 19 | it "answers class ID" do 20 | expect(described_class.id).to eq("commit_trailer_order") 21 | end 22 | end 23 | 24 | describe ".label" do 25 | it "answers class label" do 26 | expect(described_class.label).to eq("Commit Trailer Order") 27 | end 28 | end 29 | 30 | describe "#valid?" do 31 | context "with alphabetical order" do 32 | let(:commit) { Gitt::Models::Commit[body_lines: [], trailers:] } 33 | 34 | it "answers true" do 35 | expect(analyzer.valid?).to be(true) 36 | end 37 | end 38 | 39 | context "without alphabetical order" do 40 | let(:commit) { Gitt::Models::Commit[body_lines: [], trailers: trailers.reverse] } 41 | 42 | it "answers false" do 43 | expect(analyzer.valid?).to be(false) 44 | end 45 | end 46 | end 47 | 48 | describe "#issue" do 49 | let(:issue) { analyzer.issue } 50 | 51 | context "when valid" do 52 | let(:commit) { Gitt::Models::Commit[body_lines: [], trailers:] } 53 | 54 | it "answers empty hash" do 55 | expect(issue).to eq({}) 56 | end 57 | end 58 | 59 | context "when invalid" do 60 | let(:commit) { Gitt::Models::Commit[body_lines: [], trailers: trailers.reverse] } 61 | 62 | it "answers issue" do 63 | expect(issue).to eq( 64 | hint: "Ensure keys are alphabetically sorted.", 65 | lines: [ 66 | {content: "Milestone: patch", number: 3}, 67 | {content: "Co-authored-by: River Tam ", number: 5} 68 | ] 69 | ) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_reviewer_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerReviewerKey do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_reviewer_key") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Reviewer Key") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Reviewer: test")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Reviewer: #\{$%}")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with valid key only" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Reviewer:")]] 52 | end 53 | 54 | it "answers true" do 55 | expect(analyzer.valid?).to be(true) 56 | end 57 | end 58 | 59 | context "with invalid key only" do 60 | let :commit do 61 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("reviewer:")]] 62 | end 63 | 64 | it "answers false" do 65 | expect(analyzer.valid?).to be(false) 66 | end 67 | end 68 | 69 | context "with no matching key" do 70 | let :commit do 71 | Gitt::Models::Commit[ 72 | body_lines: [], 73 | trailers: [ 74 | Gitt::Models::Trailer.for("unknown: value") 75 | ] 76 | ] 77 | end 78 | 79 | it "answers true" do 80 | expect(analyzer.valid?).to be(true) 81 | end 82 | end 83 | end 84 | 85 | describe "#issue" do 86 | let(:issue) { analyzer.issue } 87 | 88 | context "when valid" do 89 | let :commit do 90 | Gitt::Models::Commit[ 91 | body_lines: [], 92 | trailers: [ 93 | Gitt::Models::Trailer.for("Reviewer: test") 94 | ] 95 | ] 96 | end 97 | 98 | it "answers empty hash" do 99 | expect(issue).to eq({}) 100 | end 101 | end 102 | 103 | context "when invalid" do 104 | let :commit do 105 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("reviewer: no")]] 106 | end 107 | 108 | it "answers issue" do 109 | expect(issue).to eq( 110 | hint: "Use: /Reviewer/.", 111 | lines: [ 112 | { 113 | content: "reviewer: no", 114 | number: 3 115 | } 116 | ] 117 | ) 118 | end 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_reviewer_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerReviewerValue do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_reviewer_value") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Reviewer Value") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Reviewer: tana")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Reviewer: bogus")] 41 | ] 42 | end 43 | 44 | it "answers false" do 45 | expect(analyzer.valid?).to be(false) 46 | end 47 | end 48 | 49 | context "with valid key and no value" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Reviewer:")]] 52 | end 53 | 54 | it "answers false" do 55 | expect(analyzer.valid?).to be(false) 56 | end 57 | end 58 | 59 | context "with no matching key" do 60 | let :commit do 61 | Gitt::Models::Commit[ 62 | body_lines: [], 63 | trailers: [ 64 | Gitt::Models::Trailer.for("unknown: value") 65 | ] 66 | ] 67 | end 68 | 69 | it "answers true" do 70 | expect(analyzer.valid?).to be(true) 71 | end 72 | end 73 | end 74 | 75 | describe "#issue" do 76 | let(:issue) { analyzer.issue } 77 | 78 | context "when valid" do 79 | let :commit do 80 | Gitt::Models::Commit[ 81 | body_lines: [], 82 | trailers: [ 83 | Gitt::Models::Trailer.for("Reviewer: tana") 84 | ] 85 | ] 86 | end 87 | 88 | it "answers empty hash" do 89 | expect(issue).to eq({}) 90 | end 91 | end 92 | 93 | context "when invalid" do 94 | let :commit do 95 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("reviewer: no")]] 96 | end 97 | 98 | it "answers issue" do 99 | expect(issue).to eq( 100 | hint: "Use: /clickup/, /github/, /jira/, /linear/, /shortcut/, or /tana/.", 101 | lines: [ 102 | { 103 | content: "reviewer: no", 104 | number: 3 105 | } 106 | ] 107 | ) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_signer_capitalization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerSignerCapitalization do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_signer_capitalization") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Signer Capitalization") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with no matching key" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Unknown: value")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid capitalization" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: Test Example ")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with invalid capitalization" do 50 | let :commit do 51 | Gitt::Models::Commit[ 52 | body_lines: [], 53 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: test ")] 54 | ] 55 | end 56 | 57 | it "answers false" do 58 | expect(analyzer.valid?).to be(false) 59 | end 60 | end 61 | 62 | context "with missing name" do 63 | let :commit do 64 | Gitt::Models::Commit[ 65 | body_lines: [], 66 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: ")] 67 | ] 68 | end 69 | 70 | it "answers true" do 71 | expect(analyzer.valid?).to be(true) 72 | end 73 | end 74 | end 75 | 76 | describe "#issue" do 77 | let(:issue) { analyzer.issue } 78 | 79 | context "when valid" do 80 | let :commit do 81 | Gitt::Models::Commit[ 82 | body_lines: [], 83 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: Test Example ")] 84 | ] 85 | end 86 | 87 | it "answers empty hash" do 88 | expect(issue).to eq({}) 89 | end 90 | end 91 | 92 | context "when invalid" do 93 | let :commit do 94 | Gitt::Models::Commit[ 95 | body_lines: [], 96 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: test Example ")] 97 | ] 98 | end 99 | 100 | it "answers issue" do 101 | expect(issue).to eq( 102 | hint: "Name must be capitalized.", 103 | lines: [ 104 | { 105 | content: "Signed-off-by: test Example ", 106 | number: 3 107 | } 108 | ] 109 | ) 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_signer_email_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerSignerEmail do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_signer_email") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Signer Email") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "when valid" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Signed-By: Test Example ")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with no matching key" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Unknown: value")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with malformed email address" do 50 | let :commit do 51 | Gitt::Models::Commit[ 52 | body_lines: [], 53 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: Test Example ")] 54 | ] 55 | end 56 | 57 | it "answers false" do 58 | expect(analyzer.valid?).to be(false) 59 | end 60 | end 61 | end 62 | 63 | describe "#issue" do 64 | let(:issue) { analyzer.issue } 65 | 66 | context "when valid" do 67 | let :commit do 68 | Gitt::Models::Commit[ 69 | body_lines: [], 70 | trailers: [Gitt::Models::Trailer.for("Signed-By: Test Example ")] 71 | ] 72 | end 73 | 74 | it "answers empty hash" do 75 | expect(issue).to eq({}) 76 | end 77 | end 78 | 79 | context "when invalid" do 80 | let :commit do 81 | Gitt::Models::Commit[ 82 | body_lines: [], 83 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: Test Example ")] 84 | ] 85 | end 86 | 87 | it "answers issue" do 88 | expect(issue).to eq( 89 | hint: %(Email must follow name and use format: "".), 90 | lines: [ 91 | { 92 | content: "Signed-off-by: Test Example ", 93 | number: 3 94 | } 95 | ] 96 | ) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_signer_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerSignerKey do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_signer_key") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Signer Key") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "when valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: Test Example ")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: bogus")] 41 | ] 42 | end 43 | 44 | it "answers true" do 45 | expect(analyzer.valid?).to be(true) 46 | end 47 | end 48 | 49 | context "with valid key only" do 50 | let :commit do 51 | Gitt::Models::Commit[ 52 | body_lines: [], 53 | trailers: [Gitt::Models::Trailer.for("Signed-off-by:")] 54 | ] 55 | end 56 | 57 | it "answers true" do 58 | expect(analyzer.valid?).to be(true) 59 | end 60 | end 61 | 62 | context "with invalid key only" do 63 | let :commit do 64 | Gitt::Models::Commit[ 65 | body_lines: [], 66 | trailers: [Gitt::Models::Trailer.for("signed-off-by:")] 67 | ] 68 | end 69 | 70 | it "answers false" do 71 | expect(analyzer.valid?).to be(false) 72 | end 73 | end 74 | 75 | context "with no matching key" do 76 | let :commit do 77 | Gitt::Models::Commit[ 78 | body_lines: [], 79 | trailers: [Gitt::Models::Trailer.for("unknown: value")] 80 | ] 81 | end 82 | 83 | it "answers true" do 84 | expect(analyzer.valid?).to be(true) 85 | end 86 | end 87 | end 88 | 89 | describe "#issue" do 90 | let(:issue) { analyzer.issue } 91 | 92 | context "when valid" do 93 | let :commit do 94 | Gitt::Models::Commit[ 95 | body_lines: [], 96 | trailers: [Gitt::Models::Trailer.for("Signed-off-by: Test Example ")] 97 | ] 98 | end 99 | 100 | it "answers empty hash" do 101 | expect(issue).to eq({}) 102 | end 103 | end 104 | 105 | context "when invalid" do 106 | let :commit do 107 | Gitt::Models::Commit[ 108 | body_lines: [], 109 | trailers: [Gitt::Models::Trailer.for("signed-off-by: Test Example ")] 110 | ] 111 | end 112 | 113 | it "answers issue" do 114 | expect(issue).to eq( 115 | hint: "Use format: /Signed-off-by/.", 116 | lines: [ 117 | { 118 | content: "signed-off-by: Test Example ", 119 | number: 3 120 | } 121 | ] 122 | ) 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_tracker_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerTrackerKey do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_tracker_key") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Tracker Key") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Tracker: acme")]] 26 | end 27 | 28 | it "answers true" do 29 | expect(analyzer.valid?).to be(true) 30 | end 31 | end 32 | 33 | context "with valid key and invalid value" do 34 | let :commit do 35 | Gitt::Models::Commit[ 36 | body_lines: [], 37 | trailers: [Gitt::Models::Trailer.for("Tracker: #\{$%}")] 38 | ] 39 | end 40 | 41 | it "answers true" do 42 | expect(analyzer.valid?).to be(true) 43 | end 44 | end 45 | 46 | context "with valid key only" do 47 | let :commit do 48 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Tracker:")]] 49 | end 50 | 51 | it "answers true" do 52 | expect(analyzer.valid?).to be(true) 53 | end 54 | end 55 | 56 | context "with invalid key only" do 57 | let :commit do 58 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("tracker:")]] 59 | end 60 | 61 | it "answers false" do 62 | expect(analyzer.valid?).to be(false) 63 | end 64 | end 65 | 66 | context "with no matching key" do 67 | let :commit do 68 | Gitt::Models::Commit[ 69 | body_lines: [], 70 | trailers: [ 71 | Gitt::Models::Trailer.for("unknown: value") 72 | ] 73 | ] 74 | end 75 | 76 | it "answers true" do 77 | expect(analyzer.valid?).to be(true) 78 | end 79 | end 80 | end 81 | 82 | describe "#issue" do 83 | let(:issue) { analyzer.issue } 84 | 85 | context "when valid" do 86 | let :commit do 87 | Gitt::Models::Commit[ 88 | body_lines: [], 89 | trailers: [ 90 | Gitt::Models::Trailer.for("Tracker: acme") 91 | ] 92 | ] 93 | end 94 | 95 | it "answers empty hash" do 96 | expect(issue).to eq({}) 97 | end 98 | end 99 | 100 | context "when invalid" do 101 | let :commit do 102 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("tracker: no")]] 103 | end 104 | 105 | it "answers issue" do 106 | expect(issue).to eq( 107 | hint: "Use format: /Tracker/.", 108 | lines: [ 109 | { 110 | content: "tracker: no", 111 | number: 3 112 | } 113 | ] 114 | ) 115 | end 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/lib/git/lint/analyzers/commit_trailer_tracker_value_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Analyzers::CommitTrailerTrackerValue do 6 | subject(:analyzer) { described_class.new commit } 7 | 8 | include_context "with application dependencies" 9 | 10 | describe ".id" do 11 | it "answers class ID" do 12 | expect(described_class.id).to eq("commit_trailer_tracker_value") 13 | end 14 | end 15 | 16 | describe ".label" do 17 | it "answers class label" do 18 | expect(described_class.label).to eq("Commit Trailer Tracker Value") 19 | end 20 | end 21 | 22 | describe "#valid?" do 23 | context "with valid key and value" do 24 | let :commit do 25 | Gitt::Models::Commit[ 26 | body_lines: [], 27 | trailers: [Gitt::Models::Trailer.for("Tracker: ACME-DC0 Issues")] 28 | ] 29 | end 30 | 31 | it "answers true" do 32 | expect(analyzer.valid?).to be(true) 33 | end 34 | end 35 | 36 | context "with valid key and invalid value" do 37 | let :commit do 38 | Gitt::Models::Commit[ 39 | body_lines: [], 40 | trailers: [Gitt::Models::Trailer.for("Tracker: #\{$%}")] 41 | ] 42 | end 43 | 44 | it "answers false" do 45 | expect(analyzer.valid?).to be(false) 46 | end 47 | end 48 | 49 | context "with valid key and no value" do 50 | let :commit do 51 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Tracker:")]] 52 | end 53 | 54 | it "answers false" do 55 | expect(analyzer.valid?).to be(false) 56 | end 57 | end 58 | 59 | context "with no matching key" do 60 | let :commit do 61 | Gitt::Models::Commit[ 62 | body_lines: [], 63 | trailers: [ 64 | Gitt::Models::Trailer.for("Unknown: value") 65 | ] 66 | ] 67 | end 68 | 69 | it "answers true" do 70 | expect(analyzer.valid?).to be(true) 71 | end 72 | end 73 | end 74 | 75 | describe "#issue" do 76 | let(:issue) { analyzer.issue } 77 | 78 | context "when valid" do 79 | let :commit do 80 | Gitt::Models::Commit[ 81 | body_lines: [], 82 | trailers: [ 83 | Gitt::Models::Trailer.for("Tracker: ACME") 84 | ] 85 | ] 86 | end 87 | 88 | it "answers empty hash" do 89 | expect(issue).to eq({}) 90 | end 91 | end 92 | 93 | context "when invalid" do 94 | let :commit do 95 | Gitt::Models::Commit[body_lines: [], trailers: [Gitt::Models::Trailer.for("Tracker: +*&")]] 96 | end 97 | 98 | it "answers issue" do 99 | expect(issue).to eq( 100 | hint: "Use format: /[\\w\\-\\s]+/.", 101 | lines: [ 102 | { 103 | content: "Tracker: +*&", 104 | number: 3 105 | } 106 | ] 107 | ) 108 | end 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/lib/git/lint/cli/actions/analyze/branch_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::CLI::Actions::Analyze::Branch do 6 | using Refinements::Pathname 7 | using Refinements::StringIO 8 | 9 | subject(:action) { described_class.new } 10 | 11 | include_context "with application dependencies" 12 | include_context "with Git repository" 13 | 14 | describe "#call" do 15 | it "reports no issues with valid commits" do 16 | git_repo_dir.change_dir do 17 | `git switch --quiet --create test` 18 | `touch test.txt && git add .` 19 | `git commit --no-verify --message "Added test file" --message "For testing purposes."` 20 | 21 | action.call 22 | 23 | expect(io.reread).to match(/1 commit inspected.*0 issues.+detected/m) 24 | end 25 | end 26 | 27 | it "reports issues with invalid commits" do 28 | git_repo_dir.change_dir do 29 | `git switch --quiet --create test` 30 | `touch test.txt && git add .` 31 | `git commit --no-verify --message "Add test file"` 32 | 33 | action.call 34 | 35 | expect(io.reread).to match( 36 | /Commit Subject Prefix Error.+1 commit inspected.*1 issue.+detected/m 37 | ) 38 | end 39 | end 40 | 41 | it "aborts with invalid commits" do 42 | git_repo_dir.change_dir do 43 | `git switch --quiet --create test` 44 | `touch test.txt && git add .` 45 | `git commit --no-verify --message "Add test file"` 46 | 47 | action.call 48 | 49 | expect(kernel).to have_received(:abort) 50 | end 51 | end 52 | 53 | context "with failure" do 54 | subject(:action) { described_class.new analyzer: } 55 | 56 | let(:analyzer) { instance_double Git::Lint::Analyzer } 57 | 58 | before { allow(analyzer).to receive(:call).and_raise(Git::Lint::Errors::Base, "Danger!") } 59 | 60 | it "logs error" do 61 | action.call 62 | expect(logger.reread).to match(/🛑.+Danger!/) 63 | end 64 | 65 | it "aborts" do 66 | action.call 67 | expect(kernel).to have_received(:abort) 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/lib/git/lint/cli/actions/analyze/commit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::CLI::Actions::Analyze::Commit do 6 | using Refinements::Pathname 7 | using Refinements::StringIO 8 | 9 | subject(:action) { described_class.new } 10 | 11 | include_context "with application dependencies" 12 | include_context "with Git repository" 13 | 14 | describe "#call" do 15 | it "reports zero issues with valid SHA" do 16 | git_repo_dir.change_dir do 17 | `git switch --quiet --create test` 18 | `touch b.txt && git add . && git commit --no-verify --message "Added A" --message "Test."` 19 | 20 | action.call "-1" 21 | 22 | expect(io.reread).to match(/.+1 commit inspected.*0 issues.+detected/m) 23 | end 24 | end 25 | 26 | it "reports issue with invalid SHA" do 27 | git_repo_dir.change_dir do 28 | `git switch --quiet --create test` 29 | `touch a.txt && git add . && git commit --no-verify --message "Add A"` 30 | 31 | action.call "-1" 32 | 33 | expect(io.reread).to match( 34 | /Commit Subject Prefix Error.+1 commit inspected.*1 issue.+detected/m 35 | ) 36 | end 37 | end 38 | 39 | it "reports zero issues when given no SHAs" do 40 | git_repo_dir.change_dir do 41 | `rm -rf .git && git init` 42 | action.call 43 | expect(io.reread).to match(/.+0 commits inspected.*0 issues.+detected/m) 44 | end 45 | end 46 | 47 | it "aborts with invalid SHA" do 48 | git_repo_dir.change_dir do 49 | `git switch --quiet --create test` 50 | `touch test.txt && git add . && git commit --no-verify --message "Add test file"` 51 | sha = `git log --pretty=format:%h -1` 52 | 53 | action.call sha 54 | 55 | expect(kernel).to have_received(:abort) 56 | end 57 | end 58 | 59 | context "with failure" do 60 | subject(:action) { described_class.new analyzer: } 61 | 62 | let(:analyzer) { instance_double Git::Lint::Analyzer } 63 | 64 | before { allow(analyzer).to receive(:call).and_raise(Git::Lint::Errors::Base, "Danger!") } 65 | 66 | it "logs error" do 67 | action.call 68 | expect(logger.reread).to match(/🛑.+Danger!/) 69 | end 70 | 71 | it "aborts" do 72 | action.call 73 | expect(kernel).to have_received(:abort) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/lib/git/lint/cli/actions/hook_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::CLI::Actions::Hook do 6 | using Refinements::Pathname 7 | using Refinements::StringIO 8 | 9 | subject(:action) { described_class.new } 10 | 11 | include_context "with application dependencies" 12 | include_context "with Git repository" 13 | 14 | describe "#call" do 15 | it "answers valid commit results" do 16 | git_repo_dir.change_dir do 17 | action.call SPEC_ROOT.join("support/fixtures/commit-valid.txt") 18 | expect(io.reread).to match(/1 commit.+0 issues/m) 19 | end 20 | end 21 | 22 | it "answers invalid commit results" do 23 | git_repo_dir.change_dir do 24 | action.call SPEC_ROOT.join("support/fixtures/commit-invalid.txt") 25 | expect(io.reread).to match(/1 commit.+2 issues/m) 26 | end 27 | end 28 | 29 | it "aborts with errors" do 30 | git_repo_dir.change_dir do 31 | action.call SPEC_ROOT.join("support/fixtures/commit-invalid.txt") 32 | expect(kernel).to have_received(:abort) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/lib/git/lint/cli/shell_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::CLI::Shell do 6 | using Refinements::Pathname 7 | using Refinements::StringIO 8 | 9 | subject(:shell) { described_class.new } 10 | 11 | include_context "with Git repository" 12 | include_context "with application dependencies" 13 | 14 | before { Sod::Container.stub! logger:, io: } 15 | 16 | after { Sod::Container.restore } 17 | 18 | describe "#call" do 19 | it "prints configuration usage" do 20 | shell.call %w[config] 21 | expect(io.reread).to match(/Manage configuration.+/m) 22 | end 23 | 24 | it "analyzes feature branch with valid commits and reports no issues" do 25 | git_repo_dir.change_dir do 26 | `git switch --quiet --create test` 27 | `touch test.txt` 28 | `git add .` 29 | `git commit --no-verify --message "Added test file"` 30 | 31 | shell.call %w[analyze --branch] 32 | 33 | expect(io.reread).to match(/1 commit inspected.*0 issues.+detected/m) 34 | end 35 | end 36 | 37 | it "analyzes feature branch with invalid commits and reports issues" do 38 | git_repo_dir.change_dir do 39 | `git switch --quiet --create test` 40 | `touch test.txt` 41 | `git add .` 42 | `git commit --no-verify --message "Add test file"` 43 | 44 | shell.call %w[analyze --branch] 45 | 46 | expect(io.reread).to match( 47 | /Commit Subject Prefix Error.+1 commit inspected.*1 issue.+detected/m 48 | ) 49 | end 50 | end 51 | 52 | it "analyzes feature branch with valid SHA and reports no issues" do 53 | git_repo_dir.change_dir do 54 | `git switch --quiet --create test` 55 | `touch test.txt` 56 | `git add .` 57 | `git commit --no-verify --message "Added test file"` 58 | sha = `git log --pretty=format:%h -1` 59 | 60 | shell.call ["analyze", "--commit", sha] 61 | 62 | expect(io.reread).to match(/1 commit inspected.*0 issues.+detected/m) 63 | end 64 | end 65 | 66 | it "analyzes feature branch with invalid SHA and reports issue" do 67 | git_repo_dir.change_dir do 68 | `git switch --quiet --create test` 69 | `touch test.txt` 70 | `git add .` 71 | `git commit --no-verify --message "Add test"` 72 | sha = `git log --pretty=format:%h -1` 73 | 74 | shell.call ["analyze", "--commit", sha] 75 | 76 | expect(io.reread).to match(/1 commit inspected.*1 issue.+detected/m) 77 | end 78 | end 79 | 80 | it "analyzes hook for valid commit" do 81 | shell.call ["--hook", SPEC_ROOT.join("support/fixtures/commit-valid.txt").to_s] 82 | expect(io.reread).to match(/1 commit inspected.*0 issues.+detected/m) 83 | end 84 | 85 | it "analyzes hook for invalid commit" do 86 | shell.call ["--hook", SPEC_ROOT.join("support/fixtures/commit-invalid.txt").to_s] 87 | expect(io.reread).to match(/1 commit inspected.+2 issues.+0 warnings.+2 errors/m) 88 | end 89 | 90 | it "prints version" do 91 | shell.call %w[--version] 92 | expect(io.reread).to match(/Git Lint\s\d+\.\d+\.\d+/) 93 | end 94 | 95 | it "prints help" do 96 | shell.call %w[--help] 97 | expect(io.reread).to match(/Git Lint.+USAGE.+/m) 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/lib/git/lint/commits/hosts/circle_ci_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Commits::Hosts::CircleCI do 6 | subject(:host) { described_class.new git: } 7 | 8 | include_context "with host dependencies" 9 | 10 | describe "#call" do 11 | it "uses specific start and finish range" do 12 | host.call 13 | expect(git).to have_received(:commits).with("origin/main..origin/test") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/git/lint/commits/hosts/git_hub_action_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Commits::Hosts::GitHubAction do 6 | subject(:host) { described_class.new git: } 7 | 8 | include_context "with host dependencies" 9 | 10 | describe "#call" do 11 | it "uses specific start and finish range" do 12 | host.call 13 | expect(git).to have_received(:commits).with("origin/main..origin/test") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/git/lint/commits/hosts/local_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Commits::Hosts::Local do 6 | subject(:host) { described_class.new git: } 7 | 8 | include_context "with host dependencies" 9 | 10 | describe "#call" do 11 | it "uses specific start and finish range" do 12 | host.call 13 | expect(git).to have_received(:commits).with("main..test") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/lib/git/lint/commits/hosts/netlify_ci_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Commits::Hosts::NetlifyCI do 6 | subject(:host) { described_class.new git:, environment: } 7 | 8 | include_context "with host dependencies" 9 | 10 | let(:environment) { {"HEAD" => "test", "REPOSITORY_URL" => "https://www.example.com/test.git"} } 11 | 12 | describe "#call" do 13 | it "adds remote origin branch" do 14 | host.call 15 | 16 | expect(git).to have_received(:call).with( 17 | "remote", "add", "-f", "origin", "https://www.example.com/test.git" 18 | ) 19 | end 20 | 21 | it "fetches feature branch" do 22 | host.call 23 | expect(git).to have_received(:call).with("fetch", "origin", "test:test") 24 | end 25 | 26 | it "uses specific start and finish range" do 27 | host.call 28 | expect(git).to have_received(:commits).with("origin/main..origin/test") 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/lib/git/lint/commits/loader_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Commits::Loader do 6 | using Refinements::Pathname 7 | 8 | subject(:loader) { described_class.new git:, environment: } 9 | 10 | include_context "with Git repository" 11 | 12 | let :git do 13 | instance_spy Gitt::Repository, 14 | call: Success(), 15 | branch_default: Success("main"), 16 | branch_name: Success("test") 17 | end 18 | 19 | before { Git::Lint::Container.stub! git: } 20 | 21 | after { Git::Lint::Container.restore } 22 | 23 | describe "#call" do 24 | context "with Circle CI" do 25 | let :environment do 26 | { 27 | "CIRCLECI" => "true", 28 | "GITHUB_ACTIONS" => "false", 29 | "NETLIFY" => "false", 30 | "TRAVIS" => "false" 31 | } 32 | end 33 | 34 | it "computes correct commit range" do 35 | loader.call 36 | expect(git).to have_received(:commits).with("origin/main..origin/test") 37 | end 38 | end 39 | 40 | context "with GitHub Actions" do 41 | let :environment do 42 | { 43 | "CIRCLECI" => "false", 44 | "GITHUB_ACTIONS" => "true", 45 | "NETLIFY" => "false", 46 | "TRAVIS" => "false" 47 | } 48 | end 49 | 50 | it "computes correct commit range" do 51 | loader.call 52 | expect(git).to have_received(:commits).with("origin/main..origin/test") 53 | end 54 | end 55 | 56 | context "with Netlify CI" do 57 | let :environment do 58 | { 59 | "CIRCLECI" => "false", 60 | "GITHUB_ACTIONS" => "false", 61 | "NETLIFY" => "true", 62 | "HEAD" => "test", 63 | "TRAVIS" => "false" 64 | } 65 | end 66 | 67 | it "computes correct commit range" do 68 | loader.call 69 | expect(git).to have_received(:commits).with("origin/main..origin/test") 70 | end 71 | end 72 | 73 | context "with local host" do 74 | let(:git) { Gitt::Repository.new } 75 | let(:environment) { Hash.new } 76 | 77 | before do 78 | git_repo_dir.change_dir do 79 | `git switch --quiet --create test --track` 80 | `touch test.txt && git add . && git commit --message "Added test file"` 81 | end 82 | end 83 | 84 | it "answers commits" do 85 | git_repo_dir.change_dir do 86 | expect(loader.call.success.map(&:subject)).to contain_exactly("Added test file") 87 | end 88 | end 89 | end 90 | 91 | context "when Git repository doesn't exist" do 92 | let(:git) { instance_spy Gitt::Repository, exist?: false } 93 | let(:environment) { Hash.new } 94 | 95 | it "fails with base error" do 96 | expectation = -> { loader.call } 97 | 98 | expect(&expectation).to raise_error( 99 | Git::Lint::Errors::Base, 100 | "Invalid repository. Are you within a Git repository?" 101 | ) 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/lib/git/lint/errors/base_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Errors::Base do 6 | subject(:error) { described_class.new } 7 | 8 | describe "#message" do 9 | it "answers default message" do 10 | expect(error.message).to eq("Invalid Git Lint action.") 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/lib/git/lint/errors/severity_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Errors::Severity do 6 | describe "#message" do 7 | subject(:error) { described_class.new :bogus } 8 | 9 | it "answers default message" do 10 | expect(error.message).to eq(%(Invalid severity level: bogus. Use: "warn" or "error".)) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/lib/git/lint/errors/sha_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Errors::SHA do 6 | describe "#message" do 7 | subject(:sha_error) { described_class.new "bogus" } 8 | 9 | it "answers default message" do 10 | expect(sha_error.message).to eq( 11 | %(Invalid commit SHA: "bogus". Unable to obtain commit details.) 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/lib/git/lint/kit/filter_list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Kit::FilterList do 6 | subject(:filter_list) { described_class.new list } 7 | 8 | describe "#empty?" do 9 | context "when empty" do 10 | let(:list) { [] } 11 | 12 | it "answers true" do 13 | expect(filter_list.empty?).to be(true) 14 | end 15 | end 16 | 17 | context "when not empty" do 18 | let(:list) { ["test"] } 19 | 20 | it "answers false" do 21 | expect(filter_list.empty?).to be(false) 22 | end 23 | end 24 | end 25 | 26 | shared_context "with array" do |method| 27 | context "with list of strings" do 28 | let(:list) { %w[one two three] } 29 | 30 | it "answers regular expression array" do 31 | expect(filter_list.public_send(method)).to contain_exactly(/one/, /two/, /three/) 32 | end 33 | end 34 | 35 | context "with list of regular expressions" do 36 | let(:list) { ["\\.", "\\Atest.+"] } 37 | 38 | it "answers regular expression array" do 39 | expect(filter_list.public_send(method)).to contain_exactly(/\./, /\Atest.+/) 40 | end 41 | end 42 | 43 | context "without list" do 44 | let(:list) { [] } 45 | 46 | it "answers empty array" do 47 | expect(filter_list.public_send(method)).to eq([]) 48 | end 49 | end 50 | end 51 | 52 | describe "#to_a" do 53 | it_behaves_like "with array", :to_a 54 | end 55 | 56 | describe "#to_ary" do 57 | it_behaves_like "with array", :to_ary 58 | end 59 | 60 | describe "#to_usage" do 61 | context "with list" do 62 | let(:list) { ["one", "\\.", "\\Atest.+"] } 63 | 64 | it "answers list as string" do 65 | expect(filter_list.to_usage).to eq(%(/one/, /\\./, and /\\Atest.+/)) 66 | end 67 | end 68 | 69 | context "without list" do 70 | let(:list) { [] } 71 | 72 | it "answers list as empty string" do 73 | expect(filter_list.to_usage).to eq("") 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/lib/git/lint/rake/register_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "git/lint/rake/register" 4 | require "spec_helper" 5 | 6 | RSpec.describe Git::Lint::Rake::Register do 7 | subject(:tasks) { described_class.new shell: } 8 | 9 | let(:shell) { instance_spy Git::Lint::CLI::Shell } 10 | 11 | before { Rake::Task.clear } 12 | 13 | describe ".setup" do 14 | it "installs rake tasks" do 15 | described_class.call 16 | expect(Rake::Task.tasks.map(&:name)).to contain_exactly("git_lint") 17 | end 18 | end 19 | 20 | describe "#call" do 21 | before { tasks.call } 22 | 23 | it "executes --analyze option via git_lint task" do 24 | Rake::Task["git_lint"].invoke 25 | expect(shell).to have_received(:call).with(%w[analyze --branch]) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/lib/git/lint/reporters/branch_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Reporters::Branch do 6 | subject(:reporter) { described_class.new collector: } 7 | 8 | include_context "with Git commit" 9 | 10 | let(:collector) { Git::Lint::Collector.new } 11 | 12 | describe "#to_s" do 13 | let :analyzer do 14 | instance_spy Git::Lint::Analyzers::CommitAuthorEmail, 15 | class: class_spy( 16 | Git::Lint::Analyzers::CommitAuthorEmail, 17 | label: "Commit Author Email" 18 | ), 19 | commit: git_commit, 20 | severity: "warn", 21 | invalid?: true, 22 | warning?: true, 23 | error?: false, 24 | issue: {hint: "A test hint."} 25 | end 26 | 27 | context "with warnings" do 28 | before { collector.add analyzer } 29 | 30 | it "answers detected issues" do 31 | expect(reporter.to_s).to eq( 32 | "Running Git Lint...\n" \ 33 | "\n" \ 34 | "180dec7d8ae8cbe3565a727c63c2111e49e0b737 (Test User, 1 day ago): Added documentation\n" \ 35 | "\e[33m Commit Author Email Warning. A test hint.\n\e[0m" \ 36 | "\n" \ 37 | "1 commit inspected. \e[33m1 issue\e[0m detected " \ 38 | "(\e[33m1 warning\e[0m, \e[32m0 errors\e[0m).\n" 39 | ) 40 | end 41 | end 42 | 43 | context "with errors" do 44 | let :analyzer do 45 | instance_spy Git::Lint::Analyzers::CommitAuthorEmail, 46 | class: class_spy( 47 | Git::Lint::Analyzers::CommitAuthorEmail, 48 | label: "Commit Author Email" 49 | ), 50 | commit: git_commit, 51 | severity: "error", 52 | invalid?: true, 53 | warning?: false, 54 | error?: true, 55 | issue: {hint: "A test hint."} 56 | end 57 | 58 | before do 59 | collector.add analyzer 60 | collector.add analyzer 61 | end 62 | 63 | it "answers detected issues" do 64 | expect(reporter.to_s).to eq( 65 | "Running Git Lint...\n" \ 66 | "\n" \ 67 | "180dec7d8ae8cbe3565a727c63c2111e49e0b737 (Test User, 1 day ago): Added documentation\n" \ 68 | "\e[31m Commit Author Email Error. A test hint.\n\e[0m" \ 69 | "\e[31m Commit Author Email Error. A test hint.\n\e[0m" \ 70 | "\n" \ 71 | "1 commit inspected. \e[31m2 issues\e[0m detected " \ 72 | "(\e[32m0 warnings\e[0m, \e[31m2 errors\e[0m).\n" 73 | ) 74 | end 75 | end 76 | 77 | context "without issues" do 78 | it "answers zero detected issues" do 79 | expect(reporter.to_s).to eq( 80 | "Running Git Lint...\n" \ 81 | "0 commits inspected. \e[32m0 issues\e[0m detected.\n" 82 | ) 83 | end 84 | end 85 | end 86 | 87 | describe "#to_str" do 88 | it "answers implicit string" do 89 | expect(reporter.to_str).to eq( 90 | "Running Git Lint...\n" \ 91 | "0 commits inspected. \e[32m0 issues\e[0m detected.\n" 92 | ) 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/lib/git/lint/reporters/commit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Reporters::Commit do 6 | subject(:reporter) { described_class.new commit: git_commit, analyzers: } 7 | 8 | include_context "with Git commit" 9 | 10 | let(:analyzers) { [analyzer] } 11 | 12 | let :analyzer do 13 | instance_spy Git::Lint::Analyzers::CommitAuthorEmail, 14 | class: class_spy( 15 | Git::Lint::Analyzers::CommitAuthorEmail, 16 | label: "Commit Author Email" 17 | ), 18 | severity: "warn", 19 | invalid?: invalid, 20 | issue: {hint: "A test hint."} 21 | end 22 | 23 | describe "#to_s" do 24 | context "with invalid analyzer" do 25 | subject(:reporter) { described_class.new commit: git_commit, analyzers: } 26 | 27 | let(:invalid) { true } 28 | 29 | it "answers commit (SHA, author name, relative time, subject) and single analyzer report" do 30 | expect(reporter.to_s).to eq( 31 | "180dec7d8ae8cbe3565a727c63c2111e49e0b737 (Test User, 1 day ago): " \ 32 | "Added documentation\n" \ 33 | "\e[33m Commit Author Email Warning. A test hint.\n\e[0m" \ 34 | "\n" 35 | ) 36 | end 37 | end 38 | 39 | context "with invalid analyzers" do 40 | subject(:reporter) { described_class.new commit: git_commit, analyzers: [analyzer, analyzer] } 41 | 42 | let(:invalid) { true } 43 | 44 | it "answers commit (SHA, author name, relative time, subject) and multiple analyzer report" do 45 | expect(reporter.to_s).to eq( 46 | "180dec7d8ae8cbe3565a727c63c2111e49e0b737 (Test User, 1 day ago): " \ 47 | "Added documentation\n" \ 48 | "\e[33m Commit Author Email Warning. A test hint.\n\e[0m" \ 49 | "\e[33m Commit Author Email Warning. A test hint.\n\e[0m" \ 50 | "\n" 51 | ) 52 | end 53 | end 54 | 55 | context "with valid analyzers" do 56 | subject(:reporter) { described_class.new commit: git_commit, analyzers: [analyzer, analyzer] } 57 | 58 | let(:invalid) { false } 59 | 60 | it "empty string" do 61 | expect(reporter.to_s).to eq("") 62 | end 63 | end 64 | end 65 | 66 | describe "#to_str" do 67 | let(:invalid) { true } 68 | 69 | it "answers implicit string" do 70 | expect(reporter.to_str).to eq( 71 | "180dec7d8ae8cbe3565a727c63c2111e49e0b737 (Test User, 1 day ago): " \ 72 | "Added documentation\n" \ 73 | "\e[33m Commit Author Email Warning. A test hint.\n\e[0m" \ 74 | "\n" 75 | ) 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/lib/git/lint/reporters/line_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Reporters::Line do 6 | subject(:reporter) { described_class.new line } 7 | 8 | describe "#to_s" do 9 | context "with sentence" do 10 | let(:line) { {number: 1, content: "Example."} } 11 | 12 | it "answers non-indented content" do 13 | expect(reporter.to_s).to eq(%( Line 1: "Example."\n)) 14 | end 15 | end 16 | 17 | context "with paragraph" do 18 | let(:line) { {number: 1, content: "One.\nTwo.\nThree."} } 19 | 20 | it "answers indented multi-line content" do 21 | expect(reporter.to_s).to eq( 22 | %( Line 1: "One.\n) + 23 | %( Two.\n) + 24 | %( Three."\n) 25 | ) 26 | end 27 | end 28 | end 29 | 30 | describe "#to_str" do 31 | let(:line) { {number: 1, content: "Example."} } 32 | 33 | it "answers implicit string" do 34 | expect(reporter.to_str).to eq(%( Line 1: "Example."\n)) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/lib/git/lint/reporters/lines/paragraph_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Reporters::Lines::Paragraph do 6 | subject(:reporter) { described_class.new data } 7 | 8 | let(:data) { {number: 1, content: "One.\nTwo.\nThree."} } 9 | let(:proof) { %( Line 1: "One.\n) + %( Two.\n) + %( Three."\n) } 10 | 11 | describe "#to_s" do 12 | it "answers label and paragraph" do 13 | expect(reporter.to_s).to eq(proof) 14 | end 15 | end 16 | 17 | describe "#to_str" do 18 | it "answers implicit string" do 19 | expect(reporter.to_str).to eq(proof) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/git/lint/reporters/lines/sentence_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Reporters::Lines::Sentence do 6 | subject(:reporter) { described_class.new data } 7 | 8 | let(:data) { {number: 1, content: "Example content."} } 9 | let(:proof) { %( Line 1: "Example content."\n) } 10 | 11 | describe "#to_s" do 12 | it "answers label and sentence" do 13 | expect(reporter.to_s).to eq(proof) 14 | end 15 | end 16 | 17 | describe "#to_str" do 18 | it "answers implicit string" do 19 | expect(reporter.to_str).to eq(proof) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/git/lint/reporters/style_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Reporters::Style do 6 | subject(:reporter) { described_class.new analyzer } 7 | 8 | let :analyzer do 9 | instance_spy Git::Lint::Analyzers::CommitAuthorEmail, 10 | class: class_spy( 11 | Git::Lint::Analyzers::CommitAuthorEmail, 12 | label: "Commit Author Email" 13 | ), 14 | severity:, 15 | issue: 16 | end 17 | 18 | let(:severity) { "error" } 19 | let(:issue) { {hint: "A test hint."} } 20 | 21 | describe "#to_s" do 22 | context "with warning" do 23 | let(:severity) { "warn" } 24 | 25 | it "answers analyzer label and issue hint" do 26 | expect(reporter.to_s).to eq("\e[33m Commit Author Email Warning. A test hint.\n\e[0m") 27 | end 28 | end 29 | 30 | context "with error" do 31 | let(:severity) { "error" } 32 | 33 | it "answers analyzer label and issue hint" do 34 | expect(reporter.to_s).to eq("\e[31m Commit Author Email Error. A test hint.\n\e[0m") 35 | end 36 | end 37 | 38 | context "with unknown severity" do 39 | let(:severity) { :bogus } 40 | 41 | it "answers analyzer label and issue hint" do 42 | expect(reporter.to_s).to eq("\e[37m Commit Author Email. A test hint.\n\e[0m") 43 | end 44 | end 45 | 46 | context "with issue lines" do 47 | let :issue do 48 | { 49 | hint: "A test hint.", 50 | lines: [ 51 | {number: 1, content: "Curabitur eleifend wisi iaculis ipsum."}, 52 | {number: 3, content: "Ipsum eleifend wisi iaculis curabitur."} 53 | ] 54 | } 55 | end 56 | 57 | it "answers analyzer label, issue label, issue hint, and issue lines" do 58 | expect(reporter.to_s).to eq(<<~BODY.chomp) 59 | \e[31m Commit Author Email Error. A test hint. 60 | Line 1: "Curabitur eleifend wisi iaculis ipsum." 61 | Line 3: "Ipsum eleifend wisi iaculis curabitur."\n\e[0m 62 | BODY 63 | end 64 | end 65 | end 66 | 67 | describe "#to_str" do 68 | it "answers implicit string" do 69 | expect(reporter.to_str).to eq("\e[31m Commit Author Email Error. A test hint.\n\e[0m") 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/lib/git/lint/validators/capitalization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Validators::Capitalization do 6 | subject(:validator) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers true with custom delimiter" do 10 | validator = described_class.new delimiter: "-" 11 | expect(validator.call("Text-Example")).to be(true) 12 | end 13 | 14 | it "answers true with custom pattern" do 15 | validator = described_class.new pattern: /[[:upper:]].+/ 16 | expect(validator.call("Test Example")).to be(true) 17 | end 18 | 19 | it "answers false with leading space" do 20 | expect(validator.call(" Example")).to be(false) 21 | end 22 | 23 | it "answers true with single name capitalized" do 24 | expect(validator.call("Example")).to be(true) 25 | end 26 | 27 | it "answers false with single name lowercased" do 28 | expect(validator.call("example")).to be(false) 29 | end 30 | 31 | it "answers false with single name that starts with a number" do 32 | expect(validator.call("1Example")).to be(false) 33 | end 34 | 35 | it "answers false with single name that starts with a special character" do 36 | expect(validator.call("@Example")).to be(false) 37 | end 38 | 39 | it "answers true with single letter capitalized" do 40 | expect(validator.call("E")).to be(true) 41 | end 42 | 43 | it "answers false with single letter lowercased" do 44 | expect(validator.call("e")).to be(false) 45 | end 46 | 47 | it "answers true with multiple parts capitalized" do 48 | expect(validator.call("Example Tester")).to be(true) 49 | end 50 | 51 | it "answers false with multiple parts and only first capitalized" do 52 | expect(validator.call("Example tester")).to be(false) 53 | end 54 | 55 | it "answers false with multiple parts and without last capitalized" do 56 | expect(validator.call("example Tester")).to be(false) 57 | end 58 | 59 | it "answers true with nil text" do 60 | expect(validator.call(nil)).to be(true) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/lib/git/lint/validators/email_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Validators::Email do 6 | subject(:validator) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers true with valid validator" do 10 | expect(validator.call("test@example.com")).to be(true) 11 | end 12 | 13 | it "answers true with minimum requirements" do 14 | expect(validator.call("a@b.c")).to be(true) 15 | end 16 | 17 | it "answers true with subdomain" do 18 | expect(validator.call("test@sub.example.com")).to be(true) 19 | end 20 | 21 | it "answers false with special characters" do 22 | expect(validator.call("test@invalid!~#$%^&*(){}[].com")).to be(false) 23 | end 24 | 25 | it "answers false with missing '@' symbol" do 26 | expect(validator.call("example.com")).to be(false) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/git/lint/validators/name_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Validators::Name do 6 | subject(:validator) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers true with custom delimiter" do 10 | validator = described_class.new delimiter: "-" 11 | expect(validator.call("Text-Example")).to be(true) 12 | end 13 | 14 | it "answers true with custom minimum" do 15 | expect(validator.call("Example", minimum: 1)).to be(true) 16 | end 17 | 18 | it "answers false with leading space" do 19 | expect(validator.call(" Example Test")).to be(false) 20 | end 21 | 22 | it "answers false with multiple spaces between parts" do 23 | expect(validator.call("Example Test")).to be(false) 24 | end 25 | 26 | it "answers true with trailing space" do 27 | expect(validator.call("Example Test ")).to be(true) 28 | end 29 | 30 | it "answers true with exact minimum" do 31 | expect(validator.call("Example Test")).to be(true) 32 | end 33 | 34 | it "answers true when greater than minimum" do 35 | expect(validator.call("Example Test Tester")).to be(true) 36 | end 37 | 38 | it "answers false when less than minimum" do 39 | expect(validator.call("Example")).to be(false) 40 | end 41 | 42 | it "answers false with empty content" do 43 | expect(validator.call("")).to be(false) 44 | end 45 | 46 | it "answers false with nil content" do 47 | expect(validator.call(nil)).to be(false) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/lib/git/lint/validators/repeated_word_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint::Validators::RepeatedWord do 6 | subject(:validator) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers repeats with custom word pattern" do 10 | validator = described_class.new patterns: described_class::PATTERNS.merge(word: /\d+/) 11 | expect(validator.call("1 1 2")).to contain_exactly("1") 12 | end 13 | 14 | it "answers duplicated word at start of content" do 15 | expect(validator.call("This this is a test")).to contain_exactly("this") 16 | end 17 | 18 | it "answers duplicated word at end of content" do 19 | expect(validator.call("This is a test test")).to contain_exactly("test") 20 | end 21 | 22 | it "answers duplicated word" do 23 | expect(validator.call("This is is a test")).to contain_exactly("is") 24 | end 25 | 26 | it "answers duplicated word for each pair" do 27 | expect(validator.call("This is is is a test")).to eq(%w[is is]) 28 | end 29 | 30 | it "answers repeated, mixed case, words" do 31 | expect(validator.call("This is IS iS a test")).to eq(%w[IS iS]) 32 | end 33 | 34 | it "answers empty array with no repeats" do 35 | expect(validator.call("This a test")).to eq([]) 36 | end 37 | 38 | it "answers empty array with word boundaries respected" do 39 | expect(validator.call("link:https://test.com/test[Test]")).to eq([]) 40 | end 41 | 42 | it "answers empty array when content has code block of repeated words" do 43 | expect(validator.call("Use: `pipe method(:one), method(:two)`.")).to eq([]) 44 | end 45 | 46 | it "answers empty array when content contains duplicate version parts" do 47 | expect(validator.call("Use Version 0.0.0 now.")).to eq([]) 48 | end 49 | 50 | it "answers empty array when content has no words" do 51 | expect(validator.call("1 2 3")).to eq([]) 52 | end 53 | 54 | it "answers empty array when content is empty" do 55 | expect(validator.call("")).to eq([]) 56 | end 57 | 58 | it "answers empty array when content is nil" do 59 | expect(validator.call(nil)).to eq([]) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/lib/git/lint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe Git::Lint do 6 | describe ".loader" do 7 | it "eager loads" do 8 | expectation = proc { described_class.loader.eager_load force: true } 9 | expect(&expectation).not_to raise_error 10 | end 11 | 12 | it "answers unique tag" do 13 | expect(described_class.loader.tag).to eq("git-lint") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | 5 | unless ENV["NO_COVERAGE"] 6 | SimpleCov.start do 7 | add_filter %r((.+/container\.rb|^/spec/)) 8 | enable_coverage :branch 9 | enable_coverage_for_eval 10 | minimum_coverage_by_file line: 95, branch: 95 11 | end 12 | end 13 | 14 | Bundler.require :tools 15 | 16 | require "dry/monads" 17 | require "git/lint" 18 | require "gitt" 19 | require "gitt/rspec/shared_contexts/git_commit" 20 | require "gitt/rspec/shared_contexts/git_repo" 21 | require "gitt/rspec/shared_contexts/temp_dir" 22 | require "refinements" 23 | require "tone/rspec/matchers/have_color" 24 | require "yaml" 25 | 26 | SPEC_ROOT = Pathname(__dir__).realpath.freeze 27 | 28 | using Refinements::Pathname 29 | 30 | Pathname.require_tree SPEC_ROOT.join("support/shared_contexts") 31 | 32 | # Ensure CI environments are disabled for local testing purposes. 33 | ENV["CIRCLECI"] = "false" 34 | 35 | RSpec.configure do |config| 36 | config.color = true 37 | config.disable_monkey_patching! 38 | config.example_status_persistence_file_path = "./tmp/rspec-examples.txt" 39 | config.filter_run_when_matching :focus 40 | config.formatter = ENV.fetch("CI", false) == "true" ? :progress : :documentation 41 | config.order = :random 42 | config.pending_failure_output = :no_backtrace 43 | config.shared_context_metadata_behavior = :apply_to_host_groups 44 | config.warnings = true 45 | 46 | config.expect_with :rspec do |expectations| 47 | expectations.syntax = :expect 48 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 49 | end 50 | 51 | config.mock_with :rspec do |mocks| 52 | mocks.verify_doubled_constant_names = true 53 | mocks.verify_partial_doubles = true 54 | end 55 | 56 | config.before(:suite) { Dry::Monads.load_extensions :rspec } 57 | end 58 | -------------------------------------------------------------------------------- /spec/support/fixtures/commit-invalid.txt: -------------------------------------------------------------------------------- 1 | Pellentque morbi-trist sentus et netus et malesuada. 2 | -------------------------------------------------------------------------------- /spec/support/fixtures/commit-scissors.txt: -------------------------------------------------------------------------------- 1 | Added commit with verbose option. 2 | 3 | A fixture for commits made via `git commit --verbose` which include 4 | scissor-related content. 5 | 6 | # A test comment. 7 | 8 | # ------------------------ >8 ------------------------ 9 | # Do not modify or remove the line above. 10 | # Everything below it will be ignored. 11 | diff --git c/one.txt i/one.txt 12 | new file mode 100644 13 | index 000000000000..98038f7b36d7 14 | --- /dev/null 15 | +++ i/one.txt 16 | @@ -0,0 +1,5 @@ 17 | +A ruby example: 18 | + 19 | +def example 20 | + puts "example" 21 | +end 22 | -------------------------------------------------------------------------------- /spec/support/fixtures/commit-valid.txt: -------------------------------------------------------------------------------- 1 | Added example 2 | 3 | An example paragraph. 4 | 5 | A bullet list: 6 | - One. 7 | 8 | # A comment block. 9 | 10 | Example-One: 1 11 | Example-Two: 2 12 | -------------------------------------------------------------------------------- /spec/support/fixtures/invalid_phrases.txt: -------------------------------------------------------------------------------- 1 | absolutely 2 | actually 3 | all intents and purposes 4 | along the lines 5 | at this moment in time 6 | basically 7 | each and every one 8 | everyone knows 9 | fact of the matter 10 | furthermore 11 | however 12 | in due course 13 | in the end 14 | last but not least 15 | matter of fact 16 | obviously 17 | of course 18 | really 19 | simply 20 | things being equal 21 | would like to 22 | easy 23 | just 24 | quite 25 | as far as I am concerned 26 | as far as I'm concerned 27 | of the fact that 28 | of the opinion that 29 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/application_dependencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_context "with application dependencies" do 4 | using Refinements::Struct 5 | 6 | include_context "with temporary directory" 7 | 8 | let(:settings) { Git::Lint::Container[:settings] } 9 | let(:environment) { Hash.new } 10 | let(:logger) { Cogger.new id: "git-lint", io: StringIO.new, level: :debug } 11 | let(:kernel) { class_spy Kernel } 12 | let(:io) { StringIO.new } 13 | 14 | before do 15 | settings.merge! Etcher.call( 16 | Git::Lint::Container[:registry].remove_loader(1), 17 | commits_body_presence_enabled: false, 18 | commits_signature_includes: %w[Good Invalid] 19 | ) 20 | 21 | Git::Lint::Container.stub! environment:, logger:, kernel:, io: 22 | end 23 | 24 | after { Git::Lint::Container.restore } 25 | end 26 | -------------------------------------------------------------------------------- /spec/support/shared_contexts/host_dependencies.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | RSpec.shared_context "with host dependencies" do 6 | include_context "with application dependencies" 7 | 8 | let :git do 9 | instance_spy Gitt::Repository, 10 | call: Success(), 11 | branch_default: Success("main"), 12 | branch_name: Success("test") 13 | end 14 | end 15 | --------------------------------------------------------------------------------