hub 1.6.1 → 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.

Potentially problematic release.


This version of hub might be problematic. Click here for more details.

@@ -1,7 +1,4 @@
1
1
  module Hub
2
- # See context.rb
3
- module Context; end
4
-
5
2
  # The Commands module houses the git commands that hub
6
3
  # lovingly wraps. If a method exists here, it is expected to have a
7
4
  # corresponding git command which either gets run before or after
@@ -34,12 +31,16 @@ module Hub
34
31
  instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ }
35
32
  extend self
36
33
 
37
- # Provides `github_url` and various inspection methods
34
+ # provides git interrogation methods
38
35
  extend Context
39
36
 
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'
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_WITH_OWNER_RE = /^([\w-]+)(?:\/([\w-]+))?$/
43
44
 
44
45
  def run(args)
45
46
  slurp_global_flags(args)
@@ -57,6 +58,106 @@ module Hub
57
58
  args[0, 1] = expanded_args if expanded_args
58
59
  send(cmd, args)
59
60
  end
61
+ rescue Errno::ENOENT
62
+ if $!.message.include? "No such file or directory - git"
63
+ abort "Error: `git` command not found"
64
+ else
65
+ raise
66
+ end
67
+ end
68
+
69
+ # $ hub pull-request
70
+ # $ hub pull-request "My humble contribution"
71
+ # $ hub pull-request -i 92
72
+ # $ hub pull-request https://github.com/rtomayko/tilt/issues/92
73
+ def pull_request(args)
74
+ args.shift
75
+ options = { }
76
+ force = explicit_owner = false
77
+ base_project = local_repo.main_project
78
+ head_project = local_repo.current_project
79
+
80
+ from_github_ref = lambda do |ref, context_project|
81
+ if ref.index(':')
82
+ owner, ref = ref.split(':', 2)
83
+ project = github_project(context_project.name, owner)
84
+ end
85
+ [project || context_project, ref]
86
+ end
87
+
88
+ while arg = args.shift
89
+ case arg
90
+ when '-f'
91
+ force = true
92
+ when '-b'
93
+ base_project, options[:base] = from_github_ref.call(args.shift, base_project)
94
+ when '-h'
95
+ head = args.shift
96
+ explicit_owner = !!head.index(':')
97
+ head_project, options[:head] = from_github_ref.call(head, head_project)
98
+ when '-i'
99
+ options[:issue] = args.shift
100
+ when %r{^https?://github.com/([^/]+/[^/]+)/issues/(\d+)}
101
+ options[:issue] = $2
102
+ base_project = github_project($1)
103
+ else
104
+ if !options[:title] then options[:title] = arg
105
+ else
106
+ abort "invalid argument: #{arg}"
107
+ end
108
+ end
109
+ end
110
+
111
+ options[:project] = base_project
112
+ options[:base] ||= master_branch.short_name
113
+
114
+ if tracked_branch = options[:head].nil? && current_branch.upstream
115
+ if base_project == head_project and tracked_branch.short_name == options[:base]
116
+ $stderr.puts "Aborted: head branch is the same as base (#{options[:base].inspect})"
117
+ warn "(use `-h <branch>` to specify an explicit pull request head)"
118
+ abort
119
+ end
120
+ end
121
+ options[:head] ||= (tracked_branch || current_branch).short_name
122
+
123
+ if head_project.owner != github_user and !tracked_branch and !explicit_owner
124
+ head_project = github_project(head_project.name, github_user)
125
+ end
126
+
127
+ remote_branch = "#{head_project.remote}/#{options[:head]}"
128
+ options[:head] = "#{head_project.owner}:#{options[:head]}"
129
+
130
+ if !force and tracked_branch and local_commits = git_command("rev-list --cherry #{remote_branch}...")
131
+ $stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{remote_branch}"
132
+ warn "(use `-f` to force submit a pull request anyway)"
133
+ abort
134
+ end
135
+
136
+ if args.noop?
137
+ puts "Would reqest a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
138
+ exit
139
+ end
140
+
141
+ unless options[:title] or options[:issue]
142
+ base_branch = "#{base_project.remote}/#{options[:base]}"
143
+ changes = git_command "log --no-color --pretty=medium --cherry %s...%s" %
144
+ [base_branch, remote_branch]
145
+
146
+ options[:title], options[:body] = pullrequest_editmsg(changes) { |msg|
147
+ msg.puts "# Requesting a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
148
+ msg.puts "#"
149
+ msg.puts "# Write a message for this pull request. The first block"
150
+ msg.puts "# of text is the title and the rest is description."
151
+ }
152
+ end
153
+
154
+ pull = create_pullrequest(options)
155
+
156
+ args.executable = 'echo'
157
+ args.replace [pull['html_url']]
158
+ rescue HTTPExceptions
159
+ display_http_exception("creating pull request", $!.response)
160
+ exit 1
60
161
  end
61
162
 
62
163
  # $ hub clone rtomayko/tilt
@@ -79,13 +180,14 @@ module Hub
79
180
  arg = args[idx]
80
181
  if arg.index('-') == 0
81
182
  idx += 1 if arg =~ has_values
82
- elsif arg.index('://') or arg.index('@') or File.directory?(arg)
83
- # Bail out early for URLs and local paths.
84
- break
85
- elsif arg.scan('/').size <= 1 && !arg.include?(':')
183
+ else
86
184
  # $ hub clone rtomayko/tilt
87
185
  # $ hub clone tilt
88
- args[args.index(arg)] = github_url(:repo => arg, :private => ssh)
186
+ if arg =~ NAME_WITH_OWNER_RE
187
+ project = github_project(arg)
188
+ ssh ||= args[0] != 'submodule' && project.owner == github_user(false)
189
+ args[idx] = project.git_url(:private => ssh, :https => https_protocol?)
190
+ end
89
191
  break
90
192
  end
91
193
  idx += 1
@@ -127,17 +229,17 @@ module Hub
127
229
  # $ hub remote add origin
128
230
  # > git remote add origin git://github.com/YOUR_LOGIN/THIS_REPO.git
129
231
  def remote(args)
130
- return unless ['add','set-url'].include?(args[1]) && args.last !~ %r{.+?://|.+?@|^[./]}
232
+ if %w[add set-url].include?(args[1]) && args.last =~ NAME_WITH_OWNER_RE
233
+ user, repo = $1, $2 || repo_name
234
+ else
235
+ return # do not touch arguments
236
+ end
131
237
 
132
238
  ssh = args.delete('-p')
133
239
 
134
- # user/repo
135
- args.last =~ /\b(.+?)(?:\/(.+))?$/
136
- user, repo = $1, $2
137
-
138
240
  if args.words[2] == 'origin' && args.words[3].nil?
139
241
  # Origin special case triggers default user/repo
140
- user = repo = nil
242
+ user, repo = github_user, repo_name
141
243
  elsif args.words[-2] == args.words[1]
142
244
  # rtomayko/tilt => rtomayko
143
245
  # Make sure you dance around flags.
@@ -147,10 +249,10 @@ module Hub
147
249
  # They're specifying the remote name manually (e.g.
148
250
  # git remote add blah rtomayko/tilt), so just drop the last
149
251
  # argument.
150
- args.replace args[0...-1]
252
+ args.pop
151
253
  end
152
254
 
153
- args << github_url(:user => user, :repo => repo, :private => ssh)
255
+ args << git_url(user, repo, :private => ssh)
154
256
  end
155
257
 
156
258
  # $ hub fetch mislav
@@ -188,8 +290,31 @@ module Hub
188
290
 
189
291
  if names.any?
190
292
  names.each do |name|
191
- args.before ['remote', 'add', name, github_url(:user => name)]
293
+ args.before ['remote', 'add', name, git_url(name)]
294
+ end
295
+ end
296
+ end
297
+
298
+ # $ git checkout https://github.com/defunkt/hub/pull/73
299
+ # > git remote add -f -t feature git://github:com/mislav/hub.git
300
+ # > git checkout -b mislav-feature mislav/feature
301
+ def checkout(args)
302
+ if (2..3) === args.length and args[1] =~ %r{https?://github.com/(.+?)/(.+?)/pull/(\d+)}
303
+ owner, repo, pull_id = $1, $2, $3
304
+
305
+ load_net_http
306
+ pull_body = Net::HTTP.get URI(API_PULL % File.join(owner, repo, pull_id))
307
+
308
+ user, branch = pull_body.match(/"label":\s*"(.+?)"/)[1].split(':', 2)
309
+ new_branch_name = args[2] || "#{user}-#{branch}"
310
+
311
+ if remotes.include? user
312
+ args.before ['remote', 'set-branches', '--add', user, branch]
313
+ args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"]
314
+ else
315
+ args.before ['remote', 'add', '-f', '-t', branch, user, github_project(repo, user).git_url]
192
316
  end
317
+ args[1..-1] = ['-b', new_branch_name, "#{user}/#{branch}"]
193
318
  end
194
319
  end
195
320
 
@@ -210,7 +335,7 @@ module Hub
210
335
  when %r{^(?:https?:)//github.com/(.+?)/(.+?)/commit/([a-f0-9]{7,40})}
211
336
  user, repo, sha = $1, $2, $3
212
337
  args[args.index(ref)] = sha
213
- when /^(\w+)@([a-f1-9]{7,40})$/
338
+ when /^(\w+)@([a-f0-9]{7,40})$/
214
339
  user, repo, sha = $1, nil, $2
215
340
  args[args.index(ref)] = sha
216
341
  else
@@ -220,12 +345,11 @@ module Hub
220
345
  if user
221
346
  if user == repo_owner
222
347
  # fetch from origin if the repo belongs to the user
223
- args.before ['fetch', default_remote]
348
+ args.before ['fetch', origin_remote]
224
349
  elsif remotes.include?(user)
225
350
  args.before ['fetch', user]
226
351
  else
227
- remote_url = github_url(:user => user, :repo => repo, :private => false)
228
- args.before ['remote', 'add', '-f', user, remote_url]
352
+ args.before ['remote', 'add', '-f', user, git_url(user, repo)]
229
353
  end
230
354
  end
231
355
  end
@@ -248,13 +372,18 @@ module Hub
248
372
  end
249
373
  end
250
374
 
375
+ # $ hub apply https://github.com/defunkt/hub/pull/55
376
+ # > curl https://github.com/defunkt/hub/pull/55.patch -o /tmp/55.patch
377
+ # > git apply /tmp/55.patch
378
+ alias_method :apply, :am
379
+
251
380
  # $ hub init -g
252
381
  # > git init
253
382
  # > git remote add origin git@github.com:USER/REPO.git
254
383
  def init(args)
255
384
  if args.delete('-g')
256
- url = github_url(:private => true, :repo => current_dirname)
257
- args.after "git remote add origin #{url}"
385
+ url = git_url(github_user, repo_name, :private => true)
386
+ args.after ['remote', 'add', 'origin', url]
258
387
  end
259
388
  end
260
389
 
@@ -265,22 +394,21 @@ module Hub
265
394
  # can't do anything without token and original owner name
266
395
  if github_user && github_token && repo_owner
267
396
  if repo_exists?(github_user)
268
- puts "#{github_user}/#{repo_name} already exists on GitHub"
397
+ warn "#{github_user}/#{repo_name} already exists on GitHub"
269
398
  else
270
- fork_repo
399
+ fork_repo unless args.noop?
271
400
  end
272
401
 
273
402
  if args.include?('--no-remote')
274
403
  exit
275
404
  else
276
- url = github_url(:private => true)
405
+ url = git_url(github_user, repo_name, :private => true)
277
406
  args.replace %W"remote add -f #{github_user} #{url}"
278
- args.after { puts "new remote: #{github_user}" }
407
+ args.after 'echo', ['new remote:', github_user]
279
408
  end
280
409
  end
281
- rescue Net::HTTPExceptions
282
- response = $!.response
283
- warn "error creating fork: #{response.message} (HTTP #{response.code})"
410
+ rescue HTTPExceptions
411
+ display_http_exception("creating fork", $!.response)
284
412
  exit 1
285
413
  end
286
414
 
@@ -289,12 +417,12 @@ module Hub
289
417
  # > git remote add -f origin git@github.com:YOUR_USER/CURRENT_REPO.git
290
418
  def create(args)
291
419
  if !is_repo?
292
- puts "'create' must be run from inside a git repository"
293
- args.skip!
294
- elsif github_user && github_token
420
+ abort "'create' must be run from inside a git repository"
421
+ elsif owner = github_user and github_token
295
422
  args.shift
296
423
  options = {}
297
424
  options[:private] = true if args.delete('-p')
425
+ new_repo_name = nil
298
426
 
299
427
  until args.empty?
300
428
  case arg = args.shift
@@ -303,20 +431,26 @@ module Hub
303
431
  when '-h'
304
432
  options[:homepage] = args.shift
305
433
  else
306
- puts "unexpected argument: #{arg}"
307
- return
434
+ if arg =~ /^[^-]/ and new_repo_name.nil?
435
+ new_repo_name = arg
436
+ owner, new_repo_name = new_repo_name.split('/', 2) if new_repo_name.index('/')
437
+ else
438
+ abort "invalid argument: #{arg}"
439
+ end
308
440
  end
309
441
  end
442
+ new_repo_name ||= repo_name
443
+ repo_with_owner = "#{owner}/#{new_repo_name}"
310
444
 
311
- if repo_exists?(github_user)
312
- puts "#{github_user}/#{repo_name} already exists on GitHub"
445
+ if repo_exists?(owner, new_repo_name)
446
+ warn "#{repo_with_owner} already exists on GitHub"
313
447
  action = "set remote origin"
314
448
  else
315
449
  action = "created repository"
316
- create_repo(options)
450
+ create_repo(repo_with_owner, options) unless args.noop?
317
451
  end
318
452
 
319
- url = github_url(:private => true)
453
+ url = git_url(owner, new_repo_name, :private => true)
320
454
 
321
455
  if remotes.first != 'origin'
322
456
  args.replace %W"remote add -f origin #{url}"
@@ -324,11 +458,10 @@ module Hub
324
458
  args.replace %W"remote -v"
325
459
  end
326
460
 
327
- args.after { puts "#{action}: #{github_user}/#{repo_name}" }
461
+ args.after 'echo', ["#{action}:", repo_with_owner]
328
462
  end
329
- rescue Net::HTTPExceptions
330
- response = $!.response
331
- warn "error creating repository: #{response.message} (HTTP #{response.code})"
463
+ rescue HTTPExceptions
464
+ display_http_exception("creating repository", $!.response)
332
465
  exit 1
333
466
  end
334
467
 
@@ -338,7 +471,7 @@ module Hub
338
471
  def push(args)
339
472
  return if args[1].nil? || !args[1].index(',')
340
473
 
341
- branch = (args[2] ||= normalize_branch(current_branch))
474
+ branch = (args[2] ||= current_branch.short_name)
342
475
  remotes = args[1].split(',')
343
476
  args[1] = remotes.shift
344
477
 
@@ -364,40 +497,37 @@ module Hub
364
497
  def browse(args)
365
498
  args.shift
366
499
  browse_command(args) do
367
- user = repo = nil
368
500
  dest = args.shift
369
501
  dest = nil if dest == '--'
370
502
 
371
503
  if dest
372
504
  # $ hub browse pjhyett/github-services
373
505
  # $ hub browse github-services
374
- repo = dest
375
- elsif repo_user
376
- # $ hub browse
377
- user = repo_user
506
+ project = github_project dest
378
507
  else
379
- abort "Usage: hub browse [<USER>/]<REPOSITORY>"
508
+ # $ hub browse
509
+ project = current_project
380
510
  end
381
511
 
382
- params = { :user => user, :repo => repo }
512
+ abort "Usage: hub browse [<USER>/]<REPOSITORY>" unless project
383
513
 
384
514
  # $ hub browse -- wiki
385
- case subpage = args.shift
515
+ path = case subpage = args.shift
386
516
  when 'commits'
387
- branch = (!dest && tracked_branch) || 'master'
388
- params[:web] = "/commits/#{branch}"
517
+ branch = (!dest && current_branch.upstream) || master_branch
518
+ "/commits/#{branch.short_name}"
389
519
  when 'tree', NilClass
390
- branch = !dest && tracked_branch
391
- params[:web] = "/tree/#{branch}" if branch && branch != 'master'
520
+ branch = !dest && current_branch.upstream
521
+ "/tree/#{branch.short_name}" if branch and !branch.master?
392
522
  else
393
- params[:web] = "/#{subpage}"
523
+ "/#{subpage}"
394
524
  end
395
525
 
396
- params
526
+ project.web_url(path)
397
527
  end
398
528
  end
399
529
 
400
- # $ hub compare 1.0...fix
530
+ # $ hub compare 1.0..fix
401
531
  # > open https://github.com/CURRENT_REPO/compare/1.0...fix
402
532
  # $ hub compare refactor
403
533
  # > open https://github.com/CURRENT_REPO/compare/refactor
@@ -409,17 +539,23 @@ module Hub
409
539
  args.shift
410
540
  browse_command(args) do
411
541
  if args.empty?
412
- branch = tracked_branch
413
- if branch && branch != 'master'
414
- range, user = branch, repo_user
542
+ branch = current_branch.upstream
543
+ if branch and not branch.master?
544
+ range = branch.short_name
545
+ project = current_project
415
546
  else
416
547
  abort "Usage: hub compare [USER] [<START>...]<END>"
417
548
  end
418
549
  else
419
- range = args.pop
420
- user = args.pop || repo_user
550
+ sha_or_tag = /(\w{1,2}|\w[\w.-]+\w)/
551
+ # replaces two dots with three: "sha1...sha2"
552
+ range = args.pop.sub(/^#{sha_or_tag}\.\.#{sha_or_tag}$/, '\1...\2')
553
+ project = if owner = args.pop then github_project(nil, owner)
554
+ else current_project
555
+ end
421
556
  end
422
- { :user => user, :web => "/compare/#{range}" }
557
+
558
+ project.web_url "/compare/#{range}"
423
559
  end
424
560
  end
425
561
 
@@ -433,7 +569,7 @@ module Hub
433
569
  def hub(args)
434
570
  return help(args) unless args[1] == 'standalone'
435
571
  require 'hub/standalone'
436
- puts Hub::Standalone.build
572
+ $stdout.puts Hub::Standalone.build
437
573
  exit
438
574
  rescue LoadError
439
575
  abort "hub is running in standalone mode."
@@ -486,9 +622,7 @@ module Hub
486
622
  # > git version
487
623
  # (print hub version)
488
624
  def version(args)
489
- args.after do
490
- puts "hub version %s" % Version
491
- end
625
+ args.after 'echo', ['hub version', Version]
492
626
  end
493
627
  alias_method "--version", :version
494
628
 
@@ -508,13 +642,21 @@ module Hub
508
642
  end
509
643
  alias_method "--help", :help
510
644
 
645
+ private
646
+ #
647
+ # Helper methods are private so they cannot be invoked
648
+ # from the command line.
649
+ #
650
+
511
651
  # The text print when `hub help` is run, kept in its own method
512
652
  # for the convenience of the author.
513
653
  def improved_help_text
514
654
  <<-help
515
- usage: git [--version] [--exec-path[=GIT_EXEC_PATH]] [--html-path]
516
- [-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR]
517
- [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS]
655
+ usage: git [--version] [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
656
+ [-p|--paginate|--no-pager] [--no-replace-objects] [--bare]
657
+ [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
658
+ [-c name=value] [--help]
659
+ <command> [<args>]
518
660
 
519
661
  Basic Commands:
520
662
  init Create an empty git repository or reinitialize an existing one
@@ -548,16 +690,10 @@ Advanced commands:
548
690
  bisect Find by binary search the change that introduced a bug
549
691
  grep Print files with lines matching a pattern in your codebase
550
692
 
551
- See 'git help COMMAND' for more information on a specific command.
693
+ See 'git help <command>' for more information on a specific command.
552
694
  help
553
695
  end
554
696
 
555
- private
556
- #
557
- # Helper methods are private so they cannot be invoked
558
- # from the command line.
559
- #
560
-
561
697
  # Extract global flags from the front of the arguments list.
562
698
  # Makes sure important ones are supplied for calls to subcommands.
563
699
  #
@@ -570,7 +706,7 @@ help
570
706
  # Special: `--version`, `--help` are replaced with "version" and "help".
571
707
  # Ignored: `--exec-path`, `--html-path` are kept in args list untouched.
572
708
  def slurp_global_flags(args)
573
- flags = %w[ -c -p --paginate --no-pager --no-replace-objects --bare --version --help ]
709
+ flags = %w[ --noop -c -p --paginate --no-pager --no-replace-objects --bare --version --help ]
574
710
  flags2 = %w[ --exec-path= --git-dir= --work-tree= ]
575
711
 
576
712
  # flags that should be present in subcommands, too
@@ -581,6 +717,8 @@ help
581
717
  while args[0] && (flags.include?(args[0]) || flags2.any? {|f| args[0].index(f) == 0 })
582
718
  flag = args.shift
583
719
  case flag
720
+ when '--noop'
721
+ args.noop!
584
722
  when '--version', '--help'
585
723
  args.unshift flag.sub('--', '')
586
724
  when '-c'
@@ -588,7 +726,7 @@ help
588
726
  config_pair = args.shift
589
727
  # add configuration to our local cache
590
728
  key, value = config_pair.split('=', 2)
591
- Context::GIT_CONFIG["config #{key}"] = value.to_s
729
+ git_reader.stub_config_value(key, value)
592
730
 
593
731
  globals << flag << config_pair
594
732
  when '-p', '--paginate', '--no-pager'
@@ -598,25 +736,26 @@ help
598
736
  end
599
737
  end
600
738
 
601
- Context::GIT_CONFIG.executable = Array(Context::GIT_CONFIG.executable).concat(globals)
602
- args.executable = Array(args.executable).concat(globals).concat(locals)
739
+ git_reader.add_exec_flags(globals)
740
+ args.add_exec_flags(globals)
741
+ args.add_exec_flags(locals)
603
742
  end
604
743
 
605
744
  # Handles common functionality of browser commands like `browse`
606
745
  # and `compare`. Yields a block that returns params for `github_url`.
607
746
  def browse_command(args)
608
747
  url_only = args.delete('-u')
609
- $stderr.puts "Warning: the `-p` flag has no effect anymore" if args.delete('-p')
610
- params = yield
748
+ warn "Warning: the `-p` flag has no effect anymore" if args.delete('-p')
749
+ url = yield
611
750
 
612
751
  args.executable = url_only ? 'echo' : browser_launcher
613
- args.push github_url({:web => true, :private => true}.update(params))
752
+ args.push url
614
753
  end
615
754
 
616
755
  # Returns the terminal-formatted manpage, ready to be printed to
617
756
  # the screen.
618
757
  def hub_manpage
619
- return "** Can't find groff(1)" unless command?('groff')
758
+ abort "** Can't find groff(1)" unless command?('groff')
620
759
 
621
760
  require 'open3'
622
761
  out = nil
@@ -659,7 +798,7 @@ help
659
798
 
660
799
  # http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby
661
800
  def page_stdout
662
- return unless $stdout.tty?
801
+ return if not $stdout.tty? or windows?
663
802
 
664
803
  read, write = IO.pipe
665
804
 
@@ -692,9 +831,9 @@ help
692
831
  end
693
832
 
694
833
  # Determines whether a user has a fork of the current repo on GitHub.
695
- def repo_exists?(user)
696
- require 'net/http'
697
- url = API_REPO % [user, repo_name]
834
+ def repo_exists?(user, repo = repo_name)
835
+ load_net_http
836
+ url = API_REPO % [user, repo]
698
837
  Net::HTTPSuccess === Net::HTTP.get_response(URI(url))
699
838
  end
700
839
 
@@ -702,23 +841,79 @@ help
702
841
  #
703
842
  # Returns nothing.
704
843
  def fork_repo
705
- url = API_FORK % [repo_owner, repo_name]
706
- response = Net::HTTP.post_form(URI(url), 'login' => github_user, 'token' => github_token)
844
+ load_net_http
845
+ response = http_post API_FORK % [repo_owner, repo_name]
707
846
  response.error! unless Net::HTTPSuccess === response
708
847
  end
709
848
 
710
849
  # Creates a new repo using the GitHub API.
711
850
  #
712
851
  # Returns nothing.
713
- def create_repo(options = {})
714
- url = API_CREATE
715
- params = {'login' => github_user, 'token' => github_token, 'name' => repo_name}
852
+ def create_repo(name, options = {})
853
+ params = {'name' => name.sub(/^#{github_user}\//, '')}
716
854
  params['public'] = '0' if options[:private]
717
855
  params['description'] = options[:description] if options[:description]
718
856
  params['homepage'] = options[:homepage] if options[:homepage]
719
857
 
720
- response = Net::HTTP.post_form(URI(url), params)
858
+ load_net_http
859
+ response = http_post(API_CREATE, params)
860
+ response.error! unless Net::HTTPSuccess === response
861
+ end
862
+
863
+ # Returns parsed data from the new pull request.
864
+ def create_pullrequest(options)
865
+ project = options.fetch(:project)
866
+ params = {
867
+ 'pull[base]' => options.fetch(:base),
868
+ 'pull[head]' => options.fetch(:head)
869
+ }
870
+ params['pull[issue]'] = options[:issue] if options[:issue]
871
+ params['pull[title]'] = options[:title] if options[:title]
872
+ params['pull[body]'] = options[:body] if options[:body]
873
+
874
+ load_net_http
875
+ response = http_post(API_PULLREQUEST % [project.owner, project.name], params)
721
876
  response.error! unless Net::HTTPSuccess === response
877
+ # GitHub bug: although we request YAML, it returns JSON
878
+ if response['Content-type'].to_s.include? 'application/json'
879
+ { "html_url" => response.body.match(/"html_url":\s*"(.+?)"/)[1] }
880
+ else
881
+ require 'yaml'
882
+ YAML.load(response.body)['pull']
883
+ end
884
+ end
885
+
886
+ def pullrequest_editmsg(changes)
887
+ message_file = File.join(git_dir, 'PULLREQ_EDITMSG')
888
+ File.open(message_file, 'w') { |msg|
889
+ msg.puts
890
+ yield msg
891
+ if changes
892
+ msg.puts "#\n# Changes:\n#"
893
+ msg.puts changes.gsub(/^/, '# ').gsub(/ +$/, '')
894
+ end
895
+ }
896
+ edit_cmd = Array(git_editor).dup << message_file
897
+ system(*edit_cmd)
898
+ abort "can't open text editor for pull request message" unless $?.success?
899
+ title, body = read_editmsg(message_file)
900
+ abort "Aborting due to empty pull request title" unless title
901
+ [title, body]
902
+ end
903
+
904
+ def read_editmsg(file)
905
+ title, body = '', ''
906
+ File.open(file, 'r') { |msg|
907
+ msg.each_line do |line|
908
+ next if line.index('#') == 0
909
+ ((body.empty? and line =~ /\S/) ? title : body) << line
910
+ end
911
+ }
912
+ title.tr!("\n", ' ')
913
+ title.strip!
914
+ body.strip!
915
+
916
+ [title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil]
722
917
  end
723
918
 
724
919
  def expand_alias(cmd)
@@ -730,5 +925,49 @@ help
730
925
  end
731
926
  end
732
927
 
928
+ def http_post(url, params = nil)
929
+ url = URI(url)
930
+ post = Net::HTTP::Post.new(url.request_uri)
931
+ post.basic_auth "#{github_user}/token", github_token
932
+ post.set_form_data params if params
933
+
934
+ port = url.port
935
+ if use_ssl = 'https' == url.scheme and not use_ssl?
936
+ # ruby compiled without openssl
937
+ use_ssl = false
938
+ port = 80
939
+ end
940
+
941
+ http = Net::HTTP.new(url.host, port)
942
+ if http.use_ssl = use_ssl
943
+ # TODO: SSL peer verification
944
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
945
+ end
946
+ http.start { http.request(post) }
947
+ end
948
+
949
+ def load_net_http
950
+ require 'net/https'
951
+ rescue LoadError
952
+ require 'net/http'
953
+ end
954
+
955
+ def use_ssl?
956
+ defined? ::OpenSSL
957
+ end
958
+
959
+ # Fake exception type for net/http exception handling.
960
+ # Necessary because net/http may or may not be loaded at the time.
961
+ module HTTPExceptions
962
+ def self.===(exception)
963
+ exception.class.ancestors.map {|a| a.to_s }.include? 'Net::HTTPExceptions'
964
+ end
965
+ end
966
+
967
+ def display_http_exception(action, response)
968
+ $stderr.puts "Error #{action}: #{response.message} (HTTP #{response.code})"
969
+ warn "Check your token configuration (`git config github.token`)" if response.code.to_i == 401
970
+ end
971
+
733
972
  end
734
973
  end