From 8cd523afa9393d2347da05db13fba1b9852ade4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aldo=20Rinc=C3=B3n=20Mora?= Date: Sat, 19 Jun 2021 18:34:37 +0200 Subject: [PATCH 01/37] Add "g.current_branch" to Readme.md (#519) In my opinion .current_branch is a fairly common operation, and it would be nice to have it listed on the readme file. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0ff9a0a5..bc651c34 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ g.revparse('v2.5:Makefile') g.branches # returns Git::Branch objects g.branches.local +g.current_branch g.branches.remote g.branches[:master].gcommit g.branches['origin/master'].gcommit From 765df7c3d2831203863748e58c46dca0574b83db Mon Sep 17 00:00:00 2001 From: Valentino Stoll Date: Sat, 19 Jun 2021 12:56:25 -0400 Subject: [PATCH 02/37] Adds file option to config_set to allow adding to specific git-config files (#458) Signed-off-by: Valentino Stoll Co-authored-by: James Couball --- lib/git/base.rb | 9 +++++---- lib/git/lib.rb | 8 ++++++-- tests/units/test_config.rb | 17 +++++++++++++++-- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index 50459215..6e7c7a10 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -137,13 +137,14 @@ def chdir # :yields: the Git::Path #g.config('user.name', 'Scott Chacon') # sets value #g.config('user.email', 'email@email.com') # sets value + #g.config('user.email', 'email@email.com', file: 'path/to/custom/config) # sets value in file #g.config('user.name') # returns 'Scott Chacon' #g.config # returns whole config hash - def config(name = nil, value = nil) - if(name && value) + def config(name = nil, value = nil, options = {}) + if name && value # set value - lib.config_set(name, value) - elsif (name) + lib.config_set(name, value, options) + elsif name # return value lib.config_get(name) else diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 191b8a74..ce8c141b 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -583,8 +583,12 @@ def show(objectish=nil, path=nil) ## WRITE COMMANDS ## - def config_set(name, value) - command('config', name, value) + def config_set(name, value, options = {}) + if options[:file].to_s.empty? + command('config', name, value) + else + command('config', '--file', options[:file], name, value) + end end def global_config_set(name, value) diff --git a/tests/units/test_config.rb b/tests/units/test_config.rb index a1753831..c04c8530 100644 --- a/tests/units/test_config.rb +++ b/tests/units/test_config.rb @@ -1,6 +1,6 @@ #!/usr/bin/env ruby -require File.dirname(__FILE__) + '/../test_helper' +require_relative '../test_helper' class TestConfig < Test::Unit::TestCase def setup @@ -26,7 +26,20 @@ def test_set_config g.config('user.name', 'bully') assert_equal('bully', g.config('user.name')) end - end + end + + def test_set_config_with_custom_file + in_temp_dir do |_path| + custom_config_path = "#{Dir.pwd}/bare/.git/custom-config" + g = Git.clone(@wbare, 'bare') + assert_not_equal('bully', g.config('user.name')) + g.config('user.name', 'bully', file: custom_config_path) + assert_not_equal('bully', g.config('user.name')) + g.config('include.path', custom_config_path) + assert_equal('bully', g.config('user.name')) + assert_equal("[user]\n\tname = bully\n", File.read(custom_config_path)) + end + end def test_env_config with_custom_env_variables do From 0cef8ac3f2c912c35c12857f26240222c0f13b72 Mon Sep 17 00:00:00 2001 From: Othmane EL MASSARI <47815944+othmane399@users.noreply.github.com> Date: Sat, 19 Jun 2021 19:04:24 +0200 Subject: [PATCH 03/37] feat: add --gpg-sign option on commits (#518) Add --gpg-sign option on commits Signed-off-by: othmane399 --- lib/git/lib.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index ce8c141b..8dcb4f8a 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -646,6 +646,7 @@ def remove(path = '.', opts = {}) # :date # :no_verify # :allow_empty_message + # :gpg_sign # # @param [String] message the commit message to be used # @param [Hash] opts the commit options to be used @@ -659,6 +660,7 @@ 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] + arr_opts << '--gpg-sign' if opts[:gpg_sign] == true || "--gpg-sign=#{opts[:gpg_sign]}" if opts[:gpg_sign] command('commit', arr_opts) end From 8fe479bbdb8b7f3f0eb83d295b6cf6b869a098f9 Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 5 Jul 2021 17:30:51 -0700 Subject: [PATCH 04/37] Fix worktree test when git dir includes symlinks (#522) Signed-off-by: James Couball --- tests/units/test_worktree.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/units/test_worktree.rb b/tests/units/test_worktree.rb index 2b509726..f5141c8d 100644 --- a/tests/units/test_worktree.rb +++ b/tests/units/test_worktree.rb @@ -32,23 +32,24 @@ def create_temp_repo(clone_path) def setup @git = Git.open(git_working_dir) - + @commit = @git.object('1cc8667014381') @tree = @git.object('1cc8667014381^{tree}') @blob = @git.object('v2.5:example.txt') - + @worktrees = @git.worktrees end - + def test_worktrees_all assert(@git.worktrees.is_a?(Git::Worktrees)) assert(@git.worktrees.first.is_a?(Git::Worktree)) assert_equal(@git.worktrees.size, 2) end - + def test_worktrees_single worktree = @git.worktrees.first - assert_equal(worktree.dir, @git.dir.to_s) + git_dir = Pathname.new(@git.dir.to_s).realpath.to_s + assert_equal(worktree.dir, git_dir) assert_equal(worktree.gcommit, SAMPLE_LAST_COMMIT) end From 07a1167d4706ba4a66ab3025229ca4ed844a934d Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 6 Jul 2021 12:29:17 -0700 Subject: [PATCH 05/37] Release v1.9.0 (#524) Signed-off-by: James Couball Co-authored-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 e7e7963b..ae998e92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## 1.9.0 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.9.0 + ## 1.8.1 See https://github.com/ruby-git/ruby-git/releases/tag/v1.8.1 diff --git a/lib/git/version.rb b/lib/git/version.rb index 05b60fb1..6fe493dc 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.8.1' + VERSION='1.9.0' end From 45aeac931b346cc73666ac03521ebfb0cd52fd93 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 7 Jul 2021 08:58:47 -0700 Subject: [PATCH 06/37] Fix the gpg_sign commit option (#525) Signed-off-by: James Couball --- README.md | 8 +++++++ lib/git/lib.rb | 9 ++++++- tests/units/test_commit_with_gpg.rb | 37 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 tests/units/test_commit_with_gpg.rb diff --git a/README.md b/README.md index bc651c34..ab63d2fa 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,14 @@ g.remove('file.txt', :cached => true) # git rm -f --cached -- "file.txt" g.commit('message') g.commit_all('message') +# Sign a commit using the gpg key configured in the user.signingkey config setting +g.config('user.signingkey', '0A46826A') +g.commit('message', gpg_sign: true) + +# Sign a commit using a specified gpg key +key_id = '0A46826A' +g.commit('message', gpg_sign: key_id) + 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 8dcb4f8a..57220f07 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -660,7 +660,14 @@ 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] - arr_opts << '--gpg-sign' if opts[:gpg_sign] == true || "--gpg-sign=#{opts[:gpg_sign]}" if opts[:gpg_sign] + if opts[:gpg_sign] + arr_opts << + if opts[:gpg_sign] == true + '--gpg-sign' + else + "--gpg-sign=#{opts[:gpg_sign]}" + end + end command('commit', arr_opts) end diff --git a/tests/units/test_commit_with_gpg.rb b/tests/units/test_commit_with_gpg.rb new file mode 100644 index 00000000..97fb4de9 --- /dev/null +++ b/tests/units/test_commit_with_gpg.rb @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../test_helper' + +class TestCommitWithGPG < Test::Unit::TestCase + def setup + set_file_paths + end + + def test_with_configured_gpg_keyid + 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, gpg_sign: true) + assert_match(/commit.*--gpg-sign['"]/, actual_cmd) + end + end + + def test_with_specific_gpg_keyid + 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, gpg_sign: 'keykeykey') + assert_match(/commit.*--gpg-sign=keykeykey['"]/, actual_cmd) + end + end +end From 58100b0135d39215330b9c63590dd6307bb54ea7 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 7 Jul 2021 09:42:14 -0700 Subject: [PATCH 07/37] Release v1.9.1 (#527) Signed-off-by: James Couball Co-authored-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 ae998e92..bc708094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## 1.9.1 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.9.1 + ## 1.9.0 See https://github.com/ruby-git/ruby-git/releases/tag/v1.9.0 diff --git a/lib/git/version.rb b/lib/git/version.rb index 6fe493dc..b4cde914 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.9.0' + VERSION='1.9.1' end From 1023f850e7fd43392e56dbd47fb20b858d8d7e43 Mon Sep 17 00:00:00 2001 From: Daniel Leidert <14413364+dleidert@users.noreply.github.com> Date: Fri, 17 Dec 2021 19:29:34 +0100 Subject: [PATCH 08/37] Require pathname module (#536) ... otherwise the test fails with: TestWorktree: test_worktree_add_and_remove: .: (0.080734) test_worktree_prune: .: (0.055268) test_worktrees_all: .: (0.037409) test_worktrees_single: E =============================================================================== Error: test_worktrees_single(TestWorktree): NameError: uninitialized constant TestWorktree::Pathname /<>/tests/units/test_worktree.rb:51:in `test_worktrees_single' 48: 49: def test_worktrees_single 50: worktree = @git.worktrees.first => 51: git_dir = Pathname.new(@git.dir.to_s).realpath.to_s 52: assert_equal(worktree.dir, git_dir) 53: assert_equal(worktree.gcommit, SAMPLE_LAST_COMMIT) 54: end =============================================================================== : (0.041215) Signed-off-by: Daniel Leidert --- tests/units/test_worktree.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/units/test_worktree.rb b/tests/units/test_worktree.rb index f5141c8d..c0a81dcb 100644 --- a/tests/units/test_worktree.rb +++ b/tests/units/test_worktree.rb @@ -1,5 +1,6 @@ #!/usr/bin/env ruby require 'fileutils' +require 'pathname' require File.dirname(__FILE__) + '/../test_helper' SAMPLE_LAST_COMMIT = '5e53019b3238362144c2766f02a2c00d91fcc023' From ff98c42e25820347f602a8ae0fbc9df0b97f40ae Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 17 Dec 2021 13:51:01 -0500 Subject: [PATCH 09/37] Add support for the `git merge --no-commit` argument (#538) Docs at: https://git-scm.com/docs/git-merge#Documentation/git-merge.txt---no-commit Signed-off-by: Jon Dufresne --- lib/git/lib.rb | 1 + tests/units/test_merge.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 57220f07..9cb0a3a2 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -772,6 +772,7 @@ def checkout_file(version, file) def merge(branch, message = nil, opts = {}) arr_opts = [] + arr_opts << '--no-commit' if opts[:no_commit] arr_opts << '--no-ff' if opts[:no_ff] arr_opts << '-m' << message if message arr_opts += [branch] diff --git a/tests/units/test_merge.rb b/tests/units/test_merge.rb index 8fd10712..21e9ee78 100644 --- a/tests/units/test_merge.rb +++ b/tests/units/test_merge.rb @@ -130,4 +130,33 @@ def test_no_ff_merge end end end + + def test_merge_no_commit + in_temp_dir do |path| + g = Git.clone(@wbare, 'branch_merge_test') + g.chdir do + g.branch('new_branch_1').in_branch('first commit message') do + new_file('new_file_1', 'foo') + g.add + true + end + + g.branch('new_branch_2').in_branch('first commit message') do + new_file('new_file_2', 'bar') + g.add + true + end + + g.checkout('new_branch_2') + before_merge = g.show + g.merge('new_branch_1', nil, no_commit: true) + # HEAD is the same as before. + assert_equal(before_merge, g.show) + # File has not been merged in. + status = g.status['new_file_1'] + assert_equal('new_file_1', status.path) + assert_equal('A', status.type) + end + end + end end From 6cba37ef428fe17cc2a60d7de34c4426200f7a63 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 17 Dec 2021 14:01:47 -0500 Subject: [PATCH 10/37] Add support for `git init --initial-branch=main` argument (#539) Signed-off-by: Jon Dufresne --- lib/git.rb | 3 +++ lib/git/base.rb | 5 ++++- lib/git/lib.rb | 2 ++ tests/units/test_init.rb | 11 +++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/git.rb b/lib/git.rb index eb4c7cce..6e93957c 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -212,6 +212,9 @@ def self.global_config(name = nil, value = nil) # `"#{directory}/.git"`, create a bare repository at `"#{directory}"`. # See [what is a bare repository?](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefbarerepositoryabarerepository). # + # @option options [String] :initial_branch Use the specified name for the + # initial branch in the newly created repository. + # # @option options [Pathname] :repository the path to put the newly initialized # Git repository. The default for non-bare repository is `"#{directory}/.git"`. # diff --git a/lib/git/base.rb b/lib/git/base.rb index 6e7c7a10..30cf386f 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -34,7 +34,10 @@ def self.init(directory, options = {}) FileUtils.mkdir_p(options[:working_directory]) if options[:working_directory] && !File.directory?(options[:working_directory]) - init_options = { :bare => options[:bare] } + init_options = { + :bare => options[:bare], + :initial_branch => options[:initial_branch], + } options.delete(:working_directory) if options[:bare] diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 9cb0a3a2..66a37b60 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -71,10 +71,12 @@ def initialize(base = nil, logger = nil) # options: # :bare # :working_directory + # :initial_branch # def init(opts={}) arr_opts = [] arr_opts << '--bare' if opts[:bare] + arr_opts << "--initial-branch=#{opts[:initial_branch]}" if opts[:initial_branch] command('init', arr_opts) end diff --git a/tests/units/test_init.rb b/tests/units/test_init.rb index bbb04b94..c9cd1af5 100644 --- a/tests/units/test_init.rb +++ b/tests/units/test_init.rb @@ -38,6 +38,7 @@ def test_git_init assert(File.directory?(File.join(path, '.git'))) assert(File.exist?(File.join(path, '.git', 'config'))) assert_equal('false', repo.config('core.bare')) + assert_equal("ref: refs/heads/master\n", File.read("#{path}/.git/HEAD")) end end @@ -61,6 +62,16 @@ def test_git_init_remote_git end end + def test_git_init_initial_branch + in_temp_dir do |path| + repo = Git.init(path, initial_branch: 'main') + assert(File.directory?(File.join(path, '.git'))) + assert(File.exist?(File.join(path, '.git', 'config'))) + assert_equal('false', repo.config('core.bare')) + assert_equal("ref: refs/heads/main\n", File.read("#{path}/.git/HEAD")) + end + end + def test_git_clone in_temp_dir do |path| g = Git.clone(@wbare, 'bare-co') From 984ff7f0468ce9b89caeca95686f8b16983e83af Mon Sep 17 00:00:00 2001 From: Alex Bobrikovich Date: Fri, 17 Dec 2021 11:22:42 -0800 Subject: [PATCH 11/37] #533 Add --depth options for fetch call (#534) Signed-off-by: Alexande B --- lib/git/lib.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 66a37b60..4d0daef7 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -882,6 +882,7 @@ def fetch(remote, opts) arr_opts << '--tags' if opts[:t] || opts[:tags] arr_opts << '--prune' if opts[:p] || opts[:prune] arr_opts << '--unshallow' if opts[:unshallow] + arr_opts << '--depth' << opts[:depth] if opts[:depth] command('fetch', arr_opts) end From 38843144a0f96137af091a924a896aedf98a0d68 Mon Sep 17 00:00:00 2001 From: Cameron Walsh Date: Sat, 18 Dec 2021 10:15:24 +1100 Subject: [PATCH 12/37] Add -ff option to git clean (#529) Git will refuse to modify untracked nested git repositories (directories with a .git subdirectory) unless a second -f is given. This adds the ability to pass "-ff" to git clean to enable removing directories with a .git subdirectory. An example use case is when a gem from a git source is cached in vendor/cache/some_gem Removing this with "git clean" would require "git clean -dff" Signed-off-by: Cameron Walsh --- lib/git/base.rb | 1 + lib/git/lib.rb | 1 + tests/units/test_index_ops.rb | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/lib/git/base.rb b/lib/git/base.rb index 30cf386f..c902f0a5 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -290,6 +290,7 @@ def reset_hard(commitish = nil, opts = {}) # options: # :force # :d + # :ff # def clean(opts = {}) self.lib.clean(opts) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 4d0daef7..5641e4eb 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -684,6 +684,7 @@ def reset(commit, opts = {}) def clean(opts = {}) arr_opts = [] arr_opts << '--force' if opts[:force] + arr_opts << '-ff' if opts[:ff] arr_opts << '-d' if opts[:d] arr_opts << '-x' if opts[:x] diff --git a/tests/units/test_index_ops.rb b/tests/units/test_index_ops.rb index fd47e609..c033735b 100644 --- a/tests/units/test_index_ops.rb +++ b/tests/units/test_index_ops.rb @@ -49,6 +49,11 @@ def test_clean g.add g.commit("first commit") + FileUtils.mkdir_p("nested") + Dir.chdir('nested') do + Git.init + end + new_file('file-to-clean', 'blablahbla') FileUtils.mkdir_p("dir_to_clean") @@ -76,6 +81,11 @@ def test_clean g.clean(:force => true, :x => true) assert(!File.exist?('ignored_file')) + + assert(File.exist?('nested')) + + g.clean(:ff => true, :d => true) + assert(!File.exist?('nested')) end end end From 8feb4ff89ee27e5450a69e5e9904bb5ef0b0147b Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 19 Dec 2021 17:00:12 -0800 Subject: [PATCH 13/37] Refactor directory initialization (#544) Signed-off-by: James Couball --- lib/git/base.rb | 135 ++++++++++++++++++++++-------- tests/units/test_init.rb | 3 +- tests/units/test_thread_safety.rb | 2 +- 3 files changed, 102 insertions(+), 38 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index c902f0a5..13edf848 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -12,43 +12,35 @@ class Base # (see Git.bare) def self.bare(git_dir, options = {}) - self.new({:repository => git_dir}.merge(options)) + normalize_paths(options, default_repository: git_dir, bare: true) + self.new(options) end # (see Git.clone) def self.clone(repository, name, options = {}) - self.new(Git::Lib.new(nil, options[:log]).clone(repository, name, options)) + new_options = Git::Lib.new(nil, options[:log]).clone(repository, name, options) + normalize_paths(new_options, bare: options[:bare] || options[:mirror]) + self.new(new_options) end # Returns (and initialize if needed) a Git::Config instance # # @return [Git::Config] the current config instance. def self.config - return @@config ||= Config.new + @@config ||= Config.new end # (see Git.init) - def self.init(directory, options = {}) - options[:working_directory] ||= directory - options[:repository] ||= File.join(options[:working_directory], '.git') - - FileUtils.mkdir_p(options[:working_directory]) if options[:working_directory] && !File.directory?(options[:working_directory]) + def self.init(directory = '.', options = {}) + normalize_paths(options, default_working_directory: directory, default_repository: directory, bare: options[:bare]) init_options = { :bare => options[:bare], :initial_branch => options[:initial_branch], } - options.delete(:working_directory) if options[:bare] - - # Submodules have a .git *file* not a .git folder. - # This file's contents point to the location of - # where the git refs are held (In the parent repo) - if options[:working_directory] && File.file?(File.join(options[:working_directory], '.git')) - git_file = File.open('.git').read[8..-1].strip - options[:repository] = git_file - options[:index] = git_file + '/index' - end + directory = options[:bare] ? options[:repository] : options[:working_directory] + FileUtils.mkdir_p(directory) unless File.exist?(directory) # TODO: this dance seems awkward: this creates a Git::Lib so we can call # init so we can create a new Git::Base which in turn (ultimately) @@ -66,21 +58,8 @@ def self.init(directory, options = {}) end # (see Git.open) - def self.open(working_dir, options={}) - # TODO: move this to Git.open? - - options[:working_directory] ||= working_dir - options[:repository] ||= File.join(options[:working_directory], '.git') - - # Submodules have a .git *file* not a .git folder. - # This file's contents point to the location of - # where the git refs are held (In the parent repo) - if options[:working_directory] && File.file?(File.join(options[:working_directory], '.git')) - git_file = File.open('.git').read[8..-1].strip - options[:repository] = git_file - options[:index] = git_file + '/index' - end - + def self.open(working_dir, options = {}) + normalize_paths(options, default_working_directory: working_dir) self.new(options) end @@ -571,7 +550,6 @@ def with_temp_working &blk with_working(temp_dir, &blk) end - # runs git rev-parse to convert the objectish to a full sha # # @example @@ -596,6 +574,93 @@ def current_branch self.lib.branch_current end - end + private + + # Normalize options before they are sent to Git::Base.new + # + # Updates the options parameter by setting appropriate values for the following keys: + # * options[:working_directory] + # * options[:repository] + # * options[:index] + # + # All three values will be set to absolute paths. An exception is that + # :working_directory will be set to nil if bare is true. + # + private_class_method def self.normalize_paths( + options, default_working_directory: nil, default_repository: nil, bare: false + ) + normalize_working_directory(options, default: default_working_directory, bare: bare) + normalize_repository(options, default: default_repository, bare: bare) + normalize_index(options) + end + + # Normalize options[:working_directory] + # + # If working with a bare repository, set to `nil`. + # Otherwise, set to the first non-nil value of: + # 1. `options[:working_directory]`, + # 2. the `default` parameter, or + # 3. the current working directory + # + # Finally, if options[:working_directory] is a relative path, convert it to an absoluite + # path relative to the current directory. + # + private_class_method def self.normalize_working_directory(options, default:, bare: false) + working_directory = + if bare + nil + else + File.expand_path(options[:working_directory] || default || Dir.pwd) + end + options[:working_directory] = working_directory + end + + # Normalize options[:repository] + # + # If working with a bare repository, set to the first non-nil value out of: + # 1. `options[:repository]` + # 2. the `default` parameter + # 3. the current working directory + # + # Otherwise, set to the first non-nil value of: + # 1. `options[:repository]` + # 2. `.git` + # + # Next, if options[:repository] refers to a *file* and not a *directory*, set + # options[:repository] to the contents of that file. This is the case when + # working with a submodule or a secondary working tree (created with git worktree + # add). In these cases the repository is actually contained/nested within the + # parent's repository directory. + # + # Finally, if options[:repository] is a relative path, convert it to an absolute + # path relative to: + # 1. the current directory if working with a bare repository or + # 2. the working directory if NOT working with a bare repository + # + private_class_method def self.normalize_repository(options, default:, bare: false) + repository = + if bare + File.expand_path(options[:repository] || default || Dir.pwd) + else + File.expand_path(options[:repository] || '.git', options[:working_directory]) + end + + if File.file?(repository) + repository = File.expand_path(File.open(repository).read[8..-1].strip, options[:working_directory]) + end + + options[:repository] = repository + end + + # Normalize options[:index] + # + # If options[:index] is a relative directory, convert it to an absolute + # directory relative to the repository directory + # + private_class_method def self.normalize_index(options) + index = File.expand_path(options[:index] || 'index', options[:repository]) + options[:index] = index + end + end end diff --git a/tests/units/test_init.rb b/tests/units/test_init.rb index c9cd1af5..4ec4771d 100644 --- a/tests/units/test_init.rb +++ b/tests/units/test_init.rb @@ -45,8 +45,7 @@ def test_git_init def test_git_init_bare in_temp_dir do |path| repo = Git.init(path, :bare => true) - assert(File.directory?(File.join(path, '.git'))) - assert(File.exist?(File.join(path, '.git', 'config'))) + assert(File.exist?(File.join(path, 'config'))) assert_equal('true', repo.config('core.bare')) end end diff --git a/tests/units/test_thread_safety.rb b/tests/units/test_thread_safety.rb index 401659a5..d2500f10 100644 --- a/tests/units/test_thread_safety.rb +++ b/tests/units/test_thread_safety.rb @@ -24,7 +24,7 @@ def test_git_init_bare threads.each(&:join) dirs.each do |dir| - Git.bare("#{dir}/.git").ls_files + Git.bare(dir).ls_files end end end From 8acec7d26ef22f0de360f970672fcc5e56953063 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 19 Dec 2021 17:46:11 -0800 Subject: [PATCH 14/37] Release v1.10.0 (#545) Signed-off-by: James Couball Co-authored-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 bc708094..e53dd79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## 1.10.0 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.0 + ## 1.9.1 See https://github.com/ruby-git/ruby-git/releases/tag/v1.9.1 diff --git a/lib/git/version.rb b/lib/git/version.rb index b4cde914..ea10ef11 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.9.1' + VERSION='1.10.0' end From cb01d2b9ce3927557d5bd5360de527a546c97d7c Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 20 Dec 2021 09:55:19 -0800 Subject: [PATCH 15/37] Create a Docker image to run the changelog (#546) Signed-off-by: James Couball --- Dockerfile.changelog-rs | 12 ++++++++ RELEASING.md | 66 ++++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 Dockerfile.changelog-rs diff --git a/Dockerfile.changelog-rs b/Dockerfile.changelog-rs new file mode 100644 index 00000000..75c35d93 --- /dev/null +++ b/Dockerfile.changelog-rs @@ -0,0 +1,12 @@ +FROM rust + +# Build the docker image (from this project's root directory): +# docker build --file Dockerfile.changelog-rs --tag changelog-rs . +# +# Use this image to output a changelog (from this project's root directory): +# docker run --rm --volume "$PWD:/worktree" changelog-rs v1.9.1 v1.10.0 + +RUN cargo install changelog-rs +WORKDIR /worktree + +ENTRYPOINT ["/usr/local/cargo/bin/changelog-rs", "/worktree"] diff --git a/RELEASING.md b/RELEASING.md index 7f360370..f43697da 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -6,9 +6,11 @@ # How to release a new git.gem Releasing a new version of the `git` gem requires these steps: - * [Prepare the release](#prepare-the-release) - * [Create a GitHub release](#create-a-github-release) - * [Build and release the gem](#build-and-release-the-gem) + +- [How to release a new git.gem](#how-to-release-a-new-gitgem) + - [Prepare the release](#prepare-the-release) + - [Create a GitHub release](#create-a-github-release) + - [Build and release the gem](#build-and-release-the-gem) These instructions use an example where the current release version is `1.5.0` and the new release version to be created is `1.6.0.pre1`. @@ -18,45 +20,49 @@ and the new release version to be created is `1.6.0.pre1`. From a fork of ruby-git, create a PR containing changes to (1) bump the version number, (2) update the CHANGELOG.md, and (3) tag the release. - * Bump the version number in lib/git/version.rb following [Semantic Versioning](https://semver.org) - guidelines - * Add a link in CHANGELOG.md to the release tag which will be created later - in this guide - * Create a new tag using [git-extras](https://github.com/tj/git-extras/blob/master/Commands.md#git-release) - `git release` command - * For example: `git release v1.6.0.pre1` - * These should be the only changes in the PR - * An example of these changes for `v1.6.0.pre1` can be found in [PR #435](https://github.com/ruby-git/ruby-git/pull/435) - * Get the PR reviewed, approved and merged to master. +- Bump the version number in lib/git/version.rb following [Semantic Versioning](https://semver.org) + guidelines +- Add a link in CHANGELOG.md to the release tag which will be created later + in this guide +- Create a new tag using [git-extras](https://github.com/tj/git-extras/blob/master/Commands.md#git-release) + `git release` command + - For example: `git release v1.6.0.pre1` +- These should be the only changes in the PR +- An example of these changes for `v1.6.0.pre1` can be found in [PR #435](https://github.com/ruby-git/ruby-git/pull/435) +- Get the PR reviewed, approved and merged to master. ## Create a GitHub release On [the ruby-git releases page](https://github.com/ruby-git/ruby-git/releases), select `Draft a new release` - * Select the tag corresponding to the version being released `v1.6.0.pre1` - * The Target should be `master` - * For the release description, use the output of [changelog-rs](https://github.com/perlun/changelog-rs) - * Since the release has not been created yet, you will need to supply - `changeling-rs` with the current release tag and the tag the new release - is being created from - * For example: `changelog-rs . v1.5.0 v1.6.0.pre1` - * Copy the output, omitting the tag header `## v1.6.0.pre1` and paste into - the release description - * The release description can be edited later if needed - * Select the appropriate value for `This is a pre-release` - * Since `v1.6.0.pre1` is a pre-release, check `This is a pre-release` +- Select the tag corresponding to the version being released `v1.6.0.pre1` +- The Target should be `master` +- For the release description, use the output of [changelog-rs](https://github.com/perlun/changelog-rs) + - A Docker image is provided in [Dockerfile.changelog-rs](https://github.com/ruby-git/ruby-git/blob/master/Dockerfile.changelog-rs) + so you don't have to install changelog-rs or the Rust tool chain. To build the + Docker image, run this command from this project's root directory: + - `docker build --file Dockerfile.changelog-rs --tag changelog-rs .` + - To run the changelog-rs command using this image, run the following command + from this project's root directory (replace the tag names appropriate for the + current release): + - `docker run --rm --volume "$PWD:/worktree" changelog-rs v1.5.0 v1.6.0.pre1` + - Copy the output, omitting the tag header `## v1.6.0.pre1` and paste into + the release description + - The release description can be edited later if needed +- Select the appropriate value for `This is a pre-release` + - Since `v1.6.0.pre1` is a pre-release, check `This is a pre-release` ## Build and release the gem Clone [ruby-git/ruby-git](https://github.com/ruby-git/ruby-git) directly (not a fork) and ensure your local working copy is on the master branch - * Verify that you are not on a fork with the command `git remote -v` - * Verify that the version number is correct by running `rake -T` and inspecting - the output for the `release[remote]` task +- Verify that you are not on a fork with the command `git remote -v` +- Verify that the version number is correct by running `rake -T` and inspecting + the output for the `release[remote]` task Build the git gem and push it to rubygems.org with the command `rake release` - * Ensure that your `gem sources list` includes `https://rubygems.org` (in my - case, I usually have my work’s internal gem repository listed) +- Ensure that your `gem sources list` includes `https://rubygems.org` (in my + case, I usually have my work’s internal gem repository listed) From ea47044eb75c8820c4a525730e1aa3d445baa5ea Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 23 Dec 2021 15:00:30 -0800 Subject: [PATCH 16/37] Add Ruby 3.0 to CI build (#547) Signed-off-by: James Couball --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index ad2ea03a..3acf4743 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - ruby: [2.3, 2.7] + ruby: [2.3, 2.7, 3.0] operating-system: [ubuntu-latest] include: - ruby: head From db060fc82c89bb5abf446dd0f127f84425247ef2 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 31 Dec 2021 16:25:50 -0800 Subject: [PATCH 17/37] Properly unescape diff paths (#504) Signed-off-by: James Couball --- lib/git.rb | 2 + lib/git/base.rb | 2 +- lib/git/diff.rb | 4 +- lib/git/encoding_utils.rb | 33 ++++++++++ lib/git/escaped_path.rb | 77 ++++++++++++++++++++++ lib/git/lib.rb | 32 +-------- tests/units/test_archive.rb | 2 +- tests/units/test_diff_with_escaped_path.rb | 22 +++++++ tests/units/test_escaped_path.rb | 36 ++++++++++ tests/units/test_logger.rb | 39 +++++++---- 10 files changed, 205 insertions(+), 44 deletions(-) create mode 100644 lib/git/encoding_utils.rb create mode 100644 lib/git/escaped_path.rb create mode 100644 tests/units/test_diff_with_escaped_path.rb create mode 100755 tests/units/test_escaped_path.rb diff --git a/lib/git.rb b/lib/git.rb index 6e93957c..4ad1bd97 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -9,6 +9,8 @@ require 'git/branches' require 'git/config' require 'git/diff' +require 'git/encoding_utils' +require 'git/escaped_path' require 'git/index' require 'git/lib' require 'git/log' diff --git a/lib/git/base.rb b/lib/git/base.rb index 13edf848..815fc36a 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -36,7 +36,7 @@ def self.init(directory = '.', options = {}) init_options = { :bare => options[:bare], - :initial_branch => options[:initial_branch], + :initial_branch => options[:initial_branch] } directory = options[:bare] ? options[:repository] : options[:working_directory] diff --git a/lib/git/diff.rb b/lib/git/diff.rb index 06bd3941..d40ddce4 100644 --- a/lib/git/diff.rb +++ b/lib/git/diff.rb @@ -129,8 +129,8 @@ def process_full_diff final = {} current_file = nil @full_diff.split("\n").each do |line| - if m = /^diff --git a\/(.*?) b\/(.*?)/.match(line) - current_file = m[1] + if m = %r{\Adiff --git ("?)a/(.+?)\1 ("?)b/(.+?)\3\z}.match(line) + current_file = Git::EscapedPath.new(m[2]).unescape final[current_file] = defaults.merge({:patch => line, :path => current_file}) else if m = /^index ([0-9a-f]{4,40})\.\.([0-9a-f]{4,40})( ......)*/.match(line) diff --git a/lib/git/encoding_utils.rb b/lib/git/encoding_utils.rb new file mode 100644 index 00000000..332b5461 --- /dev/null +++ b/lib/git/encoding_utils.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rchardet' + +module Git + # Method that can be used to detect and normalize string encoding + module EncodingUtils + def self.default_encoding + __ENCODING__.name + end + + def self.best_guess_encoding + # Encoding::ASCII_8BIT.name + Encoding::UTF_8.name + end + + def self.detected_encoding(str) + CharDet.detect(str)['encoding'] || best_guess_encoding + end + + def self.encoding_options + { invalid: :replace, undef: :replace } + end + + def self.normalize_encoding(str) + return str if str.valid_encoding? && str.encoding.name == default_encoding + + return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding? + + str.encode(default_encoding, detected_encoding(str), **encoding_options) + end + end +end diff --git a/lib/git/escaped_path.rb b/lib/git/escaped_path.rb new file mode 100644 index 00000000..7519a3ac --- /dev/null +++ b/lib/git/escaped_path.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Git + # Represents an escaped Git path string + # + # Git commands that output paths (e.g. ls-files, diff), will escape usual + # characters in the path with backslashes in the same way C escapes control + # characters (e.g. \t for TAB, \n for LF, \\ for backslash) or bytes with values + # larger than 0x80 (e.g. octal \302\265 for "micro" in UTF-8). + # + # @example + # Git::GitPath.new('\302\265').unescape # => "µ" + # + class EscapedPath + UNESCAPES = { + 'a' => 0x07, + 'b' => 0x08, + 't' => 0x09, + 'n' => 0x0a, + 'v' => 0x0b, + 'f' => 0x0c, + 'r' => 0x0d, + 'e' => 0x1b, + '\\' => 0x5c, + '"' => 0x22, + "'" => 0x27 + }.freeze + + attr_reader :path + + def initialize(path) + @path = path + end + + # Convert an escaped path to an unescaped path + def unescape + bytes = escaped_path_to_bytes(path) + str = bytes.pack('C*') + str.force_encoding(Encoding::UTF_8) + end + + private + + def extract_octal(path, index) + [path[index + 1..index + 4].to_i(8), 4] + end + + def extract_escape(path, index) + [UNESCAPES[path[index + 1]], 2] + end + + def extract_single_char(path, index) + [path[index].ord, 1] + end + + def next_byte(path, index) + if path[index] == '\\' && path[index + 1] >= '0' && path[index + 1] <= '7' + extract_octal(path, index) + elsif path[index] == '\\' && UNESCAPES.include?(path[index + 1]) + extract_escape(path, index) + else + extract_single_char(path, index) + end + end + + def escaped_path_to_bytes(path) + index = 0 + [].tap do |bytes| + while index < path.length + byte, chars_used = next_byte(path, index) + bytes << byte + index += chars_used + end + end + end + end +end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 5641e4eb..d892462a 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1,4 +1,3 @@ -require 'rchardet' require 'tempfile' require 'zlib' @@ -1085,7 +1084,8 @@ def command(cmd, *opts, &block) global_opts = [] global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil? global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil? - global_opts << ["-c", "color.ui=false"] + global_opts << %w[-c core.quotePath=true] + global_opts << %w[-c color.ui=false] opts = [opts].flatten.map {|s| escape(s) }.join(' ') @@ -1176,35 +1176,10 @@ def log_path_options(opts) arr_opts end - def default_encoding - __ENCODING__.name - end - - def best_guess_encoding - # Encoding::ASCII_8BIT.name - Encoding::UTF_8.name - end - - def detected_encoding(str) - CharDet.detect(str)['encoding'] || best_guess_encoding - end - - def encoding_options - { invalid: :replace, undef: :replace } - end - - def normalize_encoding(str) - return str if str.valid_encoding? && str.encoding.name == default_encoding - - return str.encode(default_encoding, str.encoding, **encoding_options) if str.valid_encoding? - - str.encode(default_encoding, detected_encoding(str), **encoding_options) - end - def run_command(git_cmd, &block) return IO.popen(git_cmd, &block) if block_given? - `#{git_cmd}`.lines.map { |l| normalize_encoding(l) }.join + `#{git_cmd}`.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join end def escape(s) @@ -1225,6 +1200,5 @@ def windows_platform? win_platform_regex = /mingw|mswin/ RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex end - end end diff --git a/tests/units/test_archive.rb b/tests/units/test_archive.rb index 0bd0fc1f..3386a27f 100644 --- a/tests/units/test_archive.rb +++ b/tests/units/test_archive.rb @@ -45,7 +45,7 @@ def test_archive f = @git.object('v2.6').archive(tempfile, :format => 'tar', :prefix => 'test/', :path => 'ex_dir/') assert(File.exist?(f)) - + lines = Minitar::Input.open(f).each.to_a.map(&:full_name) assert_match(%r{test/}, lines[1]) assert_match(%r{test/ex_dir/ex\.txt}, lines[3]) diff --git a/tests/units/test_diff_with_escaped_path.rb b/tests/units/test_diff_with_escaped_path.rb new file mode 100644 index 00000000..6387af77 --- /dev/null +++ b/tests/units/test_diff_with_escaped_path.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby +# encoding: utf-8 + +require File.dirname(__FILE__) + '/../test_helper' + +# Test diff when the file path has to be quoted according to core.quotePath +# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath +# +class TestDiffWithEscapedPath < Test::Unit::TestCase + def test_diff_with_non_ascii_filename + in_temp_dir do |path| + create_file('my_other_file_☠', "First Line\n") + `git init` + `git add .` + `git config --local core.safecrlf false` if Gem.win_platform? + `git commit -m "First Commit"` + update_file('my_other_file_☠', "Second Line\n") + diff_paths = Git.open('.').diff.map(&:path) + assert_equal(["my_other_file_☠"], diff_paths) + end + end +end diff --git a/tests/units/test_escaped_path.rb b/tests/units/test_escaped_path.rb new file mode 100755 index 00000000..38230e4f --- /dev/null +++ b/tests/units/test_escaped_path.rb @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "#{File.dirname(__FILE__)}/../test_helper" + +# Test diff when the file path has escapes according to core.quotePath +# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath +# See https://www.jvt.me/posts/2020/06/23/byte-array-to-string-ruby/ +# See https://stackoverflow.com/questions/54788845/how-can-i-convert-a-guid-into-a-byte-array-in-ruby +# +class TestEscapedPath < Test::Unit::TestCase + def test_simple_path + path = 'my_other_file' + expected_unescaped_path = 'my_other_file' + assert_equal(expected_unescaped_path, Git::EscapedPath.new(path).unescape) + end + + def test_unicode_path + path = 'my_other_file_\\342\\230\\240' + expected_unescaped_path = 'my_other_file_☠' + assert_equal(expected_unescaped_path, Git::EscapedPath.new(path).unescape) + end + + def test_single_char_escapes + Git::EscapedPath::UNESCAPES.each_pair do |escape_char, expected_char| + path = "\\#{escape_char}" + assert_equal(expected_char.chr, Git::EscapedPath.new(path).unescape) + end + end + + def test_compound_escape + path = 'my_other_file_"\\342\\230\\240\\n"' + expected_unescaped_path = "my_other_file_\"☠\n\"" + assert_equal(expected_unescaped_path, Git::EscapedPath.new(path).unescape) + end +end diff --git a/tests/units/test_logger.rb b/tests/units/test_logger.rb index 0f72cc95..954c5e0c 100644 --- a/tests/units/test_logger.rb +++ b/tests/units/test_logger.rb @@ -7,32 +7,49 @@ class TestLogger < Test::Unit::TestCase def setup set_file_paths end - + + def missing_log_entry + 'Did not find expected log entry.' + end + + def unexpected_log_entry + 'Unexpected log entry found' + end + def test_logger log = Tempfile.new('logfile') log.close - + logger = Logger.new(log.path) logger.level = Logger::DEBUG - + @git = Git.open(@wdir, :log => logger) @git.branches.size - + logc = File.read(log.path) - assert(/INFO -- : git ['"]--git-dir=[^'"]+['"] ['"]--work-tree=[^'"]+['"] ['"]-c['"] ['"]color.ui=false['"] branch ['"]-a['"]/.match(logc)) - assert(/DEBUG -- : cherry\n diff_over_patches\n\* git_grep/m.match(logc)) + expected_log_entry = /INFO -- : git (?.*?) branch ['"]-a['"]/ + assert_match(expected_log_entry, logc, missing_log_entry) + + expected_log_entry = /DEBUG -- : cherry/ + assert_match(expected_log_entry, logc, missing_log_entry) + end + + def test_logging_at_info_level_should_not_show_debug_messages log = Tempfile.new('logfile') log.close logger = Logger.new(log.path) logger.level = Logger::INFO - + @git = Git.open(@wdir, :log => logger) @git.branches.size - + logc = File.read(log.path) - assert(/INFO -- : git ['"]--git-dir=[^'"]+['"] ['"]--work-tree=[^'"]+['"] ['"]-c['"] ['"]color.ui=false['"] branch ['"]-a['"]/.match(logc)) - assert(!/DEBUG -- : cherry\n diff_over_patches\n\* git_grep/m.match(logc)) + + expected_log_entry = /INFO -- : git (?.*?) branch ['"]-a['"]/ + assert_match(expected_log_entry, logc, missing_log_entry) + + expected_log_entry = /DEBUG -- : cherry/ + assert_not_match(expected_log_entry, logc, unexpected_log_entry) end - end From ea281181c540980b7c85ef1afe35158b8049e2ca Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 3 Jan 2022 09:18:58 -0800 Subject: [PATCH 18/37] Properly escape double quotes in shell commands on Windows (#552) * Add test for properly escaping double quote on Windows * Properly escape Windows command line arguments Signed-off-by: James Couball --- lib/git/lib.rb | 5 +++-- tests/units/test_windows_cmd_escaping.rb | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/units/test_windows_cmd_escaping.rb diff --git a/lib/git/lib.rb b/lib/git/lib.rb index d892462a..2d6c129d 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1191,8 +1191,9 @@ def escape_for_sh(s) end def escape_for_windows(s) - # Windows does not need single quote escaping inside double quotes - %Q{"#{s}"} + # Escape existing double quotes in s and then wrap the result with double quotes + escaped_string = s.to_s.gsub('"','\\"') + %Q{"#{escaped_string}"} end def windows_platform? diff --git a/tests/units/test_windows_cmd_escaping.rb b/tests/units/test_windows_cmd_escaping.rb new file mode 100644 index 00000000..a5d994d9 --- /dev/null +++ b/tests/units/test_windows_cmd_escaping.rb @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby +# encoding: utf-8 + +require File.dirname(__FILE__) + '/../test_helper' + +# Test diff when the file path has to be quoted according to core.quotePath +# See https://git-scm.com/docs/git-config#Documentation/git-config.txt-corequotePath +# +class TestWindowsCmdEscaping < Test::Unit::TestCase + def test_commit_with_double_quote_in_commit_message + expected_commit_message = 'Commit message with "double quotes"' + in_temp_dir do |path| + create_file('README.md', "# README\n") + git = Git.init('.') + git.add + git.commit(expected_commit_message) + commits = git.log(1) + actual_commit_message = commits.first.message + assert_equal(expected_commit_message, actual_commit_message) + end + end +end From 735b0833f54fd74a1584070ff22611a6e1a7611f Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 3 Jan 2022 13:14:27 -0800 Subject: [PATCH 19/37] Release v1.10.1 (#553) 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 e53dd79e..5c8ad209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## 1.10.1 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.1 + ## 1.10.0 See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.0 diff --git a/lib/git/version.rb b/lib/git/version.rb index ea10ef11..89ef1017 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.10.0' + VERSION='1.10.1' end From 12e3d0300c6aead9c416e5d8445bacb84d8a5b37 Mon Sep 17 00:00:00 2001 From: Brandon Fish Date: Wed, 5 Jan 2022 20:31:53 -0600 Subject: [PATCH 20/37] Store tempfile objects to prevent deletion during tests (#555) --- tests/units/test_archive.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/units/test_archive.rb b/tests/units/test_archive.rb index 3386a27f..93ec66f2 100644 --- a/tests/units/test_archive.rb +++ b/tests/units/test_archive.rb @@ -7,10 +7,16 @@ class TestArchive < Test::Unit::TestCase def setup set_file_paths @git = Git.open(@wdir) + @tempfiles = [] + end + + def teardown + @tempfiles.clear end def tempfile tempfile_object = Tempfile.new('archive-test') + @tempfiles << tempfile_object # prevent deletion until teardown tempfile_object.close # close to avoid locking from git processes tempfile_object.path end From c987a74776f186e97f958662beea719fa83000f7 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 6 Jan 2022 14:45:55 -0800 Subject: [PATCH 21/37] Add create-release, setup, and console dev scripts (#560) Signed-off-by: James Couball --- bin/console | 15 ++ bin/create-release | 506 +++++++++++++++++++++++++++++++++++++++++++++ bin/setup | 8 + git.gemspec | 3 +- 4 files changed, 531 insertions(+), 1 deletion(-) create mode 100755 bin/console create mode 100755 bin/create-release create mode 100755 bin/setup diff --git a/bin/console b/bin/console new file mode 100755 index 00000000..0199a6fc --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'bundler/setup' +require 'git' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) diff --git a/bin/create-release b/bin/create-release new file mode 100755 index 00000000..fdc8aa83 --- /dev/null +++ b/bin/create-release @@ -0,0 +1,506 @@ +#!/usr/bin/env ruby + +# Run this script while in the root directory of the project with the default +# branch checked out. + +require 'bump' +require 'English' +require 'fileutils' +require 'optparse' +require 'tempfile' + +# TODO: Right now the default branch and the remote name are hard coded + +class Options + attr_accessor :current_version, :next_version, :tag, :current_tag, :next_tag, :branch, :quiet + + def initialize + yield self if block_given? + end + + def release_type + raise "release_type not set" if @release_type.nil? + @release_type + end + + VALID_RELEASE_TYPES = %w(major minor patch) + + def release_type=(release_type) + raise 'release_type must be one of: ' + VALID_RELEASE_TYPES.join(', ') unless VALID_RELEASE_TYPES.include?(release_type) + @release_type = release_type + end + + def quiet + @quiet = false unless instance_variable_defined?(:@quiet) + @quiet + end + + def current_version + @current_version ||= Bump::Bump.current + end + + def next_version + current_version # Save the current version before bumping + @next_version ||= Bump::Bump.next_version(release_type) + end + + def tag + @tag ||= "v#{next_version}" + end + + def current_tag + @current_tag ||= "v#{current_version}" + end + + def next_tag + tag + end + + def branch + @branch ||= "release-#{tag}" + end + + def default_branch + @default_branch ||= `git remote show '#{remote}'`.match(/HEAD branch: (.*?)$/)[1] + end + + def remote + @remote ||= 'origin' + end + + def to_s + <<~OUTPUT + release_type='#{release_type}' + current_version='#{current_version}' + next_version='#{next_version}' + tag='#{tag}' + branch='#{branch}' + quiet=#{quiet} + OUTPUT + end +end + +class CommandLineParser + attr_reader :options + + def initialize + @option_parser = OptionParser.new + define_options + @options = Options.new + end + + def parse(args) + option_parser.parse!(remaining_args = args.dup) + parse_remaining_args(remaining_args) + # puts options unless options.quiet + options + end + + private + + attr_reader :option_parser + + def parse_remaining_args(remaining_args) + error_with_usage('No release type specified') if remaining_args.empty? + @options.release_type = remaining_args.shift || nil + error_with_usage('Too many args') unless remaining_args.empty? + end + + def error_with_usage(message) + warn <<~MESSAGE + ERROR: #{message} + #{option_parser} + MESSAGE + exit 1 + end + + def define_options + option_parser.banner = 'Usage: create_release --help | release-type' + option_parser.separator '' + option_parser.separator 'Options:' + + define_quiet_option + define_help_option + end + + def define_quiet_option + option_parser.on('-q', '--[no-]quiet', 'Do not show output') do |quiet| + options.quiet = quiet + end + end + + def define_help_option + option_parser.on_tail('-h', '--help', 'Show this message') do + puts option_parser + exit 0 + end + end +end + +class ReleaseAssertions + attr_reader :options + + def initialize(options) + @options = options + end + + def make_assertions + bundle_is_up_to_date + in_git_repo + in_repo_toplevel_directory + on_default_branch + no_uncommitted_changes + local_and_remote_on_same_commit + tag_does_not_exist + branch_does_not_exist + docker_is_running + changelog_docker_container_exists + gh_command_exists + end + + private + + def gh_command_exists + print "Checking that the gh command exists..." + `which gh > /dev/null 2>&1` + if $CHILD_STATUS.success? + puts "OK" + else + error "The gh command was not found" + end + end + + def docker_is_running + print "Checking that docker is installed and running..." + `docker info > /dev/null 2>&1` + if $CHILD_STATUS.success? + puts "OK" + else + error "Docker is not installed or not running" + end + end + + + def changelog_docker_container_exists + print "Checking that the changelog docker container exists (might take time to build)..." + `docker build --file Dockerfile.changelog-rs --tag changelog-rs . 1>/dev/null` + if $CHILD_STATUS.success? + puts "OK" + else + error "Failed to build the changelog-rs docker container" + end + end + + def bundle_is_up_to_date + print "Checking that the bundle is up to date..." + if File.exist?('Gemfile.lock') + print "Running bundle update..." + `bundle update --quiet` + if $CHILD_STATUS.success? + puts "OK" + else + error "bundle update failed" + end + else + print "Running bundle install..." + `bundle install --quiet` + if $CHILD_STATUS.success? + puts "OK" + else + error "bundle install failed" + end + end + end + + def in_git_repo + print "Checking that you are in a git repo..." + `git rev-parse --is-inside-work-tree --quiet > /dev/null 2>&1` + if $CHILD_STATUS.success? + puts "OK" + else + error "You are not in a git repo" + end + end + + def in_repo_toplevel_directory + print "Checking that you are in the repo's toplevel directory..." + toplevel_directory = `git rev-parse --show-toplevel`.chomp + if toplevel_directory == FileUtils.pwd + puts "OK" + else + error "You are not in the repo's toplevel directory" + end + end + + def on_default_branch + print "Checking that you are on the default branch..." + current_branch = `git branch --show-current`.chomp + if current_branch == options.default_branch + puts "OK" + else + error "You are not on the default branch '#{default_branch}'" + end + end + + def no_uncommitted_changes + print "Checking that there are no uncommitted changes..." + if `git status --porcelain | wc -l`.to_i == 0 + puts "OK" + else + error "There are uncommitted changes" + end + end + + def no_staged_changes + print "Checking that there are no staged changes..." + if `git diff --staged --name-only | wc -l`.to_i == 0 + puts "OK" + else + error "There are staged changes" + end + end + + def local_and_remote_on_same_commit + print "Checking that local and remote are on the same commit..." + local_commit = `git rev-parse HEAD`.chomp + remote_commit = `git ls-remote '#{options.remote}' '#{options.default_branch}' | cut -f 1`.chomp + if local_commit == remote_commit + puts "OK" + else + error "Local and remote are not on the same commit" + end + end + + def local_tag_does_not_exist + print "Checking that local tag '#{options.tag}' does not exist..." + + tags = `git tag --list "#{options.tag}"`.chomp + error 'Could not list tags' unless $CHILD_STATUS.success? + + if tags.split.empty? + puts 'OK' + else + error "'#{options.tag}' already exists" + end + end + + def remote_tag_does_not_exist + print "Checking that the remote tag '#{options.tag}' does not exist..." + `git ls-remote --tags --exit-code '#{options.remote}' #{options.tag} >/dev/null 2>&1` + unless $CHILD_STATUS.success? + puts "OK" + else + error "'#{options.tag}' already exists" + end + end + + def tag_does_not_exist + local_tag_does_not_exist + remote_tag_does_not_exist + end + + def local_branch_does_not_exist + print "Checking that local branch '#{options.branch}' does not exist..." + + if `git branch --list "#{options.branch}" | wc -l`.to_i.zero? + puts "OK" + else + error "'#{options.branch}' already exists." + end + end + + def remote_branch_does_not_exist + print "Checking that the remote branch '#{options.branch}' does not exist..." + `git ls-remote --heads --exit-code '#{options.remote}' '#{options.branch}' >/dev/null 2>&1` + unless $CHILD_STATUS.success? + puts "OK" + else + error "'#{options.branch}' already exists" + end + end + + def branch_does_not_exist + local_branch_does_not_exist + remote_branch_does_not_exist + end + + private + + def print(*args) + super unless options.quiet + end + + def puts(*args) + super unless options.quiet + end + + def error(message) + warn "ERROR: #{message}" + exit 1 + end +end + +class ReleaseCreator + attr_reader :options + + def initialize(options) + @options = options + end + + def create_release + create_branch + update_changelog + update_version + make_release_commit + create_tag + push_release_commit_and_tag + create_github_release + create_release_pull_request + end + + private + + def create_branch + print "Creating branch '#{options.branch}'..." + `git checkout -b "#{options.branch}" > /dev/null 2>&1` + if $CHILD_STATUS.success? + puts "OK" + else + error "Could not create branch '#{options.branch}'" unless $CHILD_STATUS.success? + end + end + + def update_changelog + print 'Updating CHANGELOG.md...' + changelog_lines = File.readlines('CHANGELOG.md') + first_entry = changelog_lines.index { |e| e =~ /^## / } + error "Could not find changelog insertion point" unless first_entry + FileUtils.rm('CHANGELOG.md') + File.write('CHANGELOG.md', <<~CHANGELOG.chomp) + #{changelog_lines[0..first_entry - 1].join}## #{options.tag} + + See https://github.com/ruby-git/ruby-git/releases/tag/#{options.tag} + + #{changelog_lines[first_entry..].join} + CHANGELOG + `git add CHANGELOG.md` + if $CHILD_STATUS.success? + puts 'OK' + else + error 'Could not stage changes to CHANGELOG.md' + end + end + + def update_version + print 'Updating version...' + message, status = Bump::Bump.run(options.release_type, commit: false) + error 'Could not bump version' unless status == 0 + `git add lib/git/version.rb` + if $CHILD_STATUS.success? + puts 'OK' + else + error 'Could not stage changes to lib/git/version.rb' + end + end + + def make_release_commit + print 'Making release commit...' + `git commit -s -m 'Release #{options.tag}'` + error 'Could not make release commit' unless $CHILD_STATUS.success? + end + + def create_tag + print "Creating tag '#{options.tag}'..." + `git tag '#{options.tag}'` + if $CHILD_STATUS.success? + puts 'OK' + else + error "Could not create tag '#{options.tag}'" + end + end + + def push_release_commit_and_tag + print "Pushing branch '#{options.branch}' to remote..." + `git push --tags --set-upstream '#{options.remote}' '#{options.branch}' > /dev/null 2>&1` + if $CHILD_STATUS.success? + puts 'OK' + else + error 'Could not push release commit' + end + end + + def changelog + @changelog ||= begin + print "Generating changelog..." + pwd = FileUtils.pwd + from = options.current_tag + to = options.next_tag + command = "docker run --rm --volume '#{pwd}:/worktree' changelog-rs '#{from}' '#{to}'" + changelog = `#{command}` + if $CHILD_STATUS.success? + puts 'OK' + changelog.rstrip.lines[1..].join + else + error 'Could not generate the changelog' + end + end + end + + def create_github_release + Tempfile.create do |f| + f.write changelog + f.close + + print "Creating GitHub release '#{options.tag}'..." + tag = options.tag + `gh release create #{tag} --title 'Release #{tag}' --notes-file '#{f.path}' --target #{options.default_branch}` + if $CHILD_STATUS.success? + puts 'OK' + else + error 'Could not create release' + end + end + end + + def create_release_pull_request + Tempfile.create do |f| + f.write <<~PR + ### Your checklist for this pull request + 🚨Please review the [guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/#{options.default_branch}/CONTRIBUTING.md) to this repository. + + - [X] Ensure all commits include DCO sign-off. + - [X] Ensure that your contributions pass unit testing. + - [X] Ensure that your contributions contain documentation if applicable. + + ### Description + #{changelog} + PR + f.close + + print "Creating GitHub pull request..." + `gh pr create --title 'Release #{options.tag}' --body-file '#{f.path}' --base '#{options.default_branch}'` + if $CHILD_STATUS.success? + puts 'OK' + else + error 'Could not create release pull request' + end + end + end + + def error(message) + warn "ERROR: #{message}" + exit 1 + end + + def print(*args) + super unless options.quiet + end + + def puts(*args) + super unless options.quiet + end +end + +options = CommandLineParser.new.parse(ARGV) +ReleaseAssertions.new(options).make_assertions +ReleaseCreator.new(options).create_release diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..dce67d86 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/git.gemspec b/git.gemspec index 3fcee253..8d974e28 100644 --- a/git.gemspec +++ b/git.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rchardet', '~> 1.8' + s.add_development_dependency 'bump', '~> 0.10' s.add_development_dependency 'minitar', '~> 0.9' s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'test-unit', '~> 3.3' @@ -41,6 +42,6 @@ Gem::Specification.new do |s| # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. s.files = Dir.chdir(File.expand_path(__dir__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(tests|spec|features)/}) } + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(tests|spec|features|bin)/}) } end end From 521b8e7384cd7ccf3e6c681bd904d1744ac3d70b Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 6 Jan 2022 15:28:41 -0800 Subject: [PATCH 22/37] Release v1.10.2 (#561) --- CHANGELOG.md | 4 ++++ lib/git/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c8ad209..2c1d27e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ # Change Log +## v1.10.2 + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.2 + ## 1.10.1 See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.1 diff --git a/lib/git/version.rb b/lib/git/version.rb index 89ef1017..54ebae36 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.10.1' + VERSION='1.10.2' end From 291ca0946bec7164b90ad5c572ac147f512c7159 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 13 Apr 2022 09:54:39 -0700 Subject: [PATCH 23/37] Address command line injection in Git::Lib#fetch Signed-off-by: James Couball --- lib/git/lib.rb | 7 ++-- tests/units/test_remotes.rb | 81 +++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 2d6c129d..765362bb 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -875,14 +875,15 @@ def tag(name, *opts) command('tag', arr_opts) end - def fetch(remote, opts) - arr_opts = [remote] - arr_opts << opts[:ref] if opts[:ref] + arr_opts = [] arr_opts << '--tags' if opts[:t] || opts[:tags] arr_opts << '--prune' if opts[:p] || opts[:prune] arr_opts << '--unshallow' if opts[:unshallow] arr_opts << '--depth' << opts[:depth] if opts[:depth] + arr_opts << '--' + arr_opts << remote + arr_opts << opts[:ref] if opts[:ref] command('fetch', arr_opts) end diff --git a/tests/units/test_remotes.rb b/tests/units/test_remotes.rb index 3e90c3b5..cc547f8b 100644 --- a/tests/units/test_remotes.rb +++ b/tests/units/test_remotes.rb @@ -23,29 +23,29 @@ def test_add_remote assert(local.remotes.map{|b| b.name}.include?('testremote2')) local.add_remote('testremote3', remote, :track => 'master') - - assert(local.branches.map{|b| b.full}.include?('master')) #We actually a new branch ('test_track') on the remote and track that one intead. + + assert(local.branches.map{|b| b.full}.include?('master')) #We actually a new branch ('test_track') on the remote and track that one intead. assert(local.remotes.map{|b| b.name}.include?('testremote3')) - end + end end def test_remove_remote_remove in_temp_dir do |path| local = Git.clone(@wbare, 'local') remote = Git.clone(@wbare, 'remote') - + local.add_remote('testremote', remote) local.remove_remote('testremote') - + assert(!local.remotes.map{|b| b.name}.include?('testremote')) local.add_remote('testremote', remote) local.remote('testremote').remove - + assert(!local.remotes.map{|b| b.name}.include?('testremote')) end end - + def test_set_remote_url in_temp_dir do |path| local = Git.clone(@wbare, 'local') @@ -65,33 +65,33 @@ def test_remote_fun in_temp_dir do |path| loc = Git.clone(@wbare, 'local') rem = Git.clone(@wbare, 'remote') - + r = loc.add_remote('testrem', rem) Dir.chdir('remote') do new_file('test-file1', 'blahblahblah1') rem.add rem.commit('master commit') - + rem.branch('testbranch').in_branch('tb commit') do new_file('test-file3', 'blahblahblah3') rem.add - true + true end end assert(!loc.status['test-file1']) assert(!loc.status['test-file3']) - + r.fetch - r.merge + r.merge assert(loc.status['test-file1']) - + loc.merge(loc.remote('testrem').branch('testbranch')) - assert(loc.status['test-file3']) - + assert(loc.status['test-file3']) + #puts loc.remotes.map { |r| r.to_s }.inspect - - #r.remove + + #r.remove #puts loc.remotes.inspect end end @@ -123,18 +123,37 @@ def test_fetch end end + def test_fetch_command_injection + test_file = 'VULNERABILITY_EXISTS' + vulnerability_exists = false + in_temp_dir do |_path| + git = Git.init('test_project') + origin = "--upload-pack=touch #{test_file};" + begin + git.fetch(origin, { ref: 'some/ref/head' }) + rescue Git::GitExecuteError + # This is expected + else + raise 'Expected Git::GitExecuteError to be raised' + end + + vulnerability_exists = File.exist?(test_file) + end + assert(!vulnerability_exists) + end + def test_fetch_ref_adds_ref_option in_temp_dir do |path| loc = Git.clone(@wbare, 'local') rem = Git.clone(@wbare, 'remote', :config => 'receive.denyCurrentBranch=ignore') loc.add_remote('testrem', rem) - + loc.chdir do new_file('test-file1', 'gonnaCommitYou') loc.add loc.commit('master commit 1') first_commit_sha = loc.log.first.sha - + new_file('test-file2', 'gonnaCommitYouToo') loc.add loc.commit('master commit 2') @@ -146,16 +165,16 @@ def test_fetch_ref_adds_ref_option # Make sure fetch message only has the second commit when we fetch the second commit assert(loc.fetch('origin', {:ref => second_commit_sha}).include?(second_commit_sha)) - assert(!loc.fetch('origin', {:ref => second_commit_sha}).include?(first_commit_sha)) - end + assert(!loc.fetch('origin', {:ref => second_commit_sha}).include?(first_commit_sha)) + end end end - + def test_push in_temp_dir do |path| loc = Git.clone(@wbare, 'local') rem = Git.clone(@wbare, 'remote', :config => 'receive.denyCurrentBranch=ignore') - + loc.add_remote('testrem', rem) loc.chdir do @@ -163,29 +182,29 @@ def test_push loc.add loc.commit('master commit') loc.add_tag('test-tag') - + loc.branch('testbranch').in_branch('tb commit') do new_file('test-file3', 'blahblahblah3') loc.add - true + true end end assert(!rem.status['test-file1']) assert(!rem.status['test-file3']) - + loc.push('testrem') - assert(rem.status['test-file1']) - assert(!rem.status['test-file3']) + assert(rem.status['test-file1']) + assert(!rem.status['test-file3']) assert_raise Git::GitTagNameDoesNotExist do rem.tag('test-tag') end - + loc.push('testrem', 'testbranch', true) rem.checkout('testbranch') - assert(rem.status['test-file1']) - assert(rem.status['test-file3']) + assert(rem.status['test-file1']) + assert(rem.status['test-file3']) assert(rem.tag('test-tag')) end end From c04d16ee74cb3ee259b98f0062e162a4eee92a9c Mon Sep 17 00:00:00 2001 From: Vern Burton Date: Wed, 13 Apr 2022 14:56:58 -0500 Subject: [PATCH 24/37] remove from maintainer (#567) Signed-off-by: Vern Burton --- MAINTAINERS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 2d8ac7b1..ef13361f 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -10,5 +10,4 @@ When making changes in this repository, one of the maintainers below must review ### Maintainers * [Per Lundberg](https://github.com/perlun) -* [Vern Burton](https://github.com/tarcinil) * [James Couball](https://github.com/jcouball) \ No newline at end of file From 018d919a6fe57f863e652a556b2e0dcf9bc47e9b Mon Sep 17 00:00:00 2001 From: James Fairbairn <4-Eyes@users.noreply.github.com> Date: Thu, 14 Apr 2022 08:08:01 +1200 Subject: [PATCH 25/37] Fix bug when grepping lines that contain numbers surrounded by colons (#566) Signed-off-by: James Fairbairn --- lib/git/lib.rb | 2 +- tests/files/working/colon_numbers.txt | 1 + tests/files/working/dot_git/index | Bin 352 -> 526 bytes tests/files/working/dot_git/logs/HEAD | 1 + .../working/dot_git/logs/refs/heads/git_grep | 1 + .../46/abbf07e3c564c723c7c039a43ab3a39e5d02dd | 1 + .../55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d | Bin 0 -> 158 bytes .../e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373 | Bin 0 -> 69 bytes .../files/working/dot_git/refs/heads/git_grep | 2 +- .../dot_git/refs/tags/grep_colon_numbers | 1 + tests/units/test_describe.rb | 4 +-- tests/units/test_lib.rb | 9 +++++-- tests/units/test_log.rb | 24 +++++++++--------- tests/units/test_status.rb | 4 ++- 14 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 tests/files/working/colon_numbers.txt create mode 100644 tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd create mode 100644 tests/files/working/dot_git/objects/55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d create mode 100644 tests/files/working/dot_git/objects/e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373 create mode 100644 tests/files/working/dot_git/refs/tags/grep_colon_numbers diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 765362bb..cd13aa54 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -419,7 +419,7 @@ def grep(string, opts = {}) hsh = {} command_lines('grep', grep_opts).each do |line| - if m = /(.*)\:(\d+)\:(.*)/.match(line) + if m = /(.*?)\:(\d+)\:(.*)/.match(line) hsh[m[1]] ||= [] hsh[m[1]] << [m[2].to_i, m[3]] end diff --git a/tests/files/working/colon_numbers.txt b/tests/files/working/colon_numbers.txt new file mode 100644 index 00000000..e76778b7 --- /dev/null +++ b/tests/files/working/colon_numbers.txt @@ -0,0 +1 @@ +Grep regex doesn't like this:4342: because it is bad diff --git a/tests/files/working/dot_git/index b/tests/files/working/dot_git/index index ef22be73369fe2742f17b338c3e9017511ad4112..9896710a283289dc577f4196d6ec40026e0a2a25 100644 GIT binary patch delta 371 zcmaFB)W=fq;u+-3z`(!+#H>kr2Udyem|q6cMg0HT(-?tbjA1)YFfcSOVPIhV3REKk z#HP>FE4CZ3ZMeJOZrDrJn&@xa?iv>}2qx#}1E!P68`iP zMuW}s741VZZ=$}gN|IjbZOKckO<^=xS%9`8k}{Er4LT5Yx57Vz)d6X!I%O1f7oh4M zaI1sW0cof@6=Zdj6&Q7jLV{dff&K|&FjX+(3O)UA{(a3`-E-Hlp0)`L2>lndMz=UQ zzodl0NWp;1RpHR=j<~ryH{7?WUR_ZjS|`hJIJF`^C9{aZ5Gb8p$$KYmwZX@p)puEi hU;Mar;L_!{0kQHgT>V>P^Hv1 1378910110 -0400 commit (merge): Merge commit '4ce44a75510cbfe200b131fdbcc56a86f1b2dc08' into cherry faf8d899a0f123c3c5def10857920be1c930e8ed 5e392652a881999392c2757cf9b783c5d47b67f7 Scott Chacon 1378910135 -0400 checkout: moving from cherry to master 5e392652a881999392c2757cf9b783c5d47b67f7 5e53019b3238362144c2766f02a2c00d91fcc023 Scott Chacon 1378910138 -0400 checkout: moving from master to git_grep +5e53019b3238362144c2766f02a2c00d91fcc023 46abbf07e3c564c723c7c039a43ab3a39e5d02dd Scott Chacon 1647231179 +1300 commit: add example for grep with colon and numbers diff --git a/tests/files/working/dot_git/logs/refs/heads/git_grep b/tests/files/working/dot_git/logs/refs/heads/git_grep index 0123a146..22a6f143 100644 --- a/tests/files/working/dot_git/logs/refs/heads/git_grep +++ b/tests/files/working/dot_git/logs/refs/heads/git_grep @@ -3,3 +3,4 @@ a3db7143944dcfa006fefe7fb49c48793cb29ade 34a566d193dc4702f03149969a2aad1443231560 scott Chacon 1194632975 -0800 commit: modified to not show up 34a566d193dc4702f03149969a2aad1443231560 935badc874edd62a8629aaf103418092c73f0a56 scott Chacon 1194633382 -0800 commit: more search help 935badc874edd62a8629aaf103418092c73f0a56 5e53019b3238362144c2766f02a2c00d91fcc023 scott Chacon 1194720731 -0800 commit: diff test +5e53019b3238362144c2766f02a2c00d91fcc023 46abbf07e3c564c723c7c039a43ab3a39e5d02dd Scott Chacon 1647231179 +1300 commit: add example for grep with colon and numbers diff --git a/tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd b/tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd new file mode 100644 index 00000000..9675e231 --- /dev/null +++ b/tests/files/working/dot_git/objects/46/abbf07e3c564c723c7c039a43ab3a39e5d02dd @@ -0,0 +1 @@ +xQj0DSH+A('XVG0<I-ezS"YƜ2ėe#K9сuq/>&9lQMe𳯵ᶲ+_!| ӌ޹9»֚Aal 7=Àdz,/RL \ No newline at end of file diff --git a/tests/files/working/dot_git/objects/55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d b/tests/files/working/dot_git/objects/55/cbfe9fdf29da8b9dac05cb3c515055fe52ac2d new file mode 100644 index 0000000000000000000000000000000000000000..8ea983cf5d1e6d05d6a6776e1e921df6377f0eab GIT binary patch literal 158 zcmV;P0Ac@l0V^p=O;s>7v1BkbFfcPQQAp0u$vo{1$3jYDHph zK~5^zoZjQJo+oDQm(%r)oqjd#)246d9ymkHDNfEWDPeF`I5fK>Ztl(v_id_IR}_fW M$ub-U02SXuthmBTz5oCK literal 0 HcmV?d00001 diff --git a/tests/files/working/dot_git/objects/e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373 b/tests/files/working/dot_git/objects/e7/6778b73006b0dda0dd56e9257c5bf6b6dd3373 new file mode 100644 index 0000000000000000000000000000000000000000..28df1dc013314799f6595566aad255a4c4327684 GIT binary patch literal 69 zcmV-L0J{Hp0ZYosPf{>7W^gY`El?;*O;4>*NXbtv&QmW@$jQu3RVc~GEVeQ+HZihN bNJ>pkEG true}), 'v2.8') + assert_equal(@git.describe(nil, {:tags => true}), 'grep_colon_numbers') end end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index 8c8c420d..d589eac6 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -19,7 +19,7 @@ def test_fetch_unshallow git = Git.clone("file://#{@wdir}", "shallow", path: dir, depth: 1).lib assert_equal(1, git.log_commits.length) git.fetch("file://#{@wdir}", unshallow: true) - assert_equal(71, git.log_commits.length) + assert_equal(72, git.log_commits.length) end end @@ -282,10 +282,15 @@ def test_grep match = @lib.grep('search', :object => 'gitsearch1', :invert_match => true) assert_equal(6, match['gitsearch1:scott/text.txt'].size) assert_equal(2, match.size) + + match = @lib.grep('Grep', :object => 'grep_colon_numbers') + assert_equal("Grep regex doesn't like this:4342: because it is bad", match['grep_colon_numbers:colon_numbers.txt'].first[1]) + assert_equal(1, match.size) end def test_show - assert(/^commit 5e53019b3238362144c2766f02a2c00d91fcc023.+\+replace with new text - diff test$/m.match(@lib.show)) + puts @lib.show + assert_match(/^commit 46abbf07e3c564c723c7c039a43ab3a39e5d02dd.+\+Grep regex doesn't like this:4342: because it is bad\n$/m, @lib.show) assert(/^commit 935badc874edd62a8629aaf103418092c73f0a56.+\+nothing!$/m.match(@lib.show('gitsearch1'))) assert(/^hello.+nothing!$/m.match(@lib.show('gitsearch1', 'scott/text.txt'))) assert(@lib.show('gitsearch1', 'scott/text.txt') == "hello\nthis is\na file\nthat is\nput here\nto search one\nto search two\nnothing!\n") diff --git a/tests/units/test_log.rb b/tests/units/test_log.rb index 81c6e300..4a947842 100644 --- a/tests/units/test_log.rb +++ b/tests/units/test_log.rb @@ -12,13 +12,13 @@ def setup def test_get_fisrt_and_last_entries log = @git.log assert(log.first.is_a?(Git::Object::Commit)) - assert_equal('5e53019b3238362144c2766f02a2c00d91fcc023', log.first.objectish) + assert_equal('46abbf07e3c564c723c7c039a43ab3a39e5d02dd', log.first.objectish) assert(log.last.is_a?(Git::Object::Commit)) - assert_equal('f1410f8735f6f73d3599eb9b5cdd2fb70373335c', log.last.objectish) + assert_equal('b03003311ad3fa368b475df58390353868e13c91', log.last.objectish) end - - def test_get_log_entries + + def test_get_log_entries assert_equal(30, @git.log.size) assert_equal(50, @git.log(50).size) assert_equal(10, @git.log(10).size) @@ -35,15 +35,15 @@ def test_log_skip assert_equal(three2.sha, three3.sha) assert_equal(three1.sha, three2.sha) end - + def test_get_log_since l = @git.log.since("2 seconds ago") assert_equal(0, l.size) - + l = @git.log.since("#{Date.today.year - 2006} years ago") assert_equal(30, l.size) end - + def test_get_log_grep l = @git.log.grep("search") assert_equal(2, l.size) @@ -55,11 +55,11 @@ def test_get_log_author l = @git.log(5).author("lazySusan") assert_equal(0, l.size) end - - def test_get_log_since_file + + def test_get_log_since_file l = @git.log.path('example.txt') assert_equal(30, l.size) - + l = @git.log.between('v2.5', 'test').path('example.txt') assert_equal(1, l.size) end @@ -72,7 +72,7 @@ def test_get_log_path log = @git.log.path(['example.txt','scott/text.txt']) assert_equal(30, log.size) end - + def test_log_file_noexist assert_raise Git::GitExecuteError do @git.log.object('no-exist.txt').size @@ -96,5 +96,5 @@ def test_log_cherry l = @git.log.between( 'master', 'cherry').cherry assert_equal( 1, l.size ) end - + end diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 2a2e7836..964a59ae 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -12,7 +12,9 @@ def setup def test_status_pretty in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') - string = "ex_dir/ex.txt\n\tsha(r) \n\tsha(i) e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 " \ + string = "colon_numbers.txt\n\tsha(r) \n\tsha(i) " \ + "e76778b73006b0dda0dd56e9257c5bf6b6dd3373 100644\n\ttype \n\tstage 0\n\tuntrac \n" \ + "ex_dir/ex.txt\n\tsha(r) \n\tsha(i) e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 " \ "100644\n\ttype \n\tstage 0\n\tuntrac \nexample.txt\n\tsha(r) \n\tsha(i) " \ "8dc79ae7616abf1e2d4d5d97d566f2b2f6cee043 100644\n\ttype \n\tstage 0\n\tuntrac " \ "\nscott/newfile\n\tsha(r) \n\tsha(i) 5d4606820736043f9eed2a6336661d6892c820a5 " \ From 19dfe5eb097f06e3445491cf6794ca522272bb3c Mon Sep 17 00:00:00 2001 From: Dirk Heinrichs Date: Wed, 13 Apr 2022 23:27:07 +0200 Subject: [PATCH 26/37] Add support for fetch options "--force/-f" and "--prune-tags/-P". (#563) Signed-off-by: Dirk Heinrichs --- lib/git/lib.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index cd13aa54..0fdae6f8 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -879,6 +879,8 @@ def fetch(remote, opts) arr_opts = [] 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 << '--' From 292087efabc8423c3cf616d78fac5311d58e7425 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 13 Apr 2022 15:54:58 -0700 Subject: [PATCH 27/37] Supress unneeded test output (#570) Signed-off-by: James Couball --- tests/units/test_git_dir.rb | 2 +- tests/units/test_lib.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/units/test_git_dir.rb b/tests/units/test_git_dir.rb index 551b0f34..8034d859 100644 --- a/tests/units/test_git_dir.rb +++ b/tests/units/test_git_dir.rb @@ -84,7 +84,7 @@ def test_git_diff_to_a Dir.chdir(work_tree) do `git init` `git commit --allow-empty -m 'init'` - `git worktree add child` + `git worktree add --quiet child` Dir.chdir('child') do result = Git.open('.').diff.to_a assert_equal([], result) diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index d589eac6..bdf50a75 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -289,7 +289,6 @@ def test_grep end def test_show - puts @lib.show assert_match(/^commit 46abbf07e3c564c723c7c039a43ab3a39e5d02dd.+\+Grep regex doesn't like this:4342: because it is bad\n$/m, @lib.show) assert(/^commit 935badc874edd62a8629aaf103418092c73f0a56.+\+nothing!$/m.match(@lib.show('gitsearch1'))) assert(/^hello.+nothing!$/m.match(@lib.show('gitsearch1', 'scott/text.txt'))) From 546bc038ff6604f8304fdeca738c2a7c20cbacc8 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 17 Apr 2022 16:19:19 -0700 Subject: [PATCH 28/37] Release v1.11.0 Signed-off-by: James Couball --- CHANGELOG.md | 11 +++++++++++ lib/git/version.rb | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c1d27e4..a08297c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ # Change Log +## v1.11.0 + +* 292087e Supress unneeded test output (#570) +* 19dfe5e Add support for fetch options "--force/-f" and "--prune-tags/-P". (#563) +* 018d919 Fix bug when grepping lines that contain numbers surrounded by colons (#566) +* c04d16e remove from maintainer (#567) +* 291ca09 Address command line injection in Git::Lib#fetch +* 521b8e7 Release v1.10.2 (#561) + +See https://github.com/ruby-git/ruby-git/releases/tag/v1.11.0 + ## v1.10.2 See https://github.com/ruby-git/ruby-git/releases/tag/v1.10.2 diff --git a/lib/git/version.rb b/lib/git/version.rb index 54ebae36..87bffb51 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.10.2' + VERSION='1.11.0' end From 0a43d8b848b2454f822315a3239e8884b0293c30 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 17 Apr 2022 17:28:55 -0700 Subject: [PATCH 29/37] 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 30/37] 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 31/37] 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 32/37] 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 33/37] 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 34/37] 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 35/37] 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 2fea684fa3e899f5fbddcf4aec12f5be04e1f504 Mon Sep 17 00:00:00 2001 From: Oleg Pudeyev Date: Mon, 14 Jun 2021 14:42:13 -0400 Subject: [PATCH 36/37] Add :push option to remote_set_url. This allows specifying, for example: git.set_remote_url('upstream', 'git@github.com:foo/bar', push: true) --- lib/git/base.rb | 7 +++++-- lib/git/lib.rb | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index 2d931cf3..c58bafe8 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -402,11 +402,14 @@ def add_remote(name, url, opts = {}) # sets the url for a remote # url can be a git url or a Git::Base object if it's a local reference # + # accepts options: + # :push + # # @git.set_remote_url('scotts_git', 'git://repo.or.cz/rubygit.git') # - def set_remote_url(name, url) + def set_remote_url(name, url, opts = {}) url = url.repo.path if url.is_a?(Git::Base) - self.lib.remote_set_url(name, url) + self.lib.remote_set_url(name, url, opts) Git::Remote.new(self, name) end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index cb408246..907c7196 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -830,10 +830,13 @@ def remote_add(name, url, opts = {}) command('remote', arr_opts) end - def remote_set_url(name, url) + # accepts options: + # :push + def remote_set_url(name, url, opts = {}) arr_opts = ['set-url'] arr_opts << name arr_opts << url + arr_opts << '--push' if opts[:push] command('remote', arr_opts) end From bc0d2d14d5e9aa2f44d050683c49f27a42a2ddbb Mon Sep 17 00:00:00 2001 From: Oleg Pudeyev Date: Thu, 20 May 2021 15:10:41 -0400 Subject: [PATCH 37/37] Support --submodule option to git diff. To include submodule diff in top-level diff: @git.diff(from, to, submodule: 'diff') Signed-off-by: Oleg Pudeyev --- lib/git/base/factory.rb | 4 +- lib/git/diff.rb | 5 +- lib/git/lib.rb | 7 + tests/files/submod | 1 + tests/files/submod.git/HEAD | 1 + tests/files/submod.git/config | 4 + tests/files/submod.git/description | 1 + .../submod.git/hooks/applypatch-msg.sample | 15 ++ .../files/submod.git/hooks/commit-msg.sample | 24 +++ .../hooks/fsmonitor-watchman.sample | 173 ++++++++++++++++++ .../files/submod.git/hooks/post-update.sample | 8 + .../submod.git/hooks/pre-applypatch.sample | 14 ++ .../files/submod.git/hooks/pre-commit.sample | 49 +++++ .../submod.git/hooks/pre-merge-commit.sample | 13 ++ tests/files/submod.git/hooks/pre-push.sample | 53 ++++++ .../files/submod.git/hooks/pre-rebase.sample | 169 +++++++++++++++++ .../files/submod.git/hooks/pre-receive.sample | 24 +++ .../hooks/prepare-commit-msg.sample | 42 +++++ .../submod.git/hooks/push-to-checkout.sample | 78 ++++++++ tests/files/submod.git/hooks/update.sample | 128 +++++++++++++ tests/files/submod.git/info/exclude | 6 + .../20/77d7e4f28649f836b1107a07562ff5e0a7375c | Bin 0 -> 58 bytes .../26/3384e25d3a245c98766579193e11583bd8c053 | Bin 0 -> 39 bytes .../4b/825dc642cb6eb9a060e54bf8d69288fbee4904 | Bin 0 -> 15 bytes .../61/0955cb4abecf174f9591e8e79dc722747c2ed0 | 2 + .../e6/e2eb76562f22fb29d1cb07be1d4c35b04c4898 | Bin 0 -> 130 bytes tests/files/submod.git/refs/heads/master | 1 + tests/files/top | 1 + tests/units/test_diff_submodule.rb | 30 +++ 29 files changed, 849 insertions(+), 4 deletions(-) create mode 160000 tests/files/submod create mode 100644 tests/files/submod.git/HEAD create mode 100644 tests/files/submod.git/config create mode 100644 tests/files/submod.git/description create mode 100755 tests/files/submod.git/hooks/applypatch-msg.sample create mode 100755 tests/files/submod.git/hooks/commit-msg.sample create mode 100755 tests/files/submod.git/hooks/fsmonitor-watchman.sample create mode 100755 tests/files/submod.git/hooks/post-update.sample create mode 100755 tests/files/submod.git/hooks/pre-applypatch.sample create mode 100755 tests/files/submod.git/hooks/pre-commit.sample create mode 100755 tests/files/submod.git/hooks/pre-merge-commit.sample create mode 100755 tests/files/submod.git/hooks/pre-push.sample create mode 100755 tests/files/submod.git/hooks/pre-rebase.sample create mode 100755 tests/files/submod.git/hooks/pre-receive.sample create mode 100755 tests/files/submod.git/hooks/prepare-commit-msg.sample create mode 100755 tests/files/submod.git/hooks/push-to-checkout.sample create mode 100755 tests/files/submod.git/hooks/update.sample create mode 100644 tests/files/submod.git/info/exclude create mode 100644 tests/files/submod.git/objects/20/77d7e4f28649f836b1107a07562ff5e0a7375c create mode 100644 tests/files/submod.git/objects/26/3384e25d3a245c98766579193e11583bd8c053 create mode 100644 tests/files/submod.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904 create mode 100644 tests/files/submod.git/objects/61/0955cb4abecf174f9591e8e79dc722747c2ed0 create mode 100644 tests/files/submod.git/objects/e6/e2eb76562f22fb29d1cb07be1d4c35b04c4898 create mode 100644 tests/files/submod.git/refs/heads/master create mode 160000 tests/files/top create mode 100644 tests/units/test_diff_submodule.rb diff --git a/lib/git/base/factory.rb b/lib/git/base/factory.rb index 7b601306..fd35a130 100644 --- a/lib/git/base/factory.rb +++ b/lib/git/base/factory.rb @@ -32,8 +32,8 @@ def commit_tree(tree = nil, opts = {}) end # @return [Git::Diff] a Git::Diff object - def diff(objectish = 'HEAD', obj2 = nil) - Git::Diff.new(self, objectish, obj2) + def diff(objectish = 'HEAD', obj2 = nil, **opts) + Git::Diff.new(self, objectish, obj2, **opts) end # @return [Git::Object] a Git object diff --git a/lib/git/diff.rb b/lib/git/diff.rb index d40ddce4..78f02745 100644 --- a/lib/git/diff.rb +++ b/lib/git/diff.rb @@ -4,10 +4,11 @@ module Git class Diff include Enumerable - def initialize(base, from = nil, to = nil) + def initialize(base, from = nil, to = nil, **opts) @base = base @from = from && from.to_s @to = to && to.to_s + @opts = opts @path = nil @full_diff = nil @@ -101,7 +102,7 @@ def blob(type = :dst) private def cache_full - @full_diff ||= @base.lib.diff_full(@from, @to, {:path_limiter => @path}) + @full_diff ||= @base.lib.diff_full(@from, @to, {:path_limiter => @path}.update(**@opts)) end def process_full diff --git a/lib/git/lib.rb b/lib/git/lib.rb index cb408246..4bae518b 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -432,6 +432,9 @@ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) diff_opts << obj1 diff_opts << obj2 if obj2.is_a?(String) diff_opts << '--' << opts[:path_limiter] if opts[:path_limiter].is_a? String + if v = opts[:submodule] + diff_opts << "--submodule=#{v}" + end command('diff', diff_opts) end @@ -1086,6 +1089,10 @@ def command(cmd, *opts, &block) end global_opts = [] + if cmd == 'diff' + # For submodule diffs --work-tree alone is insufficient and -C is needed. + global_opts << "-C" << @git_work_dir if !@git_work_dir.nil? + end global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil? global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil? global_opts << %w[-c core.quotePath=true] diff --git a/tests/files/submod b/tests/files/submod new file mode 160000 index 00000000..610955cb --- /dev/null +++ b/tests/files/submod @@ -0,0 +1 @@ +Subproject commit 610955cb4abecf174f9591e8e79dc722747c2ed0 diff --git a/tests/files/submod.git/HEAD b/tests/files/submod.git/HEAD new file mode 100644 index 00000000..cb089cd8 --- /dev/null +++ b/tests/files/submod.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/files/submod.git/config b/tests/files/submod.git/config new file mode 100644 index 00000000..07d359d0 --- /dev/null +++ b/tests/files/submod.git/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/tests/files/submod.git/description b/tests/files/submod.git/description new file mode 100644 index 00000000..498b267a --- /dev/null +++ b/tests/files/submod.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/files/submod.git/hooks/applypatch-msg.sample b/tests/files/submod.git/hooks/applypatch-msg.sample new file mode 100755 index 00000000..a5d7b84a --- /dev/null +++ b/tests/files/submod.git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/tests/files/submod.git/hooks/commit-msg.sample b/tests/files/submod.git/hooks/commit-msg.sample new file mode 100755 index 00000000..b58d1184 --- /dev/null +++ b/tests/files/submod.git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/tests/files/submod.git/hooks/fsmonitor-watchman.sample b/tests/files/submod.git/hooks/fsmonitor-watchman.sample new file mode 100755 index 00000000..14ed0aa4 --- /dev/null +++ b/tests/files/submod.git/hooks/fsmonitor-watchman.sample @@ -0,0 +1,173 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + } + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $last_update_token, + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/tests/files/submod.git/hooks/post-update.sample b/tests/files/submod.git/hooks/post-update.sample new file mode 100755 index 00000000..ec17ec19 --- /dev/null +++ b/tests/files/submod.git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/tests/files/submod.git/hooks/pre-applypatch.sample b/tests/files/submod.git/hooks/pre-applypatch.sample new file mode 100755 index 00000000..4142082b --- /dev/null +++ b/tests/files/submod.git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/tests/files/submod.git/hooks/pre-commit.sample b/tests/files/submod.git/hooks/pre-commit.sample new file mode 100755 index 00000000..e144712c --- /dev/null +++ b/tests/files/submod.git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/tests/files/submod.git/hooks/pre-merge-commit.sample b/tests/files/submod.git/hooks/pre-merge-commit.sample new file mode 100755 index 00000000..399eab19 --- /dev/null +++ b/tests/files/submod.git/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/tests/files/submod.git/hooks/pre-push.sample b/tests/files/submod.git/hooks/pre-push.sample new file mode 100755 index 00000000..4ce688d3 --- /dev/null +++ b/tests/files/submod.git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/tests/files/submod.git/hooks/pre-rebase.sample b/tests/files/submod.git/hooks/pre-rebase.sample new file mode 100755 index 00000000..6cbef5c3 --- /dev/null +++ b/tests/files/submod.git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/tests/files/submod.git/hooks/pre-receive.sample b/tests/files/submod.git/hooks/pre-receive.sample new file mode 100755 index 00000000..a1fd29ec --- /dev/null +++ b/tests/files/submod.git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/tests/files/submod.git/hooks/prepare-commit-msg.sample b/tests/files/submod.git/hooks/prepare-commit-msg.sample new file mode 100755 index 00000000..10fa14c5 --- /dev/null +++ b/tests/files/submod.git/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/tests/files/submod.git/hooks/push-to-checkout.sample b/tests/files/submod.git/hooks/push-to-checkout.sample new file mode 100755 index 00000000..af5a0c00 --- /dev/null +++ b/tests/files/submod.git/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/tests/files/submod.git/info/exclude b/tests/files/submod.git/info/exclude new file mode 100644 index 00000000..a5196d1b --- /dev/null +++ b/tests/files/submod.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/files/submod.git/objects/20/77d7e4f28649f836b1107a07562ff5e0a7375c b/tests/files/submod.git/objects/20/77d7e4f28649f836b1107a07562ff5e0a7375c new file mode 100644 index 0000000000000000000000000000000000000000..10f9b7b34ac546950c50a45ffcc4115af48c2cc2 GIT binary patch literal 58 zcmb>0iBJ}2?8+?1^rGH+kmo{OZE~Fu>)=4HrZ3buo82M-5an2Ki|O2OIpgU z!7$OLRS|F;tj~#@O-#aZMndt9myjvi5Ed435uKUWcGU`}yIkPBX8Fhq_S)BxpZTG0 ksk@}*00Y^