git-lint 1.0.0

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