hub 1.6.1 → 1.7.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 +94 -60
- data/Rakefile +74 -38
- data/lib/hub/args.rb +16 -3
- data/lib/hub/commands.rb +340 -101
- data/lib/hub/context.rb +270 -112
- data/lib/hub/runner.rb +4 -2
- data/lib/hub/standalone.rb +13 -7
- data/lib/hub/version.rb +1 -1
- data/man/hub.1 +162 -80
- data/man/hub.1.html +163 -96
- data/man/hub.1.ronn +84 -167
- data/test/alias_test.rb +0 -1
- data/test/helper.rb +8 -8
- data/test/hub_test.rb +395 -71
- data/test/standalone_test.rb +0 -1
- metadata +8 -6
data/lib/hub/context.rb
CHANGED
@@ -1,66 +1,274 @@
|
|
1
1
|
require 'shellwords'
|
2
|
+
require 'forwardable'
|
2
3
|
|
3
4
|
module Hub
|
4
5
|
# Provides methods for inspecting the environment, such as GitHub user/token
|
5
6
|
# settings, repository info, and similar.
|
6
7
|
module Context
|
7
|
-
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
NULL = defined?(File::NULL) ? File::NULL : File.exist?('/dev/null') ? '/dev/null' : 'NUL'
|
11
|
+
|
12
|
+
# Shells out to git to get output of its commands
|
13
|
+
class GitReader
|
14
|
+
attr_reader :executable
|
15
|
+
|
16
|
+
def initialize(executable = nil, &read_proc)
|
17
|
+
@executable = executable || 'git'
|
18
|
+
# caches output when shelling out to git
|
19
|
+
read_proc ||= lambda { |cache, cmd|
|
20
|
+
result = %x{#{command_to_string(cmd)} 2>#{NULL}}.chomp
|
21
|
+
cache[cmd] = $?.success? && !result.empty? ? result : nil
|
22
|
+
}
|
23
|
+
@cache = Hash.new(&read_proc)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_exec_flags(flags)
|
27
|
+
@executable = Array(executable).concat(flags)
|
28
|
+
end
|
8
29
|
|
9
|
-
|
10
|
-
|
30
|
+
def read_config(cmd, all = false)
|
31
|
+
config_cmd = ['config', (all ? '--get-all' : '--get'), *cmd]
|
32
|
+
config_cmd = config_cmd.join(' ') unless cmd.respond_to? :join
|
33
|
+
read config_cmd
|
34
|
+
end
|
35
|
+
|
36
|
+
def read(cmd)
|
37
|
+
@cache[cmd]
|
38
|
+
end
|
39
|
+
|
40
|
+
def stub_config_value(key, value, get = '--get')
|
41
|
+
stub_command_output "config #{get} #{key}", value
|
42
|
+
end
|
43
|
+
|
44
|
+
def stub_command_output(cmd, value)
|
45
|
+
@cache[cmd] = value.nil? ? nil : value.to_s
|
46
|
+
end
|
11
47
|
|
12
|
-
def
|
13
|
-
|
14
|
-
@executable = executable
|
48
|
+
def stub!(values)
|
49
|
+
@cache.update values
|
15
50
|
end
|
16
51
|
|
52
|
+
private
|
53
|
+
|
17
54
|
def to_exec(args)
|
18
55
|
args = Shellwords.shellwords(args) if args.respond_to? :to_str
|
19
56
|
Array(executable) + Array(args)
|
20
57
|
end
|
58
|
+
|
59
|
+
def command_to_string(cmd)
|
60
|
+
full_cmd = to_exec(cmd)
|
61
|
+
full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ')
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module GitReaderMethods
|
66
|
+
extend Forwardable
|
67
|
+
|
68
|
+
def_delegator :git_reader, :read_config, :git_config
|
69
|
+
def_delegator :git_reader, :read, :git_command
|
70
|
+
|
71
|
+
def self.extended(base)
|
72
|
+
base.extend Forwardable
|
73
|
+
base.def_delegators :'self.class', :git_config, :git_command
|
74
|
+
end
|
21
75
|
end
|
22
76
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
result = %x{#{cmd_string}}.chomp
|
28
|
-
cache[cmd] = $?.success? && !result.empty? ? result : nil
|
77
|
+
private
|
78
|
+
|
79
|
+
def git_reader
|
80
|
+
@git_reader ||= GitReader.new ENV['GIT']
|
29
81
|
end
|
30
82
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
83
|
+
include GitReaderMethods
|
84
|
+
private :git_config, :git_command
|
85
|
+
|
86
|
+
def local_repo
|
87
|
+
@local_repo ||= begin
|
88
|
+
LocalRepo.new git_reader, current_dir if is_repo?
|
89
|
+
end
|
90
|
+
end
|
35
91
|
|
36
|
-
|
37
|
-
|
92
|
+
repo_methods = [
|
93
|
+
:current_branch, :master_branch,
|
94
|
+
:current_project, :upstream_project,
|
95
|
+
:repo_owner,
|
96
|
+
:remotes, :remotes_group, :origin_remote
|
97
|
+
]
|
98
|
+
def_delegator :local_repo, :name, :repo_name
|
99
|
+
def_delegators :local_repo, *repo_methods
|
100
|
+
private :repo_name, *repo_methods
|
101
|
+
|
102
|
+
class LocalRepo < Struct.new(:git_reader, :dir)
|
103
|
+
include GitReaderMethods
|
104
|
+
|
105
|
+
def name
|
106
|
+
if project = main_project
|
107
|
+
project.name
|
38
108
|
else
|
39
|
-
|
109
|
+
File.basename(dir)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def repo_owner
|
114
|
+
if project = main_project
|
115
|
+
project.owner
|
40
116
|
end
|
41
|
-
|
42
|
-
|
117
|
+
end
|
118
|
+
|
119
|
+
def main_project
|
120
|
+
remote = origin_remote and remote.project
|
121
|
+
end
|
122
|
+
|
123
|
+
def upstream_project
|
124
|
+
if upstream = current_branch.upstream
|
125
|
+
remote = remote_by_name upstream.remote_name
|
126
|
+
remote.project
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def current_project
|
131
|
+
upstream_project || main_project
|
132
|
+
end
|
133
|
+
|
134
|
+
def current_branch
|
135
|
+
if branch = git_command('symbolic-ref -q HEAD')
|
136
|
+
Branch.new self, branch
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def master_branch
|
141
|
+
Branch.new self, 'refs/heads/master'
|
142
|
+
end
|
143
|
+
|
144
|
+
def remotes
|
145
|
+
@remotes ||= begin
|
146
|
+
# TODO: is there a plumbing command to get a list of remotes?
|
147
|
+
list = git_command('remote').to_s.split("\n")
|
148
|
+
# force "origin" to be first in the list
|
149
|
+
main = list.delete('origin') and list.unshift(main)
|
150
|
+
list.map { |name| Remote.new self, name }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def remotes_group(name)
|
155
|
+
git_config "remotes.#{name}"
|
156
|
+
end
|
157
|
+
|
158
|
+
def origin_remote
|
159
|
+
remotes.first
|
160
|
+
end
|
161
|
+
|
162
|
+
def remote_by_name(remote_name)
|
163
|
+
remotes.find {|r| r.name == remote_name }
|
43
164
|
end
|
44
165
|
end
|
45
166
|
|
46
|
-
|
167
|
+
class GithubProject < Struct.new(:local_repo, :owner, :name)
|
168
|
+
def name_with_owner
|
169
|
+
"#{owner}/#{name}"
|
170
|
+
end
|
171
|
+
|
172
|
+
def ==(other)
|
173
|
+
name_with_owner == other.name_with_owner
|
174
|
+
end
|
175
|
+
|
176
|
+
def remote
|
177
|
+
local_repo.remotes.find { |r| r.project == self }
|
178
|
+
end
|
179
|
+
|
180
|
+
def web_url(path = nil)
|
181
|
+
project_name = name_with_owner
|
182
|
+
if project_name.sub!(/\.wiki$/, '')
|
183
|
+
unless '/wiki' == path
|
184
|
+
path = if path =~ %r{^/commits/} then '/_history'
|
185
|
+
else path.to_s.sub(/\w+/, '_\0')
|
186
|
+
end
|
187
|
+
path = '/wiki' + path
|
188
|
+
end
|
189
|
+
end
|
190
|
+
'https://github.com/' + project_name + path.to_s
|
191
|
+
end
|
47
192
|
|
48
|
-
|
49
|
-
|
193
|
+
def git_url(options = {})
|
194
|
+
if options[:https] then 'https://github.com/'
|
195
|
+
elsif options[:private] then 'git@github.com:'
|
196
|
+
else 'git://github.com/'
|
197
|
+
end + name_with_owner + '.git'
|
198
|
+
end
|
50
199
|
end
|
51
200
|
|
52
|
-
|
53
|
-
|
201
|
+
class Branch < Struct.new(:local_repo, :name)
|
202
|
+
alias to_s name
|
203
|
+
|
204
|
+
def short_name
|
205
|
+
name.split('/').last
|
206
|
+
end
|
207
|
+
|
208
|
+
def master?
|
209
|
+
short_name == 'master'
|
210
|
+
end
|
211
|
+
|
212
|
+
def upstream
|
213
|
+
if branch = local_repo.git_command("rev-parse --symbolic-full-name #{short_name}@{upstream}")
|
214
|
+
Branch.new local_repo, branch
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def remote?
|
219
|
+
name.index('refs/remotes/') == 0
|
220
|
+
end
|
221
|
+
|
222
|
+
def remote_name
|
223
|
+
name =~ %r{^refs/remotes/([^/]+)} and $1 or
|
224
|
+
raise "can't get remote name from #{name.inspect}"
|
225
|
+
end
|
54
226
|
end
|
55
227
|
|
56
|
-
|
57
|
-
|
228
|
+
class Remote < Struct.new(:local_repo, :name)
|
229
|
+
alias to_s name
|
230
|
+
|
231
|
+
def ==(other)
|
232
|
+
other.respond_to?(:to_str) ? name == other.to_str : super
|
233
|
+
end
|
234
|
+
|
235
|
+
def project
|
236
|
+
if urls.find { |u| u =~ %r{\bgithub\.com[:/](.+)/(.+).git$} }
|
237
|
+
GithubProject.new local_repo, $1, $2
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def urls
|
242
|
+
@urls ||= local_repo.git_config("remote.#{name}.url", :all).to_s.split("\n")
|
243
|
+
end
|
58
244
|
end
|
59
245
|
|
246
|
+
## helper methods for local repo, GH projects
|
247
|
+
|
248
|
+
def github_project(name, owner = nil)
|
249
|
+
if owner and owner.index('/')
|
250
|
+
owner, name = owner.split('/', 2)
|
251
|
+
elsif name and name.index('/')
|
252
|
+
owner, name = name.split('/', 2)
|
253
|
+
else
|
254
|
+
name ||= repo_name
|
255
|
+
owner ||= github_user
|
256
|
+
end
|
257
|
+
|
258
|
+
GithubProject.new local_repo, owner, name
|
259
|
+
end
|
260
|
+
|
261
|
+
def git_url(owner = nil, name = nil, options = {})
|
262
|
+
project = github_project(name, owner)
|
263
|
+
project.git_url({:https => https_protocol?}.update(options))
|
264
|
+
end
|
265
|
+
|
266
|
+
LGHCONF = "http://help.github.com/set-your-user-name-email-and-github-token/"
|
267
|
+
|
60
268
|
# Either returns the GitHub user as set by git-config(1) or aborts
|
61
269
|
# with an error message.
|
62
270
|
def github_user(fatal = true)
|
63
|
-
if user = ENV['GITHUB_USER'] ||
|
271
|
+
if user = ENV['GITHUB_USER'] || git_config('github.user')
|
64
272
|
user
|
65
273
|
elsif fatal
|
66
274
|
abort("** No GitHub user set. See #{LGHCONF}")
|
@@ -68,121 +276,71 @@ module Hub
|
|
68
276
|
end
|
69
277
|
|
70
278
|
def github_token(fatal = true)
|
71
|
-
if token = ENV['GITHUB_TOKEN'] ||
|
279
|
+
if token = ENV['GITHUB_TOKEN'] || git_config('github.token')
|
72
280
|
token
|
73
281
|
elsif fatal
|
74
282
|
abort("** No GitHub token set. See #{LGHCONF}")
|
75
283
|
end
|
76
284
|
end
|
77
285
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
def tracked_branch
|
83
|
-
branch = current_branch && tracked_for(current_branch)
|
84
|
-
normalize_branch(branch) if branch
|
85
|
-
end
|
86
|
-
|
87
|
-
def remotes
|
88
|
-
list = GIT_CONFIG['remote'].to_s.split("\n")
|
89
|
-
main = list.delete('origin') and list.unshift(main)
|
90
|
-
list
|
91
|
-
end
|
92
|
-
|
93
|
-
def remotes_group(name)
|
94
|
-
GIT_CONFIG["config remotes.#{name}"]
|
95
|
-
end
|
96
|
-
|
97
|
-
def current_remote
|
98
|
-
return if remotes.empty?
|
99
|
-
(current_branch && remote_for(current_branch)) || default_remote
|
100
|
-
end
|
101
|
-
|
102
|
-
def default_remote
|
103
|
-
remotes.first
|
286
|
+
# legacy setting
|
287
|
+
def http_clone?
|
288
|
+
git_config('--bool hub.http-clone') == 'true'
|
104
289
|
end
|
105
290
|
|
106
|
-
def
|
107
|
-
|
291
|
+
def https_protocol?
|
292
|
+
git_config('hub.protocol') == 'https' or http_clone?
|
108
293
|
end
|
109
294
|
|
110
|
-
def
|
111
|
-
|
295
|
+
def git_alias_for(name)
|
296
|
+
git_config "alias.#{name}"
|
112
297
|
end
|
113
298
|
|
114
|
-
|
115
|
-
GIT_CONFIG['config branch.%s.merge' % normalize_branch(branch)]
|
116
|
-
end
|
299
|
+
PWD = Dir.pwd
|
117
300
|
|
118
|
-
def
|
119
|
-
|
301
|
+
def current_dir
|
302
|
+
PWD
|
120
303
|
end
|
121
304
|
|
122
|
-
def
|
123
|
-
|
305
|
+
def git_dir
|
306
|
+
git_command 'rev-parse -q --git-dir'
|
124
307
|
end
|
125
308
|
|
126
|
-
# Core.repositoryformatversion should exist for all git
|
127
|
-
# repositories, and be blank for all non-git repositories. If
|
128
|
-
# there's a better config setting to check here, this can be
|
129
|
-
# changed without breaking anything.
|
130
309
|
def is_repo?
|
131
|
-
|
310
|
+
!!git_dir
|
132
311
|
end
|
133
312
|
|
134
|
-
def
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
if options[:web]
|
142
|
-
scheme = secure ? 'https:' : 'http:'
|
143
|
-
path = options[:web] == true ? '' : options[:web].to_s
|
144
|
-
if repo =~ /\.wiki$/
|
145
|
-
repo = repo.sub(/\.wiki$/, '')
|
146
|
-
unless '/wiki' == path
|
147
|
-
path = '/wiki%s' % if path =~ %r{^/commits/} then '/_history'
|
148
|
-
else path.sub(/\w+/, '_\0')
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
'%s//github.com/%s/%s%s' % [scheme, user, repo, path]
|
153
|
-
else
|
154
|
-
if secure
|
155
|
-
url = 'git@github.com:%s/%s.git'
|
156
|
-
elsif http_clone?
|
157
|
-
url = 'http://github.com/%s/%s.git'
|
158
|
-
else
|
159
|
-
url = 'git://github.com/%s/%s.git'
|
160
|
-
end
|
161
|
-
|
162
|
-
url % [user, repo]
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
|
-
DIRNAME = File.basename(Dir.pwd)
|
167
|
-
|
168
|
-
def current_dirname
|
169
|
-
DIRNAME
|
313
|
+
def git_editor
|
314
|
+
# possible: ~/bin/vi, $SOME_ENVIRONMENT_VARIABLE, "C:\Program Files\Vim\gvim.exe" --nofork
|
315
|
+
editor = git_command 'var GIT_EDITOR'
|
316
|
+
editor = ENV[$1] if editor =~ /^\$(\w+)$/
|
317
|
+
editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/
|
318
|
+
editor.shellsplit
|
170
319
|
end
|
171
320
|
|
172
321
|
# Cross-platform web browser command; respects the value set in $BROWSER.
|
173
322
|
#
|
174
323
|
# Returns an array, e.g.: ['open']
|
175
324
|
def browser_launcher
|
176
|
-
|
177
|
-
|
178
|
-
(RbConfig::CONFIG['host_os'].include?('darwin') && 'open') ||
|
179
|
-
(RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ && 'start') ||
|
325
|
+
browser = ENV['BROWSER'] || (
|
326
|
+
osx? ? 'open' : windows? ? 'start' :
|
180
327
|
%w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm }
|
328
|
+
)
|
181
329
|
|
182
330
|
abort "Please set $BROWSER to a web launcher to use this command." unless browser
|
183
331
|
Array(browser)
|
184
332
|
end
|
185
333
|
|
334
|
+
def osx?
|
335
|
+
require 'rbconfig'
|
336
|
+
RbConfig::CONFIG['host_os'].to_s.include?('darwin')
|
337
|
+
end
|
338
|
+
|
339
|
+
def windows?
|
340
|
+
require 'rbconfig'
|
341
|
+
RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/
|
342
|
+
end
|
343
|
+
|
186
344
|
# Cross-platform way of finding an executable in the $PATH.
|
187
345
|
#
|
188
346
|
# which('ruby') #=> /usr/bin/ruby
|