hub 1.10.6 → 1.11.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of hub might be problematic. Click here for more details.
- data/README.md +30 -62
- data/Rakefile +24 -15
- data/bin/bench +37 -0
- data/lib/hub.rb +1 -0
- data/lib/hub/commands.rb +178 -57
- data/lib/hub/context.rb +63 -20
- data/lib/hub/github_api.rb +193 -71
- data/lib/hub/speedy_stdlib.rb +107 -0
- data/lib/hub/ssh_config.rb +1 -1
- data/lib/hub/standalone.rb +31 -3
- data/lib/hub/version.rb +1 -1
- data/man/hub.1 +46 -23
- data/man/hub.1.html +46 -29
- data/man/hub.1.ronn +30 -15
- data/script/cached-bundle +46 -0
- data/script/s3-put +71 -0
- data/script/test +41 -0
- data/script/test_each +9 -0
- data/test/context_test.rb +79 -0
- data/test/fakebin/git +1 -1
- data/test/fakebin/open +2 -2
- data/test/github_api_test.rb +79 -0
- data/test/helper.rb +2 -2
- data/test/hub_test.rb +85 -197
- data/test/standalone_test.rb +6 -2
- metadata +22 -15
- data/HISTORY.md +0 -244
data/lib/hub/context.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'shellwords'
|
2
2
|
require 'forwardable'
|
3
|
-
require '
|
3
|
+
require 'delegate'
|
4
4
|
|
5
5
|
module Hub
|
6
6
|
# Methods for inspecting the environment, such as reading git config,
|
@@ -99,7 +99,7 @@ module Hub
|
|
99
99
|
|
100
100
|
repo_methods = [
|
101
101
|
:current_branch,
|
102
|
-
:
|
102
|
+
:remote_branch_and_project,
|
103
103
|
:repo_owner, :repo_host,
|
104
104
|
:remotes, :remotes_group, :origin_remote
|
105
105
|
]
|
@@ -141,15 +141,13 @@ module Hub
|
|
141
141
|
remote = origin_remote and remote.project
|
142
142
|
end
|
143
143
|
|
144
|
-
def
|
145
|
-
|
146
|
-
|
147
|
-
|
144
|
+
def remote_branch_and_project(username_fetcher)
|
145
|
+
project = main_project
|
146
|
+
if project and branch = current_branch
|
147
|
+
branch = branch.push_target(username_fetcher.call(project.host))
|
148
|
+
project = remote_by_name(branch.remote_name).project if branch && branch.remote?
|
148
149
|
end
|
149
|
-
|
150
|
-
|
151
|
-
def current_project
|
152
|
-
upstream_project || main_project
|
150
|
+
[branch, project]
|
153
151
|
end
|
154
152
|
|
155
153
|
def current_branch
|
@@ -159,19 +157,31 @@ module Hub
|
|
159
157
|
end
|
160
158
|
|
161
159
|
def master_branch
|
162
|
-
|
160
|
+
if remote = origin_remote
|
161
|
+
default_branch = git_command("rev-parse --symbolic-full-name #{remote}")
|
162
|
+
end
|
163
|
+
Branch.new(self, default_branch || 'refs/heads/master')
|
163
164
|
end
|
164
165
|
|
166
|
+
ORIGIN_NAMES = %w[ upstream github origin ]
|
167
|
+
|
165
168
|
def remotes
|
166
169
|
@remotes ||= begin
|
167
170
|
# TODO: is there a plumbing command to get a list of remotes?
|
168
171
|
list = git_command('remote').to_s.split("\n")
|
169
|
-
|
170
|
-
|
172
|
+
list = ORIGIN_NAMES.inject([]) { |sorted, name|
|
173
|
+
sorted << list.delete(name)
|
174
|
+
}.compact.concat(list)
|
171
175
|
list.map { |name| Remote.new self, name }
|
172
176
|
end
|
173
177
|
end
|
174
178
|
|
179
|
+
def remotes_for_publish(owner_name)
|
180
|
+
list = ORIGIN_NAMES.map {|n| remote_by_name(n) }
|
181
|
+
list << remotes.find {|r| p = r.project and p.owner == owner_name }
|
182
|
+
list.compact.uniq.reverse
|
183
|
+
end
|
184
|
+
|
175
185
|
def remotes_group(name)
|
176
186
|
git_config "remotes.#{name}"
|
177
187
|
end
|
@@ -269,7 +279,7 @@ module Hub
|
|
269
279
|
end
|
270
280
|
end
|
271
281
|
|
272
|
-
class GithubURL < URI::
|
282
|
+
class GithubURL < DelegateClass(URI::HTTP)
|
273
283
|
extend Forwardable
|
274
284
|
|
275
285
|
attr_reader :project
|
@@ -279,16 +289,15 @@ module Hub
|
|
279
289
|
def self.resolve(url, local_repo)
|
280
290
|
u = URI(url)
|
281
291
|
if %[http https].include? u.scheme and project = GithubProject.from_url(u, local_repo)
|
282
|
-
self.new(u
|
283
|
-
u.path, u.opaque, u.query, u.fragment, project)
|
292
|
+
self.new(u, project)
|
284
293
|
end
|
285
294
|
rescue URI::InvalidURIError
|
286
295
|
nil
|
287
296
|
end
|
288
297
|
|
289
|
-
def initialize(
|
290
|
-
@project =
|
291
|
-
super(
|
298
|
+
def initialize(uri, project)
|
299
|
+
@project = project
|
300
|
+
super(uri)
|
292
301
|
end
|
293
302
|
|
294
303
|
# segment of path after the project owner and name
|
@@ -305,7 +314,10 @@ module Hub
|
|
305
314
|
end
|
306
315
|
|
307
316
|
def master?
|
308
|
-
|
317
|
+
master_name = if local_repo then local_repo.master_branch.short_name
|
318
|
+
else 'master'
|
319
|
+
end
|
320
|
+
short_name == master_name
|
309
321
|
end
|
310
322
|
|
311
323
|
def upstream
|
@@ -314,6 +326,21 @@ module Hub
|
|
314
326
|
end
|
315
327
|
end
|
316
328
|
|
329
|
+
def push_target(owner_name)
|
330
|
+
push_default = local_repo.git_config('push.default')
|
331
|
+
if %w[upstream tracking].include?(push_default)
|
332
|
+
upstream
|
333
|
+
else
|
334
|
+
short = short_name
|
335
|
+
refs = local_repo.remotes_for_publish(owner_name).map { |remote|
|
336
|
+
"refs/remotes/#{remote}/#{short}"
|
337
|
+
}
|
338
|
+
if branch = refs.detect {|ref| local_repo.git_command("rev-parse -q --verify #{ref}") }
|
339
|
+
Branch.new(local_repo, branch)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
317
344
|
def remote?
|
318
345
|
name.index('refs/remotes/') == 0
|
319
346
|
end
|
@@ -431,6 +458,7 @@ module Hub
|
|
431
458
|
def git_editor
|
432
459
|
# possible: ~/bin/vi, $SOME_ENVIRONMENT_VARIABLE, "C:\Program Files\Vim\gvim.exe" --nofork
|
433
460
|
editor = git_command 'var GIT_EDITOR'
|
461
|
+
editor.gsub!(/\$(\w+|\{\w+\})/) { ENV[$1.tr('{}', '')] }
|
434
462
|
editor = ENV[$1] if editor =~ /^\$(\w+)$/
|
435
463
|
editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/
|
436
464
|
# avoid shellsplitting "C:\Program Files"
|
@@ -463,6 +491,11 @@ module Hub
|
|
463
491
|
RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/
|
464
492
|
end
|
465
493
|
|
494
|
+
def unix?
|
495
|
+
require 'rbconfig'
|
496
|
+
RbConfig::CONFIG['host_os'] =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i
|
497
|
+
end
|
498
|
+
|
466
499
|
# Cross-platform way of finding an executable in the $PATH.
|
467
500
|
#
|
468
501
|
# which('ruby') #=> /usr/bin/ruby
|
@@ -489,6 +522,16 @@ module Hub
|
|
489
522
|
def tmp_dir
|
490
523
|
ENV['TMPDIR'] || ENV['TEMP'] || '/tmp'
|
491
524
|
end
|
525
|
+
|
526
|
+
def terminal_width
|
527
|
+
if unix?
|
528
|
+
width = %x{stty size 2>#{NULL}}.split[1].to_i
|
529
|
+
width = %x{tput cols 2>#{NULL}}.to_i if width.zero?
|
530
|
+
else
|
531
|
+
width = 0
|
532
|
+
end
|
533
|
+
width < 10 ? 78 : width
|
534
|
+
end
|
492
535
|
end
|
493
536
|
|
494
537
|
include System
|
data/lib/hub/github_api.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
require 'uri'
|
2
|
-
require 'yaml'
|
3
1
|
require 'forwardable'
|
4
|
-
require 'fileutils'
|
5
2
|
|
6
3
|
module Hub
|
7
4
|
# Client for the GitHub v3 API.
|
@@ -15,7 +12,7 @@ module Hub
|
|
15
12
|
# config_file = ENV['HUB_CONFIG'] || '~/.config/hub'
|
16
13
|
# file_store = GitHubAPI::FileStore.new File.expand_path(config_file)
|
17
14
|
# file_config = GitHubAPI::Configuration.new file_store
|
18
|
-
# GitHubAPI.new file_config, :app_url => 'http://
|
15
|
+
# GitHubAPI.new file_config, :app_url => 'http://hub.github.com/'
|
19
16
|
# end
|
20
17
|
class GitHubAPI
|
21
18
|
attr_reader :config, :oauth_app_url
|
@@ -25,7 +22,6 @@ module Hub
|
|
25
22
|
# Options:
|
26
23
|
# - config: an object that implements:
|
27
24
|
# - username(host)
|
28
|
-
# - api_token(host, user)
|
29
25
|
# - password(host, user)
|
30
26
|
# - oauth_token(host, user)
|
31
27
|
def initialize config, options
|
@@ -46,6 +42,19 @@ module Hub
|
|
46
42
|
'github.com' == host ? 'api.github.com' : host
|
47
43
|
end
|
48
44
|
|
45
|
+
def username_via_auth_dance host
|
46
|
+
host = api_host(host)
|
47
|
+
config.username(host) do
|
48
|
+
if block_given?
|
49
|
+
yield
|
50
|
+
else
|
51
|
+
res = get("https://%s/user" % host)
|
52
|
+
res.error! unless res.success?
|
53
|
+
config.value_to_persist(res.data['login'])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
49
58
|
# Public: Fetch data for a specific repo.
|
50
59
|
def repo_info project
|
51
60
|
get "https://%s/repos/%s/%s" %
|
@@ -66,7 +75,7 @@ module Hub
|
|
66
75
|
|
67
76
|
# Public: Create a new project.
|
68
77
|
def create_repo project, options = {}
|
69
|
-
is_org = project.owner.downcase !=
|
78
|
+
is_org = project.owner.downcase != username_via_auth_dance(project.host).downcase
|
70
79
|
params = { :name => project.name, :private => !!options[:private] }
|
71
80
|
params[:description] = options[:description] if options[:description]
|
72
81
|
params[:homepage] = options[:homepage] if options[:homepage]
|
@@ -88,6 +97,38 @@ module Hub
|
|
88
97
|
res.data
|
89
98
|
end
|
90
99
|
|
100
|
+
# Public: Fetch a pull request's patch
|
101
|
+
def pullrequest_patch project, pull_id
|
102
|
+
res = get "https://%s/repos/%s/%s/pulls/%d" %
|
103
|
+
[api_host(project.host), project.owner, project.name, pull_id] do |req|
|
104
|
+
req["Accept"] = "application/vnd.github.v3.patch"
|
105
|
+
end
|
106
|
+
res.error! unless res.success?
|
107
|
+
res.body
|
108
|
+
end
|
109
|
+
|
110
|
+
# Public: Fetch the patch from a commit
|
111
|
+
def commit_patch project, sha
|
112
|
+
res = get "https://%s/repos/%s/%s/commits/%s" %
|
113
|
+
[api_host(project.host), project.owner, project.name, sha] do |req|
|
114
|
+
req["Accept"] = "application/vnd.github.v3.patch"
|
115
|
+
end
|
116
|
+
res.error! unless res.success?
|
117
|
+
res.body
|
118
|
+
end
|
119
|
+
|
120
|
+
# Public: Fetch the first raw blob from a gist
|
121
|
+
def gist_raw gist_id
|
122
|
+
res = get("https://%s/gists/%s" % [api_host('github.com'), gist_id])
|
123
|
+
res.error! unless res.success?
|
124
|
+
raw_url = res.data['files'].values.first['raw_url']
|
125
|
+
res = get(raw_url) do |req|
|
126
|
+
req['Accept'] = 'text/plain'
|
127
|
+
end
|
128
|
+
res.error! unless res.success?
|
129
|
+
res.body
|
130
|
+
end
|
131
|
+
|
91
132
|
# Returns parsed data from the new pull request.
|
92
133
|
def create_pullrequest options
|
93
134
|
project = options.fetch(:project)
|
@@ -110,12 +151,19 @@ module Hub
|
|
110
151
|
res.data
|
111
152
|
end
|
112
153
|
|
154
|
+
def statuses project, sha
|
155
|
+
res = get "https://%s/repos/%s/%s/statuses/%s" %
|
156
|
+
[api_host(project.host), project.owner, project.name, sha]
|
157
|
+
|
158
|
+
res.error! unless res.success?
|
159
|
+
res.data
|
160
|
+
end
|
161
|
+
|
113
162
|
# Methods for performing HTTP requests
|
114
163
|
#
|
115
164
|
# Requires access to a `config` object that implements:
|
116
165
|
# - proxy_uri(with_ssl)
|
117
166
|
# - username(host)
|
118
|
-
# - update_username(host, old_username, new_username)
|
119
167
|
# - password(host, user)
|
120
168
|
module HttpMethods
|
121
169
|
# Decorator for Net::HTTPResponse
|
@@ -180,6 +228,7 @@ module Hub
|
|
180
228
|
req['User-Agent'] = "Hub #{Hub::VERSION}"
|
181
229
|
apply_authentication(req, url)
|
182
230
|
yield req if block_given?
|
231
|
+
finalize_request(req, url)
|
183
232
|
|
184
233
|
begin
|
185
234
|
res = http.start { http.request(req) }
|
@@ -192,7 +241,7 @@ module Hub
|
|
192
241
|
|
193
242
|
def request_uri url
|
194
243
|
str = url.request_uri
|
195
|
-
str = '/api/v3' << str if url.host != 'api.github.com'
|
244
|
+
str = '/api/v3' << str if url.host != 'api.github.com' && url.host != 'gist.github.com'
|
196
245
|
str
|
197
246
|
end
|
198
247
|
|
@@ -208,11 +257,17 @@ module Hub
|
|
208
257
|
end
|
209
258
|
|
210
259
|
def apply_authentication req, url
|
211
|
-
user = url.user
|
260
|
+
user = url.user ? CGI.unescape(url.user) : config.username(url.host)
|
212
261
|
pass = config.password(url.host, user)
|
213
262
|
req.basic_auth user, pass
|
214
263
|
end
|
215
264
|
|
265
|
+
def finalize_request(req, url)
|
266
|
+
if !req['Accept'] || req['Accept'] == '*/*'
|
267
|
+
req['Accept'] = 'application/vnd.github.v3+json'
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
216
271
|
def create_connection url
|
217
272
|
use_ssl = 'https' == url.scheme
|
218
273
|
|
@@ -220,7 +275,6 @@ module Hub
|
|
220
275
|
if proxy = config.proxy_uri(use_ssl)
|
221
276
|
proxy_args << proxy.host << proxy.port
|
222
277
|
if proxy.userinfo
|
223
|
-
require 'cgi'
|
224
278
|
# proxy user + password
|
225
279
|
proxy_args.concat proxy.userinfo.split(':', 2).map {|a| CGI.unescape a }
|
226
280
|
end
|
@@ -238,44 +292,59 @@ module Hub
|
|
238
292
|
|
239
293
|
module OAuth
|
240
294
|
def apply_authentication req, url
|
241
|
-
if
|
295
|
+
if req.path =~ %r{^(/api/v3)?/authorizations$}
|
242
296
|
super
|
243
297
|
else
|
244
|
-
|
245
|
-
user = url.user || config.username(url.host)
|
298
|
+
user = url.user ? CGI.unescape(url.user) : config.username(url.host)
|
246
299
|
token = config.oauth_token(url.host, user) {
|
247
|
-
refresh = true
|
248
300
|
obtain_oauth_token url.host, user
|
249
301
|
}
|
250
|
-
if refresh
|
251
|
-
# get current user info user to persist correctly capitalized login name
|
252
|
-
res = get "https://#{url.host}/user"
|
253
|
-
res.error! unless res.success?
|
254
|
-
config.update_username(url.host, user, res.data['login'])
|
255
|
-
end
|
256
302
|
req['Authorization'] = "token #{token}"
|
257
303
|
end
|
258
304
|
end
|
259
305
|
|
260
|
-
def obtain_oauth_token host, user
|
306
|
+
def obtain_oauth_token host, user, two_factor_code = nil
|
307
|
+
auth_url = URI.parse("https://%s@%s/authorizations" % [CGI.escape(user), host])
|
308
|
+
|
309
|
+
# dummy request to trigger a 2FA SMS since a HTTP GET won't do it
|
310
|
+
post(auth_url) if !two_factor_code
|
311
|
+
|
261
312
|
# first try to fetch existing authorization
|
262
|
-
res = get
|
263
|
-
|
313
|
+
res = get(auth_url) do |req|
|
314
|
+
req['X-GitHub-OTP'] = two_factor_code if two_factor_code
|
315
|
+
end
|
316
|
+
unless res.success?
|
317
|
+
if !two_factor_code && res['X-GitHub-OTP'].to_s.include?('required')
|
318
|
+
two_factor_code = config.prompt_auth_code
|
319
|
+
return obtain_oauth_token(host, user, two_factor_code)
|
320
|
+
else
|
321
|
+
res.error!
|
322
|
+
end
|
323
|
+
end
|
264
324
|
|
265
325
|
if found = res.data.find {|auth| auth['app']['url'] == oauth_app_url }
|
266
326
|
found['token']
|
267
327
|
else
|
268
328
|
# create a new authorization
|
269
|
-
res = post
|
270
|
-
:scopes => %w[repo], :note => 'hub', :note_url => oauth_app_url
|
329
|
+
res = post auth_url,
|
330
|
+
:scopes => %w[repo], :note => 'hub', :note_url => oauth_app_url do |req|
|
331
|
+
req['X-GitHub-OTP'] = two_factor_code if two_factor_code
|
332
|
+
end
|
271
333
|
res.error! unless res.success?
|
272
334
|
res.data['token']
|
273
335
|
end
|
274
336
|
end
|
275
337
|
end
|
276
338
|
|
339
|
+
module GistAuth
|
340
|
+
def apply_authentication(req, url)
|
341
|
+
super unless url.host == 'gist.github.com'
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
277
345
|
include HttpMethods
|
278
346
|
include OAuth
|
347
|
+
include GistAuth
|
279
348
|
|
280
349
|
# Filesystem store suitable for Configuration
|
281
350
|
class FileStore
|
@@ -286,47 +355,83 @@ module Hub
|
|
286
355
|
def initialize filename
|
287
356
|
@filename = filename
|
288
357
|
@data = Hash.new {|d, host| d[host] = [] }
|
358
|
+
@persist_next_change = false
|
289
359
|
load if File.exist? filename
|
290
360
|
end
|
291
361
|
|
292
|
-
def fetch_user host
|
293
|
-
unless entry = get(host).first
|
294
|
-
user = yield
|
295
|
-
# FIXME: more elegant handling of empty strings
|
296
|
-
return nil if user.nil? or user.empty?
|
297
|
-
entry = entry_for_user(host, user)
|
298
|
-
end
|
299
|
-
entry['user']
|
300
|
-
end
|
301
|
-
|
302
362
|
def fetch_value host, user, key
|
303
|
-
|
304
|
-
|
363
|
+
entries = get(host)
|
364
|
+
entries << {} if entries.empty?
|
365
|
+
entry = entries.first
|
366
|
+
entry.fetch(key.to_s) {
|
305
367
|
value = yield
|
306
|
-
if value
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
raise "no value"
|
312
|
-
end
|
313
|
-
end
|
368
|
+
raise "no value for key :#{key}" if value.nil? || value.empty?
|
369
|
+
entry[key.to_s] = value
|
370
|
+
save_if_needed
|
371
|
+
value
|
372
|
+
}
|
314
373
|
end
|
315
374
|
|
316
|
-
def
|
317
|
-
|
318
|
-
|
319
|
-
|
375
|
+
def persist_next_change!
|
376
|
+
@persist_next_change = true
|
377
|
+
end
|
378
|
+
|
379
|
+
def save_if_needed
|
380
|
+
@persist_next_change && save
|
381
|
+
@persist_next_change = false
|
320
382
|
end
|
321
383
|
|
322
384
|
def load
|
323
385
|
existing_data = File.read(@filename)
|
324
|
-
@data.update
|
386
|
+
@data.update yaml_load(existing_data) unless existing_data.strip.empty?
|
325
387
|
end
|
326
388
|
|
327
389
|
def save
|
328
|
-
|
329
|
-
File.open(@filename, 'w', 0600) {|f| f <<
|
390
|
+
mkdir_p File.dirname(@filename)
|
391
|
+
File.open(@filename, 'w', 0600) {|f| f << yaml_dump(@data) }
|
392
|
+
end
|
393
|
+
|
394
|
+
def mkdir_p(dir)
|
395
|
+
dir.split('/').inject do |parent, name|
|
396
|
+
d = File.join(parent, name)
|
397
|
+
Dir.mkdir(d) unless File.exist?(d)
|
398
|
+
d
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
def yaml_load(string)
|
403
|
+
hash = {}
|
404
|
+
host = nil
|
405
|
+
string.split("\n").each do |line|
|
406
|
+
case line
|
407
|
+
when /^---\s*$/, /^\s*(?:#|$)/
|
408
|
+
# ignore
|
409
|
+
when /^(.+):\s*$/
|
410
|
+
host = hash[$1] = []
|
411
|
+
when /^([- ]) (.+?): (.+)/
|
412
|
+
key, value = $2, $3
|
413
|
+
host << {} if $1 == '-'
|
414
|
+
host.last[key] = value.gsub(/^'|'$/, '')
|
415
|
+
else
|
416
|
+
raise "unsupported YAML line: #{line}"
|
417
|
+
end
|
418
|
+
end
|
419
|
+
hash
|
420
|
+
end
|
421
|
+
|
422
|
+
def yaml_dump(data)
|
423
|
+
yaml = ['---']
|
424
|
+
data.each do |host, values|
|
425
|
+
yaml << "#{host}:"
|
426
|
+
values.each do |hash|
|
427
|
+
dash = '-'
|
428
|
+
hash.each do |key, value|
|
429
|
+
yaml << "#{dash} #{key}: #{value}"
|
430
|
+
dash = ' '
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
yaml.join("\n")
|
330
435
|
end
|
331
436
|
end
|
332
437
|
|
@@ -347,41 +452,36 @@ module Hub
|
|
347
452
|
def username host
|
348
453
|
return ENV['GITHUB_USER'] unless ENV['GITHUB_USER'].to_s.empty?
|
349
454
|
host = normalize_host host
|
350
|
-
@data.
|
455
|
+
@data.fetch_value(host, nil, :user) do
|
351
456
|
if block_given? then yield
|
352
457
|
else prompt "#{host} username"
|
353
458
|
end
|
354
459
|
end
|
355
460
|
end
|
356
461
|
|
357
|
-
def update_username host, old_username, new_username
|
358
|
-
entry = @data.entry_for_user(normalize_host(host), old_username)
|
359
|
-
entry['user'] = new_username
|
360
|
-
@data.save
|
361
|
-
end
|
362
|
-
|
363
|
-
def api_token host, user
|
364
|
-
host = normalize_host host
|
365
|
-
@data.fetch_value host, user, :api_token do
|
366
|
-
if block_given? then yield
|
367
|
-
else prompt "#{host} API token for #{user}"
|
368
|
-
end
|
369
|
-
end
|
370
|
-
end
|
371
|
-
|
372
462
|
def password host, user
|
373
463
|
return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.empty?
|
374
464
|
host = normalize_host host
|
375
465
|
@password_cache["#{user}@#{host}"] ||= prompt_password host, user
|
376
466
|
end
|
377
467
|
|
378
|
-
def oauth_token host, user
|
379
|
-
|
468
|
+
def oauth_token host, user
|
469
|
+
host = normalize_host(host)
|
470
|
+
@data.fetch_value(host, user, :oauth_token) do
|
471
|
+
value_to_persist(yield)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def value_to_persist(value = nil)
|
476
|
+
@data.persist_next_change!
|
477
|
+
value
|
380
478
|
end
|
381
479
|
|
382
480
|
def prompt what
|
383
481
|
print "#{what}: "
|
384
482
|
$stdin.gets.chomp
|
483
|
+
rescue Interrupt
|
484
|
+
abort
|
385
485
|
end
|
386
486
|
|
387
487
|
# special prompt that has hidden input
|
@@ -395,16 +495,38 @@ module Hub
|
|
395
495
|
# in testing
|
396
496
|
$stdin.gets.chomp
|
397
497
|
end
|
498
|
+
rescue Interrupt
|
499
|
+
abort
|
500
|
+
end
|
501
|
+
|
502
|
+
def prompt_auth_code
|
503
|
+
print "two-factor authentication code: "
|
504
|
+
$stdin.gets.chomp
|
505
|
+
rescue Interrupt
|
506
|
+
abort
|
398
507
|
end
|
399
508
|
|
400
509
|
NULL = defined?(File::NULL) ? File::NULL :
|
401
510
|
File.exist?('/dev/null') ? '/dev/null' : 'NUL'
|
402
511
|
|
403
512
|
def askpass
|
513
|
+
noecho $stdin do |input|
|
514
|
+
input.gets.chomp
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
def noecho io
|
519
|
+
require 'io/console'
|
520
|
+
io.noecho { yield io }
|
521
|
+
rescue LoadError
|
522
|
+
fallback_noecho io
|
523
|
+
end
|
524
|
+
|
525
|
+
def fallback_noecho io
|
404
526
|
tty_state = `stty -g 2>#{NULL}`
|
405
527
|
system 'stty raw -echo -icanon isig' if $?.success?
|
406
528
|
pass = ''
|
407
|
-
while char = getbyte(
|
529
|
+
while char = getbyte(io) and !(char == 13 or char == 10)
|
408
530
|
if char == 127 or char == 8
|
409
531
|
pass[-1,1] = '' unless pass.empty?
|
410
532
|
else
|