diff --git a/Gemfile b/Gemfile index edc953e9d..741ddfada 100644 --- a/Gemfile +++ b/Gemfile @@ -3,5 +3,7 @@ source 'https://rubygems.org' gem 'ronn' gem 'aruba' gem 'cucumber' +gem 'sinatra' +gem 'thin' gemspec diff --git a/features/fork.feature b/features/fork.feature new file mode 100644 index 000000000..04a045e7d --- /dev/null +++ b/features/fork.feature @@ -0,0 +1,108 @@ +Feature: hub fork + Background: + Given I am in "dotfiles" git repo + And the "origin" remote has url "git://github.com/evilchelu/dotfiles.git" + And I am "mislav" on github.com with OAuth token "OTOKEN" + + Scenario: Fork the repository + Given the GitHub API server: + """ + before { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' } + get('/repos/evilchelu/dotfiles', :host_name => 'api.github.com') { '' } + post('/repos/evilchelu/dotfiles/forks', :host_name => 'api.github.com') { '' } + """ + When I successfully run `hub fork` + Then the output should contain exactly "new remote: mislav\n" + And "git remote add -f mislav git@github.com:mislav/dotfiles.git" should be run + And the url for "mislav" should be "git@github.com:mislav/dotfiles.git" + + Scenario: --no-remote + Given the GitHub API server: + """ + get('/repos/evilchelu/dotfiles') { '' } + post('/repos/evilchelu/dotfiles/forks') { '' } + """ + When I successfully run `hub fork --no-remote` + Then there should be no output + And there should be no "mislav" remote + + Scenario: Fork failed + Given the GitHub API server: + """ + get('/repos/evilchelu/dotfiles') { '' } + post('/repos/evilchelu/dotfiles/forks') { halt 500 } + """ + When I run `hub fork` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error creating fork: Internal Server Error (HTTP 500)\n + """ + And there should be no "mislav" remote + + Scenario: Fork already exists + Given the GitHub API server: + """ + get('/repos/evilchelu/dotfiles') { '' } + get('/repos/mislav/dotfiles') { '' } + """ + When I run `hub fork` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error creating fork: mislav/dotfiles already exists on github.com\n + """ + And there should be no "mislav" remote + + Scenario: Invalid OAuth token + Given the GitHub API server: + """ + before { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token OTOKEN' } + get('/repos/evilchelu/dotfiles') { '' } + """ + And I am "mislav" on github.com with OAuth token "WRONGTOKEN" + When I run `hub fork` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error creating fork: Unauthorized (HTTP 401)\n + """ + + Scenario: HTTPS is preferred + Given the GitHub API server: + """ + get('/repos/evilchelu/dotfiles') { '' } + post('/repos/evilchelu/dotfiles/forks') { '' } + """ + And HTTPS is preferred + When I successfully run `hub fork` + Then the output should contain exactly "new remote: mislav\n" + And the url for "mislav" should be "https://github.com/mislav/dotfiles.git" + + Scenario: Not in repo + Given the current dir is not a repo + When I run `hub fork` + Then the exit status should be 1 + And the stderr should contain "fatal: Not a git repository" + + Scenario: Unknown host + Given the "origin" remote has url "git@git.my.org:evilchelu/dotfiles.git" + When I run `hub fork` + Then the exit status should be 1 + And the stderr should contain exactly: + """ + Error: repository under 'origin' remote is not a GitHub project\n + """ + + Scenario: Enterprise fork + Given the GitHub API server: + """ + before { halt 401 unless request.env['HTTP_AUTHORIZATION'] == 'token FITOKEN' } + get('/repos/evilchelu/dotfiles', :host_name => 'git.my.org') { '' } + post('/repos/evilchelu/dotfiles/forks', :host_name => 'git.my.org') { '' } + """ + And the "origin" remote has url "git@git.my.org:evilchelu/dotfiles.git" + And I am "mislav" on git.my.org with OAuth token "FITOKEN" + And "git.my.org" is a whitelisted Enterprise host + When I successfully run `hub fork` + Then the url for "mislav" should be "git@git.my.org:mislav/dotfiles.git" diff --git a/features/steps.rb b/features/steps.rb index c1fe64512..0347f5ae4 100644 --- a/features/steps.rb +++ b/features/steps.rb @@ -1,3 +1,5 @@ +require 'fileutils' + Given /^HTTPS is preferred$/ do run_silent %(git config --global hub.protocol https) end @@ -11,12 +13,19 @@ end Given /^the "([^"]*)" remote has url "([^"]*)"$/ do |remote_name, url| - run_silent %(git remote add #{remote_name} "#{url}") + remotes = run_silent('git remote').split("\n") + unless remotes.include? remote_name + run_silent %(git remote add #{remote_name} "#{url}") + else + run_silent %(git remote set-url #{remote_name} "#{url}") + end end -Given /^I am "([^"]*)" on ([\w.-]+)$/ do |name, host| +Given /^I am "([^"]*)" on ([\w.-]+)(?: with OAuth token "([^"]*)")?$/ do |name, host, token| edit_hub_config do |cfg| - cfg[host.downcase] = [{'user' => name}] + entry = {'user' => name} + entry['oauth_token'] = token if token + cfg[host.downcase] = [entry] end end @@ -36,6 +45,20 @@ dirs.pop end +Given /^the current dir is not a repo$/ do + in_current_dir do + FileUtils.rm_rf '.git' + end +end + +Given /^the GitHub API server:$/ do |endpoints_str| + @server = Hub::LocalServer.start_sinatra do + eval endpoints_str, binding + end + # hit our Sinatra server instead of github.com + set_env 'HUB_TEST_HOST', "127.0.0.1:#{@server.port}" +end + Then /^"([^"]*)" should be run$/ do |cmd| assert_command_run cmd end @@ -66,3 +89,8 @@ found = run_silent %(git config --get-all submodule."#{name}".url) found.should eql(url) end + +Then /^there should be no "([^"]*)" remote$/ do |remote_name| + remotes = run_silent('git remote').split("\n") + remotes.should_not include(remote_name) +end diff --git a/features/support/env.rb b/features/support/env.rb index c58574931..311042a52 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -24,10 +24,16 @@ set_env 'HOME', File.expand_path(File.join(current_dir, 'home')) # used in fakebin/git set_env 'HUB_SYSTEM_GIT', system_git + # ensure that api.github.com is actually never hit in tests + set_env 'HUB_TEST_HOST', '127.0.0.1:0' FileUtils.mkdir_p ENV['HOME'] end +After do + @server.stop if defined? @server and @server +end + Before '~@noexec' do set_env 'GIT', nil end diff --git a/features/support/fakebin/git b/features/support/fakebin/git index 8624dcd5d..27172f1cb 100755 --- a/features/support/fakebin/git +++ b/features/support/fakebin/git @@ -8,12 +8,17 @@ command="$1" case "$command" in "clone" | "fetch" | "pull" | "push" ) + # don't actually execute these commands exit ;; * ) - [ "$command $2 $3" == "remote add -f" ] && exit # note: `submodule add` also initiates a clone, but we work around it + if [ "$command $2 $3" == "remote add -f" ]; then + subcommand=$2 + shift 3 + exec "$HUB_SYSTEM_GIT" $command $subcommand "$@" + else + exec "$HUB_SYSTEM_GIT" "$@" + fi ;; esac - -exec "$HUB_SYSTEM_GIT" "$@" diff --git a/features/support/local_server.rb b/features/support/local_server.rb new file mode 100644 index 000000000..0dc9dfd54 --- /dev/null +++ b/features/support/local_server.rb @@ -0,0 +1,96 @@ +# based on +require 'net/http' + +module Hub + class LocalServer + class Identify < Struct.new(:app) + def call(env) + if env["PATH_INFO"] == "/__identify__" + [200, {}, [app.object_id.to_s]] + else + app.call(env) + end + end + end + + def self.ports + @ports ||= {} + end + + def self.run_handler(app, port, &block) + begin + require 'rack/handler/thin' + Thin::Logging.silent = true + Rack::Handler::Thin.run(app, :Port => port, &block) + rescue LoadError + require 'rack/handler/webrick' + Rack::Handler::WEBrick.run(app, :Port => port, :AccessLog => [], :Logger => WEBrick::Log::new(nil, 0), &block) + end + end + + def self.start_sinatra(&block) + require 'sinatra/base' + klass = Class.new(Sinatra::Base) + klass.set :environment, :test + klass.disable :protection + klass.class_eval(&block) + + new(klass.new).start + end + + attr_reader :app, :host, :port + attr_accessor :server + + def initialize(app, host = '127.0.0.1') + @app = app + @host = host + @server = nil + @server_thread = nil + end + + def responsive? + return false if @server_thread && @server_thread.join(0) + + res = Net::HTTP.start(host, port) { |http| http.get('/__identify__') } + + res.is_a?(Net::HTTPSuccess) and res.body == app.object_id.to_s + rescue Errno::ECONNREFUSED, Errno::EBADF + return false + end + + def start + @port = self.class.ports[app.object_id] + + if not @port or not responsive? + @port = find_available_port + self.class.ports[app.object_id] = @port + + @server_thread = Thread.new do + self.class.run_handler(Identify.new(app), @port) { |server| + self.server = server + } + end + + Timeout.timeout(60) { @server_thread.join(0.1) until responsive? } + end + rescue TimeoutError + raise "Rack application timed out during boot" + else + self + end + + def stop + server.respond_to?(:stop!) ? server.stop! : server.stop + @server_thread.join + end + + private + + def find_available_port + server = TCPServer.new('127.0.0.1', 0) + server.addr[1] + ensure + server.close if server + end + end +end diff --git a/lib/hub/github_api.rb b/lib/hub/github_api.rb index 8c9c4ead7..bde502720 100644 --- a/lib/hub/github_api.rb +++ b/lib/hub/github_api.rb @@ -147,11 +147,14 @@ def post_form url, params 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) + # TODO: better naming? + http = configure_connection(req, url) do |host_url| + create_connection host_url + end apply_authentication(req, url) yield req if block_given? @@ -162,6 +165,17 @@ def perform_request url, type raise Context::FatalError, "error with #{type.to_s.upcase} #{url} (#{err.message})" 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) diff --git a/test/hub_test.rb b/test/hub_test.rb index 890be1b84..1f449a852 100644 --- a/test/hub_test.rb +++ b/test/hub_test.rb @@ -437,72 +437,6 @@ def test_create_origin_already_exists assert_equal expected, hub("create") { ENV['GIT'] = 'echo' } end - def test_fork - stub_nonexisting_fork('tpw') - stub_request(:post, "https://api.github.com/repos/defunkt/hub/forks"). - with { |req| req.headers['Content-Length'] == 0 } - - expected = "remote add -f tpw git@github.com:tpw/hub.git\n" - expected << "new remote: tpw\n" - assert_output expected, "fork" - end - - def test_fork_https_protocol - stub_https_is_preferred - stub_nonexisting_fork('tpw') - stub_request(:post, "https://api.github.com/repos/defunkt/hub/forks") - - 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' } - end - - def test_fork_not_in_repo - stub_no_git_repo - expected = "fatal: Not a git repository\n" - assert_output expected, "fork" - end - - def test_fork_enterprise - stub_hub_host('git.my.org') - stub_repo_url('git@git.my.org:defunkt/hub.git') - edit_hub_config do |data| - data['git.my.org'] = [{'user'=>'myfiname', 'oauth_token' => 'FITOKEN'}] - end - - stub_request(:get, "https://git.my.org/repos/myfiname/hub"). - to_return(:status => 404) - stub_request(:post, "https://git.my.org/repos/defunkt/hub/forks"). - with(:headers => {"Authorization" => "token FITOKEN"}) - - 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://api.github.com/repos/defunkt/hub/forks"). - to_return(:status => [500, "Your fork is fail"]) - - expected = "Error creating fork: Your fork is fail (HTTP 500)\n" - assert_equal expected, hub("fork") { ENV['GIT'] = 'echo' } - end - - def test_fork_no_remote - stub_nonexisting_fork('tpw') - stub_request(:post, "https://api.github.com/repos/defunkt/hub/forks") - - assert_equal "", hub("fork --no-remote") { ENV['GIT'] = 'echo' } - end - - def test_fork_already_exists - stub_existing_fork('tpw') - - expected = "Error creating fork: tpw/hub already exists on github.com\n" - assert_equal expected, hub("fork") { ENV['GIT'] = 'echo' } - end - def test_pullrequest expected = "Aborted: head branch is the same as base (\"master\")\n" << "(use `-h ` to specify an explicit pull request head)\n"