Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore commit messages if they match regular expression #17

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
66 changes: 61 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Every commit between current HEAD and specified commit will be checked.
## Configuration

The behavior of Poper can be controlled via the `.poper.yml` configuration
file. It must be placed in your project directory. A sample file, `.poper.sample.yml`, is included for easy setup.
file. It must be placed in your project directory. A sample file, `.poper.sample.yml`, is included
for easy setup.

The file has the following format:

Expand Down Expand Up @@ -74,7 +75,62 @@ enforce_capitalized:
enabled: true
```

All properties that can be specified via `.poper.yml`, can also be specified
via environment variables. Their names will be the upcased path to the property.
For example: `POPER_ENFORCE_CAPITALIZED_ENABLED` or `POPER_DISALLOW_GENERIC_WORDS`. (In the case of the latter, since environment variables don't support arrays, use a comma-separated list of words and poper will parse them appropriately.) Environment variables
will always take precedence over values in configuration file.
All properties that can be specified via `.poper.yml`, can also be specified via environment
variables. Their names will be the upcased path to the property. For example:
`POPER_ENFORCE_CAPITALIZED_ENABLED` or `POPER_DISALLOW_GENERIC_WORDS`. (In the case of the latter,
since environment variables don't support arrays, use a comma-separated list of words and poper will
parse them appropriately.) Environment variables will always take precedence over values in
configuration file.

## Ignoring commits

```
Merge pull request #12345 from myLongUsername/plus-a-long-feature-branch-name
```
The above commit message would fail the `character_limit` and `summary_character_limit` checks
unless the number of allowed characters were set to a high number. Since the commit was
automatically generated, it may not be desirable to check it.

You can ignore commit messages matching a certain pattern by adding the following settings either to
`character_limit` or `summary_character_limit`:

```yaml
# Adding to .poper.yml

character_limit:
ignore_if_message_matches: '^Merge pull request'

summary_character_limit:
ignore_if_summary_matches: '^Merge pull request'
```

```
# Or to ENV vars

POPER_CHARACTER_LIMIT_IGNORE_IF_MESSAGE_MATCHES='^Merge pull request'
POPER_SUMMARY_CHARACTER_LIMIT_IGNORE_IF_SUMMARY_MATCHES='^Merge pull request'
```

Note there is a naming difference: `ignore_if_message_matches` versus `ignore_if_summary_matches`.

With a few exceptions, the string you pass to this setting will be used to create a regular
expression directly.
```ruby
Regexp.new('^Merge pull request')
```

The ignore settings are `nil` by default. If a setting is `nil` or `false`, its rule ignores no
commit messages. If a setting is `true`, it ignores ALL commit messages (not recommended).

If you want to escape a character in a pattern string, you will need to escape it twice.
For example:
```ruby
string = "Look: ^ a caret"
escaped_once = "Look: \^ a caret"
escaped_twice = "Look: \\^ a caret"

