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