From f795951a5f293c9923b1d25b09116be93185a970 Mon Sep 17 00:00:00 2001 From: Tony Duckles Date: Tue, 25 Dec 2012 23:40:49 -0600 Subject: [PATCH] bin/hub: hub version 1.10.3 --- bin/hub | 2175 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 1714 insertions(+), 461 deletions(-) diff --git a/bin/hub b/bin/hub index 74ca08d..9f3cdaa 100755 --- a/bin/hub +++ b/bin/hub @@ -1,14 +1,15 @@ #!/usr/bin/env ruby # -# This file, hub, is generated code. -# Please DO NOT EDIT or send patches for it. +# This file is generated code. DO NOT send patches for it. # -# Please take a look at the source from -# http://github.com/defunkt/hub -# and submit patches against the individual files -# that build hub. +# Original source files with comments are at: +# https://github.com/defunkt/hub # +module Hub + Version = VERSION = '1.10.3' +end + module Hub class Args < Array attr_accessor :executable @@ -17,7 +18,7 @@ module Hub super @executable = ENV["GIT"] || "git" @after = nil - @skip = false + @skip = @noop = false @original_args = args.first @chain = [nil] end @@ -41,17 +42,29 @@ module Hub end def skip! - @skip ||= true + @skip = true end - + def skip? @skip end + def noop! + @noop = true + end + + def noop? + @noop + end + def to_exec(args = self) Array(executable) + args end + def add_exec_flags(flags) + self.executable = Array(executable).concat(flags) + end + def words reject { |arg| arg.index('-') == 0 } end @@ -86,18 +99,1075 @@ module Hub end end end + module Hub - module Context; end + class SshConfig + CONFIG_FILES = %w(~/.ssh/config /etc/ssh_config /etc/ssh/ssh_config) + + def initialize files = nil + @settings = Hash.new {|h,k| h[k] = {} } + Array(files || CONFIG_FILES).each do |path| + file = File.expand_path path + parse_file file if File.exist? file + end + end + + def get_value hostname, key + key = key.to_s.downcase + @settings.each do |pattern, settings| + if pattern.match? hostname and found = settings[key] + return found + end + end + yield + end + + class HostPattern + def initialize pattern + @pattern = pattern.to_s.downcase + end + + def to_s() @pattern end + def ==(other) other.to_s == self.to_s end + + def matcher + @matcher ||= + if '*' == @pattern + Proc.new { true } + elsif @pattern !~ /[?*]/ + lambda { |hostname| hostname.to_s.downcase == @pattern } + else + re = self.class.pattern_to_regexp @pattern + lambda { |hostname| re =~ hostname } + end + end + + def match? hostname + matcher.call hostname + end + + def self.pattern_to_regexp pattern + escaped = Regexp.escape(pattern) + escaped.gsub!('\*', '.*') + escaped.gsub!('\?', '.') + /^#{escaped}$/i + end + end + + def parse_file file + host_patterns = [HostPattern.new('*')] + + IO.foreach(file) do |line| + case line + when /^\s*(#|$)/ then next + when /^\s*(\S+)\s*=/ + key, value = $1, $' + else + key, value = line.strip.split(/\s+/, 2) + end + + next if value.nil? + key.downcase! + value = $1 if value =~ /^"(.*)"$/ + value.chomp! + + if 'host' == key + host_patterns = value.split(/\s+/).map {|p| HostPattern.new p } + else + record_setting key, value, host_patterns + end + end + end + + def record_setting key, value, patterns + patterns.each do |pattern| + @settings[pattern][key] ||= value + end + end + end +end + +require 'uri' +require 'yaml' +require 'forwardable' +require 'fileutils' + +module Hub + class GitHubAPI + attr_reader :config, :oauth_app_url + + def initialize config, options + @config = config + @oauth_app_url = options.fetch(:app_url) + end + + module Exceptions + def self.===(exception) + exception.class.ancestors.map {|a| a.to_s }.include? 'Net::HTTPExceptions' + end + end + + def api_host host + host = host.downcase + 'github.com' == host ? 'api.github.com' : host + end + + def repo_info project + get "https://%s/repos/%s/%s" % + [api_host(project.host), project.owner, project.name] + end + + def repo_exists? project + repo_info(project).success? + end + + def fork_repo project + res = post "https://%s/repos/%s/%s/forks" % + [api_host(project.host), project.owner, project.name] + res.error! unless res.success? + end + + def create_repo project, options = {} + is_org = project.owner != config.username(api_host(project.host)) + params = { :name => project.name, :private => !!options[:private] } + params[:description] = options[:description] if options[:description] + params[:homepage] = options[:homepage] if options[:homepage] + + if is_org + res = post "https://%s/orgs/%s/repos" % [api_host(project.host), project.owner], params + else + res = post "https://%s/user/repos" % api_host(project.host), params + end + res.error! unless res.success? + res.data + end + + def pullrequest_info project, pull_id + res = get "https://%s/repos/%s/%s/pulls/%d" % + [api_host(project.host), project.owner, project.name, pull_id] + res.error! unless res.success? + res.data + end + + def create_pullrequest options + project = options.fetch(:project) + params = { + :base => options.fetch(:base), + :head => options.fetch(:head) + } + + if options[:issue] + params[:issue] = options[:issue] + else + params[:title] = options[:title] if options[:title] + params[:body] = options[:body] if options[:body] + end + + res = post "https://%s/repos/%s/%s/pulls" % + [api_host(project.host), project.owner, project.name], params + + res.error! unless res.success? + res.data + end + + module HttpMethods + module ResponseMethods + def status() code.to_i end + def data?() content_type =~ /\bjson\b/ end + def data() @data ||= JSON.parse(body) end + def error_message?() data? and data['errors'] || data['message'] end + def error_message() error_sentences || data['message'] end + def success?() Net::HTTPSuccess === self end + def error_sentences + data['errors'].map do |err| + case err['code'] + when 'custom' then err['message'] + when 'missing_field' then "field '%s' is missing" % err['field'] + end + end.compact if data['errors'] + end + end + + def get url, &block + perform_request url, :Get, &block + end + + def post url, params = nil + perform_request url, :Post do |req| + if params + req.body = JSON.dump params + req['Content-Type'] = 'application/json;charset=utf-8' + end + yield req if block_given? + req['Content-Length'] = byte_size req.body + end + end + + def byte_size str + if str.respond_to? :bytesize then str.bytesize + elsif str.respond_to? :length then str.length + else 0 + end + end + + def post_form url, params + post(url) {|req| req.set_form_data params } + end + + def perform_request url, type + url = URI.parse url unless url.respond_to? :host + + require 'net/https' + req = Net::HTTP.const_get(type).new request_uri(url) + http = configure_connection(req, url) do |host_url| + create_connection host_url + end + + apply_authentication(req, url) + yield req if block_given? + + begin + res = http.start { http.request(req) } + res.extend ResponseMethods + return res + rescue SocketError => err + raise Context::FatalError, "error with #{type.to_s.upcase} #{url} (#{err.message})" + end + end + + def request_uri url + str = url.request_uri + str = '/api/v3' << str if url.host != 'api.github.com' + str + end + + def configure_connection req, url + if ENV['HUB_TEST_HOST'] + req['Host'] = url.host + url = url.dup + url.scheme = 'http' + url.host, test_port = ENV['HUB_TEST_HOST'].split(':') + url.port = test_port.to_i if test_port + end + yield url + end + + def apply_authentication req, url + user = url.user || config.username(url.host) + pass = config.password(url.host, user) + req.basic_auth user, pass + end + + def create_connection url + use_ssl = 'https' == url.scheme + + proxy_args = [] + if proxy = config.proxy_uri(use_ssl) + proxy_args << proxy.host << proxy.port + if proxy.userinfo + require 'cgi' + proxy_args.concat proxy.userinfo.split(':', 2).map {|a| CGI.unescape a } + end + end + + http = Net::HTTP.new(url.host, url.port, *proxy_args) + + if http.use_ssl = use_ssl + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + return http + end + end + + module OAuth + def apply_authentication req, url + if (req.path =~ /\/authorizations$/) + super + else + user = url.user || config.username(url.host) + token = config.oauth_token(url.host, user) { + obtain_oauth_token url.host, user + } + req['Authorization'] = "token #{token}" + end + end + + def obtain_oauth_token host, user + res = get "https://#{user}@#{host}/authorizations" + res.error! unless res.success? + + if found = res.data.find {|auth| auth['app']['url'] == oauth_app_url } + found['token'] + else + res = post "https://#{user}@#{host}/authorizations", + :scopes => %w[repo], :note => 'hub', :note_url => oauth_app_url + res.error! unless res.success? + res.data['token'] + end + end + end + + include HttpMethods + include OAuth + + class FileStore + extend Forwardable + def_delegator :@data, :[], :get + def_delegator :@data, :[]=, :set + + def initialize filename + @filename = filename + @data = Hash.new {|d, host| d[host] = [] } + load if File.exist? filename + end + + def fetch_user host + unless entry = get(host).first + user = yield + return nil if user.nil? or user.empty? + entry = entry_for_user(host, user) + end + entry['user'] + end + + def fetch_value host, user, key + entry = entry_for_user host, user + entry[key.to_s] || begin + value = yield + if value and !value.empty? + entry[key.to_s] = value + save + value + else + raise "no value" + end + end + end + + def entry_for_user host, username + entries = get(host) + entries.find {|e| e['user'] == username } or + (entries << {'user' => username}).last + end + + def load + existing_data = File.read(@filename) + @data.update YAML.load(existing_data) unless existing_data.strip.empty? + end + + def save + FileUtils.mkdir_p File.dirname(@filename) + File.open(@filename, 'w', 0600) {|f| f << YAML.dump(@data) } + end + end + + class Configuration + def initialize store + @data = store + @password_cache = {} + end + + def normalize_host host + host = host.downcase + 'api.github.com' == host ? 'github.com' : host + end + + def username host + return ENV['GITHUB_USER'] unless ENV['GITHUB_USER'].to_s.empty? + host = normalize_host host + @data.fetch_user host do + if block_given? then yield + else prompt "#{host} username" + end + end + end + + def api_token host, user + host = normalize_host host + @data.fetch_value host, user, :api_token do + if block_given? then yield + else prompt "#{host} API token for #{user}" + end + end + end + + def password host, user + return ENV['GITHUB_PASSWORD'] unless ENV['GITHUB_PASSWORD'].to_s.empty? + host = normalize_host host + @password_cache["#{user}@#{host}"] ||= prompt_password host, user + end + + def oauth_token host, user, &block + @data.fetch_value normalize_host(host), user, :oauth_token, &block + end + + def prompt what + print "#{what}: " + $stdin.gets.chomp + end + + def prompt_password host, user + print "#{host} password for #{user} (never stored): " + if $stdin.tty? + password = askpass + puts '' + password + else + $stdin.gets.chomp + end + end + + def askpass + tty_state = `stty -g` + system 'stty raw -echo -icanon isig' if $?.success? + pass = '' + while char = $stdin.getbyte and !(char == 13 or char == 10) + if char == 127 or char == 8 + pass[-1,1] = '' unless pass.empty? + else + pass << char.chr + end + end + pass + ensure + system "stty #{tty_state}" unless tty_state.empty? + end + + def proxy_uri(with_ssl) + env_name = "HTTP#{with_ssl ? 'S' : ''}_PROXY" + if proxy = ENV[env_name] || ENV[env_name.downcase] and !proxy.empty? + proxy = "http://#{proxy}" unless proxy.include? '://' + URI.parse proxy + end + end + end + end +end + +require 'shellwords' +require 'forwardable' +require 'uri' + +module Hub + module Context + extend Forwardable + + NULL = defined?(File::NULL) ? File::NULL : File.exist?('/dev/null') ? '/dev/null' : 'NUL' + + class GitReader + attr_reader :executable + + def initialize(executable = nil, &read_proc) + @executable = executable || 'git' + read_proc ||= lambda { |cache, cmd| + result = %x{#{command_to_string(cmd)} 2>#{NULL}}.chomp + cache[cmd] = $?.success? && !result.empty? ? result : nil + } + @cache = Hash.new(&read_proc) + end + + def add_exec_flags(flags) + @executable = Array(executable).concat(flags) + end + + def read_config(cmd, all = false) + config_cmd = ['config', (all ? '--get-all' : '--get'), *cmd] + config_cmd = config_cmd.join(' ') unless cmd.respond_to? :join + read config_cmd + end + + def read(cmd) + @cache[cmd] + end + + def stub_config_value(key, value, get = '--get') + stub_command_output "config #{get} #{key}", value + end + + def stub_command_output(cmd, value) + @cache[cmd] = value.nil? ? nil : value.to_s + end + + def stub!(values) + @cache.update values + end + + private + + def to_exec(args) + args = Shellwords.shellwords(args) if args.respond_to? :to_str + Array(executable) + Array(args) + end + + def command_to_string(cmd) + full_cmd = to_exec(cmd) + full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ') + end + end + + module GitReaderMethods + extend Forwardable + + def_delegator :git_reader, :read_config, :git_config + def_delegator :git_reader, :read, :git_command + + def self.extended(base) + base.extend Forwardable + base.def_delegators :'self.class', :git_config, :git_command + end + end + + class Error < RuntimeError; end + class FatalError < Error; end + + private + + def git_reader + @git_reader ||= GitReader.new ENV['GIT'] + end + + include GitReaderMethods + private :git_config, :git_command + + def local_repo(fatal = true) + @local_repo ||= begin + if is_repo? + LocalRepo.new git_reader, current_dir + elsif fatal + raise FatalError, "Not a git repository" + end + end + end + + repo_methods = [ + :current_branch, + :current_project, :upstream_project, + :repo_owner, :repo_host, + :remotes, :remotes_group, :origin_remote + ] + def_delegator :local_repo, :name, :repo_name + def_delegators :local_repo, *repo_methods + private :repo_name, *repo_methods + + def master_branch + if local_repo(false) + local_repo.master_branch + else + Branch.new nil, 'refs/heads/master' + end + end + + class LocalRepo < Struct.new(:git_reader, :dir) + include GitReaderMethods + + def name + if project = main_project + project.name + else + File.basename(dir) + end + end + + def repo_owner + if project = main_project + project.owner + end + end + + def repo_host + project = main_project and project.host + end + + def main_project + remote = origin_remote and remote.project + end + + def upstream_project + if branch = current_branch and upstream = branch.upstream and upstream.remote? + remote = remote_by_name upstream.remote_name + remote.project + end + end + + def current_project + upstream_project || main_project + end + + def current_branch + if branch = git_command('symbolic-ref -q HEAD') + Branch.new self, branch + end + end + def master_branch + Branch.new self, 'refs/heads/master' + end + + def remotes + @remotes ||= begin + list = git_command('remote').to_s.split("\n") + main = list.delete('origin') and list.unshift(main) + list.map { |name| Remote.new self, name } + end + end + + def remotes_group(name) + git_config "remotes.#{name}" + end + + def origin_remote + remotes.first + end + + def remote_by_name(remote_name) + remotes.find {|r| r.name == remote_name } + end + + def known_hosts + hosts = git_config('hub.host', :all).to_s.split("\n") + hosts << default_host + hosts << "ssh.#{default_host}" + end + + def self.default_host + ENV['GITHUB_HOST'] || main_host + end + + def self.main_host + 'github.com' + end + + extend Forwardable + def_delegators :'self.class', :default_host, :main_host + + def ssh_config + @ssh_config ||= SshConfig.new + end + end + + class GithubProject < Struct.new(:local_repo, :owner, :name, :host) + def self.from_url(url, local_repo) + if local_repo.known_hosts.include? url.host + _, owner, name = url.path.split('/', 4) + GithubProject.new(local_repo, owner, name.sub(/\.git$/, ''), url.host) + end + end + + attr_accessor :repo_data + + def initialize(*args) + super + self.name = self.name.tr(' ', '-') + self.host ||= (local_repo || LocalRepo).default_host + self.host = host.sub(/^ssh\./i, '') if 'ssh.github.com' == host.downcase + end + + def private? + repo_data ? repo_data.fetch('private') : + host != (local_repo || LocalRepo).main_host + end + + def owned_by(new_owner) + new_project = dup + new_project.owner = new_owner + new_project + end + + def name_with_owner + "#{owner}/#{name}" + end + + def ==(other) + name_with_owner == other.name_with_owner + end + + def remote + local_repo.remotes.find { |r| r.project == self } + end + + def web_url(path = nil) + project_name = name_with_owner + if project_name.sub!(/\.wiki$/, '') + unless '/wiki' == path + path = if path =~ %r{^/commits/} then '/_history' + else path.to_s.sub(/\w+/, '_\0') + end + path = '/wiki' + path + end + end + "https://#{host}/" + project_name + path.to_s + end + + def git_url(options = {}) + if options[:https] then "https://#{host}/" + elsif options[:private] or private? then "git@#{host}:" + else "git://#{host}/" + end + name_with_owner + '.git' + end + end + + class GithubURL < URI::HTTPS + extend Forwardable + + attr_reader :project + def_delegator :project, :name, :project_name + def_delegator :project, :owner, :project_owner + + def self.resolve(url, local_repo) + u = URI(url) + if %[http https].include? u.scheme and project = GithubProject.from_url(u, local_repo) + self.new(u.scheme, u.userinfo, u.host, u.port, u.registry, + u.path, u.opaque, u.query, u.fragment, project) + end + rescue URI::InvalidURIError + nil + end + + def initialize(*args) + @project = args.pop + super(*args) + end + + def project_path + path.split('/', 4)[3] + end + end + + class Branch < Struct.new(:local_repo, :name) + alias to_s name + + def short_name + name.sub(%r{^refs/(remotes/)?.+?/}, '') + end + + def master? + short_name == 'master' + end + + def upstream + if branch = local_repo.git_command("rev-parse --symbolic-full-name #{short_name}@{upstream}") + Branch.new local_repo, branch + end + end + + def remote? + name.index('refs/remotes/') == 0 + end + + def remote_name + name =~ %r{^refs/remotes/([^/]+)} and $1 or + raise Error, "can't get remote name from #{name.inspect}" + end + end + + class Remote < Struct.new(:local_repo, :name) + alias to_s name + + def ==(other) + other.respond_to?(:to_str) ? name == other.to_str : super + end + + def project + urls.each_value { |url| + if valid = GithubProject.from_url(url, local_repo) + return valid + end + } + nil + end + + def urls + return @urls if defined? @urls + @urls = {} + local_repo.git_command('remote -v').to_s.split("\n").map do |line| + next if line !~ /^(.+?)\t(.+) \((.+)\)$/ + remote, uri, type = $1, $2, $3 + next if remote != self.name + if uri =~ %r{^[\w-]+://} or uri =~ %r{^([^/]+?):} + uri = "ssh://#{$1}/#{$'}" if $1 + begin + @urls[type] = uri_parse(uri) + rescue URI::InvalidURIError + end + end + end + @urls + end + + def uri_parse uri + uri = URI.parse uri + uri.host = local_repo.ssh_config.get_value(uri.host, 'hostname') { uri.host } + uri.user = local_repo.ssh_config.get_value(uri.host, 'user') { uri.user } + uri + end + end + + + def github_project(name, owner = nil) + if owner and owner.index('/') + owner, name = owner.split('/', 2) + elsif name and name.index('/') + owner, name = name.split('/', 2) + else + name ||= repo_name + owner ||= github_user + end + + if local_repo(false) and main_project = local_repo.main_project + project = main_project.dup + project.owner = owner + project.name = name + project + else + GithubProject.new(local_repo(false), owner, name) + end + end + + def git_url(owner = nil, name = nil, options = {}) + project = github_project(name, owner) + project.git_url({:https => https_protocol?}.update(options)) + end + + def resolve_github_url(url) + GithubURL.resolve(url, local_repo) if url =~ /^https?:/ + end + + def http_clone? + git_config('--bool hub.http-clone') == 'true' + end + + def https_protocol? + git_config('hub.protocol') == 'https' or http_clone? + end + + def git_alias_for(name) + git_config "alias.#{name}" + end + + def rev_list(a, b) + git_command("rev-list --cherry-pick --right-only --no-merges #{a}...#{b}") + end + + PWD = Dir.pwd + + def current_dir + PWD + end + + def git_dir + git_command 'rev-parse -q --git-dir' + end + + def is_repo? + !!git_dir + end + + def git_editor + editor = git_command 'var GIT_EDITOR' + editor = ENV[$1] if editor =~ /^\$(\w+)$/ + editor = File.expand_path editor if (editor =~ /^[~.]/ or editor.index('/')) and editor !~ /["']/ + editor.shellsplit + end + + module System + def browser_launcher + browser = ENV['BROWSER'] || ( + osx? ? 'open' : windows? ? %w[cmd /c start] : + %w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm } + ) + + abort "Please set $BROWSER to a web launcher to use this command." unless browser + Array(browser) + end + + def osx? + require 'rbconfig' + RbConfig::CONFIG['host_os'].to_s.include?('darwin') + end + + def windows? + require 'rbconfig' + RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ + end + + def which(cmd) + exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each { |ext| + exe = "#{path}/#{cmd}#{ext}" + return exe if File.executable? exe + } + end + return nil + end + + def command?(name) + !which(name).nil? + end + end + + include System + extend System + end +end + +require 'strscan' +require 'forwardable' + +class Hub::JSON + def self.parse(data) new(data).parse end + + WSP = /\s+/ + OBJ = /[{\[]/; HEN = /\}/; AEN = /\]/ + COL = /\s*:\s*/; KEY = /\s*,\s*/ + NUM = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/ + BOL = /true|false/; NUL = /null/ + + extend Forwardable + + attr_reader :scanner + alias_method :s, :scanner + def_delegators :scanner, :scan, :matched + private :s, :scan, :matched + + def initialize data + @scanner = StringScanner.new data.to_s + end + + def parse + space + object + end + + private + + def space() scan WSP end + + def endkey() scan(KEY) or space end + + def object + matched == '{' ? hash : array if scan(OBJ) + end + + def value + object or string or + scan(NUL) ? nil : + scan(BOL) ? matched.size == 4: + scan(NUM) ? eval(matched) : + error + end + + def hash + obj = {} + space + repeat_until(HEN) { k = string; scan(COL); obj[k] = value; endkey } + obj + end + + def array + ary = [] + space + repeat_until(AEN) { ary << value; endkey } + ary + end + + SPEC = {'b' => "\b", 'f' => "\f", 'n' => "\n", 'r' => "\r", 't' => "\t"} + UNI = 'u'; CODE = /[a-fA-F0-9]{4}/ + STR = /"/; STE = '"' + ESC = '\\' + + def string + if scan(STR) + str, esc = '', false + while c = s.getch + if esc + str << (c == UNI ? (s.scan(CODE) || error).to_i(16).chr : SPEC[c] || c) + esc = false + else + case c + when ESC then esc = true + when STE then break + else str << c + end + end + end + str + end + end + + def error + raise "parse error at: #{scan(/.{1,10}/m).inspect}" + end + + def repeat_until reg + until scan(reg) + pos = s.pos + yield + error unless s.pos > pos + end + end + + module Generator + def generate(obj) + raise ArgumentError unless obj.is_a? Array or obj.is_a? Hash + generate_type(obj) + end + alias dump generate + + private + + def generate_type(obj) + type = obj.is_a?(Numeric) ? :Numeric : obj.class.name + begin send(:"generate_#{type}", obj) + rescue NoMethodError; raise ArgumentError, "can't serialize #{type}" + end + end + + ESC_MAP = Hash.new {|h,k| k }.update \ + "\r" => 'r', + "\n" => 'n', + "\f" => 'f', + "\t" => 't', + "\b" => 'b' + + def generate_String(str) + escaped = str.gsub(/[\r\n\f\t\b"\\]/) { "\\#{ESC_MAP[$&]}"} + %("#{escaped}") + end + + def generate_simple(obj) obj.inspect end + alias generate_Numeric generate_simple + alias generate_TrueClass generate_simple + alias generate_FalseClass generate_simple + + def generate_Symbol(sym) generate_String(sym.to_s) end + + def generate_NilClass(*) 'null' end + + def generate_Array(ary) '[%s]' % ary.map {|o| generate_type(o) }.join(', ') end + + def generate_Hash(hash) + '{%s}' % hash.map { |key, value| + "#{generate_String(key.to_s)}: #{generate_type(value)}" + }.join(', ') + end + end + + extend Generator +end + +module Hub module Commands instance_methods.each { |m| undef_method(m) unless m =~ /(^__|send|to\?$)/ } extend self extend Context - API_REPO = 'http://github.com/api/v2/yaml/repos/show/%s/%s' - API_FORK = 'http://github.com/api/v2/yaml/repos/fork/%s/%s' - API_CREATE = 'http://github.com/api/v2/yaml/repos/create' + NAME_RE = /[\w.][\w.-]*/ + OWNER_RE = /[a-zA-Z0-9-]+/ + NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/ + + CUSTOM_COMMANDS = %w[alias create browse compare fork pull-request] def run(args) slurp_global_flags(args) @@ -105,14 +1175,138 @@ module Hub args.unshift 'help' if args.empty? cmd = args[0] - expanded_args = expand_alias(cmd) - cmd = expanded_args[0] if expanded_args + if expanded_args = expand_alias(cmd) + cmd = expanded_args[0] + expanded_args.concat args[1..-1] + end - cmd = cmd.sub(/(\w)-/, '\1_') + respect_help_flags(expanded_args || args) if custom_command? cmd + + cmd = cmd.gsub(/(\w)-/, '\1_') if method_defined?(cmd) and cmd != 'run' - args[0, 1] = expanded_args if expanded_args + args.replace expanded_args if expanded_args send(cmd, args) end + rescue Errno::ENOENT + if $!.message.include? "No such file or directory - git" + abort "Error: `git` command not found" + else + raise + end + rescue Context::FatalError => err + abort "fatal: #{err.message}" + end + + def pull_request(args) + args.shift + options = { } + force = explicit_owner = false + base_project = local_repo.main_project + head_project = local_repo.current_project + + unless base_project + abort "Aborted: the origin remote doesn't point to a GitHub repository." + end + + from_github_ref = lambda do |ref, context_project| + if ref.index(':') + owner, ref = ref.split(':', 2) + project = github_project(context_project.name, owner) + end + [project || context_project, ref] + end + + while arg = args.shift + case arg + when '-f' + force = true + when '-b' + base_project, options[:base] = from_github_ref.call(args.shift, base_project) + when '-h' + head = args.shift + explicit_owner = !!head.index(':') + head_project, options[:head] = from_github_ref.call(head, head_project) + when '-i' + options[:issue] = args.shift + else + if url = resolve_github_url(arg) and url.project_path =~ /^issues\/(\d+)/ + options[:issue] = $1 + base_project = url.project + elsif !options[:title] then options[:title] = arg + else + abort "invalid argument: #{arg}" + end + end + end + + options[:project] = base_project + options[:base] ||= master_branch.short_name + + if tracked_branch = options[:head].nil? && current_branch.upstream + if !tracked_branch.remote? + tracked_branch = nil + elsif base_project == head_project and tracked_branch.short_name == options[:base] + $stderr.puts "Aborted: head branch is the same as base (#{options[:base].inspect})" + warn "(use `-h ` to specify an explicit pull request head)" + abort + end + end + options[:head] ||= (tracked_branch || current_branch).short_name + + user = github_user(head_project.host) + if head_project.owner != user and !tracked_branch and !explicit_owner + head_project = head_project.owned_by(user) + end + + remote_branch = "#{head_project.remote}/#{options[:head]}" + options[:head] = "#{head_project.owner}:#{options[:head]}" + + if !force and tracked_branch and local_commits = rev_list(remote_branch, nil) + $stderr.puts "Aborted: #{local_commits.split("\n").size} commits are not yet pushed to #{remote_branch}" + warn "(use `-f` to force submit a pull request anyway)" + abort + end + + if args.noop? + puts "Would request a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}" + exit + end + + unless options[:title] or options[:issue] + base_branch = "#{base_project.remote}/#{options[:base]}" + commits = rev_list(base_branch, remote_branch).to_s.split("\n") + + case commits.size + when 0 + default_message = commit_summary = nil + when 1 + format = '%w(78,0,0)%s%n%+b' + default_message = git_command "show -s --format='#{format}' #{commits.first}" + commit_summary = nil + else + format = '%h (%aN, %ar)%n%w(78,3,3)%s%n%+b' + default_message = nil + commit_summary = git_command "log --no-color --format='%s' --cherry %s...%s" % + [format, base_branch, remote_branch] + end + + options[:title], options[:body] = pullrequest_editmsg(commit_summary) { |msg| + msg.puts default_message if default_message + msg.puts "" + msg.puts "# Requesting a pull to #{base_project.owner}:#{options[:base]} from #{options[:head]}" + msg.puts "#" + msg.puts "# Write a message for this pull request. The first block" + msg.puts "# of text is the title and the rest is description." + } + end + + pull = api_client.create_pullrequest(options) + + args.executable = 'echo' + args.replace [pull['html_url']] + rescue GitHubAPI::Exceptions + display_api_exception("creating pull request", $!.response) + exit 1 end def clone(args) @@ -124,10 +1318,14 @@ module Hub arg = args[idx] if arg.index('-') == 0 idx += 1 if arg =~ has_values - elsif arg.index('://') or arg.index('@') or File.directory?(arg) - break - elsif arg.scan('/').size <= 1 && !arg.include?(':') - args[args.index(arg)] = github_url(:repo => arg, :private => ssh) + else + if arg =~ NAME_WITH_OWNER_RE and !File.directory?(arg) + name, owner = arg, nil + owner, name = name.split('/', 2) if name.index('/') + project = github_project(name, owner || github_user) + ssh ||= args[0] != 'submodule' && project.owner == github_user(project.host) { } + args[idx] = project.git_url(:private => ssh, :https => https_protocol?) + end break end idx += 1 @@ -153,23 +1351,26 @@ module Hub end def remote(args) - return unless ['add','set-url'].include?(args[1]) && args.last !~ %r{.+?://|.+?@|^[./]} + if %w[add set-url].include?(args[1]) + name = args.last + if name =~ /^(#{OWNER_RE})$/ || name =~ /^(#{OWNER_RE})\/(#{NAME_RE})$/ + user, repo = $1, $2 || repo_name + end + end + return unless user # do not touch arguments ssh = args.delete('-p') - args.last =~ /\b(.+?)(?:\/(.+))?$/ - user, repo = $1, $2 - if args.words[2] == 'origin' && args.words[3].nil? - user = repo = nil + user, repo = github_user, repo_name elsif args.words[-2] == args.words[1] idx = args.index( args.words[-1] ) args[idx] = user else - args.replace args[0...-1] + args.pop end - args << github_url(:user => user, :repo => repo, :private => ssh) + args << git_url(user, repo, :private => ssh) end def fetch(args) @@ -183,45 +1384,95 @@ module Hub args.insert(index, *names) args.insert(index, '--multiple') else - names = [remote_name] + names = [remote_name] + end + else + names = [] + end + + projects = names.map { |name| + unless name =~ /\W/ or remotes.include?(name) or remotes_group(name) + project = github_project(nil, name) + repo_info = api_client.repo_info(project) + if repo_info.success? + project.repo_data = repo_info.data + project + end + end + }.compact + + if projects.any? + projects.each do |project| + args.before ['remote', 'add', project.owner, project.git_url(:https => https_protocol?)] + end + end + end + + def checkout(args) + _, url_arg, new_branch_name = args.words + if url = resolve_github_url(url_arg) and url.project_path =~ /^pull\/(\d+)/ + pull_id = $1 + pull_data = api_client.pullrequest_info(url.project, pull_id) + + args.delete new_branch_name + user, branch = pull_data['head']['label'].split(':', 2) + abort "Error: #{user}'s fork is not available anymore" unless pull_data['head']['repo'] + new_branch_name ||= "#{user}-#{branch}" + + if remotes.include? user + args.before ['remote', 'set-branches', '--add', user, branch] + args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"] + else + url = github_project(url.project_name, user).git_url(:private => pull_data['head']['repo']['private'], + :https => https_protocol?) + args.before ['remote', 'add', '-f', '-t', branch, user, url] end - else - names = [] + idx = args.index url_arg + args.delete_at idx + args.insert idx, '--track', '-B', new_branch_name, "#{user}/#{branch}" end + end - names.reject! { |name| - name =~ /\W/ or remotes.include?(name) or - remotes_group(name) or not repo_exists?(name) - } + def merge(args) + _, url_arg = args.words + if url = resolve_github_url(url_arg) and url.project_path =~ /^pull\/(\d+)/ + pull_id = $1 + pull_data = api_client.pullrequest_info(url.project, pull_id) - if names.any? - names.each do |name| - args.before ['remote', 'add', name, github_url(:user => name)] - end + user, branch = pull_data['head']['label'].split(':', 2) + abort "Error: #{user}'s fork is not available anymore" unless pull_data['head']['repo'] + + url = github_project(url.project_name, user).git_url(:private => pull_data['head']['repo']['private'], + :https => https_protocol?) + + merge_head = "#{user}/#{branch}" + args.before ['fetch', url, "+refs/heads/#{branch}:refs/remotes/#{merge_head}"] + + idx = args.index url_arg + args.delete_at idx + args.insert idx, merge_head, '--no-ff', '-m', + "Merge pull request ##{pull_id} from #{merge_head}\n\n#{pull_data['title']}" end end def cherry_pick(args) unless args.include?('-m') or args.include?('--mainline') - case ref = args.words.last - when %r{^(?:https?:)//github.com/(.+?)/(.+?)/commit/([a-f0-9]{7,40})} - user, repo, sha = $1, $2, $3 - args[args.index(ref)] = sha - when /^(\w+)@([a-f1-9]{7,40})$/ - user, repo, sha = $1, nil, $2 - args[args.index(ref)] = sha - else - user = nil + ref = args.words.last + if url = resolve_github_url(ref) and url.project_path =~ /^commit\/([a-f0-9]{7,40})/ + sha = $1 + project = url.project + elsif ref =~ /^(#{OWNER_RE})@([a-f0-9]{7,40})$/ + owner, sha = $1, $2 + project = local_repo.main_project.owned_by(owner) end - if user - if user == repo_owner - args.before ['fetch', default_remote] - elsif remotes.include?(user) - args.before ['fetch', user] + if project + args[args.index(ref)] = sha + + if remote = project.remote and remotes.include? remote + args.before ['fetch', remote.to_s] else - remote_url = github_url(:user => user, :repo => repo, :private => false) - args.before ['remote', 'add', '-f', user, remote_url] + args.before ['remote', 'add', '-f', project.owner, project.git_url(:https => https_protocol?)] end end end @@ -231,52 +1482,65 @@ module Hub if url = args.find { |a| a =~ %r{^https?://(gist\.)?github\.com/} } idx = args.index(url) gist = $1 == 'gist.' + url = url.sub(/#.+/, '') url = url.sub(%r{(/pull/\d+)/\w*$}, '\1') unless gist ext = gist ? '.txt' : '.patch' url += ext unless File.extname(url) == ext - patch_file = File.join(ENV['TMPDIR'], "#{gist ? 'gist-' : ''}#{File.basename(url)}") + patch_file = File.join(ENV['TMPDIR'] || '/tmp', "#{gist ? 'gist-' : ''}#{File.basename(url)}") args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file] args[idx] = patch_file end end + alias_method :apply, :am + def init(args) if args.delete('-g') - url = github_url(:private => true, :repo => current_dirname) - args.after "git remote add origin #{url}" + project = github_project(File.basename(current_dir)) + url = project.git_url(:private => true, :https => https_protocol?) + args.after ['remote', 'add', 'origin', url] end end def fork(args) - if github_user && github_token && repo_owner - if repo_exists?(github_user) - puts "#{github_user}/#{repo_name} already exists on GitHub" - else - fork_repo + unless project = local_repo.main_project + abort "Error: repository under 'origin' remote is not a GitHub project" + end + forked_project = project.owned_by(github_user(project.host)) + + existing_repo = api_client.repo_info(forked_project) + if existing_repo.success? + parent_data = existing_repo.data['parent'] + parent_url = parent_data && resolve_github_url(parent_data['html_url']) + if !parent_url or parent_url.project != project + abort "Error creating fork: %s already exists on %s" % + [ forked_project.name_with_owner, forked_project.host ] end + else + api_client.fork_repo(project) unless args.noop? + end - if args.include?('--no-remote') - exit - else - url = github_url(:private => true) - args.replace %W"remote add -f #{github_user} #{url}" - args.after { puts "new remote: #{github_user}" } - end + if args.include?('--no-remote') + exit + else + url = forked_project.git_url(:private => true, :https => https_protocol?) + args.replace %W"remote add -f #{forked_project.owner} #{url}" + args.after 'echo', ['new remote:', forked_project.owner] end - rescue Net::HTTPExceptions - response = $!.response - warn "error creating fork: #{response.message} (HTTP #{response.code})" + rescue GitHubAPI::Exceptions + display_api_exception("creating fork", $!.response) exit 1 end def create(args) if !is_repo? - puts "'create' must be run from inside a git repository" - args.skip! - elsif github_user && github_token + abort "'create' must be run from inside a git repository" + else + owner = github_user args.shift options = {} options[:private] = true if args.delete('-p') + new_repo_name = nil until args.empty? case arg = args.shift @@ -285,20 +1549,29 @@ module Hub when '-h' options[:homepage] = args.shift else - puts "unexpected argument: #{arg}" - return + if arg =~ /^[^-]/ and new_repo_name.nil? + new_repo_name = arg + owner, new_repo_name = new_repo_name.split('/', 2) if new_repo_name.index('/') + else + abort "invalid argument: #{arg}" + end end end + new_repo_name ||= repo_name + new_project = github_project(new_repo_name, owner) - if repo_exists?(github_user) - puts "#{github_user}/#{repo_name} already exists on GitHub" + if api_client.repo_exists?(new_project) + warn "#{new_project.name_with_owner} already exists on #{new_project.host}" action = "set remote origin" else action = "created repository" - create_repo(options) + unless args.noop? + repo_data = api_client.create_repo(new_project, options) + new_project = github_project(repo_data['full_name']) + end end - url = github_url(:private => true) + url = new_project.git_url(:private => true, :https => https_protocol?) if remotes.first != 'origin' args.replace %W"remote add -f origin #{url}" @@ -306,55 +1579,56 @@ module Hub args.replace %W"remote -v" end - args.after { puts "#{action}: #{github_user}/#{repo_name}" } + args.after 'echo', ["#{action}:", new_project.name_with_owner] end - rescue Net::HTTPExceptions - response = $!.response - warn "error creating repository: #{response.message} (HTTP #{response.code})" + rescue GitHubAPI::Exceptions + display_api_exception("creating repository", $!.response) exit 1 end def push(args) return if args[1].nil? || !args[1].index(',') - branch = (args[2] ||= normalize_branch(current_branch)) + refs = args.words[2..-1] remotes = args[1].split(',') args[1] = remotes.shift + if refs.empty? + refs = [current_branch.short_name] + args.concat refs + end + remotes.each do |name| - args.after ['push', name, branch] + args.after ['push', name, *refs] end end def browse(args) args.shift browse_command(args) do - user = repo = nil dest = args.shift dest = nil if dest == '--' if dest - repo = dest - elsif repo_user - user = repo_user + project = github_project dest + branch = master_branch else - abort "Usage: hub browse [/]" + project = current_project + branch = current_branch && current_branch.upstream || master_branch end - params = { :user => user, :repo => repo } + abort "Usage: hub browse [/]" unless project - case subpage = args.shift + path = case subpage = args.shift when 'commits' - branch = (!dest && tracked_branch) || 'master' - params[:web] = "/commits/#{branch}" + "/commits/#{branch.short_name}" when 'tree', NilClass - branch = !dest && tracked_branch - params[:web] = "/tree/#{branch}" if branch && branch != 'master' + "/tree/#{branch.short_name}" if branch and !branch.master? else - params[:web] = "/#{subpage}" + "/#{subpage}" end - params + project.web_url(path) end end @@ -362,76 +1636,76 @@ module Hub args.shift browse_command(args) do if args.empty? - branch = tracked_branch - if branch && branch != 'master' - range, user = branch, repo_user + branch = current_branch.upstream + if branch and not branch.master? + range = branch.short_name + project = current_project else abort "Usage: hub compare [USER] [...]" end else - range = args.pop - user = args.pop || repo_user + sha_or_tag = /(\w{1,2}|\w[\w.-]+\w)/ + range = args.pop.sub(/^#{sha_or_tag}\.\.#{sha_or_tag}$/, '\1...\2') + project = if owner = args.pop then github_project(nil, owner) + else current_project + end end - { :user => user, :web => "/compare/#{range}" } + + project.web_url "/compare/#{range}" end end def hub(args) return help(args) unless args[1] == 'standalone' require 'hub/standalone' - puts Hub::Standalone.build + Hub::Standalone.build $stdout exit rescue LoadError - abort "hub is running in standalone mode." + abort "hub is already running in standalone mode." + rescue Errno::EPIPE + exit # ignore broken pipe end def alias(args) - shells = { - 'sh' => 'alias git=hub', - 'bash' => 'alias git=hub', - 'zsh' => 'function git(){hub "$@"}', - 'csh' => 'alias git hub', - 'fish' => 'alias git hub' - } - - silent = args.delete('-s') + shells = %w[bash zsh sh ksh csh fish] - if shell = args[1] - if silent.nil? - puts "Run this in your shell to start using `hub` as `git`:" - print " " - end - else - puts "usage: hub alias [-s] SHELL", "" - puts "You already have hub installed and available in your PATH," - puts "but to get the full experience you'll want to alias it to" - puts "`git`.", "" - puts "To see how to accomplish this for your shell, run the alias" - puts "command again with the name of your shell.", "" - puts "Known shells:" - shells.map { |key, _| key }.sort.each do |key| - puts " " + key - end - puts "", "Options:" - puts " -s Silent. Useful when using the output with eval, e.g." - puts " $ eval `hub alias -s bash`" + script = !!args.delete('-s') + shell = args[1] || ENV['SHELL'] + abort "hub alias: unknown shell" if shell.nil? or shell.empty? + shell = File.basename shell - exit + unless shells.include? shell + $stderr.puts "hub alias: unsupported shell" + warn "supported shells: #{shells.join(' ')}" + abort end - if shells[shell] - puts shells[shell] + if script + puts "alias git=hub" + if 'zsh' == shell + puts "if type compdef >/dev/null; then" + puts " compdef hub=git" + puts "fi" + end else - abort "fatal: never heard of `#{shell}'" + profile = case shell + when 'bash' then '~/.bash_profile' + when 'zsh' then '~/.zshrc' + when 'ksh' then '~/.profile' + else + 'your profile' + end + + puts "# Wrap git automatically by adding the following to #{profile}:" + puts + puts 'eval "$(hub alias -s)"' end exit end def version(args) - args.after do - puts "hub version %s" % Version - end + args.after 'echo', ['hub version', Version] end alias_method "--version", :version @@ -449,11 +1723,52 @@ module Hub end alias_method "--help", :help + private + + def api_client + @api_client ||= begin + config_file = ENV['HUB_CONFIG'] || '~/.config/hub' + file_store = GitHubAPI::FileStore.new File.expand_path(config_file) + file_config = GitHubAPI::Configuration.new file_store + GitHubAPI.new file_config, :app_url => 'http://defunkt.io/hub/' + end + end + + def github_user host = nil, &block + host ||= (local_repo(false) || Context::LocalRepo).default_host + api_client.config.username(host, &block) + end + + def custom_command? cmd + CUSTOM_COMMANDS.include? cmd + end + + def respect_help_flags args + return if args.size > 2 + case args[1] + when '-h' + pattern = /(git|hub) #{Regexp.escape args[0].gsub('-', '\-')}/ + hub_raw_manpage.each_line { |line| + if line =~ pattern + $stderr.print "Usage: " + $stderr.puts line.gsub(/\\f./, '').gsub('\-', '-') + abort + end + } + abort "Error: couldn't find usage help for #{args[0]}" + when '--help' + puts hub_manpage + exit + end + end + def improved_help_text <<-help -usage: git [--version] [--exec-path[=GIT_EXEC_PATH]] [--html-path] - [-p|--paginate|--no-pager] [--bare] [--git-dir=GIT_DIR] - [--work-tree=GIT_WORK_TREE] [--help] COMMAND [ARGS] +usage: git [--version] [--exec-path[=]] [--html-path] [--man-path] [--info-path] + [-p|--paginate|--no-pager] [--no-replace-objects] [--bare] + [--git-dir=] [--work-tree=] [--namespace=] + [-c name=value] [--help] + [] Basic Commands: init Create an empty git repository or reinitialize an existing one @@ -481,20 +1796,25 @@ Remote Commands: push Upload data, tags and branches to a remote repository remote View and manage a set of remote repositories -Advanced commands: +Advanced Commands: reset Reset your staging area or working directory to another point rebase Re-apply a series of patches in one branch onto another bisect Find by binary search the change that introduced a bug grep Print files with lines matching a pattern in your codebase -See 'git help COMMAND' for more information on a specific command. +GitHub Commands: + pull-request Open a pull request on GitHub + fork Make a fork of a remote repository on GitHub and add as remote + create Create this repository on GitHub and add GitHub as origin + browse Open a GitHub page in the default browser + compare Open a compare page on GitHub + +See 'git help ' for more information on a specific command. help end - private - def slurp_global_flags(args) - flags = %w[ -c -p --paginate --no-pager --no-replace-objects --bare --version --help ] + flags = %w[ --noop -c -p --paginate --no-pager --no-replace-objects --bare --version --help ] flags2 = %w[ --exec-path= --git-dir= --work-tree= ] globals = [] @@ -503,12 +1823,14 @@ help while args[0] && (flags.include?(args[0]) || flags2.any? {|f| args[0].index(f) == 0 }) flag = args.shift case flag + when '--noop' + args.noop! when '--version', '--help' args.unshift flag.sub('--', '') when '-c' config_pair = args.shift key, value = config_pair.split('=', 2) - Context::GIT_CONFIG["config #{key}"] = value.to_s + git_reader.stub_config_value(key, value) globals << flag << config_pair when '-p', '--paginate', '--no-pager' @@ -518,21 +1840,22 @@ help end end - Context::GIT_CONFIG.executable = Array(Context::GIT_CONFIG.executable).concat(globals) - args.executable = Array(args.executable).concat(globals).concat(locals) + git_reader.add_exec_flags(globals) + args.add_exec_flags(globals) + args.add_exec_flags(locals) end def browse_command(args) url_only = args.delete('-u') - $stderr.puts "Warning: the `-p` flag has no effect anymore" if args.delete('-p') - params = yield + warn "Warning: the `-p` flag has no effect anymore" if args.delete('-p') + url = yield args.executable = url_only ? 'echo' : browser_launcher - args.push github_url({:web => true, :private => true}.update(params)) + args.push url end def hub_manpage - return "** Can't find groff(1)" unless command?('groff') + abort "** Can't find groff(1)" unless command?('groff') require 'open3' out = nil @@ -562,7 +1885,7 @@ help end def page_stdout - return unless $stdout.tty? + return if not $stdout.tty? or windows? read, write = IO.pipe @@ -588,29 +1911,41 @@ help read.close write.close end + rescue NotImplementedError end - def repo_exists?(user) - require 'net/http' - url = API_REPO % [user, repo_name] - Net::HTTPSuccess === Net::HTTP.get_response(URI(url)) - end - - def fork_repo - url = API_FORK % [repo_owner, repo_name] - response = Net::HTTP.post_form(URI(url), 'login' => github_user, 'token' => github_token) - response.error! unless Net::HTTPSuccess === response - end - - def create_repo(options = {}) - url = API_CREATE - params = {'login' => github_user, 'token' => github_token, 'name' => repo_name} - params['public'] = '0' if options[:private] - params['description'] = options[:description] if options[:description] - params['homepage'] = options[:homepage] if options[:homepage] + def pullrequest_editmsg(changes) + message_file = File.join(git_dir, 'PULLREQ_EDITMSG') + File.open(message_file, 'w') { |msg| + yield msg + if changes + msg.puts "#\n# Changes:\n#" + msg.puts changes.gsub(/^/, '# ').gsub(/ +$/, '') + end + } + edit_cmd = Array(git_editor).dup + edit_cmd << '-c' << 'set ft=gitcommit' if edit_cmd[0] =~ /^[mg]?vim$/ + edit_cmd << message_file + system(*edit_cmd) + abort "can't open text editor for pull request message" unless $?.success? + title, body = read_editmsg(message_file) + abort "Aborting due to empty pull request title" unless title + [title, body] + end + + def read_editmsg(file) + title, body = '', '' + File.open(file, 'r') { |msg| + msg.each_line do |line| + next if line.index('#') == 0 + ((body.empty? and line =~ /\S/) ? title : body) << line + end + } + title.tr!("\n", ' ') + title.strip! + body.strip! - response = Net::HTTP.post_form(URI(url), params) - response.error! unless Net::HTTPSuccess === response + [title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil] end def expand_alias(cmd) @@ -621,197 +1956,19 @@ help end end end - - end -end -require 'shellwords' - -module Hub - module Context - private - - class ShellOutCache < Hash - attr_accessor :executable - - def initialize(executable = nil, &block) - super(&block) - @executable = executable - end - - def to_exec(args) - args = Shellwords.shellwords(args) if args.respond_to? :to_str - Array(executable) + Array(args) - end - end - - GIT_CONFIG = ShellOutCache.new(ENV['GIT'] || 'git') do |cache, cmd| - full_cmd = cache.to_exec(cmd) - cmd_string = full_cmd.respond_to?(:shelljoin) ? full_cmd.shelljoin : full_cmd.join(' ') - result = %x{#{cmd_string}}.chomp - cache[cmd] = $?.success? && !result.empty? ? result : nil - end - - REMOTES = Hash.new do |cache, remote| - if remote - urls = GIT_CONFIG["config --get-all remote.#{remote}.url"].to_s.split("\n") - - if urls.find { |u| u =~ %r{\bgithub\.com[:/](.+)/(.+).git$} } - cache[remote] = { :user => $1, :repo => $2 } - else - cache[remote] = { } - end - else - cache[remote] = { } - end - end - - LGHCONF = "http://help.github.com/git-email-settings/" - - def repo_owner - REMOTES[default_remote][:user] - end - - def repo_user - REMOTES[current_remote][:user] - end - - def repo_name - REMOTES[default_remote][:repo] || current_dirname - end - - def github_user(fatal = true) - if user = ENV['GITHUB_USER'] || GIT_CONFIG['config github.user'] - user - elsif fatal - abort("** No GitHub user set. See #{LGHCONF}") - end - end - - def github_token(fatal = true) - if token = ENV['GITHUB_TOKEN'] || GIT_CONFIG['config github.token'] - token - elsif fatal - abort("** No GitHub token set. See #{LGHCONF}") - end - end - - def current_branch - GIT_CONFIG['symbolic-ref -q HEAD'] - end - - def tracked_branch - branch = current_branch && tracked_for(current_branch) - normalize_branch(branch) if branch - end - - def remotes - list = GIT_CONFIG['remote'].to_s.split("\n") - main = list.delete('origin') and list.unshift(main) - list - end - - def remotes_group(name) - GIT_CONFIG["config remotes.#{name}"] - end - - def current_remote - return if remotes.empty? - (current_branch && remote_for(current_branch)) || default_remote - end - - def default_remote - remotes.first - end - - def normalize_branch(branch) - branch.sub('refs/heads/', '') - end - - def remote_for(branch) - GIT_CONFIG['config branch.%s.remote' % normalize_branch(branch)] - end - - def tracked_for(branch) - GIT_CONFIG['config branch.%s.merge' % normalize_branch(branch)] - end - - def http_clone? - GIT_CONFIG['config --bool hub.http-clone'] == 'true' - end - - def git_alias_for(name) - GIT_CONFIG["config alias.#{name}"] - end - - def is_repo? - GIT_CONFIG['config core.repositoryformatversion'] - end - - def github_url(options = {}) - repo = options[:repo] - user, repo = repo.split('/') if repo && repo.index('/') - user ||= options[:user] || github_user - repo ||= repo_name - secure = options[:private] - - if options[:web] - scheme = secure ? 'https:' : 'http:' - path = options[:web] == true ? '' : options[:web].to_s - if repo =~ /\.wiki$/ - repo = repo.sub(/\.wiki$/, '') - unless '/wiki' == path - path = '/wiki%s' % if path =~ %r{^/commits/} then '/_history' - else path.sub(/\w+/, '_\0') - end - end - end - '%s//github.com/%s/%s%s' % [scheme, user, repo, path] - else - if secure - url = 'git@github.com:%s/%s.git' - elsif http_clone? - url = 'http://github.com/%s/%s.git' - else - url = 'git://github.com/%s/%s.git' - end - - url % [user, repo] - end - end - - DIRNAME = File.basename(Dir.pwd) - - def current_dirname - DIRNAME - end - - def browser_launcher - require 'rbconfig' - browser = ENV['BROWSER'] || - (RbConfig::CONFIG['host_os'].include?('darwin') && 'open') || - (RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ && 'start') || - %w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm } - - abort "Please set $BROWSER to a web launcher to use this command." unless browser - Array(browser) - end - - def which(cmd) - exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] - ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| - exts.each { |ext| - exe = "#{path}/#{cmd}#{ext}" - return exe if File.executable? exe - } + + def display_api_exception(action, response) + $stderr.puts "Error #{action}: #{response.message.strip} (HTTP #{response.status})" + if 422 == response.status and response.error_message? + msg = response.error_message + msg = msg.join("\n") if msg.respond_to? :join + warn msg end - return nil end - def command?(name) - !which(name).nil? - end end end + module Hub class Runner attr_reader :args @@ -836,7 +1993,7 @@ module Hub def commands args.commands.map do |cmd| if cmd.respond_to?(:join) - cmd.map { |c| (c.index(' ') || c.empty?) ? "'#{c}'" : c }.join(' ') + cmd.map { |arg| arg = arg.to_s; (arg.index(' ') || arg.empty?) ? "'#{arg}'" : arg }.join(' ') else cmd.to_s end @@ -844,7 +2001,9 @@ module Hub end def execute - unless args.skip? + if args.noop? + puts commands + elsif not args.skip? if args.chained? execute_command_chain else @@ -866,32 +2025,28 @@ module Hub end end end -module Hub - Version = VERSION = '1.6.1' -end + Hub::Runner.execute(*ARGV) + __END__ .\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . -.TH "HUB" "1" "May 2011" "DEFUNKT" "Git Manual" +.TH "HUB" "1" "November 2012" "DEFUNKT" "Git Manual" . .SH "NAME" \fBhub\fR \- git + hub = github . .SH "SYNOPSIS" -\fBhub\fR \fICOMMAND\fR \fIOPTIONS\fR +\fBhub\fR [\fB\-\-noop\fR] \fICOMMAND\fR \fIOPTIONS\fR . .br -\fBhub alias\fR [\fB\-s\fR] \fISHELL\fR +\fBhub alias\fR [\fB\-s\fR] [\fISHELL\fR] . -.P +.SS "Expanded git commands:" \fBgit init \-g\fR \fIOPTIONS\fR . .br -\fBgit create\fR [\fB\-p\fR] [\fB\-d \fR] [\fB\-h \fR] -. -.br \fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR . .br @@ -904,148 +2059,174 @@ __END__ \fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.] . .br +\fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR] +. +.br +\fBgit merge\fR \fIPULLREQ\-URL\fR +. +.br \fBgit cherry\-pick\fR \fIGITHUB\-REF\fR . .br \fBgit am\fR \fIGITHUB\-URL\fR . .br +\fBgit apply\fR \fIGITHUB\-URL\fR +. +.br \fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR] . .br +\fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR +. +.SS "Custom git commands:" +\fBgit create\fR [\fINAME\fR] [\fB\-p\fR] [\fB\-d\fR \fIDESCRIPTION\fR] [\fB\-h\fR \fIHOMEPAGE\fR] +. +.br \fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE] . .br \fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR . .br -\fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR +\fBgit fork\fR [\fB\-\-no\-remote\fR] . .br -\fBgit fork\fR [\fB\-\-no\-remote\fR] +\fBgit pull\-request\fR [\fB\-f\fR] [\fITITLE\fR|\fB\-i\fR \fIISSUE\fR] [\fB\-b\fR \fIBASE\fR] [\fB\-h\fR \fIHEAD\fR] . .SH "DESCRIPTION" -\fBhub\fR enhances various \fBgit\fR commands with GitHub remote expansion\. The alias command displays information on configuring your environment: +hub enhances various git commands to ease most common workflows with GitHub\. . -.IP "\(bu" 4 -\fBhub alias\fR [\fB\-s\fR] \fISHELL\fR: Writes shell aliasing code for \fISHELL\fR (\fBbash\fR, \fBsh\fR, \fBzsh\fR, \fBcsh\fR) to standard output\. With the \fB\-s\fR option, the output of this command can be evaluated directly within the shell: +.TP +\fBhub \-\-noop\fR \fICOMMAND\fR +Shows which command(s) would be run as a result of the current command\. Doesn\'t perform anything\. . -.br -\fBeval $(hub alias \-s bash)\fR +.TP +\fBhub alias\fR [\fB\-s\fR] [\fISHELL\fR] +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\. . -.IP "\(bu" 4 -\fBgit init\fR \fB\-g\fR \fIOPTIONS\fR: 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\. +.TP +\fBgit init\fR \fB\-g\fR \fIOPTIONS\fR +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\. . -.IP "\(bu" 4 -\fBgit create\fR [\fB\-p\fR] [\fB\-d \fR] [\fB\-h \fR]: +.TP +\fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR \fIDIRECTORY\fR +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\. . -.br -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\'s basename\. With \fB\-p\fR, create a private repository\. \fB\-d\fR and \fB\-h\fR set the repository\'s description and homepage, respectively\. +.TP +\fBgit remote add\fR [\fB\-p\fR] \fIOPTIONS\fR \fIUSER\fR[\fB/\fR\fIREPOSITORY\fR] +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\. . -.IP "\(bu" 4 -\fBgit clone\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR \fIDIRECTORY\fR: +.TP +\fBgit remote set\-url\fR [\fB\-p\fR] \fIOPTIONS\fR \fIREMOTE\-NAME\fR \fIUSER\fR[/\fIREPOSITORY\fR] +Sets the url of remote \fIREMOTE\-NAME\fR using the same rules as \fBgit remote add\fR\. . -.br -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, use private remote "git@github\.com:\fIUSER\fR/\fIREPOSITORY\fR\.git"\. +.TP +\fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.] +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\. . -.IP "\(bu" 4 -\fBgit remote add\fR [\fB\-p\fR] \fIOPTIONS\fR \fIUSER\fR[\fB/\fR\fIREPOSITORY\fR]: +.TP +\fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR] +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\. . -.br -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\. +.TP +\fBgit merge\fR \fIPULLREQ\-URL\fR +Merge the pull request with a commit message that includes the pull request ID and title, similar to the GitHub Merge Button\. . -.IP "\(bu" 4 -\fBgit remote set\-url\fR [\fB\-p\fR] \fIOPTIONS\fR \fIREMOTE\-NAME\fR \fIUSER\fR[/\fIREPOSITORY\fR] +.TP +\fBgit cherry\-pick\fR \fIGITHUB\-REF\fR +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 \fR is issued prior to the cherry\-pick attempt\. . -.br -Sets the url of remote \fIREMOTE\-NAME\fR using the same rules as \fBgit remote add\fR\. +.TP +\fBgit [am|apply]\fR \fIGITHUB\-URL\fR +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\. . -.IP "\(bu" 4 -\fBgit fetch\fR \fIUSER\-1\fR,[\fIUSER\-2\fR,\.\.\.]: 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\. +.TP +\fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR] +Push \fIREF\fR to each of \fIREMOTE\-1\fR through \fIREMOTE\-N\fR by executing multiple \fBgit push\fR commands\. . -.IP "\(bu" 4 -\fBgit cherry\-pick\fR \fIGITHUB\-REF\fR: 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 \fR is issued prior to the cherry\-pick attempt\. +.TP +\fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR +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"\. . -.IP "\(bu" 4 -\fBgit am\fR \fIGITHUB\-URL\fR: Downloads the patch file for the pull request or commit at the URL and applies that patch from disk with \fBgit am\fR\. Similar to \fBcherry\-pick\fR, but doesn\'t add new remotes\. +.TP +\fBgit help\fR +Display enhanced git\-help(1)\. . -.IP "\(bu" 4 -\fBgit push\fR \fIREMOTE\-1\fR,\fIREMOTE\-2\fR,\.\.\.,\fIREMOTE\-N\fR [\fIREF\fR]: Push \fIREF\fR to each of \fIREMOTE\-1\fR through \fIREMOTE\-N\fR by executing multiple \fBgit push\fR commands\. +.P +hub also adds some custom commands that are otherwise not present in git: . -.IP "\(bu" 4 -\fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE]: 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")\. +.TP +\fBgit create\fR [\fINAME\fR] [\fB\-p\fR] [\fB\-d\fR \fIDESCRIPTION\fR] [\fB\-h\fR \fIHOMEPAGE\fR] +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\. . -.IP "\(bu" 4 -\fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR: 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 \fISTART\fR is omitted, GitHub will compare against the base branch (the default is "master")\. +.TP +\fBgit browse\fR [\fB\-u\fR] [[\fIUSER\fR\fB/\fR]\fIREPOSITORY\fR] [SUBPAGE] +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")\. . -.IP "\(bu" 4 -\fBgit submodule add\fR [\fB\-p\fR] \fIOPTIONS\fR [\fIUSER\fR/]\fIREPOSITORY\fR \fIDIRECTORY\fR: +.TP +\fBgit compare\fR [\fB\-u\fR] [\fIUSER\fR] [\fISTART\fR\.\.\.]\fIEND\fR +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")\. . -.br -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"\. +.TP +\fBgit fork\fR [\fB\-\-no\-remote\fR] +Forks the original project (referenced by "origin" remote) on GitHub and adds a new remote for it under your username\. . -.IP "\(bu" 4 -\fBgit fork\fR [\fB\-\-no\-remote\fR]: Forks the original project (referenced by "origin" remote) on GitHub and adds a new remote for it under your username\. Requires \fBgithub\.token\fR to be set (see CONFIGURATION)\. +.TP +\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] +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\. . -.IP "\(bu" 4 -\fBgit help\fR: Display enhanced git\-help(1)\. +.IP +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\. . -.IP "" 0 +.IP +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\. . .SH "CONFIGURATION" -Use git\-config(1) to display the currently configured GitHub username: +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"\. +. +.P +To avoid being prompted, use \fIGITHUB_USER\fR and \fIGITHUB_PASSWORD\fR environment variables\. +. +.P +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\. . .IP "" 4 . .nf -$ git config \-\-global github\.user +$ git config \-\-global hub\.protocol https . .fi . .IP "" 0 . -.P -Or, set the GitHub username and token with: +.SS "GitHub Enterprise" +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: . .IP "" 4 . .nf -$ git config \-\-global github\.user -$ git config \-\-global github\.token +$ git config \-\-global \-\-add hub\.host my\.git\.org . .fi . .IP "" 0 . .P -See \fIhttp://github\.com/guides/local\-github\-config\fR for more information\. -. -.P -You can also tell \fBhub\fR to use \fBhttp://\fR rather than \fBgit://\fR when cloning: +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: . .IP "" 4 . .nf -$ git config \-\-global \-\-bool hub\.http\-clone true +$ GITHUB_HOST=my\.git\.org git clone myproject . .fi . .IP "" 0 . -.P -Want to use environment variables instead of a local gitconfig for authentication? -. -.IP "\(bu" 4 -\fBGITHUB_USER\fR \- If set, this will be used instead of the \fBgithub\.user\fR config -. -.IP "\(bu" 4 -\fBGITHUB_TOKEN\fR \- If set, this will be used instead of the \fBgithub\.token\fR -. -.IP "" 0 -. .SH "EXAMPLES" . .SS "git clone" @@ -1059,10 +2240,7 @@ $ git clone \-p schacon/ticgit > git clone git@github\.com:schacon/ticgit\.git $ git clone resque -> git clone git://github\.com/YOUR_USER/resque\.git - -$ git clone \-p resque -> git clone git@github\.com:YOUR_USER/resque\.git +> git clone git@github\.com/YOUR_USER/resque\.git . .fi . @@ -1114,7 +2292,7 @@ $ git cherry\-pick mislav@SHA . .fi . -.SS "git am" +.SS "git am, git apply" . .nf @@ -1125,6 +2303,10 @@ $ git am https://github\.com/defunkt/hub/pull/55 $ git am \-\-ignore\-whitespace https://github\.com/davidbalbert/hub/commit/fdb9921 > curl https://github\.com/davidbalbert/hub/commit/fdb9921\.patch \-o /tmp/fdb9921\.patch > git am \-\-ignore\-whitespace /tmp/fdb9921\.patch + +$ git apply https://gist\.github\.com/8da7fb575debd88c54cf +> curl https://gist\.github\.com/8da7fb575debd88c54cf\.txt \-o /tmp/gist\-8da7fb575debd88c54cf\.txt +> git apply /tmp/gist\-8da7fb575debd88c54cf\.txt . .fi . @@ -1133,18 +2315,47 @@ $ git am \-\-ignore\-whitespace https://github\.com/davidbalbert/hub/commit/fdb9 .nf $ git fork -\.\.\. hardcore forking action \.\.\. -> git remote add YOUR_USER git@github\.com:YOUR_USER/CURRENT_REPO\.git +[ repo forked on GitHub ] +> git remote add \-f YOUR_USER git@github\.com:YOUR_USER/CURRENT_REPO\.git . .fi . -.SS "git init" +.SS "git pull\-request" . .nf -$ git init \-g -> git init -> git remote add origin git@github\.com:YOUR_USER/REPO\.git +# while on a topic branch called "feature": +$ git pull\-request +[ opens text editor to edit title & body for the request ] +[ opened pull request on GitHub for "YOUR_USER:feature" ] + +# explicit title, pull base & head: +$ git pull\-request "I\'ve implemented feature X" \-b defunkt:master \-h mislav:feature + +$ git pull\-request \-i 123 +[ attached pull request to issue #123 ] +. +.fi +. +.SS "git checkout" +. +.nf + +$ git checkout https://github\.com/defunkt/hub/pull/73 +> git remote add \-f \-t feature git://github:com/mislav/hub\.git +> git checkout \-\-track \-B mislav\-feature mislav/feature + +$ git checkout https://github\.com/defunkt/hub/pull/73 custom\-branch\-name +. +.fi +. +.SS "git merge" +. +.nf + +$ git merge https://github\.com/defunkt/hub/pull/73 +> git fetch git://github\.com/mislav/hub\.git +refs/heads/feature:refs/remotes/mislav/feature +> git merge mislav/feature \-\-no\-ff \-m \'Merge pull request #73 from mislav/feature\.\.\.\' . .fi . @@ -1153,8 +2364,29 @@ $ git init \-g .nf $ git create -\.\.\. hardcore creating action \.\.\. +[ repo created on GitHub ] > git remote add origin git@github\.com:YOUR_USER/CURRENT_REPO\.git + +# with description: +$ git create \-d \'It shall be mine, all mine!\' + +$ git create recipes +[ repo created on GitHub ] +> git remote add origin git@github\.com:YOUR_USER/recipes\.git + +$ git create sinatra/recipes +[ repo created in GitHub organization ] +> git remote add origin git@github\.com:sinatra/recipes\.git +. +.fi +. +.SS "git init" +. +.nf + +$ git init \-g +> git init +> git remote add origin git@github\.com:YOUR_USER/REPO\.git . .fi . @@ -1176,12 +2408,18 @@ $ git push origin,staging,qa bert_timeout $ git browse > open https://github\.com/YOUR_USER/CURRENT_REPO +$ git browse \-\- commit/SHA +> open https://github\.com/YOUR_USER/CURRENT_REPO/commit/SHA + $ git browse \-\- issues > open https://github\.com/YOUR_USER/CURRENT_REPO/issues $ git browse schacon/ticgit > open https://github\.com/schacon/ticgit +$ git browse schacon/ticgit commit/SHA +> open https://github\.com/schacon/ticgit/commit/SHA + $ git browse resque > open https://github\.com/YOUR_USER/resque @@ -1197,7 +2435,7 @@ $ git browse resque network $ git compare refactor > open https://github\.com/CURRENT_REPO/compare/refactor -$ git compare 1\.0\.\.\.1\.1 +$ git compare 1\.0\.\.1\.1 > open https://github\.com/CURRENT_REPO/compare/1\.0\.\.\.1\.1 $ git compare \-u fix @@ -1208,6 +2446,21 @@ $ git compare other\-user patch . .fi . +.SS "git submodule" +. +.nf + +$ hub submodule add wycats/bundler vendor/bundler +> git submodule add git://github\.com/wycats/bundler\.git vendor/bundler + +$ hub submodule add \-p wycats/bundler vendor/bundler +> git submodule add git@github\.com:wycats/bundler\.git vendor/bundler + +$ hub submodule add \-b ryppl ryppl/pip vendor/pip +> git submodule add \-b ryppl git://github\.com/ryppl/pip\.git vendor/pip +. +.fi +. .SS "git help" . .nf @@ -1220,10 +2473,10 @@ $ git help hub .fi . .SH "BUGS" -\fIhttp://github\.com/defunkt/hub/issues\fR +\fIhttps://github\.com/defunkt/hub/issues\fR . .SH "AUTHORS" \fIhttps://github\.com/defunkt/hub/contributors\fR . .SH "SEE ALSO" -git(1), git\-clone(1), git\-remote(1), git\-init(1), \fIhttp://github\.com\fR, \fIhttp://github\.com/defunkt/hub\fR +git(1), git\-clone(1), git\-remote(1), git\-init(1), \fIhttp://github\.com\fR, \fIhttps://github\.com/defunkt/hub\fR -- 2.43.0