ghi 0.2.0 → 1.2.0
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/ghi +2 -4
- data/lib/ghi/authorization.rb +142 -0
- data/lib/ghi/client.rb +148 -0
- data/lib/ghi/commands/assign.rb +54 -0
- data/lib/ghi/commands/close.rb +51 -0
- data/lib/ghi/commands/command.rb +133 -0
- data/lib/ghi/commands/comment.rb +168 -0
- data/lib/ghi/commands/config.rb +57 -0
- data/lib/ghi/commands/disable.rb +35 -0
- data/lib/ghi/commands/edit.rb +139 -0
- data/lib/ghi/commands/enable.rb +36 -0
- data/lib/ghi/commands/help.rb +65 -0
- data/lib/ghi/commands/label.rb +196 -0
- data/lib/ghi/commands/list.rb +204 -0
- data/lib/ghi/commands/milestone.rb +206 -0
- data/lib/ghi/commands/open.rb +102 -0
- data/lib/ghi/commands/show.rb +65 -0
- data/lib/ghi/commands/status.rb +32 -0
- data/lib/ghi/commands/version.rb +16 -0
- data/lib/ghi/commands.rb +23 -0
- data/lib/ghi/editor.rb +48 -0
- data/lib/ghi/formatting/colors.rb +364 -0
- data/lib/ghi/formatting.rb +523 -0
- data/lib/ghi/web.rb +37 -0
- data/lib/ghi.rb +151 -39
- metadata +95 -53
- data/History.rdoc +0 -180
- data/Manifest.txt +0 -14
- data/README.rdoc +0 -92
- data/lib/ghi/api.rb +0 -106
- data/lib/ghi/cli.rb +0 -608
- data/lib/ghi/issue.rb +0 -29
- data/spec/ghi/api_spec.rb +0 -194
- data/spec/ghi/cli_spec.rb +0 -258
- data/spec/ghi/issue_spec.rb +0 -26
- data/spec/ghi_spec.rb +0 -91
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: cf60dfd0d186793f1f58e67aaf6e02c15099eb6a
|
|
4
|
+
data.tar.gz: f159c9cb960dc248c8dcdfef7efe827089257275
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0b0b760af55aec8b781b40932dd08d65122d2c2db8b5a73aabf83dbe82273c80a8c3a8fed1fdcbe24458f682dfa6d67ed55f08024de060d0176902117cf12c0b
|
|
7
|
+
data.tar.gz: 62776a3dfc2adbac46b59eff82181027e57355dd38ff9d79c6a7a732334855a415d61bc9898ebfec187b68f320f7f5525108e1e01ea770204c601439b9624864
|
data/bin/ghi
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
require 'socket'
|
|
3
|
+
|
|
4
|
+
module GHI
|
|
5
|
+
module Authorization
|
|
6
|
+
extend Formatting
|
|
7
|
+
|
|
8
|
+
class Required < RuntimeError
|
|
9
|
+
def message() 'Authorization required.' end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def token
|
|
14
|
+
return @token if defined? @token
|
|
15
|
+
@token = GHI.config 'ghi.token'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authorize! user = username, pass = password, local = true
|
|
19
|
+
return false unless user && pass
|
|
20
|
+
code ||= nil # 2fa
|
|
21
|
+
args = code ? [] : [54, "✔\r"]
|
|
22
|
+
note = %w[ghi]
|
|
23
|
+
note << "(#{GHI.repo})" if local
|
|
24
|
+
note << "on #{Socket.gethostname}"
|
|
25
|
+
res = throb(*args) {
|
|
26
|
+
headers = {}
|
|
27
|
+
headers['X-GitHub-OTP'] = code if code
|
|
28
|
+
body = {
|
|
29
|
+
:scopes => %w(public_repo repo),
|
|
30
|
+
:note => note.join(' '),
|
|
31
|
+
:note_url => 'https://github.com/stephencelis/ghi'
|
|
32
|
+
}
|
|
33
|
+
Client.new(user, pass).post(
|
|
34
|
+
'/authorizations', body, :headers => headers
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
@token = res.body['token']
|
|
38
|
+
|
|
39
|
+
unless username
|
|
40
|
+
system "git config#{' --global' unless local} github.user #{user}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
store_token! user, token, local
|
|
44
|
+
rescue Client::Error => e
|
|
45
|
+
if e.response['X-GitHub-OTP'] =~ /required/
|
|
46
|
+
puts "Bad code." if code
|
|
47
|
+
print "Two-factor authentication code: "
|
|
48
|
+
trap('INT') { abort }
|
|
49
|
+
code = gets
|
|
50
|
+
code = '' and puts "\n" unless code
|
|
51
|
+
retry
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if e.errors.any? { |err| err['code'] == 'already_exists' }
|
|
55
|
+
host = GHI.config('github.host') || 'github.com'
|
|
56
|
+
message = <<EOF.chomp
|
|
57
|
+
A ghi token already exists!
|
|
58
|
+
|
|
59
|
+
Please revoke all previously-generated ghi personal access tokens here:
|
|
60
|
+
|
|
61
|
+
https://#{host}/settings/tokens
|
|
62
|
+
EOF
|
|
63
|
+
else
|
|
64
|
+
message = e.message
|
|
65
|
+
end
|
|
66
|
+
abort "#{message}#{CURSOR[:column][0]}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def username
|
|
70
|
+
return @username if defined? @username
|
|
71
|
+
@username = GHI.config 'github.user'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def password
|
|
75
|
+
return @password if defined? @password
|
|
76
|
+
@password = GHI.config 'github.password'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def store_token! username, token, local
|
|
82
|
+
if security
|
|
83
|
+
run = []
|
|
84
|
+
|
|
85
|
+
run << security('delete', username)
|
|
86
|
+
run << security('add', username, token)
|
|
87
|
+
|
|
88
|
+
find = security 'find', username, false
|
|
89
|
+
run << %(git config#{' --global' unless local} ghi.token "!#{find}")
|
|
90
|
+
|
|
91
|
+
system run.join ' ; '
|
|
92
|
+
|
|
93
|
+
puts "✔︎ Token saved to keychain."
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
command = "git config#{' --global' unless local} ghi.token #{token}"
|
|
98
|
+
system command
|
|
99
|
+
|
|
100
|
+
unless local
|
|
101
|
+
at_exit do
|
|
102
|
+
warn <<EOF
|
|
103
|
+
Your ~/.gitconfig has been modified by way of:
|
|
104
|
+
|
|
105
|
+
#{command}
|
|
106
|
+
|
|
107
|
+
#{bright { blink { 'Do not check this change into public source control!' } }}
|
|
108
|
+
|
|
109
|
+
You can increase security by storing the token in a secure place that can be
|
|
110
|
+
fetched from the command line. E.g., on OS X:
|
|
111
|
+
|
|
112
|
+
git config --global ghi.token \\
|
|
113
|
+
"!security -a #{username} -s github.com -l 'ghi token' -w"
|
|
114
|
+
|
|
115
|
+
Alternatively, set the following env var in a private dotfile:
|
|
116
|
+
|
|
117
|
+
export GHI_TOKEN="#{token}"
|
|
118
|
+
EOF
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def security command = nil, username = nil, password = nil
|
|
124
|
+
if command.nil? && username.nil? && password.nil?
|
|
125
|
+
return system 'which security >/dev/null'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
run = [
|
|
129
|
+
'security',
|
|
130
|
+
"#{command}-internet-password",
|
|
131
|
+
"-a #{username}",
|
|
132
|
+
'-s github.com',
|
|
133
|
+
"-l 'ghi token'"
|
|
134
|
+
]
|
|
135
|
+
run << %(-w#{" #{password}" if password}) unless password.nil?
|
|
136
|
+
run << '>/dev/null 2>&1' unless command == 'find'
|
|
137
|
+
|
|
138
|
+
run.join ' '
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/ghi/client.rb
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require 'net/https'
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
unless defined? Net::HTTP::Patch
|
|
6
|
+
# PATCH support for 1.8.7.
|
|
7
|
+
Net::HTTP::Patch = Class.new(Net::HTTP::Post) { METHOD = 'PATCH' }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module GHI
|
|
11
|
+
class Client
|
|
12
|
+
|
|
13
|
+
class Error < RuntimeError
|
|
14
|
+
attr_reader :response
|
|
15
|
+
def initialize response
|
|
16
|
+
@response, @json = response, JSON.parse(response.body)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def body() @json end
|
|
20
|
+
def message() body['message'] end
|
|
21
|
+
def errors() [*body['errors']] end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Response
|
|
25
|
+
def initialize response
|
|
26
|
+
@response = response
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def body
|
|
30
|
+
@body ||= JSON.parse @response.body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def next_page() links['next'] end
|
|
34
|
+
def last_page() links['last'] end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def links
|
|
39
|
+
return @links if defined? @links
|
|
40
|
+
@links = {}
|
|
41
|
+
if links = @response['Link']
|
|
42
|
+
links.scan(/<([^>]+)>; rel="([^"]+)"/).each { |l, r| @links[r] = l }
|
|
43
|
+
end
|
|
44
|
+
@links
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
CONTENT_TYPE = 'application/vnd.github.beta+json'
|
|
49
|
+
USER_AGENT = 'ghi/%s (%s; +%s)' % [
|
|
50
|
+
GHI::Commands::Version::VERSION,
|
|
51
|
+
RUBY_DESCRIPTION,
|
|
52
|
+
'https://github.com/stephencelis/ghi'
|
|
53
|
+
]
|
|
54
|
+
METHODS = {
|
|
55
|
+
:head => Net::HTTP::Head,
|
|
56
|
+
:get => Net::HTTP::Get,
|
|
57
|
+
:post => Net::HTTP::Post,
|
|
58
|
+
:put => Net::HTTP::Put,
|
|
59
|
+
:patch => Net::HTTP::Patch,
|
|
60
|
+
:delete => Net::HTTP::Delete
|
|
61
|
+
}
|
|
62
|
+
DEFAULT_HOST = 'api.github.com'
|
|
63
|
+
HOST = GHI.config('github.host') || DEFAULT_HOST
|
|
64
|
+
PORT = 443
|
|
65
|
+
|
|
66
|
+
attr_reader :username, :password
|
|
67
|
+
def initialize username = nil, password = nil
|
|
68
|
+
@username, @password = username, password
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def head path, options = {}
|
|
72
|
+
request :head, path, options
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def get path, params = {}, options = {}
|
|
76
|
+
request :get, path, options.merge(:params => params)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def post path, body = nil, options = {}
|
|
80
|
+
request :post, path, options.merge(:body => body)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def put path, body = nil, options = {}
|
|
84
|
+
request :put, path, options.merge(:body => body)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def patch path, body = nil, options = {}
|
|
88
|
+
request :patch, path, options.merge(:body => body)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def delete path, options = {}
|
|
92
|
+
request :delete, path, options
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def request method, path, options
|
|
98
|
+
path = "/api/v3#{path}" if HOST != DEFAULT_HOST
|
|
99
|
+
|
|
100
|
+
path = URI.escape path
|
|
101
|
+
if params = options[:params] and !params.empty?
|
|
102
|
+
q = params.map { |k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}" }
|
|
103
|
+
path += "?#{q.join '&'}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
headers = options.fetch :headers, {}
|
|
107
|
+
headers.update 'Accept' => CONTENT_TYPE, 'User-Agent' => USER_AGENT
|
|
108
|
+
req = METHODS[method].new path, headers
|
|
109
|
+
if GHI::Authorization.token
|
|
110
|
+
req['Authorization'] = "token #{GHI::Authorization.token}"
|
|
111
|
+
end
|
|
112
|
+
if options.key? :body
|
|
113
|
+
req['Content-Type'] = CONTENT_TYPE
|
|
114
|
+
req.body = options[:body] ? JSON.dump(options[:body]) : ''
|
|
115
|
+
end
|
|
116
|
+
req.basic_auth username, password if username && password
|
|
117
|
+
|
|
118
|
+
proxy = GHI.config 'https.proxy', :upcase => false
|
|
119
|
+
proxy ||= GHI.config 'http.proxy', :upcase => false
|
|
120
|
+
if proxy
|
|
121
|
+
proxy = URI.parse proxy
|
|
122
|
+
http = Net::HTTP::Proxy(proxy.host, proxy.port).new HOST, PORT
|
|
123
|
+
else
|
|
124
|
+
http = Net::HTTP.new HOST, PORT
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
http.use_ssl = true
|
|
128
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE # FIXME 1.8.7
|
|
129
|
+
|
|
130
|
+
GHI.v? and puts "\r===> #{method.to_s.upcase} #{path} #{req.body}"
|
|
131
|
+
res = http.start { http.request req }
|
|
132
|
+
GHI.v? and puts "\r<=== #{res.code}: #{res.body}"
|
|
133
|
+
|
|
134
|
+
case res
|
|
135
|
+
when Net::HTTPSuccess
|
|
136
|
+
return Response.new(res)
|
|
137
|
+
when Net::HTTPUnauthorized
|
|
138
|
+
if password.nil?
|
|
139
|
+
raise Authorization::Required, 'Authorization required'
|
|
140
|
+
end
|
|
141
|
+
when Net::HTTPMovedPermanently
|
|
142
|
+
return Response.new(http.get(res['location']))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
raise Error, res
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Assign < Command
|
|
4
|
+
def options
|
|
5
|
+
OptionParser.new do |opts|
|
|
6
|
+
opts.banner = <<EOF
|
|
7
|
+
usage: ghi assign [options] [<issueno>]
|
|
8
|
+
or: ghi assign <issueno> <user>
|
|
9
|
+
or: ghi unassign <issueno>
|
|
10
|
+
EOF
|
|
11
|
+
opts.separator ''
|
|
12
|
+
opts.on(
|
|
13
|
+
'-u', '--assignee <user>', 'assign to specified user'
|
|
14
|
+
) do |assignee|
|
|
15
|
+
assigns[:assignee] = assignee
|
|
16
|
+
end
|
|
17
|
+
opts.on '-d', '--no-assignee', 'unassign this issue' do
|
|
18
|
+
assigns[:assignee] = nil
|
|
19
|
+
end
|
|
20
|
+
opts.on '-l', '--list', 'list assigned issues' do
|
|
21
|
+
self.action = 'list'
|
|
22
|
+
end
|
|
23
|
+
opts.separator ''
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute
|
|
28
|
+
self.action = 'edit'
|
|
29
|
+
assigns[:args] = []
|
|
30
|
+
|
|
31
|
+
require_repo
|
|
32
|
+
extract_issue
|
|
33
|
+
options.parse! args
|
|
34
|
+
|
|
35
|
+
unless assigns.key? :assignee
|
|
36
|
+
assigns[:assignee] = args.pop || Authorization.username
|
|
37
|
+
end
|
|
38
|
+
if assigns.key? :assignee
|
|
39
|
+
assigns[:assignee].sub! /^@/, '' if assigns[:assignee]
|
|
40
|
+
assigns[:args].concat(
|
|
41
|
+
assigns[:assignee] ? %W(-u #{assigns[:assignee]}) : %w(--no-assign)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
assigns[:args] << issue if issue
|
|
45
|
+
assigns[:args].concat %W(-- #{repo})
|
|
46
|
+
|
|
47
|
+
case action
|
|
48
|
+
when 'list' then List.execute assigns[:args]
|
|
49
|
+
when 'edit' then Edit.execute assigns[:args]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class Close < Command
|
|
4
|
+
attr_accessor :web
|
|
5
|
+
|
|
6
|
+
def options
|
|
7
|
+
OptionParser.new do |opts|
|
|
8
|
+
opts.banner = <<EOF
|
|
9
|
+
usage: ghi close [options] <issueno>
|
|
10
|
+
EOF
|
|
11
|
+
opts.separator ''
|
|
12
|
+
opts.on '-l', '--list', 'list closed issues' do
|
|
13
|
+
assigns[:command] = List
|
|
14
|
+
end
|
|
15
|
+
opts.on('-w', '--web') { self.web = true }
|
|
16
|
+
opts.separator ''
|
|
17
|
+
opts.separator 'Issue modification options'
|
|
18
|
+
opts.on '-m', '--message [<text>]', 'close with message' do |text|
|
|
19
|
+
assigns[:comment] = text
|
|
20
|
+
end
|
|
21
|
+
opts.separator ''
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute
|
|
26
|
+
options.parse! args
|
|
27
|
+
require_repo
|
|
28
|
+
|
|
29
|
+
if list?
|
|
30
|
+
args.unshift(*%W(-sc -- #{repo}))
|
|
31
|
+
args.unshift '-w' if web
|
|
32
|
+
List.execute args
|
|
33
|
+
else
|
|
34
|
+
require_issue
|
|
35
|
+
if assigns.key? :comment
|
|
36
|
+
Comment.execute [
|
|
37
|
+
issue, '-m', assigns[:comment], '--', repo
|
|
38
|
+
].compact
|
|
39
|
+
end
|
|
40
|
+
Edit.execute %W(-sc #{issue} -- #{repo})
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def list?
|
|
47
|
+
assigns[:command] == List
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
module GHI
|
|
2
|
+
module Commands
|
|
3
|
+
class MissingArgument < RuntimeError
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class Command
|
|
7
|
+
include Formatting
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :detected_repo
|
|
11
|
+
|
|
12
|
+
def execute args
|
|
13
|
+
command = new args
|
|
14
|
+
if i = args.index('--')
|
|
15
|
+
command.repo = args.slice!(i, args.length)[1] # Raise if too many?
|
|
16
|
+
end
|
|
17
|
+
command.execute
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :args
|
|
22
|
+
attr_writer :issue
|
|
23
|
+
attr_accessor :action
|
|
24
|
+
attr_accessor :verbose
|
|
25
|
+
|
|
26
|
+
def initialize args
|
|
27
|
+
@args = args.map! { |a| a.dup }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def assigns
|
|
31
|
+
@assigns ||= {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def api
|
|
35
|
+
@api ||= Client.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def repo
|
|
39
|
+
return @repo if defined? @repo
|
|
40
|
+
@repo = GHI.config('ghi.repo', :flags => '--local') || detect_repo
|
|
41
|
+
if @repo && !@repo.include?('/')
|
|
42
|
+
@repo = [Authorization.username, @repo].join '/'
|
|
43
|
+
end
|
|
44
|
+
@repo
|
|
45
|
+
end
|
|
46
|
+
alias extract_repo repo
|
|
47
|
+
|
|
48
|
+
def repo= repo
|
|
49
|
+
@repo = repo.dup
|
|
50
|
+
unless @repo.include? '/'
|
|
51
|
+
@repo.insert 0, "#{Authorization.username}/"
|
|
52
|
+
end
|
|
53
|
+
@repo
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def require_repo
|
|
59
|
+
return true if repo
|
|
60
|
+
warn 'Not a GitHub repo.'
|
|
61
|
+
warn ''
|
|
62
|
+
abort options.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def require_repo_name
|
|
66
|
+
require_repo
|
|
67
|
+
repo_array = repo.partition "/"
|
|
68
|
+
if repo_array.length >= 2
|
|
69
|
+
repo_name = repo_array[2]
|
|
70
|
+
else
|
|
71
|
+
repo_name = nil
|
|
72
|
+
end
|
|
73
|
+
return repo_name
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def detect_repo
|
|
77
|
+
remote = remotes.find { |r| r[:remote] == 'upstream' }
|
|
78
|
+
remote ||= remotes.find { |r| r[:remote] == 'origin' }
|
|
79
|
+
remote ||= remotes.find { |r| r[:user] == Authorization.username }
|
|
80
|
+
Command.detected_repo = true and remote[:repo] if remote
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remotes
|
|
84
|
+
return @remotes if defined? @remotes
|
|
85
|
+
@remotes = `git config --get-regexp remote\..+\.url`.split "\n"
|
|
86
|
+
github_host = GHI.config('github.host') || 'github.com'
|
|
87
|
+
@remotes.reject! { |r| !r.include? github_host}
|
|
88
|
+
@remotes.map! { |r|
|
|
89
|
+
remote, user, repo = r.scan(
|
|
90
|
+
%r{remote\.([^\.]+)\.url .*?([^:/]+)/([^/\s]+?)(?:\.git)?$}
|
|
91
|
+
).flatten
|
|
92
|
+
{ :remote => remote, :user => user, :repo => "#{user}/#{repo}" }
|
|
93
|
+
}
|
|
94
|
+
@remotes
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def issue
|
|
98
|
+
return @issue if defined? @issue
|
|
99
|
+
if index = args.index { |arg| /^\d+$/ === arg }
|
|
100
|
+
@issue = args.delete_at index
|
|
101
|
+
else
|
|
102
|
+
infer_issue_from_branch_prefix
|
|
103
|
+
end
|
|
104
|
+
@issue
|
|
105
|
+
end
|
|
106
|
+
alias extract_issue issue
|
|
107
|
+
alias milestone issue
|
|
108
|
+
alias extract_milestone issue
|
|
109
|
+
|
|
110
|
+
def infer_issue_from_branch_prefix
|
|
111
|
+
@issue = `git symbolic-ref --short HEAD 2>/dev/null`[/^\d+/];
|
|
112
|
+
warn "(Inferring issue from branch prefix: ##@issue)" if @issue
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def require_issue
|
|
116
|
+
raise MissingArgument, 'Issue required.' unless issue
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def require_milestone
|
|
120
|
+
raise MissingArgument, 'Milestone required.' unless milestone
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Handles, e.g. `--[no-]milestone [<n>]`.
|
|
124
|
+
def any_or_none_or input
|
|
125
|
+
input ? input : { nil => '*', false => 'none' }[input]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def sort_by_creation(arr)
|
|
129
|
+
arr.sort_by { |el| el['created_at'] }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|