github_changelog_generator 1.3.11 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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