hubflow 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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