Compare commits

..

29 Commits

Author SHA1 Message Date
Petr Korolev
d4664e389d add tests 2015-05-25 18:04:32 +03:00
Petr Korolev
0e352886ea fix #172 2015-05-25 17:16:35 +03:00
Petr Korolev
01e30d1132 fix log 2015-05-25 17:01:10 +03:00
Petr Korolev
a72ad326c4 fixes #226, and closes #228
fixup

fx
2015-05-25 16:27:15 +03:00
Petr Korolev
da4bc8952d simplify code complexity 2015-05-25 15:47:43 +03:00
Petr Korolev
c8c633a65d add tests for second arg 2015-05-25 15:43:59 +03:00
Petr Korolev
d2cd79e710 fix arg is nil 2015-05-25 15:39:24 +03:00
Petr Korolev
8f4a931256 reduce complexity accouding codeclimate recomendations 2015-05-25 15:17:51 +03:00
Petr Korolev
8e5e7de71d Merge branch 'master' into develop 2015-05-25 14:33:26 +03:00
Petr Korolev
5aa4352acf Merge branch 'refacktoring' 2015-05-25 14:24:53 +03:00
Petr Korolev
171e536e76 add tests for regex 2015-05-25 14:21:23 +03:00
Petr Korolev
757f6d40b4 add tests 2015-05-25 13:34:37 +03:00
Petr Korolev
c3b9455dfd fix regex mess 2015-05-25 13:02:10 +03:00
Petr Korolev
134c18ba06 typo 2015-05-25 09:56:59 +03:00
Petr Korolev
4ffb493787 slice fetching 2015-05-25 09:18:58 +03:00
Petr Korolev
b10707b259 reorganaize issues fetching in more clear way 2015-05-22 17:44:06 +03:00
Petr Korolev
3f076b3069 spread methods by files 2015-05-22 16:39:08 +03:00
Petr Korolev
9a24eb1cb3 rubocop autofixes 2015-05-22 15:59:29 +03:00
Petr Korolev
cf7ae57e3d prettify code. fix rubocop waning. move all methods with change log
generation to generator
2015-05-22 15:55:37 +03:00
Petr Korolev
3c289de79b move 2 other methods 2015-05-22 15:28:43 +03:00
Petr Korolev
4a96a7c0c9 move compund to generator 2015-05-22 14:11:29 +03:00
Petr Korolev
7f696b6b09 rename, prepare to moving methods to generator 2015-05-22 14:06:48 +03:00
Petr Korolev
536b39c961 add doc 2015-05-22 13:37:06 +03:00
Petr Korolev
944adc92cd refacktoring. move separation logic in funciton 2015-05-22 13:34:01 +03:00
Petr Korolev
3fc3e3e143 minor changes 2015-05-22 09:22:44 +03:00
Petr Korolev
154ac01226 Merge branch 'hotfix/update-changelog' into develop 2015-05-19 12:23:29 +03:00
Petr Korolev
e2548e049a Merge branch 'hotfix/update-changelog' 2015-05-19 12:23:26 +03:00
Petr Korolev
1ab6f2a5eb Update changelog for version 1.4.1 2015-05-19 12:23:25 +03:00
Petr Korolev
00d4242fa6 Merge branch 'release/1.4.1' into develop 2015-05-19 12:19:29 +03:00
15 changed files with 893 additions and 666 deletions

View File

@@ -1,68 +1,46 @@
# This configuration was generated by `rubocop --auto-gen-config` # This configuration was generated by `rubocop --auto-gen-config`
# on 2015-05-14 16:51:06 +0300 using RuboCop version 0.31.0. # on 2015-05-25 17:16:04 +0300 using RuboCop version 0.31.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 15 # Offense count: 14
Metrics/AbcSize: Metrics/AbcSize:
Enabled: false Max: 57
# Offense count: 2 # Offense count: 4
Metrics/BlockNesting:
Max: 4
# Offense count: 3
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/ClassLength: Metrics/ClassLength:
Max: 337 Max: 182
# Offense count: 5 # Offense count: 1
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 15 Max: 7
# Offense count: 22 # Offense count: 22
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/MethodLength: Metrics/MethodLength:
Enabled: false Max: 84
# Offense count: 5 # Offense count: 1
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 18 Max: 8
# Offense count: 4 # Offense count: 2
Style/AccessorMethodName: Style/AccessorMethodName:
Enabled: false Enabled: false
# Offense count: 1 # Offense count: 8
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
Style/AndOr:
Enabled: false
# Offense count: 19
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods.
Style/BlockDelimiters:
Enabled: false
# Offense count: 4
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
# Offense count: 5 # Offense count: 1
# Configuration parameters: MinBodyLength. # Configuration parameters: MinBodyLength.
Style/GuardClause: Style/GuardClause:
Enabled: false Enabled: false
# Offense count: 15 # Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/IfUnlessModifier:
Enabled: false
# Offense count: 2
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles. # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
Style/Next: Style/Next:
Enabled: false Enabled: false

View File