Regexp.new(escaped_once).match?(string) # => false
Regexp.new(escaped_twice).match?(string) # => true
```
This is necessary because Ruby encloses all strings in double quotes when it loads the configuration
YAML.
6 changes: 4 additions & 2 deletions lib/poper/config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ class ConfigFile
},
'character_limit' => {
'enabled' => true,
'number' => 72
'number' => 72,
'ignore_if_message_matches' => nil
},
'summary_character_limit' => {
'enabled' => true,
'number' => 50
'number' => 50,
'ignore_if_summary_matches' => nil
},
'disallow_generic' => {
'enabled' => true,
Expand Down
40 changes: 40 additions & 0 deletions lib/poper/regexp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# A regular expression that may be initialized with any value that responds to `to_s`.
#
# @param [Object] init_value
# The value from which to create a regular expression. Must respond to `to_s`.
# If `init_value` is `true`, the regular expression will match against all strings.
# If `init_value` is `false` or `nil`, the regular expression will match against no strings.
#
# @param [Object] options
# From 'https://ruby-doc.org/core-2.3.0/Regexp.html#method-c-new':
# 'If options is an Fixnum, it should be one or more of the constants Regexp::EXTENDED,
# Regexp::IGNORECASE, and Regexp::MULTILINE, or-ed together.
# 'Otherwise, if options is not nil or false, the regexp will be case insensitive.'
module Poper
class Regexp < ::Regexp
ANY_STRING_MATCHER = ''
NO_STRING_MATCHER = '.^'
INIT_VALUES_MATCHING_ALL_STRINGS = [true].freeze
INIT_VALUES_MATCHING_NO_STRINGS = [nil, false].freeze

def initialize(init_value, options = nil)
@init_value = init_value
super(modified_init_value, options)
end

private

attr_reader :init_value

def modified_init_value
case init_value
when *INIT_VALUES_MATCHING_ALL_STRINGS
ANY_STRING_MATCHER
when *INIT_VALUES_MATCHING_NO_STRINGS
NO_STRING_MATCHER
else
init_value.to_s
end
end
end
end
10 changes: 10 additions & 0 deletions lib/poper/rule/character_limit.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
require 'poper/rule/ignore/matching_pattern'

module Poper
module Rule
class CharacterLimit < Rule
include Ignore::MatchingPattern

def check(message)
return if should_ignore?(message, ignore_pattern: ignore_message_pattern)

error_message if message.lines.any? do |line|
line.chomp.length > character_limit
end
Expand All @@ -20,6 +26,10 @@ def character_limit
def error_message
"Every line of git commit message should be #{character_limit} chars or less"
end

def ignore_message_pattern
@config.character_limit_ignore_if_message_matches
end
end
end
end
15 changes: 15 additions & 0 deletions lib/poper/rule/ignore/matching_pattern.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'poper/regexp'

# Provides an interface to determine whether a rule should be ignored.
# Matches a string against a pattern to make this determination.
module Poper
module Rule
module Ignore
module MatchingPattern
def should_ignore?(string, ignore_pattern: nil)
Regexp.new(ignore_pattern).match?(string)
end
end
end
end
end
13 changes: 12 additions & 1 deletion lib/poper/rule/summary_character_limit.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
require 'poper/rule/ignore/matching_pattern'

module Poper
module Rule
class SummaryCharacterLimit < Rule
include Ignore::MatchingPattern

def check(message)
error_message if message.lines.first.chomp.length > character_limit
summary = message.lines.first.chomp
return if should_ignore?(summary, ignore_pattern: ignore_summary_pattern)

error_message if summary.length > character_limit
end

def enabled?
Expand All @@ -18,6 +25,10 @@ def character_limit
def error_message
"Git commit message summary (first line) should be #{character_limit} chars or less"
end

def ignore_summary_pattern
@config.summary_character_limit_ignore_if_summary_matches
end
end
end
end
2 changes: 1 addition & 1 deletion poper.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Gem::Specification.new do |s|
s.require_paths = ['lib']
s.executables << 'poper'

s.add_runtime_dependency('rugged', '~> 0.23', '>= 0.23.0')
s.add_runtime_dependency('rugged', '~> 0.23.0')
s.add_runtime_dependency('thor', '~> 0.20.0')
s.add_development_dependency('bundler', '>= 1.10')
s.add_development_dependency('codeclimate-test-reporter', '~> 1.0')
Expand Down
12 changes: 8 additions & 4 deletions spec/poper/config_file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ module Poper
should include(
'character_limit' => {
'enabled' => true,
'number' => 72
'number' => 72,
'ignore_if_message_matches' => nil
}
)
end
Expand All @@ -30,7 +31,8 @@ module Poper
should include(
'summary_character_limit' => {
'enabled' => true,
'number' => 50
'number' => 50,
'ignore_if_summary_matches' => nil
}
)
end
Expand Down Expand Up @@ -68,7 +70,8 @@ module Poper
should include(
'summary_character_limit' => {
'enabled' => true,
'number' => 95
'number' => 95,
'ignore_if_summary_matches' => nil
}
)
end
Expand All @@ -89,7 +92,8 @@ module Poper
should include(
'summary_character_limit' => {
'enabled' => false,
'number' => 95
'number' => 95,
'ignore_if_summary_matches' => nil
}
)
end
Expand Down
75 changes: 75 additions & 0 deletions spec/poper/regexp_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
require 'spec_helper'

module Poper
describe Regexp do
subject { described_class.new('a string') }

it { should match('Look, a string') }
it { should_not match('Look, another string') }

context 'when set to case-insensitive' do
subject { described_class.new('a string', described_class::IGNORECASE) }

it { should match('LOOK, A STRING') }
end

context 'when initialized with nil' do
subject { described_class.new(nil) }

it { should == /.^/ }
it { should_not match('Look, a string') }
it { should_not match('') }
end

context 'when initialized with false' do
subject { described_class.new(false) }

it { should == /.^/ }
it { should_not match('Look, a string') }
it { should_not match('') }
end

context 'when initialized with true' do
subject { described_class.new(true) }

it { should == // }
it { should match('Look, a string') }
it { should match('') }
it { should match("\n") }
end

context 'when intialized with an empty string' do
subject { described_class.new('') }

it { should == // }
end

context 'when initialized with a string containing special characters' do
subject { described_class.new('^Look') }

it { should match('Look, a string') }
it { should_not match('^Look, a string') }

context 'and the string has double quotes' do
subject { described_class.new("^Look") }

it { should match('Look, a string') }
end
end

context 'when initialized with a string containing escaped characters' do
subject { described_class.new('\^Look') }

it { should match('^Look, a string') }
it { should_not match('Look, a string') }

context 'and the string has double quotes' do
subject { described_class.new("\^Look") }

it { should match('Look, a string') }
it { should_not match('^Look, a string') }
end
end

end
end
32 changes: 32 additions & 0 deletions spec/poper/rule/character_limit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ module Rule
end

it { should_not be_nil }

context 'when message matches ignore pattern' do
before do
File.should_receive(:exist?)
.and_return(true)

YAML.should_receive(:load_file)
.and_return(
'character_limit' => {
'ignore_if_message_matches' => 'Implement that feature'
}
)
end

it { should be_nil }
end
end

context 'seventy-three char last line' do
Expand All @@ -57,6 +73,22 @@ module Rule
end

it { should_not be_nil }

context 'when message matches ignore pattern' do
before do
File.should_receive(:exist?)
.and_return(true)

YAML.should_receive(:load_file)
.and_return(
'character_limit' => {
'ignore_if_message_matches' => 'Implement that feature'
}
)
end

it { should be_nil }
end
end
end
end
Expand Down