diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 3acf4743..34dd49a6 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -5,6 +5,7 @@ on: branches: [master] pull_request: branches: [master] + workflow_dispatch: jobs: continuous_integration_build: @@ -28,6 +29,9 @@ jobs: runs-on: ${{ matrix.operating-system }} + env: + JAVA_OPTS: -Djdk.io.File.enableADS=true + steps: - name: Checkout Code uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a08297c5..9711c891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## v1.12.0 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.12.0 + ## v1.11.0 * 292087e Supress unneeded test output (#570) diff --git a/Gemfile b/Gemfile index 7054c552..2e8f4fe2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ -source 'https://rubygems.org' +# frozen_string_literal: true -gemspec :name => 'git' +source 'https://rubygems.org' +gemspec name: 'git' diff --git a/README.md b/README.md index ab63d2fa..db38fbf6 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ g.index.writable? g.repo g.dir -g.log # returns array of Git::Commit objects +g.log # returns a Git::Log object, which is an Enumerator of Git::Commit objects g.log.since('2 weeks ago') g.log.between('v2.5', 'v2.6') g.log.each {|l| puts l.sha } @@ -204,13 +204,23 @@ g = Git.init { :repository => '/opt/git/proj.git', :index => '/tmp/index'} ) -g = Git.clone(URI, NAME, :path => '/tmp/checkout') +# Clone from a git url +git_url = 'https://github.com/ruby-git/ruby-git.git' +# Clone into the ruby-git directory +g = Git.clone(git_url) + +# Clone into /tmp/clone/ruby-git-clean +name = 'ruby-git-clean' +path = '/tmp/clone' +g = Git.clone(git_url, name, :path => path) +g.dir #=> /tmp/clone/ruby-git-clean + g.config('user.name', 'Scott Chacon') g.config('user.email', 'email@email.com') # Clone can take an optional logger logger = Logger.new -g = Git.clone(URI, NAME, :log => logger) +g = Git.clone(git_url, NAME, :log => logger) g.add # git add -- "." g.add(:all=>true) # git add --all -- "." @@ -234,6 +244,9 @@ g.commit('message', gpg_sign: true) key_id = '0A46826A' g.commit('message', gpg_sign: key_id) +# Skip signing a commit (overriding any global gpgsign setting) +g.commit('message', no_gpg_sign: true) + g = Git.clone(repo, 'myrepo') g.chdir do new_file('test-file', 'blahblahblah') @@ -278,6 +291,7 @@ g.remote(name).merge(branch) g.fetch g.fetch(g.remotes.first) g.fetch('origin', {:ref => 'some/ref/head'} ) +g.fetch(all: true, force: true, depth: 2) g.pull g.pull(Git::Repo, Git::Branch) # fetch and a merge diff --git a/git.gemspec b/git.gemspec index 8d974e28..f53ea98d 100644 --- a/git.gemspec +++ b/git.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=) s.requirements = ['git 1.6.0.0, or greater'] + s.add_runtime_dependency 'addressable', '~> 2.8' s.add_runtime_dependency 'rchardet', '~> 1.8' s.add_development_dependency 'bump', '~> 0.10' @@ -35,7 +36,7 @@ Gem::Specification.new do |s| unless RUBY_PLATFORM == 'java' s.add_development_dependency 'redcarpet', '~> 3.5' - s.add_development_dependency 'yard', '~> 0.9' + s.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28' s.add_development_dependency 'yardstick', '~> 0.9' end diff --git a/lib/git.rb b/lib/git.rb index 4ad1bd97..1da03ce5 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -21,6 +21,7 @@ require 'git/status' require 'git/stash' require 'git/stashes' +require 'git/url' require 'git/version' require 'git/working_directory' require 'git/worktree' @@ -106,11 +107,23 @@ def self.bare(git_dir, options = {}) # @see https://git-scm.com/docs/git-clone git clone # @see https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a GIT URLs # - # @param [URI, Pathname] repository The (possibly remote) repository to clone + # @param repository_url [URI, Pathname] The (possibly remote) repository url to clone # from. See [GIT URLS](https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a) # for more information. # - # @param [Pathname] name The directory to clone into. + # @param directory [Pathname, nil] The directory to clone into + # + # If `directory` is a relative directory it is relative to the `path` option if + # given. If `path` is not given, `directory` is relative to the current working + # directory. + # + # If `nil`, `directory` will be set to the basename of the last component of + # the path from the `repository_url`. For example, for the URL: + # `https://github.com/org/repo.git`, `directory` will be set to `repo`. + # + # If the last component of the path is `.git`, the next-to-last component of + # the path is used. For example, for the URL `/Users/me/foo/.git`, `directory` + # will be set to `foo`. # # @param [Hash] options The options for this command (see list of valid # options below) @@ -157,8 +170,10 @@ def self.bare(git_dir, options = {}) # @return [Git::Base] an object that can execute git commands in the context # of the cloned local working copy or cloned repository. # - def self.clone(repository, name, options = {}) - Base.clone(repository, name, options) + def self.clone(repository_url, directory = nil, options = {}) + clone_to_options = options.select { |key, _value| %i[bare mirror].include?(key) } + directory ||= Git::URL.clone_to(repository_url, **clone_to_options) + Base.clone(repository_url, directory, options) end # Export the current HEAD (or a branch, if options[:branch] diff --git a/lib/git/base.rb b/lib/git/base.rb index 815fc36a..2d931cf3 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -17,10 +17,10 @@ def self.bare(git_dir, options = {}) end # (see Git.clone) - def self.clone(repository, name, options = {}) - new_options = Git::Lib.new(nil, options[:log]).clone(repository, name, options) + def self.clone(repository_url, directory, options = {}) + new_options = Git::Lib.new(nil, options[:log]).clone(repository_url, directory, options) normalize_paths(new_options, bare: options[:bare] || options[:mirror]) - self.new(new_options) + new(new_options) end # Returns (and initialize if needed) a Git::Config instance @@ -336,7 +336,11 @@ def checkout_file(version, file) # fetches changes from a remote branch - this does not modify the working directory, # it just gets the changes from the remote if there are any - def fetch(remote = 'origin', opts={}) + def fetch(remote = 'origin', opts = {}) + if remote.is_a?(Hash) + opts = remote + remote = nil + end self.lib.fetch(remote, opts) end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 0fdae6f8..fce8b274 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -95,9 +95,9 @@ def init(opts={}) # # @return [Hash] the options to pass to {Git::Base.new} # - def clone(repository, name, opts = {}) + def clone(repository_url, directory, opts = {}) @path = opts[:path] || '.' - clone_dir = opts[:path] ? File.join(@path, name) : name + clone_dir = opts[:path] ? File.join(@path, directory) : directory arr_opts = [] arr_opts << '--bare' if opts[:bare] @@ -106,11 +106,11 @@ def clone(repository, name, opts = {}) arr_opts << '--config' << opts[:config] if opts[:config] arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin] arr_opts << '--recursive' if opts[:recursive] - arr_opts << "--mirror" if opts[:mirror] + arr_opts << '--mirror' if opts[:mirror] arr_opts << '--' - arr_opts << repository + arr_opts << repository_url arr_opts << clone_dir command('clone', arr_opts) @@ -647,7 +647,8 @@ def remove(path = '.', opts = {}) # :date # :no_verify # :allow_empty_message - # :gpg_sign + # :gpg_sign (accepts true or a gpg key ID as a String) + # :no_gpg_sign (conflicts with :gpg_sign) # # @param [String] message the commit message to be used # @param [Hash] opts the commit options to be used @@ -661,13 +662,18 @@ def commit(message, opts = {}) arr_opts << "--date=#{opts[:date]}" if opts[:date].is_a? String arr_opts << '--no-verify' if opts[:no_verify] arr_opts << '--allow-empty-message' if opts[:allow_empty_message] - if opts[:gpg_sign] + + if opts[:gpg_sign] && opts[:no_gpg_sign] + raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign' + elsif opts[:gpg_sign] arr_opts << if opts[:gpg_sign] == true '--gpg-sign' else "--gpg-sign=#{opts[:gpg_sign]}" end + elsif opts[:no_gpg_sign] + arr_opts << '--no-gpg-sign' end command('commit', arr_opts) @@ -877,14 +883,15 @@ def tag(name, *opts) def fetch(remote, opts) arr_opts = [] + arr_opts << '--all' if opts[:all] arr_opts << '--tags' if opts[:t] || opts[:tags] arr_opts << '--prune' if opts[:p] || opts[:prune] arr_opts << '--prune-tags' if opts[:P] || opts[:'prune-tags'] arr_opts << '--force' if opts[:f] || opts[:force] arr_opts << '--unshallow' if opts[:unshallow] arr_opts << '--depth' << opts[:depth] if opts[:depth] - arr_opts << '--' - arr_opts << remote + arr_opts << '--' if remote || opts[:ref] + arr_opts << remote if remote arr_opts << opts[:ref] if opts[:ref] command('fetch', arr_opts) diff --git a/lib/git/url.rb b/lib/git/url.rb new file mode 100644 index 00000000..af170615 --- /dev/null +++ b/lib/git/url.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'addressable/uri' + +module Git + # Methods for parsing a Git URL + # + # Any URL that can be passed to `git clone` can be parsed by this class. + # + # @see https://git-scm.com/docs/git-clone#_git_urls GIT URLs + # @see https://github.com/sporkmonger/addressable Addresable::URI + # + # @api public + # + class URL + # Regexp used to match a Git URL with an alternative SSH syntax + # such as `user@host:path` + # + GIT_ALTERNATIVE_SSH_SYNTAX = %r{ + ^ + (?:(?[^@/]+)@)? # user or nil + (?[^:/]+) # host is required + :(?!/) # : serparator is required, but must not be followed by / + (?.*?) # path is required + $ + }x.freeze + + # Parse a Git URL and return an Addressable::URI object + # + # The URI returned can be converted back to a string with 'to_s'. This is + # guaranteed to return the same URL string that was parsed. + # + # @example + # uri = Git::URL.parse('https://github.com/ruby-git/ruby-git.git') + # #=> # + # uri.scheme #=> "https" + # uri.host #=> "github.com" + # uri.path #=> "/ruby-git/ruby-git.git" + # + # Git::URL.parse('/Users/James/projects/ruby-git') + # #=> # + # + # @param url [String] the Git URL to parse + # + # @return [Addressable::URI] the parsed URI + # + def self.parse(url) + if !url.start_with?('file:') && (m = GIT_ALTERNATIVE_SSH_SYNTAX.match(url)) + GitAltURI.new(user: m[:user], host: m[:host], path: m[:path]) + else + Addressable::URI.parse(url) + end + end + + # The directory `git clone` would use for the repository directory for the given URL + # + # @example + # Git::URL.clone_to('https://github.com/ruby-git/ruby-git.git') #=> 'ruby-git' + # + # @param url [String] the Git URL containing the repository directory + # + # @return [String] the name of the repository directory + # + def self.clone_to(url, bare: false, mirror: false) + uri = parse(url) + path_parts = uri.path.split('/') + path_parts.pop if path_parts.last == '.git' + directory = path_parts.last + if bare || mirror + directory += '.git' unless directory.end_with?('.git') + elsif directory.end_with?('.git') + directory = directory[0..-5] + end + directory + end + end + + # The URI for git's alternative scp-like syntax + # + # This class is necessary to ensure that #to_s returns the same string + # that was passed to the initializer. + # + # @api public + # + class GitAltURI < Addressable::URI + # Create a new GitAltURI object + # + # @example + # uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git') + # uri.to_s #=> 'james@github.com/james/ruby-git' + # + # @param user [String, nil] the user from the URL or nil + # @param host [String] the host from the URL + # @param path [String] the path from the URL + # + def initialize(user:, host:, path:) + super(scheme: 'git-alt', user: user, host: host, path: path) + end + + # Convert the URI to a String + # + # Addressible::URI forces path to be absolute by prepending a '/' to the + # path. This method removes the '/' when converting back to a string + # since that is what is expected by git. The following is a valid git URL: + # + # `james@github.com:ruby-git/ruby-git.git` + # + # and the following (with the initial '/'' in the path) is NOT a valid git URL: + # + # `james@github.com:/ruby-git/ruby-git.git` + # + # @example + # uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'james/ruby-git') + # uri.path #=> '/james/ruby-git' + # uri.to_s #=> 'james@github.com:james/ruby-git' + # + # @return [String] the URI as a String + # + def to_s + if user + "#{user}@#{host}:#{path[1..-1]}" + else + "#{host}:#{path[1..-1]}" + end + end + end +end diff --git a/lib/git/version.rb b/lib/git/version.rb index 87bffb51..52159024 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -1,5 +1,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='1.11.0' + VERSION='1.12.0' end diff --git a/tests/test_helper.rb b/tests/test_helper.rb index b04f3f4d..31ed8477 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -97,4 +97,67 @@ def with_custom_env_variables(&block) Git::Lib::ENV_VARIABLE_NAMES.each { |k| ENV[k] = saved_env[k] } end end + + # Assert that the expected command line args are generated for a given Git::Lib method + # + # This assertion generates an empty git repository and then runs calls + # Git::Base method named by `git_cmd` passing that method `git_cmd_args`. + # + # Before calling `git_cmd`, this method stubs the `Git::Lib#command` method to + # capture the args sent to it by `git_cmd`. These args are captured into + # `actual_command_line`. + # + # assert_equal is called comparing the given `expected_command_line` to + # `actual_command_line`. + # + # @example Fetch with no args + # expected_command_line = ['fetch', '--', 'origin'] + # git_cmd = :fetch + # git_cmd_args = [] + # assert_command_line(expected_command_line, git_cmd, git_cmd_args) + # + # @example Fetch with some args + # expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master'] + # git_cmd = :fetch + # git_cmd_args = ['origin', ref: 'master', depth: '2'] + # assert_command_line(expected_command_line, git_cmd, git_cmd_args) + # + # @example Fetch all + # expected_command_line = ['fetch', '--all'] + # git_cmd = :fetch + # git_cmd_args = [all: true] + # assert_command_line(expected_command_line, git_cmd, git_cmd_args) + # + # @param expected_command_line [Array] The expected arguments to be sent to Git::Lib#command + # @param git_cmd [Symbol] the method to be called on the Git::Base object + # @param git_cmd_args [Array] The arguments to be sent to the git_cmd method + # + # @yield [git] An initialization block + # The initialization block is called after a test project is created with Git.init. + # The current working directory is set to the root of the test project's working tree. + # @yieldparam git [Git::Base] The Git::Base object resulting from initializing the test project + # @yieldreturn [void] the return value of the block is ignored + # + # @return [void] + # + def assert_command_line(expected_command_line, git_cmd, git_cmd_args) + actual_command_line = nil + + in_temp_dir do |path| + git = Git.init('test_project') + + Dir.chdir 'test_project' do + yield(git) if block_given? + + # Mock the Git::Lib#command method to capture the actual command line args + git.lib.define_singleton_method(:command) do |cmd, *opts, &block| + actual_command_line = [cmd, *opts.flatten] + end + + git.send(git_cmd, *git_cmd_args) + end + end + + assert_equal(expected_command_line, actual_command_line) + end end diff --git a/tests/units/test_commit_with_gpg.rb b/tests/units/test_commit_with_gpg.rb index 97fb4de9..5663def3 100644 --- a/tests/units/test_commit_with_gpg.rb +++ b/tests/units/test_commit_with_gpg.rb @@ -34,4 +34,29 @@ def test_with_specific_gpg_keyid assert_match(/commit.*--gpg-sign=keykeykey['"]/, actual_cmd) end end + + def test_disabling_gpg_sign + Dir.mktmpdir do |dir| + git = Git.init(dir) + actual_cmd = nil + git.lib.define_singleton_method(:run_command) do |git_cmd, &block| + actual_cmd = git_cmd + `true` + end + message = 'My commit message' + git.commit(message, no_gpg_sign: true) + assert_match(/commit.*--no-gpg-sign['"]/, actual_cmd) + end + end + + def test_conflicting_gpg_sign_options + Dir.mktmpdir do |dir| + git = Git.init(dir) + message = 'My commit message' + + assert_raises ArgumentError do + git.commit(message, gpg_sign: true, no_gpg_sign: true) + end + end + end end diff --git a/tests/units/test_git_alt_uri.rb b/tests/units/test_git_alt_uri.rb new file mode 100644 index 00000000..b01ea1bb --- /dev/null +++ b/tests/units/test_git_alt_uri.rb @@ -0,0 +1,27 @@ +require 'test/unit' + +# Tests for the Git::GitAltURI class +# +class TestGitAltURI < Test::Unit::TestCase + def test_new + uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'ruby-git/ruby-git.git') + actual_attributes = uri.to_hash.delete_if { |_key, value| value.nil? } + expected_attributes = { + scheme: 'git-alt', + user: 'james', + host: 'github.com', + path: '/ruby-git/ruby-git.git' + } + assert_equal(expected_attributes, actual_attributes) + end + + def test_to_s + uri = Git::GitAltURI.new(user: 'james', host: 'github.com', path: 'ruby-git/ruby-git.git') + assert_equal('james@github.com:ruby-git/ruby-git.git', uri.to_s) + end + + def test_to_s_with_nil_user + uri = Git::GitAltURI.new(user: nil, host: 'github.com', path: 'ruby-git/ruby-git.git') + assert_equal('github.com:ruby-git/ruby-git.git', uri.to_s) + end +end diff --git a/tests/units/test_git_clone.rb b/tests/units/test_git_clone.rb new file mode 100644 index 00000000..8a5d1806 --- /dev/null +++ b/tests/units/test_git_clone.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'test/unit' +require_relative '../test_helper' + +# Tests for Git.clone +class TestGitClone < Test::Unit::TestCase + def setup_repo + Git.init('repository.git', bare: true) + git = Git.clone('repository.git', 'temp') + File.write('temp/test.txt', 'test') + git.add('test.txt') + git.commit('Initial commit') + end + + def test_git_clone_with_name + in_temp_dir do |path| + setup_repo + clone_dir = 'clone_to_this_dir' + git = Git.clone('repository.git', clone_dir) + assert(Dir.exist?(clone_dir)) + expected_dir = File.realpath(clone_dir) + assert_equal(expected_dir, git.dir.to_s) + end + end + + def test_git_clone_with_no_name + in_temp_dir do |path| + setup_repo + git = Git.clone('repository.git') + assert(Dir.exist?('repository')) + expected_dir = File.realpath('repository') + assert_equal(expected_dir, git.dir.to_s) + end + end +end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index bdf50a75..71acd21e 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -1,6 +1,7 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../test_helper' +require "fileutils" # tests all the low level git communication # @@ -51,8 +52,13 @@ def test_commit_with_no_verify move_file(pre_commit_path, pre_commit_path_bak) # Adds a pre-commit file that should throw an error - create_file(pre_commit_path, 'echo Pre-commit file. Shoud not execute; exit 1') # Error when executed - File.chmod(0111, pre_commit_path) + create_file(pre_commit_path, <<~PRE_COMMIT_SCRIPT) + #!/bin/sh + echo "pre-commit script exits with an error" + exit 1 + PRE_COMMIT_SCRIPT + + FileUtils.chmod("+x", pre_commit_path) create_file("#{@wdir}/test_file_2", 'content test_file_2') @lib.add('test_file_2') diff --git a/tests/units/test_remotes.rb b/tests/units/test_remotes.rb index cc547f8b..ab8f6f85 100644 --- a/tests/units/test_remotes.rb +++ b/tests/units/test_remotes.rb @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -require File.dirname(__FILE__) + '/../test_helper' +require_relative '../test_helper' class TestRemotes < Test::Unit::TestCase def setup @@ -123,6 +123,34 @@ def test_fetch end end + def test_fetch_cmd_with_no_args + expected_command_line = ['fetch', '--', 'origin'] + git_cmd = :fetch + git_cmd_args = [] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + def test_fetch_cmd_with_origin_and_branch + expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master'] + git_cmd = :fetch + git_cmd_args = ['origin', ref: 'master', depth: '2'] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + def test_fetch_cmd_with_all + expected_command_line = ['fetch', '--all'] + git_cmd = :fetch + git_cmd_args = [all: true] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + + def test_fetch_cmd_with_all_with_other_args + expected_command_line = ['fetch', '--all', '--force', '--depth', '2'] + git_cmd = :fetch + git_cmd_args = [all: true, force: true, depth: '2'] + assert_command_line(expected_command_line, git_cmd, git_cmd_args) + end + def test_fetch_command_injection test_file = 'VULNERABILITY_EXISTS' vulnerability_exists = false @@ -208,6 +236,4 @@ def test_push assert(rem.tag('test-tag')) end end - - end diff --git a/tests/units/test_url_clone_to.rb b/tests/units/test_url_clone_to.rb new file mode 100644 index 00000000..6f5c9e82 --- /dev/null +++ b/tests/units/test_url_clone_to.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'test/unit' +require File.join(File.dirname(__dir__), 'test_helper') + +# Tests Git::URL.clone_to +# +class TestURLCloneTo < Test::Unit::TestCase + def test_clone_to_full_repo + GIT_URLS.each do |url_data| + url = url_data[:url] + expected_path = url_data[:expected_path] + actual_path = Git::URL.clone_to(url) + assert_equal( + expected_path, actual_path, + "Failed to determine the clone path for URL '#{url}' correctly" + ) + end + end + + def test_clone_to_bare_repo + GIT_URLS.each do |url_data| + url = url_data[:url] + expected_path = url_data[:expected_bare_path] + actual_path = Git::URL.clone_to(url, bare: true) + assert_equal( + expected_path, actual_path, + "Failed to determine the clone path for URL '#{url}' correctly" + ) + end + end + + def test_clone_to_mirror_repo + GIT_URLS.each do |url_data| + url = url_data[:url] + # The expected_path is the same for bare and mirror repos + expected_path = url_data[:expected_bare_path] + actual_path = Git::URL.clone_to(url, mirror: true) + assert_equal( + expected_path, actual_path, + "Failed to determine the clone path for URL '#{url}' correctly" + ) + end + end + + GIT_URLS = [ + { + url: 'https://github.com/org/repo', + expected_path: 'repo', + expected_bare_path: 'repo.git' + }, + { + url: 'https://github.com/org/repo.git', + expected_path: 'repo', + expected_bare_path: 'repo.git' + }, + { + url: 'https://git.mydomain.com/org/repo/.git', + expected_path: 'repo', + expected_bare_path: 'repo.git' + } + ].freeze + + # Git::URL.clone_to makes some assumptions about how the `git` command names + # the directory to clone to. This test ensures that the assumptions are + # correct. + # + def test_git_clone_naming_assumptions + in_temp_dir do |_path| + setup_test_repositories + + GIT_CLONE_COMMANDS.each do |command_data| + command = command_data[:command] + expected_path = command_data[:expected_path] + + output = `#{command} 2>&1` + + assert_match(/Cloning into (?:bare repository )?'#{expected_path}'/, output) + FileUtils.rm_rf(expected_path) + end + end + end + + GIT_CLONE_COMMANDS = [ + # Clone to full repository + { command: 'git clone server/my_project', expected_path: 'my_project' }, + { command: 'git clone server/my_project/.git', expected_path: 'my_project' }, + { command: 'git clone server/my_project.git', expected_path: 'my_project' }, + + # Clone to bare repository + { command: 'git clone --bare server/my_project', expected_path: 'my_project.git' }, + { command: 'git clone --bare server/my_project/.git', expected_path: 'my_project.git' }, + { command: 'git clone --bare server/my_project.git', expected_path: 'my_project.git' }, + + # Clone to mirror repository + { command: 'git clone --mirror server/my_project', expected_path: 'my_project.git' }, + { command: 'git clone --mirror server/my_project/.git', expected_path: 'my_project.git' }, + { command: 'git clone --mirror server/my_project.git', expected_path: 'my_project.git' } + ].freeze + + def setup_test_repositories + # Create a repository to clone from + Dir.mkdir 'server' + remote = Git.init('server/my_project') + Dir.chdir('server/my_project') do + new_file('README.md', '# My New Project') + remote.add + remote.commit('Initial version') + end + + # Create a bare repository to clone from + Git.clone('server/my_project', 'server/my_project.git', bare: true) + end +end diff --git a/tests/units/test_url_parse.rb b/tests/units/test_url_parse.rb new file mode 100644 index 00000000..2ca97333 --- /dev/null +++ b/tests/units/test_url_parse.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'test/unit' + +# Tests Git::URL.parse +# +class TestURLParse < Test::Unit::TestCase + def test_parse_with_invalid_url + url = 'user@host.xz:/path/to/repo.git/' + assert_raise(Addressable::URI::InvalidURIError) do + Git::URL.parse(url) + end + end + + def test_parse + GIT_URLS.each do |url_data| + url = url_data[:url] + expected_uri = url_data[:expected_uri] + actual_uri = Git::URL.parse(url).to_hash.delete_if { |_key, value| value.nil? } + assert_equal(expected_uri, actual_uri, "Failed to parse URL '#{url}' correctly") + end + end + + # For any URL, #to_s should return the url passed to Git::URL.parse(url) + def test_to_s + GIT_URLS.each do |url_data| + url = url_data[:url] + to_s = Git::URL.parse(url).to_s + assert_equal(url, to_s, "Parsed URI#to_s does not return the original URL '#{url}' correctly") + end + end + + GIT_URLS = [ + { + url: 'ssh://host.xz/path/to/repo.git/', + expected_uri: { scheme: 'ssh', host: 'host.xz', path: '/path/to/repo.git/' } + }, + { + url: 'ssh://host.xz:4443/path/to/repo.git/', + expected_uri: { scheme: 'ssh', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' } + }, + { + url: 'ssh:///path/to/repo.git/', + expected_uri: { scheme: 'ssh', host: '', path: '/path/to/repo.git/' } + }, + { + url: 'user@host.xz:path/to/repo.git/', + expected_uri: { scheme: 'git-alt', user: 'user', host: 'host.xz', path: '/path/to/repo.git/' } + }, + { + url: 'host.xz:path/to/repo.git/', + expected_uri: { scheme: 'git-alt', host: 'host.xz', path: '/path/to/repo.git/' } + }, + { + url: 'git://host.xz:4443/path/to/repo.git/', + expected_uri: { scheme: 'git', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' } + }, + { + url: 'git://user@host.xz:4443/path/to/repo.git/', + expected_uri: { scheme: 'git', user: 'user', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' } + }, + { + url: 'https://host.xz/path/to/repo.git/', + expected_uri: { scheme: 'https', host: 'host.xz', path: '/path/to/repo.git/' } + }, + { + url: 'https://host.xz:4443/path/to/repo.git/', + expected_uri: { scheme: 'https', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' } + }, + { + url: 'ftps://host.xz:4443/path/to/repo.git/', + expected_uri: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' } + }, + { + url: 'ftps://host.xz:4443/path/to/repo.git/', + expected_uri: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' } + }, + { + url: 'file:./relative-path/to/repo.git/', + expected_uri: { scheme: 'file', path: './relative-path/to/repo.git/' } + }, + { + url: 'file:///path/to/repo.git/', + expected_uri: { scheme: 'file', host: '', path: '/path/to/repo.git/' } + }, + { + url: 'file:///path/to/repo.git', + expected_uri: { scheme: 'file', host: '', path: '/path/to/repo.git' } + }, + { + url: 'file://host.xz/path/to/repo.git', + expected_uri: { scheme: 'file', host: 'host.xz', path: '/path/to/repo.git' } + }, + { url: '/path/to/repo.git/', expected_uri: { path: '/path/to/repo.git/' } }, + { url: '/path/to/bare-repo/.git', expected_uri: { path: '/path/to/bare-repo/.git' } }, + { url: 'relative-path/to/repo.git/', expected_uri: { path: 'relative-path/to/repo.git/' } }, + { url: './relative-path/to/repo.git/', expected_uri: { path: './relative-path/to/repo.git/' } }, + { url: '../ruby-git/.git', expected_uri: { path: '../ruby-git/.git' } } + ].freeze +end pFad - Phonifier reborn Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved. Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages. Alternative Proxies:Alternative ProxypFad ProxypFad v3 ProxypFad v4 Proxy
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy