hubflow 1.7.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.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # hubflow(1)
4
+ # alias git=hubflow
5
+
6
+ require 'hub'
7
+ Hub::Runner.execute(*ARGV)
@@ -0,0 +1,5 @@
1
+ require 'hub/version'
2
+ require 'hub/args'
3
+ require 'hub/context'
4
+ require 'hub/commands'
5
+ require 'hub/runner'
@@ -0,0 +1,117 @@
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 = @noop = 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
+ # Mark that this command shouldn't really run.
55
+ def noop!
56
+ @noop = true
57
+ end
58
+
59
+ def noop?
60
+ @noop
61
+ end
62
+
63
+ # Array of `executable` followed by all args suitable as arguments
64
+ # for `exec` or `system` calls.
65
+ def to_exec(args = self)
66
+ Array(executable) + args
67
+ end
68
+
69
+ def add_exec_flags(flags)
70
+ self.executable = Array(executable).concat(flags)
71
+ end
72
+
73
+ # All the words (as opposed to flags) contained in this argument
74
+ # list.
75
+ #
76
+ # args = Args.new([ 'remote', 'add', '-f', 'tekkub' ])
77
+ # args.words == [ 'remote', 'add', 'tekkub' ]
78
+ def words
79
+ reject { |arg| arg.index('-') == 0 }
80
+ end
81
+
82
+ # All the flags (as opposed to words) contained in this argument
83
+ # list.
84
+ #
85
+ # args = Args.new([ 'remote', 'add', '-f', 'tekkub' ])
86
+ # args.flags == [ '-f' ]
87
+ def flags
88
+ self - words
89
+ end
90
+
91
+ # Tests if arguments were modified since instantiation
92
+ def changed?
93
+ chained? or self != @original_args
94
+ end
95
+
96
+ def has_flag?(*flags)
97
+ pattern = flags.flatten.map { |f| Regexp.escape(f) }.join('|')
98
+ !grep(/^#{pattern}(?:=|$)/).empty?
99
+ end
100
+
101
+ private
102
+
103
+ def normalize_callback(cmd_or_args, args, block)
104
+ if block
105
+ block
106
+ elsif args
107
+ [cmd_or_args].concat args
108
+ elsif Array === cmd_or_args
109
+ self.to_exec cmd_or_args
110
+ elsif cmd_or_args
111
+ cmd_or_args
112
+ else
113
+ raise ArgumentError, "command or block required"
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,977 @@
1
+ module Hub
2
+ # The Commands module houses the git commands that hub
3
+ # lovingly wraps. If a method exists here, it is expected to have a
4
+ # corresponding git command which either gets run before or after
5
+ # the method executes.
6
+ #
7
+ # The typical flow is as follows:
8
+ #
9
+ # 1. hub is invoked from the command line:
10
+ # $ hub clone rtomayko/tilt
11
+ #
12
+ # 2. The Hub class is initialized:
13
+ # >> hub = Hub.new('clone', 'rtomayko/tilt')
14
+ #
15
+ # 3. The method representing the git subcommand is executed with the
16
+ # full args:
17
+ # >> Commands.clone('clone', 'rtomayko/tilt')
18
+ #
19
+ # 4. That method rewrites the args as it sees fit:
20
+ # >> args[1] = "git://github.com/" + args[1] + ".git"
21
+ # => "git://github.com/rtomayko/tilt.git"
22
+ #
23
+ # 5. The new args are used to run `git`:
24
+ # >> exec "git", "clone", "git://github.com/rtomayko/tilt.git"
25
+ #
26
+ # An optional `after` callback can be set. If so, it is run after
27
+ # step 5 (which then performs a `system` call rather than an
28
+ # `exec`). See `Hub::Args` for more information on the `after` callback.
29
+ module Commands
30
+ # We are a blank slate.
31
+ instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ }
32
+ extend self
33
+
34
+ # provides git interrogation methods
35
+ extend Context
36
+
37
+ API_REPO = 'http://github.com/api/v2/yaml/repos/show/%s/%s'
38
+ API_FORK = 'https://github.com/api/v2/yaml/repos/fork/%s/%s'
39
+ API_CREATE = 'https://github.com/api/v2/yaml/repos/create'
40
+ API_PULL = 'http://github.com/api/v2/json/pulls/%s'
41
+ API_PULLREQUEST = 'https://github.com/api/v2/yaml/pulls/%s/%s'
42
+
43
+ NAME_RE = /[\w.-]+/
44
+ OWNER_RE = /[a-zA-Z0-9-]+/
45
+ NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/
46
+
47
+ def run(args)
48
+ slurp_global_flags(args)
49
+
50
+ # Hack to emulate git-style
51
+ args.unshift 'help' if args.empty?
52
+
53
+ cmd = args[0]
54
+ expanded_args = expand_alias(cmd)
55
+ cmd = expanded_args[0] if expanded_args
56
+
57
+ # git commands can have dashes
58
+ cmd = cmd.sub(/(\w)-/, '\1_')
59
+ if method_defined?(cmd) and cmd != 'run'
60
+ args[0, 1] = expanded_args if expanded_args
61
+ send(cmd, args)
62
+ end
63
+ rescue Errno::ENOENT
64
+ if $!.message.include? "No such file or directory - git"
65
+ abort "Error: `git` command not found"
66
+ else
67
+ raise
68
+ end
69
+ end
70
+
71
+ # $ hub pull-request
72
+ # $ hub pull-request "My humble contribution"
73
+ # $ hub pull-request -i 92
74
+ # $ hub pull-request https://github.com/rtomayko/tilt/issues/92
75
+ def pull_request(args)
76
+ args.shift
77
+ options = { }
78
+ force = explicit_owner = false
79
+ base_project = local_repo.main_project
80
+ head_project = local_repo.current_project
81
+
82
+ from_github_ref = lambda do |ref, context_project|
83
+ if ref.index(':')
84
+ owner, ref = ref.split(':', 2)
85
+ project = github_project(context_project.name, owner)
86
+ end
87
+ [project || context_project, ref]
88
+ end
89
+
90
+ while arg = args.shift
91
+ case arg
92
+ when '-f'
93
+ force = true
94
+ when '-b'
95
+ base_project, options[:base] = from_github_ref.call(args.shift, base_project)
96
+ when '-h'
97
+ head = args.shift
98
+ explicit_owner = !!head.index(':')
99
+ head_project, options[:head] = from_github_ref.call(head, head_project)
100
+ when '-i'
101
+ options[:issue] = args.shift
102
+ when %r{^https?://github.com/([^/]+/[^/]+)/issues/(\d+)}
103
+ options[:issue] = $2
104
+ base_project = github_project($1)
105
+ else
106
+ if !options[:title] then options[:title] = arg
107
+ else
108
+ abort "invalid argument: #{arg}"
109
+ end
110
+ end
111
+ end
112
+
113
+ options[:project] = base_project
114
+ options[:base] ||= master_branch.short_name
115
+
116
+ if tracked_branch = options[:head].nil? && current_branch.upstream
117
+ if base_project == head_project and tracked_branch.short_name == options[:base]
118
+ $stderr.puts "Aborted: head branch is the same as base (#{options[:base].inspect})"
119
+ warn "(use `-h <branch>` to specify an explicit pull request head)"
120
+ abort
121
+ end
122
+ end
123
+ options[:head] ||= (tracked_branch || current_branch).short_name
124
+
125
+ if head_project.owner != github_user and !tracked_branch and !explicit_owner
126
+ head_project = github_project(head_project.name, github_user)
127
+ end
128
+
129
+ remote_branch = "#{head_project.remote}/#{options[:head]}"
130
+ options[:head] = "#{head_project.owner}:#{options[:head]}"
131
+
132
+ if !force and tracked_branch and local_commits = git_command("rev-list --cherry #{remote_branch}...")
133
+ $stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{remote_branch}"
134
+ warn "(use `-f` to force submit a pull request anyway)"
135
+ abort
136
+ end
137
+
138
+ if args.noop?
139
+ puts "Would reqest a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
140
+ exit
141
+ end
142
+
143
+ unless options[:title] or options[:issue]
144
+ base_branch = "#{base_project.remote}/#{options[:base]}"
145
+ changes = git_command "log --no-color --pretty=medium --cherry %s...%s" %
146
+ [base_branch, remote_branch]
147
+
148
+ options[:title], options[:body] = pullrequest_editmsg(changes) { |msg|
149
+ msg.puts "# Requesting a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
150
+ msg.puts "#"
151
+ msg.puts "# Write a message for this pull request. The first block"
152
+ msg.puts "# of text is the title and the rest is description."
153
+ }
154
+ end
155
+
156
+ pull = create_pullrequest(options)
157
+
158
+ args.executable = 'echo'
159
+ args.replace [pull['html_url']]
160
+ rescue HTTPExceptions
161
+ display_http_exception("creating pull request", $!.response)
162
+ exit 1
163
+ end
164
+
165
+ # $ hub clone rtomayko/tilt
166
+ # > git clone git://github.com/rtomayko/tilt.
167
+ #
168
+ # $ hub clone -p kneath/hemingway
169
+ # > git clone git@github.com:kneath/hemingway.git
170
+ #
171
+ # $ hub clone tilt
172
+ # > git clone git://github.com/YOUR_LOGIN/tilt.
173
+ #
174
+ # $ hub clone -p github
175
+ # > git clone git@github.com:YOUR_LOGIN/hemingway.git
176
+ def clone(args)
177
+ ssh = args.delete('-p')
178
+ has_values = /^(--(upload-pack|template|depth|origin|branch|reference)|-[ubo])$/
179
+
180
+ idx = 1
181
+ while idx < args.length
182
+ arg = args[idx]
183
+ if arg.index('-') == 0
184
+ idx += 1 if arg =~ has_values
185
+ else
186
+ # $ hub clone rtomayko/tilt
187
+ # $ hub clone tilt
188
+ if arg =~ NAME_WITH_OWNER_RE
189
+ project = github_project(arg)
190
+ ssh ||= args[0] != 'submodule' && project.owner == github_user(false)
191
+ args[idx] = project.git_url(:private => ssh, :https => https_protocol?)
192
+ end
193
+ break
194
+ end
195
+ idx += 1
196
+ end
197
+ end
198
+
199
+ # $ hub submodule add wycats/bundler vendor/bundler
200
+ # > git submodule add git://github.com/wycats/bundler.git vendor/bundler
201
+ #
202
+ # $ hub submodule add -p wycats/bundler vendor/bundler
203
+ # > git submodule add git@github.com:wycats/bundler.git vendor/bundler
204
+ #
205
+ # $ hub submodule add -b ryppl ryppl/pip vendor/bundler
206
+ # > git submodule add -b ryppl git://github.com/ryppl/pip.git vendor/pip
207
+ def submodule(args)
208
+ return unless index = args.index('add')
209
+ args.delete_at index
210
+
211
+ branch = args.index('-b') || args.index('--branch')
212
+ if branch
213
+ args.delete_at branch
214
+ branch_name = args.delete_at branch
215
+ end
216
+
217
+ clone(args)
218
+
219
+ if branch_name
220
+ args.insert branch, '-b', branch_name
221
+ end
222
+ args.insert index, 'add'
223
+ end
224
+
225
+ # $ hub remote add pjhyett
226
+ # > git remote add pjhyett git://github.com/pjhyett/THIS_REPO.git
227
+ #
228
+ # $ hub remote add -p mojombo
229
+ # > git remote add mojombo git@github.com:mojombo/THIS_REPO.git
230
+ #
231
+ # $ hub remote add origin
232
+ # > git remote add origin git://github.com/YOUR_LOGIN/THIS_REPO.git
233
+ def remote(args)
234
+ if %w[add set-url].include?(args[1])
235
+ name = args.last
236
+ if name =~ /^(#{OWNER_RE})$/ || name =~ /^(#{OWNER_RE})\/(#{NAME_RE})$/
237
+ user, repo = $1, $2 || repo_name
238
+ end
239
+ end
240
+ return unless user # do not touch arguments
241
+
242
+ ssh = args.delete('-p')
243
+
244
+ if args.words[2] == 'origin' && args.words[3].nil?
245
+ # Origin special case triggers default user/repo
246
+ user, repo = github_user, repo_name
247
+ elsif args.words[-2] == args.words[1]
248
+ # rtomayko/tilt => rtomayko
249
+ # Make sure you dance around flags.
250
+ idx = args.index( args.words[-1] )
251
+ args[idx] = user
252
+ else
253
+ # They're specifying the remote name manually (e.g.
254
+ # git remote add blah rtomayko/tilt), so just drop the last
255
+ # argument.
256
+ args.pop
257
+ end
258
+
259
+ args << git_url(user, repo, :private => ssh)
260
+ end
261
+
262
+ # $ hub fetch mislav
263
+ # > git remote add mislav git://github.com/mislav/REPO.git
264
+ # > git fetch mislav
265
+ #
266
+ # $ hub fetch --multiple mislav xoebus
267
+ # > git remote add mislav ...
268
+ # > git remote add xoebus ...
269
+ # > git fetch --multiple mislav xoebus
270
+ def fetch(args)
271
+ # $ hub fetch --multiple <name1>, <name2>, ...
272
+ if args.include?('--multiple')
273
+ names = args.words[1..-1]
274
+ # $ hub fetch <name>
275
+ elsif remote_name = args.words[1]
276
+ # $ hub fetch <name1>,<name2>,...
277
+ if remote_name =~ /^\w+(,\w+)+$/
278
+ index = args.index(remote_name)
279
+ args.delete(remote_name)
280
+ names = remote_name.split(',')
281
+ args.insert(index, *names)
282
+ args.insert(index, '--multiple')
283
+ else
284
+ names = [remote_name]
285
+ end
286
+ else
287
+ names = []
288
+ end
289
+
290
+ names.reject! { |name|
291
+ name =~ /\W/ or remotes.include?(name) or
292
+ remotes_group(name) or not repo_exists?(name)
293
+ }
294
+
295
+ if names.any?
296
+ names.each do |name|
297
+ args.before ['remote', 'add', name, git_url(name)]
298
+ end
299
+ end
300
+ end
301
+
302
+ # $ git checkout https://github.com/defunkt/hub/pull/73
303
+ # > git remote add -f -t feature git://github:com/mislav/hub.git
304
+ # > git checkout -b mislav-feature mislav/feature
305
+ def checkout(args)
306
+ if (2..3) === args.length and args[1] =~ %r{https?://github.com/(.+?)/(.+?)/pull/(\d+)}
307
+ owner, repo, pull_id = $1, $2, $3
308
+
309
+ load_net_http
310
+ pull_body = Net::HTTP.get URI(API_PULL % File.join(owner, repo, pull_id))
311
+
312
+ user, branch = pull_body.match(/"label":\s*"(.+?)"/)[1].split(':', 2)
313
+ new_branch_name = args[2] || "#{user}-#{branch}"
314
+
315
+ if remotes.include? user
316
+ args.before ['remote', 'set-branches', '--add', user, branch]
317
+ args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"]
318
+ else
319
+ args.before ['remote', 'add', '-f', '-t', branch, user, github_project(repo, user).git_url]
320
+ end
321
+ args[1..-1] = ['-b', new_branch_name, "#{user}/#{branch}"]
322
+ end
323
+ end
324
+
325
+ # $ git cherry-pick http://github.com/mislav/hub/commit/a319d88#comments
326
+ # > git remote add -f mislav git://github.com/mislav/hub.git
327
+ # > git cherry-pick a319d88
328
+ #
329
+ # $ git cherry-pick mislav@a319d88
330
+ # > git remote add -f mislav git://github.com/mislav/hub.git
331
+ # > git cherry-pick a319d88
332
+ #
333
+ # $ git cherry-pick mislav@SHA
334
+ # > git fetch mislav
335
+ # > git cherry-pick SHA
336
+ def cherry_pick(args)
337
+ unless args.include?('-m') or args.include?('--mainline')
338
+ case ref = args.words.last
339
+ when %r{^(?:https?:)//github.com/(.+?)/(.+?)/commit/([a-f0-9]{7,40})}
340
+ user, repo, sha = $1, $2, $3
341
+ args[args.index(ref)] = sha
342
+ when /^(\w+)@([a-f0-9]{7,40})$/
343
+ user, repo, sha = $1, nil, $2
344
+ args[args.index(ref)] = sha
345
+ else
346
+ user = nil
347
+ end
348
+
349
+ if user
350
+ if user == repo_owner
351
+ # fetch from origin if the repo belongs to the user
352
+ args.before ['fetch', origin_remote]
353
+ elsif remotes.include?(user)
354
+ args.before ['fetch', user]
355
+ else
356
+ args.before ['remote', 'add', '-f', user, git_url(user, repo)]
357
+ end
358
+ end
359
+ end
360
+ end
361
+
362
+ # $ hub am https://github.com/defunkt/hub/pull/55
363
+ # > curl https://github.com/defunkt/hub/pull/55.patch -o /tmp/55.patch
364
+ # > git am /tmp/55.patch
365
+ def am(args)
366
+ if url = args.find { |a| a =~ %r{^https?://(gist\.)?github\.com/} }
367
+ idx = args.index(url)
368
+ gist = $1 == 'gist.'
369
+ # strip extra path from "pull/42/files", "pull/42/commits"
370
+ url = url.sub(%r{(/pull/\d+)/\w*$}, '\1') unless gist
371
+ ext = gist ? '.txt' : '.patch'
372
+ url += ext unless File.extname(url) == ext
373
+ patch_file = File.join(ENV['TMPDIR'] || '/tmp', "#{gist ? 'gist-' : ''}#{File.basename(url)}")
374
+ args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file]
375
+ args[idx] = patch_file
376
+ end
377
+ end
378
+
379
+ # $ hub apply https://github.com/defunkt/hub/pull/55
380
+ # > curl https://github.com/defunkt/hub/pull/55.patch -o /tmp/55.patch
381
+ # > git apply /tmp/55.patch
382
+ alias_method :apply, :am
383
+
384
+ # $ hub init -g
385
+ # > git init
386
+ # > git remote add origin git@github.com:USER/REPO.git
387
+ def init(args)
388
+ if args.delete('-g')
389
+ url = git_url(github_user, repo_name, :private => true)
390
+ args.after ['remote', 'add', 'origin', url]
391
+ end
392
+ end
393
+
394
+ # $ hub fork
395
+ # ... hardcore forking action ...
396
+ # > git remote add -f YOUR_USER git@github.com:YOUR_USER/CURRENT_REPO.git
397
+ def fork(args)
398
+ # can't do anything without token and original owner name
399
+ if github_user && github_token && repo_owner
400
+ if repo_exists?(github_user)
401
+ warn "#{github_user}/#{repo_name} already exists on GitHub"
402
+ else
403
+ fork_repo unless args.noop?
404
+ end
405
+
406
+ if args.include?('--no-remote')
407
+ exit
408
+ else
409
+ url = git_url(github_user, repo_name, :private => true)
410
+ args.replace %W"remote add -f #{github_user} #{url}"
411
+ args.after 'echo', ['new remote:', github_user]
412
+ end
413
+ end
414
+ rescue HTTPExceptions
415
+ display_http_exception("creating fork", $!.response)
416
+ exit 1
417
+ end
418
+
419
+ # $ hub create
420
+ # ... create repo on github ...
421
+ # > git remote add -f origin git@github.com:YOUR_USER/CURRENT_REPO.git
422
+ def create(args)
423
+ if !is_repo?
424
+ abort "'create' must be run from inside a git repository"
425
+ elsif owner = github_user and github_token
426
+ args.shift
427
+ options = {}
428
+ options[:private] = true if args.delete('-p')
429
+ new_repo_name = nil
430
+
431
+ until args.empty?
432
+ case arg = args.shift
433
+ when '-d'
434
+ options[:description] = args.shift
435
+ when '-h'
436
+ options[:homepage] = args.shift
437
+ else
438
+ if arg =~ /^[^-]/ and new_repo_name.nil?
439
+ new_repo_name = arg
440
+ owner, new_repo_name = new_repo_name.split('/', 2) if new_repo_name.index('/')
441
+ else
442
+ abort "invalid argument: #{arg}"
443
+ end
444
+ end
445
+ end
446
+ new_repo_name ||= repo_name
447
+ repo_with_owner = "#{owner}/#{new_repo_name}"
448
+
449
+ if repo_exists?(owner, new_repo_name)
450
+ warn "#{repo_with_owner} already exists on GitHub"
451
+ action = "set remote origin"
452
+ else
453
+ action = "created repository"
454
+ create_repo(repo_with_owner, options) unless args.noop?
455
+ end
456
+
457
+ url = git_url(owner, new_repo_name, :private => true)
458
+
459
+ if remotes.first != 'origin'
460
+ args.replace %W"remote add -f origin #{url}"
461
+ else
462
+ args.replace %W"remote -v"
463
+ end
464
+
465
+ args.after 'echo', ["#{action}:", repo_with_owner]
466
+ end
467
+ rescue HTTPExceptions
468
+ display_http_exception("creating repository", $!.response)
469
+ exit 1
470
+ end
471
+
472
+ # $ hub push origin,staging cool-feature
473
+ # > git push origin cool-feature
474
+ # > git push staging cool-feature
475
+ def push(args)
476
+ return if args[1].nil? || !args[1].index(',')
477
+
478
+ branch = (args[2] ||= current_branch.short_name)
479
+ remotes = args[1].split(',')
480
+ args[1] = remotes.shift
481
+
482
+ remotes.each do |name|
483
+ args.after ['push', name, branch]
484
+ end
485
+ end
486
+
487
+ # $ hub browse
488
+ # > open https://github.com/CURRENT_REPO
489
+ #
490
+ # $ hub browse -- issues
491
+ # > open https://github.com/CURRENT_REPO/issues
492
+ #
493
+ # $ hub browse pjhyett/github-services
494
+ # > open https://github.com/pjhyett/github-services
495
+ #
496
+ # $ hub browse github-services
497
+ # > open https://github.com/YOUR_LOGIN/github-services
498
+ #
499
+ # $ hub browse github-services wiki
500
+ # > open https://github.com/YOUR_LOGIN/github-services/wiki
501
+ def browse(args)
502
+ args.shift
503
+ browse_command(args) do
504
+ dest = args.shift
505
+ dest = nil if dest == '--'
506
+
507
+ if dest
508
+ # $ hub browse pjhyett/github-services
509
+ # $ hub browse github-services
510
+ project = github_project dest
511
+ else
512
+ # $ hub browse
513
+ project = current_project
514
+ end
515
+
516
+ abort "Usage: hub browse [<USER>/]<REPOSITORY>" unless project
517
+
518
+ # $ hub browse -- wiki
519
+ path = case subpage = args.shift
520
+ when 'commits'
521
+ branch = (!dest && current_branch.upstream) || master_branch
522
+ "/commits/#{branch.short_name}"
523
+ when 'tree', NilClass
524
+ branch = !dest && current_branch.upstream
525
+ "/tree/#{branch.short_name}" if branch and !branch.master?
526
+ else
527
+ "/#{subpage}"
528
+ end
529
+
530
+ project.web_url(path)
531
+ end
532
+ end
533
+
534
+ # $ hub compare 1.0..fix
535
+ # > open https://github.com/CURRENT_REPO/compare/1.0...fix
536
+ # $ hub compare refactor
537
+ # > open https://github.com/CURRENT_REPO/compare/refactor
538
+ # $ hub compare myfork feature
539
+ # > open https://github.com/myfork/REPO/compare/feature
540
+ # $ hub compare -u 1.0...2.0
541
+ # "https://github.com/CURRENT_REPO/compare/1.0...2.0"
542
+ def compare(args)
543
+ args.shift
544
+ browse_command(args) do
545
+ if args.empty?
546
+ branch = current_branch.upstream
547
+ if branch and not branch.master?
548
+ range = branch.short_name
549
+ project = current_project
550
+ else
551
+ abort "Usage: hub compare [USER] [<START>...]<END>"
552
+ end
553
+ else
554
+ sha_or_tag = /(\w{1,2}|\w[\w.-]+\w)/
555
+ # replaces two dots with three: "sha1...sha2"
556
+ range = args.pop.sub(/^#{sha_or_tag}\.\.#{sha_or_tag}$/, '\1...\2')
557
+ project = if owner = args.pop then github_project(nil, owner)
558
+ else current_project
559
+ end
560
+ end
561
+
562
+ project.web_url "/compare/#{range}"
563
+ end
564
+ end
565
+
566
+ # $ hub hub standalone
567
+ # Prints the "standalone" version of hub for an easy, memorable
568
+ # installation sequence:
569
+ #
570
+ # $ gem install hub
571
+ # $ hub hub standalone > ~/bin/hub && chmod 755 ~/bin/hub
572
+ # $ gem uninstall hub
573
+ def hub(args)
574
+ return help(args) unless args[1] == 'standalone'
575
+ require 'hub/standalone'
576
+ $stdout.puts Hub::Standalone.build
577
+ exit
578
+ rescue LoadError
579
+ abort "hub is running in standalone mode."
580
+ end
581
+
582
+ def alias(args)
583
+ shells = {
584
+ 'sh' => 'alias git=hub',
585
+ 'bash' => 'alias git=hub',
586
+ 'zsh' => 'function git(){hub "$@"}',
587
+ 'csh' => 'alias git hub',
588
+ 'fish' => 'alias git hub'
589
+ }
590
+
591
+ silent = args.delete('-s')
592
+
593
+ if shell = args[1]
594
+ if silent.nil?
595
+ puts "Run this in your shell to start using `hub` as `git`:"
596
+ print " "
597
+ end
598
+ else
599
+ puts "usage: hub alias [-s] SHELL", ""
600
+ puts "You already have hub installed and available in your PATH,"
601
+ puts "but to get the full experience you'll want to alias it to"
602
+ puts "`git`.", ""
603
+ puts "To see how to accomplish this for your shell, run the alias"
604
+ puts "command again with the name of your shell.", ""
605
+ puts "Known shells:"
606
+ shells.map { |key, _| key }.sort.each do |key|
607
+ puts " " + key
608
+ end
609
+ puts "", "Options:"
610
+ puts " -s Silent. Useful when using the output with eval, e.g."
611
+ puts " $ eval `hub alias -s bash`"
612
+
613
+ exit
614
+ end
615
+
616
+ if shells[shell]
617
+ puts shells[shell]
618
+ else
619
+ abort "fatal: never heard of `#{shell}'"
620
+ end
621
+
622
+ exit
623
+ end
624
+
625
+ # $ hub version
626
+ # > git version
627
+ # (print hub version)
628
+ def version(args)
629
+ args.after 'echo', ['hub version', Version]
630
+ end
631
+ alias_method "--version", :version
632
+
633
+ # $ hub help
634
+ # (print improved help text)
635
+ def help(args)
636
+ command = args.words[1]
637
+
638
+ if command == 'hub'
639
+ puts hub_manpage
640
+ exit
641
+ elsif command.nil? && !args.has_flag?('-a', '--all')
642
+ ENV['GIT_PAGER'] = '' unless args.has_flag?('-p', '--paginate') # Use `cat`.
643
+ puts improved_help_text
644
+ exit
645
+ end
646
+ end
647
+ alias_method "--help", :help
648
+
649
+ private
650
+ #
651
+ # Helper methods are private so they cannot be invoked
652
+ # from the command line.
653
+ #
654
+
655
+ # The text print when `hub help` is run, kept in its own method
656
+ # for the convenience of the author.
657
+ def improved_help_text
658
+ <<-help
659
+ usage: git [--version] [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
660
+ [-p|--paginate|--no-pager] [--no-replace-objects] [--bare]
661
+ [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
662
+ [-c name=value] [--help]
663
+ <command> [<args>]
664
+
665
+ Basic Commands:
666
+ init Create an empty git repository or reinitialize an existing one
667
+ add Add new or modified files to the staging area
668
+ rm Remove files from the working directory and staging area
669
+ mv Move or rename a file, a directory, or a symlink
670
+ status Show the status of the working directory and staging area
671
+ commit Record changes to the repository
672
+
673
+ History Commands:
674
+ log Show the commit history log
675
+ diff Show changes between commits, commit and working tree, etc
676
+ show Show information about commits, tags or files
677
+
678
+ Branching Commands:
679
+ branch List, create, or delete branches
680
+ checkout Switch the active branch to another branch
681
+ merge Join two or more development histories (branches) together
682
+ tag Create, list, delete, sign or verify a tag object
683
+
684
+ Remote Commands:
685
+ clone Clone a remote repository into a new directory
686
+ fetch Download data, tags and branches from a remote repository
687
+ pull Fetch from and merge with another repository or a local branch
688
+ push Upload data, tags and branches to a remote repository
689
+ remote View and manage a set of remote repositories
690
+
691
+ Advanced commands:
692
+ reset Reset your staging area or working directory to another point
693
+ rebase Re-apply a series of patches in one branch onto another
694
+ bisect Find by binary search the change that introduced a bug
695
+ grep Print files with lines matching a pattern in your codebase
696
+
697
+ See 'git help <command>' for more information on a specific command.
698
+ help
699
+ end
700
+
701
+ # Extract global flags from the front of the arguments list.
702
+ # Makes sure important ones are supplied for calls to subcommands.
703
+ #
704
+ # Known flags are:
705
+ # --version --exec-path=<path> --html-path
706
+ # -p|--paginate|--no-pager --no-replace-objects
707
+ # --bare --git-dir=<path> --work-tree=<path>
708
+ # -c name=value --help
709
+ #
710
+ # Special: `--version`, `--help` are replaced with "version" and "help".
711
+ # Ignored: `--exec-path`, `--html-path` are kept in args list untouched.
712
+ def slurp_global_flags(args)
713
+ flags = %w[ --noop -c -p --paginate --no-pager --no-replace-objects --bare --version --help ]
714
+ flags2 = %w[ --exec-path= --git-dir= --work-tree= ]
715
+
716
+ # flags that should be present in subcommands, too
717
+ globals = []
718
+ # flags that apply only to main command
719
+ locals = []
720
+
721
+ while args[0] && (flags.include?(args[0]) || flags2.any? {|f| args[0].index(f) == 0 })
722
+ flag = args.shift
723
+ case flag
724
+ when '--noop'
725
+ args.noop!
726
+ when '--version', '--help'
727
+ args.unshift flag.sub('--', '')
728
+ when '-c'
729
+ # slurp one additional argument
730
+ config_pair = args.shift
731
+ # add configuration to our local cache
732
+ key, value = config_pair.split('=', 2)
733
+ git_reader.stub_config_value(key, value)
734
+
735
+ globals << flag << config_pair
736
+ when '-p', '--paginate', '--no-pager'
737
+ locals << flag
738
+ else
739
+ globals << flag
740
+ end
741
+ end
742
+
743
+ git_reader.add_exec_flags(globals)
744
+ args.add_exec_flags(globals)
745
+ args.add_exec_flags(locals)
746
+ end
747
+
748
+ # Handles common functionality of browser commands like `browse`
749
+ # and `compare`. Yields a block that returns params for `github_url`.
750
+ def browse_command(args)
751
+ url_only = args.delete('-u')
752
+ warn "Warning: the `-p` flag has no effect anymore" if args.delete('-p')
753
+ url = yield
754
+
755
+ args.executable = url_only ? 'echo' : browser_launcher
756
+ args.push url
757
+ end
758
+
759
+ # Returns the terminal-formatted manpage, ready to be printed to
760
+ # the screen.
761
+ def hub_manpage
762
+ abort "** Can't find groff(1)" unless command?('groff')
763
+
764
+ require 'open3'
765
+ out = nil
766
+ Open3.popen3(groff_command) do |stdin, stdout, _|
767
+ stdin.puts hub_raw_manpage
768
+ stdin.close
769
+ out = stdout.read.strip
770
+ end
771
+ out
772
+ end
773
+
774
+ # The groff command complete with crazy arguments we need to run
775
+ # in order to turn our raw roff (manpage markup) into something
776
+ # readable on the terminal.
777
+ def groff_command
778
+ "groff -Wall -mtty-char -mandoc -Tascii"
779
+ end
780
+
781
+ # Returns the raw hub manpage. If we're not running in standalone
782
+ # mode, it's a file sitting at the root under the `man`
783
+ # directory.
784
+ #
785
+ # If we are running in standalone mode the manpage will be
786
+ # included after the __END__ of the file so we can grab it using
787
+ # DATA.
788
+ def hub_raw_manpage
789
+ if File.exists? file = File.dirname(__FILE__) + '/../../man/hub.1'
790
+ File.read(file)
791
+ else
792
+ DATA.read
793
+ end
794
+ end
795
+
796
+ # All calls to `puts` in after hooks or commands are paged,
797
+ # git-style.
798
+ def puts(*args)
799
+ page_stdout
800
+ super
801
+ end
802
+
803
+ # http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
804
+ def page_stdout
805
+ return if not $stdout.tty? or windows?
806
+
807
+ read, write = IO.pipe
808
+
809
+ if Kernel.fork
810
+ # Parent process, become pager
811
+ $stdin.reopen(read)
812
+ read.close
813
+ write.close
814
+
815
+ # Don't page if the input is short enough
816
+ ENV['LESS'] = 'FSRX'
817
+
818
+ # Wait until we have input before we start the pager
819
+ Kernel.select [STDIN]
820
+
821
+ pager = ENV['GIT_PAGER'] ||
822
+ `git config --get-all core.pager`.split.first || ENV['PAGER'] ||
823
+ 'less -isr'
824
+
825
+ pager = 'cat' if pager.empty?
826
+
827
+ exec pager rescue exec "/bin/sh", "-c", pager
828
+ else
829
+ # Child process
830
+ $stdout.reopen(write)
831
+ $stderr.reopen(write) if $stderr.tty?
832
+ read.close
833
+ write.close
834
+ end
835
+ end
836
+
837
+ # Determines whether a user has a fork of the current repo on GitHub.
838
+ def repo_exists?(user, repo = repo_name)
839
+ load_net_http
840
+ url = API_REPO % [user, repo]
841
+ Net::HTTPSuccess === Net::HTTP.get_response(URI(url))
842
+ end
843
+
844
+ # Forks the current repo using the GitHub API.
845
+ #
846
+ # Returns nothing.
847
+ def fork_repo
848
+ load_net_http
849
+ response = http_post API_FORK % [repo_owner, repo_name]
850
+ response.error! unless Net::HTTPSuccess === response
851
+ end
852
+
853
+ # Creates a new repo using the GitHub API.
854
+ #
855
+ # Returns nothing.
856
+ def create_repo(name, options = {})
857
+ params = {'name' => name.sub(/^#{github_user}\//, '')}
858
+ params['public'] = '0' if options[:private]
859
+ params['description'] = options[:description] if options[:description]
860
+ params['homepage'] = options[:homepage] if options[:homepage]
861
+
862
+ load_net_http
863
+ response = http_post(API_CREATE, params)
864
+ response.error! unless Net::HTTPSuccess === response
865
+ end
866
+
867
+ # Returns parsed data from the new pull request.
868
+ def create_pullrequest(options)
869
+ project = options.fetch(:project)
870
+ params = {
871
+ 'pull[base]' => options.fetch(:base),
872
+ 'pull[head]' => options.fetch(:head)
873
+ }
874
+ params['pull[issue]'] = options[:issue] if options[:issue]
875
+ params['pull[title]'] = options[:title] if options[:title]
876
+ params['pull[body]'] = options[:body] if options[:body]
877
+
878
+ load_net_http
879
+ response = http_post(API_PULLREQUEST % [project.owner, project.name], params)
880
+ response.error! unless Net::HTTPSuccess === response
881
+ # GitHub bug: although we request YAML, it returns JSON
882
+ if response['Content-type'].to_s.include? 'application/json'
883
+ { "html_url" => response.body.match(/"html_url":\s*"(.+?)"/)[1] }
884
+ else
885
+ require 'yaml'
886
+ YAML.load(response.body)['pull']
887
+ end
888
+ end
889
+
890
+ def pullrequest_editmsg(changes)
891
+ message_file = File.join(git_dir, 'PULLREQ_EDITMSG')
892
+ File.open(message_file, 'w') { |msg|
893
+ msg.puts
894
+ yield msg
895
+ if changes
896
+ msg.puts "#\n# Changes:\n#"
897
+ msg.puts changes.gsub(/^/, '# ').gsub(/ +$/, '')
898
+ end
899
+ }
900
+ edit_cmd = Array(git_editor).dup << message_file
901
+ system(*edit_cmd)
902
+ abort "can't open text editor for pull request message" unless $?.success?
903
+ title, body = read_editmsg(message_file)
904
+ abort "Aborting due to empty pull request title" unless title
905
+ [title, body]
906
+ end
907
+
908
+ def read_editmsg(file)
909
+ title, body = '', ''
910
+ File.open(file, 'r') { |msg|
911
+ msg.each_line do |line|
912
+ next if line.index('#') == 0
913
+ ((body.empty? and line =~ /\S/) ? title : body) << line
914
+ end
915
+ }
916
+ title.tr!("\n", ' ')
917
+ title.strip!
918
+ body.strip!
919
+
920
+ [title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil]
921
+ end
922
+
923
+ def expand_alias(cmd)
924
+ if expanded = git_alias_for(cmd)
925
+ if expanded.index('!') != 0
926
+ require 'shellwords' unless defined?(::Shellwords)
927
+ Shellwords.shellwords(expanded)
928
+ end
929
+ end
930
+ end
931
+
932
+ def http_post(url, params = nil)
933
+ url = URI(url)
934
+ post = Net::HTTP::Post.new(url.request_uri)
935
+ post.basic_auth "#{github_user}/token", github_token
936
+ post.set_form_data params if params
937
+
938
+ port = url.port
939
+ if use_ssl = 'https' == url.scheme and not use_ssl?
940
+ # ruby compiled without openssl
941
+ use_ssl = false
942
+ port = 80
943
+ end
944
+
945
+ http = Net::HTTP.new(url.host, port)
946
+ if http.use_ssl = use_ssl
947
+ # TODO: SSL peer verification
948
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
949
+ end
950
+ http.start { http.request(post) }
951
+ end
952
+
953
+ def load_net_http
954
+ require 'net/https'
955
+ rescue LoadError
956
+ require 'net/http'
957
+ end
958
+
959
+ def use_ssl?
960
+ defined? ::OpenSSL
961
+ end
962
+
963
+ # Fake exception type for net/http exception handling.
964
+ # Necessary because net/http may or may not be loaded at the time.
965
+ module HTTPExceptions
966
+ def self.===(exception)
967
+ exception.class.ancestors.map {|a| a.to_s }.include? 'Net::HTTPExceptions'
968
+ end
969
+ end
970
+
971
+ def display_http_exception(action, response)
972
+ $stderr.puts "Error #{action}: #{response.message} (HTTP #{response.code})"
973
+ warn "Check your token configuration (`git config github.token`)" if response.code.to_i == 401
974
+ end
975
+
976
+ end
977
+ end