587557e1ac
There's a lot in this PR. - Added a Section class to more easily make the other changes and hopefully add flexibility for the future - Added an option called `configure_sections` that allows you create your own custom sections. It blows away all other sections and uses only the ones you give it. - Added an option called `add_sections` that allows you to add_sections to the default section set - Added an option called `include_merged` that can be used when configure_sections is defined. Configure sections blows away any and all default sections so to get this one back, you have to set this option. - Added tests for this stuff @HAIL9000 was a co-author. Because of a little git snafu, I accidentally squashed all of our work into one so it looks like it was just me. --- Refactor details: Before this change, the code in generator.rb and generator_generation.rb was conflated and method call flow went back and forth between the two files seemingly randomly. They also both defined the exact same class, which is un-ruby-ish. I tried to separate methods used for the whole changelog generation from methods used for specific parts of the changelog and move them into specific classes. I reasoned that a changelog is a series of "entries" of all tagged releases plus an extra entry for the unreleased entry. Each entry is comprised of a header and a series of "sections" for that entry. Each section is comprized of a list of issues and/or pull requests for that entry. So the log contains entries, entries contain sections, and sections contain issues & prs. I have structured the classes around this idea. - lib/github_changelog_generator/generator/generator.rb is for code related to generating the entire changelog. - lib/github_changelog_generator/generator/entry.rb is for code related to generating entries. - lib/github_changelog_generator/generator/section.rb is for code relating to geneating entry sections. Issues and PRs are already special objects, so it doesn't make sense to break those out into their own class.
238 lines
8.4 KiB
Ruby
238 lines
8.4 KiB
Ruby
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
|