Merge pull request #587 from eputnam/config_sections
Add option --configure-sections, --add-sections, --include-merged
This commit is contained in:
commit
e2ddb73373
237
lib/github_changelog_generator/generator/entry.rb
Normal file
237
lib/github_changelog_generator/generator/entry.rb
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
require "github_changelog_generator/generator/section"
|
||||||
|
|
||||||
|
module GitHubChangelogGenerator
|
||||||
|
# This class generates the content for a single changelog entry. An entry is
|
||||||
|
# generally either for a specific tagged release or the collection of
|
||||||
|
# unreleased changes.
|
||||||
|
#
|
||||||
|
# An entry is comprised of header text followed by a series of sections
|
||||||
|
# relating to the entry.
|
||||||
|
#
|
||||||
|
# @see GitHubChangelogGenerator::Generator
|
||||||
|
# @see GitHubChangelogGenerator::Section
|
||||||
|
class Entry
|
||||||
|
attr_reader :content
|
||||||
|
|
||||||
|
def initialize(options = Options.new({}))
|
||||||
|
@content = ""
|
||||||
|
@options = Options.new(options)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generates log entry with header and body
|
||||||
|
#
|
||||||
|
# @param [Array] pull_requests List or PR's in new section
|
||||||
|
# @param [Array] issues List of issues in new section
|
||||||
|
# @param [String] newer_tag_name Name of the newer tag. Could be nil for `Unreleased` section.
|
||||||
|
# @param [String] newer_tag_link Name of the newer tag. Could be "HEAD" for `Unreleased` section.
|
||||||
|
# @param [Time] newer_tag_time Time of the newer tag
|
||||||
|
# @param [Hash, nil] older_tag Older tag, used for the links. Could be nil for last tag.
|
||||||
|
# @return [String] Ready and parsed section
|
||||||
|
def create_entry_for_tag(pull_requests, issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name) # rubocop:disable Metrics/ParameterLists
|
||||||
|
github_site = @options[:github_site] || "https://github.com"
|
||||||
|
project_url = "#{github_site}/#{@options[:user]}/#{@options[:project]}"
|
||||||
|
|
||||||
|
set_sections_and_maps
|
||||||
|
|
||||||
|
@content = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
|
||||||
|
|
||||||
|
@content += generate_body(pull_requests, issues)
|
||||||
|
|
||||||
|
@content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Creates section objects and the label and section maps needed for
|
||||||
|
# sorting
|
||||||
|
def set_sections_and_maps
|
||||||
|
@sections = if @options.configure_sections?
|
||||||
|
parse_sections(@options[:configure_sections])
|
||||||
|
elsif @options.add_sections?
|
||||||
|
default_sections.concat parse_sections(@options[:add_sections])
|
||||||
|
else
|
||||||
|
default_sections
|
||||||
|
end
|
||||||
|
|
||||||
|
@lmap = label_map
|
||||||
|
@smap = section_map
|
||||||
|
end
|
||||||
|
|
||||||
|
# Turns a string from the commandline into an array of Section objects
|
||||||
|
#
|
||||||
|
# @param [String, Hash] either string or hash describing sections
|
||||||
|
# @return [Array] array of Section objects
|
||||||
|
def parse_sections(sections_desc)
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
sections_desc = sections_desc.to_json if sections_desc.class == Hash
|
||||||
|
|
||||||
|
begin
|
||||||
|
sections_json = JSON.parse(sections_desc)
|
||||||
|
rescue JSON::ParserError => e
|
||||||
|
raise "There was a problem parsing your JSON string for sections: #{e}"
|
||||||
|
end
|
||||||
|
|
||||||
|
sections_json.collect do |name, v|
|
||||||
|
Section.new(name: name.to_s, prefix: v["prefix"], labels: v["labels"], options: @options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Creates a hash map of labels => section objects
|
||||||
|
#
|
||||||
|
# @return [Hash] map of labels => section objects
|
||||||
|
def label_map
|
||||||
|
@sections.each_with_object({}) do |section_obj, memo|
|
||||||
|
section_obj.labels.each do |label|
|
||||||
|
memo[label] = section_obj.name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Creates a hash map of 'section name' => section object
|
||||||
|
#
|
||||||
|
# @return [Hash] map of 'section name' => section object
|
||||||
|
def section_map
|
||||||
|
@sections.each_with_object({}) do |section, memo|
|
||||||
|
memo[section.name] = section
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# It generates header text for an entry with specific parameters.
|
||||||
|
#
|
||||||
|
# @param [String] newer_tag_name - name of newer tag
|
||||||
|
# @param [String] newer_tag_link - used for links. Could be same as #newer_tag_name or some specific value, like HEAD
|
||||||
|
# @param [Time] newer_tag_time - time, when newer tag created
|
||||||
|
# @param [String] older_tag_name - tag name, used for links.
|
||||||
|
# @param [String] project_url - url for current project.
|
||||||
|
# @return [String] - Header text for a changelog entry.
|
||||||
|
def generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
|
||||||
|
header = ""
|
||||||
|
|
||||||
|
# Generate date string:
|
||||||
|
time_string = newer_tag_time.strftime(@options[:date_format])
|
||||||
|
|
||||||
|
# Generate tag name and link
|
||||||
|
release_url = if @options[:release_url]
|
||||||
|
format(@options[:release_url], newer_tag_link)
|
||||||
|
else
|
||||||
|
"#{project_url}/tree/#{newer_tag_link}"
|
||||||
|
end
|
||||||
|
header += if newer_tag_name.equal?(@options[:unreleased_label])
|
||||||
|
"## [#{newer_tag_name}](#{release_url})\n\n"
|
||||||
|
else
|
||||||
|
"## [#{newer_tag_name}](#{release_url}) (#{time_string})\n\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
if @options[:compare_link] && older_tag_name
|
||||||
|
# Generate compare link
|
||||||
|
header += "[Full Changelog](#{project_url}/compare/#{older_tag_name}...#{newer_tag_link})\n\n"
|
||||||
|
end
|
||||||
|
|
||||||
|
header
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generates complete body text for a tag (without a header)
|
||||||
|
#
|
||||||
|
# @param [Array] pull_requests
|
||||||
|
# @param [Array] issues
|
||||||
|
# @returns [String] ready-to-go tag body
|
||||||
|
def generate_body(pull_requests, issues)
|
||||||
|
body = ""
|
||||||
|
body += main_sections_to_log(pull_requests, issues)
|
||||||
|
body += merged_section_to_log(pull_requests) if @options[:pulls] && @options[:add_pr_wo_labels]
|
||||||
|
body
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generates main sections for a tag
|
||||||
|
#
|
||||||
|
# @param [Array] pull_requests
|
||||||
|
# @param [Array] issues
|
||||||
|
# @return [string] ready-to-go sub-sections
|
||||||
|
def main_sections_to_log(pull_requests, issues)
|
||||||
|
if @options[:issues]
|
||||||
|
sections_to_log = parse_by_sections(pull_requests, issues)
|
||||||
|
|
||||||
|
sections_to_log.map(&:generate_content).join
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generates section for prs with no labels (for a tag)
|
||||||
|
#
|
||||||
|
# @param [Array] pull_requests
|
||||||
|
# @return [string] ready-to-go sub-section
|
||||||
|
def merged_section_to_log(pull_requests)
|
||||||
|
merged = Section.new(name: "merged", prefix: @options[:merge_prefix], labels: [], issues: pull_requests, options: @options)
|
||||||
|
@sections << merged unless @sections.find { |section| section.name == "merged" }
|
||||||
|
merged.generate_content
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set of default sections for backwards-compatibility/defaults
|
||||||
|
#
|
||||||
|
# @return [Array] array of Section objects
|
||||||
|
def default_sections
|
||||||
|
[
|
||||||
|
Section.new(name: "breaking", prefix: @options[:breaking_prefix], labels: @options[:breaking_labels], options: @options),
|
||||||
|
Section.new(name: "enhancements", prefix: @options[:enhancement_prefix], labels: @options[:enhancement_labels], options: @options),
|
||||||
|
Section.new(name: "bugs", prefix: @options[:bug_prefix], labels: @options[:bug_labels], options: @options),
|
||||||
|
Section.new(name: "issues", prefix: @options[:issue_prefix], labels: @options[:issue_labels], options: @options)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method sorts issues by types
|
||||||
|
# (bugs, features, or just closed issues) by labels
|
||||||
|
#
|
||||||
|
# @param [Array] pull_requests
|
||||||
|
# @param [Array] issues
|
||||||
|
# @return [Hash] Mapping of filtered arrays: (Bugs, Enhancements, Breaking stuff, Issues)
|
||||||
|
def parse_by_sections(pull_requests, issues)
|
||||||
|
issues.each do |dict|
|
||||||
|
added = false
|
||||||
|
|
||||||
|
dict["labels"].each do |label|
|
||||||
|
break if @lmap[label["name"]].nil?
|
||||||
|
@smap[@lmap[label["name"]]].issues << dict
|
||||||
|
added = true
|
||||||
|
|
||||||
|
break if added
|
||||||
|
end
|
||||||
|
if @smap["issues"]
|
||||||
|
@sections.find { |sect| sect.name == "issues" }.issues << dict unless added
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sort_pull_requests(pull_requests)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method iterates through PRs and sorts them into sections
|
||||||
|
#
|
||||||
|
# @param [Array] pull_requests
|
||||||
|
# @param [Hash] sections
|
||||||
|
# @return [Hash] sections
|
||||||
|
def sort_pull_requests(pull_requests)
|
||||||
|
added_pull_requests = []
|
||||||
|
pull_requests.each do |pr|
|
||||||
|
added = false
|
||||||
|
|
||||||
|
pr["labels"].each do |label|
|
||||||
|
break if @lmap[label["name"]].nil?
|
||||||
|
@smap[@lmap[label["name"]]].issues << pr
|
||||||
|
added_pull_requests << pr
|
||||||
|
added = true
|
||||||
|
|
||||||
|
break if added
|
||||||
|
end
|
||||||
|
end
|
||||||
|
added_pull_requests.each { |req| pull_requests.delete(req) }
|
||||||
|
@sections
|
||||||
|
end
|
||||||
|
|
||||||
|
def line_labels_for(issue)
|
||||||
|
labels = if @options[:issue_line_labels] == ["ALL"]
|
||||||
|
issue["labels"]
|
||||||
|
else
|
||||||
|
issue["labels"].select { |label| @options[:issue_line_labels].include?(label["name"]) }
|
||||||
|
end
|
||||||
|
labels.map { |label| " \[[#{label['name']}](#{label['url'].sub('api.github.com/repos', 'github.com')})\]" }.join("")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,18 +1,30 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "github_changelog_generator/octo_fetcher"
|
require "github_changelog_generator/octo_fetcher"
|
||||||
require "github_changelog_generator/generator/generator_generation"
|
|
||||||
require "github_changelog_generator/generator/generator_fetcher"
|
require "github_changelog_generator/generator/generator_fetcher"
|
||||||
require "github_changelog_generator/generator/generator_processor"
|
require "github_changelog_generator/generator/generator_processor"
|
||||||
require "github_changelog_generator/generator/generator_tags"
|
require "github_changelog_generator/generator/generator_tags"
|
||||||
|
require "github_changelog_generator/generator/entry"
|
||||||
|
require "github_changelog_generator/generator/section"
|
||||||
|
|
||||||
module GitHubChangelogGenerator
|
module GitHubChangelogGenerator
|
||||||
# Default error for ChangelogGenerator
|
# Default error for ChangelogGenerator
|
||||||
class ChangelogGeneratorError < StandardError
|
class ChangelogGeneratorError < StandardError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This class is the high-level code for gathering issues and PRs for a github
|
||||||
|
# repository and generating a CHANGELOG.md file. A changelog is made up of a
|
||||||
|
# series of "entries" of all tagged releases, plus an extra entry for the
|
||||||
|
# unreleased changes. Entries are made up of various organizational
|
||||||
|
# "sections," and sections contain the github issues and PRs.
|
||||||
|
#
|
||||||
|
# So the changelog contains entries, entries contain sections, and sections
|
||||||
|
# contain issues and PRs.
|
||||||
|
#
|
||||||
|
# @see GitHubChangelogGenerator::Entry
|
||||||
|
# @see GitHubChangelogGenerator::Section
|
||||||
class Generator
|
class Generator
|
||||||
attr_accessor :options, :filtered_tags, :github, :tag_section_mapping, :sorted_tags
|
attr_accessor :options, :filtered_tags, :tag_section_mapping, :sorted_tags
|
||||||
|
|
||||||
# A Generator responsible for all logic, related with changelog generation from ready-to-parse issues
|
# A Generator responsible for all logic, related with changelog generation from ready-to-parse issues
|
||||||
#
|
#
|
||||||
|
@ -23,6 +35,104 @@ module GitHubChangelogGenerator
|
||||||
@options = options
|
@options = options
|
||||||
@tag_times_hash = {}
|
@tag_times_hash = {}
|
||||||
@fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
|
@fetcher = GitHubChangelogGenerator::OctoFetcher.new(options)
|
||||||
|
@sections = []
|
||||||
|
end
|
||||||
|
|
||||||
|
# Main function to start changelog generation
|
||||||
|
#
|
||||||
|
# @return [String] Generated changelog file
|
||||||
|
def compound_changelog
|
||||||
|
options.load_custom_ruby_files
|
||||||
|
fetch_and_filter_tags
|
||||||
|
fetch_issues_and_pr
|
||||||
|
|
||||||
|
log = ""
|
||||||
|
log += options[:frontmatter] if options[:frontmatter]
|
||||||
|
log += "#{options[:header]}\n\n"
|
||||||
|
|
||||||
|
log += if options[:unreleased_only]
|
||||||
|
generate_entry_between_tags(filtered_tags[0], nil)
|
||||||
|
else
|
||||||
|
generate_entries_for_all_tags
|
||||||
|
end
|
||||||
|
|
||||||
|
log += File.read(options[:base]) if File.file?(options[:base])
|
||||||
|
|
||||||
|
credit_line = "\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*"
|
||||||
|
log.gsub!(credit_line, "") # Remove old credit lines
|
||||||
|
log += credit_line
|
||||||
|
|
||||||
|
@log = log
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Generate log only between 2 specified tags
|
||||||
|
# @param [String] older_tag all issues before this tag date will be excluded. May be nil, if it's first tag
|
||||||
|
# @param [String] newer_tag all issue after this tag will be excluded. May be nil for unreleased section
|
||||||
|
def generate_entry_between_tags(older_tag, newer_tag)
|
||||||
|
filtered_issues, filtered_pull_requests = filter_issues_for_tags(newer_tag, older_tag)
|
||||||
|
|
||||||
|
if newer_tag.nil? && filtered_issues.empty? && filtered_pull_requests.empty?
|
||||||
|
# do not generate empty unreleased section
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
newer_tag_link, newer_tag_name, newer_tag_time = detect_link_tag_time(newer_tag)
|
||||||
|
|
||||||
|
# If the older tag is nil, go back in time from the latest tag and find
|
||||||
|
# the SHA for the first commit.
|
||||||
|
older_tag_name =
|
||||||
|
if older_tag.nil?
|
||||||
|
@fetcher.commits_before(newer_tag_time).last["sha"]
|
||||||
|
else
|
||||||
|
older_tag["name"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Entry.new(options).create_entry_for_tag(filtered_pull_requests, filtered_issues, newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Filters issues and pull requests based on, respectively, `closed_at` and `merged_at`
|
||||||
|
# timestamp fields.
|
||||||
|
#
|
||||||
|
# @return [Array] filtered issues and pull requests
|
||||||
|
def filter_issues_for_tags(newer_tag, older_tag)
|
||||||
|
filtered_pull_requests = delete_by_time(@pull_requests, "merged_at", older_tag, newer_tag)
|
||||||
|
filtered_issues = delete_by_time(@issues, "closed_at", older_tag, newer_tag)
|
||||||
|
|
||||||
|
newer_tag_name = newer_tag.nil? ? nil : newer_tag["name"]
|
||||||
|
|
||||||
|
if options[:filter_issues_by_milestone]
|
||||||
|
# delete excess irrelevant issues (according milestones). Issue #22.
|
||||||
|
filtered_issues = filter_by_milestone(filtered_issues, newer_tag_name, @issues)
|
||||||
|
filtered_pull_requests = filter_by_milestone(filtered_pull_requests, newer_tag_name, @pull_requests)
|
||||||
|
end
|
||||||
|
[filtered_issues, filtered_pull_requests]
|
||||||
|
end
|
||||||
|
|
||||||
|
# The full cycle of generation for whole project
|
||||||
|
# @return [String] All entries in the changelog
|
||||||
|
def generate_entries_for_all_tags
|
||||||
|
puts "Generating entry..." if options[:verbose]
|
||||||
|
|
||||||
|
entries = generate_unreleased_entry
|
||||||
|
|
||||||
|
@tag_section_mapping.each_pair do |_tag_section, left_right_tags|
|
||||||
|
older_tag, newer_tag = left_right_tags
|
||||||
|
entries += generate_entry_between_tags(older_tag, newer_tag)
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_unreleased_entry
|
||||||
|
entry = ""
|
||||||
|
if options[:unreleased]
|
||||||
|
start_tag = filtered_tags[0] || sorted_tags.last
|
||||||
|
unreleased_entry = generate_entry_between_tags(start_tag, nil)
|
||||||
|
entry += unreleased_entry if unreleased_entry
|
||||||
|
end
|
||||||
|
entry
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_issues_and_pr
|
def fetch_issues_and_pr
|
||||||
|
@ -35,144 +145,5 @@ module GitHubChangelogGenerator
|
||||||
fetch_events_for_issues_and_pr
|
fetch_events_for_issues_and_pr
|
||||||
detect_actual_closed_dates(@issues + @pull_requests)
|
detect_actual_closed_dates(@issues + @pull_requests)
|
||||||
end
|
end
|
||||||
|
|
||||||
ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
|
|
||||||
|
|
||||||
# Encapsulate characters to make Markdown look as expected.
|
|
||||||
#
|
|
||||||
# @param [String] string
|
|
||||||
# @return [String] encapsulated input string
|
|
||||||
def encapsulate_string(string)
|
|
||||||
string = string.gsub('\\', '\\\\')
|
|
||||||
|
|
||||||
ENCAPSULATED_CHARACTERS.each do |char|
|
|
||||||
string = string.gsub(char, "\\#{char}")
|
|
||||||
end
|
|
||||||
|
|
||||||
string
|
|
||||||
end
|
|
||||||
|
|
||||||
# Generates log for section with header and body
|
|
||||||
#
|
|
||||||
# @param [Array] pull_requests List or PR's in new section
|
|
||||||
# @param [Array] issues List of issues in new section
|
|
||||||
# @param [String] newer_tag Name of the newer tag. Could be nil for `Unreleased` section
|
|
||||||
# @param [Hash, nil] older_tag Older tag, used for the links. Could be nil for last tag.
|
|
||||||
# @return [String] Ready and parsed section
|
|
||||||
def create_log_for_tag(pull_requests, issues, newer_tag, older_tag = nil)
|
|
||||||
newer_tag_link, newer_tag_name, newer_tag_time = detect_link_tag_time(newer_tag)
|
|
||||||
|
|
||||||
github_site = options[:github_site] || "https://github.com"
|
|
||||||
project_url = "#{github_site}/#{options[:user]}/#{options[:project]}"
|
|
||||||
|
|
||||||
# If the older tag is nil, go back in time from the latest tag and find
|
|
||||||
# the SHA for the first commit.
|
|
||||||
older_tag_name =
|
|
||||||
if older_tag.nil?
|
|
||||||
@fetcher.commits_before(newer_tag_time).last["sha"]
|
|
||||||
else
|
|
||||||
older_tag["name"]
|
|
||||||
end
|
|
||||||
|
|
||||||
log = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
|
|
||||||
|
|
||||||
if options[:issues]
|
|
||||||
# Generate issues:
|
|
||||||
log += issues_to_log(issues, pull_requests)
|
|
||||||
end
|
|
||||||
|
|
||||||
if options[:pulls] && options[:add_pr_wo_labels]
|
|
||||||
# Generate pull requests:
|
|
||||||
log += generate_sub_section(pull_requests, options[:merge_prefix])
|
|
||||||
end
|
|
||||||
|
|
||||||
log
|
|
||||||
end
|
|
||||||
|
|
||||||
# Generate ready-to-paste log from list of issues and pull requests.
|
|
||||||
#
|
|
||||||
# @param [Array] issues
|
|
||||||
# @param [Array] pull_requests
|
|
||||||
# @return [String] generated log for issues
|
|
||||||
def issues_to_log(issues, pull_requests)
|
|
||||||
sections = parse_by_sections(issues, pull_requests)
|
|
||||||
|
|
||||||
log = ""
|
|
||||||
log += generate_sub_section(sections[:breaking], options[:breaking_prefix])
|
|
||||||
log += generate_sub_section(sections[:enhancements], options[:enhancement_prefix])
|
|
||||||
log += generate_sub_section(sections[:bugs], options[:bug_prefix])
|
|
||||||
log += generate_sub_section(sections[:issues], options[:issue_prefix])
|
|
||||||
log
|
|
||||||
end
|
|
||||||
|
|
||||||
# This method sort issues by types
|
|
||||||
# (bugs, features, or just closed issues) by labels
|
|
||||||
#
|
|
||||||
# @param [Array] issues
|
|
||||||
# @param [Array] pull_requests
|
|
||||||
# @return [Hash] Mapping of filtered arrays: (Bugs, Enhancements, Breaking stuff, Issues)
|
|
||||||
def parse_by_sections(issues, pull_requests)
|
|
||||||
sections = {
|
|
||||||
issues: [],
|
|
||||||
enhancements: [],
|
|
||||||
bugs: [],
|
|
||||||
breaking: []
|
|
||||||
}
|
|
||||||
|
|
||||||
issues.each do |dict|
|
|
||||||
added = false
|
|
||||||
|
|
||||||
dict["labels"].each do |label|
|
|
||||||
if options[:bug_labels].include?(label["name"])
|
|
||||||
sections[:bugs] << dict
|
|
||||||
added = true
|
|
||||||
elsif options[:enhancement_labels].include?(label["name"])
|
|
||||||
sections[:enhancements] << dict
|
|
||||||
added = true
|
|
||||||
elsif options[:breaking_labels].include?(label["name"])
|
|
||||||
sections[:breaking] << dict
|
|
||||||
added = true
|
|
||||||
end
|
|
||||||
|
|
||||||
break if added
|
|
||||||
end
|
|
||||||
|
|
||||||
sections[:issues] << dict unless added
|
|
||||||
end
|
|
||||||
|
|
||||||
sort_pull_requests(pull_requests, sections)
|
|
||||||
end
|
|
||||||
|
|
||||||
# This method iterates through PRs and sorts them into sections
|
|
||||||
#
|
|
||||||
# @param [Array] pull_requests
|
|
||||||
# @param [Hash] sections
|
|
||||||
# @return [Hash] sections
|
|
||||||
def sort_pull_requests(pull_requests, sections)
|
|
||||||
added_pull_requests = []
|
|
||||||
pull_requests.each do |pr|
|
|
||||||
added = false
|
|
||||||
|
|
||||||
pr["labels"].each do |label|
|
|
||||||
if options[:bug_labels].include?(label["name"])
|
|
||||||
sections[:bugs] << pr
|
|
||||||
added_pull_requests << pr
|
|
||||||
added = true
|
|
||||||
elsif options[:enhancement_labels].include?(label["name"])
|
|
||||||
sections[:enhancements] << pr
|
|
||||||
added_pull_requests << pr
|
|
||||||
added = true
|
|
||||||
elsif options[:breaking_labels].include?(label["name"])
|
|
||||||
sections[:breaking] << pr
|
|
||||||
added_pull_requests << pr
|
|
||||||
added = true
|
|
||||||
end
|
|
||||||
|
|
||||||
break if added
|
|
||||||
end
|
|
||||||
end
|
|
||||||
added_pull_requests.each { |p| pull_requests.delete(p) }
|
|
||||||
sections
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module GitHubChangelogGenerator
|
|
||||||
class Generator
|
|
||||||
# Main function to start changelog generation
|
|
||||||
#
|
|
||||||
# @return [String] Generated changelog file
|
|
||||||
def compound_changelog
|
|
||||||
options.load_custom_ruby_files
|
|
||||||
fetch_and_filter_tags
|
|
||||||
fetch_issues_and_pr
|
|
||||||
|
|
||||||
log = ""
|
|
||||||
log += options[:frontmatter] if options[:frontmatter]
|
|
||||||
log += "#{options[:header]}\n\n"
|
|
||||||
|
|
||||||
log += if options[:unreleased_only]
|
|
||||||
generate_log_between_tags(filtered_tags[0], nil)
|
|
||||||
else
|
|
||||||
generate_log_for_all_tags
|
|
||||||
end
|
|
||||||
|
|
||||||
log += File.read(options[:base]) if File.file?(options[:base])
|
|
||||||
|
|
||||||
credit_line = "\n\n\\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*"
|
|
||||||
log.gsub!(credit_line, "") # Remove old credit lines
|
|
||||||
log += credit_line
|
|
||||||
|
|
||||||
@log = log
|
|
||||||
end
|
|
||||||
|
|
||||||
# @param [Array] issues List of issues on sub-section
|
|
||||||
# @param [String] prefix Name of sub-section
|
|
||||||
# @return [String] Generate ready-to-go sub-section
|
|
||||||
def generate_sub_section(issues, prefix)
|
|
||||||
log = ""
|
|
||||||
|
|
||||||
if issues.any?
|
|
||||||
log += "#{prefix}\n\n" unless options[:simple_list]
|
|
||||||
issues.each do |issue|
|
|
||||||
merge_string = get_string_for_issue(issue)
|
|
||||||
log += "- #{merge_string}\n"
|
|
||||||
end
|
|
||||||
log += "\n"
|
|
||||||
end
|
|
||||||
log
|
|
||||||
end
|
|
||||||
|
|
||||||
# It generate one header for section with specific parameters.
|
|
||||||
#
|
|
||||||
# @param [String] newer_tag_name - name of newer tag
|
|
||||||
# @param [String] newer_tag_link - used for links. Could be same as #newer_tag_name or some specific value, like HEAD
|
|
||||||
# @param [Time] newer_tag_time - time, when newer tag created
|
|
||||||
# @param [String] older_tag_link - tag name, used for links.
|
|
||||||
# @param [String] project_url - url for current project.
|
|
||||||
# @return [String] - Generate one ready-to-add section.
|
|
||||||
def generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_link, project_url)
|
|
||||||
log = ""
|
|
||||||
|
|
||||||
# Generate date string:
|
|
||||||
time_string = newer_tag_time.strftime(options[:date_format])
|
|
||||||
|
|
||||||
# Generate tag name and link
|
|
||||||
release_url = if options[:release_url]
|
|
||||||
format(options[:release_url], newer_tag_link)
|
|
||||||
else
|
|
||||||
"#{project_url}/tree/#{newer_tag_link}"
|
|
||||||
end
|
|
||||||
log += if newer_tag_name.equal?(options[:unreleased_label])
|
|
||||||
"## [#{newer_tag_name}](#{release_url})\n\n"
|
|
||||||
else
|
|
||||||
"## [#{newer_tag_name}](#{release_url}) (#{time_string})\n\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
if options[:compare_link] && older_tag_link
|
|
||||||
# Generate compare link
|
|
||||||
log += "[Full Changelog](#{project_url}/compare/#{older_tag_link}...#{newer_tag_link})\n\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
log
|
|
||||||
end
|
|
||||||
|
|
||||||
# Generate log only between 2 specified tags
|
|
||||||
# @param [String] older_tag all issues before this tag date will be excluded. May be nil, if it's first tag
|
|
||||||
# @param [String] newer_tag all issue after this tag will be excluded. May be nil for unreleased section
|
|
||||||
def generate_log_between_tags(older_tag, newer_tag)
|
|
||||||
filtered_issues, filtered_pull_requests = filter_issues_for_tags(newer_tag, older_tag)
|
|
||||||
|
|
||||||
if newer_tag.nil? && filtered_issues.empty? && filtered_pull_requests.empty?
|
|
||||||
# do not generate empty unreleased section
|
|
||||||
return ""
|
|
||||||
end
|
|
||||||
|
|
||||||
create_log_for_tag(filtered_pull_requests, filtered_issues, newer_tag, older_tag)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Filters issues and pull requests based on, respectively, `closed_at` and `merged_at`
|
|
||||||
# timestamp fields.
|
|
||||||
#
|
|
||||||
# @return [Array] filtered issues and pull requests
|
|
||||||
def filter_issues_for_tags(newer_tag, older_tag)
|
|
||||||
filtered_pull_requests = delete_by_time(@pull_requests, "merged_at", older_tag, newer_tag)
|
|
||||||
filtered_issues = delete_by_time(@issues, "closed_at", older_tag, newer_tag)
|
|
||||||
|
|
||||||
newer_tag_name = newer_tag.nil? ? nil : newer_tag["name"]
|
|
||||||
|
|
||||||
if options[:filter_issues_by_milestone]
|
|
||||||
# delete excess irrelevant issues (according milestones). Issue #22.
|
|
||||||
filtered_issues = filter_by_milestone(filtered_issues, newer_tag_name, @issues)
|
|
||||||
filtered_pull_requests = filter_by_milestone(filtered_pull_requests, newer_tag_name, @pull_requests)
|
|
||||||
end
|
|
||||||
[filtered_issues, filtered_pull_requests]
|
|
||||||
end
|
|
||||||
|
|
||||||
# The full cycle of generation for whole project
|
|
||||||
# @return [String] The complete changelog
|
|
||||||
def generate_log_for_all_tags
|
|
||||||
puts "Generating log..." if options[:verbose]
|
|
||||||
|
|
||||||
log = generate_unreleased_section
|
|
||||||
|
|
||||||
@tag_section_mapping.each_pair do |_tag_section, left_right_tags|
|
|
||||||
older_tag, newer_tag = left_right_tags
|
|
||||||
log += generate_log_between_tags(older_tag, newer_tag)
|
|
||||||
end
|
|
||||||
|
|
||||||
log
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_unreleased_section
|
|
||||||
log = ""
|
|
||||||
if options[:unreleased]
|
|
||||||
start_tag = filtered_tags[0] || sorted_tags.last
|
|
||||||
unreleased_log = generate_log_between_tags(start_tag, nil)
|
|
||||||
log += unreleased_log if unreleased_log
|
|
||||||
end
|
|
||||||
log
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse issue and generate single line formatted issue line.
|
|
||||||
#
|
|
||||||
# Example output:
|
|
||||||
# - Add coveralls integration [\#223](https://github.com/skywinder/github-changelog-generator/pull/223) (@skywinder)
|
|
||||||
#
|
|
||||||
# @param [Hash] issue Fetched issue from GitHub
|
|
||||||
# @return [String] Markdown-formatted single issue
|
|
||||||
def get_string_for_issue(issue)
|
|
||||||
encapsulated_title = encapsulate_string issue["title"]
|
|
||||||
|
|
||||||
title_with_number = "#{encapsulated_title} [\\##{issue['number']}](#{issue['html_url']})"
|
|
||||||
if options[:issue_line_labels].present?
|
|
||||||
title_with_number = "#{title_with_number}#{line_labels_for(issue)}"
|
|
||||||
end
|
|
||||||
issue_line_with_user(title_with_number, issue)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def line_labels_for(issue)
|
|
||||||
labels = if options[:issue_line_labels] == ["ALL"]
|
|
||||||
issue["labels"]
|
|
||||||
else
|
|
||||||
issue["labels"].select { |label| options[:issue_line_labels].include?(label["name"]) }
|
|
||||||
end
|
|
||||||
labels.map { |label| " \[[#{label['name']}](#{label['url'].sub('api.github.com/repos', 'github.com')})\]" }.join("")
|
|
||||||
end
|
|
||||||
|
|
||||||
def issue_line_with_user(line, issue)
|
|
||||||
return line if !options[:author] || issue["pull_request"].nil?
|
|
||||||
|
|
||||||
user = issue["user"]
|
|
||||||
return "#{line} ({Null user})" unless user
|
|
||||||
|
|
||||||
if options[:usernames_as_github_logins]
|
|
||||||
"#{line} (@#{user['login']})"
|
|
||||||
else
|
|
||||||
"#{line} ([#{user['login']}](#{user['html_url']}))"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
83
lib/github_changelog_generator/generator/section.rb
Normal file
83
lib/github_changelog_generator/generator/section.rb
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
module GitHubChangelogGenerator
|
||||||
|
# This class generates the content for a single section of a changelog entry.
|
||||||
|
# It turns the tagged issues and PRs into a well-formatted list of changes to
|
||||||
|
# be later incorporated into a changelog entry.
|
||||||
|
#
|
||||||
|
# @see GitHubChangelogGenerator::Entry
|
||||||
|
class Section
|
||||||
|
attr_accessor :name, :prefix, :issues, :labels
|
||||||
|
|
||||||
|
def initialize(opts = {})
|
||||||
|
@name = opts[:name]
|
||||||
|
@prefix = opts[:prefix]
|
||||||
|
@labels = opts[:labels] || []
|
||||||
|
@issues = opts[:issues] || []
|
||||||
|
@options = opts[:options] || Options.new({})
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [Array] issues List of issues on sub-section
|
||||||
|
# @param [String] prefix Name of sub-section
|
||||||
|
# @return [String] Generate section content
|
||||||
|
def generate_content
|
||||||
|
content = ""
|
||||||
|
|
||||||
|
if @issues.any?
|
||||||
|
content += "#{@prefix}\n\n" unless @options[:simple_list]
|
||||||
|
@issues.each do |issue|
|
||||||
|
merge_string = get_string_for_issue(issue)
|
||||||
|
content += "- #{merge_string}\n"
|
||||||
|
end
|
||||||
|
content += "\n"
|
||||||
|
end
|
||||||
|
content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Parse issue and generate single line formatted issue line.
|
||||||
|
#
|
||||||
|
# Example output:
|
||||||
|
# - Add coveralls integration [\#223](https://github.com/skywinder/github-changelog-generator/pull/223) (@skywinder)
|
||||||
|
#
|
||||||
|
# @param [Hash] issue Fetched issue from GitHub
|
||||||
|
# @return [String] Markdown-formatted single issue
|
||||||
|
def get_string_for_issue(issue)
|
||||||
|
encapsulated_title = encapsulate_string issue["title"]
|
||||||
|
|
||||||
|
title_with_number = "#{encapsulated_title} [\\##{issue['number']}](#{issue['html_url']})"
|
||||||
|
if @options[:issue_line_labels].present?
|
||||||
|
title_with_number = "#{title_with_number}#{line_labels_for(issue)}"
|
||||||
|
end
|
||||||
|
issue_line_with_user(title_with_number, issue)
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue_line_with_user(line, issue)
|
||||||
|
return line if !@options[:author] || issue["pull_request"].nil?
|
||||||
|
|
||||||
|
user = issue["user"]
|
||||||
|
return "#{line} ({Null user})" unless user
|
||||||
|
|
||||||
|
if @options[:usernames_as_github_logins]
|
||||||
|
"#{line} (@#{user['login']})"
|
||||||
|
else
|
||||||
|
"#{line} ([#{user['login']}](#{user['html_url']}))"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ENCAPSULATED_CHARACTERS = %w(< > * _ \( \) [ ] #)
|
||||||
|
|
||||||
|
# Encapsulate characters to make Markdown look as expected.
|
||||||
|
#
|
||||||
|
# @param [String] string
|
||||||
|
# @return [String] encapsulated input string
|
||||||
|
def encapsulate_string(string)
|
||||||
|
string = string.gsub('\\', '\\\\')
|
||||||
|
|
||||||
|
ENCAPSULATED_CHARACTERS.each do |char|
|
||||||
|
string = string.gsub(char, "\\#{char}")
|
||||||
|
end
|
||||||
|
|
||||||
|
string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,6 +15,7 @@ module GitHubChangelogGenerator
|
||||||
KNOWN_OPTIONS = %i[
|
KNOWN_OPTIONS = %i[
|
||||||
add_issues_wo_labels
|
add_issues_wo_labels
|
||||||
add_pr_wo_labels
|
add_pr_wo_labels
|
||||||
|
add_sections
|
||||||
author
|
author
|
||||||
base
|
base
|
||||||
between_tags
|
between_tags
|
||||||
|
@ -29,6 +30,7 @@ module GitHubChangelogGenerator
|
||||||
enhancement_prefix
|
enhancement_prefix
|
||||||
breaking_labels
|
breaking_labels
|
||||||
breaking_prefix
|
breaking_prefix
|
||||||
|
configure_sections
|
||||||
exclude_labels
|
exclude_labels
|
||||||
exclude_tags
|
exclude_tags
|
||||||
exclude_tags_regex
|
exclude_tags_regex
|
||||||
|
@ -100,6 +102,16 @@ module GitHubChangelogGenerator
|
||||||
puts ""
|
puts ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Boolean method for whether the user is using configure_sections
|
||||||
|
def configure_sections?
|
||||||
|
!self[:configure_sections].nil? && !self[:configure_sections].empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Boolean method for whether the user is using add_sections
|
||||||
|
def add_sections?
|
||||||
|
!self[:add_sections].nil? && !self[:add_sections].empty?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def values
|
def values
|
||||||
|
|
|
@ -71,6 +71,12 @@ module GitHubChangelogGenerator
|
||||||
opts.on("--header-label [LABEL]", "Setup custom header label. Default is \"# Changelog\"") do |v|
|
opts.on("--header-label [LABEL]", "Setup custom header label. Default is \"# Changelog\"") do |v|
|
||||||
options[:header] = v
|
options[:header] = v
|
||||||
end
|
end
|
||||||
|
opts.on("--configure-sections [Hash, String]", "Define your own set of sections which overrides all default sections") do |v|
|
||||||
|
options[:configure_sections] = v
|
||||||
|
end
|
||||||
|
opts.on("--add-sections [Hash, String]", "Add new sections but keep the default sections") do |v|
|
||||||
|
options[:add_sections] = v
|
||||||
|
end
|
||||||
opts.on("--front-matter [JSON]", "Add YAML front matter. Formatted as JSON because it's easier to add on the command line") do |v|
|
opts.on("--front-matter [JSON]", "Add YAML front matter. Formatted as JSON because it's easier to add on the command line") do |v|
|
||||||
options[:frontmatter] = JSON.parse(v).to_yaml + "---\n"
|
options[:frontmatter] = JSON.parse(v).to_yaml + "---\n"
|
||||||
end
|
end
|
||||||
|
@ -209,6 +215,8 @@ module GitHubChangelogGenerator
|
||||||
bug_labels: ["bug", "Bug", "Type: Bug"],
|
bug_labels: ["bug", "Bug", "Type: Bug"],
|
||||||
exclude_labels: ["duplicate", "question", "invalid", "wontfix", "Duplicate", "Question", "Invalid", "Wontfix", "Meta: Exclude From Changelog"],
|
exclude_labels: ["duplicate", "question", "invalid", "wontfix", "Duplicate", "Question", "Invalid", "Wontfix", "Meta: Exclude From Changelog"],
|
||||||
breaking_labels: %w[backwards-incompatible breaking],
|
breaking_labels: %w[backwards-incompatible breaking],
|
||||||
|
configure_sections: {},
|
||||||
|
add_sections: {},
|
||||||
issue_line_labels: [],
|
issue_line_labels: [],
|
||||||
max_issues: nil,
|
max_issues: nil,
|
||||||
simple_list: false,
|
simple_list: false,
|
||||||
|
|
|
@ -10,7 +10,6 @@ module GitHubChangelogGenerator
|
||||||
|
|
||||||
OPTIONS = %w[ user project token date_format output
|
OPTIONS = %w[ user project token date_format output
|
||||||
bug_prefix enhancement_prefix issue_prefix
|
bug_prefix enhancement_prefix issue_prefix
|
||||||
breaking_labels issue_line_labels
|
|
||||||
header merge_prefix issues
|
header merge_prefix issues
|
||||||
add_issues_wo_labels add_pr_wo_labels
|
add_issues_wo_labels add_pr_wo_labels
|
||||||
pulls filter_issues_by_milestone author
|
pulls filter_issues_by_milestone author
|
||||||
|
@ -20,7 +19,7 @@ module GitHubChangelogGenerator
|
||||||
between_tags exclude_tags exclude_tags_regex since_tag max_issues
|
between_tags exclude_tags exclude_tags_regex since_tag max_issues
|
||||||
github_site github_endpoint simple_list
|
github_site github_endpoint simple_list
|
||||||
future_release release_branch verbose release_url
|
future_release release_branch verbose release_url
|
||||||
base ]
|
base configure_sections add_sections]
|
||||||
|
|
||||||
OPTIONS.each do |o|
|
OPTIONS.each do |o|
|
||||||
attr_accessor o.to_sym
|
attr_accessor o.to_sym
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.\" generated with Ronn/v0.7.3
|
.\" generated with Ronn/v0.7.3
|
||||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||||
.
|
.
|
||||||
.TH "GIT\-GENERATE\-CHANGELOG" "1" "October 2017" "" ""
|
.TH "GIT\-GENERATE\-CHANGELOG" "1" "December 2017" "" ""
|
||||||
.
|
.
|
||||||
.SH "NAME"
|
.SH "NAME"
|
||||||
\fBgit\-generate\-changelog\fR \- Generate changelog from github
|
\fBgit\-generate\-changelog\fR \- Generate changelog from github
|
||||||
|
@ -284,6 +284,18 @@ Paths to Ruby file(s) to require before generating changelog\.
|
||||||
Run verbosely\. Default is true
|
Run verbosely\. Default is true
|
||||||
.
|
.
|
||||||
.P
|
.P
|
||||||
|
\-\-configure\-sections [HASH, STRING]
|
||||||
|
.
|
||||||
|
.P
|
||||||
|
Define your own set of sections which overrides all default sections") do |v|
|
||||||
|
.
|
||||||
|
.P
|
||||||
|
\-\-add\-sections [HASH, STRING]
|
||||||
|
.
|
||||||
|
.P
|
||||||
|
Add new sections but keep the default sections"
|
||||||
|
.
|
||||||
|
.P
|
||||||
\-v, \-\-version
|
\-v, \-\-version
|
||||||
.
|
.
|
||||||
.P
|
.P
|
||||||
|
|
|
@ -261,13 +261,21 @@
|
||||||
|
|
||||||
<p> Run verbosely. Default is true</p>
|
<p> Run verbosely. Default is true</p>
|
||||||
|
|
||||||
<p> -v, --version</p>
|
<p> --configure-sections [HASH, STRING]</p>
|
||||||
|
|
||||||
<p> Print version number</p>
|
<p> Define your own set of sections which overrides all default sections") do |v|</p>
|
||||||
|
|
||||||
<p> -h, --help</p>
|
<p> --add-sections [HASH, STRING]</p>
|
||||||
|
|
||||||
<p> Displays Help</p>
|
<p> Add new sections but keep the default sections"</p>
|
||||||
|
|
||||||
|
<p> -v, --version</p>
|
||||||
|
|
||||||
|
<p> Print version number</p>
|
||||||
|
|
||||||
|
<p> -h, --help</p>
|
||||||
|
|
||||||
|
<p> Displays Help</p>
|
||||||
|
|
||||||
<h2 id="EXAMPLES">EXAMPLES</h2>
|
<h2 id="EXAMPLES">EXAMPLES</h2>
|
||||||
|
|
||||||
|
@ -286,7 +294,7 @@
|
||||||
|
|
||||||
<ol class='man-decor man-foot man foot'>
|
<ol class='man-decor man-foot man foot'>
|
||||||
<li class='tl'></li>
|
<li class='tl'></li>
|
||||||
<li class='tc'>October 2017</li>
|
<li class='tc'>December 2017</li>
|
||||||
<li class='tr'>git-generate-changelog(1)</li>
|
<li class='tr'>git-generate-changelog(1)</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
|
|
@ -220,6 +220,18 @@
|
||||||
|
|
||||||
<p> Put the unreleased changes in the specified release number.</p>
|
<p> Put the unreleased changes in the specified release number.</p>
|
||||||
|
|
||||||
|
<p> --configure-sections [HASH, STRING]</p>
|
||||||
|
|
||||||
|
<p> Define your own set of sections which overrides all default sections") do |v|</p>
|
||||||
|
|
||||||
|
<p> --add-sections [HASH, STRING]</p>
|
||||||
|
|
||||||
|
<p> Add new sections but keep the default sections"</p>
|
||||||
|
|
||||||
|
<p> --include-merged</p>
|
||||||
|
|
||||||
|
<p> If configure_sections is set, use this to restore the merged pull requests sections</p>
|
||||||
|
|
||||||
<p> --[no-]verbose</p>
|
<p> --[no-]verbose</p>
|
||||||
|
|
||||||
<p> Run verbosely. Default is true</p>
|
<p> Run verbosely. Default is true</p>
|
||||||
|
|
|
@ -187,13 +187,21 @@ Automatically generate changelog from your tags, issues, labels and pull request
|
||||||
|
|
||||||
Run verbosely. Default is true
|
Run verbosely. Default is true
|
||||||
|
|
||||||
-v, --version
|
--configure-sections [HASH, STRING]
|
||||||
|
|
||||||
Print version number
|
Define your own set of sections which overrides all default sections") do |v|
|
||||||
|
|
||||||
-h, --help
|
--add-sections [HASH, STRING]
|
||||||
|
|
||||||
Displays Help
|
Add new sections but keep the default sections"
|
||||||
|
|
||||||
|
-v, --version
|
||||||
|
|
||||||
|
Print version number
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
|
||||||
|
Displays Help
|
||||||
|
|
||||||
|
|
||||||
## EXAMPLES
|
## EXAMPLES
|
||||||
|
|
363
spec/unit/generator/entry_spec.rb
Normal file
363
spec/unit/generator/entry_spec.rb
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/ModuleLength
|
||||||
|
module GitHubChangelogGenerator
|
||||||
|
RSpec.describe Entry do
|
||||||
|
def label(name)
|
||||||
|
{ "name" => name }
|
||||||
|
end
|
||||||
|
|
||||||
|
def issue(title, labels, number = "1", user = { "login" => "user" })
|
||||||
|
{
|
||||||
|
"title" => "issue #{title}",
|
||||||
|
"labels" => labels.map { |l| label(l) },
|
||||||
|
"number" => number,
|
||||||
|
"html_url" => "https://github.com/owner/repo/issue/#{number}",
|
||||||
|
"user" => user
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def pr(title, labels, number = "1", user = { "login" => "user" })
|
||||||
|
{
|
||||||
|
"pull_request" => true,
|
||||||
|
"title" => "pr #{title}",
|
||||||
|
"labels" => labels.map { |l| label(l) },
|
||||||
|
"number" => number,
|
||||||
|
"html_url" => "https://github.com/owner/repo/pull/#{number}",
|
||||||
|
"user" => user.merge("html_url" => "https://github.com/#{user['login']}")
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def titles_for(issues)
|
||||||
|
issues.map { |issue| issue["title"] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_sections
|
||||||
|
%w[enhancements bugs breaking issues]
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#create_entry_for_tag" do
|
||||||
|
let(:options) do
|
||||||
|
Parser.default_options.merge(
|
||||||
|
user: "owner",
|
||||||
|
project: "repo",
|
||||||
|
bug_labels: ["bug"],
|
||||||
|
enhancement_labels: ["enhancement"],
|
||||||
|
breaking_labels: ["breaking"]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:issues) do
|
||||||
|
[
|
||||||
|
issue("no labels", [], "5", "login" => "user1"),
|
||||||
|
issue("enhancement", ["enhancement"], "6", "login" => "user2"),
|
||||||
|
issue("bug", ["bug"], "7", "login" => "user1"),
|
||||||
|
issue("breaking", ["breaking"], "8", "login" => "user5"),
|
||||||
|
issue("all the labels", %w[enhancement bug breaking], "9", "login" => "user9")
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:pull_requests) do
|
||||||
|
[
|
||||||
|
pr("no labels", [], "10", "login" => "user1"),
|
||||||
|
pr("enhancement", ["enhancement"], "11", "login" => "user5"),
|
||||||
|
pr("bug", ["bug"], "12", "login" => "user5"),
|
||||||
|
pr("breaking", ["breaking"], "13", "login" => "user5"),
|
||||||
|
pr("all the labels", %w[enhancement bug breaking], "14", "login" => "user5")
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(options) }
|
||||||
|
|
||||||
|
it "generates a header and body" do
|
||||||
|
expect(subject.create_entry_for_tag(pull_requests, issues, "1.0.1", "1.0.1", Time.new(2017, 12, 4), "1.0.0")).to eq(<<-CHANGELOG.gsub(/^ {8}/, "")
|
||||||
|
## [1.0.1](https://github.com/owner/repo/tree/1.0.1) (2017-12-04)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/owner/repo/compare/1.0.0...1.0.1)
|
||||||
|
|
||||||
|
**Breaking changes:**
|
||||||
|
|
||||||
|
- issue breaking [\\#8](https://github.com/owner/repo/issue/8)
|
||||||
|
- pr breaking [\\#13](https://github.com/owner/repo/pull/13) ([user5](https://github.com/user5))
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- issue enhancement [\\#6](https://github.com/owner/repo/issue/6)
|
||||||
|
- issue all the labels [\\#9](https://github.com/owner/repo/issue/9)
|
||||||
|
- pr enhancement [\\#11](https://github.com/owner/repo/pull/11) ([user5](https://github.com/user5))
|
||||||
|
- pr all the labels [\\#14](https://github.com/owner/repo/pull/14) ([user5](https://github.com/user5))
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- issue bug [\\#7](https://github.com/owner/repo/issue/7)
|
||||||
|
- pr bug [\\#12](https://github.com/owner/repo/pull/12) ([user5](https://github.com/user5))
|
||||||
|
|
||||||
|
**Closed issues:**
|
||||||
|
|
||||||
|
- issue no labels [\\#5](https://github.com/owner/repo/issue/5)
|
||||||
|
|
||||||
|
**Merged pull requests:**
|
||||||
|
|
||||||
|
- pr no labels [\\#10](https://github.com/owner/repo/pull/10) ([user1](https://github.com/user1))
|
||||||
|
|
||||||
|
CHANGELOG
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
describe "#parse_sections" do
|
||||||
|
before do
|
||||||
|
subject { described_class.new }
|
||||||
|
end
|
||||||
|
context "valid json" do
|
||||||
|
let(:sections_string) { "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}, \"bar\": { \"prefix\": \"barfix\", \"labels\": [\"test3\", \"test4\"]}}" }
|
||||||
|
|
||||||
|
let(:sections_array) do
|
||||||
|
[
|
||||||
|
Section.new(name: "foo", prefix: "foofix", labels: %w[test1 test2]),
|
||||||
|
Section.new(name: "bar", prefix: "barfix", labels: %w[test3 test4])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an array with 2 objects" do
|
||||||
|
arr = subject.send(:parse_sections, sections_string)
|
||||||
|
expect(arr.size).to eq 2
|
||||||
|
arr.each { |section| expect(section).to be_an_instance_of Section }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns correctly constructed sections" do
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
sections_json = JSON.parse(sections_string)
|
||||||
|
sections_array.each_index do |i|
|
||||||
|
aggregate_failures "checks each component" do
|
||||||
|
expect(sections_array[i].name).to eq sections_json.first[0]
|
||||||
|
expect(sections_array[i].prefix).to eq sections_json.first[1]["prefix"]
|
||||||
|
expect(sections_array[i].labels).to eq sections_json.first[1]["labels"]
|
||||||
|
expect(sections_array[i].issues).to eq []
|
||||||
|
end
|
||||||
|
sections_json.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "hash" do
|
||||||
|
let(:sections_hash) do
|
||||||
|
{
|
||||||
|
enhancements: {
|
||||||
|
prefix: "**Enhancements**",
|
||||||
|
labels: %w[feature enhancement]
|
||||||
|
},
|
||||||
|
breaking: {
|
||||||
|
prefix: "**Breaking**",
|
||||||
|
labels: ["breaking"]
|
||||||
|
},
|
||||||
|
bugs: {
|
||||||
|
prefix: "**Bugs**",
|
||||||
|
labels: ["bug"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:sections_array) do
|
||||||
|
[
|
||||||
|
Section.new(name: "enhancements", prefix: "**Enhancements**", labels: %w[feature enhancement]),
|
||||||
|
Section.new(name: "breaking", prefix: "**Breaking**", labels: ["breaking"]),
|
||||||
|
Section.new(name: "bugs", prefix: "**Bugs**", labels: ["bug"])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns an array with 3 objects" do
|
||||||
|
arr = subject.send(:parse_sections, sections_hash)
|
||||||
|
expect(arr.size).to eq 3
|
||||||
|
arr.each { |section| expect(section).to be_an_instance_of Section }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns correctly constructed sections" do
|
||||||
|
sections_array.each_index do |i|
|
||||||
|
aggregate_failures "checks each component" do
|
||||||
|
expect(sections_array[i].name).to eq sections_hash.first[0].to_s
|
||||||
|
expect(sections_array[i].prefix).to eq sections_hash.first[1][:prefix]
|
||||||
|
expect(sections_array[i].labels).to eq sections_hash.first[1][:labels]
|
||||||
|
expect(sections_array[i].issues).to eq []
|
||||||
|
end
|
||||||
|
sections_hash.shift
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#parse_by_sections" do
|
||||||
|
context "default sections" do
|
||||||
|
let(:options) do
|
||||||
|
{
|
||||||
|
bug_labels: ["bug"],
|
||||||
|
enhancement_labels: ["enhancement"],
|
||||||
|
breaking_labels: ["breaking"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:issues) do
|
||||||
|
[
|
||||||
|
issue("no labels", []),
|
||||||
|
issue("enhancement", ["enhancement"]),
|
||||||
|
issue("bug", ["bug"]),
|
||||||
|
issue("breaking", ["breaking"]),
|
||||||
|
issue("all the labels", %w[enhancement bug breaking])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:pull_requests) do
|
||||||
|
[
|
||||||
|
pr("no labels", []),
|
||||||
|
pr("enhancement", ["enhancement"]),
|
||||||
|
pr("bug", ["bug"]),
|
||||||
|
pr("breaking", ["breaking"]),
|
||||||
|
pr("all the labels", %w[enhancement bug breaking])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(options) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.send(:set_sections_and_maps)
|
||||||
|
@arr = subject.send(:parse_by_sections, pull_requests, issues)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 4 sections" do
|
||||||
|
expect(@arr.size).to eq 4
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns default sections" do
|
||||||
|
default_sections.each { |default_section| expect(@arr.select { |section| section.name == default_section }.size).to eq 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "assigns issues to the correct sections" do
|
||||||
|
breaking_section = @arr.select { |section| section.name == "breaking" }[0]
|
||||||
|
enhancement_section = @arr.select { |section| section.name == "enhancements" }[0]
|
||||||
|
issue_section = @arr.select { |section| section.name == "issues" }[0]
|
||||||
|
bug_section = @arr.select { |section| section.name == "bugs" }[0]
|
||||||
|
|
||||||
|
expect(titles_for(breaking_section.issues)).to eq(["issue breaking", "pr breaking"])
|
||||||
|
expect(titles_for(enhancement_section.issues)).to eq(["issue enhancement", "issue all the labels", "pr enhancement", "pr all the labels"])
|
||||||
|
expect(titles_for(issue_section.issues)).to eq(["issue no labels"])
|
||||||
|
expect(titles_for(bug_section.issues)).to eq(["issue bug", "pr bug"])
|
||||||
|
expect(titles_for(pull_requests)).to eq(["pr no labels"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "configure sections" do
|
||||||
|
let(:options) do
|
||||||
|
{
|
||||||
|
configure_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}, \"bar\": { \"prefix\": \"barfix\", \"labels\": [\"test3\", \"test4\"]}}"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:issues) do
|
||||||
|
[
|
||||||
|
issue("no labels", []),
|
||||||
|
issue("test1", ["test1"]),
|
||||||
|
issue("test3", ["test3"]),
|
||||||
|
issue("test4", ["test4"]),
|
||||||
|
issue("all the labels", %w[test1 test2 test3 test4])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:pull_requests) do
|
||||||
|
[
|
||||||
|
pr("no labels", []),
|
||||||
|
pr("test1", ["test1"]),
|
||||||
|
pr("test3", ["test3"]),
|
||||||
|
pr("test4", ["test4"]),
|
||||||
|
pr("all the labels", %w[test1 test2 test3 test4])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(options) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.send(:set_sections_and_maps)
|
||||||
|
@arr = subject.send(:parse_by_sections, pull_requests, issues)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 2 sections" do
|
||||||
|
expect(@arr.size).to eq 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns only configured sections" do
|
||||||
|
expect(@arr.select { |section| section.name == "foo" }.size).to eq 1
|
||||||
|
expect(@arr.select { |section| section.name == "bar" }.size).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "assigns issues to the correct sections" do
|
||||||
|
foo_section = @arr.select { |section| section.name == "foo" }[0]
|
||||||
|
bar_section = @arr.select { |section| section.name == "bar" }[0]
|
||||||
|
|
||||||
|
aggregate_failures "checks all sections" do
|
||||||
|
expect(titles_for(foo_section.issues)).to eq(["issue test1", "issue all the labels", "pr test1", "pr all the labels"])
|
||||||
|
expect(titles_for(bar_section.issues)).to eq(["issue test3", "issue test4", "pr test3", "pr test4"])
|
||||||
|
expect(titles_for(pull_requests)).to eq(["pr no labels"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context "add sections" do
|
||||||
|
let(:options) do
|
||||||
|
{
|
||||||
|
bug_labels: ["bug"],
|
||||||
|
enhancement_labels: ["enhancement"],
|
||||||
|
breaking_labels: ["breaking"],
|
||||||
|
add_sections: "{ \"foo\": { \"prefix\": \"foofix\", \"labels\": [\"test1\", \"test2\"]}}"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:issues) do
|
||||||
|
[
|
||||||
|
issue("no labels", []),
|
||||||
|
issue("test1", ["test1"]),
|
||||||
|
issue("bugaboo", ["bug"]),
|
||||||
|
issue("all the labels", %w[test1 test2 enhancement bug])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:pull_requests) do
|
||||||
|
[
|
||||||
|
pr("no labels", []),
|
||||||
|
pr("test1", ["test1"]),
|
||||||
|
pr("enhance", ["enhancement"]),
|
||||||
|
pr("all the labels", %w[test1 test2 enhancement bug])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(options) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
subject.send(:set_sections_and_maps)
|
||||||
|
@arr = subject.send(:parse_by_sections, pull_requests, issues)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns 5 sections" do
|
||||||
|
expect(@arr.size).to eq 5
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns default sections" do
|
||||||
|
default_sections.each { |default_section| expect(@arr.select { |section| section.name == default_section }.size).to eq 1 }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns added section" do
|
||||||
|
expect(@arr.select { |section| section.name == "foo" }.size).to eq 1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "assigns issues to the correct sections" do
|
||||||
|
foo_section = @arr.select { |section| section.name == "foo" }[0]
|
||||||
|
enhancement_section = @arr.select { |section| section.name == "enhancements" }[0]
|
||||||
|
bug_section = @arr.select { |section| section.name == "bugs" }[0]
|
||||||
|
|
||||||
|
aggregate_failures "checks all sections" do
|
||||||
|
expect(titles_for(foo_section.issues)).to eq(["issue test1", "issue all the labels", "pr test1", "pr all the labels"])
|
||||||
|
expect(titles_for(enhancement_section.issues)).to eq(["pr enhance"])
|
||||||
|
expect(titles_for(bug_section.issues)).to eq(["issue bugaboo"])
|
||||||
|
expect(titles_for(pull_requests)).to eq(["pr no labels"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/ModuleLength
|
|
@ -1,73 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module GitHubChangelogGenerator
|
|
||||||
describe Generator do
|
|
||||||
describe "#get_string_for_issue" do
|
|
||||||
let(:issue) do
|
|
||||||
{ "title" => "Bug in code" }
|
|
||||||
end
|
|
||||||
|
|
||||||
it "formats an issue according to options" do
|
|
||||||
expect do
|
|
||||||
described_class.new.get_string_for_issue(issue)
|
|
||||||
end.not_to raise_error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#parse_by_sections" do
|
|
||||||
def label(name)
|
|
||||||
{ "name" => name }
|
|
||||||
end
|
|
||||||
|
|
||||||
def issue(title, labels)
|
|
||||||
{ "title" => "issue #{title}", "labels" => labels.map { |l| label(l) } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def pr(title, labels)
|
|
||||||
{ "title" => "pr #{title}", "labels" => labels.map { |l| label(l) } }
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_titles(issues)
|
|
||||||
issues.map { |issue| issue["title"] }
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:options) do
|
|
||||||
{
|
|
||||||
bug_labels: ["bug"],
|
|
||||||
enhancement_labels: ["enhancement"],
|
|
||||||
breaking_labels: ["breaking"]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:issues) do
|
|
||||||
[
|
|
||||||
issue("no labels", []),
|
|
||||||
issue("enhancement", ["enhancement"]),
|
|
||||||
issue("bug", ["bug"]),
|
|
||||||
issue("breaking", ["breaking"]),
|
|
||||||
issue("all the labels", %w[enhancement bug breaking])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:pull_requests) do
|
|
||||||
[
|
|
||||||
pr("no labels", []),
|
|
||||||
pr("enhancement", ["enhancement"]),
|
|
||||||
pr("bug", ["bug"]),
|
|
||||||
pr("breaking", ["breaking"]),
|
|
||||||
pr("all the labels", %w[enhancement bug breaking])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it "works" do
|
|
||||||
sections = described_class.new(options).parse_by_sections(issues, pull_requests)
|
|
||||||
|
|
||||||
expect(get_titles(sections[:issues])).to eq(["issue no labels"])
|
|
||||||
expect(get_titles(sections[:enhancements])).to eq(["issue enhancement", "issue all the labels", "pr enhancement", "pr all the labels"])
|
|
||||||
expect(get_titles(sections[:bugs])).to eq(["issue bug", "pr bug"])
|
|
||||||
expect(get_titles(sections[:breaking])).to eq(["issue breaking", "pr breaking"])
|
|
||||||
expect(get_titles(pull_requests)).to eq(["pr no labels"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue
Block a user