githuh 0.2.0 → 0.2.1

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