#!/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 class ChangelogOptionParser 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 exit 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 $stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}" exit 1 end end def git_user_name %x{git config user.name}.strip end end end class ChangelogEntry attr_reader :options def initialize(options) @options = options assert_feature_branch! 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 contents unless options.dry_run write amend_commit if options.amend 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 amend_commit %x{git add #{file_path}} exec("git commit --amend") end def fail_with(message) $stderr.puts "\e[31merror\e[0m #{message}" exit 1 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 %x{git log --format="%s" -1}.strip end def file_path File.join( unreleased_path, branch_name.gsub(/[^\w-]/, '-') << '.yml' ) end def unreleased_path File.join('changelogs', 'unreleased').tap do |path| path << '-ee' if ee? end end def ee? @ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__)) end def branch_name @branch_name ||= %x{git symbolic-ref --short HEAD}.strip end def remove_trailing_whitespace(yaml_content) yaml_content.gsub(/ +$/, '') end end if $0 == __FILE__ options = ChangelogOptionParser.parse(ARGV) ChangelogEntry.new(options) end # vim: ft=ruby