git-lint 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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