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
|