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