]> Tony Duckles's Git Repositories (git.nynim.org) - dotfiles.git/blob - bin/hub
.gitconfig: Alias ls="ls-files"
[dotfiles.git] / bin / hub
1 #!/usr/bin/env ruby
2 #
3 # This file is generated code. DO NOT send patches for it.
4 #
5 # Original source files with comments are at:
6 # https://github.com/defunkt/hub
7 #
8
9 module Hub
10 Version = VERSION = '1.10.3'
11 end
12
13 module Hub
14 class Args < Array
15 attr_accessor :executable
16
17 def initialize(*args)
18 super
19 @executable = ENV["GIT"] || "git"
20 @after = nil
21 @skip = @noop = false
22 @original_args = args.first
23 @chain = [nil]
24 end
25
26 def after(cmd_or_args = nil, args = nil, &block)
27 @chain.insert(-1, normalize_callback(cmd_or_args, args, block))
28 end
29
30 def before(cmd_or_args = nil, args = nil, &block)
31 @chain.insert(@chain.index(nil), normalize_callback(cmd_or_args, args, block))
32 end
33
34 def chained?
35 @chain.size > 1
36 end
37
38 def commands
39 chain = @chain.dup
40 chain[chain.index(nil)] = self.to_exec
41 chain
42 end
43
44 def skip!
45 @skip = true
46 end
47
48 def skip?
49 @skip
50 end
51
52 def noop!
53 @noop = true
54 end
55
56 def noop?
57 @noop
58 end
59
60 def to_exec(args = self)
61 Array(executable) + args
62 end
63
64 def add_exec_flags(flags)
65 self.executable = Array(executable).concat(flags)
66 end
67
68 def words
69 reject { |arg| arg.index('-') == 0 }
70 end
71
72 def flags
73 self - words
74 end
75
76 def changed?
77 chained? or self != @original_args
78 end
79
80 def has_flag?(*flags)
81 pattern = flags.flatten.map { |f| Regexp.escape(f) }.join('|')
82 !grep(/^#{pattern}(?:=|$)/).empty?
83 end
84
85 private
86
87 def normalize_callback(cmd_or_args, args, block)
88 if block
89 block
90 elsif args
91 [cmd_or_args].concat args
92 elsif Array === cmd_or_args
93 self.to_exec cmd_or_args
94 elsif cmd_or_args
95 cmd_or_args
96 else
97 raise ArgumentError, "command or block required"
98 end
99 end
100 end
101 end
102
103 module Hub
104 class SshConfig
105 CONFIG_FILES = %w(~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config)
106
107 def initialize files = nil
108 @settings = Hash.new {|h,k| h[k] = {} }
109 Array(files || CONFIG_FILES).each do |path|
110 file = File.expand_path path
111 parse_file file if File.exist? file
112 end
113 end
114
115 def get_value hostname, key
116 key = key.to_s.downcase
117 @settings.each do |pattern, settings|
118 if pattern.match? hostname and found = settings[key]
119 return found
120 end
121 end
122 yield
123 end
124
125 class HostPattern
126 def initialize pattern
127 @pattern = pattern.to_s.downcase
128 end
129
130 def to_s() @pattern end
131 def ==(other) other.to_s == self.to_s end
132
133 def matcher
134 @matcher ||=
135 if '*' == @pattern
136 Proc.new { true }
137 elsif @pattern !~ /[?*]/
138 lambda { |hostname| hostname.to_s.downcase == @pattern }
139 else
140 re = self.class.pattern_to_regexp @pattern
141 lambda { |hostname| re =~ hostname }
142 end
143 end
144
145 def match? hostname
146 matcher.call hostname
147 end
148
149 def self.pattern_to_regexp pattern
150 escaped = Regexp.escape(pattern)
151 escaped.gsub!('\*', '.*')
152 escaped.gsub!('\?', '.')
153 /^#{escaped}$/i
154 end
155 end
156
157 def parse_file file
158 host_patterns = [HostPattern.new('*')]
159
160 IO.foreach(file) do |line|
161 case line
162 when /^\s*(#|$)/ then next
163 when /^\s*(\S+)\s*=/
164 key, value = $1, $'
165 else
166 key, value = line.strip.split(/\s+/, 2)
167 end
168
169 next if value.nil?
170 key.downcase!
171 value = $1 if value =~ /^"(.*)"$/
172 value.chomp!
173
174 if 'host' == key
175 host_patterns = value.split(/\s+/).map {|p| HostPattern.new p }
176 else
177 record_setting key, value, host_patterns
178 end
179 end
180 end
181
182 def record_setting key, value, patterns
183 patterns.each do |pattern|
184 @settings[pattern][key] ||= value
185 end
186 end
187 end
188 end
189
190 require 'uri'
191 require 'yaml'
192 require 'forwardable'
193 require 'fileutils'
194
195 module Hub
196 class GitHubAPI
197 attr_reader :config, :oauth_app_url
198
199 def initialize config, options
200 @config = config
201 @oauth_app_url = options.fetch(:app_url)
202 end
203
204 module Exceptions
205 def self.===(exception)
206 exception.class.ancestors.map {|a| a.to_s }.include? 'Net::HTTPExceptions'
207 end
208 end
209
210 def api_host host
211 host = host.downcase
212 'github.com' == host ? 'api.github.com' : host
213 end
214
215 def repo_info project
216 get "https://%s/repos/%s/%s" %
217 [api_host(project.host), project.owner, project.name]
218 end
219
220 def repo_exists? project
221 repo_info(project).success?
222 end
223
224 def fork_repo project
225 res = post "https://%s/repos/%s/%s/forks" %
226 [api_host(project.host), project.owner, project.name]
227 res.error! unless res.success?
228 end
229
230 def create_repo project, options = {}
231 is_org = project.owner != config.username(api_host(project.host))
232 params = { :name => project.name, :private => !!options[:private] }
233 params[:description] = options[:description] if options[:description]
234 params[:homepage] = options[:homepage] if options[:homepage]
235
236 if is_org
237 res = post "https://%s/orgs/%s/repos" % [api_host(project.host), project.owner], params
238 else
239 res = post "https://%s/user/repos" % api_host(project.host), params
240 end
241 res.error! unless res.success?
242 res.data
243 end
244
245 def pullrequest_info project, pull_id
246 res = get "https://%s/repos/%s/%s/pulls/%d" %
247 [api_host(project.host), project.owner, project.name, pull_id]
248 res.error! unless res.success?
249 res.data
250 end
251
252 def create_pullrequest options
253 project = options.fetch(:project)
254 params = {
255 :base => options.fetch(:base),
256 :head => options.fetch(:head)
257 }
258
259 if options[:issue]
260 params[:issue] = options[:issue]
261 else
262 params[:title] = options[:title] if options[:title]
263 params[:body] = options[:body] if options[:body]
264 end
265
266 res = post "https://%s/repos/%s/%s/pulls" %
267 [api_host(project.host), project.owner, project.name], params
268
269 res.error! unless res.success?
270 res.data
271 end
272
273 module HttpMethods
274 module ResponseMethods
275 def status() code.to_i end
276 def data?() content_type =~ /\bjson\b/ end
277 def data() @data ||= JSON.parse(body) end
278 def error_message?() data? and data['errors'] || data['message'] end
279 def error_message() error_sentences || data['message'] end
280 def success?() Net::HTTPSuccess === self end
281 def error_sentences
282 data['errors'].map do |err|
283 case err['code']
284 when 'custom' then err['message']
285 when 'missing_field' then "field '%s' is missing" % err['field']
286 end
287 end.compact if data['errors']
288 end
289 end
290
291 def get url, &block
292 perform_request url, :Get, &block
293 end
294
295 def post url, params = nil
296 perform_request url, :Post do |req|
297 if params
298 req.body = JSON.dump params
299 req['Content-Type'] = 'application/json;charset=utf-8'
300 end
301 yield req if block_given?
302 req['Content-Length'] = byte_size req.body
303 end
304 end
305
306 def byte_size str
307 if str.respond_to? :bytesize then str.bytesize
308 elsif str.respond_to? :length then str.length
309 else 0
310 end
311 end
312
313 def post_form url, params
314 post(url) {|req| req.set_form_data params }
315 end
316
317 def perform_request url, type
318 url = URI.parse url unless url.respond_to? :host
319
320 require 'net/https'
321 req = Net::HTTP.const_get(type).new request_uri(url)
322 http = configure_connection(req, url) do |host_url|
323 create_connection host_url
324 end
325
326 apply_authentication(req, url)
327 yield req if block_given?
328
329 begin
330 res = http.start { http.request(req) }
331 res.extend ResponseMethods
332 return res
333 rescue SocketError => err
334 raise Context::FatalError, "error with #{type.to_s.upcase} #{url} (#{err.message})"
335 end
336 end
337
338 def request_uri url
339 str = url.request_uri
340 str = '/api/v3' << str if url.host != 'api.github.com'
341 str
342 end
343
344 def configure_connection req, url
345 if ENV['HUB_TEST_HOST']
346 req['Host'] = url.host
347 url = url.dup
348 url.scheme = 'http'
349 url.host, test_port = ENV['HUB_TEST_HOST'].split(':')
350 url.port = test_port.to_i if test_port
351 end
352 yield url
353 end
354
355 def apply_authentication req, url
356 user = url.user || config.username(url.host)
357 pass = config.password(url.host, user)
358 req.basic_auth user, pass
359 end
360
361 def create_connection url
362 use_ssl = 'https' == url.scheme
363
364 proxy_args = []
365 if proxy = config.proxy_uri(use_ssl)
366 proxy_args << proxy.host << proxy.port
367 if proxy.userinfo
368 require 'cgi'
369 proxy_args.concat proxy.userinfo.split(':', 2).map {|a| CGI.unescape a }
370 end
371 end
372
373 http = Net::HTTP.new(url.host, url.port, *proxy_args)
374
375 if http.use_ssl = use_ssl
376 http.verify_mode = OpenSSL::SSL::VERIFY_NONE
377 end
378 return http
379 end
380 end
381
382 module OAuth
383 def apply_authentication req, url
384 if (req.path =~ /\/authorizations$/)
385 super
386 else
387 user = url.user || config.username(url.host)
388 token = config.oauth_token(url.host, user) {
389 obtain_oauth_token url.host, user
390 }
391 req['Authorization'] = "token #{token}"
392 end
393 end
394
395 def obtain_oauth_token host, user
396 res = get "https://#{user}@#{host}/authorizations"
397 res.error! unless res.success?
398
399 if found = res.data.find {|auth| auth['app']['url'] == oauth_app_url }
400 found['token']
401 else
402 res = post "https://#{user}@#{host}/authorizations",
403 :scopes => %w[repo], :note => 'hub', :note_url => oauth_app_url
404 res.error! unless res.success?
405 res.data['token']
406 end
407 end
408 end
409
410 include HttpMethods
411 include OAuth
412
413 class FileStore
414 extend Forwardable
415 def_delegator :@data, :[], :get
416 def_delegator :@data, :[]=, :set
417
418 def initialize filename
419 @filename = filename
420 @data = Hash.new {|d, host| d[host] = [] }
421 load if File.exist? filename
422 end
423
424 def fetch_user host
425 unless entry = get(host).first
426 user = yield
427 return nil if user.nil? or user.empty?
428 entry = entry_for_user(host, user)
429 end
430 entry['user']
431 end
432
433 def fetch_value host, user, key
434 entry = entry_for_user host, user
435 entry[key.to_s] || begin
436 value = yield
437 if value and !value.empty?
438 entry[key.to_s] = value
439 save
440 value
441 else
442 raise "no value"
443 end
444 end
445 end
446
447 def entry_for_user host, username
448 entries = get(host)
449 entries.find {|e| e['user'] == username } or
450 (entries << {'user' => username}).last
451 end
452
453 def load
454 existing_data = File.read(@filename)
455 @data.update YAML.load(existing_data) unless existing_data.strip.empty?
456 end
457
458 def save
459 FileUtils.mkdir_p File.dirname(@filename)
460 File.open(@filename, 'w', 0600) {|f| f << YAML.dump(@data) }
461 end
462 end
463
464 class Configuration
465 def initialize store
466 @data = store
467 @password_cache = {}
468 end
469
470 def normalize_host host
471 host = host.downcase
472 'api.github.com' == host ? 'github.com' : host
473 end
474
475 def username host
476 return ENV['GITHUB_USER'] unless ENV['GITHUB_USER'].to_s.empty?
477 host = normalize_host host
478 @data.fetch_user host do
479 if block_given? then yield
480 else prompt "#{host} username"
481 end
482 end
483 end
484
485 def api_token host, user
486 host = normalize_host host
487 @data.fetch_value host, user, :api_token do
488 if block_given? then yield
489 else prompt "#{host} API token for #{user}"
490 end
491 end
492 end
493
494 def password host, user
495 return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.empty?
496 host = normalize_host host
497 @password_cache["#{user}@#{host}"] ||= prompt_password host, user
498 end
499
500 def oauth_token host, user, &block
501 @data.fetch_value normalize_host(host), user, :oauth_token, &block
502 end
503
504 def prompt what
505 print "#{what}: "
506 $stdin.gets.chomp
507 end
508
509 def prompt_password host, user
510 print "#{host} password for #{user} (never stored): "
511 if $stdin.tty?
512 password = askpass
513 puts ''
514 password
515 else
516 $stdin.gets.chomp
517 end
518 end
519
520 def askpass
521 tty_state = `stty -g`
522 system 'stty raw -echo -icanon isig' if $?.success?
523 pass = ''
524 while char = $stdin.getbyte and !(char == 13 or char == 10)
525 if char == 127 or char == 8
526 pass[-1,1] = '' unless pass.empty?
527 else
528 pass << char.chr
529 end
530 end
531 pass
532 ensure
533 system "stty #{tty_state}" unless tty_state.empty?
534 end
535
536 def proxy_uri(with_ssl)
537 env_name = "HTTP#{with_ssl ? 'S' : ''}_PROXY"
538 if proxy = ENV[env_name] || ENV[env_name.downcase] and !proxy.empty?
539 proxy = "http://#{proxy}" unless proxy.include? '://'
540 URI.parse proxy
541 end
542 end
543 end
544 end
545 end
546
547 require 'shellwords'
548 require 'forwardable'
549 require 'uri'
550
551 module Hub
552 module Context
553 extend Forwardable
554
555 NULL = defined?(File::NULL) ? File::NULL : File.exist?('/dev/null') ? '/dev/null' : 'NUL'
556
557 class GitReader
558 attr_reader :executable
559
560 def initialize(executable = nil, &read_proc)
561 @executable = executable || 'git'
562 read_proc ||= lambda { |cache, cmd|
563 result = %x{#{command_to_string(cmd)} 2>#{NULL}}.chomp
564 cache[cmd] = $?.success? && !result.empty? ? result : nil
565 }
566 @cache = Hash.new(&read_proc)
567 end
568
569 def add_exec_flags(flags)
570 @executable = Array(executable).concat(flags)
571 end
572
573 def read_config(cmd, all = false)
574 config_cmd = ['config', (all ? '--get-all' : '--get'), *cmd]
575 config_cmd = config_cmd.join(' ') unless cmd.respond_to? :join
576 read config_cmd
577 end
578
579 def read(cmd)
580 @cache[cmd]
581 end
582
583 def stub_config_value(key, value, get = '--get')
584 stub_command_output "config #{get} #{key}", value
585 end
586
587 def stub_command_output(cmd, value)
588 @cache[cmd] = value.nil? ? nil : value.to_s
589 end
590
591 def stub!(values)
592 @cache.update values
593 end
594
595 private
596
597 def to_exec(args)
598 args = Shellwords.shellwords(args) if args.respond_to? :to_str
599 Array(executable) + Array(args)
600 end
601
602 def command_to_string(cmd)
603 full_cmd = to_exec(cmd)
604 full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ')
605 end
606 end
607
608 module GitReaderMethods
609 extend Forwardable
610
611 def_delegator :git_reader, :read_config, :git_config
612 def_delegator :git_reader, :read, :git_command
613
614 def self.extended(base)
615 base.extend Forwardable
616 base.def_delegators :'self.class', :git_config, :git_command
617 end
618 end
619
620 class Error < RuntimeError; end
621 class FatalError < Error; end
622
623 private
624
625 def git_reader
626 @git_reader ||= GitReader.new ENV['GIT']
627 end
628
629 include GitReaderMethods
630 private :git_config, :git_command
631
632 def local_repo(fatal = true)
633 @local_repo ||= begin
634 if is_repo?
635 LocalRepo.new git_reader, current_dir
636 elsif fatal
637 raise FatalError, "Not a git repository"
638 end
639 end
640 end
641
642 repo_methods = [
643 :current_branch,
644 :current_project, :upstream_project,
645 :repo_owner, :repo_host,
646 :remotes, :remotes_group, :origin_remote
647 ]
648 def_delegator :local_repo, :name, :repo_name
649 def_delegators :local_repo, *repo_methods
650 private :repo_name, *repo_methods
651
652 def master_branch
653 if local_repo(false)
654 local_repo.master_branch
655 else
656 Branch.new nil, 'refs/heads/master'
657 end
658 end
659
660 class LocalRepo < Struct.new(:git_reader, :dir)
661 include GitReaderMethods
662
663 def name
664 if project = main_project
665 project.name
666 else
667 File.basename(dir)
668 end
669 end
670
671 def repo_owner
672 if project = main_project
673 project.owner
674 end
675 end
676
677 def repo_host
678 project = main_project and project.host
679 end
680
681 def main_project
682 remote = origin_remote and remote.project
683 end
684
685 def upstream_project
686 if branch = current_branch and upstream = branch.upstream and upstream.remote?
687 remote = remote_by_name upstream.remote_name
688 remote.project
689 end
690 end
691
692 def current_project
693 upstream_project || main_project
694 end
695
696 def current_branch
697 if branch = git_command('symbolic-ref -q HEAD')
698 Branch.new self, branch
699 end
700 end
701
702 def master_branch
703 Branch.new self, 'refs/heads/master'
704 end
705
706 def remotes
707 @remotes ||= begin
708 list = git_command('remote').to_s.split("\n")
709 main = list.delete('origin') and list.unshift(main)
710 list.map { |name| Remote.new self, name }
711 end
712 end
713
714 def remotes_group(name)
715 git_config "remotes.#{name}"
716 end
717
718 def origin_remote
719 remotes.first
720 end
721
722 def remote_by_name(remote_name)
723 remotes.find {|r| r.name == remote_name }
724 end
725
726 def known_hosts
727 hosts = git_config('hub.host', :all).to_s.split("\n")
728 hosts << default_host
729 hosts << "ssh.#{default_host}"
730 end
731
732 def self.default_host
733 ENV['GITHUB_HOST'] || main_host
734 end
735
736 def self.main_host
737 'github.com'
738 end
739
740 extend Forwardable
741 def_delegators :'self.class', :default_host, :main_host
742
743 def ssh_config
744 @ssh_config ||= SshConfig.new
745 end
746 end
747
748 class GithubProject < Struct.new(:local_repo, :owner, :name, :host)
749 def self.from_url(url, local_repo)
750 if local_repo.known_hosts.include? url.host
751 _, owner, name = url.path.split('/', 4)
752 GithubProject.new(local_repo, owner, name.sub(/\.git$/, ''), url.host)
753 end
754 end
755
756 attr_accessor :repo_data
757
758 def initialize(*args)
759 super
760 self.name = self.name.tr(' ', '-')
761 self.host ||= (local_repo || LocalRepo).default_host
762 self.host = host.sub(/^ssh\./i, '') if 'ssh.github.com' == host.downcase
763 end
764
765 def private?
766 repo_data ? repo_data.fetch('private') :
767 host != (local_repo || LocalRepo).main_host
768 end
769
770 def owned_by(new_owner)
771 new_project = dup
772 new_project.owner = new_owner
773 new_project
774 end
775
776 def name_with_owner
777 "#{owner}/#{name}"
778 end
779
780 def ==(other)
781 name_with_owner == other.name_with_owner
782 end
783
784 def remote
785 local_repo.remotes.find { |r| r.project == self }
786 end
787
788 def web_url(path = nil)
789 project_name = name_with_owner
790 if project_name.sub!(/\.wiki$/, '')
791 unless '/wiki' == path
792 path = if path =~ %r{^/commits/} then '/_history'
793 else path.to_s.sub(/\w+/, '_\0')
794 end
795 path = '/wiki' + path
796 end
797 end
798 "https://#{host}/" + project_name + path.to_s
799 end
800
801 def git_url(options = {})
802 if options[:https] then "https://#{host}/"
803 elsif options[:private] or private? then "git@#{host}:"
804 else "git://#{host}/"
805 end + name_with_owner + '.git'
806 end
807 end
808
809 class GithubURL < URI::HTTPS
810 extend Forwardable
811
812 attr_reader :project
813 def_delegator :project, :name, :project_name
814 def_delegator :project, :owner, :project_owner
815
816 def self.resolve(url, local_repo)
817 u = URI(url)
818 if %[http https].include? u.scheme and project = GithubProject.from_url(u, local_repo)
819 self.new(u.scheme, u.userinfo, u.host, u.port, u.registry,
820 u.path, u.opaque, u.query, u.fragment, project)
821 end
822 rescue URI::InvalidURIError
823 nil
824 end
825
826 def initialize(*args)
827 @project = args.pop
828 super(*args)
829 end
830
831 def project_path
832 path.split('/', 4)[3]
833 end
834 end
835
836 class Branch < Struct.new(:local_repo, :name)
837 alias to_s name
838
839 def short_name
840 name.sub(%r{^refs/(remotes/)?.+?/}, '')
841 end
842
843 def master?
844 short_name == 'master'
845 end
846
847 def upstream
848 if branch = local_repo.git_command("rev-parse --symbolic-full-name #{short_name}@{upstream}")
849 Branch.new local_repo, branch
850 end
851 end
852
853 def remote?
854 name.index('refs/remotes/') == 0
855 end
856
857 def remote_name
858 name =~ %r{^refs/remotes/([^/]+)} and $1 or
859 raise Error, "can't get remote name from #{name.inspect}"
860 end
861 end
862
863 class Remote < Struct.new(:local_repo, :name)
864 alias to_s name
865
866 def ==(other)
867 other.respond_to?(:to_str) ? name == other.to_str : super
868 end
869
870 def project
871 urls.each_value { |url|
872 if valid = GithubProject.from_url(url, local_repo)
873 return valid
874 end
875 }
876 nil
877 end
878
879 def urls
880 return @urls if defined? @urls
881 @urls = {}
882 local_repo.git_command('remote -v').to_s.split("\n").map do |line|
883 next if line !~ /^(.+?)\t(.+) \((.+)\)$/
884 remote, uri, type = $1, $2, $3
885 next if remote != self.name
886 if uri =~ %r{^[\w-]+://} or uri =~ %r{^([^/]+?):}
887 uri = "ssh://#{$1}/#{$'}" if $1
888 begin
889 @urls[type] = uri_parse(uri)
890 rescue URI::InvalidURIError
891 end
892 end
893 end
894 @urls
895 end
896
897 def uri_parse uri
898 uri = URI.parse uri
899 uri.host = local_repo.ssh_config.get_value(uri.host, 'hostname') { uri.host }
900 uri.user = local_repo.ssh_config.get_value(uri.host, 'user') { uri.user }
901 uri
902 end
903 end
904
905
906 def github_project(name, owner = nil)
907 if owner and owner.index('/')
908 owner, name = owner.split('/', 2)
909 elsif name and name.index('/')
910 owner, name = name.split('/', 2)
911 else
912 name ||= repo_name
913 owner ||= github_user
914 end
915
916 if local_repo(false) and main_project = local_repo.main_project
917 project = main_project.dup
918 project.owner = owner
919 project.name = name
920 project
921 else
922 GithubProject.new(local_repo(false), owner, name)
923 end
924 end
925
926 def git_url(owner = nil, name = nil, options = {})
927 project = github_project(name, owner)
928 project.git_url({:https => https_protocol?}.update(options))
929 end
930
931 def resolve_github_url(url)
932 GithubURL.resolve(url, local_repo) if url =~ /^https?:/
933 end
934
935 def http_clone?
936 git_config('--bool hub.http-clone') == 'true'
937 end
938
939 def https_protocol?
940 git_config('hub.protocol') == 'https' or http_clone?
941 end
942
943 def git_alias_for(name)
944 git_config "alias.#{name}"
945 end
946
947 def rev_list(a, b)
948 git_command("rev-list --cherry-pick --right-only --no-merges #{a}...#{b}")
949 end
950
951 PWD = Dir.pwd
952
953 def current_dir
954 PWD
955 end
956
957 def git_dir
958 git_command 'rev-parse -q --git-dir'
959 end
960
961 def is_repo?
962 !!git_dir
963 end
964
965 def git_editor
966 editor = git_command 'var GIT_EDITOR'
967 editor = ENV[$1] if editor =~ /^\$(\w+)$/
968 editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/
969 editor.shellsplit
970 end
971
972 module System
973 def browser_launcher
974 browser = ENV['BROWSER'] || (
975 osx? ? 'open' : windows? ? %w[cmd /c start] :
976 %w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm }
977 )
978
979 abort "Please set $BROWSER to a web launcher to use this command." unless browser
980 Array(browser)
981 end
982
983 def osx?
984 require 'rbconfig'
985 RbConfig::CONFIG['host_os'].to_s.include?('darwin')
986 end
987
988 def windows?
989 require 'rbconfig'
990 RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/
991 end
992
993 def which(cmd)
994 exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
995 ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
996 exts.each { |ext|
997 exe = "#{path}/#{cmd}#{ext}"
998 return exe if File.executable? exe
999 }
1000 end
1001 return nil
1002 end
1003
1004 def command?(name)
1005 !which(name).nil?
1006 end
1007 end
1008
1009 include System
1010 extend System
1011 end
1012 end
1013
1014 require 'strscan'
1015 require 'forwardable'
1016
1017 class Hub::JSON
1018 def self.parse(data) new(data).parse end
1019
1020 WSP = /\s+/
1021 OBJ = /[{\[]/; HEN = /\}/; AEN = /\]/
1022 COL = /\s*:\s*/; KEY = /\s*,\s*/
1023 NUM = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/
1024 BOL = /true|false/; NUL = /null/
1025
1026 extend Forwardable
1027
1028 attr_reader :scanner
1029 alias_method :s, :scanner
1030 def_delegators :scanner, :scan, :matched
1031 private :s, :scan, :matched
1032
1033 def initialize data
1034 @scanner = StringScanner.new data.to_s
1035 end
1036
1037 def parse
1038 space
1039 object
1040 end
1041
1042 private
1043
1044 def space() scan WSP end
1045
1046 def endkey() scan(KEY) or space end
1047
1048 def object
1049 matched == '{' ? hash : array if scan(OBJ)
1050 end
1051
1052 def value
1053 object or string or
1054 scan(NUL) ? nil :
1055 scan(BOL) ? matched.size == 4:
1056 scan(NUM) ? eval(matched) :
1057 error
1058 end
1059
1060 def hash
1061 obj = {}
1062 space
1063 repeat_until(HEN) { k = string; scan(COL); obj[k] = value; endkey }
1064 obj
1065 end
1066
1067 def array
1068 ary = []
1069 space
1070 repeat_until(AEN) { ary << value; endkey }
1071 ary
1072 end
1073
1074 SPEC = {'b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t"}
1075 UNI = 'u'; CODE = /[a-fA-F0-9]{4}/
1076 STR = /"/; STE = '"'
1077 ESC = '\\'
1078
1079 def string
1080 if scan(STR)
1081 str, esc = '', false
1082 while c = s.getch
1083 if esc
1084 str << (c == UNI ? (s.scan(CODE) || error).to_i(16).chr : SPEC[c] || c)
1085 esc = false
1086 else
1087 case c
1088 when ESC then esc = true
1089 when STE then break
1090 else str << c
1091 end
1092 end
1093 end
1094 str
1095 end
1096 end
1097
1098 def error
1099 raise "parse error at: #{scan(/.{1,10}/m).inspect}"
1100 end
1101
1102 def repeat_until reg
1103 until scan(reg)
1104 pos = s.pos
1105 yield
1106 error unless s.pos > pos
1107 end
1108 end
1109
1110 module Generator
1111 def generate(obj)
1112 raise ArgumentError unless obj.is_a? Array or obj.is_a? Hash
1113 generate_type(obj)
1114 end
1115 alias dump generate
1116
1117 private
1118
1119 def generate_type(obj)
1120 type = obj.is_a?(Numeric) ? :Numeric : obj.class.name
1121 begin send(:"generate_#{type}", obj)
1122 rescue NoMethodError; raise ArgumentError, "can't serialize #{type}"
1123 end
1124 end
1125
1126 ESC_MAP = Hash.new {|h,k| k }.update \
1127 "\r" => 'r',
1128 "\n" => 'n',
1129 "\f" => 'f',
1130 "\t" => 't',
1131 "\b" => 'b'
1132
1133 def generate_String(str)
1134 escaped = str.gsub(/[\r\n\f\t\b"\\]/) { "\\#{ESC_MAP[$&]}"}
1135 %("#{escaped}")
1136 end
1137
1138 def generate_simple(obj) obj.inspect end
1139 alias generate_Numeric generate_simple
1140 alias generate_TrueClass generate_simple
1141 alias generate_FalseClass generate_simple
1142
1143 def generate_Symbol(sym) generate_String(sym.to_s) end
1144
1145 def generate_NilClass(*) 'null' end
1146
1147 def generate_Array(ary) '[%s]' % ary.map {|o| generate_type(o) }.join(', ') end
1148
1149 def generate_Hash(hash)
1150 '{%s}' % hash.map { |key, value|
1151 "#{generate_String(key.to_s)}: #{generate_type(value)}"
1152 }.join(', ')
1153 end
1154 end
1155
1156 extend Generator
1157 end
1158
1159 module Hub
1160 module Commands
1161 instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ }
1162 extend self
1163
1164 extend Context
1165
1166 NAME_RE = /[\w.][\w.-]*/
1167 OWNER_RE = /[a-zA-Z0-9-]+/
1168 NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/
1169
1170 CUSTOM_COMMANDS = %w[alias create browse compare fork pull-request]
1171
1172 def run(args)
1173 slurp_global_flags(args)
1174
1175 args.unshift 'help' if args.empty?
1176
1177 cmd = args[0]
1178 if expanded_args = expand_alias(cmd)
1179 cmd = expanded_args[0]
1180 expanded_args.concat args[1..-1]
1181 end
1182
1183 respect_help_flags(expanded_args || args) if custom_command? cmd
1184
1185 cmd = cmd.gsub(/(\w)-/, '\1_')
1186 if method_defined?(cmd) and cmd != 'run'
1187 args.replace expanded_args if expanded_args
1188 send(cmd, args)
1189 end
1190 rescue Errno::ENOENT
1191 if $!.message.include? "No such file or directory - git"
1192 abort "Error: `git` command not found"
1193 else
1194 raise
1195 end
1196 rescue Context::FatalError => err
1197 abort "fatal: #{err.message}"
1198 end
1199
1200 def pull_request(args)
1201 args.shift
1202 options = { }
1203 force = explicit_owner = false
1204 base_project = local_repo.main_project
1205 head_project = local_repo.current_project
1206
1207 unless base_project
1208 abort "Aborted: the origin remote doesn't point to a GitHub repository."
1209 end
1210
1211 from_github_ref = lambda do |ref, context_project|
1212 if ref.index(':')
1213 owner, ref = ref.split(':', 2)
1214 project = github_project(context_project.name, owner)
1215 end
1216 [project || context_project, ref]
1217 end
1218
1219 while arg = args.shift
1220 case arg
1221 when '-f'
1222 force = true
1223 when '-b'
1224 base_project, options[:base] = from_github_ref.call(args.shift, base_project)
1225 when '-h'
1226 head = args.shift
1227 explicit_owner = !!head.index(':')
1228 head_project, options[:head] = from_github_ref.call(head, head_project)
1229 when '-i'
1230 options[:issue] = args.shift
1231 else
1232 if url = resolve_github_url(arg) and url.project_path =~ /^issues\/(\d+)/
1233 options[:issue] = $1
1234 base_project = url.project
1235 elsif !options[:title] then options[:title] = arg
1236 else
1237 abort "invalid argument: #{arg}"
1238 end
1239 end
1240 end
1241
1242 options[:project] = base_project
1243 options[:base] ||= master_branch.short_name
1244
1245 if tracked_branch = options[:head].nil? && current_branch.upstream
1246 if !tracked_branch.remote?
1247 tracked_branch = nil
1248 elsif base_project == head_project and tracked_branch.short_name == options[:base]
1249 $stderr.puts "Aborted: head branch is the same as base (#{options[:base].inspect})"
1250 warn "(use `-h <branch>` to specify an explicit pull request head)"
1251 abort
1252 end
1253 end
1254 options[:head] ||= (tracked_branch || current_branch).short_name
1255
1256 user = github_user(head_project.host)
1257 if head_project.owner != user and !tracked_branch and !explicit_owner
1258 head_project = head_project.owned_by(user)
1259 end
1260
1261 remote_branch = "#{head_project.remote}/#{options[:head]}"
1262 options[:head] = "#{head_project.owner}:#{options[:head]}"
1263
1264 if !force and tracked_branch and local_commits = rev_list(remote_branch, nil)
1265 $stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{remote_branch}"
1266 warn "(use `-f` to force submit a pull request anyway)"
1267 abort
1268 end
1269
1270 if args.noop?
1271 puts "Would request a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
1272 exit
1273 end
1274
1275 unless options[:title] or options[:issue]
1276 base_branch = "#{base_project.remote}/#{options[:base]}"
1277 commits = rev_list(base_branch, remote_branch).to_s.split("\n")
1278
1279 case commits.size
1280 when 0
1281 default_message = commit_summary = nil
1282 when 1
1283 format = '%w(78,0,0)%s%n%+b'
1284 default_message = git_command "show -s --format='#{format}' #{commits.first}"
1285 commit_summary = nil
1286 else
1287 format = '%h (%aN, %ar)%n%w(78,3,3)%s%n%+b'
1288 default_message = nil
1289 commit_summary = git_command "log --no-color --format='%s' --cherry %s...%s" %
1290 [format, base_branch, remote_branch]
1291 end
1292
1293 options[:title], options[:body] = pullrequest_editmsg(commit_summary) { |msg|
1294 msg.puts default_message if default_message
1295 msg.puts ""
1296 msg.puts "# Requesting a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}"
1297 msg.puts "#"
1298 msg.puts "# Write a message for this pull request. The first block"
1299 msg.puts "# of text is the title and the rest is description."
1300 }
1301 end
1302
1303 pull = api_client.create_pullrequest(options)
1304
1305 args.executable = 'echo'
1306 args.replace [pull['html_url']]
1307 rescue GitHubAPI::Exceptions
1308 display_api_exception("creating pull request", $!.response)
1309 exit 1
1310 end
1311
1312 def clone(args)
1313 ssh = args.delete('-p')
1314 has_values = /^(--(upload-pack|template|depth|origin|branch|reference)|-[ubo])$/
1315
1316 idx = 1
1317 while idx < args.length
1318 arg = args[idx]
1319 if arg.index('-') == 0
1320 idx += 1 if arg =~ has_values
1321 else
1322 if arg =~ NAME_WITH_OWNER_RE and !File.directory?(arg)
1323 name, owner = arg, nil
1324 owner, name = name.split('/', 2) if name.index('/')
1325 project = github_project(name, owner || github_user)
1326 ssh ||= args[0] != 'submodule' && project.owner == github_user(project.host) { }
1327 args[idx] = project.git_url(:private => ssh, :https => https_protocol?)
1328 end
1329 break
1330 end
1331 idx += 1
1332 end
1333 end
1334
1335 def submodule(args)
1336 return unless index = args.index('add')
1337 args.delete_at index
1338
1339 branch = args.index('-b') || args.index('--branch')
1340 if branch
1341 args.delete_at branch
1342 branch_name = args.delete_at branch
1343 end
1344
1345 clone(args)
1346
1347 if branch_name
1348 args.insert branch, '-b', branch_name
1349 end
1350 args.insert index, 'add'
1351 end
1352
1353 def remote(args)
1354 if %w[add set-url].include?(args[1])
1355 name = args.last
1356 if name =~ /^(#{OWNER_RE})$/ || name =~ /^(#{OWNER_RE})\/(#{NAME_RE})$/
1357 user, repo = $1, $2 || repo_name
1358 end
1359 end
1360 return unless user # do not touch arguments
1361
1362 ssh = args.delete('-p')
1363
1364 if args.words[2] == 'origin' && args.words[3].nil?
1365 user, repo = github_user, repo_name
1366 elsif args.words[-2] == args.words[1]
1367 idx = args.index( args.words[-1] )
1368 args[idx] = user
1369 else
1370 args.pop
1371 end
1372
1373 args << git_url(user, repo, :private => ssh)
1374 end
1375
1376 def fetch(args)
1377 if args.include?('--multiple')
1378 names = args.words[1..-1]
1379 elsif remote_name = args.words[1]
1380 if remote_name =~ /^\w+(,\w+)+$/
1381 index = args.index(remote_name)
1382 args.delete(remote_name)
1383 names = remote_name.split(',')
1384 args.insert(index, *names)
1385 args.insert(index, '--multiple')
1386 else
1387 names = [remote_name]
1388 end
1389 else
1390 names = []
1391 end
1392
1393 projects = names.map { |name|
1394 unless name =~ /\W/ or remotes.include?(name) or remotes_group(name)
1395 project = github_project(nil, name)
1396 repo_info = api_client.repo_info(project)
1397 if repo_info.success?
1398 project.repo_data = repo_info.data
1399 project
1400 end
1401 end
1402 }.compact
1403
1404 if projects.any?
1405 projects.each do |project|
1406 args.before ['remote', 'add', project.owner, project.git_url(:https => https_protocol?)]
1407 end
1408 end
1409 end
1410
1411 def checkout(args)
1412 _, url_arg, new_branch_name = args.words
1413 if url = resolve_github_url(url_arg) and url.project_path =~ /^pull\/(\d+)/
1414 pull_id = $1
1415 pull_data = api_client.pullrequest_info(url.project, pull_id)
1416
1417 args.delete new_branch_name
1418 user, branch = pull_data['head']['label'].split(':', 2)
1419 abort "Error: #{user}'s fork is not available anymore" unless pull_data['head']['repo']
1420 new_branch_name ||= "#{user}-#{branch}"
1421
1422 if remotes.include? user
1423 args.before ['remote', 'set-branches', '--add', user, branch]
1424 args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"]
1425 else
1426 url = github_project(url.project_name, user).git_url(:private => pull_data['head']['repo']['private'],
1427 :https => https_protocol?)
1428 args.before ['remote', 'add', '-f', '-t', branch, user, url]
1429 end
1430 idx = args.index url_arg
1431 args.delete_at idx
1432 args.insert idx, '--track', '-B', new_branch_name, "#{user}/#{branch}"
1433 end
1434 end
1435
1436 def merge(args)
1437 _, url_arg = args.words
1438 if url = resolve_github_url(url_arg) and url.project_path =~ /^pull\/(\d+)/
1439 pull_id = $1
1440 pull_data = api_client.pullrequest_info(url.project, pull_id)
1441
1442 user, branch = pull_data['head']['label'].split(':', 2)
1443 abort "Error: #{user}'s fork is not available anymore" unless pull_data['head']['repo']
1444
1445 url = github_project(url.project_name, user).git_url(:private => pull_data['head']['repo']['private'],
1446 :https => https_protocol?)
1447
1448 merge_head = "#{user}/#{branch}"
1449 args.before ['fetch', url, "+refs/heads/#{branch}:refs/remotes/#{merge_head}"]
1450
1451 idx = args.index url_arg
1452 args.delete_at idx
1453 args.insert idx, merge_head, '--no-ff', '-m',
1454 "Merge pull request ##{pull_id} from #{merge_head}\n\n#{pull_data['title']}"
1455 end
1456 end
1457
1458 def cherry_pick(args)
1459 unless args.include?('-m') or args.include?('--mainline')
1460 ref = args.words.last
1461 if url = resolve_github_url(ref) and url.project_path =~ /^commit\/([a-f0-9]{7,40})/
1462 sha = $1
1463 project = url.project
1464 elsif ref =~ /^(#{OWNER_RE})@([a-f0-9]{7,40})$/
1465 owner, sha = $1, $2
1466 project = local_repo.main_project.owned_by(owner)
1467 end
1468
1469 if project
1470 args[args.index(ref)] = sha
1471
1472 if remote = project.remote and remotes.include? remote
1473 args.before ['fetch', remote.to_s]
1474 else
1475 args.before ['remote', 'add', '-f', project.owner, project.git_url(:https => https_protocol?)]
1476 end
1477 end
1478 end
1479 end
1480
1481 def am(args)
1482 if url = args.find { |a| a =~ %r{^https?://(gist\.)?github\.com/} }
1483 idx = args.index(url)
1484 gist = $1 == 'gist.'
1485 url = url.sub(/#.+/, '')
1486 url = url.sub(%r{(/pull/\d+)/\w*$}, '\1') unless gist
1487 ext = gist ? '.txt' : '.patch'
1488 url += ext unless File.extname(url) == ext
1489 patch_file = File.join(ENV['TMPDIR'] || '/tmp', "#{gist ? 'gist-' : ''}#{File.basename(url)}")
1490 args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file]
1491 args[idx] = patch_file
1492 end
1493 end
1494
1495 alias_method :apply, :am
1496
1497 def init(args)
1498 if args.delete('-g')
1499 project = github_project(File.basename(current_dir))
1500 url = project.git_url(:private => true, :https => https_protocol?)
1501 args.after ['remote', 'add', 'origin', url]
1502 end
1503 end
1504
1505 def fork(args)
1506 unless project = local_repo.main_project
1507 abort "Error: repository under 'origin' remote is not a GitHub project"
1508 end
1509 forked_project = project.owned_by(github_user(project.host))
1510
1511 existing_repo = api_client.repo_info(forked_project)
1512 if existing_repo.success?
1513 parent_data = existing_repo.data['parent']
1514 parent_url = parent_data && resolve_github_url(parent_data['html_url'])
1515 if !parent_url or parent_url.project != project
1516 abort "Error creating fork: %s already exists on %s" %
1517 [ forked_project.name_with_owner, forked_project.host ]
1518 end
1519 else
1520 api_client.fork_repo(project) unless args.noop?
1521 end
1522
1523 if args.include?('--no-remote')
1524 exit
1525 else
1526 url = forked_project.git_url(:private => true, :https => https_protocol?)
1527 args.replace %W"remote add -f #{forked_project.owner} #{url}"
1528 args.after 'echo', ['new remote:', forked_project.owner]
1529 end
1530 rescue GitHubAPI::Exceptions
1531 display_api_exception("creating fork", $!.response)
1532 exit 1
1533 end
1534
1535 def create(args)
1536 if !is_repo?
1537 abort "'create' must be run from inside a git repository"
1538 else
1539 owner = github_user
1540 args.shift
1541 options = {}
1542 options[:private] = true if args.delete('-p')
1543 new_repo_name = nil
1544
1545 until args.empty?
1546 case arg = args.shift
1547 when '-d'
1548 options[:description] = args.shift
1549 when '-h'
1550 options[:homepage] = args.shift
1551 else
1552 if arg =~ /^[^-]/ and new_repo_name.nil?
1553 new_repo_name = arg
1554 owner, new_repo_name = new_repo_name.split('/', 2) if new_repo_name.index('/')
1555 else
1556 abort "invalid argument: #{arg}"
1557 end
1558 end
1559 end
1560 new_repo_name ||= repo_name
1561 new_project = github_project(new_repo_name, owner)
1562
1563 if api_client.repo_exists?(new_project)
1564 warn "#{new_project.name_with_owner} already exists on #{new_project.host}"
1565 action = "set remote origin"
1566 else
1567 action = "created repository"
1568 unless args.noop?
1569 repo_data = api_client.create_repo(new_project, options)
1570 new_project = github_project(repo_data['full_name'])
1571 end
1572 end
1573
1574 url = new_project.git_url(:private => true, :https => https_protocol?)
1575
1576 if remotes.first != 'origin'
1577 args.replace %W"remote add -f origin #{url}"
1578 else
1579 args.replace %W"remote -v"
1580 end
1581
1582 args.after 'echo', ["#{action}:", new_project.name_with_owner]
1583 end
1584 rescue GitHubAPI::Exceptions
1585 display_api_exception("creating repository", $!.response)
1586 exit 1
1587 end
1588
1589 def push(args)
1590 return if args[1].nil? || !args[1].index(',')
1591
1592 refs = args.words[2..-1]
1593 remotes = args[1].split(',')
1594 args[1] = remotes.shift
1595
1596 if refs.empty?
1597 refs = [current_branch.short_name]
1598 args.concat refs
1599 end
1600
1601 remotes.each do |name|
1602 args.after ['push', name, *refs]
1603 end
1604 end
1605
1606 def browse(args)
1607 args.shift
1608 browse_command(args) do
1609 dest = args.shift
1610 dest = nil if dest == '--'
1611
1612 if dest
1613 project = github_project dest
1614 branch = master_branch
1615 else
1616 project = current_project
1617 branch = current_branch && current_branch.upstream || master_branch
1618 end
1619
1620 abort "Usage: hub browse [<USER>/]<REPOSITORY>" unless project
1621
1622 path = case subpage = args.shift
1623 when 'commits'
1624 "/commits/#{branch.short_name}"
1625 when 'tree', NilClass
1626 "/tree/#{branch.short_name}" if branch and !branch.master?
1627 else
1628 "/#{subpage}"
1629 end
1630
1631 project.web_url(path)
1632 end
1633 end
1634
1635 def compare(args)
1636 args.shift
1637 browse_command(args) do
1638 if args.empty?
1639 branch = current_branch.upstream
1640 if branch and not branch.master?
1641 range = branch.short_name
1642 project = current_project
1643 else
1644 abort "Usage: hub compare [USER] [<START>...]<END>"
1645 end
1646 else
1647 sha_or_tag = /(\w{1,2}|\w[\w.-]+\w)/
1648 range = args.pop.sub(/^#{sha_or_tag}\.\.#{sha_or_tag}$/, '\1...\2')
1649 project = if owner = args.pop then github_project(nil, owner)
1650 else current_project
1651 end
1652 end
1653
1654 project.web_url "/compare/#{range}"
1655 end
1656 end
1657
1658 def hub(args)
1659 return help(args) unless args[1] == 'standalone'
1660 require 'hub/standalone'
1661 Hub::Standalone.build $stdout
1662 exit
1663 rescue LoadError
1664 abort "hub is already running in standalone mode."
1665 rescue Errno::EPIPE
1666 exit # ignore broken pipe
1667 end
1668
1669 def alias(args)
1670 shells = %w[bash zsh sh ksh csh fish]
1671
1672 script = !!args.delete('-s')
1673 shell = args[1] || ENV['SHELL']
1674 abort "hub alias: unknown shell" if shell.nil? or shell.empty?
1675 shell = File.basename shell
1676
1677 unless shells.include? shell
1678 $stderr.puts "hub alias: unsupported shell"
1679 warn "supported shells: #{shells.join(' ')}"
1680 abort
1681 end
1682
1683 if script
1684 puts "alias git=hub"
1685 if 'zsh' == shell
1686 puts "if type compdef >/dev/null; then"
1687 puts " compdef hub=git"
1688 puts "fi"
1689 end
1690 else
1691 profile = case shell
1692 when 'bash' then '~/.bash_profile'
1693 when 'zsh' then '~/.zshrc'
1694 when 'ksh' then '~/.profile'
1695 else
1696 'your profile'
1697 end
1698
1699 puts "# Wrap git automatically by adding the following to #{profile}:"
1700 puts
1701 puts 'eval "$(hub alias -s)"'
1702 end
1703
1704 exit
1705 end
1706
1707 def version(args)
1708 args.after 'echo', ['hub version', Version]
1709 end
1710 alias_method "--version", :version
1711
1712 def help(args)
1713 command = args.words[1]
1714
1715 if command == 'hub'
1716 puts hub_manpage
1717 exit
1718 elsif command.nil? && !args.has_flag?('-a', '--all')
1719 ENV['GIT_PAGER'] = '' unless args.has_flag?('-p', '--paginate') # Use `cat`.
1720 puts improved_help_text
1721 exit
1722 end
1723 end
1724 alias_method "--help", :help
1725
1726 private
1727
1728 def api_client
1729 @api_client ||= begin
1730 config_file = ENV['HUB_CONFIG'] || '~/.config/hub'
1731 file_store = GitHubAPI::FileStore.new File.expand_path(config_file)
1732 file_config = GitHubAPI::Configuration.new file_store
1733 GitHubAPI.new file_config, :app_url => 'http://defunkt.io/hub/'
1734 end
1735 end
1736
1737 def github_user host = nil, &block
1738 host ||= (local_repo(false) || Context::LocalRepo).default_host
1739 api_client.config.username(host, &block)
1740 end
1741
1742 def custom_command? cmd
1743 CUSTOM_COMMANDS.include? cmd
1744 end
1745
1746 def respect_help_flags args
1747 return if args.size > 2
1748 case args[1]
1749 when '-h'
1750 pattern = /(git|hub) #{Regexp.escape args[0].gsub('-', '\-')}/
1751 hub_raw_manpage.each_line { |line|
1752 if line =~ pattern
1753 $stderr.print "Usage: "
1754 $stderr.puts line.gsub(/\\f./, '').gsub('\-', '-')
1755 abort
1756 end
1757 }
1758 abort "Error: couldn't find usage help for #{args[0]}"
1759 when '--help'
1760 puts hub_manpage
1761 exit
1762 end
1763 end
1764
1765 def improved_help_text
1766 <<-help
1767 usage: git [--version] [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
1768 [-p|--paginate|--no-pager] [--no-replace-objects] [--bare]
1769 [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
1770 [-c name=value] [--help]
1771 <command> [<args>]
1772
1773 Basic Commands:
1774 init Create an empty git repository or reinitialize an existing one
1775 add Add new or modified files to the staging area
1776 rm Remove files from the working directory and staging area
1777 mv Move or rename a file, a directory, or a symlink
1778 status Show the status of the working directory and staging area
1779 commit Record changes to the repository
1780
1781 History Commands:
1782 log Show the commit history log
1783 diff Show changes between commits, commit and working tree, etc
1784 show Show information about commits, tags or files
1785
1786 Branching Commands:
1787 branch List, create, or delete branches
1788 checkout Switch the active branch to another branch
1789 merge Join two or more development histories (branches) together
1790 tag Create, list, delete, sign or verify a tag object
1791
1792 Remote Commands:
1793 clone Clone a remote repository into a new directory
1794 fetch Download data, tags and branches from a remote repository
1795 pull Fetch from and merge with another repository or a local branch
1796 push Upload data, tags and branches to a remote repository
1797 remote View and manage a set of remote repositories
1798
1799 Advanced Commands:
1800 reset Reset your staging area or working directory to another point
1801 rebase Re-apply a series of patches in one branch onto another
1802 bisect Find by binary search the change that introduced a bug
1803 grep Print files with lines matching a pattern in your codebase
1804
1805 GitHub Commands:
1806 pull-request Open a pull request on GitHub
1807 fork Make a fork of a remote repository on GitHub and add as remote
1808 create Create this repository on GitHub and add GitHub as origin
1809 browse Open a GitHub page in the default browser
1810 compare Open a compare page on GitHub
1811
1812 See 'git help <command>' for more information on a specific command.
1813 help
1814 end
1815
1816 def slurp_global_flags(args)
1817 flags = %w[ --noop -c -p --paginate --no-pager --no-replace-objects --bare --version --help ]
1818 flags2 = %w[ --exec-path= --git-dir= --work-tree= ]
1819
1820 globals = []
1821 locals = []
1822
1823 while args[0] && (flags.include?(args[0]) || flags2.any? {|f| args[0].index(f) == 0 })
1824 flag = args.shift
1825 case flag
1826 when '--noop'
1827 args.noop!
1828 when '--version', '--help'
1829 args.unshift flag.sub('--', '')
1830 when '-c'
1831 config_pair = args.shift
1832 key, value = config_pair.split('=', 2)
1833 git_reader.stub_config_value(key, value)
1834
1835 globals << flag << config_pair
1836 when '-p', '--paginate', '--no-pager'
1837 locals << flag
1838 else
1839 globals << flag
1840 end
1841 end
1842
1843 git_reader.add_exec_flags(globals)
1844 args.add_exec_flags(globals)
1845 args.add_exec_flags(locals)
1846 end
1847
1848 def browse_command(args)
1849 url_only = args.delete('-u')
1850 warn "Warning: the `-p` flag has no effect anymore" if args.delete('-p')
1851 url = yield
1852
1853 args.executable = url_only ? 'echo' : browser_launcher
1854 args.push url
1855 end
1856
1857 def hub_manpage
1858 abort "** Can't find groff(1)" unless command?('groff')
1859
1860 require 'open3'
1861 out = nil
1862 Open3.popen3(groff_command) do |stdin, stdout, _|
1863 stdin.puts hub_raw_manpage
1864 stdin.close
1865 out = stdout.read.strip
1866 end
1867 out
1868 end
1869
1870 def groff_command
1871 "groff -Wall -mtty-char -mandoc -Tascii"
1872 end
1873
1874 def hub_raw_manpage
1875 if File.exists? file = File.dirname(__FILE__) + '/../../man/hub.1'
1876 File.read(file)
1877 else
1878 DATA.read
1879 end
1880 end
1881
1882 def puts(*args)
1883 page_stdout
1884 super
1885 end
1886
1887 def page_stdout
1888 return if not $stdout.tty? or windows?
1889
1890 read, write = IO.pipe
1891
1892 if Kernel.fork
1893 $stdin.reopen(read)
1894 read.close
1895 write.close
1896
1897 ENV['LESS'] = 'FSRX'
1898
1899 Kernel.select [STDIN]
1900
1901 pager = ENV['GIT_PAGER'] ||
1902 `git config --get-all core.pager`.split.first || ENV['PAGER'] ||
1903 'less -isr'
1904
1905 pager = 'cat' if pager.empty?
1906
1907 exec pager rescue exec "/bin/sh", "-c", pager
1908 else
1909 $stdout.reopen(write)
1910 $stderr.reopen(write) if $stderr.tty?
1911 read.close
1912 write.close
1913 end
1914 rescue NotImplementedError
1915 end
1916
1917 def pullrequest_editmsg(changes)
1918 message_file = File.join(git_dir, 'PULLREQ_EDITMSG')
1919 File.open(message_file, 'w') { |msg|
1920 yield msg
1921 if changes
1922 msg.puts "#\n# Changes:\n#"
1923 msg.puts changes.gsub(/^/, '# ').gsub(/ +$/, '')
1924 end
1925 }
1926 edit_cmd = Array(git_editor).dup
1927 edit_cmd << '-c' << 'set ft=gitcommit' if edit_cmd[0] =~ /^[mg]?vim$/
1928 edit_cmd << message_file
1929 system(*edit_cmd)
1930 abort "can't open text editor for pull request message" unless $?.success?
1931 title, body = read_editmsg(message_file)
1932 abort "Aborting due to empty pull request title" unless title
1933 [title, body]
1934 end
1935
1936 def read_editmsg(file)
1937 title, body = '', ''
1938 File.open(file, 'r') { |msg|
1939 msg.each_line do |line|
1940 next if line.index('#') == 0
1941 ((body.empty? and line =~ /\S/) ? title : body) << line
1942 end
1943 }
1944 title.tr!("\n", ' ')
1945 title.strip!
1946 body.strip!
1947
1948 [title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil]
1949 end
1950
1951 def expand_alias(cmd)
1952 if expanded = git_alias_for(cmd)
1953 if expanded.index('!') != 0
1954 require 'shellwords' unless defined?(::Shellwords)
1955 Shellwords.shellwords(expanded)
1956 end
1957 end
1958 end
1959
1960 def display_api_exception(action, response)
1961 $stderr.puts "Error #{action}: #{response.message.strip} (HTTP #{response.status})"
1962 if 422 == response.status and response.error_message?
1963 msg = response.error_message
1964 msg = msg.join("\n") if msg.respond_to? :join
1965 warn msg
1966 end
1967 end
1968
1969 end
1970 end
1971
1972 module Hub
1973 class Runner
1974 attr_reader :args
1975
1976 def initialize(*args)
1977 @args = Args.new(args)
1978 Commands.run(@args)
1979 end
1980
1981 def self.execute(*args)
1982 new(*args).execute
1983 end
1984
1985 def command
1986 if args.skip?
1987 ''
1988 else
1989 commands.join('; ')
1990 end
1991 end
1992
1993 def commands
1994 args.commands.map do |cmd|
1995 if cmd.respond_to?(:join)
1996 cmd.map { |arg| arg = arg.to_s; (arg.index(' ') || arg.empty?) ? "'#{arg}'" : arg }.join(' ')
1997 else
1998 cmd.to_s
1999 end
2000 end
2001 end
2002
2003 def execute
2004 if args.noop?
2005 puts commands
2006 elsif not args.skip?
2007 if args.chained?
2008 execute_command_chain
2009 else
2010 exec(*args.to_exec)
2011 end
2012 end
2013 end
2014
2015 def execute_command_chain
2016 commands = args.commands
2017 commands.each_with_index do |cmd, i|
2018 if cmd.respond_to?(:call) then cmd.call
2019 elsif i == commands.length - 1
2020 exec(*cmd)
2021 else
2022 exit($?.exitstatus) unless system(*cmd)
2023 end
2024 end
2025 end
2026 end
2027 end
2028
2029 Hub::Runner.execute(*ARGV)
2030
2031 __END__
2032 .\" generated with Ronn/v0.7.3
2033 .\" http://github.com/rtomayko/ronn/tree/0.7.3
2034 .
2035 .TH "HUB" "1" "November 2012" "DEFUNKT" "Git Manual"
2036 .
2037 .SH "NAME"
2038 \fBhub\fR \- git + hub = github
2039 .
2040 .SH "SYNOPSIS"
2041 \fBhub\fR [\fB\-\-noop\fR] \fICOMMAND\fR \fIOPTIONS\fR
2042 .
2043 .br
2044 \fBhub alias\fR [\fB\-s\fR] [\fISHELL\fR]
2045 .
2046 .SS "Expanded git commands:"
2047 \fBgit init \-g\fR \fIOPTIONS\fR
2048 .
2049 .br
2050 \fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR
2051 .
2052 .br
2053 \fBgit remote add\fR [\fB\-p\fR] \fIOPTIONS\fR \fIUSER\fR[/\fIREPOSITORY\fR]
2054 .
2055 .br
2056 \fBgit remote set\-url\fR [\fB\-p\fR] \fIOPTIONS\fR \fIREMOTE\-NAME\fR \fIUSER\fR[/\fIREPOSITORY\fR]
2057 .
2058 .br
2059 \fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.]
2060 .
2061 .br
2062 \fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR]
2063 .
2064 .br
2065 \fBgit merge\fR \fIPULLREQ\-URL\fR
2066 .
2067 .br
2068 \fBgit cherry\-pick\fR \fIGITHUB\-REF\fR
2069 .
2070 .br
2071 \fBgit am\fR \fIGITHUB\-URL\fR
2072 .
2073 .br
2074 \fBgit apply\fR \fIGITHUB\-URL\fR
2075 .
2076 .br
2077 \fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR]
2078 .
2079 .br
2080 \fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR
2081 .
2082 .SS "Custom git commands:"
2083 \fBgit create\fR [\fINAME\fR] [\fB\-p\fR] [\fB\-d\fR \fIDESCRIPTION\fR] [\fB\-h\fR \fIHOMEPAGE\fR]
2084 .
2085 .br
2086 \fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE]
2087 .
2088 .br
2089 \fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR
2090 .
2091 .br
2092 \fBgit fork\fR [\fB\-\-no\-remote\fR]
2093 .
2094 .br
2095 \fBgit pull\-request\fR [\fB\-f\fR] [\fITITLE\fR|\fB\-i\fR \fIISSUE\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR]
2096 .
2097 .SH "DESCRIPTION"
2098 hub enhances various git commands to ease most common workflows with GitHub\.
2099 .
2100 .TP
2101 \fBhub \-\-noop\fR \fICOMMAND\fR
2102 Shows which command(s) would be run as a result of the current command\. Doesn\'t perform anything\.
2103 .
2104 .TP
2105 \fBhub alias\fR [\fB\-s\fR] [\fISHELL\fR]
2106 Shows shell instructions for wrapping git\. If given, \fISHELL\fR specifies the type of shell; otherwise defaults to the value of SHELL environment variable\. With \fB\-s\fR, outputs shell script suitable for \fBeval\fR\.
2107 .
2108 .TP
2109 \fBgit init\fR \fB\-g\fR \fIOPTIONS\fR
2110 Create a git repository as with git\-init(1) and add remote \fBorigin\fR at "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"; \fIUSER\fR is your GitHub username and \fIREPOSITORY\fR is the current working directory\'s basename\.
2111 .
2112 .TP
2113 \fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR \fIDIRECTORY\fR
2114 Clone repository "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" into \fIDIRECTORY\fR as with git\-clone(1)\. When \fIUSER\fR/ is omitted, assumes your GitHub login\. With \fB\-p\fR, clone private repositories over SSH\. For repositories under your GitHub login, \fB\-p\fR is implicit\.
2115 .
2116 .TP
2117 \fBgit remote add\fR [\fB\-p\fR] \fIOPTIONS\fR \fIUSER\fR[\fB/\fR\fIREPOSITORY\fR]
2118 Add remote "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" as with git\-remote(1)\. When /\fIREPOSITORY\fR is omitted, the basename of the current working directory is used\. With \fB\-p\fR, use private remote "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"\. If \fIUSER\fR is "origin" then uses your GitHub login\.
2119 .
2120 .TP
2121 \fBgit remote set\-url\fR [\fB\-p\fR] \fIOPTIONS\fR \fIREMOTE\-NAME\fR \fIUSER\fR[/\fIREPOSITORY\fR]
2122 Sets the url of remote \fIREMOTE\-NAME\fR using the same rules as \fBgit remote add\fR\.
2123 .
2124 .TP
2125 \fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.]
2126 Adds missing remote(s) with \fBgit remote add\fR prior to fetching\. New remotes are only added if they correspond to valid forks on GitHub\.
2127 .
2128 .TP
2129 \fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR]
2130 Checks out the head of the pull request as a local branch, to allow for reviewing, rebasing and otherwise cleaning up the commits in the pull request before merging\. The name of the local branch can explicitly be set with \fIBRANCH\fR\.
2131 .
2132 .TP
2133 \fBgit merge\fR \fIPULLREQ\-URL\fR
2134 Merge the pull request with a commit message that includes the pull request ID and title, similar to the GitHub Merge Button\.
2135 .
2136 .TP
2137 \fBgit cherry\-pick\fR \fIGITHUB\-REF\fR
2138 Cherry\-pick a commit from a fork using either full URL to the commit or GitHub\-flavored Markdown notation, which is \fBuser@sha\fR\. If the remote doesn\'t yet exist, it will be added\. A \fBgit fetch <user>\fR is issued prior to the cherry\-pick attempt\.
2139 .
2140 .TP
2141 \fBgit [am|apply]\fR \fIGITHUB\-URL\fR
2142 Downloads the patch file for the pull request or commit at the URL and applies that patch from disk with \fBgit am\fR or \fBgit apply\fR\. Similar to \fBcherry\-pick\fR, but doesn\'t add new remotes\. \fBgit am\fR creates commits while preserving authorship info while \fBapply\fR only applies the patch to the working copy\.
2143 .
2144 .TP
2145 \fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR]
2146 Push \fIREF\fR to each of \fIREMOTE\-1\fR through \fIREMOTE\-N\fR by executing multiple \fBgit push\fR commands\.
2147 .
2148 .TP
2149 \fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR
2150 Submodule repository "git://github\.com/\fIUSER\fR/\fIREPOSITORY\fR\.git" into \fIDIRECTORY\fR as with git\-submodule(1)\. When \fIUSER\fR/ is omitted, assumes your GitHub login\. With \fB\-p\fR, use private remote "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"\.
2151 .
2152 .TP
2153 \fBgit help\fR
2154 Display enhanced git\-help(1)\.
2155 .
2156 .P
2157 hub also adds some custom commands that are otherwise not present in git:
2158 .
2159 .TP
2160 \fBgit create\fR [\fINAME\fR] [\fB\-p\fR] [\fB\-d\fR \fIDESCRIPTION\fR] [\fB\-h\fR \fIHOMEPAGE\fR]
2161 Create a new public GitHub repository from the current git repository and add remote \fBorigin\fR at "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"; \fIUSER\fR is your GitHub username and \fIREPOSITORY\fR is the current working directory name\. To explicitly name the new repository, pass in \fINAME\fR, optionally in \fIORGANIZATION\fR/\fINAME\fR form to create under an organization you\'re a member of\. With \fB\-p\fR, create a private repository, and with \fB\-d\fR and \fB\-h\fR set the repository\'s description and homepage URL, respectively\.
2162 .
2163 .TP
2164 \fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE]
2165 Open repository\'s GitHub page in the system\'s default web browser using \fBopen(1)\fR or the \fBBROWSER\fR env variable\. If the repository isn\'t specified, \fBbrowse\fR opens the page of the repository found in the current directory\. If SUBPAGE is specified, the browser will open on the specified subpage: one of "wiki", "commits", "issues" or other (the default is "tree")\.
2166 .
2167 .TP
2168 \fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR
2169 Open a GitHub compare view page in the system\'s default web browser\. \fISTART\fR to \fIEND\fR are branch names, tag names, or commit SHA1s specifying the range of history to compare\. If a range with two dots (\fBa\.\.b\fR) is given, it will be transformed into one with three dots\. If \fISTART\fR is omitted, GitHub will compare against the base branch (the default is "master")\.
2170 .
2171 .TP
2172 \fBgit fork\fR [\fB\-\-no\-remote\fR]
2173 Forks the original project (referenced by "origin" remote) on GitHub and adds a new remote for it under your username\.
2174 .
2175 .TP
2176 \fBgit pull\-request\fR [\fB\-f\fR] [\fITITLE\fR|\fB\-i\fR \fIISSUE\fR|\fIISSUE\-URL\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR]
2177 Opens a pull request on GitHub for the project that the "origin" remote points to\. The default head of the pull request is the current branch\. Both base and head of the pull request can be explicitly given in one of the following formats: "branch", "owner:branch", "owner/repo:branch"\. This command will abort operation if it detects that the current topic branch has local commits that are not yet pushed to its upstream branch on the remote\. To skip this check, use \fB\-f\fR\.
2178 .
2179 .IP
2180 If \fITITLE\fR is omitted, a text editor will open in which title and body of the pull request can be entered in the same manner as git commit message\.
2181 .
2182 .IP
2183 If instead of normal \fITITLE\fR an issue number is given with \fB\-i\fR, the pull request will be attached to an existing GitHub issue\. Alternatively, instead of title you can paste a full URL to an issue on GitHub\.
2184 .
2185 .SH "CONFIGURATION"
2186 Hub will prompt for GitHub username & password the first time it needs to access the API and exchange it for an OAuth token, which it saves in "~/\.config/hub"\.
2187 .
2188 .P
2189 To avoid being prompted, use \fIGITHUB_USER\fR and \fIGITHUB_PASSWORD\fR environment variables\.
2190 .
2191 .P
2192 If you prefer the HTTPS protocol for GitHub repositories, you can set "hub\.protocol" to "https"\. This will affect \fBclone\fR, \fBfork\fR, \fBremote add\fR and other operations that expand references to GitHub repositories as full URLs that otherwise use git and ssh protocols\.
2193 .
2194 .IP "" 4
2195 .
2196 .nf
2197
2198 $ git config \-\-global hub\.protocol https
2199 .
2200 .fi
2201 .
2202 .IP "" 0
2203 .
2204 .SS "GitHub Enterprise"
2205 By default, hub will only work with repositories that have remotes which point to github\.com\. GitHub Enterprise hosts need to be whitelisted to configure hub to treat such remotes same as github\.com:
2206 .
2207 .IP "" 4
2208 .
2209 .nf
2210
2211 $ git config \-\-global \-\-add hub\.host my\.git\.org
2212 .
2213 .fi
2214 .
2215 .IP "" 0
2216 .
2217 .P
2218 The default host for commands like \fBinit\fR and \fBclone\fR is still github\.com, but this can be affected with the \fIGITHUB_HOST\fR environment variable:
2219 .
2220 .IP "" 4
2221 .
2222 .nf
2223
2224 $ GITHUB_HOST=my\.git\.org git clone myproject
2225 .
2226 .fi
2227 .
2228 .IP "" 0
2229 .
2230 .SH "EXAMPLES"
2231 .
2232 .SS "git clone"
2233 .
2234 .nf
2235
2236 $ git clone schacon/ticgit
2237 > git clone git://github\.com/schacon/ticgit\.git
2238
2239 $ git clone \-p schacon/ticgit
2240 > git clone git@github\.com:schacon/ticgit\.git
2241
2242 $ git clone resque
2243 > git clone git@github\.com/YOUR_USER/resque\.git
2244 .
2245 .fi
2246 .
2247 .SS "git remote add"
2248 .
2249 .nf
2250
2251 $ git remote add rtomayko
2252 > git remote add rtomayko git://github\.com/rtomayko/CURRENT_REPO\.git
2253
2254 $ git remote add \-p rtomayko
2255 > git remote add rtomayko git@github\.com:rtomayko/CURRENT_REPO\.git
2256
2257 $ git remote add origin
2258 > git remote add origin git://github\.com/YOUR_USER/CURRENT_REPO\.git
2259 .
2260 .fi
2261 .
2262 .SS "git fetch"
2263 .
2264 .nf
2265
2266 $ git fetch mislav
2267 > git remote add mislav git://github\.com/mislav/REPO\.git
2268 > git fetch mislav
2269
2270 $ git fetch mislav,xoebus
2271 > git remote add mislav \.\.\.
2272 > git remote add xoebus \.\.\.
2273 > git fetch \-\-multiple mislav xoebus
2274 .
2275 .fi
2276 .
2277 .SS "git cherry\-pick"
2278 .
2279 .nf
2280
2281 $ git cherry\-pick http://github\.com/mislav/REPO/commit/SHA
2282 > git remote add \-f mislav git://github\.com/mislav/REPO\.git
2283 > git cherry\-pick SHA
2284
2285 $ git cherry\-pick mislav@SHA
2286 > git remote add \-f mislav git://github\.com/mislav/CURRENT_REPO\.git
2287 > git cherry\-pick SHA
2288
2289 $ git cherry\-pick mislav@SHA
2290 > git fetch mislav
2291 > git cherry\-pick SHA
2292 .
2293 .fi
2294 .
2295 .SS "git am, git apply"
2296 .
2297 .nf
2298
2299 $ git am https://github\.com/defunkt/hub/pull/55
2300 > curl https://github\.com/defunkt/hub/pull/55\.patch \-o /tmp/55\.patch
2301 > git am /tmp/55\.patch
2302
2303 $ git am \-\-ignore\-whitespace https://github\.com/davidbalbert/hub/commit/fdb9921
2304 > curl https://github\.com/davidbalbert/hub/commit/fdb9921\.patch \-o /tmp/fdb9921\.patch
2305 > git am \-\-ignore\-whitespace /tmp/fdb9921\.patch
2306
2307 $ git apply https://gist\.github\.com/8da7fb575debd88c54cf
2308 > curl https://gist\.github\.com/8da7fb575debd88c54cf\.txt \-o /tmp/gist\-8da7fb575debd88c54cf\.txt
2309 > git apply /tmp/gist\-8da7fb575debd88c54cf\.txt
2310 .
2311 .fi
2312 .
2313 .SS "git fork"
2314 .
2315 .nf
2316
2317 $ git fork
2318 [ repo forked on GitHub ]
2319 > git remote add \-f YOUR_USER git@github\.com:YOUR_USER/CURRENT_REPO\.git
2320 .
2321 .fi
2322 .
2323 .SS "git pull\-request"
2324 .
2325 .nf
2326
2327 # while on a topic branch called "feature":
2328 $ git pull\-request
2329 [ opens text editor to edit title & body for the request ]
2330 [ opened pull request on GitHub for "YOUR_USER:feature" ]
2331
2332 # explicit title, pull base & head:
2333 $ git pull\-request "I\'ve implemented feature X" \-b defunkt:master \-h mislav:feature
2334
2335 $ git pull\-request \-i 123
2336 [ attached pull request to issue #123 ]
2337 .
2338 .fi
2339 .
2340 .SS "git checkout"
2341 .
2342 .nf
2343
2344 $ git checkout https://github\.com/defunkt/hub/pull/73
2345 > git remote add \-f \-t feature git://github:com/mislav/hub\.git
2346 > git checkout \-\-track \-B mislav\-feature mislav/feature
2347
2348 $ git checkout https://github\.com/defunkt/hub/pull/73 custom\-branch\-name
2349 .
2350 .fi
2351 .
2352 .SS "git merge"
2353 .
2354 .nf
2355
2356 $ git merge https://github\.com/defunkt/hub/pull/73
2357 > git fetch git://github\.com/mislav/hub\.git +refs/heads/feature:refs/remotes/mislav/feature
2358 > git merge mislav/feature \-\-no\-ff \-m \'Merge pull request #73 from mislav/feature\.\.\.\'
2359 .
2360 .fi
2361 .
2362 .SS "git create"
2363 .
2364 .nf
2365
2366 $ git create
2367 [ repo created on GitHub ]
2368 > git remote add origin git@github\.com:YOUR_USER/CURRENT_REPO\.git
2369
2370 # with description:
2371 $ git create \-d \'It shall be mine, all mine!\'
2372
2373 $ git create recipes
2374 [ repo created on GitHub ]
2375 > git remote add origin git@github\.com:YOUR_USER/recipes\.git
2376
2377 $ git create sinatra/recipes
2378 [ repo created in GitHub organization ]
2379 > git remote add origin git@github\.com:sinatra/recipes\.git
2380 .
2381 .fi
2382 .
2383 .SS "git init"
2384 .
2385 .nf
2386
2387 $ git init \-g
2388 > git init
2389 > git remote add origin git@github\.com:YOUR_USER/REPO\.git
2390 .
2391 .fi
2392 .
2393 .SS "git push"
2394 .
2395 .nf
2396
2397 $ git push origin,staging,qa bert_timeout
2398 > git push origin bert_timeout
2399 > git push staging bert_timeout
2400 > git push qa bert_timeout
2401 .
2402 .fi
2403 .
2404 .SS "git browse"
2405 .
2406 .nf
2407
2408 $ git browse
2409 > open https://github\.com/YOUR_USER/CURRENT_REPO
2410
2411 $ git browse \-\- commit/SHA
2412 > open https://github\.com/YOUR_USER/CURRENT_REPO/commit/SHA
2413
2414 $ git browse \-\- issues
2415 > open https://github\.com/YOUR_USER/CURRENT_REPO/issues
2416
2417 $ git browse schacon/ticgit
2418 > open https://github\.com/schacon/ticgit
2419
2420 $ git browse schacon/ticgit commit/SHA
2421 > open https://github\.com/schacon/ticgit/commit/SHA
2422
2423 $ git browse resque
2424 > open https://github\.com/YOUR_USER/resque
2425
2426 $ git browse resque network
2427 > open https://github\.com/YOUR_USER/resque/network
2428 .
2429 .fi
2430 .
2431 .SS "git compare"
2432 .
2433 .nf
2434
2435 $ git compare refactor
2436 > open https://github\.com/CURRENT_REPO/compare/refactor
2437
2438 $ git compare 1\.0\.\.1\.1
2439 > open https://github\.com/CURRENT_REPO/compare/1\.0\.\.\.1\.1
2440
2441 $ git compare \-u fix
2442 > (https://github\.com/CURRENT_REPO/compare/fix)
2443
2444 $ git compare other\-user patch
2445 > open https://github\.com/other\-user/REPO/compare/patch
2446 .
2447 .fi
2448 .
2449 .SS "git submodule"
2450 .
2451 .nf
2452
2453 $ hub submodule add wycats/bundler vendor/bundler
2454 > git submodule add git://github\.com/wycats/bundler\.git vendor/bundler
2455
2456 $ hub submodule add \-p wycats/bundler vendor/bundler
2457 > git submodule add git@github\.com:wycats/bundler\.git vendor/bundler
2458
2459 $ hub submodule add \-b ryppl ryppl/pip vendor/pip
2460 > git submodule add \-b ryppl git://github\.com/ryppl/pip\.git vendor/pip
2461 .
2462 .fi
2463 .
2464 .SS "git help"
2465 .
2466 .nf
2467
2468 $ git help
2469 > (improved git help)
2470 $ git help hub
2471 > (hub man page)
2472 .
2473 .fi
2474 .
2475 .SH "BUGS"
2476 \fIhttps://github\.com/defunkt/hub/issues\fR
2477 .
2478 .SH "AUTHORS"
2479 \fIhttps://github\.com/defunkt/hub/contributors\fR
2480 .
2481 .SH "SEE ALSO"
2482 git(1), git\-clone(1), git\-remote(1), git\-init(1), \fIhttp://github\.com\fR, \fIhttps://github\.com/defunkt/hub\fR