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.
@@ -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 *lines,
81
- padding: 1,
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.bold.blue
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 = nil
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 = create_progress_bar if info && !verbose && page == 0
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 create_progress_bar
102
- number_of_pages = client.last_response.rels[:last].href.match(/page=(\d+).*$/)[1]
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
- when 'exclude'
142
- r.fork
143
- when 'only'
144
- !r.fork
145
- when 'include'
146
- false
147
- end
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
- when true
151
- !r.private
152
- when false
153
- r.private
154
- when nil
155
- false
156
- end
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
@@ -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
- box = TTY::Box.frame('ERROR:', ' ', e.message, **BOX_OPTIONS)
40
- stderr.print box
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(10) unless Githuh.in_test
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: :red,
75
+ bg: :yellow,
76
+ fg: :black,
62
77
  border: {
63
- fg: :bright_yellow,
64
- bg: :red
78
+ fg: :red,
79
+ bg: :yellow
65
80
  }
66
81
  }
67
82
  }.freeze
68
-
69
83
  end
70
84
  end