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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "refinements/strings"
4
+
5
+ module Git
6
+ module Lint
7
+ class Collector
8
+ using Refinements::Strings
9
+
10
+ def initialize
11
+ @collection = Hash.new { |default, missing_id| default[missing_id] = [] }
12
+ end
13
+
14
+ def add analyzer
15
+ collection[analyzer.commit] << analyzer
16
+ analyzer
17
+ end
18
+
19
+ def retrieve id
20
+ collection[id]
21
+ end
22
+
23
+ def empty?
24
+ collection.empty?
25
+ end
26
+
27
+ def warnings?
28
+ collection.values.flatten.any?(&:warning?)
29
+ end
30
+
31
+ def errors?
32
+ collection.values.flatten.any?(&:error?)
33
+ end
34
+
35
+ def issues?
36
+ collection.values.flatten.any?(&:invalid?)
37
+ end
38
+
39
+ def total_warnings
40
+ collection.values.flatten.count(&:warning?)
41
+ end
42
+
43
+ def total_errors
44
+ collection.values.flatten.count(&:error?)
45
+ end
46
+
47
+ def total_issues
48
+ collection.values.flatten.count(&:invalid?)
49
+ end
50
+
51
+ def total_commits
52
+ collection.keys.size
53
+ end
54
+
55
+ def to_h
56
+ collection
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :collection
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Git
6
+ module Lint
7
+ module Commits
8
+ # Represents an existing commit.
9
+ # :reek:TooManyMethods
10
+ class Saved
11
+ using Refinements::Strings
12
+
13
+ FORMATS = {
14
+ sha: "%H",
15
+ author_name: "%an",
16
+ author_email: "%ae",
17
+ author_date_relative: "%ar",
18
+ subject: "%s",
19
+ body: "%b",
20
+ raw_body: "%B",
21
+ trailers: "%(trailers)"
22
+ }.freeze
23
+
24
+ def self.pattern
25
+ FORMATS.reduce("") { |pattern, (key, value)| pattern + "<#{key}>#{value}</#{key}>%n" }
26
+ end
27
+
28
+ def initialize sha:, shell: Open3
29
+ data, status = shell.capture2e show_command(sha)
30
+ fail Errors::SHA, sha unless status.success?
31
+
32
+ @data = data.scrub "?"
33
+ end
34
+
35
+ def == other
36
+ other.is_a?(self.class) && sha == other.sha
37
+ end
38
+ alias eql? ==
39
+
40
+ def <=> other
41
+ sha <=> other.sha
42
+ end
43
+
44
+ def hash
45
+ sha.hash
46
+ end
47
+
48
+ def body_lines
49
+ body_without_trailing_spaces
50
+ end
51
+
52
+ def body_paragraphs
53
+ body_without_trailers.split("\n\n").map(&:chomp).reject { |line| line.start_with? "#" }
54
+ end
55
+
56
+ def trailer_lines
57
+ trailers.split "\n"
58
+ end
59
+
60
+ def trailer_index
61
+ body.split("\n").index trailer_lines.first
62
+ end
63
+
64
+ def fixup?
65
+ subject.fixup?
66
+ end
67
+
68
+ def squash?
69
+ subject.squash?
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :data
75
+
76
+ def show_command sha
77
+ %(git show --stat --pretty=format:"#{self.class.pattern}" #{sha})
78
+ end
79
+
80
+ def body_without_trailing_spaces
81
+ body_without_comments.reverse.drop_while(&:empty?).reverse
82
+ end
83
+
84
+ def body_without_comments
85
+ body_without_trailers.split("\n").reject { |line| line.start_with? "#" }
86
+ end
87
+
88
+ def body_without_trailers
89
+ body.sub trailers, ""
90
+ end
91
+
92
+ def method_missing name, *arguments, &block
93
+ return super unless respond_to_missing? name
94
+
95
+ String data[%r(<#{name}>(?<content>.*?)</#{name}>)m, :content]
96
+ end
97
+
98
+ def respond_to_missing? name, include_private = false
99
+ FORMATS.key?(name.to_sym) || super
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "open3"
5
+ require "securerandom"
6
+
7
+ module Git
8
+ module Lint
9
+ module Commits
10
+ # Represents a partially formed, unsaved commit.
11
+ # :reek:TooManyMethods
12
+ class Unsaved
13
+ using Refinements::Strings
14
+
15
+ SUBJECT_LINE = 1
16
+ SCISSOR_PATTERN = /\#\s-+\s>8\s-+\n.+/m.freeze
17
+
18
+ attr_reader :sha, :raw_body
19
+
20
+ def initialize path:, sha: SecureRandom.hex(20), shell: Open3
21
+ fail Errors::Base, %(Invalid commit message path: "#{path}".) unless File.exist? path
22
+
23
+ @path = Pathname path
24
+ @sha = sha
25
+ @shell = shell
26
+ @raw_body = File.read(path).scrub "?"
27
+ end
28
+
29
+ def author_name
30
+ shell.capture2e("git config --get user.name").then { |result, _status| result.chomp }
31
+ end
32
+
33
+ def author_email
34
+ shell.capture2e("git config --get user.email").then { |result, _status| result.chomp }
35
+ end
36
+
37
+ def author_date_relative
38
+ "0 seconds ago"
39
+ end
40
+
41
+ def subject
42
+ String raw_body.split("\n").first
43
+ end
44
+
45
+ # :reek:FeatureEnvy
46
+ def body
47
+ raw_body.sub(SCISSOR_PATTERN, "").split("\n").drop(SUBJECT_LINE).then do |lines|
48
+ computed_body = lines.join "\n"
49
+ lines.empty? ? computed_body : "#{computed_body}\n"
50
+ end
51
+ end
52
+
53
+ def body_lines
54
+ body_without_trailing_spaces
55
+ end
56
+
57
+ # :reek:FeatureEnvy
58
+ def body_paragraphs
59
+ body_without_trailers.split("\n\n")
60
+ .map { |line| line.delete_prefix "\n" }
61
+ .map(&:chomp)
62
+ .reject { |line| line.start_with? "#" }
63
+ end
64
+
65
+ def trailers
66
+ trailers, status = shell.capture2e %(git interpret-trailers --only-trailers "#{path}")
67
+
68
+ return "" unless status.success?
69
+
70
+ trailers
71
+ end
72
+
73
+ def trailer_lines
74
+ trailers.split "\n"
75
+ end
76
+
77
+ def trailer_index
78
+ body.split("\n").index trailer_lines.first
79
+ end
80
+
81
+ def == other
82
+ other.is_a?(self.class) && raw_body == other.raw_body
83
+ end
84
+ alias eql? ==
85
+
86
+ def <=> other
87
+ raw_body <=> other.raw_body
88
+ end
89
+
90
+ def hash
91
+ raw_body.hash
92
+ end
93
+
94
+ def fixup?
95
+ subject.fixup?
96
+ end
97
+
98
+ def squash?
99
+ subject.squash?
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :path, :shell
105
+
106
+ def body_without_trailing_spaces
107
+ body_without_comments.reverse.drop_while(&:empty?).reverse
108
+ end
109
+
110
+ def body_without_comments
111
+ body_without_trailers.split("\n").reject { |line| line.start_with? "#" }
112
+ end
113
+
114
+ def body_without_trailers
115
+ body.sub trailers, ""
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,14 @@
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 #{Identity::LABEL} action."
9
+ super message
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Lint
5
+ module Errors
6
+ class Severity < Base
7
+ def initialize level
8
+ super %(Invalid severity level: #{level}. Use: #{Analyzers::Abstract::LEVELS.join ", "}.)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Lint
5
+ module Errors
6
+ class SHA < Base
7
+ def initialize sha
8
+ super %(Invalid commit SHA: "#{sha}". Unable to obtain commit details.)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Lint
5
+ # Gem identity information.
6
+ module Identity
7
+ NAME = "git-lint"
8
+ LABEL = "Git Lint"
9
+ VERSION = "1.0.0"
10
+ VERSION_LABEL = "#{LABEL} #{VERSION}"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Lint
5
+ module Kit
6
+ class FilterList
7
+ # Represents an regular expression list which may be used as an analyzer setting.
8
+ def initialize list = []
9
+ @list = Array list
10
+ end
11
+
12
+ def to_hint
13
+ to_regexp.map(&:inspect).join ", "
14
+ end
15
+
16
+ def to_regexp
17
+ list.map { |item| Regexp.new item }
18
+ end
19
+
20
+ def empty?
21
+ list.empty?
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :list
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Lint
5
+ module Parsers
6
+ module Trailers
7
+ class Collaborator
8
+ DEFAULT_KEY_PATTERN = /\ACo.*Authored.*By.*\Z/i.freeze
9
+
10
+ DEFAULT_MATCH_PATTERN = /
11
+ (?<key>\A.+) # Key (anchored to start of line).
12
+ (?<delimiter>:) # Key delimiter.
13
+ (?<key_space>\s?) # Space delimiter (optional).
14
+ (?<name>.*?) # Collaborator name (smallest possible).
15
+ (?<name_space>\s?) # Space delimiter (optional).
16
+ (?<email><.+>)? # Collaborator email (optional).
17
+ \Z # End of line.
18
+ /x.freeze
19
+
20
+ def initialize text,
21
+ key_pattern: DEFAULT_KEY_PATTERN,
22
+ match_pattern: DEFAULT_MATCH_PATTERN
23
+
24
+ @text = String text
25
+ @key_pattern = key_pattern
26
+ @match_pattern = match_pattern
27
+ @matches = build_matches
28
+ end
29
+
30
+ def key
31
+ String matches["key"]
32
+ end
33
+
34
+ def name
35
+ String matches["name"]
36
+ end
37
+
38
+ def email
39
+ String(matches["email"]).delete_prefix("<").delete_suffix(">")
40
+ end
41
+
42
+ def match?
43
+ text.match? key_pattern
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :text, :key_pattern, :match_pattern, :matches
49
+
50
+ def build_matches
51
+ text.match(match_pattern).then { |data| data ? data.named_captures : Hash.new }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "git/lint/rake/tasks"
4
+ Git::Lint::Rake::Tasks.setup
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "git/lint"
5
+
6
+ module Git
7
+ module Lint
8
+ module Rake
9
+ class Tasks
10
+ include ::Rake::DSL
11
+
12
+ def self.setup
13
+ new.install
14
+ end
15
+
16
+ def initialize cli: CLI
17
+ @cli = cli
18
+ end
19
+
20
+ def install
21
+ desc "Run Git Lint"
22
+ task :git_lint do
23
+ cli.start ["--analyze"]
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :cli
30
+ end
31
+ end
32
+ end
33
+ end