github-csv-changelog 0.0.1

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