#!/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! 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 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