Browse files

HUGE project-centric refactoring to support GitHub Enterprise

Add your GH Enterprise host to the list of known hosts:

  $ git config --global --add hub.host my.git.org

Configure your username and token for this host:

  $ git config --global github."my.git.org".user mislav
  $ git config --global github."my.git.org".token 1234abcd

Now a lot of operations should work for repos that were cloned from "my.git.org".

The default host is still github.com but this can be affected with the
`GITHUB_HOST` environment variable.

  $ GITHUB_HOST=my.git.org git clone myproject

Closes #98
  • Loading branch information...
1 parent c69991f commit 06c18ff190ae116210edfe508b732eaea9dc9708 @mislav mislav committed Dec 21, 2011
Showing with 268 additions and 100 deletions.
  1. +64 −67 lib/hub/commands.rb
  2. +148 −18 lib/hub/context.rb
  3. +56 −15 test/hub_test.rb
View
131 lib/hub/commands.rb
@@ -34,12 +34,6 @@ module Commands
# provides git interrogation methods
extend Context
- API_REPO = 'https://github.com/api/v2/yaml/repos/show/%s/%s'
- API_FORK = 'https://github.com/api/v2/yaml/repos/fork/%s/%s'
- API_CREATE = 'https://github.com/api/v2/yaml/repos/create'
- API_PULL = 'https://github.com/api/v2/json/pulls/%s'
- API_PULLREQUEST = 'https://github.com/api/v2/yaml/pulls/%s/%s'
-
NAME_RE = /[\w.-]+/
OWNER_RE = /[a-zA-Z0-9-]+/
NAME_WITH_OWNER_RE = /^(?:#{NAME_RE}|#{OWNER_RE}\/#{NAME_RE})$/
@@ -99,11 +93,11 @@ def pull_request(args)
head_project, options[:head] = from_github_ref.call(head, head_project)
when '-i'
options[:issue] = args.shift
- when %r{^https?://github.com/([^/]+/[^/]+)/issues/(\d+)}
- options[:issue] = $2
- base_project = github_project($1)
else
- if !options[:title] then options[:title] = arg
+ 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
@@ -287,14 +281,16 @@ def fetch(args)
names = []
end
- names.reject! { |name|
- name =~ /\W/ or remotes.include?(name) or
- remotes_group(name) or not repo_exists?(name)
- }
+ projects = names.map { |name|
+ unless name =~ /\W/ or remotes.include?(name) or remotes_group(name)
+ project = github_project(nil, name)
+ project if repo_exists?(project)
+ end
+ }.compact
- if names.any?
- names.each do |name|
- args.before ['remote', 'add', name, git_url(name)]
+ if projects.any?
+ projects.each do |project|
+ args.before ['remote', 'add', project.owner, project.git_url(:https => https_protocol?)]
end
end
end
@@ -303,11 +299,11 @@ def fetch(args)
# > git remote add -f -t feature git://github:com/mislav/hub.git
# > git checkout -b mislav-feature mislav/feature
def checkout(args)
- if (2..3) === args.length and args[1] =~ %r{https?://github.com/(.+?)/(.+?)/pull/(\d+)}
- owner, repo, pull_id = $1, $2, $3
+ if (2..3) === args.length and url = resolve_github_url(args[1]) and url.project_path =~ /^pull\/(\d+)/
+ pull_id = $1
load_net_http
- response = http_request(API_PULL % File.join(owner, repo, pull_id))
+ response = http_request(url.project.api_pullrequest_url(pull_id, 'json'))
pull_body = response.body
user, branch = pull_body.match(/"label":\s*"(.+?)"/)[1].split(':', 2)
@@ -317,7 +313,7 @@ def checkout(args)
args.before ['remote', 'set-branches', '--add', user, branch]
args.before ['fetch', user, "+refs/heads/#{branch}:refs/remotes/#{user}/#{branch}"]
else
- args.before ['remote', 'add', '-f', '-t', branch, user, github_project(repo, user).git_url]
+ args.before ['remote', 'add', '-f', '-t', branch, user, github_project(url.project_name, user).git_url]
end
args[1..-1] = ['-b', new_branch_name, "#{user}/#{branch}"]
end
@@ -336,25 +332,22 @@ def checkout(args)
# > git cherry-pick SHA
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-f0-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
- # fetch from origin if the repo belongs to the user
- args.before ['fetch', origin_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]
else
- args.before ['remote', 'add', '-f', user, git_url(user, repo)]
+ args.before ['remote', 'add', '-f', project.owner, project.git_url(:https => https_protocol?)]
end
end
end
@@ -387,7 +380,9 @@ def am(args)
# > git remote add origin git@github.com:USER/REPO.git
def init(args)
if args.delete('-g')
- url = git_url(github_user, File.basename(current_dir), :private => true)
+ # can't use default_host because there is no local_repo yet
+ project = Context::GithubProject.new(nil, github_user, File.basename(current_dir), 'github.com')
+ url = project.git_url(:private => true, :https => https_protocol?)
args.after ['remote', 'add', 'origin', url]
end
end
@@ -396,21 +391,22 @@ def init(args)
# ... hardcore forking action ...
# > git remote add -f YOUR_USER git@github.com:YOUR_USER/CURRENT_REPO.git
def fork(args)
- # can't do anything without token and original owner name
- if github_user && github_token && repo_owner
- if repo_exists?(github_user)
- warn "#{github_user}/#{repo_name} already exists on GitHub"
- else
- fork_repo unless args.noop?
- end
+ 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(true, project.host))
+ if repo_exists?(forked_project)
+ warn "#{forked_project.name_with_owner} already exists on #{forked_project.host}"
+ else
+ fork_repo(project) unless args.noop?
+ end
- if args.include?('--no-remote')
- exit
- else
- url = git_url(github_user, repo_name, :private => true)
- args.replace %W"remote add -f #{github_user} #{url}"
- args.after 'echo', ['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 HTTPExceptions
display_http_exception("creating fork", $!.response)
@@ -445,25 +441,25 @@ def create(args)
end
end
new_repo_name ||= repo_name
- repo_with_owner = "#{owner}/#{new_repo_name}"
+ new_project = github_project(new_repo_name, owner)
- if repo_exists?(owner, new_repo_name)
- warn "#{repo_with_owner} already exists on GitHub"
+ if 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(repo_with_owner, options) unless args.noop?
+ create_repo(new_project, options) unless args.noop?
end
- url = git_url(owner, new_repo_name, :private => true)
+ url = new_project.git_url(:private => true, :https => https_protocol?)
if remotes.first != 'origin'
args.replace %W"remote add -f origin #{url}"
else
args.replace %W"remote -v"
end
- args.after 'echo', ["#{action}:", repo_with_owner]
+ args.after 'echo', ["#{action}:", new_project.name_with_owner]
end
rescue HTTPExceptions
display_http_exception("creating repository", $!.response)
@@ -836,31 +832,32 @@ def page_stdout
end
# Determines whether a user has a fork of the current repo on GitHub.
- def repo_exists?(user, repo = repo_name)
+ def repo_exists?(project)
load_net_http
- Net::HTTPSuccess === http_request(API_REPO % [user, repo])
+ Net::HTTPSuccess === http_request(project.api_show_url('yaml'))
end
# Forks the current repo using the GitHub API.
#
# Returns nothing.
- def fork_repo
+ def fork_repo(project)
load_net_http
- response = http_post API_FORK % [repo_owner, repo_name]
+ response = http_post project.api_fork_url('yaml')
response.error! unless Net::HTTPSuccess === response
end
# Creates a new repo using the GitHub API.
#
# Returns nothing.
- def create_repo(name, options = {})
- params = {'name' => name.sub(/^#{github_user}\//, '')}
+ def create_repo(project, options = {})
+ is_org = project.owner != github_user(true, project.host)
+ params = {'name' => is_org ? project.name_with_owner : project.name}
params['public'] = '0' if options[:private]
params['description'] = options[:description] if options[:description]
params['homepage'] = options[:homepage] if options[:homepage]
load_net_http
- response = http_post(API_CREATE, params)
+ response = http_post(project.api_create_url('yaml'), params)
response.error! unless Net::HTTPSuccess === response
end
@@ -876,7 +873,7 @@ def create_pullrequest(options)
params['pull[body]'] = options[:body] if options[:body]
load_net_http
- response = http_post(API_PULLREQUEST % [project.owner, project.name], params)
+ response = http_post(project.api_create_pullrequest_url('yaml'), params)
response.error! unless Net::HTTPSuccess === response
# GitHub bug: although we request YAML, it returns JSON
if response['Content-type'].to_s.include? 'application/json'
@@ -931,7 +928,7 @@ def expand_alias(cmd)
def http_request(url, type = :Get)
url = URI(url)
- user, token = github_user(type != :Get), github_token(type != :Get)
+ user, token = github_user(type != :Get, url.host), github_token(type != :Get, url.host)
req = Net::HTTP.const_get(type).new(url.request_uri)
req.basic_auth "#{user}/token", token if user and token
View
166 lib/hub/context.rb
@@ -1,5 +1,6 @@
require 'shellwords'
require 'forwardable'
+require 'uri'
module Hub
# Provides methods for inspecting the environment, such as GitHub user/token
@@ -92,7 +93,7 @@ def local_repo
repo_methods = [
:current_branch, :master_branch,
:current_project, :upstream_project,
- :repo_owner,
+ :repo_owner, :repo_host,
:remotes, :remotes_group, :origin_remote
]
def_delegator :local_repo, :name, :repo_name
@@ -116,6 +117,10 @@ def repo_owner
end
end
+ def repo_host
+ project = main_project and project.host
+ end
+
def main_project
remote = origin_remote and remote.project
end
@@ -162,9 +167,43 @@ def origin_remote
def remote_by_name(remote_name)
remotes.find {|r| r.name == remote_name }
end
+
+ def known_hosts
+ git_config('hub.host', :all).to_s.split("\n") + [default_host]
+ end
+
+ def default_host
+ ENV['GITHUB_HOST'] || main_host
+ end
+
+ def main_host
+ 'github.com'
+ end
end
- class GithubProject < Struct.new(:local_repo, :owner, :name)
+ 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
+
+ def initialize(*args)
+ super
+ self.host ||= local_repo.default_host
+ end
+
+ def private?
+ local_repo and host != local_repo.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
@@ -187,15 +226,67 @@ def web_url(path = nil)
path = '/wiki' + path
end
end
- 'https://github.com/' + project_name + path.to_s
+ "https://#{host}/" + project_name + path.to_s
end
def git_url(options = {})
- if options[:https] then 'https://github.com/'
- elsif options[:private] then 'git@github.com:'
- else 'git://github.com/'
+ if options[:https] then "https://#{host}/"
+ elsif options[:private] or private? then "git@#{host}:"
+ else "git://#{host}/"
end + name_with_owner + '.git'
end
+
+ def api_url(type, resource, action)
+ URI("https://#{host}/api/v2/#{type}/#{resource}/#{action}")
+ end
+
+ def api_show_url(type)
+ api_url(type, 'repos', "show/#{owner}/#{name}")
+ end
+
+ def api_fork_url(type)
+ api_url(type, 'repos', "fork/#{owner}/#{name}")
+ end
+
+ def api_create_url(type)
+ api_url(type, 'repos', 'create')
+ end
+
+ def api_pullrequest_url(id, type)
+ api_url(type, 'pulls', "#{owner}/#{name}/#{id}")
+ end
+
+ def api_create_pullrequest_url(type)
+ api_url(type, 'pulls', "#{owner}/#{name}")
+ 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
+
+ # segment of path after the project owner and name
+ def project_path
+ path.split('/', 4)[3]
+ end
end
class Branch < Struct.new(:local_repo, :name)
@@ -233,14 +324,24 @@ def ==(other)
end
def project
- if urls.find { |u| u =~ %r{\bgithub\.com[:/](.+)/(.+)\z} }
- owner = $1
- GithubProject.new local_repo, owner, $2.sub(/\.git$/, '')
- end
+ urls.each { |url|
+ if valid = GithubProject.from_url(url, local_repo)
+ return valid
+ end
+ }
+ nil
end
def urls
- @urls ||= local_repo.git_config("remote.#{name}.url", :all).to_s.split("\n")
+ @urls ||= local_repo.git_config("remote.#{name}.url", :all).to_s.split("\n").map { |uri|
+ begin
+ if uri =~ %r{^[\w-]+://} then URI(uri)
+ elsif uri =~ %r{^([^/]+?):} then URI("ssh://#{$1}/#{$'}") # scp-like syntax
+ end
+ rescue URI::InvalidURIError
+ nil
+ end
+ }.compact
end
end
@@ -256,31 +357,60 @@ def github_project(name, owner = nil)
owner ||= github_user
end
- GithubProject.new local_repo, owner, name
+ if local_repo and main_project = local_repo.main_project
+ project = main_project.dup
+ project.owner = owner
+ project.name = name
+ project
+ else
+ GithubProject.new(local_repo, 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
+
LGHCONF = "http://help.github.com/set-your-user-name-email-and-github-token/"
# Either returns the GitHub user as set by git-config(1) or aborts
# with an error message.
- def github_user(fatal = true)
- if user = ENV['GITHUB_USER'] || git_config('github.user')
+ def github_user(fatal = true, host = nil)
+ if local = local_repo
+ host ||= local.default_host
+ host = nil if host == local.main_host
+ end
+ host = %(."#{host}") if host
+ if user = ENV['GITHUB_USER'] || git_config("github#{host}.user")
user
elsif fatal
- abort("** No GitHub user set. See #{LGHCONF}")
+ if host.nil?
+ abort("** No GitHub user set. See #{LGHCONF}")
+ else
+ abort("** No user set for github#{host}")
+ end
end
end
- def github_token(fatal = true)
- if token = ENV['GITHUB_TOKEN'] || git_config('github.token')
+ def github_token(fatal = true, host = nil)
+ if local = local_repo
+ host ||= local.default_host
+ host = nil if host == local.main_host
+ end
+ host = %(."#{host}") if host
+ if token = ENV['GITHUB_TOKEN'] || git_config("github#{host}.token")
token
elsif fatal
- abort("** No GitHub token set. See #{LGHCONF}")
+ if host.nil?
+ abort("** No GitHub token set. See #{LGHCONF}")
+ else
+ abort("** No token set for github#{host}")
+ end
end
end
View
71 test/hub_test.rb
@@ -57,6 +57,7 @@ def setup
'rev-parse --symbolic-full-name master@{upstream}' => 'refs/remotes/origin/master',
'config --get --bool hub.http-clone' => 'false',
'config --get hub.protocol' => nil,
+ 'config --get-all hub.host' => nil,
'rev-parse -q --git-dir' => '.git'
end
@@ -149,6 +150,14 @@ def test_clone_with_host_alias
assert_forwarded "clone server:git/repo.git"
end
+ def test_enterprise_clone
+ stub_github_user('myfiname', 'git.my.org')
+ with_host_env('git.my.org') do
+ assert_command "clone myrepo", "git clone git@git.my.org:myfiname/myrepo.git"
+ assert_command "clone another/repo", "git clone git@git.my.org:another/repo.git"
+ end
+ end
+
def test_alias_expand
stub_alias 'c', 'clone --bare'
input = "c rtomayko/ronn"
@@ -186,10 +195,12 @@ def test_private_remote_origin
assert_command input, command
end
- def test_public_remote_origin_as_normal
- input = "remote add origin http://github.com/defunkt/resque.git"
- command = "git remote add origin http://github.com/defunkt/resque.git"
- assert_command input, command
+ def test_public_remote_url_untouched
+ assert_forwarded "remote add origin http://github.com/defunkt/resque.git"
+ end
+
+ def test_private_remote_url_untouched
+ assert_forwarded "remote add origin git@github.com:defunkt/resque.git"
end
def test_remote_from_rel_path
@@ -204,8 +215,10 @@ def test_remote_with_host_alias
assert_forwarded "remote add origin server:/git/repo.git"
end
- def test_private_remote_origin_as_normal
- assert_forwarded "remote add origin git@github.com:defunkt/resque.git"
+ def test_remote_add_enterprise
+ stub_hub_host('git.my.org')
+ stub_repo_url('git@git.my.org:defunkt/hub.git')
+ assert_command "remote add another", "git remote add another git@git.my.org:another/hub.git"
end
def test_public_submodule
@@ -655,7 +668,7 @@ def test_create_with_existing_repository
stub_no_remotes
stub_existing_fork('tpw')
- expected = "tpw/hub already exists on GitHub\n"
+ expected = "tpw/hub already exists on github.com\n"
expected << "remote add -f origin git@github.com:tpw/hub.git\n"
expected << "set remote origin: tpw/hub\n"
assert_equal expected, hub("create") { ENV['GIT'] = 'echo' }
@@ -666,7 +679,7 @@ def test_create_https_protocol
stub_existing_fork('tpw')
stub_https_is_preferred
- expected = "tpw/hub already exists on GitHub\n"
+ expected = "tpw/hub already exists on github.com\n"
expected << "remote add -f origin https://github.com/tpw/hub.git\n"
expected << "set remote origin: tpw/hub\n"
assert_equal expected, hub("create") { ENV['GIT'] = 'echo' }
@@ -703,6 +716,21 @@ def test_fork
assert_equal expected, hub("fork") { ENV['GIT'] = 'echo' }
end
+ def test_fork_enterprise
+ stub_hub_host('git.my.org')
+ stub_repo_url('git@git.my.org:defunkt/hub.git')
+ stub_github_user('myfiname', 'git.my.org')
+ stub_github_token('789xyz', 'git.my.org')
+
+ stub_request(:get, "https://#{auth('myfiname', '789xyz')}git.my.org/api/v2/yaml/repos/show/myfiname/hub").
+ to_return(:status => 404)
+ stub_request(:post, "https://#{auth('myfiname', '789xyz')}git.my.org/api/v2/yaml/repos/fork/defunkt/hub")
+
+ expected = "remote add -f myfiname git@git.my.org:myfiname/hub.git\n"
+ expected << "new remote: myfiname\n"
+ assert_output expected, "fork"
+ end
+
def test_fork_failed
stub_nonexisting_fork('tpw')
stub_request(:post, "https://#{auth}github.com/api/v2/yaml/repos/fork/defunkt/hub").
@@ -722,7 +750,7 @@ def test_fork_no_remote
def test_fork_already_exists
stub_existing_fork('tpw')
- expected = "tpw/hub already exists on GitHub\n"
+ expected = "tpw/hub already exists on github.com\n"
expected << "remote add -f tpw git@github.com:tpw/hub.git\n"
expected << "new remote: tpw\n"
assert_equal expected, hub("fork") { ENV['GIT'] = 'echo' }
@@ -732,7 +760,7 @@ def test_fork_https_protocol
stub_existing_fork('tpw')
stub_https_is_preferred
- expected = "tpw/hub already exists on GitHub\n"
+ expected = "tpw/hub already exists on github.com\n"
expected << "remote add -f tpw https://github.com/tpw/hub.git\n"
expected << "new remote: tpw\n"
assert_equal expected, hub("fork") { ENV['GIT'] = 'echo' }
@@ -1146,14 +1174,16 @@ def test_global_flags_preserved
assert_equal %w[git --bare -c core.awesome=true -c name=value --git-dir=/srv/www], git_reader.executable
end
- protected
+ private
- def stub_github_user(name)
- stub_config_value 'github.user', name
+ def stub_github_user(name, host = '')
+ host = %(."#{host}") unless host.empty?
+ stub_config_value "github#{host}.user", name
end
- def stub_github_token(token)
- stub_config_value 'github.token', token
+ def stub_github_token(token, host = '')
+ host = %(."#{host}") unless host.empty?
+ stub_config_value "github#{host}.token", token
end
def stub_repo_url(value, remote_name = 'origin')
@@ -1210,6 +1240,10 @@ def stub_https_is_preferred
stub_config_value 'hub.protocol', 'https'
end
+ def stub_hub_host(names)
+ stub_config_value "hub.host", Array(names).join("\n"), '--get-all'
+ end
+
def with_browser_env(value)
browser, ENV['BROWSER'] = ENV['BROWSER'], value
yield
@@ -1224,6 +1258,13 @@ def with_tmpdir(value)
ENV['TMPDIR'] = dir
end
+ def with_host_env(value)
+ host, ENV['GITHUB_HOST'] = ENV['GITHUB_HOST'], value
+ yield
+ ensure
+ ENV['GITHUB_HOST'] = host
+ end
+
def assert_browser(browser)
assert_command "browse", "#{browser} https://github.com/defunkt/hub"
end

0 comments on commit 06c18ff

Please sign in to comment.