@@ -1,5 +1,29 @@
# Change Log # Change Log
## [1.4.1](https://github.com/skywinder/github-changelog-generator/tree/1.4.1) (2015-05-19)
[Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.4.0...1.4.1)
**Implemented enhancements:**
- Trees/Archives with missing change log notes for the current tag. [\#230](https://github.com/skywinder/github-changelog-generator/issues/230)
**Fixed bugs:**
- github\_changelog\_generator.rb:220:in ``': No such file or directory - pwd \(Errno::ENOENT\) [\#237](https://github.com/skywinder/github-changelog-generator/issues/237)
- Doesnot generator changelog [\#235](https://github.com/skywinder/github-changelog-generator/issues/235)
- Exclude closed \(not merged\) PR's from changelog. [\#69](https://github.com/skywinder/github-changelog-generator/issues/69)
**Merged pull requests:**
- Wrap GitHub requests in function check\_github\_response [\#238](https://github.com/skywinder/github-changelog-generator/pull/238) ([skywinder](https://github.com/skywinder))
- Add fetch token tests [\#236](https://github.com/skywinder/github-changelog-generator/pull/236) ([skywinder](https://github.com/skywinder))
- Add future release option [\#231](https://github.com/skywinder/github-changelog-generator/pull/231) ([sildur](https://github.com/sildur))
## [1.4.0](https://github.com/skywinder/github-changelog-generator/tree/1.4.0) (2015-05-07) ## [1.4.0](https://github.com/skywinder/github-changelog-generator/tree/1.4.0) (2015-05-07)
[Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.3.11...1.4.0) [Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.3.11...1.4.0)
@@ -62,10 +86,6 @@
[Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.3.6...1.3.8) [Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.3.6...1.3.8)
**Merged pull requests:**
- Fix `git remote` parsing in case, when script running without parameters inside destination directory [\#61](https://github.com/skywinder/github-changelog-generator/pull/61) ([skywinder](https://github.com/skywinder))
## [1.3.6](https://github.com/skywinder/github-changelog-generator/tree/1.3.6) (2015-03-05) ## [1.3.6](https://github.com/skywinder/github-changelog-generator/tree/1.3.6) (2015-03-05)
[Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.3.5...1.3.6) [Full Changelog](https://github.com/skywinder/github-changelog-generator/compare/1.3.5...1.3.6)

View File

@@ -1,4 +1,4 @@
#! /usr/bin/env ruby #! /usr/bin/env ruby
require_relative "../lib/github_changelog_generator" require_relative "../lib/github_changelog_generator"
GitHubChangelogGenerator::ChangelogGenerator.new.compound_changelog GitHubChangelogGenerator::ChangelogGenerator.new.run

View File

@@ -1,8 +1,12 @@
# Change Log # Change Log
## [Unreleased](https://github.com/skywinder/changelog_test/tree/HEAD) ## [0.0.4](https://github.com/skywinder/changelog_test/tree/0.0.4) (2015-05-22)
[Full Changelog](https://github.com/skywinder/changelog_test/compare/v0.0.3...HEAD) [Full Changelog](https://github.com/skywinder/changelog_test/compare/v0.0.3...0.0.4)
**Closed issues:**
- Test issue, that should appear in 0.0.4 [\#3](https://github.com/skywinder/changelog_test/issues/3)
**Merged pull requests:** **Merged pull requests:**

View File

@@ -6,517 +6,34 @@ require "colorize"
require "benchmark" require "benchmark"
require_relative "github_changelog_generator/parser" require_relative "github_changelog_generator/parser"
require_relative "github_changelog_generator/generator" require_relative "github_changelog_generator/generator/generator"
require_relative "github_changelog_generator/version" require_relative "github_changelog_generator/version"
require_relative "github_changelog_generator/reader" require_relative "github_changelog_generator/reader"
require_relative "github_changelog_generator/fetcher"
# The main module, where placed all classes (now, at least)
module GitHubChangelogGenerator module GitHubChangelogGenerator
# Default error for ChangelogGenerator
class ChangelogGeneratorError < StandardError
end
# Main class and entry point for this script. # Main class and entry point for this script.
class ChangelogGenerator class ChangelogGenerator
attr_accessor :options, :all_tags, :github
# Class, responsible for whole change log generation cycle # Class, responsible for whole change log generation cycle
# @return initialised instance of ChangelogGenerator # @return initialised instance of ChangelogGenerator
def initialize def initialize
@options = Parser.parse_options @options = Parser.parse_options
@fetcher = GitHubChangelogGenerator::Fetcher.new @options
@generator = Generator.new @options @generator = Generator.new @options
# @all_tags = get_filtered_tags
@all_tags = @fetcher.get_all_tags
# TODO: refactor this double asssign of @issues and @pull_requests and move all logic in one method
@issues, @pull_requests = @fetcher.fetch_closed_issues_and_pr
@pull_requests = @options[:pulls] ? get_filtered_pull_requests : []
@issues = @options[:issues] ? get_filtered_issues : []
fetch_event_for_issues_and_pr
detect_actual_closed_dates
end
# Return tags after filtering tags in lists provided by option: --between-tags & --exclude-tags
#
# @return [Array]
def get_filtered_tags
all_tags = @fetcher.get_all_tags
filtered_tags = []
if @options[:between_tags]
@options[:between_tags].each do |tag|
unless all_tags.include? tag
puts "Warning: can't find tag #{tag}, specified with --between-tags option.".yellow
end
end
filtered_tags = all_tags.select { |tag| @options[:between_tags].include? tag }
end
filtered_tags
end
def detect_actual_closed_dates
if @options[:verbose]
print "Fetching closed dates for issues...\r"
end
threads = []
@issues.each { |issue|
threads << Thread.new {
find_closed_date_by_commit(issue)
}
}
@pull_requests.each { |pull_request|
threads << Thread.new {
find_closed_date_by_commit(pull_request)
}
}
threads.each(&:join)
if @options[:verbose]
puts "Fetching closed dates for issues: Done!"
end
end
# Fill :actual_date parameter of specified issue by closed date of the commit, it it was closed by commit.
# @param [Hash] issue
def find_closed_date_by_commit(issue)
unless issue["events"].nil?
# if it's PR -> then find "merged event", in case of usual issue -> fond closed date
compare_string = issue[:merged_at].nil? ? "closed" : "merged"
# reverse! - to find latest closed event. (event goes in date order)
issue["events"].reverse!.each { |event|
if event[:event].eql? compare_string
if event[:commit_id].nil?
issue[:actual_date] = issue[:closed_at]
else
begin
commit = @fetcher.fetch_commit(event)
issue[:actual_date] = commit[:author][:date]
rescue
puts "Warning: Can't fetch commit #{event[:commit_id]}. It is probably referenced from another repo.".yellow
issue[:actual_date] = issue[:closed_at]
end
end
break
end
}
end
# TODO: assert issues, that remain without 'actual_date' hash for some reason.
end
def print_json(json)
puts JSON.pretty_generate(json)
end
# This method fetches missing params for PR and filter them by specified options
# It include add all PR's with labels from @options[:include_labels] array
# And exclude all from :exclude_labels array.
# @return [Array] filtered PR's
def get_filtered_pull_requests
filter_merged_pull_requests
filtered_pull_requests = include_issues_by_labels(@pull_requests)
filtered_pull_requests = exclude_issues_by_labels(filtered_pull_requests)
if @options[:verbose]
puts "Filtered pull requests: #{filtered_pull_requests.count}"
end
filtered_pull_requests
end
# This method filter only merged PR and
# fetch missing required attributes for pull requests
# :merged_at - is a date, when issue PR was merged.
# More correct to use merged date, rather than closed date.
def filter_merged_pull_requests
if @options[:verbose]
print "Fetching merged dates...\r"
end
pull_requests = @fetcher.fetch_closed_pull_requests
@pull_requests.each { |pr|
fetched_pr = pull_requests.find { |fpr|
fpr.number == pr.number
}
pr[:merged_at] = fetched_pr[:merged_at]
pull_requests.delete(fetched_pr)
}
@pull_requests.select! do |pr|
!pr[:merged_at].nil?
end
end
# Include issues with labels, specified in :include_labels
# @param [Array] issues to filter
# @return [Array] filtered array of issues
def include_issues_by_labels(issues)
filtered_issues = @options[:include_labels].nil? ? issues : issues.select { |issue| (issue.labels.map(&:name) & @options[:include_labels]).any? }
if @options[:add_issues_wo_labels]
issues_wo_labels = issues.select { |issue|
!issue.labels.map(&:name).any?
}
filtered_issues |= issues_wo_labels
end
filtered_issues
end
# delete all labels with labels from @options[:exclude_labels] array
# @param [Array] issues
# @return [Array] filtered array
def exclude_issues_by_labels(issues)
unless @options[:exclude_labels].nil?
issues = issues.select { |issue|
!(issue.labels.map(&:name) & @options[:exclude_labels]).any?
}
end
issues
end end
# The entry point of this script to generate change log # The entry point of this script to generate change log
# @raise (ChangelogGeneratorError) Is thrown when one of specified tags was not found in list of tags. # @raise (ChangelogGeneratorError) Is thrown when one of specified tags was not found in list of tags.
def compound_changelog def run
log = "# Change Log\n\n" log = @generator.compound_changelog
if @options[:unreleased_only]
log += generate_log_between_tags(all_tags[0], nil)
elsif @options[:tag1] and @options[:tag2]
tag1 = @options[:tag1]
tag2 = @options[:tag2]
tags_strings = []
all_tags.each { |x| tags_strings.push(x["name"]) }
if tags_strings.include?(tag1)
if tags_strings.include?(tag2)
to_a = tags_strings.map.with_index.to_a
hash = Hash[to_a]
index1 = hash[tag1]
index2 = hash[tag2]
log += generate_log_between_tags(all_tags[index1], all_tags[index2])
else
fail ChangelogGeneratorError, "Can't find tag #{tag2} -> exit".red
end
else
fail ChangelogGeneratorError, "Can't find tag #{tag1} -> exit".red
end
else
log += generate_log_for_all_tags
end
log += "\n\n\\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*"
output_filename = "#{@options[:output]}" output_filename = "#{@options[:output]}"
File.open(output_filename, "w") { |file| file.write(log) } File.open(output_filename, "w") { |file| file.write(log) }
puts "Done!" puts "Done!"
puts "Generated log placed in #{Dir.pwd}/#{output_filename}" puts "Generated log placed in #{Dir.pwd}/#{output_filename}"
end end
# The full cycle of generation for whole project
# @return [String] The complete change log
def generate_log_for_all_tags
fetch_tags_dates
if @options[:verbose]
puts "Sorting tags..."
end
@all_tags.sort_by! { |x| @fetcher.get_time_of_tag(x) }.reverse!
if @options[:verbose]
puts "Generating log..."
end
log = ""
if @options[:unreleased] && @all_tags.count != 0
unreleased_log = generate_log_between_tags(all_tags[0], nil)
if unreleased_log
log += unreleased_log
end
end
(1...all_tags.size).each { |index|
log += generate_log_between_tags(all_tags[index], all_tags[index - 1])
}
if @all_tags.count != 0
log += generate_log_between_tags(nil, all_tags.last)
end
log
end
# Async fetching of all tags dates
def fetch_tags_dates
if @options[:verbose]
print "Fetching tag dates...\r"
end
# Async fetching tags:
threads = []
i = 0
all = @all_tags.count
@all_tags.each { |tag|
threads << Thread.new {
@fetcher.get_time_of_tag(tag)
if @options[:verbose]
print "Fetching tags dates: #{i + 1}/#{all}\r"
i += 1
end
}
}
print " \r"
threads.each(&:join)
if @options[:verbose]
puts "Fetching tags dates: #{i}"
end
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_pull_requests = delete_by_time(@pull_requests, :actual_date, older_tag, newer_tag)
filtered_issues = delete_by_time(@issues, :actual_date, older_tag, newer_tag)
newer_tag_name = newer_tag.nil? ? nil : newer_tag["name"]
older_tag_name = older_tag.nil? ? nil : older_tag["name"]
if @options[:filter_issues_by_milestone]
# delete excess irrelevant issues (according milestones)
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
if filtered_issues.empty? && filtered_pull_requests.empty? && newer_tag.nil?
# do not generate empty unreleased section
return ""
end
create_log(filtered_pull_requests, filtered_issues, newer_tag, older_tag_name)
end
def filter_by_milestone(filtered_issues, newer_tag_name, src_array)
filtered_issues.select! { |issue|
# leave issues without milestones
if issue.milestone.nil?
true
else
# check, that this milestone in tag list:
@all_tags.find { |tag| tag.name == issue.milestone.title }.nil?
end
}
unless newer_tag_name.nil?
# add missed issues (according milestones)
issues_to_add = src_array.select { |issue|
if issue.milestone.nil?
false
else
# check, that this milestone in tag list:
milestone_is_tag = @all_tags.find { |tag|
tag.name == issue.milestone.title
}
if milestone_is_tag.nil?
false
else
issue.milestone.title == newer_tag_name
end
end
}
filtered_issues |= issues_to_add
end
filtered_issues
end
# Method filter issues, that belong only specified tag range
# @param [Array] array of issues to filter
# @param [Symbol] hash_key key of date value default is :actual_date
# @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
# @return [Array] filtered issues
def delete_by_time(array, hash_key = :actual_date, older_tag = nil, newer_tag = nil)
fail ChangelogGeneratorError, "At least one of the tags should be not nil!".red if older_tag.nil? && newer_tag.nil?
newer_tag_time = newer_tag && @fetcher.get_time_of_tag(newer_tag)
older_tag_time = older_tag && @fetcher.get_time_of_tag(older_tag)
array.select { |req|
if req[hash_key]
t = Time.parse(req[hash_key]).utc
if older_tag_time.nil?
tag_in_range_old = true
else
tag_in_range_old = t > older_tag_time
end
if newer_tag_time.nil?
tag_in_range_new = true
else
tag_in_range_new = t <= newer_tag_time
end
tag_in_range = (tag_in_range_old) && (tag_in_range_new)
tag_in_range
else
false
end
}
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 [String] older_tag_name Older tag, used for the links. Could be nil for last tag.
# @return [String] Ready and parsed section
def create_log(pull_requests, issues, newer_tag, older_tag_name = nil)
newer_tag_time = newer_tag.nil? ? Time.new : @fetcher.get_time_of_tag(newer_tag)
if newer_tag.nil? && @options[:future_release]
newer_tag_name = @options[:future_release]
newer_tag_link = @options[:future_release]
else
newer_tag_name = newer_tag.nil? ? @options[:unreleased_label] : newer_tag["name"]
newer_tag_link = newer_tag.nil? ? "HEAD" : newer_tag_name
end
github_site = options[:github_site] || "https://github.com"
project_url = "#{github_site}/#{@options[:user]}/#{@options[:project]}"
log = generate_header(newer_tag_name, newer_tag_link, newer_tag_time, older_tag_name, project_url)
if @options[:issues]
# Generate issues:
issues_a = []
enhancement_a = []
bugs_a = []
issues.each { |dict|
added = false
dict.labels.each { |label|
if label.name == "bug"
bugs_a.push dict
added = true
next
end
if label.name == "enhancement"
enhancement_a.push dict
added = true
next
end
}
unless added
issues_a.push dict
end
}
log += generate_sub_section(enhancement_a, @options[:enhancement_prefix])
log += generate_sub_section(bugs_a, @options[:bug_prefix])
log += generate_sub_section(issues_a, @options[:issue_prefix])
end
if @options[:pulls]
# Generate pull requests:
log += generate_sub_section(pull_requests, @options[:merge_prefix])
end
log
end
# @param [Array] issues List of issues on sub-section
# @param [String] prefix Nae of sub-section
# @return [String] Generate ready-to-go sub-section
def generate_sub_section(issues, prefix)
log = ""
if options[:simple_list] != true && issues.any?
log += "#{prefix}\n\n"
end
if issues.any?
issues.each { |issue|
merge_string = @generator.get_string_for_issue(issue)
log += "- #{merge_string}\n\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[:dateformat]
# Generate tag name and link
if newer_tag_name.equal? @options[:unreleased_label]
log += "## [#{newer_tag_name}](#{project_url}/tree/#{newer_tag_link})\n\n"
else
log += "## [#{newer_tag_name}](#{project_url}/tree/#{newer_tag_link}) (#{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
# Filter issues according labels
# @return [Array] Filtered issues
def get_filtered_issues
filtered_issues = include_issues_by_labels(@issues)
filtered_issues = exclude_issues_by_labels(filtered_issues)
if @options[:verbose]
puts "Filtered issues: #{filtered_issues.count}"
end
filtered_issues
end
# Fetch event for issues and pull requests
# @return [Array] array of fetched issues
def fetch_event_for_issues_and_pr
if @options[:verbose]
print "Fetching events for issues and PR: 0/#{@issues.count + @pull_requests.count}\r"
end
# Async fetching events:
@fetcher.fetch_events_async(@issues + @pull_requests)
end
end end
if __FILE__ == $PROGRAM_NAME if __FILE__ == $PROGRAM_NAME
GitHubChangelogGenerator::ChangelogGenerator.new.compound_changelog GitHubChangelogGenerator::ChangelogGenerator.new.run
end end
end end

View File

@@ -15,7 +15,7 @@ module GitHubChangelogGenerator
"This script can make only 50 requests to GitHub API per hour without token!" "This script can make only 50 requests to GitHub API per hour without token!"
def initialize(options = {}) def initialize(options = {})
@options = options @options = options || {}
@logger = Logger.new(STDOUT) @logger = Logger.new(STDOUT)
@logger.formatter = proc do |_severity, _datetime, _progname, msg| @logger.formatter = proc do |_severity, _datetime, _progname, msg|
@@ -28,8 +28,8 @@ module GitHubChangelogGenerator
@tag_times_hash = {} @tag_times_hash = {}
github_options = { per_page: PER_PAGE_NUMBER } github_options = { per_page: PER_PAGE_NUMBER }
github_options[:oauth_token] = @github_token unless @github_token.nil? github_options[:oauth_token] = @github_token unless @github_token.nil?
github_options[:endpoint] = options[:github_endpoint] unless options[:github_endpoint].nil? github_options[:endpoint] = @options[:github_endpoint] unless @options[:github_endpoint].nil?
github_options[:site] = options[:github_endpoint] unless options[:github_site].nil? github_options[:site] = @options[:github_endpoint] unless @options[:github_site].nil?
@github = check_github_response { Github.new github_options } @github = check_github_response { Github.new github_options }
end end
@@ -41,9 +41,7 @@ module GitHubChangelogGenerator
def fetch_github_token def fetch_github_token
env_var = @options[:token] ? @options[:token] : (ENV.fetch CHANGELOG_GITHUB_TOKEN, nil) env_var = @options[:token] ? @options[:token] : (ENV.fetch CHANGELOG_GITHUB_TOKEN, nil)
unless env_var @logger.warn NO_TOKEN_PROVIDED.yellow unless env_var
@logger.warn NO_TOKEN_PROVIDED.yellow
end
env_var env_var
end end
@@ -51,9 +49,7 @@ module GitHubChangelogGenerator
# Fetch all tags from repo # Fetch all tags from repo
# @return [Array] array of tags # @return [Array] array of tags
def get_all_tags def get_all_tags
if @options[:verbose] print "Fetching tags...\r" if @options[:verbose]
print "Fetching tags...\r"
end
tags = [] tags = []
@@ -98,9 +94,7 @@ Make sure, that you push tags to remote repo via 'git push --tags'".yellow
# (pull request is kind of issue in term of GitHub) # (pull request is kind of issue in term of GitHub)
# @return [Tuple] with (issues, pull-requests) # @return [Tuple] with (issues, pull-requests)
def fetch_closed_issues_and_pr def fetch_closed_issues_and_pr
if @options[:verbose] print "Fetching closed issues...\r" if @options[:verbose]
print "Fetching closed issues...\r"
end
issues = [] issues = []
begin begin

View File

@@ -1,42 +0,0 @@
module GitHubChangelogGenerator
class Generator
def initialize(options = nil)
@options = options
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](https://github.com/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})"
unless issue.pull_request.nil?
if @options[:author]
if issue.user.nil?
title_with_number += " ({Null user})"
else
title_with_number += " ([#{issue.user.login}](#{issue.user.html_url}))"
end
end
end
title_with_number
end
def encapsulate_string(string)
string.gsub! '\\', '\\\\'
encpas_chars = %w(> * _ \( \) [ ] #)
encpas_chars.each do |char|
string.gsub! char, "\\#{char}"
end
string
end
end
end

View File

@@ -0,0 +1,123 @@
require "github_changelog_generator/fetcher"
require_relative "generator_generation"
require_relative "generator_fetcher"
require_relative "generator_processor"
require_relative "generator_tags"
module GitHubChangelogGenerator
# Default error for ChangelogGenerator
class ChangelogGeneratorError < StandardError
end
class Generator
attr_accessor :options, :all_tags, :github
# A Generator responsible for all logic, related with change log generation from ready-to-parse issues
#
# Example:
# generator = GitHubChangelogGenerator::Generator.new
# content = generator.compound_changelog
def initialize(options = nil)
@options = options || {}
@fetcher = GitHubChangelogGenerator::Fetcher.new @options
end
def fetch_issues_and_pr
issues, pull_requests = @fetcher.fetch_closed_issues_and_pr
@pull_requests = @options[:pulls] ? get_filtered_pull_requests(pull_requests) : []
@issues = @options[:issues] ? get_filtered_issues(issues) : []
fetch_events_for_issues_and_pr
detect_actual_closed_dates(@issues + @pull_requests)
end
# Encapsulate characters to make markdown look as expected.
#
# @param [String] string
# @return [String] encapsulated input string
def encapsulate_string(string)
string.gsub! '\\', '\\\\'
encpas_chars = %w(> * _ \( \) [ ] #)
encpas_chars.each do |char|
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 [String] older_tag_name Older tag, used for the links. Could be nil for last tag.
# @return [String] Ready and parsed section
def create_log(pull_requests, issues, newer_tag, older_tag_name = 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]}"
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)
end
if @options[:pulls]
# Generate pull requests:
log += generate_sub_section(pull_requests, @options[:merge_prefix])
end
log
end
# Generate ready-to-paste log from list of issues.
#
# @param [Array] issues
# @return [String] generated log for issues
def issues_to_log(issues)
log = ""
bugs_a, enhancement_a, issues_a = parse_by_sections(issues)
log += generate_sub_section(enhancement_a, @options[:enhancement_prefix])
log += generate_sub_section(bugs_a, @options[:bug_prefix])
log += generate_sub_section(issues_a, @options[:issue_prefix])
log
end
# This method sort issues by types
# (bugs, features, or just closed issues) by labels
#
# @param [Array] issues
# @return [Array] tuple of filtered arrays: (Bugs, Enhancements Issues)
def parse_by_sections(issues)
issues_a = []
enhancement_a = []
bugs_a = []
issues.each do |dict|
added = false
dict.labels.each do |label|
if label.name == "bug"
bugs_a.push dict
added = true
next
end
if label.name == "enhancement"
enhancement_a.push dict
added = true
next
end
end
issues_a.push dict unless added
end
[bugs_a, enhancement_a, issues_a]
end
end
end

View File

@@ -0,0 +1,83 @@
module GitHubChangelogGenerator
class Generator
# Fetch event for issues and pull requests
# @return [Array] array of fetched issues
def fetch_events_for_issues_and_pr
if @options[:verbose]
print "Fetching events for issues and PR: 0/#{@issues.count + @pull_requests.count}\r"
end
# Async fetching events:
@fetcher.fetch_events_async(@issues + @pull_requests)
end
# Async fetching of all tags dates
def fetch_tags_dates
print "Fetching tag dates...\r" if @options[:verbose]
# Async fetching tags:
threads = []
i = 0
all = @all_tags.count
@all_tags.each do |tag|
print " \r"
threads << Thread.new do
@fetcher.get_time_of_tag(tag)
print "Fetching tags dates: #{i + 1}/#{all}\r" if @options[:verbose]
i += 1
end
end
threads.each(&:join)
puts "Fetching tags dates: #{i}" if @options[:verbose]
end
# Find correct closed dates, if issues was closed by commits
def detect_actual_closed_dates(issues)
print "Fetching closed dates for issues...\r" if @options[:verbose]
max_thread_number = 50
issues.each_slice(max_thread_number) do |issues_slice|
threads = []
issues_slice.each do |issue|
threads << Thread.new { find_closed_date_by_commit(issue) }
end
threads.each(&:join)
puts "Fetching closed dates for issues: Done!" if @options[:verbose]
end
end
# Fill :actual_date parameter of specified issue by closed date of the commit, if it was closed by commit.
# @param [Hash] issue
def find_closed_date_by_commit(issue)
unless issue["events"].nil?
# if it's PR -> then find "merged event", in case of usual issue -> fond closed date
compare_string = issue[:merged_at].nil? ? "closed" : "merged"
# reverse! - to find latest closed event. (event goes in date order)
issue["events"].reverse!.each do |event|
if event[:event].eql? compare_string
set_date_from_event(event, issue)
break
end
end
end
# TODO: assert issues, that remain without 'actual_date' hash for some reason.
end
# Set closed date from this issue
#
# @param [Hash] event
# @param [Hash] issue
def set_date_from_event(event, issue)
if event[:commit_id].nil?
issue[:actual_date] = issue[:closed_at]
else
begin
commit = @fetcher.fetch_commit(event)
issue[:actual_date] = commit[:author][:date]
rescue
puts "Warning: Can't fetch commit #{event[:commit_id]}. It is probably referenced from another repo.".yellow
issue[:actual_date] = issue[:closed_at]
end
end
end
end
end

View File

@@ -0,0 +1,174 @@
module GitHubChangelogGenerator
class Generator
# Main function to start change log generation
#
# @return [String] Generated change log file
def compound_changelog
fetch_and_filter_tags
fetch_issues_and_pr
log = "# Change Log\n\n"
if @options[:unreleased_only]
log += generate_log_between_tags(all_tags[0], nil)
else
log += generate_log_for_all_tags
end
log += "\n\n\\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*"
@log = log
end
# @return [String] temp method should be removed soon
def generate_for_2_tags(log)
tag1 = @options[:tag1]
tag2 = @options[:tag2]
tags_strings = []
all_tags.each { |x| tags_strings.push(x["name"]) }
if tags_strings.include?(tag1)
if tags_strings.include?(tag2)
to_a = tags_strings.map.with_index.to_a
hash = Hash[to_a]
index1 = hash[tag1]
index2 = hash[tag2]
log += generate_log_between_tags(all_tags[index1], all_tags[index2])
else
fail ChangelogGeneratorError, "Can't find tag #{tag2} -> exit".red
end
else
fail ChangelogGeneratorError, "Can't find tag #{tag1} -> exit".red
end
log
end
# @param [Array] issues List of issues on sub-section
# @param [String] prefix Nae of sub-section
# @return [String] Generate ready-to-go sub-section
def generate_sub_section(issues, prefix)
log = ""
log += "#{prefix}\n\n" if options[:simple_list] != true && issues.any?
if issues.any?
issues.each do |issue|
merge_string = get_string_for_issue(issue)
log += "- #{merge_string}\n\n"
end
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
if newer_tag_name.equal? @options[:unreleased_label]
log += "## [#{newer_tag_name}](#{project_url}/tree/#{newer_tag_link})\n\n"
else
log += "## [#{newer_tag_name}](#{project_url}/tree/#{newer_tag_link}) (#{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)
older_tag_name = older_tag.nil? ? nil : older_tag["name"]
if newer_tag.nil? && filtered_issues.empty? && filtered_pull_requests.empty?
# do not generate empty unreleased section
return ""
end
create_log(filtered_pull_requests, filtered_issues, newer_tag, older_tag_name)
end
# Apply all filters to issues and pull requests
#
# @return [Array] filtered issues and pull requests
def filter_issues_for_tags(newer_tag, older_tag)
filtered_pull_requests = delete_by_time(@pull_requests, :actual_date, older_tag, newer_tag)
filtered_issues = delete_by_time(@issues, :actual_date, 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 change log
def generate_log_for_all_tags
puts "Generating log..." if @options[:verbose]
log = generate_unreleased_section
(1...all_tags.size).each do |index|
log += generate_log_between_tags(all_tags[index], all_tags[index - 1])
end
if @all_tags.count != 0
log += generate_log_between_tags(nil, all_tags.last)
end
log
end
def generate_unreleased_section
log = ""
if @options[:unreleased]
unreleased_log = generate_log_between_tags(all_tags[0], 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](https://github.com/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})"
unless issue.pull_request.nil?
if @options[:author]
if issue.user.nil?
title_with_number += " ({Null user})"
else
title_with_number += " ([#{issue.user.login}](#{issue.user.html_url}))"
end
end
end
title_with_number
end
end
end

View File

@@ -0,0 +1,192 @@
module GitHubChangelogGenerator
class Generator
# delete all labels with labels from @options[:exclude_labels] array
# @param [Array] issues
# @return [Array] filtered array
def exclude_issues_by_labels(issues)
unless @options[:exclude_labels].nil?
issues = issues.select do |issue|
var = issue.labels.map(&:name) & @options[:exclude_labels]
!(var).any?
end
end
issues
end
# @return [Array] filtered issues accourding milestone
def filter_by_milestone(filtered_issues, tag_name, all_issues)
remove_issues_in_milestones(filtered_issues)
unless tag_name.nil?
# add missed issues (according milestones)
issues_to_add = find_issues_to_add(all_issues, tag_name)
filtered_issues |= issues_to_add
end
filtered_issues
end
# Add all issues, that should be in that tag, according milestone
#
# @param [Array] all_issues
# @param [String] tag_name
# @return [Array] issues with milestone #tag_name
def find_issues_to_add(all_issues, tag_name)
all_issues.select do |issue|
if issue.milestone.nil?
false
else
# check, that this milestone in tag list:
milestone_is_tag = @all_tags.find do |tag|
tag.name == issue.milestone.title
end
if milestone_is_tag.nil?
false
else
issue.milestone.title == tag_name
end
end
end
end
# @return [Array] array with removed issues, that contain milestones with same name as a tag
def remove_issues_in_milestones(filtered_issues)
filtered_issues.select! do |issue|
# leave issues without milestones
if issue.milestone.nil?
true
else
# check, that this milestone in tag list:
@all_tags.find { |tag| tag.name == issue.milestone.title }.nil?
end
end
end
# Method filter issues, that belong only specified tag range
# @param [Array] array of issues to filter
# @param [Symbol] hash_key key of date value default is :actual_date
# @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
# @return [Array] filtered issues
def delete_by_time(array, hash_key = :actual_date, older_tag = nil, newer_tag = nil)
# in case if not tags specified - return unchanged array
return array if older_tag.nil? && newer_tag.nil?
newer_tag_time = newer_tag && @fetcher.get_time_of_tag(newer_tag)
older_tag_time = older_tag && @fetcher.get_time_of_tag(older_tag)
array.select do |req|
if req[hash_key]
time = Time.parse(req[hash_key]).utc
tag_in_range_old = tag_newer_old_tag?(older_tag_time, time)
tag_in_range_new = tag_older_new_tag?(newer_tag_time, time)
tag_in_range = (tag_in_range_old) && (tag_in_range_new)
tag_in_range
else
false
end
end
end
def tag_older_new_tag?(newer_tag_time, time)
if newer_tag_time.nil?
tag_in_range_new = true
else
tag_in_range_new = time <= newer_tag_time
end
tag_in_range_new
end
def tag_newer_old_tag?(older_tag_time, t)
if older_tag_time.nil?
tag_in_range_old = true
else
tag_in_range_old = t > older_tag_time
end
tag_in_range_old
end
# Include issues with labels, specified in :include_labels
# @param [Array] issues to filter
# @return [Array] filtered array of issues
def include_issues_by_labels(issues)
filtered_issues = filter_by_include_labels(issues)
filtered_issues |= filter_wo_labels(issues)
filtered_issues
end
# @return [Array] issues without labels or empty array if add_issues_wo_labels is false
def filter_wo_labels(issues)
if @options[:add_issues_wo_labels]
issues_wo_labels = issues.select do |issue|
!issue.labels.map(&:name).any?
end
return issues_wo_labels
end
[]
end
def filter_by_include_labels(issues)
filtered_issues = @options[:include_labels].nil? ? issues : issues.select do |issue|
labels = issue.labels.map(&:name) & @options[:include_labels]
(labels).any?
end
filtered_issues
end
# General filtered function
#
# @param [Array] all_issues
# @return [Array] filtered issues
def filter_array_by_labels(all_issues)
filtered_issues = include_issues_by_labels(all_issues)
exclude_issues_by_labels(filtered_issues)
end
# Filter issues according labels
# @return [Array] Filtered issues
def get_filtered_issues(issues)
issues = filter_array_by_labels(issues)
puts "Filtered issues: #{issues.count}" if @options[:verbose]
issues
end
# This method fetches missing params for PR and filter them by specified options
# It include add all PR's with labels from @options[:include_labels] array
# And exclude all from :exclude_labels array.
# @return [Array] filtered PR's
def get_filtered_pull_requests(pull_requests)
pull_requests = filter_array_by_labels(pull_requests)
pull_requests = filter_merged_pull_requests(pull_requests)
puts "Filtered pull requests: #{pull_requests.count}" if @options[:verbose]
pull_requests
end
# This method filter only merged PR and
# fetch missing required attributes for pull requests
# :merged_at - is a date, when issue PR was merged.
# More correct to use merged date, rather than closed date.
def filter_merged_pull_requests(pull_requests)
print "Fetching merged dates...\r" if @options[:verbose]
closed_pull_requests = @fetcher.fetch_closed_pull_requests
pull_requests.each do |pr|
fetched_pr = closed_pull_requests.find do |fpr|
fpr.number == pr.number
end
pr[:merged_at] = fetched_pr[:merged_at]
closed_pull_requests.delete(fetched_pr)
end
pull_requests.select! do |pr|
!pr[:merged_at].nil?
end
pull_requests
end
end
end

View File

@@ -0,0 +1,53 @@
module GitHubChangelogGenerator
class Generator
# fetch, filter tags, fetch dates and sort them in time order
def fetch_and_filter_tags
@all_tags = get_filtered_tags(@fetcher.get_all_tags)
fetch_tags_dates
sort_tags_by_date
end
# Sort all tags by date
def sort_tags_by_date
puts "Sorting tags..." if @options[:verbose]
@all_tags.sort_by! { |x| @fetcher.get_time_of_tag(x) }.reverse!
end
# Detect link, name and time for specified tag.
#
# @param [Hash] newer_tag newer tag. Can be nil, if it's Unreleased section.
# @return [Array] link, name and time of the tag
def detect_link_tag_time(newer_tag)
# if tag is nil - set current time
newer_tag_time = newer_tag.nil? ? Time.new : @fetcher.get_time_of_tag(newer_tag)
# if it's future release tag - set this value
if newer_tag.nil? && @options[:future_release]
newer_tag_name = @options[:future_release]
newer_tag_link = @options[:future_release]
else
# put unreleased label if there is no name for the tag
newer_tag_name = newer_tag.nil? ? @options[:unreleased_label] : newer_tag["name"]
newer_tag_link = newer_tag.nil? ? "HEAD" : newer_tag_name
end
[newer_tag_link, newer_tag_name, newer_tag_time]
end
# Return tags after filtering tags in lists provided by option: --between-tags & --exclude-tags
#
# @return [Array]
def get_filtered_tags(all_tags)
all_tags = all_tags
filtered_tags = all_tags
if @options[:between_tags]
@options[:between_tags].each do |tag|
unless all_tags.include? tag
puts "Warning: can't find tag #{tag}, specified with --between-tags option.".yellow
end
end
filtered_tags = all_tags.select { |tag| @options[:between_tags].include? tag }
end
filtered_tags
end
end
end

View File

@@ -5,34 +5,32 @@ require_relative "version"
module GitHubChangelogGenerator module GitHubChangelogGenerator
class Parser class Parser
# parse options with optparse
def self.parse_options def self.parse_options
options = { options = get_default_options
tag1: nil,
tag2: nil,
dateformat: "%Y-%m-%d",
output: "CHANGELOG.md",
issues: true,
add_issues_wo_labels: true,
add_pr_wo_labels: true,
pulls: true,
filter_issues_by_milestone: true,
author: true,
unreleased: true,
unreleased_label: "Unreleased",
compare_link: true,
include_labels: %w(bug enhancement),
exclude_labels: %w(duplicate question invalid wontfix),
max_issues: nil,
simple_list: false,
verbose: true,
merge_prefix: "**Merged pull requests:**", parser = setup_parser(options)
issue_prefix: "**Closed issues:**",
bug_prefix: "**Fixed bugs:**",
enhancement_prefix: "**Implemented enhancements:**",
branch: "origin"
}
parser.parse!
detect_user_and_project(options)
if !options[:user] || !options[:project]
puts parser.banner
exit
end
if options[:verbose]
puts "Performing task with options:"
pp options
puts ""
end
options
end
# setup parsing options
def self.setup_parser(options)
parser = OptionParser.new do |opts| parser = OptionParser.new do |opts|
opts.banner = "Usage: github_changelog_generator [options]" opts.banner = "Usage: github_changelog_generator [options]"
opts.on("-u", "--user [USER]", "Username of the owner of target GitHub repo") do |last| opts.on("-u", "--user [USER]", "Username of the owner of target GitHub repo") do |last|
@@ -45,7 +43,7 @@ module GitHubChangelogGenerator
options[:token] = last options[:token] = last
end end
opts.on("-f", "--date-format [FORMAT]", "Date format. Default is %Y-%m-%d") do |last| opts.on("-f", "--date-format [FORMAT]", "Date format. Default is %Y-%m-%d") do |last|
options[:dateformat] = last options[:date_format] = last
end end
opts.on("-o", "--output [NAME]", "Output file. Default is CHANGELOG.md") do |last| opts.on("-o", "--output [NAME]", "Output file. Default is CHANGELOG.md") do |last|
options[:output] = last options[:output] = last
@@ -86,6 +84,9 @@ module GitHubChangelogGenerator
opts.on("--exclude-labels x,y,z", Array, 'Issues with the specified labels will be always excluded from changelog. Default is \'duplicate,question,invalid,wontfix\'') do |list| opts.on("--exclude-labels x,y,z", Array, 'Issues with the specified labels will be always excluded from changelog. Default is \'duplicate,question,invalid,wontfix\'') do |list|
options[:exclude_labels] = list options[:exclude_labels] = list
end end
opts.on("--between-tags x,y,z", Array, "Change log will be filed only between specified tags") do |list|
options[:between_tags] = list
end
opts.on("--max-issues [NUMBER]", Integer, "Max number of issues to fetch from GitHub. Default is unlimited") do |max| opts.on("--max-issues [NUMBER]", Integer, "Max number of issues to fetch from GitHub. Default is unlimited") do |max|
options[:max_issues] = max options[:max_issues] = max
end end
@@ -113,79 +114,120 @@ module GitHubChangelogGenerator
exit exit
end end
end end
parser
end
parser.parse! # just get default options
def self.get_default_options
detect_user_and_project(options) options = {
tag1: nil,
if !options[:user] || !options[:project] tag2: nil,
puts parser.banner date_format: "%Y-%m-%d",
exit output: "CHANGELOG.md",
end issues: true,
add_issues_wo_labels: true,
if ARGV[1] add_pr_wo_labels: true,
options[:tag1] = ARGV[0] pulls: true,
options[:tag2] = ARGV[1] filter_issues_by_milestone: true,
end author: true,
unreleased: true,
if options[:verbose] unreleased_label: "Unreleased",
puts "Performing task with options:" compare_link: true,
pp options include_labels: %w(bug enhancement),
puts "" exclude_labels: %w(duplicate question invalid wontfix),
end max_issues: nil,
simple_list: false,
verbose: true,
merge_prefix: "**Merged pull requests:**",
issue_prefix: "**Closed issues:**",
bug_prefix: "**Fixed bugs:**",
enhancement_prefix: "**Implemented enhancements:**",
git_remote: "origin"
}
options options
end end
# Detects user and project from git
def self.detect_user_and_project(options) def self.detect_user_and_project(options)
if ARGV[0] && !ARGV[1] options[:user], options[:project] = user_project_from_option(ARGV[0], ARGV[1], options[:github_site])
github_site = options[:github_site] ? options[:github_site] : "github.com" if !options[:user] || !options[:project]
if ENV["RUBYLIB"] =~ /ruby-debug-ide/
options[:user] = "skywinder"
options[:project] = "changelog_test"
else
remote = `git config --get remote.#{options[:git_remote]}.url`
options[:user], options[:project] = user_project_from_remote(remote)
end
end
end
# Try to find user and project name from git remote output
#
# @param [String] output of git remote command
# @return [Array] user and project
def self.user_project_from_option(arg0, arg1, github_site = nil)
user = nil
project = nil
github_site ||= "github.com"
if arg0 && !arg1
# this match should parse strings such "https://github.com/skywinder/Github-Changelog-Generator" or "skywinder/Github-Changelog-Generator" to user and name # this match should parse strings such "https://github.com/skywinder/Github-Changelog-Generator" or "skywinder/Github-Changelog-Generator" to user and name
match = /(?:.+#{Regexp.escape(github_site)}\/)?(.+)\/(.+)/.match(ARGV[0]) puts arg0
match = /(?:.+#{Regexp.escape(github_site)}\/)?(.+)\/(.+)/.match(arg0)
begin begin
param = match[2].nil? param = match[2].nil?
rescue rescue
puts "Can't detect user and name from first parameter: '#{ARGV[0]}' -> exit'" puts "Can't detect user and name from first parameter: '#{arg0}' -> exit'"
exit exit
end end
if param if param
exit exit
else else
options[:user] = match[1] user = match[1]
options[:project] = match[2] project = match[2]
end
end
if !options[:user] && !options[:project]
if ENV["RUBYLIB"] =~ /ruby-debug-ide/
options[:user] = "skywinder"
options[:project] = "changelog_test"
else
remote = `git config --get remote.#{options[:branch]}.url`
# try to find repo in format:
# origin git@github.com:skywinder/Github-Changelog-Generator.git (fetch)
# git@github.com:skywinder/Github-Changelog-Generator.git
match = /.*(?:[:\/])((?:-|\w|\.)*)\/((?:-|\w|\.)*)(?:\.git).*/.match(remote)
if match && match[1] && match[2]
puts "Detected user:#{match[1]}, project:#{match[2]}"
options[:user] = match[1]
options[:project] = match[2]
else
# try to find repo in format:
# origin https://github.com/skywinder/ChangelogMerger (fetch)
# https://github.com/skywinder/ChangelogMerger
match = /.*\/((?:-|\w|\.)*)\/((?:-|\w|\.)*).*/.match(remote)
if match && match[1] && match[2]
puts "Detected user:#{match[1]}, project:#{match[2]}"
options[:user] = match[1]
options[:project] = match[2]
end
end
end end
end end
[user, project]
end
# Try to find user and project name from git remote output
#
# @param [String] output of git remote command
# @return [Array] user and project
def self.user_project_from_remote(remote)
# try to find repo in format:
# origin git@github.com:skywinder/Github-Changelog-Generator.git (fetch)
# git@github.com:skywinder/Github-Changelog-Generator.git
regex1 = /.*(?:[:\/])((?:-|\w|\.)*)\/((?:-|\w|\.)*)(?:\.git).*/
# try to find repo in format:
# origin https://github.com/skywinder/ChangelogMerger (fetch)
# https://github.com/skywinder/ChangelogMerger
regex2 = /.*\/((?:-|\w|\.)*)\/((?:-|\w|\.)*).*/
remote_structures = [regex1, regex2]
user = nil
project = nil
remote_structures.each do |regex|
matches = Regexp.new(regex).match(remote)
if matches && matches[1] && matches[2]
puts "Detected user:#{matches[1]}, project:#{matches[2]}"
user = matches[1]
project = matches[2]
end
break unless matches.nil?
end
[user, project]
end end
end end
if __FILE__ == $PROGRAM_NAME
remote = "invalid reference to project"
p user_project_from_option(ARGV[0], ARGV[1], remote)
end
end end

View File

@@ -0,0 +1,29 @@
describe GitHubChangelogGenerator::Generator do
describe "#get_filtered_tags" do
before(:all) do
@generator = GitHubChangelogGenerator::Generator.new
end
context "when between_tags nil" do
# before(:each) do
# @generator.options = {}
# end
subject { @generator.get_filtered_tags(%w(1 2 3)) }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(%w(1 2 3)) }
end
context "when between_tags 1" do
# before(:each) do
# @generator.options = {between_tags: ["1"]}
# end
subject do
@generator.instance_variable_set("@options", between_tags: ["1"])
@generator.get_filtered_tags(%w(1 2 3))
end
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(%w(1)) }
end
end
end

60
spec/unit/parser_spec.rb Normal file
View File

@@ -0,0 +1,60 @@
describe GitHubChangelogGenerator::Parser do
describe "#self.user_project_from_remote" do
context "when remote is 1" do
subject { GitHubChangelogGenerator::Parser.user_project_from_remote("origin https://github.com/skywinder/ActionSheetPicker-3.0 (fetch)") }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when remote is 2" do
subject { GitHubChangelogGenerator::Parser.user_project_from_remote("https://github.com/skywinder/ActionSheetPicker-3.0") }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when remote is 3" do
subject { GitHubChangelogGenerator::Parser.user_project_from_remote("https://github.com/skywinder/ActionSheetPicker-3.0") }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when remote is 4" do
subject { GitHubChangelogGenerator::Parser.user_project_from_remote("origin git@github.com:skywinder/ActionSheetPicker-3.0.git (fetch)") }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when remote is invalid" do
subject { GitHubChangelogGenerator::Parser.user_project_from_remote("some invalid text") }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array([nil, nil]) }
end
end
describe "#self.user_project_from_option" do
# context "when option is invalid" do
# it("should exit") { expect { GitHubChangelogGenerator::Parser.user_project_from_option("blah", nil) }.to raise_error(SystemExit) }
# end
context "when option is valid" do
subject { GitHubChangelogGenerator::Parser.user_project_from_option("skywinder/ActionSheetPicker-3.0", nil) }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when option nil" do
subject { GitHubChangelogGenerator::Parser.user_project_from_option(nil, nil) }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array([nil, nil]) }
end
context "when site is nil" do
subject { GitHubChangelogGenerator::Parser.user_project_from_option("skywinder/ActionSheetPicker-3.0", nil, nil) }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when site is valid" do
subject { GitHubChangelogGenerator::Parser.user_project_from_option("skywinder/ActionSheetPicker-3.0", nil, "https://codeclimate.com") }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array(["skywinder", "ActionSheetPicker-3.0"]) }
end
context "when second arg is not nil" do
subject { GitHubChangelogGenerator::Parser.user_project_from_option("skywinder/ActionSheetPicker-3.0", "blah", nil) }
it { is_expected.to be_a(Array) }
it { is_expected.to match_array([nil, nil]) }
end
end
end