github_changelog_generator 1.3.11 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ require "logger"
2
+
3
+ module GitHubChangelogGenerator
4
+ # A Fetcher responsible for all requests to GitHub and all basic manipulation with related data
5
+ # (such as filtering, validating, e.t.c)
6
+ #
7
+ # Example:
8
+ # fetcher = GitHubChangelogGenerator::Fetcher.new options
9
+ class Fetcher
10
+ PER_PAGE_NUMBER = 30
11
+ GH_RATE_LIMIT_EXCEEDED_MSG = "Warning: GitHub API rate limit (5000 per hour) exceeded, change log may be " \
12
+ "missing some issues. You can limit the number of issues fetched using the `--max-issues NUM` argument."
13
+
14
+ def initialize(options = {})
15
+ @options = options
16
+
17
+ @user = @options[:user]
18
+ @project = @options[:project]
19
+ @github_token = fetch_github_token
20
+ @tag_times_hash = {}
21
+
22
+ @logger = Logger.new(STDOUT)
23
+ @logger.formatter = proc do |_severity, _datetime, _progname, msg|
24
+ "#{msg}\n"
25
+ end
26
+ github_options = { per_page: PER_PAGE_NUMBER }
27
+ github_options[:oauth_token] = @github_token unless @github_token.nil?
28
+ github_options[:endpoint] = options[:github_endpoint] unless options[:github_endpoint].nil?
29
+ github_options[:site] = options[:github_endpoint] unless options[:github_site].nil?
30
+
31
+ begin
32
+ @github = Github.new github_options
33
+ rescue
34
+ @logger.warn GH_RATE_LIMIT_EXCEEDED_MSG.yellow
35
+ end
36
+ end
37
+
38
+ # Returns GitHub token. First try to use variable, provided by --token option,
39
+ # otherwise try to fetch it from CHANGELOG_GITHUB_TOKEN env variable.
40
+ #
41
+ # @return [String]
42
+ def fetch_github_token
43
+ env_var = @options[:token] ? @options[:token] : (ENV.fetch "CHANGELOG_GITHUB_TOKEN", nil)
44
+
45
+ unless env_var
46
+ @logger.warn "Warning: No token provided (-t option) and variable $CHANGELOG_GITHUB_TOKEN was not found.".yellow
47
+ @logger.warn "This script can make only 50 requests to GitHub API per hour without token!".yellow
48
+ end
49
+
50
+ env_var
51
+ end
52
+
53
+ # Fetch all tags from repo
54
+ # @return [Array] array of tags
55
+ def get_all_tags
56
+ if @options[:verbose]
57
+ print "Fetching tags...\r"
58
+ end
59
+
60
+ tags = []
61
+
62
+ begin
63
+ response = @github.repos.tags @options[:user], @options[:project]
64
+ page_i = 0
65
+ count_pages = response.count_pages
66
+ response.each_page do |page|
67
+ page_i += PER_PAGE_NUMBER
68
+ print "Fetching tags... #{page_i}/#{count_pages * PER_PAGE_NUMBER}\r"
69
+ tags.concat(page)
70
+ end
71
+ print " \r"
72
+
73
+ if tags.count == 0
74
+ @logger.warn "Warning: Can't find any tags in repo.\
75
+ Make sure, that you push tags to remote repo via 'git push --tags'".yellow
76
+ elsif @options[:verbose]
77
+ @logger.info "Found #{tags.count} tags"
78
+ end
79
+
80
+ rescue
81
+ @logger.warn GH_RATE_LIMIT_EXCEEDED_MSG.yellow
82
+ end
83
+
84
+ tags
85
+ end
86
+
87
+ # This method fetch all closed issues and separate them to pull requests and pure issues
88
+ # (pull request is kind of issue in term of GitHub)
89
+ # @return [Tuple] with issues and pull requests
90
+ def fetch_issues_and_pull_requests
91
+ if @options[:verbose]
92
+ print "Fetching closed issues...\r"
93
+ end
94
+ issues = []
95
+
96
+ begin
97
+ response = @github.issues.list user: @options[:user],
98
+ repo: @options[:project],
99
+ state: "closed",
100
+ filter: "all",
101
+ labels: nil
102
+ page_i = 0
103
+ count_pages = response.count_pages
104
+ response.each_page do |page|
105
+ page_i += PER_PAGE_NUMBER
106
+ print "Fetching issues... #{page_i}/#{count_pages * PER_PAGE_NUMBER}\r"
107
+ issues.concat(page)
108
+ break if @options[:max_issues] && issues.length >= @options[:max_issues]
109
+ end
110
+ rescue
111
+ @logger.warn GH_RATE_LIMIT_EXCEEDED_MSG.yellow
112
+ end
113
+
114
+ print " \r"
115
+
116
+ if @options[:verbose]
117
+ @logger.info "Received issues: #{issues.count}"
118
+ end
119
+
120
+ # remove pull request from issues:
121
+ issues.partition { |x|
122
+ x[:pull_request].nil?
123
+ }
124
+ end
125
+
126
+ # Fetch all pull requests. We need them to detect :merged_at parameter
127
+ # @return [Array] all pull requests
128
+ def fetch_pull_requests
129
+ pull_requests = []
130
+ begin
131
+ response = @github.pull_requests.list @options[:user], @options[:project], state: "closed"
132
+ page_i = 0
133
+ response.each_page do |page|
134
+ page_i += PER_PAGE_NUMBER
135
+ count_pages = response.count_pages
136
+ print "Fetching merged dates... #{page_i}/#{count_pages * PER_PAGE_NUMBER}\r"
137
+ pull_requests.concat(page)
138
+ end
139
+ rescue
140
+ @logger.warn GH_RATE_LIMIT_EXCEEDED_MSG.yellow
141
+ end
142
+
143
+ print " \r"
144
+ pull_requests
145
+ end
146
+
147
+ # Fetch event for all issues and add them to :events
148
+ # @param [Array] issues
149
+ # @return [Void]
150
+ def fetch_events_async(issues)
151
+ i = 0
152
+ max_thread_number = 50
153
+ threads = []
154
+ issues.each_slice(max_thread_number) { |issues_slice|
155
+ issues_slice.each { |issue|
156
+ threads << Thread.new {
157
+ begin
158
+ obj = @github.issues.events.list user: @options[:user],
159
+ repo: @options[:project],
160
+ issue_number: issue["number"]
161
+ rescue
162
+ @logger.warn GH_RATE_LIMIT_EXCEEDED_MSG.yellow
163
+ end
164
+ issue[:events] = obj.body
165
+ print "Fetching events for issues and PR: #{i + 1}/#{issues.count}\r"
166
+ i += 1
167
+ }
168
+ }
169
+ threads.each(&:join)
170
+ threads = []
171
+ }
172
+
173
+ # to clear line from prev print
174
+ print " \r"
175
+
176
+ if @options[:verbose]
177
+ @logger.info "Fetching events for issues and PR: #{i} Done!"
178
+ end
179
+ end
180
+
181
+ # Try to find tag date in local hash.
182
+ # Otherwise fFetch tag time and put it to local hash file.
183
+ # @param [String] tag_name name of the tag
184
+ # @return [Time] time of specified tag
185
+ def get_time_of_tag(tag_name)
186
+ fail ChangelogGeneratorError, "tag_name is nil".red if tag_name.nil?
187
+
188
+ if @tag_times_hash[tag_name["name"]]
189
+ return @tag_times_hash[tag_name["name"]]
190
+ end
191
+
192
+ begin
193
+ github_git_data_commits_get = @github.git_data.commits.get @options[:user],
194
+ @options[:project],
195
+ tag_name["commit"]["sha"]
196
+ rescue
197
+ @logger.warn GH_RATE_LIMIT_EXCEEDED_MSG.yellow
198
+ end
199
+ time_string = github_git_data_commits_get["committer"]["date"]
200
+ @tag_times_hash[tag_name["name"]] = Time.parse(time_string)
201
+ end
202
+
203
+ # Fetch commit for specifed event
204
+ # @return [Hash]
205
+ def fetch_commit(event)
206
+ @github.git_data.commits.get @options[:user], @options[:project], event[:commit_id]
207
+ end
208
+ end
209
+ end
@@ -1,12 +1,18 @@
1
1
  module GitHubChangelogGenerator
2
2
  class Generator
3
-
4
3
  def initialize(options = nil)
5
4
  @options = options
6
5
  end
7
6
 
7
+ # Parse issue and generate single line formatted issue line.
8
+ #
9
+ # Example output:
10
+ # - Add coveralls integration [\#223](https://github.com/skywinder/github-changelog-generator/pull/223) ([skywinder](https://github.com/skywinder))
11
+ #
12
+ # @param [Hash] issue Fetched issue from GitHub
13
+ # @return [String] Markdown-formatted single issue
8
14
  def get_string_for_issue(issue)
9
- encapsulated_title = self.encapsulate_string issue[:title]
15
+ encapsulated_title = encapsulate_string issue[:title]
10
16
 
11
17
  title_with_number = "#{encapsulated_title} [\\##{issue[:number]}](#{issue.html_url})"
12
18
 
@@ -23,17 +29,14 @@ module GitHubChangelogGenerator
23
29
  end
24
30
 
25
31
  def encapsulate_string(string)
26
-
27
32
  string.gsub! '\\', '\\\\'
28
33
 
29
34
  encpas_chars = %w(> * _ \( \) [ ] #)
30
- encpas_chars.each { |char|
35
+ encpas_chars.each do |char|
31
36
  string.gsub! char, "\\#{char}"
32
- }
37
+ end
33
38
 
34
39
  string
35
40
  end
36
-
37
41
  end
38
-
39
- end
42
+ end
@@ -1,114 +1,142 @@
1
1
  #!/usr/bin/env ruby
2
- require 'optparse'
3
- require 'pp'
4
- require_relative 'version'
2
+ require "optparse"
3
+ require "pp"
4
+ require_relative "version"
5
5
 
6
6
  module GitHubChangelogGenerator
7
7
  class Parser
8
8
  def self.parse_options
9
-
10
9
  options = {
11
- :tag1 => nil,
12
- :tag2 => nil,
13
- :format => '%Y-%m-%d',
14
- :output => 'CHANGELOG.md',
15
- :exclude_labels => %w(duplicate question invalid wontfix),
16
- :pulls => true,
17
- :issues => true,
18
- :verbose => true,
19
- :add_issues_wo_labels => true,
20
- :add_pr_wo_labels => true,
21
- :merge_prefix => '**Merged pull requests:**',
22
- :issue_prefix => '**Closed issues:**',
23
- :bug_prefix => '**Fixed bugs:**',
24
- :enhancement_prefix => '**Implemented enhancements:**',
25
- :author => true,
26
- :filter_issues_by_milestone => true,
27
- :compare_link => true,
28
- :unreleased => true,
29
- :unreleased_label => 'Unreleased',
30
- :branch => 'origin'
10
+ tag1: nil,
11
+ tag2: nil,
12
+ dateformat: "%Y-%m-%d",
13
+ output: "CHANGELOG.md",
14
+ issues: true,
15
+ add_issues_wo_labels: true,
16
+ add_pr_wo_labels: true,
17
+ pulls: true,
18
+ filter_issues_by_milestone: true,
19
+ author: true,
20
+ unreleased: true,
21
+ unreleased_label: "Unreleased",
22
+ compare_link: true,
23
+ include_labels: %w(bug enhancement),
24
+ exclude_labels: %w(duplicate question invalid wontfix),
25
+ max_issues: nil,
26
+ simple_list: false,
27
+ verbose: true,
28
+
29
+ merge_prefix: "**Merged pull requests:**",
30
+ issue_prefix: "**Closed issues:**",
31
+ bug_prefix: "**Fixed bugs:**",
32
+ enhancement_prefix: "**Implemented enhancements:**",
33
+ branch: "origin"
31
34
  }
32
35
 
33
- parser = OptionParser.new { |opts|
34
- opts.banner = 'Usage: github_changelog_generator [options]'
35
- opts.on('-u', '--user [USER]', 'Username of the owner of target GitHub repo') do |last|
36
+ parser = OptionParser.new do |opts|
37
+ opts.banner = "Usage: github_changelog_generator [options]"
38
+ opts.on("-u", "--user [USER]", "Username of the owner of target GitHub repo") do |last|
36
39
  options[:user] = last
37
40
  end
38
- opts.on('-p', '--project [PROJECT]', 'Name of project on GitHub') do |last|
41
+ opts.on("-p", "--project [PROJECT]", "Name of project on GitHub") do |last|
39
42
  options[:project] = last
40
43
  end
41
- opts.on('-t', '--token [TOKEN]', 'To make more than 50 requests per hour your GitHub token required. You can generate it here: https://github.com/settings/tokens/new') do |last|
44
+ opts.on("-t", "--token [TOKEN]", "To make more than 50 requests per hour your GitHub token is required. You can generate it at: https://github.com/settings/tokens/new") do |last|
42
45
  options[:token] = last
43
46
  end
44
- opts.on('-f', '--date-format [FORMAT]', 'Date format. Default is %d/%m/%y') do |last|
45
- options[:format] = last
47
+ opts.on("-f", "--date-format [FORMAT]", "Date format. Default is %Y-%m-%d") do |last|
48
+ options[:dateformat] = last
46
49
  end
47
- opts.on('-o', '--output [NAME]', 'Output file. Default is CHANGELOG.md') do |last|
50
+ opts.on("-o", "--output [NAME]", "Output file. Default is CHANGELOG.md") do |last|
48
51
  options[:output] = last
49
52
  end
50
- opts.on('--[no-]issues', 'Include closed issues to changelog. Default is true') do |v|
53
+ opts.on("--[no-]issues", "Include closed issues in changelog. Default is true") do |v|
51
54
  options[:issues] = v
52
55
  end
53
- opts.on('--[no-]issues-wo-labels', 'Include closed issues without labels to changelog. Default is true') do |v|
56
+ opts.on("--[no-]issues-wo-labels", "Include closed issues without labels in changelog. Default is true") do |v|
54
57
  options[:add_issues_wo_labels] = v
55
58
  end
56
- opts.on('--[no-]pr-wo-labels', 'Include pull requests without labels to changelog. Default is true') do |v|
59
+ opts.on("--[no-]pr-wo-labels", "Include pull requests without labels in changelog. Default is true") do |v|
57
60
  options[:add_pr_wo_labels] = v
58
61
  end
59
- opts.on('--[no-]pull-requests', 'Include pull-requests to changelog. Default is true') do |v|
62
+ opts.on("--[no-]pull-requests", "Include pull-requests in changelog. Default is true") do |v|
60
63
  options[:pulls] = v
61
64
  end
62
- opts.on('--[no-]filter-by-milestone', 'Use milestone to detect when issue was resolved. Default is true') do |last|
65
+ opts.on("--[no-]filter-by-milestone", "Use milestone to detect when issue was resolved. Default is true") do |last|
63
66
  options[:filter_issues_by_milestone] = last
64
67
  end
65
- opts.on('--[no-]author', 'Add author of pull-request in the end. Default is true') do |author|
68
+ opts.on("--[no-]author", "Add author of pull-request in the end. Default is true") do |author|
66
69
  options[:author] = author
67
70
  end
68
- opts.on('--unreleased-only', 'Generate log from unreleased closed issues only.') do |v|
71
+ opts.on("--unreleased-only", "Generate log from unreleased closed issues only.") do |v|
69
72
  options[:unreleased_only] = v
70
73
  end
71
- opts.on('--[no-]unreleased', 'Add to log unreleased closed issues. Default is true') do |v|
74
+ opts.on("--[no-]unreleased", "Add to log unreleased closed issues. Default is true") do |v|
72
75
  options[:unreleased] = v
73
76
  end
74
- opts.on('--unreleased-label [label]', 'Add to log unreleased closed issues. Default is true') do |v|
77
+ opts.on("--unreleased-label [label]", "Add to log unreleased closed issues. Default is true") do |v|
75
78
  options[:unreleased_label] = v
76
79
  end
77
- opts.on('--[no-]compare-link', 'Include compare link (Full Changelog) between older version and newer version. Default is true') do |v|
80
+ opts.on("--[no-]compare-link", "Include compare link (Full Changelog) between older version and newer version. Default is true") do |v|
78
81
  options[:compare_link] = v
79
82
  end
80
- opts.on('--include-labels x,y,z', Array, 'Issues only with that labels will be included to changelog. Default is \'bug,enhancement\'') do |list|
83
+ opts.on("--include-labels x,y,z", Array, 'Only issues with the specified labels will be included in the changelog. Default is \'bug,enhancement\'') do |list|
81
84
  options[:include_labels] = list
82
85
  end
83
- opts.on('--exclude-labels x,y,z', Array, 'Issues with that labels will be always excluded from changelog. Default is \'duplicate,question,invalid,wontfix\'') do |list|
86
+ 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|
84
87
  options[:exclude_labels] = list
85
88
  end
86
- opts.on('--github-site [URL]', 'The Enterprise Github site on which your project is hosted.') do |last|
89
+ opts.on("--max-issues [NUMBER]", Integer, "Max number of issues to fetch from GitHub. Default is unlimited") do |max|
90
+ options[:max_issues] = max
91
+ end
92
+ opts.on("--github-site [URL]", "The Enterprise Github site on which your project is hosted.") do |last|
87
93
  options[:github_site] = last
88
94
  end
89
- opts.on('--github-api [URL]', 'The enterprise endpoint to use for your Github API.') do |last|
95
+ opts.on("--github-api [URL]", "The enterprise endpoint to use for your Github API.") do |last|
90
96
  options[:github_endpoint] = last
91
97
  end
92
- opts.on('--simple-list', 'Create simple list from issues and pull requests. Default is false.') do |v|
98
+ opts.on("--simple-list", "Create simple list from issues and pull requests. Default is false.") do |v|
93
99
  options[:simple_list] = v
94
100
  end
95
- opts.on('--[no-]verbose', 'Run verbosely. Default is true') do |v|
101
+ opts.on("--[no-]verbose", "Run verbosely. Default is true") do |v|
96
102
  options[:verbose] = v
97
103
  end
98
- opts.on('-v', '--version', 'Print version number') do |v|
104
+ opts.on("-v", "--version", "Print version number") do |_v|
99
105
  puts "Version: #{GitHubChangelogGenerator::VERSION}"
100
106
  exit
101
107
  end
102
- opts.on('-h', '--help', 'Displays Help') do
108
+ opts.on("-h", "--help", "Displays Help") do
103
109
  puts opts
104
110
  exit
105
111
  end
106
- }
112
+ end
107
113
 
108
114
  parser.parse!
109
115
 
116
+ detect_user_and_project(options)
117
+
118
+ if !options[:user] || !options[:project]
119
+ puts parser.banner
120
+ exit
121
+ end
122
+
123
+ if ARGV[1]
124
+ options[:tag1] = ARGV[0]
125
+ options[:tag2] = ARGV[1]
126
+ end
127
+
128
+ if options[:verbose]
129
+ puts "Performing task with options:"
130
+ pp options
131
+ puts ""
132
+ end
133
+
134
+ options
135
+ end
136
+
137
+ def self.detect_user_and_project(options)
110
138
  if ARGV[0] && !ARGV[1]
111
- github_site = options[:github_site] ? options[:github_site] : 'github.com'
139
+ github_site = options[:github_site] ? options[:github_site] : "github.com"
112
140
  # this match should parse strings such "https://github.com/skywinder/Github-Changelog-Generator" or "skywinder/Github-Changelog-Generator" to user and name
113
141
  match = /(?:.+#{Regexp.escape(github_site)}\/)?(.+)\/(.+)/.match(ARGV[0])
114
142
 
@@ -122,52 +150,37 @@ module GitHubChangelogGenerator
122
150
  exit
123
151
  else
124
152
  options[:user] = match[1]
125
- options[:project]= match[2]
153
+ options[:project] = match[2]
126
154
  end
127
155
 
128
-
129
156
  end
130
157
 
131
158
  if !options[:user] && !options[:project]
132
- remote = `git config --get remote.#{options[:branch]}.url`
133
- # try to find repo in format:
134
- # origin git@github.com:skywinder/Github-Changelog-Generator.git (fetch)
135
- # git@github.com:skywinder/Github-Changelog-Generator.git
136
- match = /.*(?:[:\/])((?:-|\w|\.)*)\/((?:-|\w|\.)*)(?:\.git).*/.match(remote)
137
-
138
- if match && match[1] && match[2]
139
- puts "Detected user:#{match[1]}, project:#{match[2]}"
140
- options[:user], options[:project] = match[1], match[2]
159
+ if ENV["RUBYLIB"] =~ /ruby-debug-ide/
160
+ options[:user] = "skywinder"
161
+ options[:project] = "changelog_test"
141
162
  else
142
- # try to find repo in format:
143
- # origin https://github.com/skywinder/ChangelogMerger (fetch)
144
- # https://github.com/skywinder/ChangelogMerger
145
- match = /.*\/((?:-|\w|\.)*)\/((?:-|\w|\.)*).*/.match(remote)
163
+ remote = `git config --get remote.#{options[:branch]}.url`
164
+ # try to find repo in format:
165
+ # origin git@github.com:skywinder/Github-Changelog-Generator.git (fetch)
166
+ # git@github.com:skywinder/Github-Changelog-Generator.git
167
+ match = /.*(?:[:\/])((?:-|\w|\.)*)\/((?:-|\w|\.)*)(?:\.git).*/.match(remote)
168
+
146
169
  if match && match[1] && match[2]
147
170
  puts "Detected user:#{match[1]}, project:#{match[2]}"
148
171
  options[:user], options[:project] = match[1], match[2]
172
+ else
173
+ # try to find repo in format:
174
+ # origin https://github.com/skywinder/ChangelogMerger (fetch)
175
+ # https://github.com/skywinder/ChangelogMerger
176
+ match = /.*\/((?:-|\w|\.)*)\/((?:-|\w|\.)*).*/.match(remote)
177
+ if match && match[1] && match[2]
178
+ puts "Detected user:#{match[1]}, project:#{match[2]}"
179
+ options[:user], options[:project] = match[1], match[2]
180
+ end
149
181
  end
150
182
  end
151
183
  end
152
-
153
-
154
- if !options[:user] || !options[:project]
155
- puts parser.banner
156
- exit
157
- end
158
-
159
- if ARGV[1]
160
- options[:tag1] = ARGV[0]
161
- options[:tag2] = ARGV[1]
162
- end
163
-
164
- if options[:verbose]
165
- puts 'Performing task with options:'
166
- pp options
167
- puts ''
168
- end
169
-
170
- options
171
184
  end
172
185
  end
173
186
  end