github-csv-changelog 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7254318b75a817eec6afaeb3be41d56fb71597b4
4
+ data.tar.gz: 6c374760e7d196abc5b11e3b175876f49d9dba35
5
+ SHA512:
6
+ metadata.gz: 34800daede9e54f57809598c2420416a19dd8a416b323f28e01712f42f4360031915f76d3d2c4ca07dcf12df81268456c2bada7c1090598db210413a31da6de1
7
+ data.tar.gz: 4213fefe64dd17ea42e22a8c2991a78f3a10aa65cce38359503aac29edc25d481ea7d23dfd0cac2472072a94f3f426d8ae6a7257d151d0bc16ef9bc9b255629d
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "github-csv-changelog"
4
+
5
+ main()
@@ -0,0 +1,276 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'json'
5
+ require 'csv'
6
+ require 'io/console'
7
+ require 'optparse'
8
+
9
+ # /// Constants. ///
10
+
11
+ GITHUB_API_NOTE = "Github commit parser."
12
+ COMMIT_PARSER_API_TOKEN_ENV_KEY = "COMMIT_PARSER_API_TOKEN"
13
+
14
+ # /// Helper functions for network requests. ///
15
+
16
+ # Makes POST request with default SSL usage.
17
+ def post(url, username, password, body_JSON)
18
+ uri = URI.parse(url)
19
+ request = Net::HTTP::Post.new(uri)
20
+ request.basic_auth username, password
21
+ request.body = JSON.dump(body_JSON)
22
+ req_options = {
23
+ use_ssl: uri.scheme == "https",
24
+ }
25
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
26
+ http.request(request)
27
+ end
28
+ return response
29
+ end
30
+
31
+ # Makes GET request for Github API request.
32
+ def get(url, access_token)
33
+ uri = URI.parse(url)
34
+ request = Net::HTTP::Get.new(uri)
35
+ request["Authorization"] = "token #{access_token}"
36
+
37
+ req_options = {
38
+ use_ssl: uri.scheme == "https",
39
+ }
40
+
41
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
42
+ http.request(request)
43
+ end
44
+
45
+ return response
46
+ end
47
+
48
+ # /// Network requests. ///
49
+
50
+ # Pings for 2-factor one-time password.
51
+ def request_2factor_passcode(username, password)
52
+ url = 'https://api.github.com/authorizations'
53
+ body_JSON = JSON.dump({
54
+ "scopes" => [
55
+ "repo",
56
+ "user"
57
+ ],
58
+ "note" => GITHUB_API_NOTE
59
+ })
60
+ response = post(url, username, password, body_JSON)
61
+ return !response['x-github-otp'].nil? && response.code === "401"
62
+ end
63
+
64
+ # Gets access token with one-time password for 2 factor authentication.
65
+ # This does not call `post` helper due to its special handling for one-time password (`otp`).
66
+ def get_token_with_2factor_otp(username, password, otp)
67
+ uri = URI.parse("https://api.github.com/authorizations")
68
+ body_JSON = JSON.dump({
69
+ "scopes" => [
70
+ "repo",
71
+ "user"
72
+ ],
73
+ "note" => GITHUB_API_NOTE
74
+ })
75
+
76
+ req_options = {
77
+ use_ssl: uri.scheme == "https",
78
+ }
79
+
80
+ request = Net::HTTP::Post.new(uri)
81
+ request["X-Github-Otp"] = otp
82
+ request.basic_auth username, password
83
+ request.body = body_JSON
84
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
85
+ http.request(request)
86
+ end
87
+ body = parse_http_response(response)
88
+ return body['token']
89
+ end
90
+
91
+ # Fetches commits between two branches.
92
+ def get_commits_between_two_branches(repo_owner, repo, branch_1, branch_2, access_token)
93
+ url = "https://api.github.com/repos/#{repo_owner}/#{repo}/compare/#{branch_1}...#{branch_2}"
94
+ response = get(url, access_token)
95
+ body = parse_http_response(response)
96
+ return body['commits']
97
+ end
98
+
99
+ # Fetches pull request and returns text content of pull request description.
100
+ def get_pull_request(pull_request_number, repo_owner, repo, access_token)
101
+ url = "https://api.github.com/repos/#{repo_owner}/#{repo}/pulls/#{pull_request_number}"
102
+ response = get(url, access_token)
103
+ pull_request_body = parse_http_response(response)['body']
104
+ return pull_request_body
105
+ end
106
+
107
+ # Extracts text from a Github pull request given regex.
108
+ def extract_text_from_pull_request_number(regex, pull_request_body)
109
+ match_results = pull_request_body.scan(regex)
110
+ if !match_results.nil? && match_results.length > 0
111
+ text = match_results[0][0]
112
+ puts text
113
+ return text.gsub!(/^[\r\n]*/, '').gsub!(/[\r\n\-]*$/, '')
114
+ else
115
+ return ""
116
+ end
117
+ end
118
+
119
+ # Exports commits with pull request information to CSV.
120
+ # pull_request_regex_by_field: optional hash from CSV field name to pull request description regex.
121
+ def export_commits_to_CSV(commits, repo_owner, repo, access_token, pull_request_regex_by_field, export_CSV_path)
122
+ pull_request_fields = pull_request_regex_by_field.nil? ? [] : pull_request_regex_by_field.keys
123
+ CSV.open(export_CSV_path, "wb") do |csv|
124
+ fields = ["Author", "Date", "Commit message"] + pull_request_fields + ["Pull request url", "Commit url", "SHA"]
125
+ csv << fields
126
+ commits.each do |commit|
127
+ author = commit['commit']['author']['name']
128
+ date = commit['commit']['author']['date']
129
+ commit_message = commit['commit']['message'].split("\n").first
130
+ commit_url = commit['html_url']
131
+ sha = commit['sha']
132
+
133
+ values = [author, date, commit_message]
134
+
135
+ # Extracts pull request number.
136
+ match_results = commit_message.scan(/\(#([0-9]+)\)\n?$/)
137
+ if !match_results.nil? && match_results.length > 0
138
+ pull_request_number = match_results[0][0]
139
+ pull_request_url = "https://github.com/#{repo_owner}/#{repo}/pull/#{pull_request_number}"
140
+
141
+ pull_request_body = get_pull_request(pull_request_number, repo_owner, repo, access_token)
142
+ puts pull_request_body
143
+
144
+ pull_request_fields.each do |field|
145
+ pull_request_regex = pull_request_regex_by_field[field]
146
+ regex = Regexp.new(pull_request_regex, Regexp::MULTILINE)
147
+ text = extract_text_from_pull_request_number(regex, pull_request_body)
148
+ values = values + [text]
149
+ end
150
+ # Pull request url.
151
+ values = values + [pull_request_url]
152
+ else
153
+ # No pull request can be deduced from commit message.
154
+ pull_request_fields.each do |_|
155
+ values = values + ['n/a']
156
+ end
157
+ # Pull request url.
158
+ values = values + ['n/a']
159
+ end
160
+ values = values + [commit_url, sha]
161
+ csv << values
162
+ end
163
+ end
164
+ end
165
+
166
+ # Parses http response as JSON and returns body field.
167
+ def parse_http_response(response)
168
+ puts JSON.parse(response.body)
169
+ raise "Network error: #{response.code}" unless response.code == "200"
170
+ body = JSON.parse(response.body)
171
+ return body
172
+ end
173
+
174
+ # Prompts for user input.
175
+ def prompt(*args)
176
+ print(*args)
177
+ gets.chomp
178
+ end
179
+
180
+ # Prompts for user input on sensitive information (e.g. password, access token).
181
+ def prompt_sensitive_info(*args)
182
+ print(*args)
183
+ STDIN.noecho(&:gets).chomp
184
+ end
185
+
186
+ # Reads value from environment variable given key.
187
+ def get_token_from_environment_variable(key)
188
+ return ENV[key]
189
+ end
190
+
191
+ # /// Beginning of script. ///
192
+ def main
193
+ options = {}
194
+ OptionParser.new do |opt|
195
+ opt.on('--api_token TOKEN') { |o| options[:access_token] = o }
196
+ opt.on('--repo_owner REPO_OWNER') { |o| options[:repo_owner] = o }
197
+ opt.on('--repo REPO') { |o| options[:repo] = o }
198
+ opt.on('--export_CSV_path EXPORT_CSV_PATH') { |o| options[:export_CSV_path] = o }
199
+ opt.on('--branch_1 BRANCH_1') { |o| options[:branch_1] = o }
200
+ opt.on('--branch_2 BRANCH_2') { |o| options[:branch_2] = o }
201
+ opt.on('--pull_request_regex_by_field PR_REGEX_BY_FIELD') { |o| options[:pull_request_regex_by_field] = o }
202
+ end.parse!
203
+
204
+ # Reads from command options.
205
+ access_token = options[:access_token]
206
+ repo_owner = options[:repo_owner]
207
+ repo = options[:repo]
208
+ export_CSV_path = options[:export_CSV_path]
209
+ branch_1 = options[:branch_1]
210
+ branch_2 = options[:branch_2]
211
+ if !options[:pull_request_regex_by_field].nil?
212
+ pull_request_regex_by_field = JSON.parse(options[:pull_request_regex_by_field])
213
+ end
214
+
215
+ # If user did not provide access token via command options, try a few things in order:
216
+ # 1) try reading from environment variable if user chose to set it before.
217
+ # 2) user probably calls script for the first time. In this case, start asking for basic
218
+ # auth (username, password), and requesting One-Time Password (OTP) for 2-factor auth.
219
+ # After a GitHub API token is generated, prompt user to save token safely:
220
+ # - save to 1Password and provide token via command options `--api_token=#{token}`
221
+ # - provide instruction to save token to local environment variable to `COMMIT_PARSER_API_TOKEN_ENV_KEY`
222
+
223
+ # 1) Tries reading environment variable.
224
+ if access_token.nil? || access_token.empty?
225
+ access_token = get_token_from_environment_variable(COMMIT_PARSER_API_TOKEN_ENV_KEY)
226
+ end
227
+
228
+ # 2) Starts requesting for API token.
229
+ if access_token.nil? || access_token.empty?
230
+ username = prompt "Your GitHub username: "
231
+ password = prompt_sensitive_info "Your GitHub password: "
232
+ print "\n"
233
+ otp_requested = request_2factor_passcode(username, password)
234
+ if otp_requested
235
+ one_time_passcode = prompt "Your GitHub One-Time Password: "
236
+ access_token = get_token_with_2factor_otp(username, password, one_time_passcode)
237
+ if !(access_token.nil? || access_token.empty?)
238
+ puts "🔑 Token fetched! Your token is: #{access_token}"
239
+ end
240
+ if access_token.nil? || access_token.empty?
241
+ puts "Please check for access token for entry with '#{GITHUB_API_NOTE}' at https://github.com/settings/tokens and regenerate access token if already exists."
242
+ access_token = prompt_sensitive_info "Your personal token for GithubCommitParser: "
243
+ end
244
+ if !(access_token.nil? || access_token.empty?)
245
+ puts "This is like a password and please save it safely like in 1Password for future access to Github API."
246
+ puts "Next time running this, you can provide this token via --api_token option, or you can save it to your environment variable via command line by running:"
247
+ puts "export COMMIT_PARSER_API_TOKEN=#{access_token}"
248
+ puts "(If using zsh, add this export to the zshrc file.)"
249
+ end
250
+ end
251
+ end
252
+
253
+ if access_token.nil? || access_token.empty?
254
+ abort("Sorry, we need a token to proceed. Please try again.")
255
+ end
256
+
257
+ # Asks user for export path, repo owner, repo, and branches info if not provided in options.
258
+ if repo_owner.nil? || repo_owner.empty?
259
+ repo_owner = prompt "Repo owner (repository url is 'repo_owner/repo'): "
260
+ end
261
+ if repo.nil? || repo.empty?
262
+ repo = prompt "Repo (repository url is 'repo_owner/repo'): "
263
+ end
264
+ if export_CSV_path.nil? || export_CSV_path.empty?
265
+ export_CSV_path = prompt "Path to export CSV (e.g. ~/Desktop): "
266
+ end
267
+ if branch_1.nil? || branch_1.empty?
268
+ branch_1 = prompt "From branch: "
269
+ end
270
+ if branch_2.nil? || branch_2.empty?
271
+ branch_2 = prompt "To branch: "
272
+ end
273
+
274
+ commits = get_commits_between_two_branches(repo_owner, repo, branch_1, branch_2, access_token)
275
+ export_commits_to_CSV(commits, repo_owner, repo, access_token, pull_request_regex_by_field, export_CSV_path)
276
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: github-csv-changelog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jaclyn Chen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Most useful for squash merge commits on Github - this gem links pull
14
+ request from each commit between two branches, and extracts information from pull
15
+ request.
16
+ email: jaclyn.y.chen+ruby@gmail.com
17
+ executables:
18
+ - github-csv-changelog
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - bin/github-csv-changelog
23
+ - lib/github-csv-changelog.rb
24
+ homepage: https://github.com/jaclync/github-csv-changelog
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.6.10
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Parses Github commits and exports to CSV.
48
+ test_files: []
49
+ has_rdoc: