Browse files

[hub] latest version

  • Loading branch information...
1 parent bf39111 commit 6911ebde1eb9e73c6c6c462f3243aea4304cdb6f @sickill committed Jan 6, 2013
Showing with 253 additions and 83 deletions.
  1. +253 −83 bin/hub
View
336 bin/hub
@@ -7,7 +7,7 @@
#
module Hub
- Version = VERSION = '1.9.0'
+ Version = VERSION = '1.10.4'
end
module Hub
@@ -17,7 +17,6 @@ module Hub
def initialize(*args)
super
@executable = ENV["GIT"] || "git"
- @after = nil
@skip = @noop = false
@original_args = args.first
@chain = [nil]
@@ -212,10 +211,13 @@ module Hub
'github.com' == host ? 'api.github.com' : host
end
- def repo_exists? project
- res = get "https://%s/repos/%s/%s" %
+ def repo_info project
+ get "https://%s/repos/%s/%s" %
[api_host(project.host), project.owner, project.name]
- res.success?
+ end
+
+ def repo_exists? project
+ repo_info(project).success?
end
def fork_repo project
@@ -225,7 +227,7 @@ module Hub
end
def create_repo project, options = {}
- is_org = project.owner != config.username(api_host(project.host))
+ is_org = project.owner.downcase != config.username(api_host(project.host)).downcase
params = { :name => project.name, :private => !!options[:private] }
params[:description] = options[:description] if options[:description]
params[:homepage] = options[:homepage] if options[:homepage]
@@ -236,6 +238,7 @@ module Hub
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
@@ -278,7 +281,12 @@ module Hub
data['errors'].map do |err|
case err['code']
when 'custom' then err['message']
- when 'missing_field' then "field '%s' is missing" % err['field']
+ when 'missing_field'
+ %(Missing field: "%s") % err['field']
+ when 'invalid'
+ %(Invalid value for "%s": "%s") % [ err['field'], err['value'] ]
+ when 'unauthorized'
+ %(Not allowed to change field "%s") % err['field']
end
end.compact if data['errors']
end
@@ -292,10 +300,17 @@ module Hub
perform_request url, :Post do |req|
if params
req.body = JSON.dump params
- req['Content-Type'] = 'application/json'
+ req['Content-Type'] = 'application/json;charset=utf-8'
end
yield req if block_given?
- req['Content-Length'] = req.body ? req.body.length : 0
+ 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
@@ -304,19 +319,41 @@ module Hub
end
def perform_request url, type
- url = URI.parse url unless url.respond_to? :hostname
+ url = URI.parse url unless url.respond_to? :host
require 'net/https'
- req = Net::HTTP.const_get(type).new(url.request_uri)
- http = create_connection(url)
+ 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?
- res = http.start { http.request(req) }
- res.extend ResponseMethods
- res
- rescue SocketError => err
- raise Context::FatalError, "error with #{type.to_s.upcase} #{url} (#{err.message})"
+
+ 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
@@ -348,13 +385,20 @@ module Hub
module OAuth
def apply_authentication req, url
- if req.path.index('/authorizations') == 0
+ if (req.path =~ /\/authorizations$/)
super
else
+ refresh = false
user = url.user || config.username(url.host)
token = config.oauth_token(url.host, user) {
+ refresh = true
obtain_oauth_token url.host, user
}
+ if refresh
+ res = get "https://#{url.host}/user"
+ res.error! unless res.success?
+ config.update_username(url.host, user, res.data['login'])
+ end
req['Authorization'] = "token #{token}"
end
end
@@ -418,12 +462,13 @@ module Hub
end
def load
- @data.update YAML.load(File.read(@filename))
+ 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') {|f| f << YAML.dump(@data) }
+ File.open(@filename, 'w', 0600) {|f| f << YAML.dump(@data) }
end
end
@@ -439,6 +484,7 @@ module Hub
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
@@ -447,6 +493,12 @@ module Hub
end
end
+ def update_username host, old_username, new_username
+ entry = @data.entry_for_user(normalize_host(host), old_username)
+ entry['user'] = new_username
+ @data.save
+ end
+
def api_token host, user
host = normalize_host host
@data.fetch_value host, user, :api_token do
@@ -457,6 +509,7 @@ module Hub
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
@@ -472,16 +525,23 @@ module Hub
def prompt_password host, user
print "#{host} password for #{user} (never stored): "
- password = askpass
- puts ''
- password
+ if $stdin.tty?
+ password = askpass
+ puts ''
+ password
+ else
+ $stdin.gets.chomp
+ end
end
+ NULL = defined?(File::NULL) ? File::NULL :
+ File.exist?('/dev/null') ? '/dev/null' : 'NUL'
+
def askpass
- tty_state = `stty -g`
+ tty_state = `stty -g 2>#{NULL}`
system 'stty raw -echo -icanon isig' if $?.success?
pass = ''
- while char = $stdin.getbyte and not (char == 13 or char == 10)
+ while char = $stdin.getbyte and !(char == 13 or char == 10)
if char == 127 or char == 8
pass[-1,1] = '' unless pass.empty?
else
@@ -495,7 +555,7 @@ module Hub
def proxy_uri(with_ssl)
env_name = "HTTP#{with_ssl ? 'S' : ''}_PROXY"
- if proxy = ENV[env_name] || ENV[env_name.downcase]
+ if proxy = ENV[env_name] || ENV[env_name.downcase] and !proxy.empty?
proxy = "http://#{proxy}" unless proxy.include? '://'
URI.parse proxy
end
@@ -600,7 +660,7 @@ module Hub
end
repo_methods = [
- :current_branch, :master_branch,
+ :current_branch,
:current_project, :upstream_project,
:repo_owner, :repo_host,
:remotes, :remotes_group, :origin_remote
@@ -609,6 +669,14 @@ module Hub
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
@@ -676,17 +744,22 @@ module Hub
end
def known_hosts
- git_config('hub.host', :all).to_s.split("\n") + [default_host]
+ hosts = git_config('hub.host', :all).to_s.split("\n")
+ hosts << default_host
+ hosts << "ssh.#{default_host}"
end
- def default_host
+ def self.default_host
ENV['GITHUB_HOST'] || main_host
end
- def main_host
+ 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
@@ -700,13 +773,18 @@ module Hub
end
end
+ attr_accessor :repo_data
+
def initialize(*args)
super
- self.host ||= local_repo.default_host
+ 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?
- local_repo and host != local_repo.main_host
+ repo_data ? repo_data.fetch('private') :
+ host != (local_repo || LocalRepo).main_host
end
def owned_by(new_owner)
@@ -810,7 +888,7 @@ module Hub
end
def project
- urls.each { |url|
+ urls.each_value { |url|
if valid = GithubProject.from_url(url, local_repo)
return valid
end
@@ -819,15 +897,21 @@ module Hub
end
def urls
- @urls ||= local_repo.git_config("remote.#{name}.url", :all).to_s.split("\n").map { |uri|
- begin
- if uri =~ %r{^[\w-]+://} then uri_parse(uri)
- elsif uri =~ %r{^([^/]+?):} then uri_parse("ssh://#{$1}/#{$'}") # scp-like syntax
+ 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
- rescue URI::InvalidURIError
- nil
end
- }.compact
+ end
+ @urls
end
def uri_parse uri
@@ -855,7 +939,7 @@ module Hub
project.name = name
project
else
- GithubProject.new(local_repo, owner, name)
+ GithubProject.new(local_repo(false), owner, name)
end
end
@@ -902,13 +986,15 @@ module Hub
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
+ if File.exist? editor then [editor]
+ else editor.shellsplit
+ end
end
module System
def browser_launcher
browser = ENV['BROWSER'] || (
- osx? ? 'open' : windows? ? 'start' :
+ osx? ? 'open' : windows? ? %w[cmd /c start] :
%w[xdg-open cygstart x-www-browser firefox opera mozilla netscape].find { |comm| which comm }
)
@@ -940,6 +1026,10 @@ module Hub
def command?(name)
!which(name).nil?
end
+
+ def tmp_dir
+ ENV['TMPDIR'] || ENV['TEMP'] || '/tmp'
+ end
end
include System
@@ -1059,10 +1149,22 @@ class Hub::JSON
end
end
- def generate_String(str) str.inspect end
- alias generate_Numeric generate_String
- alias generate_TrueClass generate_String
- alias generate_FalseClass generate_String
+ 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
@@ -1087,7 +1189,7 @@ module Hub
extend Context
- NAME_RE = /\w[\w.-]*/
+ NAME_RE = /[\w.][\w.-]*/
OWNER_RE = /[a-zA-Z0-9-]+/
NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/
@@ -1106,7 +1208,7 @@ module Hub
respect_help_flags(expanded_args || args) if custom_command? cmd
- cmd = cmd.sub(/(\w)-/, '\1_')
+ cmd = cmd.gsub(/(\w)-/, '\1_')
if method_defined?(cmd) and cmd != 'run'
args.replace expanded_args if expanded_args
send(cmd, args)
@@ -1246,9 +1348,8 @@ module Hub
if arg =~ NAME_WITH_OWNER_RE and !File.directory?(arg)
name, owner = arg, nil
owner, name = name.split('/', 2) if name.index('/')
- host = ENV['GITHUB_HOST'] || 'github.com'
- project = Context::GithubProject.new(nil, owner || github_user(host), name, host)
- ssh ||= args[0] != 'submodule' && project.owner == github_user(host) || host != 'github.com'
+ 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
@@ -1318,7 +1419,11 @@ module Hub
projects = names.map { |name|
unless name =~ /\W/ or remotes.include?(name) or remotes_group(name)
project = github_project(nil, name)
- project if api_client.repo_exists?(project)
+ repo_info = api_client.repo_info(project)
+ if repo_info.success?
+ project.repo_data = repo_info.data
+ project
+ end
end
}.compact
@@ -1354,6 +1459,28 @@ module Hub
end
end
+ 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)
+
+ 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')
ref = args.words.last
@@ -1385,7 +1512,7 @@ module Hub
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'] || '/tmp', "#{gist ? 'gist-' : ''}#{File.basename(url)}")
+ patch_file = File.join(tmp_dir, "#{gist ? 'gist-' : ''}#{File.basename(url)}")
args.before 'curl', ['-#LA', "hub #{Hub::Version}", url, '-o', patch_file]
args[idx] = patch_file
end
@@ -1395,8 +1522,7 @@ module Hub
def init(args)
if args.delete('-g')
- host = ENV['GITHUB_HOST'] || 'github.com'
- project = Context::GithubProject.new(nil, github_user(host), File.basename(current_dir), host)
+ project = github_project(File.basename(current_dir))
url = project.git_url(:private => true, :https => https_protocol?)
args.after ['remote', 'add', 'origin', url]
end
@@ -1408,9 +1534,14 @@ module Hub
end
forked_project = project.owned_by(github_user(project.host))
- if api_client.repo_exists?(forked_project)
- abort "Error creating fork: %s already exists on %s" %
- [ forked_project.name_with_owner, forked_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
@@ -1430,7 +1561,8 @@ module Hub
def create(args)
if !is_repo?
abort "'create' must be run from inside a git repository"
- elsif owner = github_user
+ else
+ owner = github_user
args.shift
options = {}
options[:private] = true if args.delete('-p')
@@ -1459,7 +1591,10 @@ module Hub
action = "set remote origin"
else
action = "created repository"
- api_client.create_repo(new_project, options) unless args.noop?
+ unless args.noop?
+ repo_data = api_client.create_repo(new_project, options)
+ new_project = github_project(repo_data['full_name'])
+ end
end
url = new_project.git_url(:private => true, :https => https_protocol?)
@@ -1480,12 +1615,17 @@ module Hub
def push(args)
return if args[1].nil? || !args[1].index(',')
- branch = (args[2] ||= current_branch.short_name)
+ 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
@@ -1620,10 +1760,9 @@ module Hub
end
end
- def github_user host = nil
- host ||= local_repo(false) && local_repo.main_host
- return nil if host.nil?
- api_client.config.username(host) { }
+ 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
@@ -1683,12 +1822,19 @@ 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
+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 <command>' for more information on a specific command.
help
end
@@ -1791,6 +1937,7 @@ help
read.close
write.close
end
+ rescue NotImplementedError
end
def pullrequest_editmsg(changes)
@@ -1823,7 +1970,7 @@ help
title.tr!("\n", ' ')
title.strip!
body.strip!
-
+
[title =~ /\S/ ? title : nil, body =~ /\S/ ? body : nil]
end
@@ -1835,9 +1982,9 @@ help
end
end
end
-
+
def display_api_exception(action, response)
- $stderr.puts "Error #{action}: #{response.message} (HTTP #{response.status})"
+ $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
@@ -1883,16 +2030,11 @@ module Hub
if args.noop?
puts commands
elsif not args.skip?
- if args.chained?
- execute_command_chain
- else
- exec(*args.to_exec)
- end
+ execute_command_chain args.commands
end
end
- def execute_command_chain
- commands = args.commands
+ def execute_command_chain commands
commands.each_with_index do |cmd, i|
if cmd.respond_to?(:call) then cmd.call
elsif i == commands.length - 1
@@ -1902,6 +2044,14 @@ module Hub
end
end
end
+
+ def exec *args
+ if args.first == 'echo' && Context::windows?
+ puts args[1..-1].join(' ')
+ else
+ super
+ end
+ end
end
end
@@ -1911,7 +2061,7 @@ __END__
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "HUB" "1" "April 2012" "DEFUNKT" "Git Manual"
+.TH "HUB" "1" "November 2012" "DEFUNKT" "Git Manual"
.
.SH "NAME"
\fBhub\fR \- git + hub = github
@@ -1941,6 +2091,9 @@ __END__
\fBgit checkout\fR \fIPULLREQ\-URL\fR [\fIBRANCH\fR]
.
.br
+\fBgit merge\fR \fIPULLREQ\-URL\fR
+.
+.br
\fBgit cherry\-pick\fR \fIGITHUB\-REF\fR
.
.br
@@ -2006,6 +2159,10 @@ Adds missing remote(s) with \fBgit remote add\fR prior to fetching\. New remotes
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\.
.
.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\.
+.
+.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 <user>\fR is issued prior to the cherry\-pick attempt\.
.
@@ -2058,6 +2215,9 @@ If instead of normal \fITITLE\fR an issue number is given with \fB\-i\fR, the pu
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
@@ -2210,11 +2370,21 @@ $ git pull\-request \-i 123
.
.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
+> 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 checkout https://github\.com/defunkt/hub/pull/73 custom\-branch\-name
+$ 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
.

0 comments on commit 6911ebd

Please sign in to comment.