83106427c8
Signed-off-by: Rémy Coutable <remy@rymai.me>
276 lines
6.2 KiB
Ruby
Executable file
276 lines
6.2 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
#
|
|
# Generate a changelog entry file in the correct location.
|
|
#
|
|
# Automatically stages the file and amends the previous commit if the `--amend`
|
|
# argument is used.
|
|
|
|
require 'optparse'
|
|
require 'yaml'
|
|
|
|
Options = Struct.new(
|
|
:amend,
|
|
:author,
|
|
:dry_run,
|
|
:force,
|
|
:merge_request,
|
|
:title,
|
|
:type
|
|
)
|
|
INVALID_TYPE = -1
|
|
|
|
module ChangelogHelpers
|
|
Abort = Class.new(StandardError)
|
|
Done = Class.new(StandardError)
|
|
|
|
MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters
|
|
|
|
def capture_stdout(cmd)
|
|
output = IO.popen(cmd, &:read)
|
|
fail_with "command failed: #{cmd.join(' ')}" unless $?.success?
|
|
output
|
|
end
|
|
|
|
def fail_with(message)
|
|
raise Abort, "\e[31merror\e[0m #{message}"
|
|
end
|
|
end
|
|
|
|
class ChangelogOptionParser
|
|
extend ChangelogHelpers
|
|
|
|
Type = Struct.new(:name, :description)
|
|
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('performance', 'Performance improvement'),
|
|
Type.new('other', 'Other')
|
|
].freeze
|
|
TYPES_OFFSET = 1
|
|
|
|
class << self
|
|
def parse(argv)
|
|
options = Options.new
|
|
|
|
parser = OptionParser.new do |opts|
|
|
opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
|
|
|
|
# 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
|
|
raise Done.new
|
|
end
|
|
end
|
|
|
|
parser.parse!(argv)
|
|
|
|
# Title is everything that remains, but let's clean it up a bit
|
|
options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
|
|
|
|
options
|
|
end
|
|
|
|
def read_type
|
|
read_type_message
|
|
|
|
type = TYPES[$stdin.getc.to_i - TYPES_OFFSET]
|
|
assert_valid_type!(type)
|
|
|
|
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
|
|
raise Abort, "Invalid category index, please select an index between 1 and #{TYPES.length}"
|
|
end
|
|
end
|
|
|
|
def git_user_name
|
|
capture_stdout(%w[git config user.name]).strip
|
|
end
|
|
end
|
|
end
|
|
|
|
class ChangelogEntry
|
|
include ChangelogHelpers
|
|
|
|
attr_reader :options
|
|
|
|
def initialize(options)
|
|
@options = options
|
|
end
|
|
|
|
def execute
|
|
assert_feature_branch!
|
|
assert_title! unless editor
|
|
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 contents
|
|
|
|
unless options.dry_run
|
|
write
|
|
amend_commit if options.amend
|
|
end
|
|
|
|
if editor
|
|
system("#{editor} '#{file_path}'")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def contents
|
|
yaml_content = YAML.dump(
|
|
'title' => title,
|
|
'merge_request' => options.merge_request,
|
|
'author' => options.author,
|
|
'type' => options.type
|
|
)
|
|
remove_trailing_whitespace(yaml_content)
|
|
end
|
|
|
|
def write
|
|
File.write(file_path, contents)
|
|
end
|
|
|
|
def editor
|
|
ENV['EDITOR']
|
|
end
|
|
|
|
def amend_commit
|
|
fail_with "git add failed" unless system(*%W[git add #{file_path}])
|
|
|
|
Kernel.exec(*%w[git commit --amend])
|
|
end
|
|
|
|
def assert_feature_branch!
|
|
return unless branch_name == 'master'
|
|
|
|
fail_with "Create a branch first!"
|
|
end
|
|
|
|
def assert_new_file!
|
|
return unless File.exist?(file_path)
|
|
return if options.force
|
|
|
|
fail_with "#{file_path} already exists! Use `--force` to overwrite."
|
|
end
|
|
|
|
def assert_title!
|
|
return if options.title.length > 0 || options.amend
|
|
|
|
fail_with "Provide a title for the changelog entry or use `--amend`" \
|
|
" to use the title from the previous commit."
|
|
end
|
|
|
|
def assert_valid_type!
|
|
return unless options.type && options.type == INVALID_TYPE
|
|
|
|
fail_with 'Invalid category given!'
|
|
end
|
|
|
|
def title
|
|
if options.title.empty?
|
|
last_commit_subject
|
|
else
|
|
options.title
|
|
end
|
|
end
|
|
|
|
def last_commit_subject
|
|
capture_stdout(%w[git log --format=%s -1]).strip
|
|
end
|
|
|
|
def file_path
|
|
base_path = File.join(
|
|
unreleased_path,
|
|
branch_name.gsub(/[^\w-]/, '-'))
|
|
|
|
# Add padding for .yml extension
|
|
base_path[0..MAX_FILENAME_LENGTH - 5] + '.yml'
|
|
end
|
|
|
|
def unreleased_path
|
|
path = File.join('changelogs', 'unreleased')
|
|
path = File.join('ee', path) if ee?
|
|
|
|
path
|
|
end
|
|
|
|
def ee?
|
|
@ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__))
|
|
end
|
|
|
|
def branch_name
|
|
@branch_name ||= capture_stdout(%w[git symbolic-ref --short HEAD]).strip
|
|
end
|
|
|
|
def remove_trailing_whitespace(yaml_content)
|
|
yaml_content.gsub(/ +$/, '')
|
|
end
|
|
end
|
|
|
|
if $0 == __FILE__
|
|
begin
|
|
options = ChangelogOptionParser.parse(ARGV)
|
|
ChangelogEntry.new(options).execute
|
|
rescue ChangelogHelpers::Abort => ex
|
|
$stderr.puts ex.message
|
|
exit 1
|
|
rescue ChangelogHelpers::Done
|
|
exit
|
|
end
|
|
end
|
|
|
|
# vim: ft=ruby
|