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 +7 -0
- data/bin/github-csv-changelog +5 -0
- data/lib/github-csv-changelog.rb +276 -0
- metadata +49 -0
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,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:
|