githuh 0.2.0 → 0.2.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 +4 -4
- data/.envrc +2 -0
- data/.github/workflows/ruby.yml +2 -1
- data/.gitignore +1 -0
- data/.relaxed-rubocop-2.4.yml +176 -0
- data/.rubocop.yml +44 -0
- data/.rubocop_todo.yml +115 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +50 -15
- data/README.adoc +165 -0
- data/docs/img/coverage.svg +2 -2
- data/docs/img/githuh-issue-export.png +0 -0
- data/exe/githuh +1 -0
- data/githuh.gemspec +4 -1
- data/issues.json +3168 -0
- data/lib/githuh.rb +2 -1
- data/lib/githuh/cli/commands/base.rb +27 -6
- data/lib/githuh/cli/commands/issue/export.rb +192 -0
- data/lib/githuh/cli/commands/issue/export_paginated.rb +42 -0
- data/lib/githuh/cli/commands/repo/list.rb +18 -25
- data/lib/githuh/cli/launcher.rb +23 -9
- data/lib/githuh/version.rb +1 -1
- metadata +56 -7
- data/README.md +0 -103
data/lib/githuh.rb
CHANGED
@@ -84,6 +84,7 @@ module Githuh
|
|
84
84
|
end
|
85
85
|
|
86
86
|
require 'githuh/cli/commands/base'
|
87
|
-
require 'githuh/cli/commands/user/info'
|
88
87
|
require 'githuh/cli/commands/version'
|
88
|
+
require 'githuh/cli/commands/user/info'
|
89
89
|
require 'githuh/cli/commands/repo/list'
|
90
|
+
require 'githuh/cli/commands/issue/export'
|
@@ -9,6 +9,7 @@ module Githuh
|
|
9
9
|
module CLI
|
10
10
|
module Commands
|
11
11
|
DEFAULT_PAGE_SIZE = 20
|
12
|
+
DEFAULT_TITLE = 'Operation Progress'
|
12
13
|
|
13
14
|
class Base < Dry::CLI::Command
|
14
15
|
extend Forwardable
|
@@ -38,7 +39,7 @@ module Githuh
|
|
38
39
|
self.verbose = verbose
|
39
40
|
self.info = info
|
40
41
|
self.token = api_token || token_from_gitconfig
|
41
|
-
self.per_page = per_page || DEFAULT_PAGE_SIZE
|
42
|
+
self.per_page = per_page.to_i || DEFAULT_PAGE_SIZE
|
42
43
|
self.client = Octokit::Client.new(access_token: token)
|
43
44
|
|
44
45
|
print_userinfo if info
|
@@ -62,6 +63,26 @@ module Githuh
|
|
62
63
|
80
|
63
64
|
end
|
64
65
|
|
66
|
+
def bar(title = DEFAULT_TITLE)
|
67
|
+
@bar ||= create_progress_bar(title: title)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Overwrite me
|
71
|
+
def bar_size
|
72
|
+
0
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_progress_bar(size = bar_size, title: DEFAULT_TITLE)
|
76
|
+
return unless info || verbose
|
77
|
+
|
78
|
+
TTY::ProgressBar.new("[:bar]",
|
79
|
+
title: title,
|
80
|
+
total: size.to_i,
|
81
|
+
width: ui_width - 2,
|
82
|
+
head: '',
|
83
|
+
complete: '▉'.magenta)
|
84
|
+
end
|
85
|
+
|
65
86
|
private
|
66
87
|
|
67
88
|
def print_userinfo
|
@@ -77,23 +98,23 @@ module Githuh
|
|
77
98
|
lines << sprintf(" Followers: %s", h(user_info.followers.to_s))
|
78
99
|
lines << sprintf(" Member For: %s", h(sprintf("%d years, %d months, %d days", years, months, days)))
|
79
100
|
|
80
|
-
self.box = TTY::Box.frame
|
81
|
-
padding:
|
101
|
+
self.box = TTY::Box.frame(*lines,
|
102
|
+
padding: 0,
|
82
103
|
width: ui_width,
|
83
104
|
align: :left,
|
84
|
-
title: { top_center: Githuh::BANNER },
|
105
|
+
title: { top_center: "┤ #{Githuh::BANNER} ├" },
|
85
106
|
style: {
|
86
107
|
fg: :white,
|
87
108
|
border: {
|
88
109
|
fg: :bright_green
|
89
110
|
}
|
90
|
-
}
|
111
|
+
})
|
91
112
|
|
92
113
|
Githuh.stdout.print box
|
93
114
|
end
|
94
115
|
|
95
116
|
def h(arg)
|
96
|
-
arg.to_s
|
117
|
+
arg.to_s
|
97
118
|
end
|
98
119
|
|
99
120
|
def token_from_gitconfig
|
@@ -0,0 +1,192 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# vim: ft=ruby
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'dry/cli'
|
7
|
+
require 'json'
|
8
|
+
require 'tty/progressbar'
|
9
|
+
require 'csv'
|
10
|
+
require 'active_support/inflector'
|
11
|
+
|
12
|
+
require_relative '../base'
|
13
|
+
|
14
|
+
module Githuh
|
15
|
+
module CLI
|
16
|
+
module Commands
|
17
|
+
module Issue
|
18
|
+
class Export < Base
|
19
|
+
FORMATS = {
|
20
|
+
json: 'json',
|
21
|
+
csv: 'csv'
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
DEFAULT_FORMAT = :csv
|
25
|
+
DEFAULT_OUTPUT_FORMAT = "<username>.<repo>.issues.<format>"
|
26
|
+
|
27
|
+
attr_accessor :filename, :file, :output, :repo, :issues, :format, :record_count
|
28
|
+
|
29
|
+
desc "Export Repo issues into a CSV or JSON format\n" \
|
30
|
+
" Default output file is " + DEFAULT_OUTPUT_FORMAT.bold.yellow
|
31
|
+
|
32
|
+
argument :repo, type: :string, required: true, desc: 'Name of the repo, eg "rails/rails"'
|
33
|
+
option :file, required: false, desc: 'Output file, overrides ' + DEFAULT_OUTPUT_FORMAT
|
34
|
+
option :format, values: FORMATS.keys.map(&:to_s), default: DEFAULT_FORMAT.to_s, required: false, desc: 'Output format'
|
35
|
+
|
36
|
+
def call(repo: nil, file: nil, format: nil, **opts)
|
37
|
+
super(**opts)
|
38
|
+
|
39
|
+
self.record_count = 0
|
40
|
+
self.repo = repo
|
41
|
+
|
42
|
+
raise ArgumentError, "argument <repo> is required" unless repo
|
43
|
+
raise ArgumentError, "argument <repo> is not a repository, expected eg 'rails/rails'" unless repo =~ %r{/}
|
44
|
+
|
45
|
+
self.issues = []
|
46
|
+
self.output = StringIO.new
|
47
|
+
self.format = (format || DEFAULT_FORMAT).to_sym
|
48
|
+
|
49
|
+
self.filename = file || file_name(repo)
|
50
|
+
self.file = File.open(filename, 'w')
|
51
|
+
|
52
|
+
print_summary
|
53
|
+
|
54
|
+
# —————————— actually get all issues ———————————————
|
55
|
+
self.file.write send("render_as_#{format}", fetch_issues)
|
56
|
+
# ————————————————————————————————————————————————————————
|
57
|
+
|
58
|
+
print_conclusion
|
59
|
+
ensure
|
60
|
+
file.close if file.respond_to?(:close) && !file.closed?
|
61
|
+
end
|
62
|
+
|
63
|
+
def fetch_issues
|
64
|
+
client.auto_paginate = true
|
65
|
+
self.issues = filter_issues(client.issues(repo, query: default_options)).tap do |issue_list|
|
66
|
+
self.record_count = issue_list.size
|
67
|
+
bar('Issues')&.advance
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def filter_issues(issues_list)
|
72
|
+
issues_list.reject do |issue|
|
73
|
+
issue.html_url =~ /pull/
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def bar_size
|
78
|
+
record_count + 1
|
79
|
+
end
|
80
|
+
|
81
|
+
def default_options
|
82
|
+
{ state: 'open' }
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.issue_labels(issue)
|
86
|
+
issue.labels.map(&:name)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.find_user(client, username)
|
90
|
+
@user_cache ||= {}
|
91
|
+
@user_cache[username] ||= client.user(username).name
|
92
|
+
end
|
93
|
+
|
94
|
+
CSV_MAP = {
|
95
|
+
'Labels' => ->(_client, issue) { issue_labels(issue).reject { |l| LABEL_ESTIMATES.key?(l) }.join(',').downcase },
|
96
|
+
'Type' => ->(*) { 'feature' },
|
97
|
+
'Estimate' => ->(_client, issue) do
|
98
|
+
el = issue_labels(issue).find { |l| LABEL_ESTIMATES.key?(l) }
|
99
|
+
el ? LABEL_ESTIMATES[el] : nil
|
100
|
+
end,
|
101
|
+
'Current State' => ->(*) { 'unstarted' },
|
102
|
+
'Requested By' => ->(client, issue) do
|
103
|
+
find_user(client, issue.user.login)
|
104
|
+
end,
|
105
|
+
'Owned By' => ->(client, issue) do
|
106
|
+
find_user(client, issue.user.login)
|
107
|
+
end,
|
108
|
+
'Description' => ->(_client, issue) {
|
109
|
+
issue.body
|
110
|
+
},
|
111
|
+
'Created at' => ->(_client, issue) { issue.created_at },
|
112
|
+
}.freeze
|
113
|
+
|
114
|
+
LABEL_ESTIMATES = {
|
115
|
+
'XXL(13 eng day)' => 15,
|
116
|
+
'XL(8 eng day)' => 15,
|
117
|
+
'xs(<=1 eng day)' => 3,
|
118
|
+
'L(5 eng day)' => 15,
|
119
|
+
'm(3 eng day)' => 9,
|
120
|
+
's(2 eng day)' => 6
|
121
|
+
}.freeze
|
122
|
+
|
123
|
+
CSV_HEADER = %w(Id Title Labels Type Estimate) +
|
124
|
+
['Current State', 'Created at', 'Accepted at', 'Deadline', 'Requested By',
|
125
|
+
'Owned By', 'Description', 'Comment', 'Comment', 'Comment', 'Comment'].freeze
|
126
|
+
|
127
|
+
# Id,Title,Labels,Type,Estimate,Current State,Created at,Accepted at,Deadline,Requested By,Owned By,Description,Comment,Comment
|
128
|
+
# 100, existing started story,"label one,label two",feature,1,started,"Nov 22, 2007",,,user1,user2,this will update story 100,,
|
129
|
+
# ,new story,label one,feature,-1,unscheduled,,,,user1,,this will create a new story in the icebox,comment1,comment2
|
130
|
+
def render_as_csv(issue_list)
|
131
|
+
# puts "rendering issues as CVS:"
|
132
|
+
# pp issue_list
|
133
|
+
::CSV.generate do |csv|
|
134
|
+
csv << CSV_HEADER
|
135
|
+
issue_list.each do |issue|
|
136
|
+
row = []
|
137
|
+
CSV_HEADER.each do |column|
|
138
|
+
method = column.downcase.underscore.to_sym
|
139
|
+
value = if CSV_MAP[column]
|
140
|
+
CSV_MAP[column][client, issue]
|
141
|
+
else
|
142
|
+
begin
|
143
|
+
issue.to_h[method]
|
144
|
+
rescue StandardError
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
value = value.strip if value.is_a?(String)
|
149
|
+
row << value
|
150
|
+
end
|
151
|
+
csv << row
|
152
|
+
bar&.advance
|
153
|
+
end
|
154
|
+
bar.finish
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def render_as_json(issue_list)
|
159
|
+
JSON.pretty_generate(issue_list.map(&:to_h))
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
|
164
|
+
def print_conclusion
|
165
|
+
puts
|
166
|
+
puts TTY::Box.info("Success: written a total of #{record_count} records to #{filename}",
|
167
|
+
width: ui_width, padding: 1)
|
168
|
+
puts
|
169
|
+
end
|
170
|
+
|
171
|
+
def print_summary
|
172
|
+
puts
|
173
|
+
puts TTY::Box.info("Format : #{self.format}\n" \
|
174
|
+
"File : #{filename}\n" \
|
175
|
+
"Repo : #{repo}\n",
|
176
|
+
width: ui_width,
|
177
|
+
padding: 1)
|
178
|
+
puts
|
179
|
+
end
|
180
|
+
|
181
|
+
def file_name(repo)
|
182
|
+
"#{repo.gsub(%r{/}, '.')}.issues.#{FORMATS[self.format.to_sym]}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
register 'issue', aliases: ['r'] do |prefix|
|
188
|
+
prefix.register 'export', Issue::Export
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'export'
|
4
|
+
|
5
|
+
module Githuh
|
6
|
+
module CLI
|
7
|
+
module Commands
|
8
|
+
module Issue
|
9
|
+
class ExportPaginated < Export
|
10
|
+
def fetch_issues
|
11
|
+
page = 0
|
12
|
+
bar = nil
|
13
|
+
|
14
|
+
[].tap do |issue_list|
|
15
|
+
loop do
|
16
|
+
options = default_options.merge({
|
17
|
+
page: page,
|
18
|
+
per_page: per_page,
|
19
|
+
})
|
20
|
+
|
21
|
+
puts "page: #{page}"
|
22
|
+
issues_page = client.issues(repo, **options)
|
23
|
+
|
24
|
+
break if issues_page.nil? || issues_page.empty?
|
25
|
+
|
26
|
+
issue_list.concat(issues_page)
|
27
|
+
|
28
|
+
bar("#{repo} Issues Export")&.advance
|
29
|
+
page += 1
|
30
|
+
self.record_count += issues_page.size
|
31
|
+
end
|
32
|
+
|
33
|
+
bar&.finish; puts
|
34
|
+
|
35
|
+
issue_list << issues
|
36
|
+
end.flatten
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -67,7 +67,7 @@ module Githuh
|
|
67
67
|
|
68
68
|
def repositories
|
69
69
|
page = 0
|
70
|
-
bar
|
70
|
+
bar = nil
|
71
71
|
|
72
72
|
[].tap do |repo_list|
|
73
73
|
loop do
|
@@ -78,9 +78,8 @@ module Githuh
|
|
78
78
|
}
|
79
79
|
|
80
80
|
result = client.repos({}, query: options)
|
81
|
-
bar
|
81
|
+
bar('Repositories')&.advance
|
82
82
|
|
83
|
-
bar&.advance
|
84
83
|
filter_result!(result)
|
85
84
|
|
86
85
|
break if result.empty?
|
@@ -98,14 +97,8 @@ module Githuh
|
|
98
97
|
end.flatten.sort_by(&:stargazers_count).reverse.uniq(&:name)
|
99
98
|
end
|
100
99
|
|
101
|
-
def
|
102
|
-
|
103
|
-
TTY::ProgressBar.new("[:bar]",
|
104
|
-
title: 'Fetching Repositories',
|
105
|
-
total: number_of_pages.to_i,
|
106
|
-
width: ui_width - 2,
|
107
|
-
head: '',
|
108
|
-
complete: '▉'.magenta)
|
100
|
+
def bar_size
|
101
|
+
client.last_response.rels[:last].href.match(/page=(\d+).*$/)[1].to_i
|
109
102
|
end
|
110
103
|
|
111
104
|
def render_as_markdown(repositories)
|
@@ -138,22 +131,22 @@ module Githuh
|
|
138
131
|
def filter_result!(result)
|
139
132
|
result.reject! do |r|
|
140
133
|
fork_reject = case forks
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
134
|
+
when 'exclude'
|
135
|
+
r.fork
|
136
|
+
when 'only'
|
137
|
+
!r.fork
|
138
|
+
when 'include'
|
139
|
+
false
|
140
|
+
end
|
148
141
|
|
149
142
|
private_reject = case private
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
143
|
+
when true
|
144
|
+
!r.private
|
145
|
+
when false
|
146
|
+
r.private
|
147
|
+
when nil
|
148
|
+
false
|
149
|
+
end
|
157
150
|
|
158
151
|
fork_reject || private_reject
|
159
152
|
end
|
data/lib/githuh/cli/launcher.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'dry/cli'
|
4
4
|
require 'forwardable'
|
5
5
|
require 'tty/box'
|
6
|
+
require 'tty/screen'
|
6
7
|
|
7
8
|
require 'githuh'
|
8
9
|
module Githuh
|
@@ -34,14 +35,27 @@ module Githuh
|
|
34
35
|
|
35
36
|
self.command = ::Dry::CLI.new(::Githuh::CLI::Commands)
|
36
37
|
command.call(arguments: argv, out: stdout, err: stderr)
|
37
|
-
|
38
38
|
rescue StandardError => e
|
39
|
-
|
40
|
-
|
39
|
+
lines = [e.message.gsub(/\n/, ', ')]
|
40
|
+
if e.backtrace
|
41
|
+
lines << ''
|
42
|
+
lines.concat(e.backtrace)
|
43
|
+
end
|
41
44
|
|
45
|
+
box = TTY::Box.frame(*lines,
|
46
|
+
**BOX_OPTIONS.merge(
|
47
|
+
width: TTY::Screen.width,
|
48
|
+
title: { top_center: "┤ #{e.class.name} ├" },
|
49
|
+
))
|
50
|
+
stderr.puts
|
51
|
+
stderr.print box
|
42
52
|
ensure
|
43
53
|
Githuh.restore_kernel_behavior!
|
44
|
-
exit(
|
54
|
+
exit(0) unless Githuh.in_test
|
55
|
+
end
|
56
|
+
|
57
|
+
def trace?
|
58
|
+
argv.include?('-t') || argv.include?('--trace')
|
45
59
|
end
|
46
60
|
end
|
47
61
|
|
@@ -55,16 +69,16 @@ module Githuh
|
|
55
69
|
BOX_OPTIONS = {
|
56
70
|
padding: 1,
|
57
71
|
align: :left,
|
58
|
-
title: { top_center: Githuh::BANNER },
|
72
|
+
title: { top_center: "┤ #{Githuh::BANNER} ├" },
|
59
73
|
width: 80,
|
60
74
|
style: {
|
61
|
-
bg: :
|
75
|
+
bg: :yellow,
|
76
|
+
fg: :black,
|
62
77
|
border: {
|
63
|
-
fg: :
|
64
|
-
bg: :
|
78
|
+
fg: :red,
|
79
|
+
bg: :yellow
|
65
80
|
}
|
66
81
|
}
|
67
82
|
}.freeze
|
68
|
-
|
69
83
|
end
|
70
84
|
end
|