From 0a43d8b848b2454f822315a3239e8884b0293c30 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 17 Apr 2022 17:28:55 -0700 Subject: [PATCH 01/12] Use the head version of yard (#573) Signed-off-by: James Couball --- Gemfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7054c552..b2afa573 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,9 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -gemspec :name => 'git' +git 'https://github.com/lsegal/yard', branch: 'main' do + gem 'yard' +end +gemspec name: 'git' From 13471d729156279ce0a90fedc445b01890fe3d39 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 21 Apr 2022 17:21:29 -0700 Subject: [PATCH 02/12] Add Git::URL #parse and #clone_to methods (#575) Signed-off-by: James Couball --- git.gemspec | 1 + lib/git.rb | 1 + lib/git/url.rb | 122 +++++++++++++++++++++++++++ tests/units/test_git_alt_uri.rb | 27 ++++++ tests/units/test_url.rb | 144 ++++++++++++++++++++++++++++++++ 5 files changed, 295 insertions(+) create mode 100644 lib/git/url.rb create mode 100644 tests/units/test_git_alt_uri.rb create mode 100644 tests/units/test_url.rb diff --git a/git.gemspec b/git.gemspec index 8d974e28..53298c5a 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' diff --git a/lib/git.rb b/lib/git.rb index 4ad1bd97..addb0d59 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' diff --git a/lib/git/url.rb b/lib/git/url.rb new file mode 100644 index 00000000..19fff385 --- /dev/null +++ b/lib/git/url.rb @@ -0,0 +1,122 @@ +# 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 name `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) + uri = parse(url) + path_parts = uri.path.split('/') + path_parts.pop if path_parts.last == '.git' + + path_parts.last.sub(/\.git$/, '') + 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/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_url.rb b/tests/units/test_url.rb new file mode 100644 index 00000000..6eee2a8b --- /dev/null +++ b/tests/units/test_url.rb @@ -0,0 +1,144 @@ +require 'test/unit' + +GIT_URLS = [ + { + url: 'ssh://host.xz/path/to/repo.git/', + expected_attributes: { scheme: 'ssh', host: 'host.xz', path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'ssh://host.xz:4443/path/to/repo.git/', + expected_attributes: { scheme: 'ssh', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'ssh:///path/to/repo.git/', + expected_attributes: { scheme: 'ssh', host: '', path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'user@host.xz:path/to/repo.git/', + expected_attributes: { scheme: 'git-alt', user: 'user', host: 'host.xz', path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'host.xz:path/to/repo.git/', + expected_attributes: { scheme: 'git-alt', host: 'host.xz', path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'git://host.xz:4443/path/to/repo.git/', + expected_attributes: { scheme: 'git', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'git://user@host.xz:4443/path/to/repo.git/', + expected_attributes: { scheme: 'git', user: 'user', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'https://host.xz/path/to/repo.git/', + expected_attributes: { scheme: 'https', host: 'host.xz', path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'https://host.xz:4443/path/to/repo.git/', + expected_attributes: { scheme: 'https', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'ftps://host.xz:4443/path/to/repo.git/', + expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'ftps://host.xz:4443/path/to/repo.git/', + expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'file:./relative-path/to/repo.git/', + expected_attributes: { scheme: 'file', path: './relative-path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'file:///path/to/repo.git/', + expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: 'file:///path/to/repo.git', + expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git' }, + expected_clone_to: 'repo' + }, + { + url: 'file://host.xz/path/to/repo.git', + expected_attributes: { scheme: 'file', host: 'host.xz', path: '/path/to/repo.git' }, + expected_clone_to: 'repo' + }, + { + url: '/path/to/repo.git/', + expected_attributes: { path: '/path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: '/path/to/bare-repo/.git', + expected_attributes: { path: '/path/to/bare-repo/.git' }, + expected_clone_to: 'bare-repo' + }, + { + url: 'relative-path/to/repo.git/', + expected_attributes: { path: 'relative-path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: './relative-path/to/repo.git/', + expected_attributes: { path: './relative-path/to/repo.git/' }, + expected_clone_to: 'repo' + }, + { + url: '../ruby-git/.git', + expected_attributes: { path: '../ruby-git/.git' }, + expected_clone_to: 'ruby-git' + } +].freeze + +# Tests for the Git::URL class +# +class TestURL < 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_attributes = url_data[:expected_attributes] + actual_attributes = Git::URL.parse(url).to_hash.delete_if {| key, value | value.nil? } + assert_equal(expected_attributes, actual_attributes, "Failed to parse URL '#{url}' correctly") + end + end + + def test_clone_to + GIT_URLS.each do |url_data| + url = url_data[:url] + expected_clone_to = url_data[:expected_clone_to] + actual_repo_name = Git::URL.clone_to(url) + assert_equal( + expected_clone_to, actual_repo_name, + "Failed to determine the repository directory for URL '#{url}' correctly" + ) + end + end + + 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 +end From b92130ca462d2d149b68d61f16d2053382ce3d4e Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 25 Apr 2022 11:56:03 -0700 Subject: [PATCH 03/12] Make Git::URL.clone_to handle cloning to bare and mirror repos (#577) Signed-off-by: James Couball --- lib/git/url.rb | 13 ++- tests/units/test_url.rb | 144 ------------------------------- tests/units/test_url_clone_to.rb | 114 ++++++++++++++++++++++++ tests/units/test_url_parse.rb | 100 +++++++++++++++++++++ 4 files changed, 223 insertions(+), 148 deletions(-) delete mode 100644 tests/units/test_url.rb create mode 100644 tests/units/test_url_clone_to.rb create mode 100644 tests/units/test_url_parse.rb diff --git a/lib/git/url.rb b/lib/git/url.rb index 19fff385..af170615 100644 --- a/lib/git/url.rb +++ b/lib/git/url.rb @@ -52,7 +52,7 @@ def self.parse(url) end end - # The name `git clone` would use for the repository directory for the given URL + # 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' @@ -61,12 +61,17 @@ def self.parse(url) # # @return [String] the name of the repository directory # - def self.clone_to(url) + 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' - - path_parts.last.sub(/\.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 diff --git a/tests/units/test_url.rb b/tests/units/test_url.rb deleted file mode 100644 index 6eee2a8b..00000000 --- a/tests/units/test_url.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'test/unit' - -GIT_URLS = [ - { - url: 'ssh://host.xz/path/to/repo.git/', - expected_attributes: { scheme: 'ssh', host: 'host.xz', path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'ssh://host.xz:4443/path/to/repo.git/', - expected_attributes: { scheme: 'ssh', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'ssh:///path/to/repo.git/', - expected_attributes: { scheme: 'ssh', host: '', path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'user@host.xz:path/to/repo.git/', - expected_attributes: { scheme: 'git-alt', user: 'user', host: 'host.xz', path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'host.xz:path/to/repo.git/', - expected_attributes: { scheme: 'git-alt', host: 'host.xz', path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'git://host.xz:4443/path/to/repo.git/', - expected_attributes: { scheme: 'git', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'git://user@host.xz:4443/path/to/repo.git/', - expected_attributes: { scheme: 'git', user: 'user', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'https://host.xz/path/to/repo.git/', - expected_attributes: { scheme: 'https', host: 'host.xz', path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'https://host.xz:4443/path/to/repo.git/', - expected_attributes: { scheme: 'https', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'ftps://host.xz:4443/path/to/repo.git/', - expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'ftps://host.xz:4443/path/to/repo.git/', - expected_attributes: { scheme: 'ftps', host: 'host.xz', port: 4443, path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'file:./relative-path/to/repo.git/', - expected_attributes: { scheme: 'file', path: './relative-path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'file:///path/to/repo.git/', - expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: 'file:///path/to/repo.git', - expected_attributes: { scheme: 'file', host: '', path: '/path/to/repo.git' }, - expected_clone_to: 'repo' - }, - { - url: 'file://host.xz/path/to/repo.git', - expected_attributes: { scheme: 'file', host: 'host.xz', path: '/path/to/repo.git' }, - expected_clone_to: 'repo' - }, - { - url: '/path/to/repo.git/', - expected_attributes: { path: '/path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: '/path/to/bare-repo/.git', - expected_attributes: { path: '/path/to/bare-repo/.git' }, - expected_clone_to: 'bare-repo' - }, - { - url: 'relative-path/to/repo.git/', - expected_attributes: { path: 'relative-path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: './relative-path/to/repo.git/', - expected_attributes: { path: './relative-path/to/repo.git/' }, - expected_clone_to: 'repo' - }, - { - url: '../ruby-git/.git', - expected_attributes: { path: '../ruby-git/.git' }, - expected_clone_to: 'ruby-git' - } -].freeze - -# Tests for the Git::URL class -# -class TestURL < 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_attributes = url_data[:expected_attributes] - actual_attributes = Git::URL.parse(url).to_hash.delete_if {| key, value | value.nil? } - assert_equal(expected_attributes, actual_attributes, "Failed to parse URL '#{url}' correctly") - end - end - - def test_clone_to - GIT_URLS.each do |url_data| - url = url_data[:url] - expected_clone_to = url_data[:expected_clone_to] - actual_repo_name = Git::URL.clone_to(url) - assert_equal( - expected_clone_to, actual_repo_name, - "Failed to determine the repository directory for URL '#{url}' correctly" - ) - end - end - - 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 -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 From 45b467cd3fd5ec3facddc0c81e237cb443b0f74d Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 25 Apr 2022 18:17:15 -0700 Subject: [PATCH 04/12] Make the directory param to Git.clone optional (#578) Signed-off-by: James Couball --- README.md | 14 ++++++++++++-- lib/git.rb | 22 +++++++++++++++++---- lib/git/base.rb | 6 +++--- lib/git/lib.rb | 8 ++++---- tests/units/test_git_clone.rb | 36 +++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 13 deletions(-) create mode 100644 tests/units/test_git_clone.rb diff --git a/README.md b/README.md index ab63d2fa..bb0b81d6 100644 --- a/README.md +++ b/README.md @@ -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 -- "." diff --git a/lib/git.rb b/lib/git.rb index addb0d59..1da03ce5 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -107,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) @@ -158,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..541cc554 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 diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 0fdae6f8..5bf2e455 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) 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 From 5f0adecb6b7260870f3f28904dcd08bc1b6d6169 Mon Sep 17 00:00:00 2001 From: Mike Slinn Date: Tue, 17 May 2022 12:19:20 -0400 Subject: [PATCH 05/12] Update README.md (#580) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb0b81d6..d015e3cc 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 } From 1b13ec1f2065a6b8f6dd514111404a7d6a5a0deb Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 17 May 2022 10:40:10 -0700 Subject: [PATCH 06/12] Workaround to get JRuby build working (#582) Signed-off-by: James Couball --- .github/workflows/continuous_integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 3acf4743..c6599412 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -28,6 +28,9 @@ jobs: runs-on: ${{ matrix.operating-system }} + env: + JAVA_OPTS: -Djdk.io.File.enableADS=true + steps: - name: Checkout Code uses: actions/checkout@v2 From 6f2b3fdba6831d468e02c21dea54164bca0400b2 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 21 May 2022 09:52:01 -0700 Subject: [PATCH 07/12] Support the --all option for git fetch (#583) Signed-off-by: James Couball --- README.md | 1 + lib/git/base.rb | 6 +++- lib/git/lib.rb | 5 +-- tests/test_helper.rb | 63 +++++++++++++++++++++++++++++++++++++ tests/units/test_remotes.rb | 32 +++++++++++++++++-- 5 files changed, 101 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d015e3cc..d4b68c55 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,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/lib/git/base.rb b/lib/git/base.rb index 541cc554..2d931cf3 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -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 5bf2e455..cb408246 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -877,14 +877,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/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_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 From 4a96679d43cfdcf7b6af247486eb7aed28981d32 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 17 Aug 2022 12:19:58 -0700 Subject: [PATCH 08/12] Fix windows build (#591) Signed-off-by: James Couball --- tests/units/test_lib.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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') From 609ab8be2656c2362f126863dc2c98255fb5f68a Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 17 Aug 2022 12:30:21 -0700 Subject: [PATCH 09/12] Allow the CI build to be run manually using the GitHub interface (#590) Signed-off-by: James Couball --- .github/workflows/continuous_integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index c6599412..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: From 323383be03358c96523f60e361ad8ec21857f52e Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 17 Aug 2022 17:45:55 -0700 Subject: [PATCH 10/12] Use yard gem version 0.9.8 or later instead of HEAD from GitHub (#592) Signed-off-by: James Couball --- Gemfile | 4 ---- git.gemspec | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index b2afa573..2e8f4fe2 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,4 @@ source 'https://rubygems.org' -git 'https://github.com/lsegal/yard', branch: 'main' do - gem 'yard' -end - gemspec name: 'git' diff --git a/git.gemspec b/git.gemspec index 53298c5a..f53ea98d 100644 --- a/git.gemspec +++ b/git.gemspec @@ -36,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 From e58cd2997670561e41df22db236e06e41daea3da Mon Sep 17 00:00:00 2001 From: Bradley Buda Date: Thu, 18 Aug 2022 10:25:17 -0700 Subject: [PATCH 11/12] Support the commit --no-gpg-sign flag (#589) Support the commit --no-gpg-sign flag Signed-off-by: Bradley Buda --- README.md | 3 +++ lib/git/lib.rb | 10 ++++++++-- tests/units/test_commit_with_gpg.rb | 25 +++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d4b68c55..db38fbf6 100644 --- a/README.md +++ b/README.md @@ -244,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') diff --git a/lib/git/lib.rb b/lib/git/lib.rb index cb408246..fce8b274 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -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) 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 From ea79dadf07e65896a08487af011e60336e86d3e3 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 18 Aug 2022 10:44:01 -0700 Subject: [PATCH 12/12] Release v1.12.0 Signed-off-by: James Couball --- CHANGELOG.md | 4 ++++ lib/git/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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/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 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 Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy