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,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