Merge branch '21949-add-type-to-changelog' into 'master'
Let's start labeling our CHANGELOG entries Closes #21949 See merge request !11579
This commit is contained in:
commit
65ad11b46b
4 changed files with 223 additions and 83 deletions
136
bin/changelog
136
bin/changelog
|
@ -14,54 +14,107 @@ Options = Struct.new(
|
||||||
:dry_run,
|
:dry_run,
|
||||||
:force,
|
:force,
|
||||||
:merge_request,
|
:merge_request,
|
||||||
:title
|
:title,
|
||||||
|
:type
|
||||||
)
|
)
|
||||||
|
INVALID_TYPE = -1
|
||||||
|
|
||||||
class ChangelogOptionParser
|
class ChangelogOptionParser
|
||||||
def self.parse(argv)
|
Type = Struct.new(:name, :description)
|
||||||
options = Options.new
|
TYPES = [
|
||||||
|
Type.new('added', 'New feature'),
|
||||||
|
Type.new('fixed', 'Bug fix'),
|
||||||
|
Type.new('changed', 'Feature change'),
|
||||||
|
Type.new('deprecated', 'New deprecation'),
|
||||||
|
Type.new('removed', 'Feature removal'),
|
||||||
|
Type.new('security', 'Security fix'),
|
||||||
|
Type.new('other', 'Other')
|
||||||
|
].freeze
|
||||||
|
TYPES_OFFSET = 1
|
||||||
|
|
||||||
parser = OptionParser.new do |opts|
|
class << self
|
||||||
opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
|
def parse(argv)
|
||||||
|
options = Options.new
|
||||||
|
|
||||||
# Note: We do not provide a shorthand for this in order to match the `git
|
parser = OptionParser.new do |opts|
|
||||||
# commit` interface
|
opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
|
||||||
opts.on('--amend', 'Amend the previous commit') do |value|
|
|
||||||
options.amend = value
|
# Note: We do not provide a shorthand for this in order to match the `git
|
||||||
|
# commit` interface
|
||||||
|
opts.on('--amend', 'Amend the previous commit') do |value|
|
||||||
|
options.amend = value
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
|
||||||
|
options.force = value
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
|
||||||
|
options.merge_request = value
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
|
||||||
|
options.dry_run = value
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
|
||||||
|
options.author = git_user_name if value
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-t', '--type [string]', String, "The category of the change, valid options are: #{TYPES.map(&:name).join(', ')}") do |value|
|
||||||
|
options.type = parse_type(value)
|
||||||
|
end
|
||||||
|
|
||||||
|
opts.on('-h', '--help', 'Print help message') do
|
||||||
|
$stdout.puts opts
|
||||||
|
exit
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
|
parser.parse!(argv)
|
||||||
options.force = value
|
|
||||||
end
|
|
||||||
|
|
||||||
opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
|
# Title is everything that remains, but let's clean it up a bit
|
||||||
options.merge_request = value
|
options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
|
||||||
end
|
|
||||||
|
|
||||||
opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
|
options
|
||||||
options.dry_run = value
|
end
|
||||||
end
|
|
||||||
|
|
||||||
opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
|
def read_type
|
||||||
options.author = git_user_name if value
|
read_type_message
|
||||||
end
|
|
||||||
|
|
||||||
opts.on('-h', '--help', 'Print help message') do
|
type = TYPES[$stdin.getc.to_i - TYPES_OFFSET]
|
||||||
$stdout.puts opts
|
assert_valid_type!(type)
|
||||||
exit
|
|
||||||
|
type.name
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parse_type(name)
|
||||||
|
type_found = TYPES.find do |type|
|
||||||
|
type.name == name
|
||||||
|
end
|
||||||
|
type_found ? type_found.name : INVALID_TYPE
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_type_message
|
||||||
|
$stdout.puts "\n>> Please specify the index for the category of your change:"
|
||||||
|
TYPES.each_with_index do |type, index|
|
||||||
|
$stdout.puts "#{index + TYPES_OFFSET}. #{type.description}"
|
||||||
|
end
|
||||||
|
$stdout.print "\n?> "
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_valid_type!(type)
|
||||||
|
unless type
|
||||||
|
$stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}"
|
||||||
|
exit 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.parse!(argv)
|
def git_user_name
|
||||||
|
%x{git config user.name}.strip
|
||||||
# Title is everything that remains, but let's clean it up a bit
|
end
|
||||||
options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
|
|
||||||
|
|
||||||
options
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.git_user_name
|
|
||||||
%x{git config user.name}.strip
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -72,8 +125,12 @@ class ChangelogEntry
|
||||||
@options = options
|
@options = options
|
||||||
|
|
||||||
assert_feature_branch!
|
assert_feature_branch!
|
||||||
assert_new_file!
|
|
||||||
assert_title!
|
assert_title!
|
||||||
|
assert_new_file!
|
||||||
|
|
||||||
|
# Read type from $stdin unless is already set
|
||||||
|
options.type ||= ChangelogOptionParser.read_type
|
||||||
|
assert_valid_type!
|
||||||
|
|
||||||
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
|
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
|
||||||
$stdout.puts contents
|
$stdout.puts contents
|
||||||
|
@ -90,7 +147,8 @@ class ChangelogEntry
|
||||||
yaml_content = YAML.dump(
|
yaml_content = YAML.dump(
|
||||||
'title' => title,
|
'title' => title,
|
||||||
'merge_request' => options.merge_request,
|
'merge_request' => options.merge_request,
|
||||||
'author' => options.author
|
'author' => options.author,
|
||||||
|
'type' => options.type
|
||||||
)
|
)
|
||||||
remove_trailing_whitespace(yaml_content)
|
remove_trailing_whitespace(yaml_content)
|
||||||
end
|
end
|
||||||
|
@ -129,6 +187,12 @@ class ChangelogEntry
|
||||||
" to use the title from the previous commit."
|
" to use the title from the previous commit."
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assert_valid_type!
|
||||||
|
return unless options.type && options.type == INVALID_TYPE
|
||||||
|
|
||||||
|
fail_with 'Invalid category given!'
|
||||||
|
end
|
||||||
|
|
||||||
def title
|
def title
|
||||||
if options.title.empty?
|
if options.title.empty?
|
||||||
last_commit_subject
|
last_commit_subject
|
||||||
|
|
4
changelogs/unreleased/21949-add-type-to-changelog.yml
Normal file
4
changelogs/unreleased/21949-add-type-to-changelog.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Added type to CHANGELOG entries
|
||||||
|
merge_request:
|
||||||
|
author: Jacopo Beschi @jacopo-beschi
|
|
@ -15,11 +15,14 @@ following format:
|
||||||
title: "Going through change[log]s"
|
title: "Going through change[log]s"
|
||||||
merge_request: 1972
|
merge_request: 1972
|
||||||
author: Ozzy Osbourne
|
author: Ozzy Osbourne
|
||||||
|
type: added
|
||||||
```
|
```
|
||||||
|
|
||||||
The `merge_request` value is a reference to a merge request that adds this
|
The `merge_request` value is a reference to a merge request that adds this
|
||||||
entry, and the `author` key is used to give attribution to community
|
entry, and the `author` key is used to give attribution to community
|
||||||
contributors. **Both are optional**.
|
contributors. **Both are optional**.
|
||||||
|
The `type` field maps the category of the change,
|
||||||
|
valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**.
|
||||||
|
|
||||||
Community contributors and core team members are encouraged to add their name to
|
Community contributors and core team members are encouraged to add their name to
|
||||||
the `author` field. GitLab team members **should not**.
|
the `author` field. GitLab team members **should not**.
|
||||||
|
@ -94,6 +97,19 @@ Its simplest usage is to provide the value for `title`:
|
||||||
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
|
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
At this point the script would ask you to select the category of the change (mapped to the `type` field in the entry):
|
||||||
|
|
||||||
|
```text
|
||||||
|
>> Please specify the category of your change:
|
||||||
|
1. New feature
|
||||||
|
2. Bug fix
|
||||||
|
3. Feature change
|
||||||
|
4. New deprecation
|
||||||
|
5. Feature removal
|
||||||
|
6. Security fix
|
||||||
|
7. Other
|
||||||
|
```
|
||||||
|
|
||||||
The entry filename is based on the name of the current Git branch. If you run
|
The entry filename is based on the name of the current Git branch. If you run
|
||||||
the command above on a branch called `feature/hey-dz`, it will generate a
|
the command above on a branch called `feature/hey-dz`, it will generate a
|
||||||
`changelogs/unreleased/feature-hey-dz.yml` file.
|
`changelogs/unreleased/feature-hey-dz.yml` file.
|
||||||
|
@ -106,26 +122,29 @@ create changelogs/unreleased/my-feature.yml
|
||||||
title: Hey DZ, I added a feature to GitLab!
|
title: Hey DZ, I added a feature to GitLab!
|
||||||
merge_request:
|
merge_request:
|
||||||
author:
|
author:
|
||||||
|
type:
|
||||||
```
|
```
|
||||||
If you're working on the GitLab EE repository, the entry will be added to
|
If you're working on the GitLab EE repository, the entry will be added to
|
||||||
`changelogs/unreleased-ee/` instead.
|
`changelogs/unreleased-ee/` instead.
|
||||||
|
|
||||||
#### Arguments
|
#### Arguments
|
||||||
|
|
||||||
| Argument | Shorthand | Purpose |
|
| Argument | Shorthand | Purpose |
|
||||||
| ----------------- | --------- | --------------------------------------------- |
|
| ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- |
|
||||||
| [`--amend`] | | Amend the previous commit |
|
| [`--amend`] | | Amend the previous commit |
|
||||||
| [`--force`] | `-f` | Overwrite an existing entry |
|
| [`--force`] | `-f` | Overwrite an existing entry |
|
||||||
| [`--merge-request`] | `-m` | Set merge request ID |
|
| [`--merge-request`] | `-m` | Set merge request ID |
|
||||||
| [`--dry-run`] | `-n` | Don't actually write anything, just print |
|
| [`--dry-run`] | `-n` | Don't actually write anything, just print |
|
||||||
| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
|
| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
|
||||||
| [`--help`] | `-h` | Print help message |
|
| [`--type`] | `-t` | The category of the change, valid options are: added, fixed, changed, deprecated, removed, security, other |
|
||||||
|
| [`--help`] | `-h` | Print help message |
|
||||||
|
|
||||||
[`--amend`]: #-amend
|
[`--amend`]: #-amend
|
||||||
[`--force`]: #-force-or-f
|
[`--force`]: #-force-or-f
|
||||||
[`--merge-request`]: #-merge-request-or-m
|
[`--merge-request`]: #-merge-request-or-m
|
||||||
[`--dry-run`]: #-dry-run-or-n
|
[`--dry-run`]: #-dry-run-or-n
|
||||||
[`--git-username`]: #-git-username-or-u
|
[`--git-username`]: #-git-username-or-u
|
||||||
|
[`--type`]: #-type-or-t
|
||||||
[`--help`]: #-help
|
[`--help`]: #-help
|
||||||
|
|
||||||
##### `--amend`
|
##### `--amend`
|
||||||
|
@ -147,6 +166,7 @@ create changelogs/unreleased/feature-hey-dz.yml
|
||||||
title: Added an awesome new feature to GitLab
|
title: Added an awesome new feature to GitLab
|
||||||
merge_request:
|
merge_request:
|
||||||
author:
|
author:
|
||||||
|
type:
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `--force` or `-f`
|
##### `--force` or `-f`
|
||||||
|
@ -164,6 +184,7 @@ create changelogs/unreleased/feature-hey-dz.yml
|
||||||
title: Hey DZ, I added a feature to GitLab!
|
title: Hey DZ, I added a feature to GitLab!
|
||||||
merge_request: 1983
|
merge_request: 1983
|
||||||
author:
|
author:
|
||||||
|
type:
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `--merge-request` or `-m`
|
##### `--merge-request` or `-m`
|
||||||
|
@ -178,6 +199,7 @@ create changelogs/unreleased/feature-hey-dz.yml
|
||||||
title: Hey DZ, I added a feature to GitLab!
|
title: Hey DZ, I added a feature to GitLab!
|
||||||
merge_request: 1983
|
merge_request: 1983
|
||||||
author:
|
author:
|
||||||
|
type:
|
||||||
```
|
```
|
||||||
|
|
||||||
##### `--dry-run` or `-n`
|
##### `--dry-run` or `-n`
|
||||||
|
@ -192,6 +214,7 @@ create changelogs/unreleased/feature-hey-dz.yml
|
||||||
title: Added an awesome new feature to GitLab
|
title: Added an awesome new feature to GitLab
|
||||||
merge_request:
|
merge_request:
|
||||||
author:
|
author:
|
||||||
|
type:
|
||||||
|
|
||||||
$ ls changelogs/unreleased/
|
$ ls changelogs/unreleased/
|
||||||
```
|
```
|
||||||
|
@ -211,6 +234,21 @@ create changelogs/unreleased/feature-hey-dz.yml
|
||||||
title: Hey DZ, I added a feature to GitLab!
|
title: Hey DZ, I added a feature to GitLab!
|
||||||
merge_request:
|
merge_request:
|
||||||
author: Jane Doe
|
author: Jane Doe
|
||||||
|
type:
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `--type` or `-t`
|
||||||
|
|
||||||
|
Use the **`--type`** or **`-t`** argument to provide the `type` value:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -t added
|
||||||
|
create changelogs/unreleased/feature-hey-dz.yml
|
||||||
|
---
|
||||||
|
title: Hey DZ, I added a feature to GitLab!
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: added
|
||||||
```
|
```
|
||||||
|
|
||||||
### History and Reasoning
|
### History and Reasoning
|
||||||
|
|
|
@ -4,56 +4,90 @@ load File.expand_path('../../bin/changelog', __dir__)
|
||||||
|
|
||||||
describe 'bin/changelog' do
|
describe 'bin/changelog' do
|
||||||
describe ChangelogOptionParser do
|
describe ChangelogOptionParser do
|
||||||
it 'parses --ammend' do
|
describe '.parse' do
|
||||||
options = described_class.parse(%w[foo bar --amend])
|
it 'parses --amend' do
|
||||||
|
options = described_class.parse(%w[foo bar --amend])
|
||||||
|
|
||||||
expect(options.amend).to eq true
|
expect(options.amend).to eq true
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses --force and -f' do
|
it 'parses --force and -f' do
|
||||||
%w[--force -f].each do |flag|
|
%w[--force -f].each do |flag|
|
||||||
options = described_class.parse(%W[foo #{flag} bar])
|
options = described_class.parse(%W[foo #{flag} bar])
|
||||||
|
|
||||||
expect(options.force).to eq true
|
expect(options.force).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses --merge-request and -m' do
|
||||||
|
%w[--merge-request -m].each do |flag|
|
||||||
|
options = described_class.parse(%W[foo #{flag} 1234 bar])
|
||||||
|
|
||||||
|
expect(options.merge_request).to eq 1234
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses --dry-run and -n' do
|
||||||
|
%w[--dry-run -n].each do |flag|
|
||||||
|
options = described_class.parse(%W[foo #{flag} bar])
|
||||||
|
|
||||||
|
expect(options.dry_run).to eq true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses --git-username and -u' do
|
||||||
|
allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
|
||||||
|
|
||||||
|
%w[--git-username -u].each do |flag|
|
||||||
|
options = described_class.parse(%W[foo #{flag} bar])
|
||||||
|
|
||||||
|
expect(options.author).to eq 'Jane Doe'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses --type and -t' do
|
||||||
|
%w[--type -t].each do |flag|
|
||||||
|
options = described_class.parse(%W[foo #{flag} security])
|
||||||
|
|
||||||
|
expect(options.type).to eq 'security'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses -h' do
|
||||||
|
expect do
|
||||||
|
expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
|
||||||
|
end.to raise_error(SystemExit)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'assigns title' do
|
||||||
|
options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
|
||||||
|
|
||||||
|
expect(options.title).to eq 'foo bar baz'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses --merge-request and -m' do
|
describe '.read_type' do
|
||||||
%w[--merge-request -m].each do |flag|
|
let(:type) { '1' }
|
||||||
options = described_class.parse(%W[foo #{flag} 1234 bar])
|
|
||||||
|
|
||||||
expect(options.merge_request).to eq 1234
|
it 'reads type from $stdin' do
|
||||||
|
expect($stdin).to receive(:getc).and_return(type)
|
||||||
|
expect do
|
||||||
|
expect(described_class.read_type).to eq('added')
|
||||||
|
end.to output.to_stdout
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
it 'parses --dry-run and -n' do
|
context 'invalid type given' do
|
||||||
%w[--dry-run -n].each do |flag|
|
let(:type) { '99' }
|
||||||
options = described_class.parse(%W[foo #{flag} bar])
|
|
||||||
|
|
||||||
expect(options.dry_run).to eq true
|
it 'shows error message and exits the program' do
|
||||||
|
allow($stdin).to receive(:getc).and_return(type)
|
||||||
|
expect do
|
||||||
|
expect do
|
||||||
|
expect{ described_class.read_type }.to raise_error(SystemExit)
|
||||||
|
end.to output("Invalid category index, please select an index between 1 and 7\n").to_stderr
|
||||||
|
end.to output.to_stdout
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'parses --git-username and -u' do
|
|
||||||
allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
|
|
||||||
|
|
||||||
%w[--git-username -u].each do |flag|
|
|
||||||
options = described_class.parse(%W[foo #{flag} bar])
|
|
||||||
|
|
||||||
expect(options.author).to eq 'Jane Doe'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'parses -h' do
|
|
||||||
expect do
|
|
||||||
expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
|
|
||||||
end.to raise_error(SystemExit)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'assigns title' do
|
|
||||||
options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
|
|
||||||
|
|
||||||
expect(options.title).to eq 'foo bar baz'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue