diff --git a/github_changelog_generator.gemspec b/github_changelog_generator.gemspec index f1721ad..ea9b248 100644 --- a/github_changelog_generator.gemspec +++ b/github_changelog_generator.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "github_changelog_generator" - s.version = "1.1.2" + s.version = GitHubChangelogGenerator::VERSION s.default_executable = "github_changelog_generator" s.required_ruby_version = '>= 1.9.3' diff --git a/lib/github_changelog_generator.rb b/lib/github_changelog_generator.rb index d04496f..16d9484 100755 --- a/lib/github_changelog_generator.rb +++ b/lib/github_changelog_generator.rb @@ -6,371 +6,373 @@ require 'httparty' require 'colorize' require_relative 'github_changelog_generator/parser' +module GitHubChangelogGenerator + class ChangelogGenerator -class ChangelogGenerator + attr_accessor :options, :all_tags, :github - attr_accessor :options, :all_tags, :github + def initialize() - def initialize() + @options = Parser.parse_options - @options = Parser.parse_options + github_token - github_token + if @github_token.nil? + @github = Github.new + else + @github = Github.new oauth_token: @github_token + end - if @github_token.nil? - @github = Github.new - else - @github = Github.new oauth_token: @github_token + @all_tags = self.get_all_tags + @pull_requests = self.get_all_closed_pull_requests + @issues = self.get_all_issues + + @tag_times_hash = {} end - @all_tags = self.get_all_tags - @pull_requests = self.get_all_closed_pull_requests - @issues = self.get_all_issues - - @tag_times_hash = {} - end - - def print_json(json) - puts JSON.pretty_generate(json) - end - - def exec_command(cmd) - exec_cmd = "cd #{$project_path} && #{cmd}" - %x[#{exec_cmd}] - end - - - def get_all_closed_pull_requests - response = @github.pull_requests.list @options[:user], @options[:project], :state => 'closed' - - pull_requests = [] - response.each_page do |page| - pull_requests.concat(page) + def print_json(json) + puts JSON.pretty_generate(json) end - if @options[:verbose] - puts "Receive all pull requests: #{pull_requests.count}" + def exec_command(cmd) + exec_cmd = "cd #{$project_path} && #{cmd}" + %x[#{exec_cmd}] end - pull_requests - end - def compund_changelog - if @options[:verbose] - puts 'Generating changelog:' + def get_all_closed_pull_requests + response = @github.pull_requests.list @options[:user], @options[:project], :state => 'closed' + + pull_requests = [] + response.each_page do |page| + pull_requests.concat(page) + end + + if @options[:verbose] + puts "Receive all pull requests: #{pull_requests.count}" + end + + pull_requests end - log = "# Changelog\n\n" + def compund_changelog + if @options[:verbose] + puts 'Generating changelog:' + end - if @options[:last] - log += self.generate_log_between_tags(self.all_tags[0], self.all_tags[1]) - elsif @options[:tag1] && @options[:tag2] + log = "# Changelog\n\n" - tag1 = @options[:tag1] - tag2 = @options[:tag2] - tags_strings = [] - self.all_tags.each { |x| tags_strings.push(x['name']) } + if @options[:last] + log += self.generate_log_between_tags(self.all_tags[0], self.all_tags[1]) + elsif @options[:tag1] && @options[:tag2] - if tags_strings.include?(tag1) - if tags_strings.include?(tag2) - hash = Hash[tags_strings.map.with_index.to_a] - index1 = hash[tag1] - index2 = hash[tag2] - log += self.generate_log_between_tags(self.all_tags[index1], self.all_tags[index2]) + tag1 = @options[:tag1] + tag2 = @options[:tag2] + tags_strings = [] + self.all_tags.each { |x| tags_strings.push(x['name']) } + + if tags_strings.include?(tag1) + if tags_strings.include?(tag2) + hash = Hash[tags_strings.map.with_index.to_a] + index1 = hash[tag1] + index2 = hash[tag2] + log += self.generate_log_between_tags(self.all_tags[index1], self.all_tags[index2]) + else + puts "Can't find tag #{tag2} -> exit" + exit + end else - puts "Can't find tag #{tag2} -> exit" + puts "Can't find tag #{tag1} -> exit" exit end else - puts "Can't find tag #{tag1} -> exit" - exit - end - else - log += self.generate_log_for_all_tags - end - - log += "\n\n\\* *This changelog was generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*" - - output_filename = "#{@options[:output]}" - File.open(output_filename, 'w') { |file| file.write(log) } - - puts "Done! Generated log placed in #{output_filename}" - - end - - def generate_log_for_all_tags - log = '' - @all_tags.each {|tag| self.get_time_of_tag(tag)} - - - if @options[:verbose] - puts "Sorting tags.." - end - - @all_tags.sort_by! {|x| self.get_time_of_tag(x)}.reverse! - - if @options[:verbose] - puts "Generating log.." - end - - for index in 1 ... self.all_tags.size - log += self.generate_log_between_tags(self.all_tags[index], self.all_tags[index-1]) - end - - log += self.generate_log_before_tag(self.all_tags.last) - - log - end - - def is_megred(number) - @github.pull_requests.merged? @options[:user], @options[:project], number - end - - def get_all_tags - - url = "https://api.github.com/repos/#{@options[:user]}/#{@options[:project]}/tags" - - if @options[:verbose] - - puts "Receive tags for repo #{url}" - end - - response = @github.repos.tags @options[:user], @options[:project] - - tags = [] - response.each_page do |page| - tags.concat(page) - end - - if @options[:verbose] - puts "Found #{tags.count} tags" - end - - tags - end - - def github_token - if @options[:token] - return @github_token ||= @options[:token] - end - - env_var = ENV.fetch 'CHANGELOG_GITHUB_TOKEN', nil - - unless env_var - puts "Warning: No token provided (-t option) and variable $CHANGELOG_GITHUB_TOKEN was not found.".yellow - puts "This script can make only 50 requests to GitHub API per hour without token!".yellow - end - - @github_token ||= env_var - - end - - - def generate_log_between_tags(older_tag, newer_tag) - older_tag_time = self.get_time_of_tag(older_tag) - newer_tag_time = self.get_time_of_tag(newer_tag) - - # if we mix up tags order - lits fix it! - # if older_tag_time < newer_tag_time - # older_tag, newer_tag = newer_tag, older_tag - # older_tag_time, newer_tag_time = newer_tag_time, older_tag_time - # puts "Swap tags!" - # end - - newer_tag_name = newer_tag['name'] - - pull_requests = Array.new(@pull_requests) - - pull_requests.delete_if { |req| - if req[:merged_at] - t = Time.parse(req[:merged_at]).utc - tag_is_older_of_older = t > older_tag_time - tag_is_newer_than_new = t <= newer_tag_time - - tag_not_in_range = (tag_is_older_of_older) && (tag_is_newer_than_new) - !tag_not_in_range - else - true + log += self.generate_log_for_all_tags end - } + log += "\n\n\\* *This changelog was generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*" - issues = Array.new(@issues) + output_filename = "#{@options[:output]}" + File.open(output_filename, 'w') { |file| file.write(log) } - issues.delete_if { |issue| - if issue[:closed_at] - t = Time.parse(issue[:closed_at]).utc - tag_is_later_since = t > older_tag_time - tag_is_before_till = t <= newer_tag_time + puts "Done! Generated log placed in #{output_filename}" - in_range = (tag_is_later_since) && (tag_is_before_till) - !in_range - else - true + end + + def generate_log_for_all_tags + log = '' + @all_tags.each { |tag| self.get_time_of_tag(tag) } + + + if @options[:verbose] + puts "Sorting tags.." end - } + @all_tags.sort_by! { |x| self.get_time_of_tag(x) }.reverse! - self.create_log(pull_requests, issues, newer_tag_name, newer_tag_time) - - end - - def generate_log_before_tag(tag) - tag_time = self.get_time_of_tag(tag) - tag_name = tag['name'] - - pull_requests = Array.new(@pull_requests) - - pull_requests.delete_if { |req| - if req[:merged_at] - t = Time.parse(req[:merged_at]).utc - t > tag_time - else - true + if @options[:verbose] + puts "Generating log.." end - } - - issues = Array.new(@issues) - - issues.delete_if { |issue| - if issue[:closed_at] - t = Time.parse(issue[:closed_at]).utc - t > tag_time - else - true + for index in 1 ... self.all_tags.size + log += self.generate_log_between_tags(self.all_tags[index], self.all_tags[index-1]) end - } - self.create_log(pull_requests, issues, tag_name, tag_time) + log += self.generate_log_before_tag(self.all_tags.last) - end + log + end - def create_log(pull_requests, issues, tag_name, tag_time) + def is_megred(number) + @github.pull_requests.merged? @options[:user], @options[:project], number + end - # Generate tag name and link - trimmed_tag = tag_name.tr('v', '') - log = "## [#{trimmed_tag}] (https://github.com/#{@options[:user]}/#{@options[:project]}/tree/#{tag_name})\n" + def get_all_tags - #Generate date string: - time_string = tag_time.strftime @options[:format] - log += "#### #{time_string}\n" + url = "https://api.github.com/repos/#{@options[:user]}/#{@options[:project]}/tags" - if @options[:pulls] - # Generate pull requests: - if pull_requests - pull_requests.each { |dict| - merge = "#{@options[:merge_prefix]}#{dict[:title]} [\\##{dict[:number]}](#{dict.html_url})\n\n" + if @options[:verbose] + + puts "Receive tags for repo #{url}" + end + + response = @github.repos.tags @options[:user], @options[:project] + + tags = [] + response.each_page do |page| + tags.concat(page) + end + + if @options[:verbose] + puts "Found #{tags.count} tags" + end + + tags + end + + def github_token + if @options[:token] + return @github_token ||= @options[:token] + end + + env_var = ENV.fetch 'CHANGELOG_GITHUB_TOKEN', nil + + unless env_var + puts "Warning: No token provided (-t option) and variable $CHANGELOG_GITHUB_TOKEN was not found.".yellow + puts "This script can make only 50 requests to GitHub API per hour without token!".yellow + end + + @github_token ||= env_var + + end + + + def generate_log_between_tags(older_tag, newer_tag) + older_tag_time = self.get_time_of_tag(older_tag) + newer_tag_time = self.get_time_of_tag(newer_tag) + + # if we mix up tags order - lits fix it! + # if older_tag_time < newer_tag_time + # older_tag, newer_tag = newer_tag, older_tag + # older_tag_time, newer_tag_time = newer_tag_time, older_tag_time + # puts "Swap tags!" + # end + + newer_tag_name = newer_tag['name'] + + pull_requests = Array.new(@pull_requests) + + pull_requests.delete_if { |req| + if req[:merged_at] + t = Time.parse(req[:merged_at]).utc + tag_is_older_of_older = t > older_tag_time + tag_is_newer_than_new = t <= newer_tag_time + + tag_not_in_range = (tag_is_older_of_older) && (tag_is_newer_than_new) + !tag_not_in_range + else + true + end + + } + + issues = Array.new(@issues) + + issues.delete_if { |issue| + if issue[:closed_at] + t = Time.parse(issue[:closed_at]).utc + tag_is_later_since = t > older_tag_time + tag_is_before_till = t <= newer_tag_time + + in_range = (tag_is_later_since) && (tag_is_before_till) + !in_range + else + true + end + + } + + self.create_log(pull_requests, issues, newer_tag_name, newer_tag_time) + + end + + def generate_log_before_tag(tag) + tag_time = self.get_time_of_tag(tag) + tag_name = tag['name'] + + pull_requests = Array.new(@pull_requests) + + pull_requests.delete_if { |req| + if req[:merged_at] + t = Time.parse(req[:merged_at]).utc + t > tag_time + else + true + end + + } + + issues = Array.new(@issues) + + issues.delete_if { |issue| + if issue[:closed_at] + t = Time.parse(issue[:closed_at]).utc + t > tag_time + else + true + end + } + + self.create_log(pull_requests, issues, tag_name, tag_time) + + end + + def create_log(pull_requests, issues, tag_name, tag_time) + + # Generate tag name and link + trimmed_tag = tag_name.tr('v', '') + log = "## [#{trimmed_tag}] (https://github.com/#{@options[:user]}/#{@options[:project]}/tree/#{tag_name})\n" + + #Generate date string: + time_string = tag_time.strftime @options[:format] + log += "#### #{time_string}\n" + + if @options[:pulls] + # Generate pull requests: + if pull_requests + pull_requests.each { |dict| + merge = "#{@options[:merge_prefix]}#{dict[:title]} [\\##{dict[:number]}](#{dict.html_url})\n\n" + log += "- #{merge}" + } + end + end + + if @options[:issues] + # Generate issues: + if issues + issues.sort! { |x, y| + if x.labels.any? && y.labels.any? + x.labels[0].name <=> y.labels[0].name + else + if x.labels.any? + 1 + else + if y.labels.any? + -1 + else + 0 + end + end + end + }.reverse! + end + issues.each { |dict| + is_bug = false + is_enhancement = false + dict.labels.each { |label| + if label.name == 'bug' + is_bug = true + end + if label.name == 'enhancement' + is_enhancement = true + end + } + + intro = 'Closed issue' + if is_bug + intro = 'Fixed bug' + end + + if is_enhancement + intro = 'Implemented enhancement' + end + + merge = "*#{intro}:* #{dict[:title]} [\\##{dict[:number]}](#{dict.html_url})\n\n" log += "- #{merge}" } end + log end - if @options[:issues] - # Generate issues: - if issues - issues.sort! { |x, y| - if x.labels.any? && y.labels.any? - x.labels[0].name <=> y.labels[0].name - else - if x.labels.any? - 1 - else - if y.labels.any? - -1 - else - 0 - end - end - end - }.reverse! + def get_time_of_tag(prev_tag) + + if @tag_times_hash[prev_tag['name']] + return @tag_times_hash[prev_tag['name']] end - issues.each { |dict| - is_bug = false - is_enhancement = false - dict.labels.each { |label| - if label.name == 'bug' - is_bug = true - end - if label.name == 'enhancement' - is_enhancement = true - end + + if @options[:verbose] + puts "Getting time for tag #{prev_tag['name']}" + end + + github_git_data_commits_get = @github.git_data.commits.get @options[:user], @options[:project], prev_tag['commit']['sha'] + time_string = github_git_data_commits_get['committer']['date'] + Time.parse(time_string) + @tag_times_hash[prev_tag['name']] = Time.parse(time_string) + end + + def get_all_issues + response = @github.issues.list user: @options[:user], repo: @options[:project], state: 'closed', filter: 'all', labels: nil + + + issues = [] + response.each_page do |page| + issues.concat(page) + end + + # remove pull request from issues: + issues.select! { |x| + x.pull_request == nil + } + + if @options[:verbose] + puts "Receive all closed issues: #{issues.count}" + end + + filtered_issues = issues.select { |issue| + #compare is there any labels from @options[:labels] array + (issue.labels.map { |label| label.name } & @options[:labels]).any? + } + + if @options[:add_issues_wo_labels] + issues_wo_labels = issues.select { + # add issues without any labels + |issue| !issue.labels.map { |label| label.name }.any? } + filtered_issues.concat(issues_wo_labels) + end - intro = 'Closed issue' - if is_bug - intro = 'Fixed bug' - end + if @options[:verbose] + puts "Filter issues with labels #{@options[:labels]}#{@options[:add_issues_wo_labels] ? ' and w/o labels' : ''}: #{filtered_issues.count} issues" + end - if is_enhancement - intro = 'Implemented enhancement' - end + filtered_issues - merge = "*#{intro}:* #{dict[:title]} [\\##{dict[:number]}](#{dict.html_url})\n\n" - log += "- #{merge}" - } end - log - end - - def get_time_of_tag(prev_tag) - - if @tag_times_hash[prev_tag['name']] - return @tag_times_hash[prev_tag['name']] - end - - if @options[:verbose] - puts "Getting time for tag #{prev_tag['name']}" - end - - github_git_data_commits_get = @github.git_data.commits.get @options[:user], @options[:project], prev_tag['commit']['sha'] - time_string = github_git_data_commits_get['committer']['date'] - Time.parse(time_string) - @tag_times_hash[prev_tag['name']] = Time.parse(time_string) - end - - def get_all_issues - response = @github.issues.list user: @options[:user], repo: @options[:project], state: 'closed', filter: 'all', labels: nil - - - issues = [] - response.each_page do |page| - issues.concat(page) - end - - # remove pull request from issues: - issues.select! { |x| - x.pull_request == nil - } - - if @options[:verbose] - puts "Receive all closed issues: #{issues.count}" - end - - filtered_issues = issues.select { |issue| - #compare is there any labels from @options[:labels] array - (issue.labels.map { |label| label.name } & @options[:labels]).any? - } - - if @options[:add_issues_wo_labels] - issues_wo_labels = issues.select { - # add issues without any labels - |issue| !issue.labels.map { |label| label.name }.any? - } - filtered_issues.concat(issues_wo_labels) - end - - if @options[:verbose] - puts "Filter issues with labels #{@options[:labels]}#{@options[:add_issues_wo_labels]? ' and w/o labels' : ''}: #{filtered_issues.count} issues" - end - - filtered_issues end -end + if __FILE__ == $0 + changelog_generator = ChangelogGenerator.new + changelog_generator.compund_changelog + end -if __FILE__ == $0 - changelog_generator = ChangelogGenerator.new - changelog_generator.compund_changelog -end +end \ No newline at end of file diff --git a/lib/github_changelog_generator/parser.rb b/lib/github_changelog_generator/parser.rb index fe8f9b4..1bd08f8 100644 --- a/lib/github_changelog_generator/parser.rb +++ b/lib/github_changelog_generator/parser.rb @@ -1,81 +1,88 @@ #!/usr/bin/env ruby require 'optparse' +require_relative 'version' -class Parser - def self.parse_options - options = {:tag1 => nil, :tag2 => nil, :format => '%d/%m/%y', :output => 'CHANGELOG.md', :labels => %w(bug enhancement), :pulls => true, :issues => true, :verbose => true, :add_issues_wo_labels => true, :merge_prefix => '*Merged pull-request:* ' } +module GitHubChangelogGenerator + class Parser + def self.parse_options + options = {:tag1 => nil, :tag2 => nil, :format => '%d/%m/%y', :output => 'CHANGELOG.md', :labels => %w(bug enhancement), :pulls => true, :issues => true, :verbose => true, :add_issues_wo_labels => true, :merge_prefix => '*Merged pull-request:* '} - parser = OptionParser.new { |opts| - opts.banner = 'Usage: changelog_generator [options]' - opts.on('-u', '--user [USER]', 'Username of the owner of target GitHub repo') do |last| - options[:user] = last - end - opts.on('-p', '--project [PROJECT]', 'Name of project on GitHub') do |last| - options[:project] = last - end - opts.on('-t', '--token [TOKEN]', 'To make more than 50 requests this script required your OAuth token for GitHub. You can generate here: https://github.com/settings/tokens/new') do |last| - options[:token] = last - end - opts.on('-h', '--help', 'Displays Help') do - puts opts + parser = OptionParser.new { |opts| + opts.banner = 'Usage: changelog_generator [options]' + opts.on('-u', '--user [USER]', 'Username of the owner of target GitHub repo') do |last| + options[:user] = last + end + opts.on('-p', '--project [PROJECT]', 'Name of project on GitHub') do |last| + options[:project] = last + end + opts.on('-t', '--token [TOKEN]', 'To make more than 50 requests this script required your OAuth token for GitHub. You can generate here: https://github.com/settings/tokens/new') do |last| + options[:token] = last + end + opts.on('-h', '--help', 'Displays Help') do + puts opts + exit + end + opts.on('--[no-]verbose', 'Run verbosely. Default is true') do |v| + options[:verbose] = v + end + opts.on('--[no-]issues', 'Include closed issues to changelog. Default is true') do |v| + options[:issues] = v + end + opts.on('--[no-]issues-without-labels', 'Include closed issues without any labels to changelog. Default is true') do |v| + options[:add_issues_wo_labels] = v + end + opts.on('--[no-]pull-requests', 'Include pull-requests to changelog. Default is true') do |v| + options[:pulls] = v + end + opts.on('-l', '--last-changes', 'Generate log between last 2 tags only') do |last| + options[:last] = last + end + opts.on('-f', '--date-format [FORMAT]', 'Date format. Default is %d/%m/%y') do |last| + options[:format] = last + end + opts.on('-o', '--output [NAME]', 'Output file. Default is CHANGELOG.md') do |last| + options[:output] = last + end + opts.on('--labels x,y,z', Array, 'List of labels. Issues with that labels will be included to changelog. Default is \'bug,enhancement\'') do |list| + options[:labels] = list + end + opts.on('-v', '--version', 'Print version number') do |v| + puts "Version: #{GitHubChangelogGenerator::VERSION}" + exit + end + } + + parser.parse! + + #udefined case with 1 parameter: + if ARGV[0] && !ARGV[1] + puts parser.banner exit end - opts.on('-v', '--[no-]verbose', 'Run verbosely. Default is true') do |v| - options[:verbose] = v - end - opts.on('--[no-]issues', 'Include closed issues to changelog. Default is true') do |v| - options[:issues] = v - end - opts.on('--[no-]issues-without-labels', 'Include closed issues without any labels to changelog. Default is true') do |v| - options[:add_issues_wo_labels] = v - end - opts.on('--[no-]pull-requests', 'Include pull-requests to changelog. Default is true') do |v| - options[:pulls] = v - end - opts.on('-l', '--last-changes', 'Generate log between last 2 tags only') do |last| - options[:last] = last - end - opts.on('-f', '--date-format [FORMAT]', 'Date format. Default is %d/%m/%y') do |last| - options[:format] = last - end - opts.on('-o', '--output [NAME]', 'Output file. Default is CHANGELOG.md') do |last| - options[:output] = last - end - opts.on('--labels x,y,z', Array, 'List of labels. Issues with that labels will be included to changelog. Default is \'bug,enhancement\'') do |list| - options[:labels] = list - end - } - parser.parse! + if !options[:user] && !options[:project] + remote = `git remote -vv`.split("\n") + match = /.*(?:[:\/])((?:-|\w|\.)*)\/((?:-|\w|\.)*)\.git.*/.match(remote[0]) - #udefined case with 1 parameter: - if ARGV[0] && !ARGV[1] - puts parser.banner - exit + if match && match[1] && match[2] + puts "Detected user:#{match[1]}, project:#{match[2]}" + options[:user], options[:project] = match[1], match[2] + end + end + + + if !options[:user] || !options[:project] + puts parser.banner + exit + end + + if ARGV[1] + options[:tag1] = ARGV[0] + options[:tag2] = ARGV[1] + + end + + options end - - if !options[:user] && !options[:project] - remote = `git remote -vv`.split("\n") - match = /.*(?:[:\/])((?:-|\w|\.)*)\/((?:-|\w|\.)*)\.git.*/.match(remote[0]) - - if match && match[1] && match[2] - puts "Detected user:#{match[1]}, project:#{match[2]}" - options[:user], options[:project] = match[1], match[2] - end - end - - - if !options[:user] || !options[:project] - puts parser.banner - exit - end - - if ARGV[1] - options[:tag1] = ARGV[0] - options[:tag2] = ARGV[1] - - end - - options end end \ No newline at end of file diff --git a/lib/github_changelog_generator/version.rb b/lib/github_changelog_generator/version.rb new file mode 100644 index 0000000..0ad5364 --- /dev/null +++ b/lib/github_changelog_generator/version.rb @@ -0,0 +1,3 @@ +module GitHubChangelogGenerator + VERSION = '1.1.2' +end