hub 1.6.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/LICENSE +20 -0
- data/README.md +351 -0
- data/Rakefile +104 -0
- data/bin/hub +7 -0
- data/lib/hub.rb +5 -0
- data/lib/hub/args.rb +99 -0
- data/lib/hub/commands.rb +718 -0
- data/lib/hub/context.rb +159 -0
- data/lib/hub/runner.rb +71 -0
- data/lib/hub/standalone.rb +52 -0
- data/lib/hub/version.rb +3 -0
- data/man/hub.1 +356 -0
- data/man/hub.1.html +370 -0
- data/man/hub.1.ronn +275 -0
- data/test/alias_test.rb +41 -0
- data/test/deps.rip +1 -0
- data/test/fakebin/git +11 -0
- data/test/fakebin/open +3 -0
- data/test/helper.rb +111 -0
- data/test/hub_test.rb +847 -0
- data/test/standalone_test.rb +49 -0
- metadata +106 -0
data/bin/hub
ADDED
data/lib/hub.rb
ADDED
data/lib/hub/args.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module Hub
|
2
|
+
# The Args class exists to make it more convenient to work with
|
3
|
+
# command line arguments intended for git from within the Hub
|
4
|
+
# codebase.
|
5
|
+
#
|
6
|
+
# The ARGV array is converted into an Args instance by the Hub
|
7
|
+
# instance when instantiated.
|
8
|
+
class Args < Array
|
9
|
+
attr_accessor :executable
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
@executable = ENV["GIT"] || "git"
|
14
|
+
@after = nil
|
15
|
+
@skip = false
|
16
|
+
@original_args = args.first
|
17
|
+
@chain = [nil]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds an `after` callback.
|
21
|
+
# A callback can be a command or a proc.
|
22
|
+
def after(cmd_or_args = nil, args = nil, &block)
|
23
|
+
@chain.insert(-1, normalize_callback(cmd_or_args, args, block))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds a `before` callback.
|
27
|
+
# A callback can be a command or a proc.
|
28
|
+
def before(cmd_or_args = nil, args = nil, &block)
|
29
|
+
@chain.insert(@chain.index(nil), normalize_callback(cmd_or_args, args, block))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tells if there are multiple (chained) commands or not.
|
33
|
+
def chained?
|
34
|
+
@chain.size > 1
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of all commands.
|
38
|
+
def commands
|
39
|
+
chain = @chain.dup
|
40
|
+
chain[chain.index(nil)] = self.to_exec
|
41
|
+
chain
|
42
|
+
end
|
43
|
+
|
44
|
+
# Skip running this command.
|
45
|
+
def skip!
|
46
|
+
@skip ||= true
|
47
|
+
end
|
48
|
+
|
49
|
+
# Boolean indicating whether this command will run.
|
50
|
+
def skip?
|
51
|
+
@skip
|
52
|
+
end
|
53
|
+
|
54
|
+
# Array of `executable` followed by all args suitable as arguments
|
55
|
+
# for `exec` or `system` calls.
|
56
|
+
def to_exec(args = self)
|
57
|
+
[executable].concat args
|
58
|
+
end
|
59
|
+
|
60
|
+
# All the words (as opposed to flags) contained in this argument
|
61
|
+
# list.
|
62
|
+
#
|
63
|
+
# args = Args.new([ 'remote', 'add', '-f', 'tekkub' ])
|
64
|
+
# args.words == [ 'remote', 'add', 'tekkub' ]
|
65
|
+
def words
|
66
|
+
reject { |arg| arg.index('-') == 0 }
|
67
|
+
end
|
68
|
+
|
69
|
+
# All the flags (as opposed to words) contained in this argument
|
70
|
+
# list.
|
71
|
+
#
|
72
|
+
# args = Args.new([ 'remote', 'add', '-f', 'tekkub' ])
|
73
|
+
# args.flags == [ '-f' ]
|
74
|
+
def flags
|
75
|
+
self - words
|
76
|
+
end
|
77
|
+
|
78
|
+
# Tests if arguments were modified since instantiation
|
79
|
+
def changed?
|
80
|
+
chained? or self != @original_args
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def normalize_callback(cmd_or_args, args, block)
|
86
|
+
if block
|
87
|
+
block
|
88
|
+
elsif args
|
89
|
+
[cmd_or_args].concat args
|
90
|
+
elsif Array === cmd_or_args
|
91
|
+
self.to_exec cmd_or_args
|
92
|
+
elsif cmd_or_args
|
93
|
+
cmd_or_args
|
94
|
+
else
|
95
|
+
raise ArgumentError, "command or block required"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/hub/commands.rb
ADDED
@@ -0,0 +1,718 @@
|
|
1
|
+
module Hub
|
2
|
+
# See context.rb
|
3
|
+
module Context; end
|
4
|
+
|
5
|
+
# The Commands module houses the git commands that hub
|
6
|
+
# lovingly wraps. If a method exists here, it is expected to have a
|
7
|
+
# corresponding git command which either gets run before or after
|
8
|
+
# the method executes.
|
9
|
+
#
|
10
|
+
# The typical flow is as follows:
|
11
|
+
#
|
12
|
+
# 1. hub is invoked from the command line:
|
13
|
+
# $ hub clone rtomayko/tilt
|
14
|
+
#
|
15
|
+
# 2. The Hub class is initialized:
|
16
|
+
# >> hub = Hub.new('clone', 'rtomayko/tilt')
|
17
|
+
#
|
18
|
+
# 3. The method representing the git subcommand is executed with the
|
19
|
+
# full args:
|
20
|
+
# >> Commands.clone('clone', 'rtomayko/tilt')
|
21
|
+
#
|
22
|
+
# 4. That method rewrites the args as it sees fit:
|
23
|
+
# >> args[1] = "git://github.com/" + args[1] + ".git"
|
24
|
+
# => "git://github.com/rtomayko/tilt.git"
|
25
|
+
#
|
26
|
+
# 5. The new args are used to run `git`:
|
27
|
+
# >> exec "git", "clone", "git://github.com/rtomayko/tilt.git"
|
28
|
+
#
|
29
|
+
# An optional `after` callback can be set. If so, it is run after
|
30
|
+
# step 5 (which then performs a `system` call rather than an
|
31
|
+
# `exec`). See `Hub::Args` for more information on the `after` callback.
|
32
|
+
module Commands
|
33
|
+
# We are a blank slate.
|
34
|
+
instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ }
|
35
|
+
extend self
|
36
|
+
|
37
|
+
# Provides `github_url` and various inspection methods
|
38
|
+
extend Context
|
39
|
+
|
40
|
+
API_REPO = 'http://github.com/api/v2/yaml/repos/show/%s/%s'
|
41
|
+
API_FORK = 'http://github.com/api/v2/yaml/repos/fork/%s/%s'
|
42
|
+
API_CREATE = 'http://github.com/api/v2/yaml/repos/create'
|
43
|
+
|
44
|
+
def run(args)
|
45
|
+
# Hack to emulate git-style
|
46
|
+
args.unshift 'help' if args.grep(/^[^-]|version|exec-path$|html-path/).empty?
|
47
|
+
|
48
|
+
cmd = args[0]
|
49
|
+
expanded_args = expand_alias(cmd)
|
50
|
+
cmd = expanded_args[0] if expanded_args
|
51
|
+
|
52
|
+
# git commands can have dashes
|
53
|
+
cmd = cmd.sub(/(\w)-/, '\1_')
|
54
|
+
if method_defined?(cmd) and cmd != 'run'
|
55
|
+
args[0, 1] = expanded_args if expanded_args
|
56
|
+
send(cmd, args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# $ hub clone rtomayko/tilt
|
61
|
+
# > git clone git://github.com/rtomayko/tilt.
|
62
|
+
#
|
63
|
+
# $ hub clone -p kneath/hemingway
|
64
|
+
# > git clone git@github.com:kneath/hemingway.git
|
65
|
+
#
|
66
|
+
# $ hub clone tilt
|
67
|
+
# > git clone git://github.com/YOUR_LOGIN/tilt.
|
68
|
+
#
|
69
|
+
# $ hub clone -p github
|
70
|
+
# > git clone git@github.com:YOUR_LOGIN/hemingway.git
|
71
|
+
def clone(args)
|
72
|
+
ssh = args.delete('-p')
|
73
|
+
has_values = /^(--(upload-pack|template|depth|origin|branch|reference)|-[ubo])$/
|
74
|
+
|
75
|
+
idx = 1
|
76
|
+
while idx < args.length
|
77
|
+
arg = args[idx]
|
78
|
+
if arg.index('-') == 0
|
79
|
+
idx += 1 if arg =~ has_values
|
80
|
+
elsif arg.index('://') or arg.index('@') or File.directory?(arg)
|
81
|
+
# Bail out early for URLs and local paths.
|
82
|
+
break
|
83
|
+
elsif arg.scan('/').size <= 1 && !arg.include?(':')
|
84
|
+
# $ hub clone rtomayko/tilt
|
85
|
+
# $ hub clone tilt
|
86
|
+
args[args.index(arg)] = github_url(:repo => arg, :private => ssh)
|
87
|
+
break
|
88
|
+
end
|
89
|
+
idx += 1
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# $ hub submodule add wycats/bundler vendor/bundler
|
94
|
+
# > git submodule add git://github.com/wycats/bundler.git vendor/bundler
|
95
|
+
#
|
96
|
+
# $ hub submodule add -p wycats/bundler vendor/bundler
|
97
|
+
# > git submodule add git@github.com:wycats/bundler.git vendor/bundler
|
98
|
+
#
|
99
|
+
# $ hub submodule add -b ryppl ryppl/pip vendor/bundler
|
100
|
+
# > git submodule add -b ryppl git://github.com/ryppl/pip.git vendor/pip
|
101
|
+
def submodule(args)
|
102
|
+
return unless index = args.index('add')
|
103
|
+
args.delete_at index
|
104
|
+
|
105
|
+
branch = args.index('-b') || args.index('--branch')
|
106
|
+
if branch
|
107
|
+
args.delete_at branch
|
108
|
+
branch_name = args.delete_at branch
|
109
|
+
end
|
110
|
+
|
111
|
+
clone(args)
|
112
|
+
|
113
|
+
if branch_name
|
114
|
+
args.insert branch, '-b', branch_name
|
115
|
+
end
|
116
|
+
args.insert index, 'add'
|
117
|
+
end
|
118
|
+
|
119
|
+
# $ hub remote add pjhyett
|
120
|
+
# > git remote add pjhyett git://github.com/pjhyett/THIS_REPO.git
|
121
|
+
#
|
122
|
+
# $ hub remote add -p mojombo
|
123
|
+
# > git remote add mojombo git@github.com:mojombo/THIS_REPO.git
|
124
|
+
#
|
125
|
+
# $ hub remote add origin
|
126
|
+
# > git remote add origin git://github.com/YOUR_LOGIN/THIS_REPO.git
|
127
|
+
def remote(args)
|
128
|
+
return unless ['add','set-url'].include?(args[1]) && args.last !~ %r{.+?://|.+?@|^[./]}
|
129
|
+
|
130
|
+
ssh = args.delete('-p')
|
131
|
+
|
132
|
+
# user/repo
|
133
|
+
args.last =~ /\b(.+?)(?:\/(.+))?$/
|
134
|
+
user, repo = $1, $2
|
135
|
+
|
136
|
+
if args.words[2] == 'origin' && args.words[3].nil?
|
137
|
+
# Origin special case triggers default user/repo
|
138
|
+
user = repo = nil
|
139
|
+
elsif args.words[-2] == args.words[1]
|
140
|
+
# rtomayko/tilt => rtomayko
|
141
|
+
# Make sure you dance around flags.
|
142
|
+
idx = args.index( args.words[-1] )
|
143
|
+
args[idx] = user
|
144
|
+
else
|
145
|
+
# They're specifying the remote name manually (e.g.
|
146
|
+
# git remote add blah rtomayko/tilt), so just drop the last
|
147
|
+
# argument.
|
148
|
+
args.replace args[0...-1]
|
149
|
+
end
|
150
|
+
|
151
|
+
args << github_url(:user => user, :repo => repo, :private => ssh)
|
152
|
+
end
|
153
|
+
|
154
|
+
# $ hub fetch mislav
|
155
|
+
# > git remote add mislav git://github.com/mislav/REPO.git
|
156
|
+
# > git fetch mislav
|
157
|
+
#
|
158
|
+
# $ hub fetch --multiple mislav xoebus
|
159
|
+
# > git remote add mislav ...
|
160
|
+
# > git remote add xoebus ...
|
161
|
+
# > git fetch --multiple mislav xoebus
|
162
|
+
def fetch(args)
|
163
|
+
# $ hub fetch --multiple <name1>, <name2>, ...
|
164
|
+
if args.include?('--multiple')
|
165
|
+
names = args.words[1..-1]
|
166
|
+
# $ hub fetch <name>
|
167
|
+
elsif remote_name = args.words[1]
|
168
|
+
# $ hub fetch <name1>,<name2>,...
|
169
|
+
if remote_name =~ /^\w+(,\w+)+$/
|
170
|
+
index = args.index(remote_name)
|
171
|
+
args.delete(remote_name)
|
172
|
+
names = remote_name.split(',')
|
173
|
+
args.insert(index, *names)
|
174
|
+
args.insert(index, '--multiple')
|
175
|
+
else
|
176
|
+
names = [remote_name]
|
177
|
+
end
|
178
|
+
else
|
179
|
+
names = []
|
180
|
+
end
|
181
|
+
|
182
|
+
names.reject! { |name|
|
183
|
+
name =~ /\W/ or remotes.include?(name) or
|
184
|
+
remotes_group(name) or not repo_exists?(name)
|
185
|
+
}
|
186
|
+
|
187
|
+
if names.any?
|
188
|
+
names.each do |name|
|
189
|
+
args.before ['remote', 'add', name, github_url(:user => name)]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# $ git cherry-pick http://github.com/mislav/hub/commit/a319d88#comments
|
195
|
+
# > git remote add -f mislav git://github.com/mislav/hub.git
|
196
|
+
# > git cherry-pick a319d88
|
197
|
+
#
|
198
|
+
# $ git cherry-pick mislav@a319d88
|
199
|
+
# > git remote add -f mislav git://github.com/mislav/hub.git
|
200
|
+
# > git cherry-pick a319d88
|
201
|
+
#
|
202
|
+
# $ git cherry-pick mislav@SHA
|
203
|
+
# > git fetch mislav
|
204
|
+
# > git cherry-pick SHA
|
205
|
+
def cherry_pick(args)
|
206
|
+
unless args.include?('-m') or args.include?('--mainline')
|
207
|
+
case ref = args.words.last
|
208
|
+
when %r{^(?:https?:)//github.com/(.+?)/(.+?)/commit/([a-f0-9]{7,40})}
|
209
|
+
user, repo, sha = $1, $2, $3
|
210
|
+
args[args.index(ref)] = sha
|
211
|
+
when /^(\w+)@([a-f1-9]{7,40})$/
|
212
|
+
user, repo, sha = $1, nil, $2
|
213
|
+
args[args.index(ref)] = sha
|
214
|
+
else
|
215
|
+
user = nil
|
216
|
+
end
|
217
|
+
|
218
|
+
if user
|
219
|
+
if user == repo_owner
|
220
|
+
# fetch from origin if the repo belongs to the user
|
221
|
+
args.before ['fetch', default_remote]
|
222
|
+
elsif remotes.include?(user)
|
223
|
+
args.before ['fetch', user]
|
224
|
+
else
|
225
|
+
remote_url = github_url(:user => user, :repo => repo, :private => false)
|
226
|
+
args.before ['remote', 'add', '-f', user, remote_url]
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# $ hub am https://github.com/defunkt/hub/pull/55
|
233
|
+
# > curl https://github.com/defunkt/hub/pull/55.patch -o /tmp/55.patch
|
234
|
+
# > git am /tmp/55.patch
|
235
|
+
def am(args)
|
236
|
+
if url = args.find { |a| a =~ %r{^https?://(gist\.)?github\.com/} }
|
237
|
+
idx = args.index(url)
|
238
|
+
gist = $1 == 'gist.'
|
239
|
+
# strip extra path from "pull/42/files", "pull/42/commits"
|
240
|
+
url = url.sub(%r{(/pull/\d+)/\w*$}, '\1') unless gist
|
241
|
+
ext = gist ? '.txt' : '.patch'
|
242
|
+
url += ext unless File.extname(url) == ext
|
243
|
+
patch_file = File.join(ENV['TMPDIR'], "#{gist ? 'gist-' : ''}#{File.basename(url)}")
|
244
|
+
args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file]
|
245
|
+
args[idx] = patch_file
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# $ hub init -g
|
250
|
+
# > git init
|
251
|
+
# > git remote add origin git@github.com:USER/REPO.git
|
252
|
+
def init(args)
|
253
|
+
if args.delete('-g')
|
254
|
+
url = github_url(:private => true, :repo => current_dirname)
|
255
|
+
args.after "git remote add origin #{url}"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# $ hub fork
|
260
|
+
# ... hardcore forking action ...
|
261
|
+
# > git remote add -f YOUR_USER git@github.com:YOUR_USER/CURRENT_REPO.git
|
262
|
+
def fork(args)
|
263
|
+
# can't do anything without token and original owner name
|
264
|
+
if github_user && github_token && repo_owner
|
265
|
+
if repo_exists?(github_user)
|
266
|
+
puts "#{github_user}/#{repo_name} already exists on GitHub"
|
267
|
+
else
|
268
|
+
fork_repo
|
269
|
+
end
|
270
|
+
|
271
|
+
if args.include?('--no-remote')
|
272
|
+
exit
|
273
|
+
else
|
274
|
+
url = github_url(:private => true)
|
275
|
+
args.replace %W"remote add -f #{github_user} #{url}"
|
276
|
+
args.after { puts "new remote: #{github_user}" }
|
277
|
+
end
|
278
|
+
end
|
279
|
+
rescue Net::HTTPExceptions
|
280
|
+
response = $!.response
|
281
|
+
warn "error creating fork: #{response.message} (HTTP #{response.code})"
|
282
|
+
exit 1
|
283
|
+
end
|
284
|
+
|
285
|
+
# $ hub create
|
286
|
+
# ... create repo on github ...
|
287
|
+
# > git remote add -f origin git@github.com:YOUR_USER/CURRENT_REPO.git
|
288
|
+
def create(args)
|
289
|
+
if !is_repo?
|
290
|
+
puts "'create' must be run from inside a git repository"
|
291
|
+
args.skip!
|
292
|
+
elsif github_user && github_token
|
293
|
+
args.shift
|
294
|
+
options = {}
|
295
|
+
options[:private] = true if args.delete('-p')
|
296
|
+
|
297
|
+
until args.empty?
|
298
|
+
case arg = args.shift
|
299
|
+
when '-d'
|
300
|
+
options[:description] = args.shift
|
301
|
+
when '-h'
|
302
|
+
options[:homepage] = args.shift
|
303
|
+
else
|
304
|
+
puts "unexpected argument: #{arg}"
|
305
|
+
return
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
if repo_exists?(github_user)
|
310
|
+
puts "#{github_user}/#{repo_name} already exists on GitHub"
|
311
|
+
action = "set remote origin"
|
312
|
+
else
|
313
|
+
action = "created repository"
|
314
|
+
create_repo(options)
|
315
|
+
end
|
316
|
+
|
317
|
+
url = github_url(:private => true)
|
318
|
+
|
319
|
+
if remotes.first != 'origin'
|
320
|
+
args.replace %W"remote add -f origin #{url}"
|
321
|
+
else
|
322
|
+
args.replace %W"remote -v"
|
323
|
+
end
|
324
|
+
|
325
|
+
args.after { puts "#{action}: #{github_user}/#{repo_name}" }
|
326
|
+
end
|
327
|
+
rescue Net::HTTPExceptions
|
328
|
+
response = $!.response
|
329
|
+
warn "error creating repository: #{response.message} (HTTP #{response.code})"
|
330
|
+
exit 1
|
331
|
+
end
|
332
|
+
|
333
|
+
# $ hub push origin,staging cool-feature
|
334
|
+
# > git push origin cool-feature
|
335
|
+
# > git push staging cool-feature
|
336
|
+
def push(args)
|
337
|
+
return unless args[1] =~ /,/
|
338
|
+
|
339
|
+
branch = args[2]
|
340
|
+
remotes = args[1].split(',')
|
341
|
+
args[1] = remotes.shift
|
342
|
+
|
343
|
+
remotes.each do |name|
|
344
|
+
args.after ['push', name, branch]
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# $ hub browse
|
349
|
+
# > open https://github.com/CURRENT_REPO
|
350
|
+
#
|
351
|
+
# $ hub browse -- issues
|
352
|
+
# > open https://github.com/CURRENT_REPO/issues
|
353
|
+
#
|
354
|
+
# $ hub browse pjhyett/github-services
|
355
|
+
# > open https://github.com/pjhyett/github-services
|
356
|
+
#
|
357
|
+
# $ hub browse github-services
|
358
|
+
# > open https://github.com/YOUR_LOGIN/github-services
|
359
|
+
#
|
360
|
+
# $ hub browse github-services wiki
|
361
|
+
# > open https://github.com/YOUR_LOGIN/github-services/wiki
|
362
|
+
def browse(args)
|
363
|
+
args.shift
|
364
|
+
browse_command(args) do
|
365
|
+
user = repo = nil
|
366
|
+
dest = args.shift
|
367
|
+
dest = nil if dest == '--'
|
368
|
+
|
369
|
+
if dest
|
370
|
+
# $ hub browse pjhyett/github-services
|
371
|
+
# $ hub browse github-services
|
372
|
+
repo = dest
|
373
|
+
elsif repo_user
|
374
|
+
# $ hub browse
|
375
|
+
user = repo_user
|
376
|
+
else
|
377
|
+
abort "Usage: hub browse [<USER>/]<REPOSITORY>"
|
378
|
+
end
|
379
|
+
|
380
|
+
params = { :user => user, :repo => repo }
|
381
|
+
|
382
|
+
# $ hub browse -- wiki
|
383
|
+
case subpage = args.shift
|
384
|
+
when 'commits'
|
385
|
+
branch = (!dest && tracked_branch) || 'master'
|
386
|
+
params[:web] = "/commits/#{branch}"
|
387
|
+
when 'tree', NilClass
|
388
|
+
branch = !dest && tracked_branch
|
389
|
+
params[:web] = "/tree/#{branch}" if branch && branch != 'master'
|
390
|
+
else
|
391
|
+
params[:web] = "/#{subpage}"
|
392
|
+
end
|
393
|
+
|
394
|
+
params
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# $ hub compare 1.0...fix
|
399
|
+
# > open https://github.com/CURRENT_REPO/compare/1.0...fix
|
400
|
+
# $ hub compare refactor
|
401
|
+
# > open https://github.com/CURRENT_REPO/compare/refactor
|
402
|
+
# $ hub compare myfork feature
|
403
|
+
# > open https://github.com/myfork/REPO/compare/feature
|
404
|
+
# $ hub compare -u 1.0...2.0
|
405
|
+
# "https://github.com/CURRENT_REPO/compare/1.0...2.0"
|
406
|
+
def compare(args)
|
407
|
+
args.shift
|
408
|
+
browse_command(args) do
|
409
|
+
if args.empty?
|
410
|
+
branch = tracked_branch
|
411
|
+
if branch && branch != 'master'
|
412
|
+
range, user = branch, repo_user
|
413
|
+
else
|
414
|
+
abort "Usage: hub compare [USER] [<START>...]<END>"
|
415
|
+
end
|
416
|
+
else
|
417
|
+
range = args.pop
|
418
|
+
user = args.pop || repo_user
|
419
|
+
end
|
420
|
+
{ :user => user, :web => "/compare/#{range}" }
|
421
|
+
end
|
422
|
+
end
|
423
|
+
|
424
|
+
# $ hub hub standalone
|
425
|
+
# Prints the "standalone" version of hub for an easy, memorable
|
426
|
+
# installation sequence:
|
427
|
+
#
|
428
|
+
# $ gem install hub
|
429
|
+
# $ hub hub standalone > ~/bin/hub && chmod 755 ~/bin/hub
|
430
|
+
# $ gem uninstall hub
|
431
|
+
def hub(args)
|
432
|
+
return help(args) unless args[1] == 'standalone'
|
433
|
+
require 'hub/standalone'
|
434
|
+
puts Hub::Standalone.build
|
435
|
+
exit
|
436
|
+
rescue LoadError
|
437
|
+
abort "hub is running in standalone mode."
|
438
|
+
end
|
439
|
+
|
440
|
+
def alias(args)
|
441
|
+
shells = {
|
442
|
+
'sh' => 'alias git=hub',
|
443
|
+
'bash' => 'alias git=hub',
|
444
|
+
'zsh' => 'function git(){hub "$@"}',
|
445
|
+
'csh' => 'alias git hub',
|
446
|
+
'fish' => 'alias git hub'
|
447
|
+
}
|
448
|
+
|
449
|
+
silent = args.delete('-s')
|
450
|
+
|
451
|
+
if shell = args[1]
|
452
|
+
if silent.nil?
|
453
|
+
puts "Run this in your shell to start using `hub` as `git`:"
|
454
|
+
print " "
|
455
|
+
end
|
456
|
+
else
|
457
|
+
puts "usage: hub alias [-s] SHELL", ""
|
458
|
+
puts "You already have hub installed and available in your PATH,"
|
459
|
+
puts "but to get the full experience you'll want to alias it to"
|
460
|
+
puts "`git`.", ""
|
461
|
+
puts "To see how to accomplish this for your shell, run the alias"
|
462
|
+
puts "command again with the name of your shell.", ""
|
463
|
+
puts "Known shells:"
|
464
|
+
shells.map { |key, _| key }.sort.each do |key|
|
465
|
+
puts " " + key
|
466
|
+
end
|
467
|
+
puts "", "Options:"
|
468
|
+
puts " -s Silent. Useful when using the output with eval, e.g."
|
469
|
+
puts " $ eval `hub alias -s bash`"
|
470
|
+
|
471
|
+
exit
|
472
|
+
end
|
473
|
+
|
474
|
+
if shells[shell]
|
475
|
+
puts shells[shell]
|
476
|
+
else
|
477
|
+
abort "fatal: never heard of `#{shell}'"
|
478
|
+
end
|
479
|
+
|
480
|
+
exit
|
481
|
+
end
|
482
|
+
|
483
|
+
# $ hub version
|
484
|
+
# > git version
|
485
|
+
# (print hub version)
|
486
|
+
def version(args)
|
487
|
+
args.after do
|
488
|
+
puts "hub version %s" % Version
|
489
|
+
end
|
490
|
+
end
|
491
|
+
alias_method "--version", :version
|
492
|
+
|
493
|
+
# $ hub help
|
494
|
+
# (print improved help text)
|
495
|
+
def help(args)
|
496
|
+
command = args.grep(/^[^-]/)[1]
|
497
|
+
|
498
|
+
if command == 'hub'
|
499
|
+
puts hub_manpage
|
500
|
+
exit
|
501
|
+
elsif command.nil? && args.grep(/^--?a/).empty?
|
502
|
+
ENV['GIT_PAGER'] = '' if args.grep(/^-{1,2}p/).empty? # Use `cat`.
|
503
|
+
puts improved_help_text
|
504
|
+
exit
|
505
|
+
end
|
506
|
+
end
|
507
|
+
alias_method "--help", :help
|
508
|
+
|
509
|
+
# The text print when `hub help` is run, kept in its own method
|
510
|
+
# for the convenience of the author.
|
511
|
+
def improved_help_text
|
512
|
+
<<-help
|
513
|
+
usage: git [--version] [--exec-path[=GIT_EXEC_PATH]] [--html-path]
|
514
|
+
[-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR]
|
515
|
+
[--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS]
|
516
|
+
|
517
|
+
Basic Commands:
|
518
|
+
init Create an empty git repository or reinitialize an existing one
|
519
|
+
add Add new or modified files to the staging area
|
520
|
+
rm Remove files from the working directory and staging area
|
521
|
+
mv Move or rename a file, a directory, or a symlink
|
522
|
+
status Show the status of the working directory and staging area
|
523
|
+
commit Record changes to the repository
|
524
|
+
|
525
|
+
History Commands:
|
526
|
+
log Show the commit history log
|
527
|
+
diff Show changes between commits, commit and working tree, etc
|
528
|
+
show Show information about commits, tags or files
|
529
|
+
|
530
|
+
Branching Commands:
|
531
|
+
branch List, create, or delete branches
|
532
|
+
checkout Switch the active branch to another branch
|
533
|
+
merge Join two or more development histories (branches) together
|
534
|
+
tag Create, list, delete, sign or verify a tag object
|
535
|
+
|
536
|
+
Remote Commands:
|
537
|
+
clone Clone a remote repository into a new directory
|
538
|
+
fetch Download data, tags and branches from a remote repository
|
539
|
+
pull Fetch from and merge with another repository or a local branch
|
540
|
+
push Upload data, tags and branches to a remote repository
|
541
|
+
remote View and manage a set of remote repositories
|
542
|
+
|
543
|
+
Advanced commands:
|
544
|
+
reset Reset your staging area or working directory to another point
|
545
|
+
rebase Re-apply a series of patches in one branch onto another
|
546
|
+
bisect Find by binary search the change that introduced a bug
|
547
|
+
grep Print files with lines matching a pattern in your codebase
|
548
|
+
|
549
|
+
See 'git help COMMAND' for more information on a specific command.
|
550
|
+
help
|
551
|
+
end
|
552
|
+
|
553
|
+
private
|
554
|
+
#
|
555
|
+
# Helper methods are private so they cannot be invoked
|
556
|
+
# from the command line.
|
557
|
+
#
|
558
|
+
|
559
|
+
# Checks whether a command exists on this system in the $PATH.
|
560
|
+
#
|
561
|
+
# name - The String name of the command to check for.
|
562
|
+
#
|
563
|
+
# Returns a Boolean.
|
564
|
+
def command?(name)
|
565
|
+
`which #{name} 2>/dev/null`
|
566
|
+
$?.success?
|
567
|
+
end
|
568
|
+
|
569
|
+
# Detects commands to launch the user's browser, checking $BROWSER
|
570
|
+
# first then falling back to a few common launchers. Aborts with
|
571
|
+
# an error if it can't find anything appropriate.
|
572
|
+
#
|
573
|
+
# Returns a launch command.
|
574
|
+
def browser_launcher
|
575
|
+
if ENV['BROWSER']
|
576
|
+
ENV['BROWSER']
|
577
|
+
elsif RUBY_PLATFORM.include?('darwin')
|
578
|
+
"open"
|
579
|
+
elsif command?("xdg-open")
|
580
|
+
"xdg-open"
|
581
|
+
elsif command?("cygstart")
|
582
|
+
"cygstart"
|
583
|
+
else
|
584
|
+
abort "Please set $BROWSER to a web launcher to use this command."
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# Handles common functionality of browser commands like `browse`
|
589
|
+
# and `compare`. Yields a block that returns params for `github_url`.
|
590
|
+
def browse_command(args)
|
591
|
+
url_only = args.delete('-u')
|
592
|
+
$stderr.puts "Warning: the `-p` flag has no effect anymore" if args.delete('-p')
|
593
|
+
params = yield
|
594
|
+
|
595
|
+
args.executable = url_only ? 'echo' : browser_launcher
|
596
|
+
args.push github_url({:web => true, :private => true}.update(params))
|
597
|
+
end
|
598
|
+
|
599
|
+
|
600
|
+
# Returns the terminal-formatted manpage, ready to be printed to
|
601
|
+
# the screen.
|
602
|
+
def hub_manpage
|
603
|
+
return "** Can't find groff(1)" unless command?('groff')
|
604
|
+
|
605
|
+
require 'open3'
|
606
|
+
out = nil
|
607
|
+
Open3.popen3(groff_command) do |stdin, stdout, _|
|
608
|
+
stdin.puts hub_raw_manpage
|
609
|
+
stdin.close
|
610
|
+
out = stdout.read.strip
|
611
|
+
end
|
612
|
+
out
|
613
|
+
end
|
614
|
+
|
615
|
+
# The groff command complete with crazy arguments we need to run
|
616
|
+
# in order to turn our raw roff (manpage markup) into something
|
617
|
+
# readable on the terminal.
|
618
|
+
def groff_command
|
619
|
+
"groff -Wall -mtty-char -mandoc -Tascii"
|
620
|
+
end
|
621
|
+
|
622
|
+
# Returns the raw hub manpage. If we're not running in standalone
|
623
|
+
# mode, it's a file sitting at the root under the `man`
|
624
|
+
# directory.
|
625
|
+
#
|
626
|
+
# If we are running in standalone mode the manpage will be
|
627
|
+
# included after the __END__ of the file so we can grab it using
|
628
|
+
# DATA.
|
629
|
+
def hub_raw_manpage
|
630
|
+
if File.exists? file = File.dirname(__FILE__) + '/../../man/hub.1'
|
631
|
+
File.read(file)
|
632
|
+
else
|
633
|
+
DATA.read
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
# All calls to `puts` in after hooks or commands are paged,
|
638
|
+
# git-style.
|
639
|
+
def puts(*args)
|
640
|
+
page_stdout
|
641
|
+
super
|
642
|
+
end
|
643
|
+
|
644
|
+
# http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
|
645
|
+
def page_stdout
|
646
|
+
return unless $stdout.tty?
|
647
|
+
|
648
|
+
read, write = IO.pipe
|
649
|
+
|
650
|
+
if Kernel.fork
|
651
|
+
# Parent process, become pager
|
652
|
+
$stdin.reopen(read)
|
653
|
+
read.close
|
654
|
+
write.close
|
655
|
+
|
656
|
+
# Don't page if the input is short enough
|
657
|
+
ENV['LESS'] = 'FSRX'
|
658
|
+
|
659
|
+
# Wait until we have input before we start the pager
|
660
|
+
Kernel.select [STDIN]
|
661
|
+
|
662
|
+
pager = ENV['GIT_PAGER'] ||
|
663
|
+
`git config --get-all core.pager`.split.first || ENV['PAGER'] ||
|
664
|
+
'less -isr'
|
665
|
+
|
666
|
+
pager = 'cat' if pager.empty?
|
667
|
+
|
668
|
+
exec pager rescue exec "/bin/sh", "-c", pager
|
669
|
+
else
|
670
|
+
# Child process
|
671
|
+
$stdout.reopen(write)
|
672
|
+
$stderr.reopen(write) if $stderr.tty?
|
673
|
+
read.close
|
674
|
+
write.close
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
# Determines whether a user has a fork of the current repo on GitHub.
|
679
|
+
def repo_exists?(user)
|
680
|
+
require 'net/http'
|
681
|
+
url = API_REPO % [user, repo_name]
|
682
|
+
Net::HTTPSuccess === Net::HTTP.get_response(URI(url))
|
683
|
+
end
|
684
|
+
|
685
|
+
# Forks the current repo using the GitHub API.
|
686
|
+
#
|
687
|
+
# Returns nothing.
|
688
|
+
def fork_repo
|
689
|
+
url = API_FORK % [repo_owner, repo_name]
|
690
|
+
response = Net::HTTP.post_form(URI(url), 'login' => github_user, 'token' => github_token)
|
691
|
+
response.error! unless Net::HTTPSuccess === response
|
692
|
+
end
|
693
|
+
|
694
|
+
# Creates a new repo using the GitHub API.
|
695
|
+
#
|
696
|
+
# Returns nothing.
|
697
|
+
def create_repo(options = {})
|
698
|
+
url = API_CREATE
|
699
|
+
params = {'login' => github_user, 'token' => github_token, 'name' => repo_name}
|
700
|
+
params['public'] = '0' if options[:private]
|
701
|
+
params['description'] = options[:description] if options[:description]
|
702
|
+
params['homepage'] = options[:homepage] if options[:homepage]
|
703
|
+
|
704
|
+
response = Net::HTTP.post_form(URI(url), params)
|
705
|
+
response.error! unless Net::HTTPSuccess === response
|
706
|
+
end
|
707
|
+
|
708
|
+
def expand_alias(cmd)
|
709
|
+
if expanded = git_alias_for(cmd)
|
710
|
+
if expanded.index('!') != 0
|
711
|
+
require 'shellwords' unless expanded.respond_to? :shellsplit
|
712
|
+
expanded.shellsplit
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
|
717
|
+
end
|
718
|
+
end
|