From 56783e7d2381a40f8e05136ec4fce345a8c1b246 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 09:30:12 -0700 Subject: [PATCH 01/72] Update create_github_release dependency so pre-releases can be made Signed-off-by: James Couball --- git.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git.gemspec b/git.gemspec index 63042f0a..ea257473 100644 --- a/git.gemspec +++ b/git.gemspec @@ -32,7 +32,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'process_executer', '~> 1.1' s.add_runtime_dependency 'rchardet', '~> 1.8' - s.add_development_dependency 'create_github_release', '~> 1.3' + s.add_development_dependency 'create_github_release', '~> 1.4' s.add_development_dependency 'minitar', '~> 0.9' s.add_development_dependency 'mocha', '~> 2.1' s.add_development_dependency 'rake', '~> 13.1' From d6543aae80aaca0d315ba579e914a5324d206460 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 09:47:18 -0700 Subject: [PATCH 02/72] Release v2.0.0.pre4 Signed-off-by: James Couball --- CHANGELOG.md | 16 ++++++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f20ddb3..14c0a2ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ # Change Log +## v2.0.0.pre4 (2024-05-10) + +[Full Changelog](https://jcouball@github.com/ruby-git/ruby-git/compare/v2.0.0.pre3..v2.0.0.pre4) + +Changes since v2.0.0.pre3: + +* 56783e7 Update create_github_release dependency so pre-releases can be made +* 8566929 Add dependency on create_github_release gem used for releasing the git gem +* 7376d76 Refactor errors that are raised by this gem +* 7e99b17 Update documentation for new timeout functionality +* 705e983 Move experimental builds to a separate workflow that only runs when pushed to master +* e056d64 Build with jruby-head on Windows until jruby/jruby#7515 is fixed +* ec7c257 Remove unneeded scripts to create a new release +* d9570ab Move issue and pull request templates to the .github directory +* e4d6a77 Show log(x).since combination in README + ## v2.0.0.pre3 (2024-03-15) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.0.pre2..v2.0.0.pre3) diff --git a/lib/git/version.rb b/lib/git/version.rb index 35580479..791f22ce 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='2.0.0.pre3' + VERSION='2.0.0.pre4' end From efb724b3258f50f6e067ce86f2a155aed384413a Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 15:54:41 -0700 Subject: [PATCH 03/72] Remove the DCO requirement for commits Signed-off-by: James Couball --- .github/pull_request_template.md | 14 ++--- CONTRIBUTING.md | 89 ++++++++------------------------ 2 files changed, 29 insertions(+), 74 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index dc470a6e..5ee909d1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,9 @@ -### Your checklist for this pull request -🚨Please review the [guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/master/CONTRIBUTING.md) to this repository. +[Guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/master/CONTRIBUTING.md) to this repository -- [ ] Ensure all commits include DCO sign-off. -- [ ] Ensure that your contributions pass unit testing. -- [ ] Ensure that your contributions contain documentation if applicable. +A good start is to: + +* Ensure that your changes pass CI tests by running `rake` before pushing +* Ensure that your changes are documented in the README.md and in YARD documentation + +# Description -### Description -Please describe your pull request. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b9d7bf9..636f9c4b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,10 +40,6 @@ There is three step process for code or documentation changes: Make your changes in a fork of the ruby-git repository. -Each commit must include a [DCO sign-off](#developer-certificate-of-origin-dco) -by adding the line `Signed-off-by: Name ` to the end of the commit -message. - ### Create a pull request See [this article](https://help.github.com/articles/about-pull-requests/) if you @@ -71,15 +67,18 @@ request meets [the project's coding standards](#coding-standards). In order to ensure high quality, all pull requests must meet these requirements: ### 1 PR = 1 Commit - * All commits for a PR must be squashed into one commit - * To avoid an extra merge commit, the PR must be able to be merged as [a fast forward merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) - * The easiest way to ensure a fast forward merge is to rebase your local branch - to the ruby-git master branch + +* All commits for a PR must be squashed into one commit +* To avoid an extra merge commit, the PR must be able to be merged as [a fast forward + merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) +* The easiest way to ensure a fast forward merge is to rebase your local branch to + the ruby-git master branch ### Unit tests - * All changes must be accompanied by new or modified unit tests - * The entire test suite must pass when `bundle exec rake default` is run from the - project's local working copy. + +* All changes must be accompanied by new or modified unit tests +* The entire test suite must pass when `bundle exec rake default` is run from the + project's local working copy. While working on specific features you can run individual test files or a group of tests using `bin/test`: @@ -94,20 +93,21 @@ a group of tests using `bin/test`: $ bin/test ### Continuous integration - * All tests must pass in the project's [GitHub Continuous Integration build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) - before the pull request will be merged. - * The [Continuous Integration workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml) - runs both `bundle exec rake default` and `bundle exec rake test:gem` from the project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile). + +* All tests must pass in the project's [GitHub Continuous Integration + build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) before the + pull request will be merged. +* The [Continuous Integration + workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml) + runs both `bundle exec rake default` and `bundle exec rake test:gem` from the + project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile). ### Documentation - * New and updated public methods must have [YARD](https://yardoc.org/) - documentation added to them - * New and updated public facing features should be documented in the project's - [README.md](README.md) -### Licensing sign-off - * Each commit must contain [the DCO sign-off](#developer-certificate-of-origin-dco) - in the form: `Signed-off-by: Name ` +* New and updated public methods must have [YARD](https://yardoc.org/) documentation + added to them +* New and updated public facing features should be documented in the project's + [README.md](README.md) ## Licensing @@ -116,48 +116,3 @@ declared in the [LICENSE](LICENSE) file. Licensing is very important to open source projects. It helps ensure the software continues to be available under the terms that the author desired. - -### Developer Certificate of Origin (DCO) - -This project requires that authors have permission to submit their contributions -under the MIT license. To make a good faith effort to ensure this, ruby-git -requires the [Developer Certificate of Origin (DCO)](https://elinux.org/Developer_Certificate_Of_Origin) -process be followed. - -This process requires that each commit include a `Signed-off-by` line that -indicates the author accepts the DCO. Here is an example DCO sign-off line: - -``` -Signed-off-by: John Doe -``` - -The full text of the DCO version 1.1 is below or at . - -``` -Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -(b) The contribution is based upon previous work that, to the - best of my knowledge, is covered under an appropriate open - source license and I have the right under that license to - submit that work with modifications, whether created in whole - or in part by me, under the same open source license (unless - I am permitted to submit under a different license), as - Indicated in the file; or - -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including - all personal information I submit with it, including my - sign-off) is maintained indefinitely and may be redistributed - consistent with this project or the open source license(s) - involved. -``` From 299ae6b3c3271f2cf9b763a49433798939d11c2e Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 16:47:39 -0700 Subject: [PATCH 04/72] Remove stale bot integration --- .github/stale.yml | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 .github/stale.yml diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index b56852af..00000000 --- a/.github/stale.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Probot: Stale -# https://github.com/probot/stale - -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 - -# Number of days of inactivity before a stale issue is closed -# Set to false to disable. If disabled, issues still need to be closed -# manually, but will remain marked as stale. -daysUntilClose: false - -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security - -# Label to use when marking an issue as stale -staleLabel: stale - -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - A friendly reminder that this issue had no activity for 60 days. - -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false From ed52420875f326f3bd1340a4e5c27ef5f50e5e2f Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 17:06:22 -0700 Subject: [PATCH 05/72] Make the pull request template more concise --- .github/pull_request_template.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5ee909d1..63e23392 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,9 +1,8 @@ -[Guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/master/CONTRIBUTING.md) to this repository +Review our [guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/master/CONTRIBUTING.md) to this repository. A good start is to: -A good start is to: - -* Ensure that your changes pass CI tests by running `rake` before pushing -* Ensure that your changes are documented in the README.md and in YARD documentation +* Write tests for your changes +* Run `rake` before pushing +* Include / update docs in the README.md and in YARD documentation # Description From 1afc4c64e85e05751c97b79f37d582519e1d703a Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 17:18:34 -0700 Subject: [PATCH 06/72] Update 2.x release line description --- README.md | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 23efa669..a6a3c203 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![Code Climate](https://codeclimate.com/github/ruby-git/ruby-git.png)](https://codeclimate.com/github/ruby-git/ruby-git) * [Summary](#summary) -* [v2.0.0 pre-release](#v200-pre-release) +* [v2.x Release](#v2x-release) * [Install](#install) * [Major Objects](#major-objects) * [Errors Raised By This Gem](#errors-raised-by-this-gem) @@ -37,15 +37,20 @@ Get started by obtaining a repository object by: Methods that can be called on a repository object are documented in [Git::Base](https://rubydoc.info/gems/git/Git/Base) -## v2.0.0 pre-release +## v2.x Release -git 2.0.0 is available as a pre-release version for testing! Please give it a try. +git 2.0.0 has recently been released. Please give it a try. + + +**If you have problems with the 2.x release, open an issue and use the 1.9.1 version +instead.** We will do our best to fix your issues in a timely fashion. **JRuby on Windows is not yet supported by the 2.x release line. Users running JRuby on Windows should continue to use the 1.x release line.** -The changes coming in this major release include: +The changes in this major release include: +* Added a dependency on the activesupport gem to use the deprecation functionality * Create a policy of supported Ruby versions to support only non-EOL Ruby versions * Create a policy of supported Git CLI versions (released 2020-12-25) * Update the required Ruby version to at least 3.0 (released 2020-07-27) @@ -55,9 +60,6 @@ The changes coming in this major release include: See [PR #684](https://github.com/ruby-git/ruby-git/pull/684) for more details on the motivation for this implementation. -The tentative plan is to release `2.0.0` near the end of March 2024 depending on -the feedback received during the pre-release period. - The `master` branch will be used for `2.x` development. If needed, fixes for `1.x` version will be done on the `v1` branch. @@ -69,12 +71,24 @@ Install the gem and add to the application's Gemfile by executing: bundle add git ``` +to install version 1.x: + +```shell +bundle add git --version "~> 1.19" +``` + If bundler is not being used to manage dependencies, install the gem by executing: ```shell gem install git ``` +to install version 1.x: + +```shell +gem install git --version "~> 1.19" +``` + ## Major Objects **Git::Base** - The object returned from a `Git.open` or `Git.clone`. Most major actions are called from this object. @@ -505,9 +519,15 @@ end This gem will be expected to function correctly on: * All non-EOL versions of the MRI Ruby on Mac, Linux, and Windows -* The latest version of JRuby on Linux and Windows +* The latest version of JRuby on Linux * The latest version of Truffle Ruby on Linus +It is this project's intent to support the latest version of JRuby on Windows +once the following JRuby bug is fixed: + +jruby/jruby#7515 + ## License -licensed under MIT License Copyright (c) 2008 Scott Chacon. See LICENSE for further details. +Licensed under MIT License Copyright (c) 2008 Scott Chacon. See LICENSE for further +details. From 28224a18dd7ca1d9c6cfd7492b0e39479902756a Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 17:21:30 -0700 Subject: [PATCH 07/72] Release v2.0.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 14c0a2ea..72851251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ # Change Log +## v2.0.0 (2024-05-10) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.0.pre4..v2.0.0) + +Changes since v2.0.0.pre4: + +* 1afc4c6 Update 2.x release line description +* ed52420 Make the pull request template more concise +* 299ae6b Remove stale bot integration +* efb724b Remove the DCO requirement for commits + ## v2.0.0.pre4 (2024-05-10) [Full Changelog](https://jcouball@github.com/ruby-git/ruby-git/compare/v2.0.0.pre3..v2.0.0.pre4) diff --git a/lib/git/version.rb b/lib/git/version.rb index 791f22ce..c8463646 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='2.0.0.pre4' + VERSION='2.0.0' end From 6a59bc86992e834bb642f004385a929e58bb2bdb Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 15 May 2024 17:23:11 -0700 Subject: [PATCH 08/72] Remove the Git::Base::Factory module --- lib/git/base.rb | 93 ++++++++++++++++++++++++++++++++++++-- lib/git/base/factory.rb | 99 ----------------------------------------- 2 files changed, 90 insertions(+), 102 deletions(-) delete mode 100644 lib/git/base/factory.rb diff --git a/lib/git/base.rb b/lib/git/base.rb index 90575e74..056029a4 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -1,4 +1,3 @@ -require 'git/base/factory' require 'logger' require 'open3' @@ -10,8 +9,6 @@ module Git # {Git.clone}, or {Git.bare}. # class Base - include Git::Base::Factory - # (see Git.bare) def self.bare(git_dir, options = {}) normalize_paths(options, default_repository: git_dir, bare: true) @@ -632,6 +629,96 @@ def current_branch self.lib.branch_current end + # @return [Git::Branch] an object for branch_name + def branch(branch_name = self.current_branch) + Git::Branch.new(self, branch_name) + end + + # @return [Git::Branches] a collection of all the branches in the repository. + # Each branch is represented as a {Git::Branch}. + def branches + Git::Branches.new(self) + end + + # returns a Git::Worktree object for dir, commitish + def worktree(dir, commitish = nil) + Git::Worktree.new(self, dir, commitish) + end + + # returns a Git::worktrees object of all the Git::Worktrees + # objects for this repo + def worktrees + Git::Worktrees.new(self) + end + + # @return [Git::Object::Commit] a commit object + def commit_tree(tree = nil, opts = {}) + Git::Object::Commit.new(self, self.lib.commit_tree(tree, opts)) + end + + # @return [Git::Diff] a Git::Diff object + def diff(objectish = 'HEAD', obj2 = nil) + Git::Diff.new(self, objectish, obj2) + end + + # @return [Git::Object] a Git object + def gblob(objectish) + Git::Object.new(self, objectish, 'blob') + end + + # @return [Git::Object] a Git object + def gcommit(objectish) + Git::Object.new(self, objectish, 'commit') + end + + # @return [Git::Object] a Git object + def gtree(objectish) + Git::Object.new(self, objectish, 'tree') + end + + # @return [Git::Log] a log with the specified number of commits + def log(count = 30) + Git::Log.new(self, count) + end + + # returns a Git::Object of the appropriate type + # you can also call @git.gtree('tree'), but that's + # just for readability. If you call @git.gtree('HEAD') it will + # still return a Git::Object::Commit object. + # + # object calls a method that will run a rev-parse + # on the objectish and determine the type of the object and return + # an appropriate object for that type + # + # @return [Git::Object] an instance of the appropriate type of Git::Object + def object(objectish) + Git::Object.new(self, objectish) + end + + # @return [Git::Remote] a remote of the specified name + def remote(remote_name = 'origin') + Git::Remote.new(self, remote_name) + end + + # @return [Git::Status] a status object + def status + Git::Status.new(self) + end + + # @return [Git::Object::Tag] a tag object + def tag(tag_name) + Git::Object.new(self, tag_name, 'tag', true) + end + + # Find as good common ancestors as possible for a merge + # example: g.merge_base('master', 'some_branch', 'some_sha', octopus: true) + # + # @return [Array] a collection of common ancestors + def merge_base(*args) + shas = self.lib.merge_base(*args) + shas.map { |sha| gcommit(sha) } + end + private # Normalize options before they are sent to Git::Base.new diff --git a/lib/git/base/factory.rb b/lib/git/base/factory.rb deleted file mode 100644 index 25cb1090..00000000 --- a/lib/git/base/factory.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Git - - class Base - - module Factory - # @return [Git::Branch] an object for branch_name - def branch(branch_name = self.current_branch) - Git::Branch.new(self, branch_name) - end - - # @return [Git::Branches] a collection of all the branches in the repository. - # Each branch is represented as a {Git::Branch}. - def branches - Git::Branches.new(self) - end - - # returns a Git::Worktree object for dir, commitish - def worktree(dir, commitish = nil) - Git::Worktree.new(self, dir, commitish) - end - - # returns a Git::worktrees object of all the Git::Worktrees - # objects for this repo - def worktrees - Git::Worktrees.new(self) - end - - # @return [Git::Object::Commit] a commit object - def commit_tree(tree = nil, opts = {}) - Git::Object::Commit.new(self, self.lib.commit_tree(tree, opts)) - end - - # @return [Git::Diff] a Git::Diff object - def diff(objectish = 'HEAD', obj2 = nil) - Git::Diff.new(self, objectish, obj2) - end - - # @return [Git::Object] a Git object - def gblob(objectish) - Git::Object.new(self, objectish, 'blob') - end - - # @return [Git::Object] a Git object - def gcommit(objectish) - Git::Object.new(self, objectish, 'commit') - end - - # @return [Git::Object] a Git object - def gtree(objectish) - Git::Object.new(self, objectish, 'tree') - end - - # @return [Git::Log] a log with the specified number of commits - def log(count = 30) - Git::Log.new(self, count) - end - - # returns a Git::Object of the appropriate type - # you can also call @git.gtree('tree'), but that's - # just for readability. If you call @git.gtree('HEAD') it will - # still return a Git::Object::Commit object. - # - # object calls a factory method that will run a rev-parse - # on the objectish and determine the type of the object and return - # an appropriate object for that type - # - # @return [Git::Object] an instance of the appropriate type of Git::Object - def object(objectish) - Git::Object.new(self, objectish) - end - - # @return [Git::Remote] a remote of the specified name - def remote(remote_name = 'origin') - Git::Remote.new(self, remote_name) - end - - # @return [Git::Status] a status object - def status - Git::Status.new(self) - end - - # @return [Git::Object::Tag] a tag object - def tag(tag_name) - Git::Object.new(self, tag_name, 'tag', true) - end - - # Find as good common ancestors as possible for a merge - # example: g.merge_base('master', 'some_branch', 'some_sha', octopus: true) - # - # @return [Array] a collection of common ancestors - def merge_base(*args) - shas = self.lib.merge_base(*args) - shas.map { |sha| gcommit(sha) } - end - end - - end - -end From 712fdaddc1131e19427e06377d2dbeaca25f6d42 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 15 May 2024 18:38:42 -0700 Subject: [PATCH 09/72] Fix Git::Status#untracked when run from worktree subdir --- lib/git/lib.rb | 3 +++ lib/git/status.rb | 8 +----- tests/units/test_status.rb | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index bfb1c66d..85d7a929 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -600,6 +600,9 @@ def ignored_files command_lines('ls-files', '--others', '-i', '--exclude-standard') end + def untracked_files + command_lines('ls-files', '--others', '--exclude-standard', chdir: @git_work_dir) + end def config_remote(name) hsh = {} diff --git a/lib/git/status.rb b/lib/git/status.rb index 3f741bfd..113a6423 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -170,13 +170,7 @@ def construct_status end def fetch_untracked - ignore = @base.lib.ignored_files - - root_dir = @base.dir.path - Dir.glob('**/*', File::FNM_DOTMATCH, base: root_dir) do |file| - next if @files[file] || File.directory?(File.join(root_dir, file)) || - ignore.include?(file) || file =~ %r{^.git\/.+} - + @base.lib.untracked_files.each do |file| @files[file] = { path: file, untracked: true } end end diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 043f2fef..584e5a6a 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -87,6 +87,56 @@ def test_deleted_boolean end end + def test_untracked + in_temp_dir do |path| + `git init` + File.write('file1', 'contents1') + File.write('file2', 'contents2') + Dir.mkdir('subdir') + File.write('subdir/file3', 'contents3') + File.write('subdir/file4', 'contents4') + `git add file1 subdir/file3` + `git commit -m "my message"` + + git = Git.open('.') + assert_equal(2, git.status.untracked.size) + assert_equal(['file2', 'subdir/file4'], git.status.untracked.keys) + end + end + + def test_untracked_no_untracked_files + in_temp_dir do |path| + `git init` + File.write('file1', 'contents1') + Dir.mkdir('subdir') + File.write('subdir/file3', 'contents3') + `git add file1 subdir/file3` + `git commit -m "my message"` + + git = Git.open('.') + assert_equal(0, git.status.untracked.size) + end + end + + def test_untracked_from_subdir + in_temp_dir do |path| + `git init` + File.write('file1', 'contents1') + File.write('file2', 'contents2') + Dir.mkdir('subdir') + File.write('subdir/file3', 'contents3') + File.write('subdir/file4', 'contents4') + `git add file1 subdir/file3` + `git commit -m "my message"` + + Dir.chdir('subdir') do + git = Git.open('..') + assert_equal(2, git.status.untracked.size) + assert_equal(['file2', 'subdir/file4'], git.status.untracked.keys) + end + end + end + def test_untracked_boolean in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') From c8a77db9a515ba951892711291212e2c1f703088 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 15 May 2024 19:57:17 -0700 Subject: [PATCH 10/72] Fix Git::Base#status on an empty repo --- lib/git/lib.rb | 13 +++++++++++++ lib/git/status.rb | 8 +++++--- tests/units/test_lib.rb | 21 +++++++++++++++++++++ tests/units/test_status.rb | 10 ++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 85d7a929..8551e7b4 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -707,6 +707,19 @@ def rm(path = '.', opts = {}) command('rm', *arr_opts) end + # Returns true if the repository is empty (meaning it has no commits) + # + # @return [Boolean] + # + def empty? + command('rev-parse', '--verify', 'HEAD') + false + rescue Git::FailedError => e + raise unless e.result.status.exitstatus == 128 && + e.result.stderr == 'fatal: Needed a single revision' + true + end + # Takes the commit message with the options and executes the commit command # # accepts options: diff --git a/lib/git/status.rb b/lib/git/status.rb index 113a6423..0362dcbd 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -183,9 +183,11 @@ def fetch_modified end def fetch_added - # find added but not committed - new files - @base.lib.diff_index('HEAD').each do |path, data| - @files[path] ? @files[path].merge!(data) : @files[path] = data + unless @base.lib.empty? + # find added but not committed - new files + @base.lib.diff_index('HEAD').each do |path, data| + @files[path] ? @files[path].merge!(data) : @files[path] = data + end end end end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index 9cf52923..a2bb067e 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -318,4 +318,25 @@ def test_compare_version_to assert lib.compare_version_to(2, 43, 0) == -1 assert lib.compare_version_to(3, 0, 0) == -1 end + + def test_empty_when_not_empty + in_temp_dir do |path| + `git init` + `touch file1` + `git add file1` + `git commit -m "my commit message"` + + git = Git.open('.') + assert_false(git.lib.empty?) + end + end + + def test_empty_when_empty + in_temp_dir do |path| + `git init` + + git = Git.open('.') + assert_true(git.lib.empty?) + end + end end diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 584e5a6a..6e790626 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -25,6 +25,16 @@ def test_status_pretty end end + def test_on_empty_repo + in_temp_dir do |path| + `git init` + git = Git.open('.') + assert_nothing_raised do + git.status + end + end + end + def test_dot_files_status in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') From da435b1352c3241fab6b9a4af1e9bfb6e6b956a0 Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 21 May 2024 09:30:48 -0700 Subject: [PATCH 11/72] Document and add tests for Git::Status --- README.md | 10 +- lib/git/status.rb | 92 ++- tests/units/test_status.rb | 39 ++ tests/units/test_status_object.rb | 615 ++++++++++++++++++ tests/units/test_status_object_empty_repo.rb | 629 +++++++++++++++++++ 5 files changed, 1369 insertions(+), 16 deletions(-) create mode 100644 tests/units/test_status_object.rb create mode 100644 tests/units/test_status_object_empty_repo.rb diff --git a/README.md b/README.md index a6a3c203..e627e1ff 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,8 @@ ## Summary -The [git gem](https://rubygems.org/gems/git) provides an API that can be used to -create, read, and manipulate Git repositories by wrapping system calls to the `git` -command line. The API can be used for working with Git in complex interactions -including branching and merging, object inspection and manipulation, history, patch -generation and more. +The [git gem](https://rubygems.org/gems/git) provides a Ruby interface to the `git` +command line. Get started by obtaining a repository object by: @@ -41,8 +38,7 @@ Methods that can be called on a repository object are documented in [Git::Base]( git 2.0.0 has recently been released. Please give it a try. - -**If you have problems with the 2.x release, open an issue and use the 1.9.1 version +**If you have problems with the 2.x release, open an issue and use the 1.x version instead.** We will do our best to fix your issues in a timely fashion. **JRuby on Windows is not yet supported by the 2.x release line. Users running JRuby diff --git a/lib/git/status.rb b/lib/git/status.rb index 0362dcbd..d31dc7b4 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -1,6 +1,12 @@ module Git + # The status class gets the status of a git repository # - # A class for git status + # This identifies which files have been modified, added, or deleted from the + # worktree. Untracked files are also identified. + # + # The Status object is an Enumerable that contains StatusFile objects. + # + # @api public # class Status include Enumerable @@ -31,7 +37,6 @@ def changed?(file) changed.member?(file) end - # # Returns an Enumerable containing files that have been added. # File path starts at git base directory # @@ -40,8 +45,8 @@ def added @files.select { |_k, f| f.type == 'A' } end - # # Determines whether the given file has been added to the repository + # # File path starts at git base directory # # @param file [String] The name of the file. @@ -126,9 +131,63 @@ def each(&block) # subclass that does heavy lifting class StatusFile - attr_accessor :path, :type, :stage, :untracked - attr_accessor :mode_index, :mode_repo - attr_accessor :sha_index, :sha_repo + # @!attribute [r] path + # The path of the file relative to the project root directory + # @return [String] + attr_accessor :path + + # @!attribute [r] type + # The type of change + # + # * 'M': modified + # * 'A': added + # * 'D': deleted + # * nil: ??? + # + # @return [String] + attr_accessor :type + + # @!attribute [r] mode_index + # The mode of the file in the index + # @return [String] + # @example 100644 + # + attr_accessor :mode_index + + # @!attribute [r] mode_repo + # The mode of the file in the repo + # @return [String] + # @example 100644 + # + attr_accessor :mode_repo + + # @!attribute [r] sha_index + # The sha of the file in the index + # @return [String] + # @example 123456 + # + attr_accessor :sha_index + + # @!attribute [r] sha_repo + # The sha of the file in the repo + # @return [String] + # @example 123456 + attr_accessor :sha_repo + + # @!attribute [r] untracked + # Whether the file is untracked + # @return [Boolean] + attr_accessor :untracked + + # @!attribute [r] stage + # The stage of the file + # + # * '0': the unmerged state + # * '1': the common ancestor (or original) version + # * '2': "our version" from the current branch head + # * '3': "their version" from the other branch head + # @return [String] + attr_accessor :stage def initialize(base, hash) @base = base @@ -158,10 +217,19 @@ def blob(type = :index) private def construct_status + # Lists all files in the index and the worktree + # git ls-files --stage + # { file => { path: file, mode_index: '100644', sha_index: 'dd4fc23', stage: '0' } } @files = @base.lib.ls_files + # Lists files in the worktree that are not in the index + # Add untracked files to @files fetch_untracked + + # Lists files that are different between the index vs. the worktree fetch_modified + + # Lists files that are different between the repo HEAD vs. the worktree fetch_added @files.each do |k, file_hash| @@ -170,13 +238,17 @@ def construct_status end def fetch_untracked + # git ls-files --others --exclude-standard, chdir: @git_work_dir) + # { file => { path: file, untracked: true } } @base.lib.untracked_files.each do |file| @files[file] = { path: file, untracked: true } end end def fetch_modified - # find modified in tree + # Files changed between the index vs. the worktree + # git diff-files + # { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } } @base.lib.diff_files.each do |path, data| @files[path] ? @files[path].merge!(data) : @files[path] = data end @@ -184,8 +256,10 @@ def fetch_modified def fetch_added unless @base.lib.empty? - # find added but not committed - new files - @base.lib.diff_index('HEAD').each do |path, data| + # Files changed between the repo HEAD vs. the worktree + # git diff-index HEAD + # { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } } + @base.lib.diff_index('HEAD').each do |path, data| @files[path] ? @files[path].merge!(data) : @files[path] = data end end diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 6e790626..b7ad4888 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -35,6 +35,45 @@ def test_on_empty_repo end end + def test_added + in_temp_dir do |path| + `git init` + File.write('file1', 'contents1') + File.write('file2', 'contents2') + `git add file1 file2` + `git commit -m "my message"` + + File.write('file2', 'contents2B') + File.write('file3', 'contents3') + + `git add file2 file3` + + git = Git.open('.') + status = assert_nothing_raised do + git.status + end + + assert_equal(1, status.added.size) + assert_equal(['file3'], status.added.keys) + end + end + + def test_added_on_empty_repo + in_temp_dir do |path| + `git init` + File.write('file1', 'contents1') + File.write('file2', 'contents2') + `git add file1 file2` + + git = Git.open('.') + status = assert_nothing_raised do + git.status + end + + assert_equal(0, status.added.size) + end + end + def test_dot_files_status in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') diff --git a/tests/units/test_status_object.rb b/tests/units/test_status_object.rb new file mode 100644 index 00000000..ee343cb6 --- /dev/null +++ b/tests/units/test_status_object.rb @@ -0,0 +1,615 @@ +require 'rbconfig' +require 'securerandom' +require 'test_helper' + +module Git + # Add methods to the Status class to make it easier to test + class Status + def size + @files.size + end + + alias count size + + def files + @files + end + end +end + +# A suite of tests for the Status class for the following scenarios +# +# For all tests, the initial state of the repo is one commit with the following +# files: +# +# * { path: 'file1', content: 'contents1', mode: '100644' } +# * { path: 'file2', content: 'contents2', mode: '100755' } +# +# Assume the repo is cloned to a temporary directory (`worktree_path`) and the +# index and worktree are in a clean state before each test. +# +# Assume the Status object is initialized with `base` which is a Git object created +# via `Git.open(worktree_path)`. +# +# Test that the status object returns the expected #files +# +class TestStatusObject < Test::Unit::TestCase + def logger + # Change log level to Logger::DEBUG to see the log entries + @logger ||= Logger.new(STDOUT, level: Logger::ERROR) + end + + def test_no_changes + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_delete_file1_from_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.delete('file1') + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 1d5ec91c189281dbbd97a00451815c8ae288c512 + # # branch.head main + # 1 .D N... 100644 100644 000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + + # When + + status = git.status + + # Then + + # ERROR: mode_index and sha_indes for file1 is not returned + + expected_status_files = [ + { + path: 'file1', type: 'D', stage: '0', untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_delete_file1_from_index + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + `git rm file1` + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 9a6c20a5ca26595796ff5c2ef6e6a806ae4427f3 + # # branch.head main + # 1 D. N... 100644 000000 000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec 0000000000000000000000000000000000000000 file1 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: 'D', stage: nil, untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_delete_file1_from_index_and_recreate_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + `git rm file1` + File.open('file1', 'w', 0o644) { |f| f.write('does_not_matter') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 9a6c20a5ca26595796ff5c2ef6e6a806ae4427f3 + # # branch.head main + # 1 D. N... 100644 000000 000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec 0000000000000000000000000000000000000000 file1 + # ? file1 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: 'D', stage: nil, untracked: true, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 1d5ec91c189281dbbd97a00451815c8ae288c512 + # # branch.head main + # 1 .M N... 100644 100644 100644 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + + # When + + status = git.status + + # Then + + # ERROR: sha_index for file1 is not returned + + expected_status_files = [ + { + path: 'file1', type: 'M', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree_and_add_to_index + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content') } + `git add file1` + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 1d5ec91c189281dbbd97a00451815c8ae288c512 + # # branch.head main + # 1 M. N... 100644 100644 100644 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec c6190329af2f07c1a949128b8e962c06eb23cfa4 file1 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: 'M', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: 'c6190329af2f07c1a949128b8e962c06eb23cfa4', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree_and_add_to_index_and_modify_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content1') } + `git add file1` + File.open('file1', 'w', 0o644) { |f| f.write('updated_content2') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 1d5ec91c189281dbbd97a00451815c8ae288c512 + # # branch.head main + # 1 MM N... 100644 100644 100644 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec a9114691c7e7d6139fa9558897eeda2c8cb2cd81 file1 + + # When + + status = git.status + + # Then + + # ERROR: there shouldn't be a mode_repo or sha_repo for file1 + + expected_status_files = [ + { + path: 'file1', type: 'M', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree_and_add_to_index_and_delete_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content1') } + `git add file1` + File.delete('file1') + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 1d5ec91c189281dbbd97a00451815c8ae288c512 + # # branch.head main + # 1 MD N... 100644 100644 000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec a9114691c7e7d6139fa9558897eeda2c8cb2cd81 file1 + + # When + + status = git.status + + # Then + + # ERROR: Impossible to tell that a change to file1 was already staged and the delete happened in the worktree + + expected_status_files = [ + { + path: 'file1', type: 'D', stage: '0', untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 9a6c20a5ca26595796ff5c2ef6e6a806ae4427f3 + # # branch.head main + # ? file3 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: nil, stage: nil, untracked: true, + mode_index: nil, sha_index: nil, + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree_and_index + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + `git add file3` + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 9a6c20a5ca26595796ff5c2ef6e6a806ae4427f3 + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 a2b32293aab475bf50798c7642f0fe0593c167f6 file3 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: 'A', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: 'a2b32293aab475bf50798c7642f0fe0593c167f6', + mode_repo: '000000', sha_repo: '0000000000000000000000000000000000000000' + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree_and_index_and_modify_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + `git add file3` + File.open('file3', 'w', 0o644) { |f| f.write('updated_content3') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 9a6c20a5ca26595796ff5c2ef6e6a806ae4427f3 + # # branch.head main + # 1 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 a2b32293aab475bf50798c7642f0fe0593c167f6 file3 + + # When + + status = git.status + + # Then + + # ERROR: the sha_mode and sha_index for file3 is not correct below + + # ERROR: impossible to tell that file3 was modified in the worktree + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: 'A', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '0000000000000000000000000000000000000000', + mode_repo: '000000', sha_repo: '0000000000000000000000000000000000000000' + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + # * Add { path: 'file3', content: 'content3', mode: expect_read_write_mode } to the worktree, add + # file3 to the index, delete file3 in the worktree [DONE] + def test_add_file3_to_worktree_and_index_and_delete_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + `git add file3` + File.delete('file3') + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 9a6c20a5ca26595796ff5c2ef6e6a806ae4427f3 + # # branch.head main + # 1 AD N... 000000 100644 000000 0000000000000000000000000000000000000000 a2b32293aab475bf50798c7642f0fe0593c167f6 file3 + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: 'D', stage: '0', untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: 'a2b32293aab475bf50798c7642f0fe0593c167f6' + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + private + + def setup_worktree(worktree_path) + `git init` + File.open('file1', 'w', 0o644) { |f| f.write('contents1') } + File.open('file2', 'w', 0o755) { |f| f.write('contents2') } + `git add file1 file2` + `git commit -m "Initial commit"` + end + + # Generate a unique string to use as file content + def random_content + SecureRandom.uuid + end + + def assert_has_attributes(expected_attrs, object) + expected_attrs.each do |expected_attr, expected_value| + assert_equal(expected_value, object.send(expected_attr), "The #{expected_attr} attribute does not match") + end + end + + def assert_has_status_files(expected_status_files, status_files) + assert_equal(expected_status_files.count, status_files.count) + + expected_status_files.each do |expected_status_file| + status_file = status_files[expected_status_file[:path]] + assert_not_nil(status_file, "Status for file #{expected_status_file[:path]} not found") + assert_has_attributes(expected_status_file, status_file) + end + end + + def log_git_status + logger.debug do + <<~LOG_ENTRY + + ========== + #{self.class.name} + #{caller[3][/`([^']*)'/, 1].split.last} + ---------- + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + #{`git status --porcelain=v2 --untracked-files=all --branch`.split("\n").map { |line| " # #{line}" }.join("\n")} + ========== + + LOG_ENTRY + end + end + + def expect_read_write_mode + '100644' + end + + def expect_execute_mode + windows? ? expect_read_write_mode : '100755' + end + + def windows? + RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ + end +end diff --git a/tests/units/test_status_object_empty_repo.rb b/tests/units/test_status_object_empty_repo.rb new file mode 100644 index 00000000..4a8c366c --- /dev/null +++ b/tests/units/test_status_object_empty_repo.rb @@ -0,0 +1,629 @@ +require 'rbconfig' +require 'securerandom' +require 'test_helper' + +module Git + # Add methods to the Status class to make it easier to test + class Status + def size + @files.size + end + + alias count size + + def files + @files + end + end +end + +# This is the same suite of tests as TestStatusObject, but the repo has no commits. +# The worktree and index are setup with the same files as TestStatusObject, but the +# repo is in a clean state with no commits. +# +class TestStatusObjectEmptyRepo < Test::Unit::TestCase + def logger + # Change log level to Logger::DEBUG to see the log entries + @logger ||= Logger.new(STDOUT, level: Logger::ERROR) + end + + def test_no_changes + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid 45bcb25ceb9c69b66337d63e2c1c5b520d8a003d + # # branch.head main + + # When + + status = git.status + + # Then + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_delete_file1_from_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.delete('file1') + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 AD N... 000000 100644 000000 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + # ERROR: mode_index/shw_index are switched with mod_repo/sha_repo + + expected_status_files = [ + { + path: 'file1', type: 'D', stage: '0', untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_delete_file1_from_index + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + `git rm -f file1` + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + # ERROR: file2 type should be 'A' + + expected_status_files = [ + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_delete_file1_from_index_and_recreate_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + `git rm -f file1` + File.open('file1', 'w', 0o644) { |f| f.write('does_not_matter') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + # ? file1 + + # When + + status = git.status + + # Then + + # ERROR: file2 type should be 'A' + + expected_status_files = [ + { + path: 'file1', type: nil, stage: nil, untracked: true, + mode_index: nil, sha_index: nil, + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + # ERROR: file1 sha_index is not returned as sha_repo + # ERROR: file1 sha_repo/sha_index should be zeros + + expected_status_files = [ + { + path: 'file1', type: 'M', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree_and_add_to_index + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content') } + `git add file1` + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 c6190329af2f07c1a949128b8e962c06eb23cfa4 file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + # ERROR: file1 type should be 'A' + # ERROR: file2 type should be 'A' + # ERROR: file1 and file2 mode_repo/show_repo should be zeros instead of nil + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: 'c6190329af2f07c1a949128b8e962c06eb23cfa4', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree_and_add_to_index_and_modify_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content1') } + `git add file1` + File.open('file1', 'w', 0o644) { |f| f.write('updated_content2') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 a9114691c7e7d6139fa9558897eeda2c8cb2cd81 file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + # ERROR: file1 mode_repo and sha_repo should be zeros + # ERROR: file1 sha_index is not set to the actual sha + # ERROR: impossible to tell that file1 was added to the index and modified in the worktree + # ERROR: file2 type should be 'A' + + expected_status_files = [ + { + path: 'file1', type: 'M', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: 'a9114691c7e7d6139fa9558897eeda2c8cb2cd81' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_modify_file1_in_worktree_and_add_to_index_and_delete_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file1', 'w', 0o644) { |f| f.write('updated_content1') } + `git add file1` + File.delete('file1') + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 AD N... 000000 100644 000000 0000000000000000000000000000000000000000 a9114691c7e7d6139fa9558897eeda2c8cb2cd81 file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + + # When + + status = git.status + + # Then + + # ERROR: impossible to tell that file1 was added to the index + # ERROR: file1 sha_index/sha_repo are swapped + # ERROR: file1 mode_repo should be all zeros + # ERROR: impossible to tell that file1 or file2 was added to the index and are not in the repo + # ERROR: inconsistent use of all zeros (in file1) and nils (in file2) + + expected_status_files = [ + { + path: 'file1', type: 'D', stage: '0', untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: 'a9114691c7e7d6139fa9558897eeda2c8cb2cd81' + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + # ? file3 + + # When + + status = git.status + + # Then + + # ERROR: hard to tell that file1 and file2 were aded to the index but are not in the repo + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: nil, stage: nil, untracked: true, + mode_index: nil, sha_index: nil, + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree_and_index + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + `git add file3` + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 a2b32293aab475bf50798c7642f0fe0593c167f6 file3 + + # When + + status = git.status + + # Then + + # WARNING: hard to tell that file1/file2/file3 were added to the index but are not in the repo + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: 'a2b32293aab475bf50798c7642f0fe0593c167f6', + mode_repo: nil, sha_repo: nil + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree_and_index_and_modify_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + `git add file3` + File.open('file3', 'w', 0o644) { |f| f.write('updated_content3') } + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + # 1 AM N... 000000 100644 100644 0000000000000000000000000000000000000000 a2b32293aab475bf50798c7642f0fe0593c167f6 file3 + + # When + + status = git.status + + # Then + + # WARNING: hard to tell that file3 was added to the index and is not in the repo + # ERROR: sha_index/sha_repo are swapped + # ERROR: mode_repo should be all zeros + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: 'M', stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: 'a2b32293aab475bf50798c7642f0fe0593c167f6' + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + def test_add_file3_to_worktree_and_index_and_delete_in_worktree + in_temp_dir do |worktree_path| + + # Given + + setup_worktree(worktree_path) + File.open('file3', 'w', 0o644) { |f| f.write('content3') } + `git add file3` + File.delete('file3') + git = Git.open(worktree_path) + + log_git_status + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + # # branch.oid (initial) + # # branch.head main + # 1 A. N... 000000 100644 100644 0000000000000000000000000000000000000000 146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec file1 + # 1 A. N... 000000 100755 100755 0000000000000000000000000000000000000000 c061beb85924d309fde78d996a7602544e4f69a5 file2 + # 1 AD N... 000000 100644 000000 0000000000000000000000000000000000000000 a2b32293aab475bf50798c7642f0fe0593c167f6 file3 + + # When + + status = git.status + + # Then + + # ERROR: mode_index/sha_index are switched with mod_repo/sha_repo + # WARNING: hard to tell that file3 was added to the index and deleted in the worktree + + expected_status_files = [ + { + path: 'file1', type: nil, stage: '0', untracked: nil, + mode_index: expect_read_write_mode, sha_index: '146edcbe0a35a475bd97aa6fbf83ecf8b21cfeec', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file2', type: nil, stage: '0', untracked: nil, + mode_index: expect_execute_mode, sha_index: 'c061beb85924d309fde78d996a7602544e4f69a5', + mode_repo: nil, sha_repo: nil + }, + { + path: 'file3', type: 'D', stage: '0', untracked: nil, + mode_index: '000000', sha_index: '0000000000000000000000000000000000000000', + mode_repo: expect_read_write_mode, sha_repo: 'a2b32293aab475bf50798c7642f0fe0593c167f6' + } + ] + + assert_has_status_files(expected_status_files, status.files) + end + end + + private + + def setup_worktree(worktree_path) + `git init` + File.open('file1', 'w', 0o644) { |f| f.write('contents1') } + File.open('file2', 'w', 0o755) { |f| f.write('contents2') } + `git add file1 file2` + end + + # Generate a unique string to use as file content + def random_content + SecureRandom.uuid + end + + def assert_has_attributes(expected_attrs, object) + expected_attrs.each do |expected_attr, expected_value| + assert_equal(expected_value, object.send(expected_attr), "The #{expected_attr} attribute does not match") + end + end + + def assert_has_status_files(expected_status_files, status_files) + assert_equal(expected_status_files.count, status_files.count) + + expected_status_files.each do |expected_status_file| + status_file = status_files[expected_status_file[:path]] + assert_not_nil(status_file, "Status for file #{expected_status_file[:path]} not found") + assert_has_attributes(expected_status_file, status_file) + end + end + + def log_git_status + logger.debug do + <<~LOG_ENTRY + + ========== + #{self.class.name} + #{caller[3][/`([^']*)'/, 1].split.last} + ---------- + # Output of `git status --porcelain=v2 --untracked-files=all --branch`: + # + #{`git status --porcelain=v2 --untracked-files=all --branch`.split("\n").map { |line| " # #{line}" }.join("\n")} + ========== + + LOG_ENTRY + end + end + + def expect_read_write_mode + '100644' + end + + def expect_execute_mode + windows? ? expect_read_write_mode : '100755' + end + + def windows? + RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ + end +end From 437f57f5d9f2755722fd2defe3831434a06f16dd Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 21 May 2024 09:46:49 -0700 Subject: [PATCH 12/72] Release v2.0.1 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 72851251..7b25e087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ # Change Log +## v2.0.1 (2024-05-21) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.0..v2.0.1) + +Changes since v2.0.0: + +* da435b1 Document and add tests for Git::Status +* c8a77db Fix Git::Base#status on an empty repo +* 712fdad Fix Git::Status#untracked when run from worktree subdir +* 6a59bc8 Remove the Git::Base::Factory module + ## v2.0.0 (2024-05-10) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.0.pre4..v2.0.0) diff --git a/lib/git/version.rb b/lib/git/version.rb index c8463646..6b5a3bbd 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='2.0.0' + VERSION='2.0.1' end From d84097bc2bfa4e7003551ff19d4a713ce77471c0 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 23 May 2024 17:40:39 -0700 Subject: [PATCH 13/72] Update YARDoc for a few a few method --- lib/git/base.rb | 101 ++++++++++++++++++++++++++++-------------------- lib/git/lib.rb | 41 +++++++++++++------- 2 files changed, 86 insertions(+), 56 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index 056029a4..97151c20 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -2,12 +2,14 @@ require 'open3' module Git - # Git::Base is the main public interface for interacting with Git commands. + # The main public interface for interacting with Git commands # # Instead of creating a Git::Base directly, obtain a Git::Base instance by # calling one of the follow {Git} class methods: {Git.open}, {Git.init}, # {Git.clone}, or {Git.bare}. # + # @api public + # class Base # (see Git.bare) def self.bare(git_dir, options = {}) @@ -119,6 +121,62 @@ def initialize(options = {}) @index = options[:index] ? Git::Index.new(options[:index], false) : nil end + # Update the index from the current worktree to prepare the for the next commit + # + # @example + # lib.add('path/to/file') + # lib.add(['path/to/file1','path/to/file2']) + # lib.add(all: true) + # + # @param [String, Array] paths a file or files to be added to the repository (relative to the worktree root) + # @param [Hash] options + # + # @option options [Boolean] :all Add, modify, and remove index entries to match the worktree + # @option options [Boolean] :force Allow adding otherwise ignored files + # + def add(paths = '.', **options) + self.lib.add(paths, options) + end + + # adds a new remote to this repository + # url can be a git url or a Git::Base object if it's a local reference + # + # @git.add_remote('scotts_git', 'git://repo.or.cz/rubygit.git') + # @git.fetch('scotts_git') + # @git.merge('scotts_git/master') + # + # Options: + # :fetch => true + # :track => + def add_remote(name, url, opts = {}) + url = url.repo.path if url.is_a?(Git::Base) + self.lib.remote_add(name, url, opts) + Git::Remote.new(self, name) + end + + # Create a new git tag + # + # @example + # repo.add_tag('tag_name', object_reference) + # repo.add_tag('tag_name', object_reference, {:options => 'here'}) + # repo.add_tag('tag_name', {:options => 'here'}) + # + # @param [String] name The name of the tag to add + # @param [Hash] options Opstions to pass to `git tag`. + # See [git-tag](https://git-scm.com/docs/git-tag) for more details. + # @option options [boolean] :annotate Make an unsigned, annotated tag object + # @option options [boolean] :a An alias for the `:annotate` option + # @option options [boolean] :d Delete existing tag with the given names. + # @option options [boolean] :f Replace an existing tag with the given name (instead of failing) + # @option options [String] :message Use the given tag message + # @option options [String] :m An alias for the `:message` option + # @option options [boolean] :s Make a GPG-signed tag. + # + def add_tag(name, *options) + self.lib.tag(name, *options) + self.tag(name) + end + # changes current working directory for a block # to the git working directory # @@ -251,29 +309,6 @@ def grep(string, path_limiter = nil, opts = {}) self.object('HEAD').grep(string, path_limiter, opts) end - # updates the repository index using the working directory content - # - # @example - # git.add - # git.add('path/to/file') - # git.add(['path/to/file1','path/to/file2']) - # git.add(:all => true) - # - # options: - # :all => true - # - # @param [String,Array] paths files paths to be added (optional, default='.') - # @param [Hash] options - # @option options [boolean] :all - # Update the index not only where the working tree has a file matching - # but also where the index already has an entry. - # See [the --all option to git-add](https://git-scm.com/docs/git-add#Documentation/git-add.txt--A) - # for more details. - # - def add(paths = '.', **options) - self.lib.add(paths, options) - end - # removes file(s) from the git repository def rm(path = '.', opts = {}) self.lib.rm(path, opts) @@ -434,22 +469,6 @@ def remotes self.lib.remotes.map { |r| Git::Remote.new(self, r) } end - # adds a new remote to this repository - # url can be a git url or a Git::Base object if it's a local reference - # - # @git.add_remote('scotts_git', 'git://repo.or.cz/rubygit.git') - # @git.fetch('scotts_git') - # @git.merge('scotts_git/master') - # - # Options: - # :fetch => true - # :track => - def add_remote(name, url, opts = {}) - url = url.repo.path if url.is_a?(Git::Base) - self.lib.remote_add(name, url, opts) - Git::Remote.new(self, name) - end - # sets the url for a remote # url can be a git url or a Git::Base object if it's a local reference # @@ -473,7 +492,7 @@ def tags self.lib.tags.map { |r| tag(r) } end - # Creates a new git tag (Git::Tag) + # Create a new git tag # # @example # repo.add_tag('tag_name', object_reference) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 8551e7b4..22f474e5 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -38,14 +38,23 @@ class Lib # Create a new Git::Lib object # - # @param [Git::Base, Hash] base An object that passes in values for - # @git_work_dir, @git_dir, and @git_index_file + # @overload initialize(base, logger) # - # @param [Logger] logger + # @param base [Hash] the hash containing paths to the Git working copy, + # the Git repository directory, and the Git index file. # - # @option base [Pathname] :working_directory - # @option base [Pathname] :repository - # @option base [Pathname] :index + # @option base [Pathname] :working_directory + # @option base [Pathname] :repository + # @option base [Pathname] :index + # + # @param [Logger] logger + # + # @overload initialize(base, logger) + # + # @param base [#dir, #repo, #index] an object with methods to get the Git worktree (#dir), + # the Git repository directory (#repo), and the Git index file (#index). + # + # @param [Logger] logger # def initialize(base = nil, logger = nil) @git_dir = nil @@ -670,18 +679,20 @@ def global_config_set(name, value) command('config', '--global', name, value) end - # updates the repository index using the working directory content - # - # lib.add('path/to/file') - # lib.add(['path/to/file1','path/to/file2']) - # lib.add(:all => true) + + # Update the index from the current worktree to prepare the for the next commit # - # options: - # :all => true - # :force => true + # @example + # lib.add('path/to/file') + # lib.add(['path/to/file1','path/to/file2']) + # lib.add(:all => true) # - # @param [String,Array] paths files paths to be added to the repository + # @param [String, Array] paths files to be added to the repository (relative to the worktree root) # @param [Hash] options + # + # @option options [Boolean] :all Add, modify, and remove index entries to match the worktree + # @option options [Boolean] :force Allow adding otherwise ignored files + # def add(paths='.',options={}) arr_opts = [] From 93c8210f32289d22d0e23c24a64abe3ccb22d5f1 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 31 May 2024 09:32:14 -0700 Subject: [PATCH 14/72] Add Git::Log#max_count --- README.md | 24 ++++++++++---- lib/git/log.rb | 69 ++++++++++++++++++++++++++++++++++++++--- tests/units/test_log.rb | 22 +++++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e627e1ff..841bcfcd 100644 --- a/README.md +++ b/README.md @@ -101,16 +101,28 @@ directory, in the index and in the repository. Similar to running 'git status' **Git::Remote**- A reference to a remote repository that is tracked by this repository. -**Git::Log** - An Enumerable object that references all the `Git::Object::Commit` objects that encompass your log query, which can be constructed through methods on the `Git::Log object`, -like: +**Git::Log** - An Enumerable object that references all the `Git::Object::Commit` +objects that encompass your log query, which can be constructed through methods on +the `Git::Log object`, like: - `@git.log(20).object("some_file").since("2 weeks ago").between('v2.6', 'v2.7').each { |commit| [block] }` +```ruby +git.log + .max_count(:all) + .object('README.md') + .since('10 years ago') + .between('v1.0.7', 'HEAD') + .map { |commit| commit.sha } +``` -Pass the `--all` option to `git log` as follows: +A maximum of 30 commits are returned if `max_count` is not called. To get all commits +that match the log query, call `max_count(:all)`. - `@git.log.all.each { |commit| [block] }` +Note that `git.log.all` adds the `--all` option to the underlying `git log` command. +This asks for the logs of all refs (basically all commits reachable by HEAD, +branches, and tags). This does not control the maximum number of commits returned. To +control how many commits are returned, you should call `max_count`. - **Git::Worktrees** - Enumerable object that holds `Git::Worktree objects`. +**Git::Worktrees** - Enumerable object that holds `Git::Worktree objects`. ## Errors Raised By This Gem diff --git a/lib/git/log.rb b/lib/git/log.rb index 24f68bcc..817d8635 100644 --- a/lib/git/log.rb +++ b/lib/git/log.rb @@ -1,15 +1,76 @@ module Git - # object that holds the last X commits on given branch + # Return the last n commits that match the specified criteria + # + # @example The last (default number) of commits + # git = Git.open('.') + # Git::Log.new(git) #=> Enumerable of the last 30 commits + # + # @example The last n commits + # Git::Log.new(git).max_commits(50) #=> Enumerable of last 50 commits + # + # @example All commits returned by `git log` + # Git::Log.new(git).max_count(:all) #=> Enumerable of all commits + # + # @example All commits that match complex criteria + # Git::Log.new(git) + # .max_count(:all) + # .object('README.md') + # .since('10 years ago') + # .between('v1.0.7', 'HEAD') + # + # @api public + # class Log include Enumerable - def initialize(base, count = 30) + # Create a new Git::Log object + # + # @example + # git = Git.open('.') + # Git::Log.new(git) + # + # @param base [Git::Base] the git repository object + # @param max_count [Integer, Symbol, nil] the number of commits to return, or + # `:all` or `nil` to return all + # + # Passing max_count to {#initialize} is equivalent to calling {#max_count} on the object. + # + def initialize(base, max_count = 30) dirty_log @base = base - @count = count + max_count(max_count) + end + + # The maximum number of commits to return + # + # @example All commits returned by `git log` + # git = Git.open('.') + # Git::Log.new(git).max_count(:all) + # + # @param num_or_all [Integer, Symbol, nil] the number of commits to return, or + # `:all` or `nil` to return all + # + # @return [self] + # + def max_count(num_or_all) + dirty_log + @max_count = (num_or_all == :all) ? nil : num_or_all + self end + # Adds the --all flag to the git log command + # + # This asks for the logs of all refs (basically all commits reachable by HEAD, + # branches, and tags). This does not control the maximum number of commits + # returned. To control how many commits are returned, call {#max_count}. + # + # @example Return the last 50 commits reachable by all refs + # git = Git.open('.') + # Git::Log.new(git).max_count(50).all + # + # @return [self] + # def all dirty_log @all = true @@ -119,7 +180,7 @@ def check_log # actually run the 'git log' command def run_log log = @base.lib.full_log_commits( - count: @count, all: @all, object: @object, path_limiter: @path, since: @since, + count: @max_count, all: @all, object: @object, path_limiter: @path, since: @since, author: @author, grep: @grep, skip: @skip, until: @until, between: @between, cherry: @cherry ) diff --git a/tests/units/test_log.rb b/tests/units/test_log.rb index d8b7c805..d220af03 100644 --- a/tests/units/test_log.rb +++ b/tests/units/test_log.rb @@ -9,6 +9,28 @@ def setup @git = Git.open(@wdir) end + def test_log_max_count_default + assert_equal(30, @git.log.size) + end + + # In these tests, note that @git.log(n) is equivalent to @git.log.max_count(n) + def test_log_max_count_20 + assert_equal(20, @git.log(20).size) + assert_equal(20, @git.log.max_count(20).size) + end + + def test_log_max_count_nil + assert_equal(72, @git.log(nil).size) + assert_equal(72, @git.log.max_count(nil).size) + end + + def test_log_max_count_all + assert_equal(72, @git.log(:all).size) + assert_equal(72, @git.log.max_count(:all).size) + end + + # Note that @git.log.all does not control the number of commits returned. For that, + # use @git.log.max_count(n) def test_log_all assert_equal(72, @git.log(100).size) assert_equal(76, @git.log(100).all.size) From 3d734489d596baf2588d6d0c2a693ab847ff9642 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 31 May 2024 09:47:48 -0700 Subject: [PATCH 15/72] Release v2.1.0 Signed-off-by: James Couball --- CHANGELOG.md | 9 +++++++++ lib/git/version.rb | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b25e087..c327e01d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ # Change Log +## v2.1.0 (2024-05-31) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.1..v2.1.0) + +Changes since v2.0.1: + +* 93c8210 Add Git::Log#max_count +* d84097b Update YARDoc for a few a few method + ## v2.0.1 (2024-05-21) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.0..v2.0.1) diff --git a/lib/git/version.rb b/lib/git/version.rb index 6b5a3bbd..b88d2356 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='2.0.1' + VERSION='2.1.0' end From d943bf449fe6fdbc28f9ce760180dc282fc2c2c9 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Sun, 26 May 2024 23:18:10 -0400 Subject: [PATCH 16/72] When core.ignoreCase, check for changed files case-insensitively Fixed #586. Include a note about the inconsistent behavior when ignoreCase is not set to match the case-sensitivity of the file-system itself. --- lib/git/status.rb | 16 +++++++++++++++- tests/units/test_status.rb | 8 ++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/git/status.rb b/lib/git/status.rb index d31dc7b4..83ba656a 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -34,7 +34,11 @@ def changed # changed?('lib/git.rb') # @return [Boolean] def changed?(file) - changed.member?(file) + if ignore_case? + changed.keys.map(&:downcase).include?(file.downcase) + else + changed.member?(file) + end end # Returns an Enumerable containing files that have been added. @@ -264,5 +268,15 @@ def fetch_added end end end + + # It's worth noting that (like git itself) this gem will not behave well if + # ignoreCase is set inconsistently with the file-system itself. For details: + # https://git-scm.com/docs/git-config#Documentation/git-config.txt-coreignoreCase + def ignore_case? + return @_ignore_case if defined?(@_ignore_case) + @_ignore_case = @base.config('core.ignoreCase') == 'true' + rescue Git::FailedError + @_ignore_case = false + end end end diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index b7ad4888..6065cfc9 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -106,6 +106,7 @@ def test_added_boolean def test_changed_boolean in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') + git.config('core.ignorecase', 'false') create_file('test_dot_files_status/test_file_1', 'content tets_file_1') create_file('test_dot_files_status/test_file_2', 'content tets_file_2') @@ -117,6 +118,13 @@ def test_changed_boolean assert(git.status.changed?('test_file_1')) assert(!git.status.changed?('test_file_2')) + + update_file('test_dot_files_status/scott/text.txt', 'definitely different') + assert(git.status.changed?('scott/text.txt')) + assert(!git.status.changed?('scott/TEXT.txt')) + + git.config('core.ignorecase', 'true') + assert(git.status.changed?('scott/TEXT.txt')) end end From 993eb78248f1e9b4520a17583ef90cfc41eb60e1 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Sun, 26 May 2024 23:21:47 -0400 Subject: [PATCH 17/72] When core.ignoreCase, check for added files case-insensitively --- lib/git/status.rb | 6 +++++- tests/units/test_status.rb | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/git/status.rb b/lib/git/status.rb index 83ba656a..615222d0 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -58,7 +58,11 @@ def added # added?('lib/git.rb') # @return [Boolean] def added?(file) - added.member?(file) + if ignore_case? + added.keys.map(&:downcase).include?(file.downcase) + else + added.member?(file) + end end # diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 6065cfc9..32bba297 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -92,6 +92,7 @@ def test_dot_files_status def test_added_boolean in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') + git.config('core.ignorecase', 'false') create_file('test_dot_files_status/test_file_1', 'content tets_file_1') create_file('test_dot_files_status/test_file_2', 'content tets_file_2') @@ -100,6 +101,10 @@ def test_added_boolean assert(git.status.added?('test_file_1')) assert(!git.status.added?('test_file_2')) + assert(!git.status.added?('TEST_FILE_1')) + + git.config('core.ignorecase', 'true') + assert(git.status.added?('TEST_FILE_1')) end end From 7758ee478381ec183ff804c9fb9833054e868828 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Sun, 26 May 2024 23:25:34 -0400 Subject: [PATCH 18/72] When core.ignoreCase, check for deleted files case-insensitively --- lib/git/status.rb | 6 +++++- tests/units/test_status.rb | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/git/status.rb b/lib/git/status.rb index 615222d0..f48d162e 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -83,7 +83,11 @@ def deleted # deleted?('lib/git.rb') # @return [Boolean] def deleted?(file) - deleted.member?(file) + if ignore_case? + deleted.keys.map(&:downcase).include?(file.downcase) + else + deleted.member?(file) + end end # diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 32bba297..b691c32f 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -136,6 +136,7 @@ def test_changed_boolean def test_deleted_boolean in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') + git.config('core.ignorecase', 'false') create_file('test_dot_files_status/test_file_1', 'content tets_file_1') create_file('test_dot_files_status/test_file_2', 'content tets_file_2') @@ -146,6 +147,10 @@ def test_deleted_boolean assert(git.status.deleted?('test_file_1')) assert(!git.status.deleted?('test_file_2')) + assert(!git.status.deleted?('TEST_FILE_1')) + + git.config('core.ignorecase', 'true') + assert(git.status.deleted?('TEST_FILE_1')) end end From 2bacccc6e2ffec4011c39969533db026ef6071d2 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Sun, 26 May 2024 23:27:19 -0400 Subject: [PATCH 19/72] When core.ignoreCase, check for untracked files case-insensitively --- lib/git/status.rb | 6 +++++- tests/units/test_status.rb | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/git/status.rb b/lib/git/status.rb index f48d162e..f937cba1 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -108,7 +108,11 @@ def untracked # untracked?('lib/git.rb') # @return [Boolean] def untracked?(file) - untracked.member?(file) + if ignore_case? + untracked.keys.map(&:downcase).include?(file.downcase) + else + untracked.member?(file) + end end def pretty diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index b691c32f..36543bc1 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -207,6 +207,7 @@ def test_untracked_from_subdir def test_untracked_boolean in_temp_dir do |path| git = Git.clone(@wdir, 'test_dot_files_status') + git.config('core.ignorecase', 'false') create_file('test_dot_files_status/test_file_1', 'content tets_file_1') create_file('test_dot_files_status/test_file_2', 'content tets_file_2') @@ -214,6 +215,10 @@ def test_untracked_boolean assert(git.status.untracked?('test_file_1')) assert(!git.status.untracked?('test_file_2')) + assert(!git.status.untracked?('TEST_FILE_1')) + + git.config('core.ignorecase', 'true') + assert(git.status.untracked?('TEST_FILE_1')) end end From 749a72d8a356110447e33c3bc0a882831c6b7372 Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Fri, 31 May 2024 23:31:21 -0400 Subject: [PATCH 20/72] Memoize all of the significant calls in Git::Status When the status has many entries, there were substantial inefficiencies in this class - calling predicates like `changed?(filename)` would iterate the status, constructing a transient `changed` subhash, then test that subhash to see if the file in question was in it (for example). After this, it will _keep_ those sub-hashes for reuse on the Status instance, as well as downcased versions if they happen to get requested (by case-insensitive calls). --- lib/git/status.rb | 60 ++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/lib/git/status.rb b/lib/git/status.rb index f937cba1..39ceace7 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -22,7 +22,7 @@ def initialize(base) # # @return [Enumerable] def changed - @files.select { |_k, f| f.type == 'M' } + @_changed ||= @files.select { |_k, f| f.type == 'M' } end # @@ -34,11 +34,7 @@ def changed # changed?('lib/git.rb') # @return [Boolean] def changed?(file) - if ignore_case? - changed.keys.map(&:downcase).include?(file.downcase) - else - changed.member?(file) - end + case_aware_include?(:changed, :lc_changed, file) end # Returns an Enumerable containing files that have been added. @@ -46,7 +42,7 @@ def changed?(file) # # @return [Enumerable] def added - @files.select { |_k, f| f.type == 'A' } + @_added ||= @files.select { |_k, f| f.type == 'A' } end # Determines whether the given file has been added to the repository @@ -58,11 +54,7 @@ def added # added?('lib/git.rb') # @return [Boolean] def added?(file) - if ignore_case? - added.keys.map(&:downcase).include?(file.downcase) - else - added.member?(file) - end + case_aware_include?(:added, :lc_added, file) end # @@ -71,7 +63,7 @@ def added?(file) # # @return [Enumerable] def deleted - @files.select { |_k, f| f.type == 'D' } + @_deleted ||= @files.select { |_k, f| f.type == 'D' } end # @@ -83,11 +75,7 @@ def deleted # deleted?('lib/git.rb') # @return [Boolean] def deleted?(file) - if ignore_case? - deleted.keys.map(&:downcase).include?(file.downcase) - else - deleted.member?(file) - end + case_aware_include?(:deleted, :lc_deleted, file) end # @@ -96,7 +84,7 @@ def deleted?(file) # # @return [Enumerable] def untracked - @files.select { |_k, f| f.untracked } + @_untracked ||= @files.select { |_k, f| f.untracked } end # @@ -108,11 +96,7 @@ def untracked # untracked?('lib/git.rb') # @return [Boolean] def untracked?(file) - if ignore_case? - untracked.keys.map(&:downcase).include?(file.downcase) - else - untracked.member?(file) - end + case_aware_include?(:untracked, :lc_untracked, file) end def pretty @@ -290,5 +274,33 @@ def ignore_case? rescue Git::FailedError @_ignore_case = false end + + def downcase_keys(hash) + hash.map { |k, v| [k.downcase, v] }.to_h + end + + def lc_changed + @_lc_changed ||= changed.transform_keys(&:downcase) + end + + def lc_added + @_lc_added ||= added.transform_keys(&:downcase) + end + + def lc_deleted + @_lc_deleted ||= deleted.transform_keys(&:downcase) + end + + def lc_untracked + @_lc_untracked ||= untracked.transform_keys(&:downcase) + end + + def case_aware_include?(cased_hash, downcased_hash, file) + if ignore_case? + send(downcased_hash).include?(file.downcase) + else + send(cased_hash).include?(file) + end + end end end From dd8e8d43dd3eed6765a980944ed9131fa7db3c0a Mon Sep 17 00:00:00 2001 From: Eric Mueller Date: Fri, 31 May 2024 22:31:10 -0400 Subject: [PATCH 21/72] Supply all of the _specific_ color options too Previously, we were supplying `color.ui=false`, but if the local gitconfig specified any of the more specific options (like `color.diff`), those would take precedence. This updates our command-runner to always supply all of the specific color options as false as well, so that we definitely get a color-free output suitable for parsing. --- lib/git/lib.rb | 8 ++++++++ tests/files/working/dot_git/config | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 22f474e5..73b92cad 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1223,6 +1223,14 @@ def global_opts global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil? global_opts << '-c' << 'core.quotePath=true' global_opts << '-c' << 'color.ui=false' + global_opts << '-c' << 'color.advice=false' + global_opts << '-c' << 'color.diff=false' + global_opts << '-c' << 'color.grep=false' + global_opts << '-c' << 'color.push=false' + global_opts << '-c' << 'color.remote=false' + global_opts << '-c' << 'color.showBranch=false' + global_opts << '-c' << 'color.status=false' + global_opts << '-c' << 'color.transport=false' end end diff --git a/tests/files/working/dot_git/config b/tests/files/working/dot_git/config index 6c545b24..50a9ab00 100644 --- a/tests/files/working/dot_git/config +++ b/tests/files/working/dot_git/config @@ -13,3 +13,12 @@ [remote "working"] url = ../working.git fetch = +refs/heads/*:refs/remotes/working/* +[color] + diff = always + showBranch = always + grep = always + advice = always + push = always + remote = always + transport = always + status = always From 6ce3d4df8847613ff1d59add61b01b1b6813575c Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 31 May 2024 23:53:52 -0700 Subject: [PATCH 22/72] Handle ignored files with quoted (non-ASCII) filenames --- lib/git/base.rb | 7 +++ lib/git/escaped_path.rb | 2 +- lib/git/lib.rb | 52 +++++++++++++++---- .../test_ignored_files_with_escaped_path.rb | 23 ++++++++ 4 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 tests/units/test_ignored_files_with_escaped_path.rb diff --git a/lib/git/base.rb b/lib/git/base.rb index 97151c20..4a04a7ec 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -309,6 +309,13 @@ def grep(string, path_limiter = nil, opts = {}) self.object('HEAD').grep(string, path_limiter, opts) end + # List the files in the worktree that are ignored by git + # @return [Array] the list of ignored files relative to teh root of the worktree + # + def ignored_files + self.lib.ignored_files + end + # removes file(s) from the git repository def rm(path = '.', opts = {}) self.lib.rm(path, opts) diff --git a/lib/git/escaped_path.rb b/lib/git/escaped_path.rb index 73e4f175..6c085e6d 100644 --- a/lib/git/escaped_path.rb +++ b/lib/git/escaped_path.rb @@ -3,7 +3,7 @@ module Git # Represents an escaped Git path string # - # Git commands that output paths (e.g. ls-files, diff), will escape usual + # Git commands that output paths (e.g. ls-files, diff), will escape unusual # 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). diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 73b92cad..1eefc70e 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -574,18 +574,52 @@ def diff_index(treeish) diff_as_hash('diff-index', treeish) end + # List all files that are in the index + # + # @param location [String] the location to list the files from + # + # @return [Hash] a hash of files in the index + # * key: file [String] the file path + # * value: file_info [Hash] the file information containing the following keys: + # * :path [String] the file path + # * :mode_index [String] the file mode + # * :sha_index [String] the file sha + # * :stage [String] the file stage + # def ls_files(location=nil) location ||= '.' - hsh = {} - command_lines('ls-files', '--stage', location).each do |line| - (info, file) = line.split("\t") - (mode, sha, stage) = info.split - if file.start_with?('"') && file.end_with?('"') - file = Git::EscapedPath.new(file[1..-2]).unescape + {}.tap do |files| + command_lines('ls-files', '--stage', location).each do |line| + (info, file) = line.split("\t") + (mode, sha, stage) = info.split + files[unescape_quoted_path(file)] = { + :path => file, :mode_index => mode, :sha_index => sha, :stage => stage + } end - hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage} end - hsh + end + + # Unescape a path if it is quoted + # + # Git commands that output paths (e.g. ls-files, diff), will escape unusual + # characters. + # + # @example + # lib.unescape_if_quoted('"quoted_file_\\342\\230\\240"') # => 'quoted_file_☠' + # lib.unescape_if_quoted('unquoted_file') # => 'unquoted_file' + # + # @param path [String] the path to unescape if quoted + # + # @return [String] the unescaped path if quoted otherwise the original path + # + # @api private + # + def unescape_quoted_path(path) + if path.start_with?('"') && path.end_with?('"') + Git::EscapedPath.new(path[1..-2]).unescape + else + path + end end def ls_remote(location=nil, opts={}) @@ -606,7 +640,7 @@ def ls_remote(location=nil, opts={}) end def ignored_files - command_lines('ls-files', '--others', '-i', '--exclude-standard') + command_lines('ls-files', '--others', '-i', '--exclude-standard').map { |f| unescape_quoted_path(f) } end def untracked_files diff --git a/tests/units/test_ignored_files_with_escaped_path.rb b/tests/units/test_ignored_files_with_escaped_path.rb new file mode 100644 index 00000000..0d40711d --- /dev/null +++ b/tests/units/test_ignored_files_with_escaped_path.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +# encoding: utf-8 + +require '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 TestIgnoredFilesWithEscapedPath < Test::Unit::TestCase + def test_ignored_files_with_non_ascii_filename + in_temp_dir do |path| + create_file('README.md', '# My Project') + `git init` + `git add .` + `git config --local core.safecrlf false` if Gem.win_platform? + `git commit -m "First Commit"` + create_file('my_other_file_☠', "First Line\n") + create_file(".gitignore", "my_other_file_☠") + files = Git.open('.').ignored_files + assert_equal(['my_other_file_☠'].sort, files) + end + end +end From 676ee17199b665482ed81a2a9c9bb7dd0d163dc6 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 1 Jun 2024 09:39:01 -0700 Subject: [PATCH 23/72] Release v2.1.1 Signed-off-by: James Couball --- CHANGELOG.md | 14 ++++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c327e01d..f7d9bcae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ # Change Log +## v2.1.1 (2024-06-01) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.1.0..v2.1.1) + +Changes since v2.1.0: + +* 6ce3d4d Handle ignored files with quoted (non-ASCII) filenames +* dd8e8d4 Supply all of the _specific_ color options too +* 749a72d Memoize all of the significant calls in Git::Status +* 2bacccc When core.ignoreCase, check for untracked files case-insensitively +* 7758ee4 When core.ignoreCase, check for deleted files case-insensitively +* 993eb78 When core.ignoreCase, check for added files case-insensitively +* d943bf4 When core.ignoreCase, check for changed files case-insensitively + ## v2.1.0 (2024-05-31) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.1..v2.1.0) diff --git a/lib/git/version.rb b/lib/git/version.rb index b88d2356..f970509b 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='2.1.0' + VERSION='2.1.1' end From 737c4bb16074f60a8887d8ce73f01993a6ffce95 Mon Sep 17 00:00:00 2001 From: Bill Franklin Date: Mon, 12 Aug 2024 11:06:29 +0100 Subject: [PATCH 24/72] ls-tree optional recursion into subtrees --- README.md | 3 +++ lib/git/base.rb | 4 ++-- lib/git/lib.rb | 9 +++++++-- tests/units/test_ls_tree.rb | 15 +++++++++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 841bcfcd..cfed3aec 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,9 @@ g.index.writable? g.repo g.dir +# ls-tree with recursion into subtrees (list files) +g.ls_tree("head", recursive: true) + # log - returns a Git::Log object, which is an Enumerator of Git::Commit objects # default configuration returns a max of 30 commits g.log diff --git a/lib/git/base.rb b/lib/git/base.rb index 4a04a7ec..27de57de 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -642,8 +642,8 @@ def revparse(objectish) self.lib.revparse(objectish) end - def ls_tree(objectish) - self.lib.ls_tree(objectish) + def ls_tree(objectish, opts = {}) + self.lib.ls_tree(objectish, opts) end def cat_file(objectish) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 1eefc70e..8f4e89bb 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -374,10 +374,15 @@ def object_contents(sha, &block) end end - def ls_tree(sha) + def ls_tree(sha, opts = {}) data = { 'blob' => {}, 'tree' => {}, 'commit' => {} } - command_lines('ls-tree', sha).each do |line| + ls_tree_opts = [] + ls_tree_opts << '-r' if opts[:recursive] + # path must be last arg + ls_tree_opts << opts[:path] if opts[:path] + + command_lines('ls-tree', sha, *ls_tree_opts).each do |line| (info, filenm) = line.split("\t") (mode, type, sha) = info.split data[type][filenm] = {:mode => mode, :sha => sha} diff --git a/tests/units/test_ls_tree.rb b/tests/units/test_ls_tree.rb index 222af233..19d487a4 100644 --- a/tests/units/test_ls_tree.rb +++ b/tests/units/test_ls_tree.rb @@ -13,11 +13,26 @@ def test_ls_tree_with_submodules repo.add('README.md') repo.commit('Add README.md') + Dir.mkdir("repo/subdir") + File.write('repo/subdir/file.md', 'Content in subdir') + repo.add('subdir/file.md') + repo.commit('Add subdir/file.md') + + # ls_tree + default_tree = assert_nothing_raised { repo.ls_tree('HEAD') } + assert_equal(default_tree.dig("blob").keys.sort, ["README.md"]) + assert_equal(default_tree.dig("tree").keys.sort, ["subdir"]) + # ls_tree with recursion into sub-trees + recursive_tree = assert_nothing_raised { repo.ls_tree('HEAD', recursive: true) } + assert_equal(recursive_tree.dig("blob").keys.sort, ["README.md", "subdir/file.md"]) + assert_equal(recursive_tree.dig("tree").keys.sort, []) + Dir.chdir('repo') do assert_child_process_success { `git -c protocol.file.allow=always submodule add ../submodule submodule 2>&1` } assert_child_process_success { `git commit -am "Add submodule" 2>&1` } end + expected_submodule_sha = submodule.object('HEAD').sha # Make sure the ls_tree command can handle submodules (which show up as a commit object in the tree) From a08f89b7de5edfbbb73dd37a20891852577ae043 Mon Sep 17 00:00:00 2001 From: Bill Franklin Date: Mon, 12 Aug 2024 11:27:42 +0100 Subject: [PATCH 25/72] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfed3aec..3152688a 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ g.repo g.dir # ls-tree with recursion into subtrees (list files) -g.ls_tree("head", recursive: true) +g.ls_tree("HEAD", recursive: true) # log - returns a Git::Log object, which is an Enumerator of Git::Commit objects # default configuration returns a max of 30 commits From 00c4939d0f622e8e5cc234b07ddcb6ae00fd5de1 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 23 Aug 2024 16:44:35 -0700 Subject: [PATCH 26/72] Verify that the commit(s) passed to git diff do not resemble a command-line option --- lib/git/lib.rb | 21 +++++++++++++++++++++ tests/units/test_diff.rb | 20 ++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 8f4e89bb..e7bcb3e2 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -526,7 +526,24 @@ def grep(string, opts = {}) hsh end + # Validate that the given arguments cannot be mistaken for a command-line option + # + # @param arg_name [String] the name of the arguments to mention in the error message + # @param args [Array] the arguments to validate + # + # @raise [ArgumentError] if any of the parameters are a string starting with a hyphen + # @return [void] + # + def validate_no_options(arg_name, *args) + invalid_args = args.select { |arg| arg&.start_with?('-') } + if invalid_args.any? + raise ArgumentError, "Invalid #{arg_name}: '#{invalid_args.join("', '")}'" + end + end + def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) + validate_no_options('commit or commit range', obj1, obj2) + diff_opts = ['-p'] diff_opts << obj1 diff_opts << obj2 if obj2.is_a?(String) @@ -536,6 +553,8 @@ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) end def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) + validate_no_options('commit or commit range', obj1, obj2) + diff_opts = ['--numstat'] diff_opts << obj1 diff_opts << obj2 if obj2.is_a?(String) @@ -556,6 +575,8 @@ def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) end def diff_name_status(reference1 = nil, reference2 = nil, opts = {}) + validate_no_options('commit or commit range', reference1, reference2) + opts_arr = ['--name-status'] opts_arr << reference1 if reference1 opts_arr << reference2 if reference2 diff --git a/tests/units/test_diff.rb b/tests/units/test_diff.rb index d640146d..89a476a9 100644 --- a/tests/units/test_diff.rb +++ b/tests/units/test_diff.rb @@ -118,5 +118,25 @@ def test_diff_each assert_equal(160, files['scott/newfile'].patch.size) end + def test_diff_patch_with_bad_commit + assert_raise(ArgumentError) do + @git.diff('-s').patch + end + assert_raise(ArgumentError) do + @git.diff('gitsearch1', '-s').patch + end + end + + def test_diff_name_status_with_bad_commit + assert_raise(ArgumentError) do + @git.diff('-s').name_status + end + end + + def test_diff_stats_with_bad_commit + assert_raise(ArgumentError) do + @git.diff('-s').stats + end + end end From dc46edea6384907fd948b5274dbebd08bd5e7acb Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 24 Aug 2024 15:22:27 -0700 Subject: [PATCH 27/72] Verify that the commit-ish passed to git describe does not resemble a command-line option --- lib/git/lib.rb | 54 ++++++++++++++++++++---------------- tests/units/test_describe.rb | 5 ++++ 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index e7bcb3e2..059d259e 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -169,27 +169,33 @@ def repository_default_branch(repository) ## READ COMMANDS ## + # Finds most recent tag that is reachable from a commit # - # Returns most recent tag that is reachable from a commit + # @see https://git-scm.com/docs/git-describe git-describe # - # accepts options: - # :all - # :tags - # :contains - # :debug - # :exact_match - # :dirty - # :abbrev - # :candidates - # :long - # :always - # :math - # - # @param [String|NilClass] committish target commit sha or object name - # @param [{Symbol=>Object}] opts the given options - # @return [String] the tag name - # - def describe(committish=nil, opts={}) + # @param commit_ish [String, nil] target commit sha or object name + # + # @param opts [Hash] the given options + # + # @option opts :all [Boolean] + # @option opts :tags [Boolean] + # @option opts :contains [Boolean] + # @option opts :debug [Boolean] + # @option opts :long [Boolean] + # @option opts :always [Boolean] + # @option opts :exact_match [Boolean] + # @option opts :dirty [true, String] + # @option opts :abbrev [String] + # @option opts :candidates [String] + # @option opts :match [String] + # + # @return [String] the tag name + # + # @raise [ArgumentError] if the commit_ish is a string starting with a hyphen + # + def describe(commit_ish = nil, opts = {}) + assert_args_are_not_options('commit-ish object', commit_ish) + arr_opts = [] arr_opts << '--all' if opts[:all] @@ -207,7 +213,7 @@ def describe(committish=nil, opts={}) arr_opts << "--candidates=#{opts[:candidates]}" if opts[:candidates] arr_opts << "--match=#{opts[:match]}" if opts[:match] - arr_opts << committish if committish + arr_opts << commit_ish if commit_ish return command('describe', *arr_opts) end @@ -534,7 +540,7 @@ def grep(string, opts = {}) # @raise [ArgumentError] if any of the parameters are a string starting with a hyphen # @return [void] # - def validate_no_options(arg_name, *args) + def assert_args_are_not_options(arg_name, *args) invalid_args = args.select { |arg| arg&.start_with?('-') } if invalid_args.any? raise ArgumentError, "Invalid #{arg_name}: '#{invalid_args.join("', '")}'" @@ -542,7 +548,7 @@ def validate_no_options(arg_name, *args) end def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) - validate_no_options('commit or commit range', obj1, obj2) + assert_args_are_not_options('commit or commit range', obj1, obj2) diff_opts = ['-p'] diff_opts << obj1 @@ -553,7 +559,7 @@ def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) end def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) - validate_no_options('commit or commit range', obj1, obj2) + assert_args_are_not_options('commit or commit range', obj1, obj2) diff_opts = ['--numstat'] diff_opts << obj1 @@ -575,7 +581,7 @@ def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) end def diff_name_status(reference1 = nil, reference2 = nil, opts = {}) - validate_no_options('commit or commit range', reference1, reference2) + assert_args_are_not_options('commit or commit range', reference1, reference2) opts_arr = ['--name-status'] opts_arr << reference1 if reference1 diff --git a/tests/units/test_describe.rb b/tests/units/test_describe.rb index 2d0e2012..967fc753 100644 --- a/tests/units/test_describe.rb +++ b/tests/units/test_describe.rb @@ -13,4 +13,9 @@ def test_describe assert_equal(@git.describe(nil, {:tags => true}), 'grep_colon_numbers') end + def test_describe_with_invalid_commitish + assert_raise ArgumentError do + @git.describe('--all') + end + end end From 9b9b31e704c0b85ffdd8d2af2ded85170a5af87d Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 24 Aug 2024 17:08:56 -0700 Subject: [PATCH 28/72] Verify that the revision-range passed to git log does not resemble a command-line option --- lib/git/lib.rb | 76 +++++++++++++++++++++++++++++++++++++++-- tests/units/test_lib.rb | 28 +++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 059d259e..84eda5a1 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -192,7 +192,7 @@ def repository_default_branch(repository) # @return [String] the tag name # # @raise [ArgumentError] if the commit_ish is a string starting with a hyphen - # + # def describe(commit_ish = nil, opts = {}) assert_args_are_not_options('commit-ish object', commit_ish) @@ -218,7 +218,37 @@ def describe(commit_ish = nil, opts = {}) return command('describe', *arr_opts) end - def log_commits(opts={}) + # Return the commits that are within the given revision range + # + # @see https://git-scm.com/docs/git-log git-log + # + # @param opts [Hash] the given options + # + # @option opts :count [Integer] the maximum number of commits to return (maps to max-count) + # @option opts :all [Boolean] + # @option opts :cherry [Boolean] + # @option opts :since [String] + # @option opts :until [String] + # @option opts :grep [String] + # @option opts :author [String] + # @option opts :between [Array] an array of two commit-ish strings to specify a revision range + # + # Only :between or :object options can be used, not both. + # + # @option opts :object [String] the revision range for the git log command + # + # Only :between or :object options can be used, not both. + # + # @option opts :path_limiter [Array, String] only include commits that impact files from the specified paths + # + # @return [Array] the log output + # + # @raise [ArgumentError] if the resulting revision range is a string starting with a hyphen + # + def log_commits(opts = {}) + assert_args_are_not_options('between', opts[:between]&.first) + assert_args_are_not_options('object', opts[:object]) + arr_opts = log_common_options(opts) arr_opts << '--pretty=oneline' @@ -228,7 +258,47 @@ def log_commits(opts={}) command_lines('log', *arr_opts).map { |l| l.split.first } end - def full_log_commits(opts={}) + # Return the commits that are within the given revision range + # + # @see https://git-scm.com/docs/git-log git-log + # + # @param opts [Hash] the given options + # + # @option opts :count [Integer] the maximum number of commits to return (maps to max-count) + # @option opts :all [Boolean] + # @option opts :cherry [Boolean] + # @option opts :since [String] + # @option opts :until [String] + # @option opts :grep [String] + # @option opts :author [String] + # @option opts :between [Array] an array of two commit-ish strings to specify a revision range + # + # Only :between or :object options can be used, not both. + # + # @option opts :object [String] the revision range for the git log command + # + # Only :between or :object options can be used, not both. + # + # @option opts :path_limiter [Array, String] only include commits that impact files from the specified paths + # @option opts :skip [Integer] + # + # @return [Array] the log output parsed into an array of hashs for each commit + # + # Each hash contains the following keys: + # * 'sha' [String] the commit sha + # * 'author' [String] the author of the commit + # * 'message' [String] the commit message + # * 'parent' [Array] the commit shas of the parent commits + # * 'tree' [String] the tree sha + # * 'author' [String] the author of the commit and timestamp of when the changes were created + # * 'committer' [String] the committer of the commit and timestamp of when the commit was applied + # + # @raise [ArgumentError] if the revision range (specified with :between or :object) is a string starting with a hyphen + # + def full_log_commits(opts = {}) + assert_args_are_not_options('between', opts[:between]&.first) + assert_args_are_not_options('object', opts[:object]) + arr_opts = log_common_options(opts) arr_opts << '--pretty=raw' diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index a2bb067e..be049c7b 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -123,6 +123,34 @@ def test_log_commits assert_equal(20, a.size) end + def test_log_commits_invalid_between + # between can not start with a hyphen + assert_raise ArgumentError do + @lib.log_commits :count => 20, :between => ['-v2.5', 'v2.6'] + end + end + + def test_log_commits_invalid_object + # :object can not start with a hyphen + assert_raise ArgumentError do + @lib.log_commits :count => 20, :object => '--all' + end + end + + def test_full_log_commits_invalid_between + # between can not start with a hyphen + assert_raise ArgumentError do + @lib.full_log_commits :count => 20, :between => ['-v2.5', 'v2.6'] + end + end + + def test_full_log_commits_invalid_object + # :object can not start with a hyphen + assert_raise ArgumentError do + @lib.full_log_commits :count => 20, :object => '--all' + end + end + def test_git_ssh_from_environment_is_passed_to_binary saved_binary_path = Git::Base.config.binary_path saved_git_ssh = Git::Base.config.git_ssh From 02964423a6ee0f573ae9facf48836b4bcd0075c4 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 25 Aug 2024 10:12:45 -0700 Subject: [PATCH 29/72] Refactor Git::Lib#rev_parse --- README.md | 2 +- lib/git/base.rb | 13 ++++++++----- lib/git/lib.rb | 33 ++++++++++++++++++++++++--------- lib/git/object.rb | 2 +- tests/units/test_branch.rb | 4 ++-- tests/units/test_lib.rb | 26 ++++++++++++++++++++++---- tests/units/test_object.rb | 4 ++-- 7 files changed, 60 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3152688a..c3f788ca 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ tree.blobs tree.subtrees tree.children # blobs and subtrees -g.revparse('v2.5:Makefile') +g.rev_parse('v2.0.0:README.md') g.branches # returns Git::Branch objects g.branches.local diff --git a/lib/git/base.rb b/lib/git/base.rb index 27de57de..ae909dcc 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -634,14 +634,17 @@ def with_temp_working &blk # runs git rev-parse to convert the objectish to a full sha # # @example - # git.revparse("HEAD^^") - # git.revparse('v2.4^{tree}') - # git.revparse('v2.4:/doc/index.html') + # git.rev_parse("HEAD^^") + # git.rev_parse('v2.4^{tree}') + # git.rev_parse('v2.4:/doc/index.html') # - def revparse(objectish) - self.lib.revparse(objectish) + def rev_parse(objectish) + self.lib.rev_parse(objectish) end + # For backwards compatibility + alias revparse rev_parse + def ls_tree(objectish, opts = {}) self.lib.ls_tree(objectish, opts) end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 84eda5a1..4f607e4f 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -311,17 +311,32 @@ def full_log_commits(opts = {}) process_commit_log_data(full_log) end - def revparse(string) - return string if string =~ /^[A-Fa-f0-9]{40}$/ # passing in a sha - just no-op it - rev = ['head', 'remotes', 'tags'].map do |d| - File.join(@git_dir, 'refs', d, string) - end.find do |path| - File.file?(path) - end - return File.read(rev).chomp if rev - command('rev-parse', string) + # Verify and resolve a Git revision to its full SHA + # + # @see https://git-scm.com/docs/git-rev-parse git-rev-parse + # @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions + # @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem Ref disambiguation rules + # + # @example + # lib.rev_parse('HEAD') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d' + # lib.rev_parse('9b9b31e') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d' + # + # @param revision [String] the revision to resolve + # + # @return [String] the full commit hash + # + # @raise [Git::FailedError] if the revision cannot be resolved + # @raise [ArgumentError] if the revision is a string starting with a hyphen + # + def rev_parse(revision) + assert_args_are_not_options('rev', revision) + + command('rev-parse', revision) end + # For backwards compatibility with the old method name + alias :revparse :rev_parse + def namerev(string) command('name-rev', string).split[1] end diff --git a/lib/git/object.rb b/lib/git/object.rb index 1ffc1013..083640b6 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -23,7 +23,7 @@ def initialize(base, objectish) end def sha - @sha ||= @base.lib.revparse(@objectish) + @sha ||= @base.lib.rev_parse(@objectish) end def size diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index 08707b63..2256f4cb 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -160,11 +160,11 @@ def test_branch_update_ref File.write('foo','rev 2') git.add('foo') git.commit('rev 2') - git.branch('testing').update_ref(git.revparse('HEAD')) + git.branch('testing').update_ref(git.rev_parse('HEAD')) # Expect the call to Branch#update_ref to pass the full ref name for the # of the testing branch to Lib#update_ref - assert_equal(git.revparse('HEAD'), git.revparse('refs/heads/testing')) + assert_equal(git.rev_parse('HEAD'), git.rev_parse('refs/heads/testing')) end end end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index be049c7b..c8e035ad 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -174,10 +174,28 @@ def test_git_ssh_from_environment_is_passed_to_binary Git::Base.config.git_ssh = saved_git_ssh end - def test_revparse - assert_equal('1cc8667014381e2788a94777532a788307f38d26', @lib.revparse('1cc8667014381')) # commit - assert_equal('94c827875e2cadb8bc8d4cdd900f19aa9e8634c7', @lib.revparse('1cc8667014381^{tree}')) #tree - assert_equal('ba492c62b6227d7f3507b4dcc6e6d5f13790eabf', @lib.revparse('v2.5:example.txt')) #blob + def test_rev_parse_commit + assert_equal('1cc8667014381e2788a94777532a788307f38d26', @lib.rev_parse('1cc8667014381')) # commit + end + + def test_rev_parse_tree + assert_equal('94c827875e2cadb8bc8d4cdd900f19aa9e8634c7', @lib.rev_parse('1cc8667014381^{tree}')) #tree + end + + def test_rev_parse_blob + assert_equal('ba492c62b6227d7f3507b4dcc6e6d5f13790eabf', @lib.rev_parse('v2.5:example.txt')) #blob + end + + def test_rev_parse_with_bad_revision + assert_raise(ArgumentError) do + @lib.rev_parse('--all') + end + end + + def test_rev_parse_with_unknown_revision + assert_raise(Git::FailedError) do + @lib.rev_parse('NOTFOUND') + end end def test_object_type diff --git a/tests/units/test_object.rb b/tests/units/test_object.rb index 784e81bf..3f31b390 100644 --- a/tests/units/test_object.rb +++ b/tests/units/test_object.rb @@ -120,8 +120,8 @@ def test_blob_contents assert(block_called) end - def test_revparse - sha = @git.revparse('v2.6:example.txt') + def test_rev_parse + sha = @git.rev_parse('v2.6:example.txt') assert_equal('1f09f2edb9c0d9275d15960771b363ca6940fbe3', sha) end From d4f66ab3beff28f65c3fe60f9f77f646c483ba89 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 25 Aug 2024 11:12:16 -0700 Subject: [PATCH 30/72] Sanitize non-option arguments passed to `git name-rev` --- lib/git/lib.rb | 16 ++++++++++++++-- lib/git/object.rb | 2 +- tests/units/test_lib.rb | 10 ++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 4f607e4f..1742130e 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -337,10 +337,22 @@ def rev_parse(revision) # For backwards compatibility with the old method name alias :revparse :rev_parse - def namerev(string) - command('name-rev', string).split[1] + # Find the first symbolic name for given commit_ish + # + # @param commit_ish [String] the commit_ish to find the symbolic name of + # + # @return [String, nil] the first symbolic name or nil if the commit_ish isn't found + # + # @raise [ArgumentError] if the commit_ish is a string starting with a hyphen + # + def name_rev(commit_ish) + assert_args_are_not_options('commit_ish', commit_ish) + + command('name-rev', commit_ish).split[1] end + alias :namerev :name_rev + def object_type(sha) command('cat-file', '-t', sha) end diff --git a/lib/git/object.rb b/lib/git/object.rb index 083640b6..6c4aada9 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -175,7 +175,7 @@ def message end def name - @base.lib.namerev(sha) + @base.lib.name_rev(sha) end def gtree diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index c8e035ad..38694980 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -198,6 +198,16 @@ def test_rev_parse_with_unknown_revision end end + def test_name_rev + assert_equal('tags/v2.5~5', @lib.name_rev('00ea60e')) + end + + def test_name_rev_with_invalid_commit_ish + assert_raise(ArgumentError) do + @lib.name_rev('-1cc8667014381') + end + end + def test_object_type assert_equal('commit', @lib.object_type('1cc8667014381')) # commit assert_equal('tree', @lib.object_type('1cc8667014381^{tree}')) #tree From 2d6157c95332b8e3907094d1229713720ff5029d Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 25 Aug 2024 15:53:51 -0700 Subject: [PATCH 31/72] Document this gem's (aspirational) design philosophy --- CONTRIBUTING.md | 195 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 60 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 636f9c4b..082a8853 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,116 +3,191 @@ # @title How To Contribute --> -# Contributing to ruby-git - -Thank you for your interest in contributing to the ruby-git project. - -This document gives the guidelines for contributing to the ruby-git project. -These guidelines may not fit every situation. When contributing use your best -judgement. - -Propose changes to these guidelines with a pull request. +* [How to contribute](#how-to-contribute) +* [How to report an issue or request a feature](#how-to-report-an-issue-or-request-a-feature) +* [How to submit a code or documentation change](#how-to-submit-a-code-or-documentation-change) + * [Commit your changes to a fork of `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) + * [Create a pull request](#create-a-pull-request) + * [Get your pull request reviewed](#get-your-pull-request-reviewed) +* [Design philosophy](#design-philosophy) + * [Direct mapping to git commands](#direct-mapping-to-git-commands) + * [Parameter naming](#parameter-naming) + * [Output processing](#output-processing) +* [Coding standards](#coding-standards) + * [1 PR = 1 Commit](#1-pr--1-commit) + * [Unit tests](#unit-tests) + * [Continuous integration](#continuous-integration) + * [Documentation](#documentation) +* [Licensing](#licensing) + + +# Contributing to the git gem + +Thank you for your interest in contributing to the `ruby-git` project. + +This document provides guidelines for contributing to the `ruby-git` project. While +these guidelines may not cover every situation, we encourage you to use your best +judgment when contributing. + +If you have suggestions for improving these guidelines, please propose changes via a +pull request. ## How to contribute -You can contribute in two ways: +You can contribute in the following ways: -1. [Report an issue or make a feature request](#how-to-report-an-issue-or-make-a-feature-request) -2. [Submit a code or documentation change](#how-to-submit-a-code-or-documentation-change) +1. [Report an issue or request a + feature](#how-to-report-an-issue-or-request-a-feature) +2. [Submit a code or documentation + change](#how-to-submit-a-code-or-documentation-change) -## How to report an issue or make a feature request +## How to report an issue or request a feature -ruby-git utilizes [GitHub Issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues) +`ruby-git` utilizes [GitHub +Issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues) for issue tracking and feature requests. -Report an issue or feature request by [creating a ruby-git Github issue](https://github.com/ruby-git/ruby-git/issues/new). -Fill in the template to describe the issue or feature request the best you can. +To report an issue or request a feature, please [create a `ruby-git` GitHub +issue](https://github.com/ruby-git/ruby-git/issues/new). Fill in the template as +thoroughly as possible to describe the issue or feature request. ## How to submit a code or documentation change -There is three step process for code or documentation changes: +There is a three-step process for submitting code or documentation changes: -1. [Commit your changes to a fork of ruby-git](#commit-changes-to-a-fork-of-ruby-git) +1. [Commit your changes to a fork of + `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) 2. [Create a pull request](#create-a-pull-request) 3. [Get your pull request reviewed](#get-your-pull-request-reviewed) -### Commit changes to a fork of ruby-git +### Commit your changes to a fork of `ruby-git` -Make your changes in a fork of the ruby-git repository. +Make your changes in a fork of the `ruby-git` repository. ### Create a pull request -See [this article](https://help.github.com/articles/about-pull-requests/) if you -are not familiar with GitHub Pull Requests. +If you are not familiar with GitHub Pull Requests, please refer to [this +article](https://help.github.com/articles/about-pull-requests/). Follow the instructions in the pull request template. ### Get your pull request reviewed -Code review takes place in a GitHub pull request using the [the Github pull request review feature](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews). +Code review takes place in a GitHub pull request using the [GitHub pull request +review +feature](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews). Once your pull request is ready for review, request a review from at least one -[maintainer](MAINTAINERS.md) and any number of other contributors. +[maintainer](MAINTAINERS.md) and any other contributors you deem necessary. + +During the review process, you may need to make additional commits, which should be +squashed. Additionally, you may need to rebase your branch to the latest `master` +branch if other changes have been merged. + +At least one approval from a project maintainer is required before your pull request +can be merged. The maintainer is responsible for ensuring that the pull request meets +[the project's coding standards](#coding-standards). + +## Design philosophy + +*Note: As of v2.x of the `git` gem, this design philosophy is aspirational. Future +versions may include interface changes to fully align with these principles.* + +The `git` gem is designed as a lightweight wrapper around the `git` command-line +tool, providing Ruby developers with a simple and intuitive interface for +programmatically interacting with Git. + +This gem adheres to the "principle of least surprise," ensuring that it does not +introduce unnecessary abstraction layers or modify Git's core functionality. Instead, +the gem maintains a close alignment with the existing `git` command-line interface, +avoiding extensions or alterations that could lead to unexpected behaviors. + +By following this philosophy, the `git` gem allows users to leverage their existing +knowledge of Git while benefiting from the expressiveness and power of Ruby's syntax +and paradigms. + +### Direct mapping to git commands -During the review process, you may need to make additional commits which would -need to be squashed. It may also be necessary to rebase to master again if other -changes are merged before your PR. +Git commands are implemented within the `Git::Base` class, with each method directly +corresponding to a `git` command. When a `Git::Base` object is instantiated via +`Git.open`, `Git.clone`, or `Git.init`, the user can invoke these methods to interact +with the underlying Git repository. -At least one approval is required from a project maintainer before your pull -request can be merged. The maintainer is responsible for ensuring that the pull -request meets [the project's coding standards](#coding-standards). +For example, the `git add` command is implemented as `Git::Base#add`, and the `git +ls-files` command is implemented as `Git::Base#ls_files`. + +When a single Git command serves multiple distinct purposes, method names within the +`Git::Base` class should use the `git` command name as a prefix, followed by a +descriptive suffix to indicate the specific function. + +For instance, `#ls_files_untracked` and `#ls_files_staged` could be used to execute +the `git ls-files` command and return untracked and staged files, respectively. + +To enhance usability, aliases may be introduced to provide more user-friendly method +names where appropriate. + +### Parameter naming + +Parameters within the `git` gem methods are named after their corresponding long +command-line options, ensuring familiarity and ease of use for developers already +accustomed to Git. Note that not all Git command options are supported. + +### Output processing + +The `git` gem translates the output of many Git commands into Ruby objects, making it +easier to work with programmatically. + +These Ruby objects often include methods that allow for further Git operations where +useful, providing additional functionality while staying true to the underlying Git +behavior. ## Coding standards -In order to ensure high quality, all pull requests must meet these requirements: +To ensure high-quality contributions, all pull requests must meet the following +requirements: ### 1 PR = 1 Commit -* All commits for a PR must be squashed into one commit -* To avoid an extra merge commit, the PR must be able to be merged as [a fast forward - merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) -* The easiest way to ensure a fast forward merge is to rebase your local branch to - the ruby-git master branch +* All commits for a PR must be squashed into a single commit. +* To avoid an extra merge commit, the PR must be able to be merged as [a fast-forward + merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging). +* The easiest way to ensure a fast-forward merge is to rebase your local branch to + the `ruby-git` master branch. ### Unit tests -* All changes must be accompanied by new or modified unit tests +* All changes must be accompanied by new or modified unit tests. * The entire test suite must pass when `bundle exec rake default` is run from the project's local working copy. -While working on specific features you can run individual test files or -a group of tests using `bin/test`: +While working on specific features, you can run individual test files or a group of +tests using `bin/test`: - # run a single file (from tests/units): - $ bin/test test_object +```bash +# run a single file (from tests/units): +$ bin/test test_object - # run multiple files: - $ bin/test test_object test_archive +# run multiple files: +$ bin/test test_object test_archive - # run all unit tests: - $ bin/test +# run all unit tests: +$ bin/test +``` ### Continuous integration -* All tests must pass in the project's [GitHub Continuous Integration - build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) before the - pull request will be merged. -* The [Continuous Integration - workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml) - runs both `bundle exec rake default` and `bundle exec rake test:gem` from the - project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile). +All tests must pass in the project's [GitHub Continuous Integration build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) before the pull request will be merged. + +The [Continuous Integration workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml) runs both `bundle exec rake default` and `bundle exec rake test:gem` from the project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile). ### Documentation -* New and updated public methods must have [YARD](https://yardoc.org/) documentation - added to them -* New and updated public facing features should be documented in the project's - [README.md](README.md) +New and updated public methods must include [YARD](https://yardoc.org/) documentation. + +New and updated public-facing features should be documented in the project's [README.md](README.md). ## Licensing -ruby-git uses [the MIT license](https://choosealicense.com/licenses/mit/) as -declared in the [LICENSE](LICENSE) file. +`ruby-git` uses [the MIT license](https://choosealicense.com/licenses/mit/) as declared in the [LICENSE](LICENSE) file. -Licensing is very important to open source projects. It helps ensure the -software continues to be available under the terms that the author desired. +Licensing is critical to open-source projects as it ensures the software remains available under the terms desired by the author. \ No newline at end of file From 7292f2c79de7c38961025386ceda76fe390f67d7 Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 26 Aug 2024 15:32:35 -0700 Subject: [PATCH 32/72] Omit the test for signed commit data on Windows --- tests/test_helper.rb | 2 +- tests/units/test_signed_commits.rb | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/test_helper.rb b/tests/test_helper.rb index f5b08ee3..7be31378 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -45,7 +45,7 @@ def in_temp_repo(clone_name) def create_temp_repo(clone_name) clone_path = File.join(TEST_FIXTURES, clone_name) filename = 'git_test' + Time.now.to_i.to_s + rand(300).to_s.rjust(3, '0') - path = File.expand_path(File.join("/tmp/", filename)) + path = File.expand_path(File.join(Dir.tmpdir, filename)) FileUtils.mkdir_p(path) @tmp_path = File.realpath(path) FileUtils.cp_r(clone_path, @tmp_path) diff --git a/tests/units/test_signed_commits.rb b/tests/units/test_signed_commits.rb index d1c4d858..871b92a5 100644 --- a/tests/units/test_signed_commits.rb +++ b/tests/units/test_signed_commits.rb @@ -13,15 +13,22 @@ class TestSignedCommits < Test::Unit::TestCase def in_repo_with_signing_config(&block) in_temp_dir do |path| `git init` - `ssh-keygen -t dsa -N "" -C "test key" -f .git/test-key` + ssh_key_file = File.expand_path(File.join('.git', 'test-key')) + `ssh-keygen -t dsa -N "" -C "test key" -f "#{ssh_key_file}"` `git config --local gpg.format ssh` - `git config --local user.signingkey .git/test-key` + `git config --local user.signingkey #{ssh_key_file}.pub` + + raise "ERROR: No .git/test-key file" unless File.exist?("#{ssh_key_file}.pub") yield end end def test_commit_data + # Signed commits should work on windows, but this test is omitted until the setup + # on windows can be figured out + omit('Omit testing of signed commits on Windows') if windows_platform? + in_repo_with_signing_config do create_file('README.md', '# My Project') `git add README.md` From 3b8de25f046c9e7952b0c181307ac1ba8c91448d Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 26 Aug 2024 15:53:17 -0700 Subject: [PATCH 33/72] Release v2.2.0 Signed-off-by: James Couball --- CHANGELOG.md | 16 ++++++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d9bcae..f9120219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ # Change Log +## v2.2.0 (2024-08-26) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.1.1..v2.2.0) + +Changes since v2.1.1: + +* 7292f2c Omit the test for signed commit data on Windows +* 2d6157c Document this gem's (aspirational) design philosophy +* d4f66ab Sanitize non-option arguments passed to `git name-rev` +* 0296442 Refactor Git::Lib#rev_parse +* 9b9b31e Verify that the revision-range passed to git log does not resemble a command-line option +* dc46ede Verify that the commit-ish passed to git describe does not resemble a command-line option +* 00c4939 Verify that the commit(s) passed to git diff do not resemble a command-line option +* a08f89b Update README +* 737c4bb ls-tree optional recursion into subtrees + ## v2.1.1 (2024-06-01) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.1.0..v2.1.1) diff --git a/lib/git/version.rb b/lib/git/version.rb index f970509b..15f996be 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='2.1.1' + VERSION='2.2.0' end From 604a9a2f9e586e057ac0b674137aee3aafb31d79 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 1 Sep 2024 09:19:56 -0700 Subject: [PATCH 34/72] Make Git::Base#branch work when HEAD is detached --- lib/git/base.rb | 9 ++- lib/git/lib.rb | 48 +++++++++++++++- tests/units/test_branch.rb | 110 +++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 2 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index ae909dcc..088d2a3d 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -653,7 +653,14 @@ def cat_file(objectish) self.lib.object_contents(objectish) end - # returns the name of the branch the working directory is currently on + # The name of the branch HEAD refers to or 'HEAD' if detached + # + # Returns one of the following: + # * The branch name that HEAD refers to (even if it is an unborn branch) + # * 'HEAD' if in a detached HEAD state + # + # @return [String] the name of the branch HEAD refers to or 'HEAD' if detached + # def current_branch self.lib.branch_current end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 1742130e..4f519ec3 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -591,8 +591,54 @@ def list_files(ref_dir) files end + # The state and name of branch pointed to by `HEAD` + # + # HEAD can be in the following states: + # + # **:active**: `HEAD` points to a branch reference which in turn points to a + # commit representing the tip of that branch. This is the typical state when + # working on a branch. + # + # **:unborn**: `HEAD` points to a branch reference that does not yet exist + # because no commits have been made on that branch. This state occurs in two + # scenarios: + # + # * When a repository is newly initialized, and no commits have been made on the + # initial branch. + # * When a new branch is created using `git checkout --orphan `, starting + # a new branch with no history. + # + # **:detached**: `HEAD` points directly to a specific commit (identified by its + # SHA) rather than a branch reference. This state occurs when you check out a + # commit, a tag, or any state that is not directly associated with a branch. The + # branch name in this case is `HEAD`. + # + HeadState = Struct.new(:state, :name) + + # The current branch state which is the state of `HEAD` + # + # @return [HeadState] the state and name of the current branch + # + def current_branch_state + branch_name = command('branch', '--show-current') + return HeadState.new(:detached, 'HEAD') if branch_name.empty? + + state = + begin + command('rev-parse', '--verify', '--quiet', branch_name) + :active + rescue Git::FailedError => e + raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty? + + :unborn + end + + return HeadState.new(state, branch_name) + end + def branch_current - branches_all.select { |b| b[1] }.first[0] rescue nil + branch_name = command('branch', '--show-current') + branch_name.empty? ? 'HEAD' : branch_name end def branch_contains(commit, branch_name="") diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index 2256f4cb..aaea661f 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -50,6 +50,116 @@ def setup end end + # Git::Lib#current_branch_state + + test 'Git::Lib#current_branch_state -- empty repository' do + in_temp_dir do + `git init --initial-branch=my_initial_branch` + git = Git.open('.') + expected_state = Git::Lib::HeadState.new(:unborn, 'my_initial_branch') + assert_equal(expected_state, git.lib.current_branch_state) + end + end + + test 'Git::Lib#current_branch_state -- new orphan branch' do + in_temp_dir do + `git init --initial-branch=main` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + `git checkout --orphan orphan_branch 2> #{File::NULL}` + git = Git.open('.') + expected_state = Git::Lib::HeadState.new(:unborn, 'orphan_branch') + assert_equal(expected_state, git.lib.current_branch_state) + end + end + + test 'Git::Lib#current_branch_state -- active branch' do + in_temp_dir do + `git init --initial-branch=my_branch` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + git = Git.open('.') + expected_state = Git::Lib::HeadState.new(:active, 'my_branch') + assert_equal(expected_state, git.lib.current_branch_state) + end + end + + test 'Git::Lib#current_branch_state -- detached HEAD' do + in_temp_dir do + `git init --initial-branch=main` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + `echo "update" > file1.txt` + `git add file1.txt` + `git commit -m "Second commit"` + `git checkout HEAD~1 2> #{File::NULL}` + git = Git.open('.') + expected_state = Git::Lib::HeadState.new(:detached, 'HEAD') + assert_equal(expected_state, git.lib.current_branch_state) + end + end + + # Git::Lib#branch_current + + test 'Git::Lib#branch_current -- active branch' do + in_temp_dir do + `git init --initial-branch=main` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + git = Git.open('.') + assert_equal('main', git.lib.branch_current) + end + end + + test 'Git::Lib#branch_current -- unborn branch' do + in_temp_dir do + `git init --initial-branch=new_branch` + git = Git.open('.') + assert_equal('new_branch', git.lib.branch_current) + end + end + + test 'Git::Lib#branch_current -- detached HEAD' do + in_temp_dir do + `git init --initial-branch=main` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + `echo "update" > file1.txt` + `git add file1.txt` + `git commit -m "Second commit"` + `git checkout HEAD~1 2> #{File::NULL}` + git = Git.open('.') + assert_equal('HEAD', git.lib.branch_current) + end + end + + # Git::Base#branch + + test 'Git::Base#branch with detached head' do + in_temp_dir do + `git init` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "Initial commit"` + `echo "hello to another world" > file2.txt` + `git add file2.txt` + `git commit -m "Add another world"` + `git checkout HEAD~1 2> #{File::NULL}` + + git = Git.open('.') + branch = git.branch + + assert_equal('HEAD', branch.name) + end + end + + # Git::Base#branchs + test 'Git::Base#branchs with detached head' do in_temp_dir do git = Git.init('.', initial_branch: 'master') From 471f5a800e9891296beb8ec9d469d28d3703a868 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 1 Sep 2024 10:32:52 -0700 Subject: [PATCH 35/72] Sanatize object ref sent to cat-file command --- lib/git/base.rb | 2 +- lib/git/lib.rb | 163 ++++++++++++++++++++++++----- lib/git/object.rb | 14 +-- tests/units/test_lib.rb | 95 +++++++++++++---- tests/units/test_object.rb | 2 +- tests/units/test_signed_commits.rb | 4 +- 6 files changed, 217 insertions(+), 63 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index 088d2a3d..0df9a5e3 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -650,7 +650,7 @@ def ls_tree(objectish, opts = {}) end def cat_file(objectish) - self.lib.object_contents(objectish) + self.lib.cat_file(objectish) end # The name of the branch HEAD refers to or 'HEAD' if detached diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 4f519ec3..d6bf4f6e 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -353,21 +353,104 @@ def name_rev(commit_ish) alias :namerev :name_rev - def object_type(sha) - command('cat-file', '-t', sha) + # Output the contents or other properties of one or more objects. + # + # @see https://git-scm.com/docs/git-cat-file git-cat-file + # + # @param object [String] the object whose contents to return + # @param opts [Hash] the options for this command + # @option opts [Boolean] :tag + # @option opts [Boolean] :size + # @option opts + # + # + # @return [String] the object contents + # + # @raise [ArgumentError] if object is a string starting with a hyphen + # + def cat_file_contents(object, &block) + assert_args_are_not_options('object', object) + + if block_given? + Tempfile.create do |file| + # If a block is given, write the output from the process to a temporary + # file and then yield the file to the block + # + command('cat-file', "-p", object, out: file, err: file) + file.rewind + yield file + end + else + # If a block is not given, return the file contents as a string + command('cat-file', '-p', object) + end end - def object_size(sha) - command('cat-file', '-s', sha).to_i + alias :object_contents :cat_file_contents + + # Get the type for the given object + # + # @see https://git-scm.com/docs/git-cat-file git-cat-file + # + # @param object [String] the object to get the type + # + # @return [String] the object type + # + # @raise [ArgumentError] if object is a string starting with a hyphen + # + def cat_file_type(object) + assert_args_are_not_options('object', object) + + command('cat-file', '-t', object) end - # returns useful array of raw commit object data - def commit_data(sha) - sha = sha.to_s - cdata = command_lines('cat-file', 'commit', sha) - process_commit_data(cdata, sha) + alias :object_type :cat_file_type + + # Get the size for the given object + # + # @see https://git-scm.com/docs/git-cat-file git-cat-file + # + # @param object [String] the object to get the type + # + # @return [String] the object type + # + # @raise [ArgumentError] if object is a string starting with a hyphen + # + def cat_file_size(object) + assert_args_are_not_options('object', object) + + command('cat-file', '-s', object).to_i end + alias :object_size :cat_file_size + + # Return a hash of commit data + # + # @see https://git-scm.com/docs/git-cat-file git-cat-file + # + # @param object [String] the object to get the type + # + # @return [Hash] commit data + # + # The returned commit data has the following keys: + # * tree [String] + # * parent [Array] + # * author [String] the author name, email, and commit timestamp + # * committer [String] the committer name, email, and merge timestamp + # * message [String] the commit message + # * gpgsig [String] the public signing key of the commit (if signed) + # + # @raise [ArgumentError] if object is a string starting with a hyphen + # + def cat_file_commit(object) + assert_args_are_not_options('object', object) + + cdata = command_lines('cat-file', 'commit', object) + process_commit_data(cdata, object) + end + + alias :commit_data :cat_file_commit + def process_commit_data(data, sha) hsh = { 'sha' => sha, @@ -402,12 +485,50 @@ def each_cat_file_header(data) end end - def tag_data(name) - sha = sha.to_s - tdata = command_lines('cat-file', 'tag', name) - process_tag_data(tdata, name) + # Return a hash of annotated tag data + # + # Does not work with lightweight tags. List all annotated tags in your repository with the following command: + # + # ```sh + # git for-each-ref --format='%(refname:strip=2)' refs/tags | while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done + # ``` + # + # @see https://git-scm.com/docs/git-cat-file git-cat-file + # + # @param object [String] the tag to retrieve + # + # @return [Hash] tag data + # + # Example tag data returned: + # ```ruby + # { + # "name" => "annotated_tag", + # "object" => "46abbf07e3c564c723c7c039a43ab3a39e5d02dd", + # "type" => "commit", + # "tag" => "annotated_tag", + # "tagger" => "Scott Chacon 1724799270 -0700", + # "message" => "Creating an annotated tag\n" + # } + # ``` + # + # The returned commit data has the following keys: + # * object [String] the sha of the tag object + # * type [String] + # * tag [String] tag name + # * tagger [String] the name and email of the user who created the tag and the timestamp of when the tag was created + # * message [String] the tag message + # + # @raise [ArgumentError] if object is a string starting with a hyphen + # + def cat_file_tag(object) + assert_args_are_not_options('object', object) + + tdata = command_lines('cat-file', 'tag', object) + process_tag_data(tdata, object) end + alias :tag_data :cat_file_tag + def process_tag_data(data, name) hsh = { 'name' => name } @@ -461,22 +582,6 @@ def process_commit_log_data(data) return hsh_array end - def object_contents(sha, &block) - if block_given? - Tempfile.create do |file| - # If a block is given, write the output from the process to a temporary - # file and then yield the file to the block - # - command('cat-file', "-p", sha, out: file, err: file) - file.rewind - yield file - end - else - # If a block is not given, return stdout - command('cat-file', '-p', sha) - end - end - def ls_tree(sha, opts = {}) data = { 'blob' => {}, 'tree' => {}, 'commit' => {} } diff --git a/lib/git/object.rb b/lib/git/object.rb index 6c4aada9..5d399523 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -27,7 +27,7 @@ def sha end def size - @size ||= @base.lib.object_size(@objectish) + @size ||= @base.lib.cat_file_size(@objectish) end # Get the object's contents. @@ -38,9 +38,9 @@ def size # Use this for large files so that they are not held in memory. def contents(&block) if block_given? - @base.lib.object_contents(@objectish, &block) + @base.lib.cat_file_contents(@objectish, &block) else - @contents ||= @base.lib.object_contents(@objectish) + @contents ||= @base.lib.cat_file_contents(@objectish) end end @@ -237,7 +237,7 @@ def commit? def check_commit return if @tree - data = @base.lib.commit_data(@objectish) + data = @base.lib.cat_file_commit(@objectish) set_commit(data) end @@ -254,7 +254,7 @@ def initialize(base, sha, name) end def annotated? - @annotated ||= (@base.lib.object_type(self.name) == 'tag') + @annotated ||= (@base.lib.cat_file_type(self.name) == 'tag') end def message @@ -279,7 +279,7 @@ def check_tag if !self.annotated? @message = @tagger = nil else - tdata = @base.lib.tag_data(@name) + tdata = @base.lib.cat_file_tag(@name) @message = tdata['message'].chomp @tagger = Git::Author.new(tdata['tagger']) end @@ -300,7 +300,7 @@ def self.new(base, objectish, type = nil, is_tag = false) return Git::Object::Tag.new(base, sha, objectish) end - type ||= base.lib.object_type(objectish) + type ||= base.lib.cat_file_type(objectish) klass = case type when /blob/ then Blob diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index 38694980..13e5c4b8 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -24,14 +24,20 @@ def test_fetch_unshallow end end - def test_commit_data - data = @lib.commit_data('1cc8667014381') + def test_cat_file_commit + data = @lib.cat_file_commit('1cc8667014381') assert_equal('scott Chacon 1194561188 -0800', data['author']) assert_equal('94c827875e2cadb8bc8d4cdd900f19aa9e8634c7', data['tree']) assert_equal("test\n", data['message']) assert_equal(["546bec6f8872efa41d5d97a369f669165ecda0de"], data['parent']) end + def test_cat_file_commit_with_bad_object + assert_raise(ArgumentError) do + @lib.cat_file_commit('--all') + end + end + def test_commit_with_date create_file("#{@wdir}/test_file_1", 'content tets_file_1') @lib.add('test_file_1') @@ -40,7 +46,7 @@ def test_commit_with_date @lib.commit('commit with date', date: author_date.strftime('%Y-%m-%dT%H:%M:%S %z')) - data = @lib.commit_data('HEAD') + data = @lib.cat_file_commit('HEAD') assert_equal("Scott Chacon #{author_date.strftime("%s %z")}", data['author']) end @@ -77,7 +83,7 @@ def test_commit_with_no_verify move_file(pre_commit_path_bak, pre_commit_path) # Verify the commit was created - data = @lib.commit_data('HEAD') + data = @lib.cat_file_commit('HEAD') assert_equal("commit with no verify and pre-commit file\n", data['message']) end @@ -208,45 +214,56 @@ def test_name_rev_with_invalid_commit_ish end end - def test_object_type - assert_equal('commit', @lib.object_type('1cc8667014381')) # commit - assert_equal('tree', @lib.object_type('1cc8667014381^{tree}')) #tree - assert_equal('blob', @lib.object_type('v2.5:example.txt')) #blob - assert_equal('commit', @lib.object_type('v2.5')) + def test_cat_file_type + assert_equal('commit', @lib.cat_file_type('1cc8667014381')) # commit + assert_equal('tree', @lib.cat_file_type('1cc8667014381^{tree}')) #tree + assert_equal('blob', @lib.cat_file_type('v2.5:example.txt')) #blob + assert_equal('commit', @lib.cat_file_type('v2.5')) + end + + def test_cat_file_type_with_bad_object + assert_raise(ArgumentError) do + @lib.cat_file_type('--batch') + end + end + + def test_cat_file_size + assert_equal(265, @lib.cat_file_size('1cc8667014381')) # commit + assert_equal(72, @lib.cat_file_size('1cc8667014381^{tree}')) #tree + assert_equal(128, @lib.cat_file_size('v2.5:example.txt')) #blob + assert_equal(265, @lib.cat_file_size('v2.5')) end - def test_object_size - assert_equal(265, @lib.object_size('1cc8667014381')) # commit - assert_equal(72, @lib.object_size('1cc8667014381^{tree}')) #tree - assert_equal(128, @lib.object_size('v2.5:example.txt')) #blob - assert_equal(265, @lib.object_size('v2.5')) + def test_cat_file_size_with_bad_object + assert_raise(ArgumentError) do + @lib.cat_file_size('--batch') + end end - def test_object_contents + def test_cat_file_contents commit = "tree 94c827875e2cadb8bc8d4cdd900f19aa9e8634c7\n" commit << "parent 546bec6f8872efa41d5d97a369f669165ecda0de\n" commit << "author scott Chacon 1194561188 -0800\n" commit << "committer scott Chacon 1194561188 -0800\n" commit << "\ntest" - assert_equal(commit, @lib.object_contents('1cc8667014381')) # commit + assert_equal(commit, @lib.cat_file_contents('1cc8667014381')) # commit tree = "040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" tree << "100644 blob 3aac4b445017a8fc07502670ec2dbf744213dd48\texample.txt" - assert_equal(tree, @lib.object_contents('1cc8667014381^{tree}')) #tree + assert_equal(tree, @lib.cat_file_contents('1cc8667014381^{tree}')) #tree blob = "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n2" - assert_equal(blob, @lib.object_contents('v2.5:example.txt')) #blob - + assert_equal(blob, @lib.cat_file_contents('v2.5:example.txt')) #blob end - def test_object_contents_with_block + def test_cat_file_contents_with_block commit = "tree 94c827875e2cadb8bc8d4cdd900f19aa9e8634c7\n" commit << "parent 546bec6f8872efa41d5d97a369f669165ecda0de\n" commit << "author scott Chacon 1194561188 -0800\n" commit << "committer scott Chacon 1194561188 -0800\n" commit << "\ntest" - @lib.object_contents('1cc8667014381') do |f| + @lib.cat_file_contents('1cc8667014381') do |f| assert_equal(commit, f.read.chomp) end @@ -255,17 +272,23 @@ def test_object_contents_with_block tree = "040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" tree << "100644 blob 3aac4b445017a8fc07502670ec2dbf744213dd48\texample.txt" - @lib.object_contents('1cc8667014381^{tree}') do |f| + @lib.cat_file_contents('1cc8667014381^{tree}') do |f| assert_equal(tree, f.read.chomp) #tree end blob = "1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n2" - @lib.object_contents('v2.5:example.txt') do |f| + @lib.cat_file_contents('v2.5:example.txt') do |f| assert_equal(blob, f.read.chomp) #blob end end + def test_cat_file_contents_with_bad_object + assert_raise(ArgumentError) do + @lib.cat_file_contents('--all') + end + end + # returns Git::Branch object array def test_branches_all branches = @lib.branches_all @@ -395,4 +418,30 @@ def test_empty_when_empty assert_true(git.lib.empty?) end end + + def test_cat_file_tag + expected_cat_file_tag_keys = %w[name object type tag tagger message].sort + + in_temp_repo('working') do + # Creeate an annotated tag: + `git tag -a annotated_tag -m 'Creating an annotated tag'` + + git = Git.open('.') + cat_file_tag = git.lib.cat_file_tag('annotated_tag') + + assert_equal(expected_cat_file_tag_keys, cat_file_tag.keys.sort) + assert_equal('annotated_tag', cat_file_tag['name']) + assert_equal('46abbf07e3c564c723c7c039a43ab3a39e5d02dd', cat_file_tag['object']) + assert_equal('commit', cat_file_tag['type']) + assert_equal('annotated_tag', cat_file_tag['tag']) + assert_match(/^Scott Chacon \d+ [+-]\d+$/, cat_file_tag['tagger']) + assert_equal("Creating an annotated tag\n", cat_file_tag['message']) + end + end + + def test_cat_file_tag_with_bad_object + assert_raise(ArgumentError) do + @lib.cat_file_tag('--all') + end + end end diff --git a/tests/units/test_object.rb b/tests/units/test_object.rb index 3f31b390..03f8d24d 100644 --- a/tests/units/test_object.rb +++ b/tests/units/test_object.rb @@ -62,7 +62,7 @@ def test_object_to_s assert_equal('ba492c62b6227d7f3507b4dcc6e6d5f13790eabf', @blob.sha) end - def test_object_size + def test_cat_file_size assert_equal(265, @commit.size) assert_equal(72, @tree.size) assert_equal(128, @blob.size) diff --git a/tests/units/test_signed_commits.rb b/tests/units/test_signed_commits.rb index 871b92a5..c50fa62f 100644 --- a/tests/units/test_signed_commits.rb +++ b/tests/units/test_signed_commits.rb @@ -24,7 +24,7 @@ def in_repo_with_signing_config(&block) end end - def test_commit_data + def test_cat_file_commit # Signed commits should work on windows, but this test is omitted until the setup # on windows can be figured out omit('Omit testing of signed commits on Windows') if windows_platform? @@ -34,7 +34,7 @@ def test_commit_data `git add README.md` `git commit -S -m "Signed, sealed, delivered"` - data = Git.open('.').lib.commit_data('HEAD') + data = Git.open('.').lib.cat_file_commit('HEAD') assert_match(SSH_SIGNATURE_REGEXP, data['gpgsig']) assert_equal("Signed, sealed, delivered\n", data['message']) From f8bc987a3b75cc6737f3cb82b8e8f197309ae324 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 1 Sep 2024 14:32:17 -0700 Subject: [PATCH 36/72] Fix windows CI build error --- lib/git/lib.rb | 11 +++++----- tests/units/test_lib.rb | 2 +- tests/units/test_logger.rb | 45 +++++++++++++++++++------------------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index d6bf4f6e..f0cd2713 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -357,12 +357,13 @@ def name_rev(commit_ish) # # @see https://git-scm.com/docs/git-cat-file git-cat-file # - # @param object [String] the object whose contents to return - # @param opts [Hash] the options for this command - # @option opts [Boolean] :tag - # @option opts [Boolean] :size - # @option opts + # @example Get the contents of a file without a block + # lib.cat_file_contents('README.md') # => "This is a README file\n" # + # @example Get the contents of a file with a block + # lib.cat_file_contents('README.md') { |f| f.read } # => "This is a README file\n" + # + # @param object [String] the object whose contents to return # # @return [String] the object contents # diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index 13e5c4b8..74be8dcd 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -424,7 +424,7 @@ def test_cat_file_tag in_temp_repo('working') do # Creeate an annotated tag: - `git tag -a annotated_tag -m 'Creating an annotated tag'` + `git tag -a annotated_tag -m "Creating an annotated tag"` git = Git.open('.') cat_file_tag = git.lib.cat_file_tag('annotated_tag') diff --git a/tests/units/test_logger.rb b/tests/units/test_logger.rb index 470a2ed8..ced39292 100644 --- a/tests/units/test_logger.rb +++ b/tests/units/test_logger.rb @@ -17,39 +17,40 @@ def unexpected_log_entry end def test_logger - log = Tempfile.new('logfile') - log.close + in_temp_dir do |path| + log_path = 'logfile.log' - logger = Logger.new(log.path) - logger.level = Logger::DEBUG + logger = Logger.new(log_path, level: Logger::DEBUG) - @git = Git.open(@wdir, :log => logger) - @git.branches.size + @git = Git.open(@wdir, :log => logger) + @git.branches.size - logc = File.read(log.path) + logc = File.read(log_path) - expected_log_entry = /INFO -- : \["git", "(?.*?)", "branch", "-a"/ - assert_match(expected_log_entry, logc, missing_log_entry) + expected_log_entry = /INFO -- : \["git", "(?.*?)", "branch", "-a"/ + assert_match(expected_log_entry, logc, missing_log_entry) - expected_log_entry = /DEBUG -- : stdout:\n" cherry/ - assert_match(expected_log_entry, logc, missing_log_entry) + expected_log_entry = /DEBUG -- : stdout:\n" cherry/ + assert_match(expected_log_entry, logc, missing_log_entry) + end 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 + in_temp_dir do |path| + log_path = 'logfile.log' - @git = Git.open(@wdir, :log => logger) - @git.branches.size + logger = Logger.new(log_path, level: Logger::INFO) - logc = File.read(log.path) + @git = Git.open(@wdir, :log => logger) + @git.branches.size - expected_log_entry = /INFO -- : \["git", "(?.*?)", "branch", "-a"/ - assert_match(expected_log_entry, logc, missing_log_entry) + logc = File.read(log_path) - expected_log_entry = /DEBUG -- : stdout:\n" cherry/ - assert_not_match(expected_log_entry, logc, unexpected_log_entry) + expected_log_entry = /INFO -- : \["git", "(?.*?)", "branch", "-a"/ + assert_match(expected_log_entry, logc, missing_log_entry) + + expected_log_entry = /DEBUG -- : stdout:\n" cherry/ + assert_not_match(expected_log_entry, logc, unexpected_log_entry) + end end end From f5299a9fdbe10f8864ccc0e4ae93705e5727a1d3 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 1 Sep 2024 14:48:52 -0700 Subject: [PATCH 37/72] Release v2.3.0 Signed-off-by: James Couball --- CHANGELOG.md | 10 ++++++++++ lib/git/version.rb | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9120219..910fc4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ # Change Log +## v2.3.0 (2024-09-01) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.2.0..v2.3.0) + +Changes since v2.2.0: + +* f8bc987 Fix windows CI build error +* 471f5a8 Sanatize object ref sent to cat-file command +* 604a9a2 Make Git::Base#branch work when HEAD is detached + ## v2.2.0 (2024-08-26) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.1.1..v2.2.0) diff --git a/lib/git/version.rb b/lib/git/version.rb index 15f996be..33cf0b9b 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='2.2.0' + VERSION='2.3.0' end From 70565e372b35b3cde272534d63e598790b47b36c Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 1 Sep 2024 23:21:30 -0700 Subject: [PATCH 38/72] Add Git.binary_version to return the version of the git command line --- CONTRIBUTING.md | 66 ++++++++++++++++++++++++-- lib/git.rb | 11 +++++ lib/git/base.rb | 14 ++++++ tests/units/test_git_binary_version.rb | 54 +++++++++++++++++++++ 4 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 tests/units/test_git_binary_version.rb diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 082a8853..92527acf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,6 +3,9 @@ # @title How To Contribute --> +# Contributing to the git gem + +* [Summary](#summary) * [How to contribute](#how-to-contribute) * [How to report an issue or request a feature](#how-to-report-an-issue-or-request-a-feature) * [How to submit a code or documentation change](#how-to-submit-a-code-or-documentation-change) @@ -19,9 +22,9 @@ * [Continuous integration](#continuous-integration) * [Documentation](#documentation) * [Licensing](#licensing) +* [Building a specific version of the git command-line](#building-a-specific-version-of-the-git-command-line) - -# Contributing to the git gem +## Summary Thank you for your interest in contributing to the `ruby-git` project. @@ -172,6 +175,9 @@ $ bin/test test_object test_archive # run all unit tests: $ bin/test + +# run unit tests with a different version of the git command line: +$ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test ``` ### Continuous integration @@ -190,4 +196,58 @@ New and updated public-facing features should be documented in the project's [RE `ruby-git` uses [the MIT license](https://choosealicense.com/licenses/mit/) as declared in the [LICENSE](LICENSE) file. -Licensing is critical to open-source projects as it ensures the software remains available under the terms desired by the author. \ No newline at end of file +Licensing is critical to open-source projects as it ensures the software remains available under the terms desired by the author. + +## Building a specific version of the git command-line + +For testing, it is helpful to be able to build and use a specific version of the git +command-line with the git gem. + +Instructions to do this can be found on the page [How to install +Git](https://www.atlassian.com/git/tutorials/install-git) from Atlassian. + +I have successfully used the instructions in the section "Build Git from source on OS +X" on MacOS 15. I have copied the following instructions from the Atlassian page. + +1. From your terminal install XCode's Command Line Tools: + + ```shell + xcode-select --install + ``` + +2. Install [Homebrew](http://brew.sh/) + +3. Using Homebrew, install openssl: + + ```shell + brew install openssl + ``` + +4. Download the source tarball for the desired version from + [here](https://mirrors.edge.kernel.org/pub/software/scm/git/) and extract it + +5. Build Git run make with the following command: + + ```shell + NO_GETTEXT=1 make CFLAGS="-I/usr/local/opt/openssl/include" LDFLAGS="-L/usr/local/opt/openssl/lib" + ``` + +6. The newly built git command will be found at `bin-wrappers/git` + +7. Use the new git command-line version + + Configure the git gem to use the newly built version: + + ```ruby + require 'git' + # set the binary path + Git.configure { |config| config.binary_path = '/Users/james/Downloads/git-2.30.2/bin-wrappers/git' } + # validate the version + assert_equal([2, 30, 2], Git.binary_version) + ``` + + or run tests using the newly built version: + + ```shell + GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test + ``` diff --git a/lib/git.rb b/lib/git.rb index e995e96c..6d0f3032 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -381,4 +381,15 @@ def self.ls_remote(location = nil, options = {}) def self.open(working_dir, options = {}) Base.open(working_dir, options) end + + # Return the version of the git binary + # + # @example + # Git.binary_version # => [2, 46, 0] + # + # @return [Array] the version of the git binary + # + def self.binary_version(binary_path = Git::Base.config.binary_path) + Base.binary_version(binary_path) + end end diff --git a/lib/git/base.rb b/lib/git/base.rb index 0df9a5e3..8a987313 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -36,6 +36,20 @@ def self.config @@config ||= Config.new end + def self.binary_version(binary_path) + git_cmd = "#{binary_path} -c core.quotePath=true -c color.ui=false version 2>&1" + result, status = Open3.capture2(git_cmd) + result = result.chomp + + if status.success? + version = result[/\d+(\.\d+)+/] + version_parts = version.split('.').collect { |i| i.to_i } + version_parts.fill(0, version_parts.length...3) + else + raise RuntimeError, "Failed to get git version: #{status}\n#{result}" + end + end + # (see Git.init) def self.init(directory = '.', options = {}) normalize_paths(options, default_working_directory: directory, default_repository: directory, bare: options[:bare]) diff --git a/tests/units/test_git_binary_version.rb b/tests/units/test_git_binary_version.rb new file mode 100644 index 00000000..09afc1a1 --- /dev/null +++ b/tests/units/test_git_binary_version.rb @@ -0,0 +1,54 @@ +require 'test_helper' + +class TestGitBinaryVersion < Test::Unit::TestCase + def windows_mocked_git_binary = <<~GIT_SCRIPT + @echo off + # Loop through the arguments and check for the version command + for %%a in (%*) do ( + if "%%a" == "version" ( + echo git version 1.2.3 + exit /b 0 + ) + ) + exit /b 1 + GIT_SCRIPT + + def linux_mocked_git_binary = <<~GIT_SCRIPT + #!/bin/sh + # Loop through the arguments and check for the version command + for arg in "$@"; do + if [ "$arg" = "version" ]; then + echo "git version 1.2.3" + exit 0 + fi + done + exit 1 + GIT_SCRIPT + + def test_binary_version_windows + omit('Only implemented for Windows') unless windows_platform? + + in_temp_dir do |path| + git_binary_path = File.join(path, 'my_git.bat') + File.write(git_binary_path, windows_mocked_git_binary) + assert_equal([1, 2, 3], Git.binary_version(git_binary_path)) + end + end + + def test_binary_version_linux + omit('Only implemented for Linux') if windows_platform? + + in_temp_dir do |path| + git_binary_path = File.join(path, 'my_git.bat') + File.write(git_binary_path, linux_mocked_git_binary) + File.chmod(0755, git_binary_path) + assert_equal([1, 2, 3], Git.binary_version(git_binary_path)) + end + end + + def test_binary_version_bad_binary_path + assert_raise RuntimeError do + Git.binary_version('/path/to/nonexistent/git') + end + end +end From 2e23d47922837729e7c73521af5177ef981eb177 Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 2 Sep 2024 11:09:38 -0700 Subject: [PATCH 39/72] Update instructions for building a specific version of Git --- CONTRIBUTING.md | 113 +++++++++++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 44 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92527acf..10793a4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,8 +21,12 @@ * [Unit tests](#unit-tests) * [Continuous integration](#continuous-integration) * [Documentation](#documentation) +* [Building a specific version of the Git command-line](#building-a-specific-version-of-the-git-command-line) + * [Install pre-requisites](#install-pre-requisites) + * [Obtain Git source code](#obtain-git-source-code) + * [Build git](#build-git) + * [Use the new Git version](#use-the-new-git-version) * [Licensing](#licensing) -* [Building a specific version of the git command-line](#building-a-specific-version-of-the-git-command-line) ## Summary @@ -182,72 +186,93 @@ $ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test ### Continuous integration -All tests must pass in the project's [GitHub Continuous Integration build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) before the pull request will be merged. +All tests must pass in the project's [GitHub Continuous Integration +build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) before the +pull request will be merged. -The [Continuous Integration workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml) runs both `bundle exec rake default` and `bundle exec rake test:gem` from the project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile). +The [Continuous Integration +workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml) +runs both `bundle exec rake default` and `bundle exec rake test:gem` from the +project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile). ### Documentation -New and updated public methods must include [YARD](https://yardoc.org/) documentation. +New and updated public methods must include [YARD](https://yardoc.org/) +documentation. -New and updated public-facing features should be documented in the project's [README.md](README.md). +New and updated public-facing features should be documented in the project's +[README.md](README.md). -## Licensing +## Building a specific version of the Git command-line + +To test with a specific version of the Git command-line, you may need to build that +version from source code. The following instructions are adapted from Atlassian’s +[How to install Git](https://www.atlassian.com/git/tutorials/install-git) page for +building Git on macOS. + +### Install pre-requisites + +Prerequisites only need to be installed if they are not already present. + +From your terminal, install Xcode’s Command Line Tools: + +```shell +xcode-select --install +``` -`ruby-git` uses [the MIT license](https://choosealicense.com/licenses/mit/) as declared in the [LICENSE](LICENSE) file. +Install [Homebrew](http://brew.sh/) by following the instructions on the Homebrew +page. -Licensing is critical to open-source projects as it ensures the software remains available under the terms desired by the author. +Using Homebrew, install OpenSSL: -## Building a specific version of the git command-line +```shell +brew install openssl +``` -For testing, it is helpful to be able to build and use a specific version of the git -command-line with the git gem. +### Obtain Git source code -Instructions to do this can be found on the page [How to install -Git](https://www.atlassian.com/git/tutorials/install-git) from Atlassian. +Download and extract the source tarball for the desired Git version from [this source +code mirror](https://mirrors.edge.kernel.org/pub/software/scm/git/). -I have successfully used the instructions in the section "Build Git from source on OS -X" on MacOS 15. I have copied the following instructions from the Atlassian page. +### Build git -1. From your terminal install XCode's Command Line Tools: +From your terminal, change to the root directory of the extracted source code and run +the build with following command: - ```shell - xcode-select --install - ``` +```shell +NO_GETTEXT=1 make CFLAGS="-I/usr/local/opt/openssl/include" LDFLAGS="-L/usr/local/opt/openssl/lib" +``` -2. Install [Homebrew](http://brew.sh/) +The build script will place the newly compiled Git executables in the `bin-wrappers` +directory (e.g., `bin-wrappers/git`). -3. Using Homebrew, install openssl: +### Use the new Git version - ```shell - brew install openssl - ``` +To configure programs that use the Git gem to utilize the newly built version, do the +following: -4. Download the source tarball for the desired version from - [here](https://mirrors.edge.kernel.org/pub/software/scm/git/) and extract it +```ruby +require 'git' -5. Build Git run make with the following command: +# Set the binary path +Git.configure { |c| c.binary_path = '/Users/james/Downloads/git-2.30.2/bin-wrappers/git' } - ```shell - NO_GETTEXT=1 make CFLAGS="-I/usr/local/opt/openssl/include" LDFLAGS="-L/usr/local/opt/openssl/lib" - ``` +# Validate the version (if desired) +assert_equal([2, 30, 2], Git.binary_version) +``` -6. The newly built git command will be found at `bin-wrappers/git` +Tests can be run using the newly built Git version as follows: -7. Use the new git command-line version +```shell +GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test +``` - Configure the git gem to use the newly built version: +Note: `GIT_PATH` refers to the directory containing the `git` executable. - ```ruby - require 'git' - # set the binary path - Git.configure { |config| config.binary_path = '/Users/james/Downloads/git-2.30.2/bin-wrappers/git' } - # validate the version - assert_equal([2, 30, 2], Git.binary_version) - ``` +## Licensing - or run tests using the newly built version: +`ruby-git` uses [the MIT license](https://choosealicense.com/licenses/mit/) as +declared in the [LICENSE](LICENSE) file. - ```shell - GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test - ``` +Licensing is critical to open-source projects as it ensures the software remains +available under the terms desired by the author. From da6fa6ed1455116d0bcea52a1c89f6354c96906e Mon Sep 17 00:00:00 2001 From: Costa Shapiro Date: Wed, 23 Oct 2024 11:18:20 +0300 Subject: [PATCH 40/72] Conatinerised the test suite with Docker: - the entry point (in a Docker-enabled env) is `bin/tests` - fixed the (rather invasive outside of a container) `bin/test` test runner --- bin/test | 6 +++--- bin/tests | 11 +++++++++++ tests/Dockerfile | 13 +++++++++++++ tests/docker-compose.yml | 5 +++++ 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100755 bin/tests create mode 100644 tests/Dockerfile create mode 100644 tests/docker-compose.yml diff --git a/bin/test b/bin/test index 8024c5ab..021d6c35 100755 --- a/bin/test +++ b/bin/test @@ -3,9 +3,9 @@ require 'bundler/setup' -`git config --global user.email "git@example.com"` if `git config user.email`.empty? -`git config --global user.name "GitExample"` if `git config user.name`.empty? -`git config --global init.defaultBranch master` if `git config init.defaultBranch`.empty? +`git config --global user.email "git@example.com"` if `git config --global user.email`.empty? +`git config --global user.name "GitExample"` if `git config --global user.name`.empty? +`git config --global init.defaultBranch master` if `git config --global init.defaultBranch`.empty? project_root = File.expand_path(File.join(__dir__, '..')) diff --git a/bin/tests b/bin/tests new file mode 100755 index 00000000..5e22f902 --- /dev/null +++ b/bin/tests @@ -0,0 +1,11 @@ +#!/bin/bash -e +test "$#" -ne 0 && echo "Unsupported args: $@" >&2 && exit 145 +cd "$( dirname "${BASH_SOURCE[0]}" )"/.. + +export COMPOSE_FILE=tests/docker-compose.yml +export COMPOSE_PROJECT_NAME=ruby-git_dev + +docker-compose rm -svf +docker-compose build --force-rm + +docker-compose run --rm tester && docker-compose rm -svf || ( docker-compose logs && exit 1 ) diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 00000000..5e90e419 --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,13 @@ +FROM ruby + +WORKDIR /ruby-git + + +ADD Gemfile git.gemspec .git* ./ +ADD lib/git/version.rb ./lib/git/version.rb +RUN bundle install + +ADD . . + +ENTRYPOINT ["bundle", "exec"] +CMD ["bin/test"] diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 00000000..c8337d44 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,5 @@ +services: + tester: + build: + context: .. + dockerfile: tests/Dockerfile From 2e79dbe657ae66905402dc663d6efbc18e445d16 Mon Sep 17 00:00:00 2001 From: Costa Shapiro Date: Wed, 23 Oct 2024 11:23:08 +0300 Subject: [PATCH 41/72] Fixed "unbranched" stash message support: - the tests are generously provided by James Couball - more proper stash metadata parsing introduced - supporting both "branched" ("On : ...") and "unbranched" messages - which might affect the future 3.x behaviour wrt "un/branched" stashes --- lib/git/lib.rb | 6 ++- tests/units/test_stashes.rb | 103 ++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index f0cd2713..83865b85 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1134,8 +1134,10 @@ def stashes_all if File.exist?(filename) File.open(filename) do |f| f.each_with_index do |line, i| - m = line.match(/:(.*)$/) - arr << [i, m[1].strip] + _, msg = line.split("\t") + # NOTE this logic may be removed/changed in 3.x + m = msg.match(/^[^:]+:(.*)$/) + arr << [i, (m ? m[1] : msg).strip] end end end diff --git a/tests/units/test_stashes.rb b/tests/units/test_stashes.rb index e147ae9c..d6aa4087 100644 --- a/tests/units/test_stashes.rb +++ b/tests/units/test_stashes.rb @@ -44,4 +44,107 @@ def test_stashes_all assert(stashes[0].include?('testing-stash-all')) end end + test 'Git::Lib#stashes_all' do + in_bare_repo_clone do |g| + assert_equal(0, g.branch.stashes.size) + new_file('test-file1', 'blahblahblah1') + new_file('test-file2', 'blahblahblah2') + assert(g.status.untracked.assoc('test-file1')) + + g.add + + assert(g.status.added.assoc('test-file1')) + + g.branch.stashes.save('testing-stash-all') + + # puts `cat .git/logs/refs/stash` + # 0000000000000000000000000000000000000000 b9b008cd179b0e8c4b8cda35bac43f7011a0836a James Couball 1729463252 -0700 On master: testing-stash-all + + stashes = assert_nothing_raised { g.lib.stashes_all } + + expected_stashes = [ + [0, 'testing-stash-all'] + ] + + assert_equal(expected_stashes, stashes) + end + end + + test 'Git::Lib#stashes_all - stash message has colon' do + in_bare_repo_clone do |g| + assert_equal(0, g.branch.stashes.size) + new_file('test-file1', 'blahblahblah1') + new_file('test-file2', 'blahblahblah2') + assert(g.status.untracked.assoc('test-file1')) + + g.add + + assert(g.status.added.assoc('test-file1')) + + g.branch.stashes.save('saving: testing-stash-all') + + # puts `cat .git/logs/refs/stash` + # 0000000000000000000000000000000000000000 b9b008cd179b0e8c4b8cda35bac43f7011a0836a James Couball 1729463252 -0700 On master: saving: testing-stash-all + + stashes = assert_nothing_raised { g.lib.stashes_all } + + expected_stashes = [ + [0, 'saving: testing-stash-all'] + ] + + assert_equal(expected_stashes, stashes) + end + end + + test 'Git::Lib#stashes_all -- git stash message with no branch and no colon' do + in_temp_dir do + `git init` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + `echo "update" > file1.txt` + commit = `git stash create "stash message"`.chomp + # Create a stash with this message: 'custom message' + `git stash store -m "custom message" #{commit}` + + # puts `cat .git/logs/refs/stash` + # 0000000000000000000000000000000000000000 0550a54ed781eda364ca3c22fcc46c37acae4bd6 James Couball 1729460302 -0700 custom message + + git = Git.open('.') + + stashes = assert_nothing_raised { git.lib.stashes_all } + + expected_stashes = [ + [0, 'custom message'] + ] + + assert_equal(expected_stashes, stashes) + end + end + + test 'Git::Lib#stashes_all -- git stash message with no branch and explicit colon' do + in_temp_dir do + `git init` + `echo "hello world" > file1.txt` + `git add file1.txt` + `git commit -m "First commit"` + `echo "update" > file1.txt` + commit = `git stash create "stash message"`.chomp + # Create a stash with this message: 'custom message' + `git stash store -m "testing: custom message" #{commit}` + + # puts `cat .git/logs/refs/stash` + # 0000000000000000000000000000000000000000 eadd7858e53ea4fb8b1383d69cade1806d948867 James Couball 1729462039 -0700 testing: custom message + + git = Git.open('.') + + stashes = assert_nothing_raised { git.lib.stashes_all } + + expected_stashes = [ + [0, 'custom message'] + ] + + assert_equal(expected_stashes, stashes) + end + end end From 51f781c7f6adc22a4effb0dea5b1dac156caf2d9 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 23 Oct 2024 08:57:56 -0700 Subject: [PATCH 42/72] test: remove duplicate test from test_stashes.rb --- tests/units/test_stashes.rb | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/units/test_stashes.rb b/tests/units/test_stashes.rb index d6aa4087..0516f273 100644 --- a/tests/units/test_stashes.rb +++ b/tests/units/test_stashes.rb @@ -26,24 +26,6 @@ def test_stash_unstash end end - def test_stashes_all - in_bare_repo_clone do |g| - assert_equal(0, g.branch.stashes.size) - new_file('test-file1', 'blahblahblah1') - new_file('test-file2', 'blahblahblah2') - assert(g.status.untracked.assoc('test-file1')) - - g.add - - assert(g.status.added.assoc('test-file1')) - - g.branch.stashes.save('testing-stash-all') - - stashes = g.branch.stashes.all - - assert(stashes[0].include?('testing-stash-all')) - end - end test 'Git::Lib#stashes_all' do in_bare_repo_clone do |g| assert_equal(0, g.branch.stashes.size) From f4747e143c4e8eb0ff75703018f7d26773198874 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 23 Oct 2024 09:23:38 -0700 Subject: [PATCH 43/72] test: rename bin/tests to bin/test-in-docker --- bin/{tests => test-in-docker} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename bin/{tests => test-in-docker} (100%) diff --git a/bin/tests b/bin/test-in-docker similarity index 100% rename from bin/tests rename to bin/test-in-docker From e236007d99ff1198225160eda94c5389797decde Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 23 Oct 2024 09:31:05 -0700 Subject: [PATCH 44/72] test: allow bin/test-in-docker to accept the test file(s) to run on command line --- bin/test | 6 ++++++ bin/test-in-docker | 10 ++++++++-- tests/Dockerfile | 3 +-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/bin/test b/bin/test index 021d6c35..599ecbd9 100755 --- a/bin/test +++ b/bin/test @@ -1,6 +1,12 @@ #!/usr/bin/env ruby # frozen_string_literal: true +# This script is used to run the tests for this project. +# +# bundle exec bin/test [test_file_name ...] +# +# If no test file names are provided, all tests in the `tests/units` directory will be run. + require 'bundler/setup' `git config --global user.email "git@example.com"` if `git config --global user.email`.empty? diff --git a/bin/test-in-docker b/bin/test-in-docker index 5e22f902..8775d56b 100755 --- a/bin/test-in-docker +++ b/bin/test-in-docker @@ -1,5 +1,11 @@ #!/bin/bash -e -test "$#" -ne 0 && echo "Unsupported args: $@" >&2 && exit 145 + +# This script is used to run the tests for this project in a Docker container. +# +# bin/test-in-docker [test_file_name ...] +# +# If no test file names are provided, all tests in the `tests/units` directory will be run. + cd "$( dirname "${BASH_SOURCE[0]}" )"/.. export COMPOSE_FILE=tests/docker-compose.yml @@ -8,4 +14,4 @@ export COMPOSE_PROJECT_NAME=ruby-git_dev docker-compose rm -svf docker-compose build --force-rm -docker-compose run --rm tester && docker-compose rm -svf || ( docker-compose logs && exit 1 ) +docker-compose run --rm tester "$@" && docker-compose rm -svf || ( docker-compose logs && exit 1 ) diff --git a/tests/Dockerfile b/tests/Dockerfile index 5e90e419..85690f59 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -9,5 +9,4 @@ RUN bundle install ADD . . -ENTRYPOINT ["bundle", "exec"] -CMD ["bin/test"] +ENTRYPOINT ["bundle", "exec", "bin/test"] From affe1a090136aa54e23628d2a9ab455e30800df4 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 23 Oct 2024 09:45:53 -0700 Subject: [PATCH 45/72] chore: release v2.3.1 Signed-off-by: James Couball --- CHANGELOG.md | 14 ++++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 910fc4ea..c570e416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ # Change Log +## v2.3.1 (2024-10-23) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.0..v2.3.1) + +Changes since v2.3.0: + +* e236007 test: allow bin/test-in-docker to accept the test file(s) to run on command line +* f4747e1 test: rename bin/tests to bin/test-in-docker +* 51f781c test: remove duplicate test from test_stashes.rb +* 2e79dbe Fixed "unbranched" stash message support: +* da6fa6e Conatinerised the test suite with Docker: +* 2e23d47 Update instructions for building a specific version of Git +* 70565e3 Add Git.binary_version to return the version of the git command line + ## v2.3.0 (2024-09-01) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.2.0..v2.3.0) diff --git a/lib/git/version.rb b/lib/git/version.rb index 33cf0b9b..abc0e3a7 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='2.3.0' + VERSION='2.3.1' end From 7646e38a16702119cdaa87f4ee3fc803c0a55d13 Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 19 Nov 2024 11:47:38 -0800 Subject: [PATCH 46/72] fix: improve error message for Git::Lib#branches_all When the output from `git branch -a` can not be parsed, return an error that shows the complete output, the line containing the error, and the line with the error. --- lib/git/lib.rb | 21 ++++++++++++++++++--- tests/units/test_lib.rb | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 83865b85..4128e173 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -362,7 +362,7 @@ def name_rev(commit_ish) # # @example Get the contents of a file with a block # lib.cat_file_contents('README.md') { |f| f.read } # => "This is a README file\n" - # + # # @param object [String] the object whose contents to return # # @return [String] the object contents @@ -641,10 +641,13 @@ def change_head_branch(branch_name) /x def branches_all - command_lines('branch', '-a').map do |line| + lines = command_lines('branch', '-a') + lines.each_with_index.map do |line, line_index| match_data = line.match(BRANCH_LINE_REGEXP) - raise Git::UnexpectedResultError, 'Unexpected branch line format' unless match_data + + raise Git::UnexpectedResultError, unexpected_branch_line_error(lines, line, line_index) unless match_data next nil if match_data[:not_a_branch] || match_data[:detached_ref] + [ match_data[:refname], !match_data[:current].nil?, @@ -654,6 +657,18 @@ def branches_all end.compact end + def unexpected_branch_line_error(lines, line, index) + <<~ERROR + Unexpected line in output from `git branch -a`, line #{index + 1} + + Full output: + #{lines.join("\n ")} + + Line #{index + 1}: + "#{line}" + ERROR + end + def worktrees_all arr = [] directory = '' diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index 74be8dcd..c92959d6 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -299,6 +299,39 @@ def test_branches_all assert(branches.select { |b| /master/.match(b[0]) }.size > 0) # has a master branch end + test 'Git::Lib#branches_all with unexpected output from git branches -a' do + # Mock command lines to return unexpected branch data + def @lib.command_lines(*_command) + <<~COMMAND_LINES.split("\n") + * (HEAD detached at origin/master) + this line should result in a Git::UnexpectedResultError + master + remotes/origin/HEAD -> origin/master + remotes/origin/master + COMMAND_LINES + end + + begin + branches = @lib.branches_all + rescue Git::UnexpectedResultError => e + assert_equal(<<~MESSAGE, e.message) + Unexpected line in output from `git branch -a`, line 2 + + Full output: + * (HEAD detached at origin/master) + this line should result in a Git::UnexpectedResultError + master + remotes/origin/HEAD -> origin/master + remotes/origin/master + + Line 2: + " this line should result in a Git::UnexpectedResultError" + MESSAGE + else + raise RuntimeError, 'Expected Git::UnexpectedResultError' + end + end + def test_config_remote config = @lib.config_remote('working') assert_equal('../working.git', config['url']) From 185c3f59dcab5254864b9b27d48b51970eb64690 Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 19 Nov 2024 12:00:42 -0800 Subject: [PATCH 47/72] Release v2.3.2 Signed-off-by: James Couball --- CHANGELOG.md | 8 ++++++++ lib/git/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c570e416..829dfcd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ # Change Log +## v2.3.2 (2024-11-19) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.1..v2.3.2) + +Changes since v2.3.1: + +* 7646e38 fix: improve error message for Git::Lib#branches_all + ## v2.3.1 (2024-10-23) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.0..v2.3.1) diff --git a/lib/git/version.rb b/lib/git/version.rb index abc0e3a7..c5710194 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='2.3.1' + VERSION='2.3.2' end From 60b58ba7eeceb248c65644bdb760bf137999c7fe Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 20 Nov 2024 09:18:34 -0800 Subject: [PATCH 48/72] test: add #run_command for tests to use instead of backticks --- tests/test_helper.rb | 55 ++++++++++++++++++++++++++++++++++++++ tests/units/test_branch.rb | 20 ++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/tests/test_helper.rb b/tests/test_helper.rb index 7be31378..f1b73422 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -162,4 +162,59 @@ def windows_platform? win_platform_regex = /mingw|mswin/ RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex end + + require 'delegate' + + # A wrapper around a ProcessExecuter::Status that also includes command output + # @api public + class CommandResult < SimpleDelegator + # Create a new CommandResult + # @example + # status = ProcessExecuter.spawn(*command, timeout:, out:, err:) + # CommandResult.new(status, out_buffer.string, err_buffer.string) + # @param status [ProcessExecuter::Status] The status of the process + # @param out [String] The standard output of the process + # @param err [String] The standard error of the process + def initialize(status, out, err) + super(status) + @out = out + @err = err + end + + # @return [String] The stdout output of the process + attr_reader :out + + # @return [String] The stderr output of the process + attr_reader :err + end + + # Run a command and return the status including stdout and stderr output + # + # @example + # command = %w[git status] + # status = run(command) + # status.success? # => true + # status.exitstatus # => 0 + # status.out # => "On branch master\nnothing to commit, working tree clean\n" + # status.err # => "" + # + # @param command [Array] The command to run + # @param timeout [Numeric, nil] Seconds to allow command to run before killing it or nil for no timeout + # @param raise_errors [Boolean] Raise an exception if the command fails + # @param error_message [String] The message to use when raising an exception + # + # @return [CommandResult] The result of running + # + def run_command(*command, timeout: nil, raise_errors: true, error_message: "#{command[0]} failed") + out_buffer = StringIO.new + out = ProcessExecuter::MonitoredPipe.new(out_buffer) + err_buffer = StringIO.new + err = ProcessExecuter::MonitoredPipe.new(err_buffer) + + status = ProcessExecuter.spawn(*command, timeout: timeout, out: out, err: err) + + raise "#{error_message}: #{err_buffer.string}" if raise_errors && !status.success? + + CommandResult.new(status, out_buffer.string, err_buffer.string) + end end diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index aaea661f..f150d878 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -50,6 +50,26 @@ def setup end end + test 'Git::Base#branches when checked out branch is a remote branch' do + in_temp_dir do + Dir.mkdir('remote_git') + Dir.chdir('remote_git') do + run_command 'git', 'init', '--initial-branch=main' + File.write('file1.txt', 'This is file1') + run_command 'git', 'add', 'file1.txt' + run_command 'git', 'commit', '-m', 'Add file1.txt' + end + + run_command 'git', 'clone', File.join('remote_git', '.git'), 'local_git' + + Dir.chdir('local_git') do + run_command 'git', 'checkout', 'origin/main' + git = Git.open('.') + assert_nothing_raised { git.branches } + end + end + end + # Git::Lib#current_branch_state test 'Git::Lib#current_branch_state -- empty repository' do From 5f43a1aa4e7b0ef94f0d793fee82ec4522d156c5 Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 3 Dec 2024 21:36:50 -0600 Subject: [PATCH 49/72] fix: open3 errors on binary paths with spaces --- lib/git/base.rb | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/git/base.rb b/lib/git/base.rb index 8a987313..ad9ca4ed 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -37,9 +37,15 @@ def self.config end def self.binary_version(binary_path) - git_cmd = "#{binary_path} -c core.quotePath=true -c color.ui=false version 2>&1" - result, status = Open3.capture2(git_cmd) - result = result.chomp + result = nil + status = nil + + begin + result, status = Open3.capture2e(binary_path, "-c", "core.quotePath=true", "-c", "color.ui=false", "version") + result = result.chomp + rescue Errno::ENOENT + raise RuntimeError, "Failed to get git version: #{binary_path} not found" + end if status.success? version = result[/\d+(\.\d+)+/] @@ -81,9 +87,12 @@ def self.root_of_worktree(working_dir) result = working_dir status = nil - git_cmd = "#{Git::Base.config.binary_path} -c core.quotePath=true -c color.ui=false rev-parse --show-toplevel 2>&1" - result, status = Open3.capture2(git_cmd, chdir: File.expand_path(working_dir)) - result = result.chomp + begin + result, status = Open3.capture2e(Git::Base.config.binary_path, "-c", "core.quotePath=true", "-c", "color.ui=false", "rev-parse", "--show-toplevel", chdir: File.expand_path(working_dir)) + result = result.chomp + rescue Errno::ENOENT + raise ArgumentError, "Failed to find the root of the worktree: git binary not found" + end raise ArgumentError, "'#{working_dir}' is not in a git working tree" unless status.success? result From c25e5e062b89685b6b2ef77ee87aa2fa3de5e3c2 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 4 Dec 2024 13:03:55 -0800 Subject: [PATCH 50/72] test: add tests for spaces in the git binary path or the working dir --- lib/git/base.rb | 2 + tests/test_helper.rb | 53 +++++++++++ tests/units/test_git_base_root_of_worktree.rb | 90 +++++++++++++++++++ tests/units/test_git_binary_version.rb | 32 ++++--- 4 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 tests/units/test_git_base_root_of_worktree.rb diff --git a/lib/git/base.rb b/lib/git/base.rb index ad9ca4ed..2e9f1951 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -87,6 +87,8 @@ def self.root_of_worktree(working_dir) result = working_dir status = nil + raise ArgumentError, "'#{working_dir}' does not exist" unless Dir.exist?(working_dir) + begin result, status = Open3.capture2e(Git::Base.config.binary_path, "-c", "core.quotePath=true", "-c", "color.ui=false", "rev-parse", "--show-toplevel", chdir: File.expand_path(working_dir)) result = result.chomp diff --git a/tests/test_helper.rb b/tests/test_helper.rb index f1b73422..0bb809ea 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -218,3 +218,56 @@ def run_command(*command, timeout: nil, raise_errors: true, error_message: "#{co CommandResult.new(status, out_buffer.string, err_buffer.string) end end + +# Replace the default git binary with the given script +# +# This method creates a temporary directory and writes the given script to a file +# named `git` in a subdirectory named `bin`. This subdirectory name can be changed by +# passing a different value for the `subdir` parameter. +# +# On non-windows platforms, make sure the script starts with a hash bang. On windows, +# make sure the script has a `.bat` extension. +# +# On non-windows platforms, the script is made executable. +# +# `Git::Base.config.binary_path` set to the path to the script. +# +# The block is called passing the path to the mocked git binary. +# +# `Git::Base.config.binary_path` is reset to its original value after the block +# returns. +# +# @example mocked_git_script = <<~GIT_SCRIPT #!/bin/sh puts 'git version 1.2.3' +# GIT_SCRIPT +# +# mock_git_binary(mocked_git_script) do +# # Run Git commands here -- they will call the mocked git script +# end +# +# @param script [String] The bash script to run instead of the real git binary +# +# @param subdir [String] The subdirectory to place the mocked git binary in +# +# @yield Call the block while the git binary is mocked +# +# @yieldparam git_binary_path [String] The path to the mocked git binary +# +# @yieldreturn [void] the return value of the block is ignored +# +# @return [void] +# +def mock_git_binary(script, subdir: 'bin') + Dir.mktmpdir do |binary_dir| + binary_name = windows_platform? ? 'git.bat' : 'git' + git_binary_path = File.join(binary_dir, subdir, binary_name) + FileUtils.mkdir_p(File.dirname(git_binary_path)) + File.write(git_binary_path, script) + File.chmod(0755, git_binary_path) unless windows_platform? + saved_binary_path = Git::Base.config.binary_path + Git::Base.config.binary_path = git_binary_path + + yield git_binary_path + + Git::Base.config.binary_path = saved_binary_path + end +end diff --git a/tests/units/test_git_base_root_of_worktree.rb b/tests/units/test_git_base_root_of_worktree.rb new file mode 100644 index 00000000..3a13b59e --- /dev/null +++ b/tests/units/test_git_base_root_of_worktree.rb @@ -0,0 +1,90 @@ +require 'test_helper' + +class TestGitBaseRootOfWorktree < Test::Unit::TestCase + def mocked_git_script(toplevel) = <<~GIT_SCRIPT + #!/bin/sh + # Loop through the arguments and check for the "rev-parse --show-toplevel" args + for arg in "$@"; do + if [ "$arg" = "version" ]; then + echo "git version 1.2.3" + exit 0 + elif [ "$arg" = "rev-parse" ]; then + REV_PARSE_ARG=true + elif [ "$REV_PARSE_ARG" = "true" ] && [ $arg = "--show-toplevel" ]; then + echo #{toplevel} + exit 0 + fi + done + exit 1 + GIT_SCRIPT + + def test_root_of_worktree + omit('Only implemented for non-windows platforms') if windows_platform? + + in_temp_dir do |toplevel| + `git init` + + mock_git_binary(mocked_git_script(toplevel)) do + working_dir = File.join(toplevel, 'config') + Dir.mkdir(working_dir) + + assert_equal(toplevel, Git::Base.root_of_worktree(working_dir)) + end + end + end + + def test_working_dir_has_spaces + omit('Only implemented for non-windows platforms') if windows_platform? + + in_temp_dir do |toplevel| + `git init` + + mock_git_binary(mocked_git_script(toplevel)) do + working_dir = File.join(toplevel, 'app config') + Dir.mkdir(working_dir) + + assert_equal(toplevel, Git::Base.root_of_worktree(working_dir)) + end + end + end + + def test_working_dir_does_not_exist + assert_raise ArgumentError do + Git::Base.root_of_worktree('/path/to/nonexistent/work_dir') + end + end + + def mocked_git_script2 = <<~GIT_SCRIPT + #!/bin/sh + # Loop through the arguments and check for the "rev-parse --show-toplevel" args + for arg in "$@"; do + if [ "$arg" = "version" ]; then + echo "git version 1.2.3" + exit 0 + elif [ "$arg" = "rev-parse" ]; then + REV_PARSE_ARG=true + elif [ "$REV_PARSE_ARG" = "true" ] && [ $arg = "--show-toplevel" ]; then + echo fatal: not a git repository 1>&2 + exit 128 + fi + done + exit 1 + GIT_SCRIPT + + def test_working_dir_not_in_work_tree + omit('Only implemented for non-windows platforms') if windows_platform? + + in_temp_dir do |temp_dir| + toplevel = File.join(temp_dir, 'my_repo') + Dir.mkdir(toplevel) do + `git init` + end + + mock_git_binary(mocked_git_script2) do + assert_raise ArgumentError do + Git::Base.root_of_worktree(temp_dir) + end + end + end + end +end diff --git a/tests/units/test_git_binary_version.rb b/tests/units/test_git_binary_version.rb index 09afc1a1..c40b99a9 100644 --- a/tests/units/test_git_binary_version.rb +++ b/tests/units/test_git_binary_version.rb @@ -1,7 +1,7 @@ require 'test_helper' class TestGitBinaryVersion < Test::Unit::TestCase - def windows_mocked_git_binary = <<~GIT_SCRIPT + def mocked_git_script_windows = <<~GIT_SCRIPT @echo off # Loop through the arguments and check for the version command for %%a in (%*) do ( @@ -13,7 +13,7 @@ def windows_mocked_git_binary = <<~GIT_SCRIPT exit /b 1 GIT_SCRIPT - def linux_mocked_git_binary = <<~GIT_SCRIPT + def mocked_git_script_linux = <<~GIT_SCRIPT #!/bin/sh # Loop through the arguments and check for the version command for arg in "$@"; do @@ -25,24 +25,28 @@ def linux_mocked_git_binary = <<~GIT_SCRIPT exit 1 GIT_SCRIPT - def test_binary_version_windows - omit('Only implemented for Windows') unless windows_platform? + def mocked_git_script + if windows_platform? + mocked_git_script_windows + else + mocked_git_script_linux + end + end + def test_binary_version in_temp_dir do |path| - git_binary_path = File.join(path, 'my_git.bat') - File.write(git_binary_path, windows_mocked_git_binary) - assert_equal([1, 2, 3], Git.binary_version(git_binary_path)) + mock_git_binary(mocked_git_script) do |git_binary_path| + assert_equal([1, 2, 3], Git.binary_version(git_binary_path)) + end end end - def test_binary_version_linux - omit('Only implemented for Linux') if windows_platform? - + def test_binary_version_with_spaces in_temp_dir do |path| - git_binary_path = File.join(path, 'my_git.bat') - File.write(git_binary_path, linux_mocked_git_binary) - File.chmod(0755, git_binary_path) - assert_equal([1, 2, 3], Git.binary_version(git_binary_path)) + subdir = 'Git Bin Directory' + mock_git_binary(mocked_git_script, subdir: subdir) do |git_binary_path| + assert_equal([1, 2, 3], Git.binary_version(git_binary_path)) + end end end From 81932be8783834c87635bf7976126307f2054d90 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 4 Dec 2024 13:19:56 -0800 Subject: [PATCH 51/72] chore: release v2.3.3 Signed-off-by: James Couball --- CHANGELOG.md | 10 ++++++++++ lib/git/version.rb | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 829dfcd6..92821c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ # Change Log +## v2.3.3 (2024-12-04) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.2..v2.3.3) + +Changes since v2.3.2: + +* c25e5e0 test: add tests for spaces in the git binary path or the working dir +* 5f43a1a fix: open3 errors on binary paths with spaces +* 60b58ba test: add #run_command for tests to use instead of backticks + ## v2.3.2 (2024-11-19) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.1..v2.3.2) diff --git a/lib/git/version.rb b/lib/git/version.rb index c5710194..475f6e81 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='2.3.2' + VERSION='2.3.3' end From d3f3a9de61c6b842b8e2c89e4b9fdc476493e643 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 26 Feb 2025 10:14:05 -0800 Subject: [PATCH 52/72] chore: add frozen_string_literal: true magic comment --- lib/git.rb | 2 ++ lib/git/author.rb | 5 ++-- lib/git/base.rb | 2 ++ lib/git/branch.rb | 2 ++ lib/git/branches.rb | 29 ++++++++++--------- lib/git/config.rb | 2 ++ lib/git/diff.rb | 2 ++ lib/git/index.rb | 3 +- lib/git/lib.rb | 4 ++- lib/git/log.rb | 2 ++ lib/git/object.rb | 2 ++ lib/git/path.rb | 17 ++++++----- lib/git/remote.rb | 2 ++ lib/git/repository.rb | 2 ++ lib/git/stash.rb | 13 +++++---- lib/git/stashes.rb | 21 +++++++------- lib/git/status.rb | 4 ++- lib/git/version.rb | 2 ++ lib/git/working_directory.rb | 2 ++ lib/git/worktree.rb | 2 ++ lib/git/worktrees.rb | 2 ++ tests/test_helper.rb | 2 ++ tests/units/test_archive.rb | 2 +- tests/units/test_bare.rb | 2 +- tests/units/test_base.rb | 27 +++++++++-------- tests/units/test_branch.rb | 2 +- tests/units/test_checkout.rb | 2 ++ tests/units/test_command_line.rb | 2 ++ tests/units/test_command_line_error.rb | 2 ++ tests/units/test_command_line_result.rb | 2 ++ tests/units/test_commit_with_empty_message.rb | 3 +- tests/units/test_commit_with_gpg.rb | 2 +- tests/units/test_config.rb | 2 +- tests/units/test_config_module.rb | 2 +- tests/units/test_describe.rb | 2 +- tests/units/test_diff.rb | 2 +- tests/units/test_diff_non_default_encoding.rb | 2 +- tests/units/test_diff_with_escaped_path.rb | 2 +- tests/units/test_each_conflict.rb | 2 +- tests/units/test_escaped_path.rb | 1 - tests/units/test_failed_error.rb | 2 ++ tests/units/test_git_alt_uri.rb | 2 ++ tests/units/test_git_base_root_of_worktree.rb | 2 ++ tests/units/test_git_binary_version.rb | 2 ++ tests/units/test_git_default_branch.rb | 2 +- tests/units/test_git_dir.rb | 2 +- tests/units/test_git_path.rb | 2 +- .../test_ignored_files_with_escaped_path.rb | 2 +- tests/units/test_index_ops.rb | 2 +- tests/units/test_init.rb | 2 +- tests/units/test_lib.rb | 10 +++---- .../units/test_lib_meets_required_version.rb | 2 +- .../test_lib_repository_default_branch.rb | 2 +- tests/units/test_log.rb | 3 +- tests/units/test_logger.rb | 3 +- .../units/test_ls_files_with_escaped_path.rb | 2 +- tests/units/test_ls_tree.rb | 2 ++ tests/units/test_merge.rb | 2 +- tests/units/test_merge_base.rb | 2 +- tests/units/test_object.rb | 2 +- tests/units/test_pull.rb | 2 ++ tests/units/test_push.rb | 2 ++ tests/units/test_remotes.rb | 2 +- tests/units/test_repack.rb | 2 +- tests/units/test_rm.rb | 2 +- tests/units/test_show.rb | 2 +- tests/units/test_signaled_error.rb | 2 ++ tests/units/test_signed_commits.rb | 2 +- tests/units/test_stashes.rb | 2 +- tests/units/test_status.rb | 2 +- tests/units/test_status_object.rb | 2 ++ tests/units/test_status_object_empty_repo.rb | 2 ++ tests/units/test_submodule.rb | 2 +- tests/units/test_tags.rb | 2 +- tests/units/test_thread_safety.rb | 2 +- tests/units/test_timeout_error.rb | 2 ++ tests/units/test_tree_ops.rb | 2 +- tests/units/test_windows_cmd_escaping.rb | 2 +- tests/units/test_worktree.rb | 2 +- 79 files changed, 171 insertions(+), 102 deletions(-) diff --git a/lib/git.rb b/lib/git.rb index 6d0f3032..34b70caf 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support' require 'active_support/deprecation' diff --git a/lib/git/author.rb b/lib/git/author.rb index 86d33047..5cf7cc72 100644 --- a/lib/git/author.rb +++ b/lib/git/author.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module Git class Author attr_accessor :name, :email, :date - + def initialize(author_string) if m = /(.*?) <(.*?)> (\d+) (.*)/.match(author_string) @name = m[1] @@ -9,6 +11,5 @@ def initialize(author_string) @date = Time.at(m[3].to_i) end end - end end diff --git a/lib/git/base.rb b/lib/git/base.rb index 2e9f1951..3f01530e 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'logger' require 'open3' diff --git a/lib/git/branch.rb b/lib/git/branch.rb index f6780b03..43d31767 100644 --- a/lib/git/branch.rb +++ b/lib/git/branch.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'git/path' module Git diff --git a/lib/git/branches.rb b/lib/git/branches.rb index fc871db8..e173faab 100644 --- a/lib/git/branches.rb +++ b/lib/git/branches.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + module Git - + # object that holds all the available branches class Branches include Enumerable - + def initialize(base) @branches = {} - + @base = base - + @base.lib.branches_all.each do |b| @branches[b[0]] = Git::Branch.new(@base, b[0]) end @@ -18,21 +20,21 @@ def initialize(base) def local self.select { |b| !b.remote } end - + def remote self.select { |b| b.remote } end - + # array like methods def size @branches.size - end - + end + def each(&block) @branches.values.each(&block) end - + # Returns the target branch # # Example: @@ -50,14 +52,14 @@ def [](branch_name) @branches.values.inject(@branches) do |branches, branch| branches[branch.full] ||= branch - # This is how Git (version 1.7.9.5) works. - # Lets you ignore the 'remotes' if its at the beginning of the branch full name (even if is not a real remote branch). + # This is how Git (version 1.7.9.5) works. + # Lets you ignore the 'remotes' if its at the beginning of the branch full name (even if is not a real remote branch). branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ /^remotes\/.+/ - + branches end[branch_name.to_s] end - + def to_s out = '' @branches.each do |k, b| @@ -65,7 +67,6 @@ def to_s end out end - end end diff --git a/lib/git/config.rb b/lib/git/config.rb index 0a3fd71e..3dd35869 100644 --- a/lib/git/config.rb +++ b/lib/git/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git class Config diff --git a/lib/git/diff.rb b/lib/git/diff.rb index d40ddce4..303a0a89 100644 --- a/lib/git/diff.rb +++ b/lib/git/diff.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git # object that holds the last X commits on given branch diff --git a/lib/git/index.rb b/lib/git/index.rb index c27820dc..45e2de40 100644 --- a/lib/git/index.rb +++ b/lib/git/index.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + module Git class Index < Git::Path - end end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 4128e173..a2ea79b2 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'git/command_line' require 'git/errors' require 'logger' @@ -570,7 +572,7 @@ def process_commit_log_data(data) case key when 'commit' hsh_array << hsh if hsh - hsh = {'sha' => value, 'message' => '', 'parent' => []} + hsh = {'sha' => value, 'message' => +'', 'parent' => []} when 'parent' hsh['parent'] << value else diff --git a/lib/git/log.rb b/lib/git/log.rb index 817d8635..dad2c2cd 100644 --- a/lib/git/log.rb +++ b/lib/git/log.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git # Return the last n commits that match the specified criteria diff --git a/lib/git/object.rb b/lib/git/object.rb index 5d399523..9abbfa08 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'git/author' require 'git/diff' require 'git/errors' diff --git a/lib/git/path.rb b/lib/git/path.rb index 4b20d9a7..a030fcb3 100644 --- a/lib/git/path.rb +++ b/lib/git/path.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + module Git - + class Path - + attr_accessor :path - + def initialize(path, check_path=true) path = File.expand_path(path) - + if check_path && !File.exist?(path) raise ArgumentError, 'path does not exist', [path] end - + @path = path end - + def readable? File.readable?(@path) end @@ -21,11 +23,10 @@ def readable? def writable? File.writable?(@path) end - + def to_s @path end - end end diff --git a/lib/git/remote.rb b/lib/git/remote.rb index 9b2f3958..0615ff9b 100644 --- a/lib/git/remote.rb +++ b/lib/git/remote.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git class Remote < Path diff --git a/lib/git/repository.rb b/lib/git/repository.rb index 95f3bef6..00f2b529 100644 --- a/lib/git/repository.rb +++ b/lib/git/repository.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git class Repository < Path diff --git a/lib/git/stash.rb b/lib/git/stash.rb index 97de906c..43897a33 100644 --- a/lib/git/stash.rb +++ b/lib/git/stash.rb @@ -1,27 +1,28 @@ +# frozen_string_literal: true + module Git class Stash - + def initialize(base, message, existing=false) @base = base @message = message save unless existing end - + def save @saved = @base.lib.stash_save(@message) end - + def saved? @saved end - + def message @message end - + def to_s message end - end end \ No newline at end of file diff --git a/lib/git/stashes.rb b/lib/git/stashes.rb index 0ebb9bed..2ccc55d7 100644 --- a/lib/git/stashes.rb +++ b/lib/git/stashes.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + module Git - + # object that holds all the available stashes class Stashes include Enumerable - + def initialize(base) @stashes = [] - + @base = base - + @base.lib.stashes_all.each do |id, message| @stashes.unshift(Git::Stash.new(@base, message, true)) end @@ -24,16 +26,16 @@ def initialize(base) def all @base.lib.stashes_all end - + def save(message) s = Git::Stash.new(@base, message) @stashes.unshift(s) if s.saved? end - + def apply(index=nil) @base.lib.stash_apply(index) end - + def clear @base.lib.stash_clear @stashes = [] @@ -42,14 +44,13 @@ def clear def size @stashes.size end - + def each(&block) @stashes.each(&block) end - + def [](index) @stashes[index.to_i] end - end end diff --git a/lib/git/status.rb b/lib/git/status.rb index 39ceace7..08deeccd 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git # The status class gets the status of a git repository # @@ -100,7 +102,7 @@ def untracked?(file) end def pretty - out = '' + out = +'' each do |file| out << pretty_file(file) end diff --git a/lib/git/version.rb b/lib/git/version.rb index 475f6e81..b0ad1154 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git # The current gem version # @return [String] the current gem version. diff --git a/lib/git/working_directory.rb b/lib/git/working_directory.rb index 3f37f1a5..94520065 100644 --- a/lib/git/working_directory.rb +++ b/lib/git/working_directory.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git class WorkingDirectory < Git::Path end diff --git a/lib/git/worktree.rb b/lib/git/worktree.rb index 24e79b5b..9754f5ab 100644 --- a/lib/git/worktree.rb +++ b/lib/git/worktree.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'git/path' module Git diff --git a/lib/git/worktrees.rb b/lib/git/worktrees.rb index 0cc53ba6..859c5054 100644 --- a/lib/git/worktrees.rb +++ b/lib/git/worktrees.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Git # object that holds all the available worktrees class Worktrees diff --git a/tests/test_helper.rb b/tests/test_helper.rb index 0bb809ea..c0a95174 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'date' require 'fileutils' require 'minitar' diff --git a/tests/units/test_archive.rb b/tests/units/test_archive.rb index 13c40f7a..96522e22 100644 --- a/tests/units/test_archive.rb +++ b/tests/units/test_archive.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_bare.rb b/tests/units/test_bare.rb index 4972a219..f168c724 100644 --- a/tests/units/test_bare.rb +++ b/tests/units/test_bare.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_base.rb b/tests/units/test_base.rb index b0d1a589..8cb24043 100644 --- a/tests/units/test_base.rb +++ b/tests/units/test_base.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' @@ -11,7 +11,7 @@ def setup def test_add in_temp_dir do |path| git = Git.clone(@wdir, 'test_add') - + create_file('test_add/test_file_1', 'content tets_file_1') create_file('test_add/test_file_2', 'content test_file_2') create_file('test_add/test_file_3', 'content test_file_3') @@ -19,7 +19,7 @@ def test_add create_file('test_add/test file with \' quote', 'content test_file_4') assert(!git.status.added.assoc('test_file_1')) - + # Adding a single file, usign String git.add('test_file_1') @@ -39,11 +39,11 @@ def test_add assert(git.status.added.assoc('test_file_3')) assert(git.status.added.assoc('test_file_4')) assert(git.status.added.assoc('test file with \' quote')) - + git.commit('test_add commit #1') assert(git.status.added.empty?) - + delete_file('test_add/test_file_3') update_file('test_add/test_file_4', 'content test_file_4 update #1') create_file('test_add/test_file_5', 'content test_file_5') @@ -54,24 +54,24 @@ def test_add assert(git.status.deleted.assoc('test_file_3')) assert(git.status.changed.assoc('test_file_4')) assert(git.status.added.assoc('test_file_5')) - + git.commit('test_add commit #2') - + assert(git.status.deleted.empty?) assert(git.status.changed.empty?) assert(git.status.added.empty?) - + delete_file('test_add/test_file_4') update_file('test_add/test_file_5', 'content test_file_5 update #1') create_file('test_add/test_file_6', 'content test_fiile_6') - + # Adding all files (new or updated), without params git.add - + assert(git.status.deleted.assoc('test_file_4')) assert(git.status.changed.assoc('test_file_5')) assert(git.status.added.assoc('test_file_6')) - + git.commit('test_add commit #3') assert(git.status.changed.empty?) @@ -82,7 +82,7 @@ def test_add def test_commit in_temp_dir do |path| git = Git.clone(@wdir, 'test_commit') - + create_file('test_commit/test_file_1', 'content tets_file_1') create_file('test_commit/test_file_2', 'content test_file_2') @@ -96,7 +96,7 @@ def test_commit original_commit_id = git.log[0].objectish create_file('test_commit/test_file_3', 'content test_file_3') - + git.add('test_file_3') git.commit(nil, :amend => true) @@ -105,5 +105,4 @@ def test_commit assert(git.log[1].objectish == base_commit_id) end end - end diff --git a/tests/units/test_branch.rb b/tests/units/test_branch.rb index f150d878..98edb8df 100644 --- a/tests/units/test_branch.rb +++ b/tests/units/test_branch.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_checkout.rb b/tests/units/test_checkout.rb index a30b3fcc..94dba2ff 100644 --- a/tests/units/test_checkout.rb +++ b/tests/units/test_checkout.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestCheckout < Test::Unit::TestCase diff --git a/tests/units/test_command_line.rb b/tests/units/test_command_line.rb index eac144fb..1570ebff 100644 --- a/tests/units/test_command_line.rb +++ b/tests/units/test_command_line.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' require 'tempfile' diff --git a/tests/units/test_command_line_error.rb b/tests/units/test_command_line_error.rb index 30b859ab..25c03765 100644 --- a/tests/units/test_command_line_error.rb +++ b/tests/units/test_command_line_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestCommandLineError < Test::Unit::TestCase diff --git a/tests/units/test_command_line_result.rb b/tests/units/test_command_line_result.rb index acec4bb6..e0cf1dd0 100644 --- a/tests/units/test_command_line_result.rb +++ b/tests/units/test_command_line_result.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestCommamndLineResult < Test::Unit::TestCase diff --git a/tests/units/test_commit_with_empty_message.rb b/tests/units/test_commit_with_empty_message.rb index 4bf04991..f896333b 100755 --- a/tests/units/test_commit_with_empty_message.rb +++ b/tests/units/test_commit_with_empty_message.rb @@ -1,4 +1,5 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true + require 'test_helper' class TestCommitWithEmptyMessage < Test::Unit::TestCase diff --git a/tests/units/test_commit_with_gpg.rb b/tests/units/test_commit_with_gpg.rb index b8a3e1ec..4bcdae70 100644 --- a/tests/units/test_commit_with_gpg.rb +++ b/tests/units/test_commit_with_gpg.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_config.rb b/tests/units/test_config.rb index b60e6c83..a72bc2e4 100644 --- a/tests/units/test_config.rb +++ b/tests/units/test_config.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_config_module.rb b/tests/units/test_config_module.rb index 060e41f6..04a1bbbb 100644 --- a/tests/units/test_config_module.rb +++ b/tests/units/test_config_module.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_describe.rb b/tests/units/test_describe.rb index 967fc753..c103c0ef 100644 --- a/tests/units/test_describe.rb +++ b/tests/units/test_describe.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_diff.rb b/tests/units/test_diff.rb index 89a476a9..3e859da5 100644 --- a/tests/units/test_diff.rb +++ b/tests/units/test_diff.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_diff_non_default_encoding.rb b/tests/units/test_diff_non_default_encoding.rb index 8bb0efa7..b9ee5231 100644 --- a/tests/units/test_diff_non_default_encoding.rb +++ b/tests/units/test_diff_non_default_encoding.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_diff_with_escaped_path.rb b/tests/units/test_diff_with_escaped_path.rb index ce0278cb..7e875be0 100644 --- a/tests/units/test_diff_with_escaped_path.rb +++ b/tests/units/test_diff_with_escaped_path.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true # encoding: utf-8 require 'test_helper' diff --git a/tests/units/test_each_conflict.rb b/tests/units/test_each_conflict.rb index f311c1ff..0854b616 100644 --- a/tests/units/test_each_conflict.rb +++ b/tests/units/test_each_conflict.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_escaped_path.rb b/tests/units/test_escaped_path.rb index ada6eafa..591429b9 100755 --- a/tests/units/test_escaped_path.rb +++ b/tests/units/test_escaped_path.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_failed_error.rb b/tests/units/test_failed_error.rb index 63b894f7..16a7c855 100644 --- a/tests/units/test_failed_error.rb +++ b/tests/units/test_failed_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestFailedError < Test::Unit::TestCase diff --git a/tests/units/test_git_alt_uri.rb b/tests/units/test_git_alt_uri.rb index b01ea1bb..0434223a 100644 --- a/tests/units/test_git_alt_uri.rb +++ b/tests/units/test_git_alt_uri.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test/unit' # Tests for the Git::GitAltURI class diff --git a/tests/units/test_git_base_root_of_worktree.rb b/tests/units/test_git_base_root_of_worktree.rb index 3a13b59e..8b58af55 100644 --- a/tests/units/test_git_base_root_of_worktree.rb +++ b/tests/units/test_git_base_root_of_worktree.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestGitBaseRootOfWorktree < Test::Unit::TestCase diff --git a/tests/units/test_git_binary_version.rb b/tests/units/test_git_binary_version.rb index c40b99a9..74c7436e 100644 --- a/tests/units/test_git_binary_version.rb +++ b/tests/units/test_git_binary_version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestGitBinaryVersion < Test::Unit::TestCase diff --git a/tests/units/test_git_default_branch.rb b/tests/units/test_git_default_branch.rb index 3b1f64fd..bb829cec 100644 --- a/tests/units/test_git_default_branch.rb +++ b/tests/units/test_git_default_branch.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require File.dirname(__FILE__) + '/../test_helper' diff --git a/tests/units/test_git_dir.rb b/tests/units/test_git_dir.rb index b33827cf..61538261 100644 --- a/tests/units/test_git_dir.rb +++ b/tests/units/test_git_dir.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_git_path.rb b/tests/units/test_git_path.rb index 9944209e..446a3dad 100644 --- a/tests/units/test_git_path.rb +++ b/tests/units/test_git_path.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_ignored_files_with_escaped_path.rb b/tests/units/test_ignored_files_with_escaped_path.rb index 0d40711d..ad609960 100644 --- a/tests/units/test_ignored_files_with_escaped_path.rb +++ b/tests/units/test_ignored_files_with_escaped_path.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true # encoding: utf-8 require 'test_helper' diff --git a/tests/units/test_index_ops.rb b/tests/units/test_index_ops.rb index 6bee051b..c726e4e5 100644 --- a/tests/units/test_index_ops.rb +++ b/tests/units/test_index_ops.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_init.rb b/tests/units/test_init.rb index 99a87593..30a9e894 100644 --- a/tests/units/test_init.rb +++ b/tests/units/test_init.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' require 'stringio' diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index c92959d6..fb319be8 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' require "fileutils" @@ -241,14 +241,14 @@ def test_cat_file_size_with_bad_object end def test_cat_file_contents - commit = "tree 94c827875e2cadb8bc8d4cdd900f19aa9e8634c7\n" + commit = +"tree 94c827875e2cadb8bc8d4cdd900f19aa9e8634c7\n" commit << "parent 546bec6f8872efa41d5d97a369f669165ecda0de\n" commit << "author scott Chacon 1194561188 -0800\n" commit << "committer scott Chacon 1194561188 -0800\n" commit << "\ntest" assert_equal(commit, @lib.cat_file_contents('1cc8667014381')) # commit - tree = "040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" + tree = +"040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" tree << "100644 blob 3aac4b445017a8fc07502670ec2dbf744213dd48\texample.txt" assert_equal(tree, @lib.cat_file_contents('1cc8667014381^{tree}')) #tree @@ -257,7 +257,7 @@ def test_cat_file_contents end def test_cat_file_contents_with_block - commit = "tree 94c827875e2cadb8bc8d4cdd900f19aa9e8634c7\n" + commit = +"tree 94c827875e2cadb8bc8d4cdd900f19aa9e8634c7\n" commit << "parent 546bec6f8872efa41d5d97a369f669165ecda0de\n" commit << "author scott Chacon 1194561188 -0800\n" commit << "committer scott Chacon 1194561188 -0800\n" @@ -269,7 +269,7 @@ def test_cat_file_contents_with_block # commit - tree = "040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" + tree = +"040000 tree 6b790ddc5eab30f18cabdd0513e8f8dac0d2d3ed\tex_dir\n" tree << "100644 blob 3aac4b445017a8fc07502670ec2dbf744213dd48\texample.txt" @lib.cat_file_contents('1cc8667014381^{tree}') do |f| diff --git a/tests/units/test_lib_meets_required_version.rb b/tests/units/test_lib_meets_required_version.rb index 25c410bf..11521d92 100644 --- a/tests/units/test_lib_meets_required_version.rb +++ b/tests/units/test_lib_meets_required_version.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_lib_repository_default_branch.rb b/tests/units/test_lib_repository_default_branch.rb index 0e012895..4240865f 100644 --- a/tests/units/test_lib_repository_default_branch.rb +++ b/tests/units/test_lib_repository_default_branch.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require File.dirname(__FILE__) + '/../test_helper' diff --git a/tests/units/test_log.rb b/tests/units/test_log.rb index d220af03..1cab1a32 100644 --- a/tests/units/test_log.rb +++ b/tests/units/test_log.rb @@ -1,4 +1,5 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true + require 'logger' require 'test_helper' diff --git a/tests/units/test_logger.rb b/tests/units/test_logger.rb index ced39292..d46fc740 100644 --- a/tests/units/test_logger.rb +++ b/tests/units/test_logger.rb @@ -1,4 +1,5 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true + require 'logger' require 'test_helper' diff --git a/tests/units/test_ls_files_with_escaped_path.rb b/tests/units/test_ls_files_with_escaped_path.rb index cdc890c0..2102a8ea 100644 --- a/tests/units/test_ls_files_with_escaped_path.rb +++ b/tests/units/test_ls_files_with_escaped_path.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true # encoding: utf-8 require 'test_helper' diff --git a/tests/units/test_ls_tree.rb b/tests/units/test_ls_tree.rb index 19d487a4..afa3181a 100644 --- a/tests/units/test_ls_tree.rb +++ b/tests/units/test_ls_tree.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestLsTree < Test::Unit::TestCase diff --git a/tests/units/test_merge.rb b/tests/units/test_merge.rb index 95ae33a8..2073c6af 100644 --- a/tests/units/test_merge.rb +++ b/tests/units/test_merge.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_merge_base.rb b/tests/units/test_merge_base.rb index 4a794993..a4a615de 100755 --- a/tests/units/test_merge_base.rb +++ b/tests/units/test_merge_base.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_object.rb b/tests/units/test_object.rb index 03f8d24d..9837bef7 100644 --- a/tests/units/test_object.rb +++ b/tests/units/test_object.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_pull.rb b/tests/units/test_pull.rb index f9a514ab..0c0147a7 100644 --- a/tests/units/test_pull.rb +++ b/tests/units/test_pull.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestPull < Test::Unit::TestCase diff --git a/tests/units/test_push.rb b/tests/units/test_push.rb index 78cc9396..cb6e2bc0 100644 --- a/tests/units/test_push.rb +++ b/tests/units/test_push.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestPush < Test::Unit::TestCase diff --git a/tests/units/test_remotes.rb b/tests/units/test_remotes.rb index 00c4c31b..602e0212 100644 --- a/tests/units/test_remotes.rb +++ b/tests/units/test_remotes.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_repack.rb b/tests/units/test_repack.rb index 4a27e8f8..7f8ef720 100644 --- a/tests/units/test_repack.rb +++ b/tests/units/test_repack.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_rm.rb b/tests/units/test_rm.rb index 658ce9ca..c80d1e50 100644 --- a/tests/units/test_rm.rb +++ b/tests/units/test_rm.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_show.rb b/tests/units/test_show.rb index 8c2e46ae..5439180c 100644 --- a/tests/units/test_show.rb +++ b/tests/units/test_show.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_signaled_error.rb b/tests/units/test_signaled_error.rb index 6bf46c2b..d489cb6f 100644 --- a/tests/units/test_signaled_error.rb +++ b/tests/units/test_signaled_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestSignaledError < Test::Unit::TestCase diff --git a/tests/units/test_signed_commits.rb b/tests/units/test_signed_commits.rb index c50fa62f..f3c783c1 100644 --- a/tests/units/test_signed_commits.rb +++ b/tests/units/test_signed_commits.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' require "fileutils" diff --git a/tests/units/test_stashes.rb b/tests/units/test_stashes.rb index 0516f273..78312651 100644 --- a/tests/units/test_stashes.rb +++ b/tests/units/test_stashes.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_status.rb b/tests/units/test_status.rb index 36543bc1..fd446e02 100644 --- a/tests/units/test_status.rb +++ b/tests/units/test_status.rb @@ -1,5 +1,5 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_status_object.rb b/tests/units/test_status_object.rb index ee343cb6..3d5d0a29 100644 --- a/tests/units/test_status_object.rb +++ b/tests/units/test_status_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rbconfig' require 'securerandom' require 'test_helper' diff --git a/tests/units/test_status_object_empty_repo.rb b/tests/units/test_status_object_empty_repo.rb index 4a8c366c..71435b11 100644 --- a/tests/units/test_status_object_empty_repo.rb +++ b/tests/units/test_status_object_empty_repo.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rbconfig' require 'securerandom' require 'test_helper' diff --git a/tests/units/test_submodule.rb b/tests/units/test_submodule.rb index 009127f2..bdf7ffdc 100644 --- a/tests/units/test_submodule.rb +++ b/tests/units/test_submodule.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_tags.rb b/tests/units/test_tags.rb index 242af137..df62a8f2 100644 --- a/tests/units/test_tags.rb +++ b/tests/units/test_tags.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_thread_safety.rb b/tests/units/test_thread_safety.rb index 48b93ae7..a4a59259 100644 --- a/tests/units/test_thread_safety.rb +++ b/tests/units/test_thread_safety.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_timeout_error.rb b/tests/units/test_timeout_error.rb index 3bfc90b6..e3e4999a 100644 --- a/tests/units/test_timeout_error.rb +++ b/tests/units/test_timeout_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' class TestTimeoutError < Test::Unit::TestCase diff --git a/tests/units/test_tree_ops.rb b/tests/units/test_tree_ops.rb index 82e65b49..2d8219fe 100644 --- a/tests/units/test_tree_ops.rb +++ b/tests/units/test_tree_ops.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true require 'test_helper' diff --git a/tests/units/test_windows_cmd_escaping.rb b/tests/units/test_windows_cmd_escaping.rb index d8b3ee54..9998fd89 100644 --- a/tests/units/test_windows_cmd_escaping.rb +++ b/tests/units/test_windows_cmd_escaping.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true # encoding: utf-8 require 'test_helper' diff --git a/tests/units/test_worktree.rb b/tests/units/test_worktree.rb index bbe377ce..910561ec 100644 --- a/tests/units/test_worktree.rb +++ b/tests/units/test_worktree.rb @@ -1,4 +1,4 @@ -#!/usr/bin/env ruby +# frozen_string_literal: true # require 'fileutils' # require 'pathname' From 38c0eb580226fbcbf98c8ee2119818ef8d666a50 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 26 Feb 2025 10:25:09 -0800 Subject: [PATCH 53/72] build: update the CI build to use current versions to TruffleRuby and JRuby --- .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 52c6c4ea..dd2b61ec 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: # Only the latest versions of JRuby and TruffleRuby are tested - ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-24.0.0", "jruby-9.4.5.0"] + ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-24.1.2", "jruby-9.4.12.0"] operating-system: [ubuntu-latest] experimental: [No] include: From 501d135cd81cf2167524e0c8fbebfe395b0b3a65 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 26 Feb 2025 10:54:46 -0800 Subject: [PATCH 54/72] feat: add support for Ruby 3.4 and drop support for Ruby 3.0 --- .github/workflows/continuous_integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index dd2b61ec..5bc83dd3 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -18,12 +18,12 @@ jobs: fail-fast: false matrix: # Only the latest versions of JRuby and TruffleRuby are tested - ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-24.1.2", "jruby-9.4.12.0"] + ruby: ["3.1", "3.2", "3.3", "3.4", "truffleruby-24.1.2", "jruby-9.4.12.0"] operating-system: [ubuntu-latest] experimental: [No] include: - # Only test with minimal Ruby version on Windows - ruby: 3.0 + ruby: 3.1 operating-system: windows-latest steps: From 629f3b64064f1ad7dd638f188ce0c89391af1087 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 26 Feb 2025 17:50:44 -0800 Subject: [PATCH 55/72] feat: update dependenices --- git.gemspec | 12 ++++++------ tests/units/test_command_line.rb | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/git.gemspec b/git.gemspec index ea257473..a81ba60b 100644 --- a/git.gemspec +++ b/git.gemspec @@ -29,13 +29,13 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'activesupport', '>= 5.0' s.add_runtime_dependency 'addressable', '~> 2.8' - s.add_runtime_dependency 'process_executer', '~> 1.1' - s.add_runtime_dependency 'rchardet', '~> 1.8' + s.add_runtime_dependency 'process_executer', '~> 1.3' + s.add_runtime_dependency 'rchardet', '~> 1.9' - s.add_development_dependency 'create_github_release', '~> 1.4' - s.add_development_dependency 'minitar', '~> 0.9' - s.add_development_dependency 'mocha', '~> 2.1' - s.add_development_dependency 'rake', '~> 13.1' + s.add_development_dependency 'create_github_release', '~> 2.1' + s.add_development_dependency 'minitar', '~> 0.12' + s.add_development_dependency 'mocha', '~> 2.7' + s.add_development_dependency 'rake', '~> 13.2' s.add_development_dependency 'test-unit', '~> 3.6' unless RUBY_PLATFORM == 'java' diff --git a/tests/units/test_command_line.rb b/tests/units/test_command_line.rb index 1570ebff..1af49efb 100644 --- a/tests/units/test_command_line.rb +++ b/tests/units/test_command_line.rb @@ -154,7 +154,7 @@ def merge def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) out_writers.each { |w| w.write(File.read('tests/files/encoding/test1.txt')) } `true` - ProcessExecuter::Status.new($?, false) # return status + ProcessExecuter::Status.new($?, false, nil) # return status end normalize = true @@ -177,7 +177,7 @@ def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) out_writers.each { |w| w.write(File.read('tests/files/encoding/test1.txt')) } `true` - ProcessExecuter::Status.new($?, false) # return status + ProcessExecuter::Status.new($?, false, nil) # return status end normalize = false From 534fcf5fa8a7934c76d75e180dec4f5c3e16cb1a Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 27 Feb 2025 11:29:04 -0800 Subject: [PATCH 56/72] chore: use ProcessExecuter.run instead of the implementing it in this gem --- bin/command_line_test | 21 +++- lib/git/command_line.rb | 188 ++++++++----------------------- tests/units/test_command_line.rb | 41 +++---- tests/units/test_logger.rb | 4 +- 4 files changed, 85 insertions(+), 169 deletions(-) diff --git a/bin/command_line_test b/bin/command_line_test index 918e2024..99c67f38 100755 --- a/bin/command_line_test +++ b/bin/command_line_test @@ -91,7 +91,8 @@ class CommandLineParser option_parser.separator '' option_parser.separator 'Options:' %i[ - define_help_option define_stdout_option define_stderr_option + define_help_option define_stdout_option define_stdout_file_option + define_stderr_option define_stderr_file_option define_exitstatus_option define_signal_option define_duration_option ].each { |m| send(m) } end @@ -116,6 +117,15 @@ class CommandLineParser end end + # Define the stdout-file option + # @return [void] + # @api private + def define_stdout_file_option + option_parser.on('--stdout-file="file"', 'Send contents of file to stdout') do |filename| + @stdout = File.read(filename) + end + end + # Define the stderr option # @return [void] # @api private @@ -125,6 +135,15 @@ class CommandLineParser end end + # Define the stderr-file option + # @return [void] + # @api private + def define_stderr_file_option + option_parser.on('--stderr-file="file"', 'Send contents of file to stderr') do |filename| + @stderr = File.read(filename) + end + end + # Define the exitstatus option # @return [void] # @api private diff --git a/lib/git/command_line.rb b/lib/git/command_line.rb index 276cdc78..6228a144 100644 --- a/lib/git/command_line.rb +++ b/lib/git/command_line.rb @@ -189,13 +189,14 @@ def initialize(env, binary_path, global_opts, logger) # # @raise [Git::TimeoutError] if the command times out # - def run(*args, out:, err:, normalize:, chomp:, merge:, chdir: nil, timeout: nil) + def run(*args, out: nil, err: nil, normalize:, chomp:, merge:, chdir: nil, timeout: nil) git_cmd = build_git_cmd(args) - out ||= StringIO.new - err ||= (merge ? out : StringIO.new) - status = execute(git_cmd, out, err, chdir: (chdir || :not_set), timeout: timeout) - - process_result(git_cmd, status, out, err, normalize, chomp, timeout) + begin + result = ProcessExecuter.run(env, *git_cmd, out: out, err: err, merge:, chdir: (chdir || :not_set), timeout: timeout, raise_errors: false) + rescue ProcessExecuter::Command::ProcessIOError => e + raise Git::ProcessIOError.new(e.message), cause: e.exception.cause + end + process_result(result, normalize, chomp, timeout) end private @@ -210,121 +211,12 @@ def build_git_cmd(args) [binary_path, *global_opts, *args].map { |e| e.to_s } end - # Determine the output to return in the `CommandLineResult` - # - # If the writer can return the output by calling `#string` (such as a StringIO), - # then return the result of normalizing the encoding and chomping the output - # as requested. - # - # If the writer does not support `#string`, then return nil. The output is - # assumed to be collected by the writer itself such as when the writer - # is a file instead of a StringIO. - # - # @param writer [#string] the writer to post-process - # - # @return [String, nil] - # - # @api private - # - def post_process(writer, normalize, chomp) - if writer.respond_to?(:string) - output = writer.string.dup - output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize - output.chomp! if chomp - output - else - nil - end - end - - # Post-process all writers and return an array of the results - # - # @param writers [Array<#write>] the writers to post-process - # @param normalize [Boolean] whether to normalize the output of each writer - # @param chomp [Boolean] whether to chomp the output of each writer - # - # @return [Array] the output of each writer that supports `#string` - # - # @api private - # - def post_process_all(writers, normalize, chomp) - Array.new.tap do |result| - writers.each { |writer| result << post_process(writer, normalize, chomp) } - end - end - - # Raise an error when there was exception while collecting the subprocess output - # - # @param git_cmd [Array] the git command that was executed - # @param pipe_name [Symbol] the name of the pipe that raised the exception - # @param pipe [ProcessExecuter::MonitoredPipe] the pipe that raised the exception - # - # @raise [Git::ProcessIOError] - # - # @return [void] this method always raises an error - # - # @api private - # - def raise_pipe_error(git_cmd, pipe_name, pipe) - raise Git::ProcessIOError.new("Pipe Exception for #{git_cmd}: #{pipe_name}"), cause: pipe.exception - end - - # Execute the git command and collect the output - # - # @param cmd [Array] the git command to execute - # @param chdir [String] the directory to run the command in - # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete - # - # If timeout is zero of nil, the command will not time out. If the command - # times out, it is killed via a SIGKILL signal and `Git::TimeoutError` is raised. - # - # If the command does not respond to SIGKILL, it will hang this method. - # - # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output - # @raise [Git::TimeoutError] if the command times out - # - # @return [ProcessExecuter::Status] the status of the completed subprocess - # - # @api private - # - def spawn(cmd, out_writers, err_writers, chdir:, timeout:) - out_pipe = ProcessExecuter::MonitoredPipe.new(*out_writers, chunk_size: 10_000) - err_pipe = ProcessExecuter::MonitoredPipe.new(*err_writers, chunk_size: 10_000) - ProcessExecuter.spawn(env, *cmd, out: out_pipe, err: err_pipe, chdir: chdir, timeout: timeout) - ensure - out_pipe.close - err_pipe.close - raise_pipe_error(cmd, :stdout, out_pipe) if out_pipe.exception - raise_pipe_error(cmd, :stderr, err_pipe) if err_pipe.exception - end - - # The writers that will be used to collect stdout and stderr - # - # Additional writers could be added here if you wanted to tee output - # or send output to the terminal. - # - # @param out [#write] the object to write stdout to - # @param err [#write] the object to write stderr to - # - # @return [Array, Array<#write>>] the writers for stdout and stderr - # - # @api private - # - def writers(out, err) - out_writers = [out] - err_writers = [err] - [out_writers, err_writers] - end - # Process the result of the command and return a Git::CommandLineResult # # Post process output, log the command and result, and raise an error if the # command failed. # - # @param git_cmd [Array] the git command that was executed - # @param status [Process::Status] the status of the completed subprocess - # @param out [#write] the object that stdout was written to - # @param err [#write] the object that stderr was written to + # @param result [ProcessExecuter::Command::Result] the result it is a Process::Status and include command, stdout, and stderr # @param normalize [Boolean] whether to normalize the output of each writer # @param chomp [Boolean] whether to chomp the output of each writer # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete @@ -338,40 +230,58 @@ def writers(out, err) # # @api private # - def process_result(git_cmd, status, out, err, normalize, chomp, timeout) - out_str, err_str = post_process_all([out, err], normalize, chomp) - logger.info { "#{git_cmd} exited with status #{status}" } - logger.debug { "stdout:\n#{out_str.inspect}\nstderr:\n#{err_str.inspect}" } - Git::CommandLineResult.new(git_cmd, status, out_str, err_str).tap do |result| - raise Git::TimeoutError.new(result, timeout) if status.timeout? - raise Git::SignaledError.new(result) if status.signaled? - raise Git::FailedError.new(result) unless status.success? + def process_result(result, normalize, chomp, timeout) + command = result.command + processed_out, processed_err = post_process_all([result.stdout, result.stderr], normalize, chomp) + logger.info { "#{command} exited with status #{result}" } + logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" } + Git::CommandLineResult.new(command, result, processed_out, processed_err).tap do |processed_result| + raise Git::TimeoutError.new(processed_result, timeout) if result.timeout? + raise Git::SignaledError.new(processed_result) if result.signaled? + raise Git::FailedError.new(processed_result) unless result.success? end end - # Execute the git command and write the command output to out and err + # Post-process command output and return an array of the results # - # @param git_cmd [Array] the git command to execute - # @param out [#write] the object to write stdout to - # @param err [#write] the object to write stderr to - # @param chdir [String] the directory to run the command in - # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete + # @param raw_outputs [Array] the output to post-process + # @param normalize [Boolean] whether to normalize the output of each writer + # @param chomp [Boolean] whether to chomp the output of each writer # - # If timeout is zero of nil, the command will not time out. If the command - # times out, it is killed via a SIGKILL signal and `Git::TimeoutError` is raised. + # @return [Array] the processed output of each command output object that supports `#string` # - # If the command does not respond to SIGKILL, it will hang this method. + # @api private # - # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output - # @raise [Git::TimeoutError] if the command times out + def post_process_all(raw_outputs, normalize, chomp) + Array.new.tap do |result| + raw_outputs.each { |raw_output| result << post_process(raw_output, normalize, chomp) } + end + end + + # Determine the output to return in the `CommandLineResult` # - # @return [Git::CommandLineResult] the result of the command to return to the caller + # If the writer can return the output by calling `#string` (such as a StringIO), + # then return the result of normalizing the encoding and chomping the output + # as requested. + # + # If the writer does not support `#string`, then return nil. The output is + # assumed to be collected by the writer itself such as when the writer + # is a file instead of a StringIO. + # + # @param raw_output [#string] the output to post-process + # @return [String, nil] # # @api private # - def execute(git_cmd, out, err, chdir:, timeout:) - out_writers, err_writers = writers(out, err) - spawn(git_cmd, out_writers, err_writers, chdir: chdir, timeout: timeout) + def post_process(raw_output, normalize, chomp) + if raw_output.respond_to?(:string) + output = raw_output.string.dup + output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize + output.chomp! if chomp + output + else + nil + end end end end diff --git a/tests/units/test_command_line.rb b/tests/units/test_command_line.rb index 1af49efb..7062d1aa 100644 --- a/tests/units/test_command_line.rb +++ b/tests/units/test_command_line.rb @@ -94,10 +94,10 @@ def merge args = ['--stdout=stdout output', '--stderr=stderr output'] result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) - assert_equal(['ruby', 'bin/command_line_test', '--stdout=stdout output', '--stderr=stderr output'], result.git_cmd) + assert_equal([{}, 'ruby', 'bin/command_line_test', '--stdout=stdout output', '--stderr=stderr output'], result.git_cmd) assert_equal('stdout output', result.stdout.chomp) assert_equal('stderr output', result.stderr.chomp) - assert(result.status.is_a? ProcessExecuter::Status) + assert(result.status.is_a? ProcessExecuter::Command::Result) assert_equal(0, result.status.exitstatus) end @@ -111,7 +111,7 @@ def merge # The error raised should include the result of the command result = error.result - assert_equal(['ruby', 'bin/command_line_test', '--exitstatus=1', '--stdout=O1', '--stderr=O2'], result.git_cmd) + assert_equal([{}, 'ruby', 'bin/command_line_test', '--exitstatus=1', '--stdout=O1', '--stderr=O2'], result.git_cmd) assert_equal('O1', result.stdout.chomp) assert_equal('O2', result.stderr.chomp) assert_equal(1, result.status.exitstatus) @@ -130,7 +130,7 @@ def merge # The error raised should include the result of the command result = error.result - assert_equal(['ruby', 'bin/command_line_test', '--signal=9', '--stdout=O1', '--stderr=O2'], result.git_cmd) + assert_equal([{}, 'ruby', 'bin/command_line_test', '--signal=9', '--stdout=O1', '--stderr=O2'], result.git_cmd) # If stdout is buffered, it may not be flushed when the process is killed # assert_equal('O1', result.stdout.chomp) assert_equal('O2', result.stderr.chomp) @@ -149,14 +149,7 @@ def merge test "run should normalize output if normalize is true" do command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) - args = ['--stdout=stdout output'] - - def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) - out_writers.each { |w| w.write(File.read('tests/files/encoding/test1.txt')) } - `true` - ProcessExecuter::Status.new($?, false, nil) # return status - end - + args = ['--stdout-file=tests/files/encoding/test1.txt'] normalize = true result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) @@ -167,28 +160,22 @@ def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) Φεθγιατ θρβανιτασ ρεπριμιqθε OUTPUT - assert_equal(expected_output, result.stdout) + assert_equal(expected_output, result.stdout.delete("\r")) end test "run should NOT normalize output if normalize is false" do command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) - args = ['--stdout=stdout output'] - - def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) - out_writers.each { |w| w.write(File.read('tests/files/encoding/test1.txt')) } - `true` - ProcessExecuter::Status.new($?, false, nil) # return status - end - + args = ['--stdout-file=tests/files/encoding/test1.txt'] normalize = false result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) - expected_output = <<~OUTPUT - \xCB\xEF\xF1\xE5\xEC \xE9\xF0\xF3\xE8\xEC \xE4\xEF\xEB\xEF\xF1 \xF3\xE9\xF4 - \xC7\xE9\xF3 \xE5\xEE \xF4\xEF\xF4\xE1 \xF3\xE8\xE1v\xE9\xF4\xE1\xF4\xE5 - \xCD\xEF \xE8\xF1\xE2\xE1\xED\xE9\xF4\xE1\xF3 - \xD6\xE5\xE8\xE3\xE9\xE1\xF4 \xE8\xF1\xE2\xE1\xED\xE9\xF4\xE1\xF3 \xF1\xE5\xF0\xF1\xE9\xEC\xE9q\xE8\xE5 - OUTPUT + eol = RUBY_PLATFORM =~ /mswin|mingw/ ? "\r\n" : "\n" + + expected_output = + "\xCB\xEF\xF1\xE5\xEC \xE9\xF0\xF3\xE8\xEC \xE4\xEF\xEB\xEF\xF1 \xF3\xE9\xF4#{eol}" \ + "\xC7\xE9\xF3 \xE5\xEE \xF4\xEF\xF4\xE1 \xF3\xE8\xE1v\xE9\xF4\xE1\xF4\xE5#{eol}" \ + "\xCD\xEF \xE8\xF1\xE2\xE1\xED\xE9\xF4\xE1\xF3#{eol}" \ + "\xD6\xE5\xE8\xE3\xE9\xE1\xF4 \xE8\xF1\xE2\xE1\xED\xE9\xF4\xE1\xF3 \xF1\xE5\xF0\xF1\xE9\xEC\xE9q\xE8\xE5#{eol}" assert_equal(expected_output, result.stdout) end diff --git a/tests/units/test_logger.rb b/tests/units/test_logger.rb index d46fc740..deadfe34 100644 --- a/tests/units/test_logger.rb +++ b/tests/units/test_logger.rb @@ -28,7 +28,7 @@ def test_logger logc = File.read(log_path) - expected_log_entry = /INFO -- : \["git", "(?.*?)", "branch", "-a"/ + expected_log_entry = /INFO -- : \[\{[^}]+}, "git", "(?.*?)", "branch", "-a"/ assert_match(expected_log_entry, logc, missing_log_entry) expected_log_entry = /DEBUG -- : stdout:\n" cherry/ @@ -47,7 +47,7 @@ def test_logging_at_info_level_should_not_show_debug_messages logc = File.read(log_path) - expected_log_entry = /INFO -- : \["git", "(?.*?)", "branch", "-a"/ + expected_log_entry = /INFO -- : \[\{[^}]+}, "git", "(?.*?)", "branch", "-a"/ assert_match(expected_log_entry, logc, missing_log_entry) expected_log_entry = /DEBUG -- : stdout:\n" cherry/ From 1a5092af9beeeacd7e58b76d7b46ed4a7e2b6859 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 27 Feb 2025 11:40:51 -0800 Subject: [PATCH 57/72] chore: release v3.0.0 Signed-off-by: James Couball --- CHANGELOG.md | 12 ++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92821c76..59dae355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ # Change Log +## v3.0.0 (2025-02-27) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.3..v3.0.0) + +Changes since v2.3.3: + +* 534fcf5 chore: use ProcessExecuter.run instead of the implementing it in this gem +* 629f3b6 feat: update dependenices +* 501d135 feat: add support for Ruby 3.4 and drop support for Ruby 3.0 +* 38c0eb5 build: update the CI build to use current versions to TruffleRuby and JRuby +* d3f3a9d chore: add frozen_string_literal: true magic comment + ## v2.3.3 (2024-12-04) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.2..v2.3.3) diff --git a/lib/git/version.rb b/lib/git/version.rb index b0ad1154..81e4d967 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -3,5 +3,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='2.3.3' + VERSION='3.0.0' end From b060e479b7eb80269c76d93b71453630b150a43d Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 27 Feb 2025 17:09:36 -0800 Subject: [PATCH 58/72] test: verify that command line envionment variables are set as expected --- lib/git/lib.rb | 2 +- tests/test_helper.rb | 10 +++- .../units/test_command_line_env_overrides.rb | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 tests/units/test_command_line_env_overrides.rb diff --git a/lib/git/lib.rb b/lib/git/lib.rb index a2ea79b2..0682a070 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1547,7 +1547,7 @@ def env_overrides 'GIT_DIR' => @git_dir, 'GIT_WORK_TREE' => @git_work_dir, 'GIT_INDEX_FILE' => @git_index_file, - 'GIT_SSH' => Git::Base.config.git_ssh + 'GIT_SSH' => Git::Base.config.git_ssh, } end diff --git a/tests/test_helper.rb b/tests/test_helper.rb index c0a95174..067fa633 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -131,7 +131,7 @@ def append_file(name, contents) # # @return [void] # - def assert_command_line_eq(expected_command_line, method: :command, mocked_output: nil) + def assert_command_line_eq(expected_command_line, method: :command, mocked_output: nil, include_env: false) actual_command_line = nil command_output = '' @@ -140,7 +140,11 @@ def assert_command_line_eq(expected_command_line, method: :command, mocked_outpu git = Git.init('test_project') git.lib.define_singleton_method(method) do |*cmd, **opts, &block| - actual_command_line = [*cmd, opts] + if include_env + actual_command_line = [env_overrides, *cmd, opts] + else + actual_command_line = [*cmd, opts] + end mocked_output end @@ -149,6 +153,8 @@ def assert_command_line_eq(expected_command_line, method: :command, mocked_outpu end end + expected_command_line = expected_command_line.call if expected_command_line.is_a?(Proc) + assert_equal(expected_command_line, actual_command_line) command_output diff --git a/tests/units/test_command_line_env_overrides.rb b/tests/units/test_command_line_env_overrides.rb new file mode 100644 index 00000000..37f14bfa --- /dev/null +++ b/tests/units/test_command_line_env_overrides.rb @@ -0,0 +1,48 @@ + +# frozen_string_literal: true + +require 'test_helper' + +class TestCommandLineEnvOverrides < Test::Unit::TestCase + test 'it should set the expected environment variables' do + expected_command_line = nil + expected_command_line_proc = ->{ expected_command_line } + assert_command_line_eq(expected_command_line_proc, include_env: true) do |git| + expected_env = { + 'GIT_DIR' => git.lib.git_dir, + 'GIT_INDEX_FILE' => git.lib.git_index_file, + 'GIT_SSH' => nil, + 'GIT_WORK_TREE' => git.lib.git_work_dir + } + expected_command_line = [expected_env, 'checkout', {}] + + git.checkout + end + end + + test 'it should set the GIT_SSH environment variable from Git::Base.config.git_ssh' do + expected_command_line = nil + expected_command_line_proc = ->{ expected_command_line } + + saved_git_ssh = Git::Base.config.git_ssh + begin + Git::Base.config.git_ssh = 'ssh -i /path/to/key' + + assert_command_line_eq(expected_command_line_proc, include_env: true) do |git| + # Set the expected command line + + expected_env = { + 'GIT_DIR' => git.lib.git_dir, + 'GIT_INDEX_FILE' => git.lib.git_index_file, + 'GIT_SSH' => 'ssh -i /path/to/key', + 'GIT_WORK_TREE' => git.lib.git_work_dir + } + + expected_command_line = [expected_env, 'checkout', {}] + git.checkout + end + ensure + Git::Base.config.git_ssh = saved_git_ssh + end + end +end From f407b92d14a5deb85dd8327f61d919c1892ef4d6 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 27 Feb 2025 17:18:16 -0800 Subject: [PATCH 59/72] feat: set the locale to en_US.UTF-8 for git commands --- lib/git/lib.rb | 1 + tests/units/test_command_line_env_overrides.rb | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 0682a070..7d9cbc3c 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1548,6 +1548,7 @@ def env_overrides 'GIT_WORK_TREE' => @git_work_dir, 'GIT_INDEX_FILE' => @git_index_file, 'GIT_SSH' => Git::Base.config.git_ssh, + 'LC_ALL' => 'en_US.UTF-8' } end diff --git a/tests/units/test_command_line_env_overrides.rb b/tests/units/test_command_line_env_overrides.rb index 37f14bfa..a89da4d4 100644 --- a/tests/units/test_command_line_env_overrides.rb +++ b/tests/units/test_command_line_env_overrides.rb @@ -12,7 +12,8 @@ class TestCommandLineEnvOverrides < Test::Unit::TestCase 'GIT_DIR' => git.lib.git_dir, 'GIT_INDEX_FILE' => git.lib.git_index_file, 'GIT_SSH' => nil, - 'GIT_WORK_TREE' => git.lib.git_work_dir + 'GIT_WORK_TREE' => git.lib.git_work_dir, + 'LC_ALL' => 'en_US.UTF-8' } expected_command_line = [expected_env, 'checkout', {}] @@ -29,16 +30,15 @@ class TestCommandLineEnvOverrides < Test::Unit::TestCase Git::Base.config.git_ssh = 'ssh -i /path/to/key' assert_command_line_eq(expected_command_line_proc, include_env: true) do |git| - # Set the expected command line - expected_env = { 'GIT_DIR' => git.lib.git_dir, 'GIT_INDEX_FILE' => git.lib.git_index_file, 'GIT_SSH' => 'ssh -i /path/to/key', - 'GIT_WORK_TREE' => git.lib.git_work_dir + 'GIT_WORK_TREE' => git.lib.git_work_dir, + 'LC_ALL' => 'en_US.UTF-8' } - expected_command_line = [expected_env, 'checkout', {}] + git.checkout end ensure From 9d441465f4f484cf965e2c28eafa6b5259424b0c Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 27 Feb 2025 17:33:55 -0800 Subject: [PATCH 60/72] chore: update the development dependency on the minitar gem --- git.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git.gemspec b/git.gemspec index a81ba60b..f8c49bdc 100644 --- a/git.gemspec +++ b/git.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency 'rchardet', '~> 1.9' s.add_development_dependency 'create_github_release', '~> 2.1' - s.add_development_dependency 'minitar', '~> 0.12' + s.add_development_dependency 'minitar', '~> 1.0' s.add_development_dependency 'mocha', '~> 2.7' s.add_development_dependency 'rake', '~> 13.2' s.add_development_dependency 'test-unit', '~> 3.6' From b47eedc15923c39e7ffe72510fda4f245debe5ef Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Wed, 14 May 2025 23:14:37 +0200 Subject: [PATCH 61/72] Improved error message of rev_parse As described by git-rev-parse: Many Git porcelainish commands take mixture of flags (i.e. parameters that begin with a dash -) and parameters meant for the underlying git rev-list command they use internally and flags and parameters for the other commands they use downstream of git rev-list. This command is used to distinguish between them. Using the `--` to separate revisions from paths is at the core of git. I do not think this behavior will ever change. The message without the extra parameters: fatal: ambiguous argument 'v3': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this: 'git [...] -- [...]' The message with new parameters: fatal: bad revision 'NOTFOUND' I think it's way more descriptive. --- lib/git/lib.rb | 2 +- tests/units/test_lib.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 7d9cbc3c..b62d69c1 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -333,7 +333,7 @@ def full_log_commits(opts = {}) def rev_parse(revision) assert_args_are_not_options('rev', revision) - command('rev-parse', revision) + command('rev-parse', '--revs-only', '--end-of-options', revision, '--') end # For backwards compatibility with the old method name diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index fb319be8..af613d1f 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -199,7 +199,7 @@ def test_rev_parse_with_bad_revision end def test_rev_parse_with_unknown_revision - assert_raise(Git::FailedError) do + assert_raise_with_message(Git::FailedError, /exit 128, stderr: "fatal: bad revision 'NOTFOUND'"/) do @lib.rev_parse('NOTFOUND') end end From 31374263eafea4e23352494ef4f6bea3ce62c1b5 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 14 May 2025 15:01:46 -0700 Subject: [PATCH 62/72] chore: release v3.0.1 Signed-off-by: James Couball --- CHANGELOG.md | 12 ++++++++++++ lib/git/version.rb | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59dae355..b31fed33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ # Change Log +## v3.0.1 (2025-05-14) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v3.0.0..v3.0.1) + +Changes since v3.0.0: + +* b47eedc Improved error message of rev_parse +* 9d44146 chore: update the development dependency on the minitar gem +* f407b92 feat: set the locale to en_US.UTF-8 for git commands +* b060e47 test: verify that command line envionment variables are set as expected +* 1a5092a chore: release v3.0.0 + ## v3.0.0 (2025-02-27) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.3.3..v3.0.0) diff --git a/lib/git/version.rb b/lib/git/version.rb index 81e4d967..eb507c85 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -3,5 +3,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='3.0.0' + VERSION='3.0.1' end From 7ebe0f8626ecb2f0da023b903b82f7332d8afaf6 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 14 May 2025 17:46:38 -0700 Subject: [PATCH 63/72] chore: enforce conventional commit messages with husky and commitlint - Add steps to bin/setup to install husky and the commitlint npm packages - Configure husky to run commitlint via the commit-msg hook - Add commitlint configuration based on my specific preferences - Add npm specific files (node_modules/, package-lock.json) to .gitignore --- .commitlintrc.yml | 38 +++++++++++++++++++++++++ .gitignore | 2 ++ .husky/commit-msg | 1 + CONTRIBUTING.md | 72 +++++++++++++++++++++++++++-------------------- bin/setup | 7 ++++- package.json | 10 +++++++ 6 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 .commitlintrc.yml create mode 100644 .husky/commit-msg create mode 100644 package.json diff --git a/.commitlintrc.yml b/.commitlintrc.yml new file mode 100644 index 00000000..3e08fa81 --- /dev/null +++ b/.commitlintrc.yml @@ -0,0 +1,38 @@ +--- +extends: '@commitlint/config-conventional' + +rules: + # See: https://commitlint.js.org/reference/rules.html + # + # Rules are made up by a name and a configuration array. The configuration + # array contains: + # + # * Severity [0..2]: 0 disable rule, 1 warning if violated, or 2 error if + # violated + # * Applicability [always|never]: never inverts the rule + # * Value: value to use for this rule (if applicable) + # + # Run `npx commitlint --print-config` to see the current setting for all + # rules. + # + header-max-length: [2, always, 100] # Header can not exceed 100 chars + + type-case: [2, always, lower-case] # Type must be lower case + type-empty: [2, never] # Type must not be empty + + # Supported conventional commit types + type-enum: [2, always, [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test]] + + scope-case: [2, always, lower-case] # Scope must be lower case + + # Error if subject is one of these cases (encourages lower-case) + subject-case: [2, never, [sentence-case, start-case, pascal-case, upper-case]] + subject-empty: [2, never] # Subject must not be empty + subject-full-stop: [2, never, "."] # Subject must not end with a period + + body-leading-blank: [2, always] # Body must have a blank line before it + body-max-line-length: [2, always, 100] # Body lines can not exceed 100 chars + + footer-leading-blank: [2, always] # Footer must have a blank line before it + footer-max-line-length: [2, always, 100] # Footer lines can not exceed 100 chars + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 611ed70c..13dcea11 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ doc pkg rdoc Gemfile.lock +node_modules +package-lock.json \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 00000000..70bd3dd2 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no-install commitlint --edit "$1" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 10793a4a..9a7a4e35 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,28 +5,28 @@ # Contributing to the git gem -* [Summary](#summary) -* [How to contribute](#how-to-contribute) -* [How to report an issue or request a feature](#how-to-report-an-issue-or-request-a-feature) -* [How to submit a code or documentation change](#how-to-submit-a-code-or-documentation-change) - * [Commit your changes to a fork of `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) - * [Create a pull request](#create-a-pull-request) - * [Get your pull request reviewed](#get-your-pull-request-reviewed) -* [Design philosophy](#design-philosophy) - * [Direct mapping to git commands](#direct-mapping-to-git-commands) - * [Parameter naming](#parameter-naming) - * [Output processing](#output-processing) -* [Coding standards](#coding-standards) - * [1 PR = 1 Commit](#1-pr--1-commit) - * [Unit tests](#unit-tests) - * [Continuous integration](#continuous-integration) - * [Documentation](#documentation) -* [Building a specific version of the Git command-line](#building-a-specific-version-of-the-git-command-line) - * [Install pre-requisites](#install-pre-requisites) - * [Obtain Git source code](#obtain-git-source-code) - * [Build git](#build-git) - * [Use the new Git version](#use-the-new-git-version) -* [Licensing](#licensing) +- [Summary](#summary) +- [How to contribute](#how-to-contribute) +- [How to report an issue or request a feature](#how-to-report-an-issue-or-request-a-feature) +- [How to submit a code or documentation change](#how-to-submit-a-code-or-documentation-change) + - [Commit your changes to a fork of `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) + - [Create a pull request](#create-a-pull-request) + - [Get your pull request reviewed](#get-your-pull-request-reviewed) +- [Design philosophy](#design-philosophy) + - [Direct mapping to git commands](#direct-mapping-to-git-commands) + - [Parameter naming](#parameter-naming) + - [Output processing](#output-processing) +- [Coding standards](#coding-standards) + - [Commit message guidelines](#commit-message-guidelines) + - [Unit tests](#unit-tests) + - [Continuous integration](#continuous-integration) + - [Documentation](#documentation) +- [Building a specific version of the Git command-line](#building-a-specific-version-of-the-git-command-line) + - [Install pre-requisites](#install-pre-requisites) + - [Obtain Git source code](#obtain-git-source-code) + - [Build git](#build-git) + - [Use the new Git version](#use-the-new-git-version) +- [Licensing](#licensing) ## Summary @@ -153,18 +153,30 @@ behavior. To ensure high-quality contributions, all pull requests must meet the following requirements: -### 1 PR = 1 Commit +### Commit message guidelines -* All commits for a PR must be squashed into a single commit. -* To avoid an extra merge commit, the PR must be able to be merged as [a fast-forward - merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging). -* The easiest way to ensure a fast-forward merge is to rebase your local branch to - the `ruby-git` master branch. +All commit messages must follow the [Conventional Commits +standard](https://www.conventionalcommits.org/en/v1.0.0/). This helps us maintain a +clear and structured commit history, automate versioning, and generate changelogs +effectively. + +To ensure compliance, this project includes: + +- A git commit-msg hook that validates your commit messages before they are accepted. + + To activate the hook, you must have node installed and run `bin/setup` or + `npm install`. + +- A GitHub Actions workflow that will enforce the Conventional Commit standard as + part of the continuous integration pipeline. + + Any commit message that does not conform to the Conventional Commits standard will + cause the workflow to fail and not allow the PR to be merged. ### Unit tests -* All changes must be accompanied by new or modified unit tests. -* The entire test suite must pass when `bundle exec rake default` is run from the +- All changes must be accompanied by new or modified unit tests. +- The entire test suite must pass when `bundle exec rake default` is run from the project's local working copy. While working on specific features, you can run individual test files or a group of diff --git a/bin/setup b/bin/setup index dce67d86..f16ff654 100755 --- a/bin/setup +++ b/bin/setup @@ -5,4 +5,9 @@ set -vx bundle install -# Do any other automated setup that you need to do here +if [ -x "$(command -v npm)" ]; then + npm install +else + echo "npm is not installed" + echo "Install npm then re-run this script to enable the conventional commit git hook." +fi diff --git a/package.json b/package.json new file mode 100644 index 00000000..2924004f --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0", + "husky": "^9.1.7" + }, + "scripts": { + "prepare": "husky" + } +} From 1da4c44620a3264d4e837befd3f40416c5d8f1d8 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 14 May 2025 18:01:49 -0700 Subject: [PATCH 64/72] chore: enforce conventional commit messages with a GitHub action - Add a GitHub Actions workflow to enforce conventional commits - Add commitlint configuration based on my specific preferences --- .../enforce_conventional_commits.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/enforce_conventional_commits.yml diff --git a/.github/workflows/enforce_conventional_commits.yml b/.github/workflows/enforce_conventional_commits.yml new file mode 100644 index 00000000..8aaa93f8 --- /dev/null +++ b/.github/workflows/enforce_conventional_commits.yml @@ -0,0 +1,28 @@ +--- +name: Conventional Commits + +permissions: + contents: read + +on: + pull_request: + branches: + - master + +jobs: + commit-lint: + name: Verify Conventional Commits + + # Skip this job if this is a release PR + if: (github.event_name == 'pull_request' && !startsWith(github.event.pull_request.head.ref, 'release-please--')) + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: { fetch-depth: 0 } + + - name: Check Commit Messages + uses: wagoid/commitlint-github-action@v6 + with: { configFile: .commitlintrc.yml } From 06480e65e2441348230ef10e05cc1c563d0e7ea8 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 14 May 2025 20:59:31 -0700 Subject: [PATCH 65/72] build: automate continuous delivery workflow Use googleapis/release-please-action and rubygems/release-gem actions to automate releasing and publishing new gem versions to rubygems. --- .github/workflows/release.yml | 52 +++++++++++++++++++++ .release-please-manifest.json | 3 ++ .yardopts | 1 - RELEASING.md | 85 ----------------------------------- Rakefile | 7 +++ release-please-config.json | 36 +++++++++++++++ 6 files changed, 98 insertions(+), 86 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .release-please-manifest.json delete mode 100644 RELEASING.md create mode 100644 release-please-config.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..607f16ce --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +--- +name: Release Gem + +description: | + This workflow creates a new release on GitHub and publishes the gem to + RubyGems.org. + + The workflow uses the `googleapis/release-please-action` to handle the + release creation process and the `rubygems/release-gem` action to publish + the gem to rubygems.org + +on: + push: + branches: ["main"] + + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + + environment: + name: RubyGems + url: https://rubygems.org/gems/git + + permissions: + contents: write + pull-requests: write + id-token: write + + steps: + - name: Checkout project + uses: actions/checkout@v4 + + - name: Create release + uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ secrets.AUTO_RELEASE_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + if: ${{ steps.release.outputs.release_created }} + with: + bundler-cache: true + ruby-version: ruby + + - name: Push to RubyGems.org + uses: rubygems/release-gem@v1 + if: ${{ steps.release.outputs.release_created }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..d6f54056 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "3.0.1" +} diff --git a/.yardopts b/.yardopts index ce1aff3c..105b79a9 100644 --- a/.yardopts +++ b/.yardopts @@ -7,5 +7,4 @@ README.md CHANGELOG.md CONTRIBUTING.md -RELEASING.md MAINTAINERS.md diff --git a/RELEASING.md b/RELEASING.md deleted file mode 100644 index ead6293a..00000000 --- a/RELEASING.md +++ /dev/null @@ -1,85 +0,0 @@ - - -# How to release a new git.gem - -Releasing a new version of the `git` gem requires these steps: - -* [Install Prerequisites](#install-prerequisites) -* [Determine the SemVer release type](#determine-the-semver-release-type) -* [Create the release](#create-the-release) -* [Review the CHANGELOG and release PR](#review-the-changelog-and-release-pr) -* [Manually merge the release PR](#manually-merge-the-release-pr) -* [Publish the git gem to RubyGems.org](#publish-the-git-gem-to-rubygemsorg) - -## Install Prerequisites - -The following tools need to be installed in order to create the release: - -* [create_githhub_release](https://github.com/main-branch/create_github_release) is used to create the release -* [git](https://git-scm.com) is used by `create-github-release` to interact with the local and remote repositories -* [gh](https://cli.github.com) is used by `create-github-release` to create the release and PR in GitHub - -On a Mac, these tools can be installed using [gem](https://guides.rubygems.org/rubygems-basics/) and [brew](https://brew.sh): - -```shell -$ gem install create_github_release -... -$ brew install git -... -$ brew install gh -... -$ -``` - -## Determine the SemVer release type - -Determine the SemVer version increment that should be applied for the new release: - -* `major`: when the release includes incompatible API or functional changes. -* `minor`: when the release adds functionality in a backward-compatible manner -* `patch`: when the release includes small user-facing changes that are - backward-compatible and do not introduce new functionality. - -## Create the release - -Create the release using the `create-github-release` command. If the release type -is `major`, the command is: - -```shell -create-github-release major -``` - -Follow the directions given by the `create-github-release` command to finish the -release. Where the instructions given by the command differ than the instructions -below, follow the instructions given by the command. - -## Review the CHANGELOG and release PR - -The `create-github-release` command will output a link to the CHANGELOG and the PR -it created for the release. Review the CHANGELOG and have someone review and approve -the release PR. - -## Manually merge the release PR - -It is important to manually merge the PR so a separate merge commit can be avoided. -Use the commands output by the `create-github-release` which will looks like this -if you are creating a 2.0.0 release: - -```shell -git checkout master -git merge --ff-only release-v2.0.0 -git push -``` - -This will automatically close the release PR. - -## Publish the git gem to RubyGems.org - -Finally, publish the git gem to RubyGems.org using the following command: - -```shell -rake release:rubygem_push -``` diff --git a/Rakefile b/Rakefile index e2d8ef2a..72b93352 100644 --- a/Rakefile +++ b/Rakefile @@ -58,3 +58,10 @@ task :'test:gem' => :install do puts 'Gem Test Succeeded' end + +# Make it so that calling `rake release` just calls `rake release:rubygem_push` to +# avoid creating and pushing a new tag. + +Rake::Task['release'].clear +desc 'Customized release task to avoid creating a new tag' +task release: 'release:rubygem_push' diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..b0c93860 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,36 @@ +{ + "bootstrap-sha": "31374263eafea4e23352494ef4f6bea3ce62c1b5", + "packages": { + ".": { + "release-type": "ruby", + "package-name": "git", + "changelog-path": "CHANGELOG.md", + "version-file": "lib/git/version.rb", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "draft": false, + "prerelease": false, + "include-component-in-tag": false, + "pull-request-title-pattern": "chore: release v${version}", + "changelog-sections": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "build", "section": "Other Changes", "hidden": false }, + { "type": "chore", "section": "Other Changes", "hidden": false }, + { "type": "ci", "section": "Other Changes", "hidden": false }, + { "type": "docs", "section": "Other Changes", "hidden": false }, + { "type": "perf", "section": "Other Changes", "hidden": false }, + { "type": "refactor", "section": "Other Changes", "hidden": false }, + { "type": "revert", "section": "Other Changes", "hidden": false }, + { "type": "style", "section": "Other Changes", "hidden": false }, + { "type": "test", "section": "Other Changes", "hidden": false } + ] + } + }, + "plugins": [ + { + "type": "sentence-case" + } + ], + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" +} From c8611f1e68e73825fd16bd475752a40b0088d4ae Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 14 May 2025 21:09:07 -0700 Subject: [PATCH 66/72] fix: trigger the release workflow on a change to 'master' insetad of 'main' --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 607f16ce..eaea43f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ description: | on: push: - branches: ["main"] + branches: ["master"] workflow_dispatch: From 880d38e4d36e598b47c7d487d49b56c6541ebf66 Mon Sep 17 00:00:00 2001 From: James Couball Date: Wed, 14 May 2025 21:31:07 -0700 Subject: [PATCH 67/72] chore: release v3.0.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ lib/git/version.rb | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d6f54056..e28eff59 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.0.1" + ".": "3.0.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b31fed33..0fec2948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ # Change Log +## [3.0.2](https://github.com/ruby-git/ruby-git/compare/v3.0.1...v3.0.2) (2025-05-15) + + +### Bug Fixes + +* Trigger the release workflow on a change to 'master' insetad of 'main' ([c8611f1](https://github.com/ruby-git/ruby-git/commit/c8611f1e68e73825fd16bd475752a40b0088d4ae)) + + +### Other Changes + +* Automate continuous delivery workflow ([06480e6](https://github.com/ruby-git/ruby-git/commit/06480e65e2441348230ef10e05cc1c563d0e7ea8)) +* Enforce conventional commit messages with a GitHub action ([1da4c44](https://github.com/ruby-git/ruby-git/commit/1da4c44620a3264d4e837befd3f40416c5d8f1d8)) +* Enforce conventional commit messages with husky and commitlint ([7ebe0f8](https://github.com/ruby-git/ruby-git/commit/7ebe0f8626ecb2f0da023b903b82f7332d8afaf6)) + ## v3.0.1 (2025-05-14) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v3.0.0..v3.0.1) diff --git a/lib/git/version.rb b/lib/git/version.rb index eb507c85..6831d2c1 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -3,5 +3,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='3.0.1' + VERSION='3.0.2' end From a832259314aa9c8bdd7719e50d425917df1df831 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 15 May 2025 09:48:44 -0700 Subject: [PATCH 68/72] docs: announce and document guidelines for using Conventional Commits --- CONTRIBUTING.md | 87 +++++++++++++++++++++++++++++++++++++++++-------- README.md | 64 ++++++++++++++++-------------------- 2 files changed, 102 insertions(+), 49 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a7a4e35..653290f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,6 +18,8 @@ - [Output processing](#output-processing) - [Coding standards](#coding-standards) - [Commit message guidelines](#commit-message-guidelines) + - [What does this mean for contributors?](#what-does-this-mean-for-contributors) + - [What to know about Conventional Commits](#what-to-know-about-conventional-commits) - [Unit tests](#unit-tests) - [Continuous integration](#continuous-integration) - [Documentation](#documentation) @@ -63,7 +65,8 @@ thoroughly as possible to describe the issue or feature request. There is a three-step process for submitting code or documentation changes: 1. [Commit your changes to a fork of - `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) + `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) using [Conventional + Commits](#commit-message-guidelines) 2. [Create a pull request](#create-a-pull-request) 3. [Get your pull request reviewed](#get-your-pull-request-reviewed) @@ -155,23 +158,81 @@ requirements: ### Commit message guidelines -All commit messages must follow the [Conventional Commits -standard](https://www.conventionalcommits.org/en/v1.0.0/). This helps us maintain a -clear and structured commit history, automate versioning, and generate changelogs -effectively. +To enhance our development workflow, enable automated changelog generation, and pave +the way for Continuous Delivery, the `ruby-git` project has adopted the [Conventional +Commits standard](https://www.conventionalcommits.org/en/v1.0.0/) for all commit +messages. -To ensure compliance, this project includes: +This structured approach to commit messages allows us to: -- A git commit-msg hook that validates your commit messages before they are accepted. +- **Automate versioning and releases:** Tools can now automatically determine the + semantic version bump (patch, minor, major) based on the types of commits merged. +- **Generate accurate changelogs:** We can automatically create and update a + `CHANGELOG.md` file, providing a clear history of changes for users and + contributors. +- **Improve commit history readability:** A standardized format makes it easier for + everyone to understand the nature of changes at a glance. - To activate the hook, you must have node installed and run `bin/setup` or - `npm install`. +#### What does this mean for contributors? -- A GitHub Actions workflow that will enforce the Conventional Commit standard as - part of the continuous integration pipeline. +Going forward, all commits to this repository **MUST** adhere to the [Conventional +Commits standard](https://www.conventionalcommits.org/en/v1.0.0/). Commits not +adhering to this standard will cause the CI build to fail. PRs will not be merged if +they include non-conventional commits. - Any commit message that does not conform to the Conventional Commits standard will - cause the workflow to fail and not allow the PR to be merged. +A git pre-commit hook may be installed to validate your conventional commit messages +before pushing them to GitHub by running `bin/setup` in the project root. + +#### What to know about Conventional Commits + +The simplist conventional commit is in the form `type: description` where `type` +indicates the type of change and `description` is your usual commit message (with +some limitations). + +- Types include: `feat`, `fix`, `docs`, `test`, `refactor`, and `chore`. See the full + list of types supported in [.commitlintrc.yml](.commitlintrc.yml). +- The description must (1) not start with an upper case letter, (2) be no more than + 100 characters, and (3) not end with punctuation. + +Examples of valid commits: + +- `feat: add the --merges option to Git::Lib.log` +- `fix: exception thrown by Git::Lib.log when repo has no commits` +- `docs: add conventional commit announcement to README.md` + +Commits that include breaking changes must include an exclaimation mark before the +colon: + +- `feat!: removed Git::Base.commit_force` + +The commit messages will drive how the version is incremented for each release: + +- a release containing a **breaking change** will do a **major** version increment +- a release containing a **new feature** will do a **minor** increment +- a release containing **neither a breaking change nor a new feature** will do a + **patch** version increment + +The full conventional commit format is: + +```text +[optional scope][!]: + +[optional body] + +[optional footer(s)] +``` + +- `optional body` may include multiple lines of descriptive text limited to 100 chars + each +- `optional footers` only uses `BREAKING CHANGE: ` where description + should describe the nature of the backward incompatibility. + +Use of the `BREAKING CHANGE:` footer flags a backward incompatible change even if it +is not flagged with an exclaimation mark after the `type`. Other footers are allowed +by not acted upon. + +See [the Conventional Commits +specification](https://www.conventionalcommits.org/en/v1.0.0/) for more details. ### Unit tests diff --git a/README.md b/README.md index c3f788ca..74e6ad4c 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,34 @@ [![Documentation](https://img.shields.io/badge/Documentation-Latest-green)](https://rubydoc.info/gems/git/) [![Change Log](https://img.shields.io/badge/CHANGELOG-Latest-green)](https://rubydoc.info/gems/git/file/CHANGELOG.md) [![Build Status](https://github.com/ruby-git/ruby-git/workflows/CI/badge.svg?branch=master)](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) -[![Code Climate](https://codeclimate.com/github/ruby-git/ruby-git.png)](https://codeclimate.com/github/ruby-git/ruby-git) - -* [Summary](#summary) -* [v2.x Release](#v2x-release) -* [Install](#install) -* [Major Objects](#major-objects) -* [Errors Raised By This Gem](#errors-raised-by-this-gem) -* [Specifying And Handling Timeouts](#specifying-and-handling-timeouts) -* [Examples](#examples) -* [Ruby version support policy](#ruby-version-support-policy) -* [License](#license) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) + +- [📢 We've Switched to Conventional Commits 📢](#-weve-switched-to-conventional-commits-) +- [Summary](#summary) +- [Install](#install) +- [Major Objects](#major-objects) +- [Errors Raised By This Gem](#errors-raised-by-this-gem) +- [Specifying And Handling Timeouts](#specifying-and-handling-timeouts) +- [Examples](#examples) +- [Ruby version support policy](#ruby-version-support-policy) +- [License](#license) + +## 📢 We've Switched to Conventional Commits 📢 + +To enhance our development workflow, enable automated changelog generation, and pave +the way for Continuous Delivery, the `ruby-git` project has adopted the [Conventional +Commits standard](https://www.conventionalcommits.org/en/v1.0.0/) for all commit +messages. + +Going forward, all commits to this repository **MUST** adhere to the Conventional +Commits standard. Commits not adhering to this standard will cause the CI build to +fail. PRs will not be merged if they include non-conventional commits. + +A git pre-commit hook may be installed to validate your conventional commit messages +before pushing them to GitHub by running `bin/setup` in the project root. + +Read more about this change in the [Commit Message Guidelines section of +CONTRIBUTING.md](CONTRIBUTING.md#commit-message-guidelines) ## Summary @@ -34,31 +51,6 @@ Get started by obtaining a repository object by: Methods that can be called on a repository object are documented in [Git::Base](https://rubydoc.info/gems/git/Git/Base) -## v2.x Release - -git 2.0.0 has recently been released. Please give it a try. - -**If you have problems with the 2.x release, open an issue and use the 1.x version -instead.** We will do our best to fix your issues in a timely fashion. - -**JRuby on Windows is not yet supported by the 2.x release line. Users running JRuby -on Windows should continue to use the 1.x release line.** - -The changes in this major release include: - -* Added a dependency on the activesupport gem to use the deprecation functionality -* Create a policy of supported Ruby versions to support only non-EOL Ruby versions -* Create a policy of supported Git CLI versions (released 2020-12-25) -* Update the required Ruby version to at least 3.0 (released 2020-07-27) -* Update the required Git command line version to at least 2.28 -* Update how CLI commands are called to use the [process_executer](https://github.com/main-branch/process_executer) - gem which is built on top of [Kernel.spawn](https://ruby-doc.org/3.3.0/Kernel.html#method-i-spawn). - See [PR #684](https://github.com/ruby-git/ruby-git/pull/684) for more details - on the motivation for this implementation. - -The `master` branch will be used for `2.x` development. If needed, fixes for `1.x` -version will be done on the `v1` branch. - ## Install Install the gem and add to the application's Gemfile by executing: From df3b07d0f14d79c6c77edc04550c1ad0207c920a Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 15 May 2025 10:48:16 -0700 Subject: [PATCH 69/72] feat: make Git::Log support the git log --merges option --- lib/git/lib.rb | 2 ++ lib/git/log.rb | 9 +++++++-- tests/test_helper.rb | 2 +- tests/units/test_log.rb | 5 +++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/git/lib.rb b/lib/git/lib.rb index b62d69c1..692ceef9 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -294,6 +294,7 @@ def log_commits(opts = {}) # * 'tree' [String] the tree sha # * 'author' [String] the author of the commit and timestamp of when the changes were created # * 'committer' [String] the committer of the commit and timestamp of when the commit was applied + # * 'merges' [Boolean] if truthy, only include merge commits (aka commits with 2 or more parents) # # @raise [ArgumentError] if the revision range (specified with :between or :object) is a string starting with a hyphen # @@ -305,6 +306,7 @@ def full_log_commits(opts = {}) arr_opts << '--pretty=raw' arr_opts << "--skip=#{opts[:skip]}" if opts[:skip] + arr_opts << '--merges' if opts[:merges] arr_opts += log_path_options(opts) diff --git a/lib/git/log.rb b/lib/git/log.rb index dad2c2cd..7ac31622 100644 --- a/lib/git/log.rb +++ b/lib/git/log.rb @@ -133,11 +133,16 @@ def cherry return self end + def merges + dirty_log + @merges = true + return self + end + def to_s self.map { |c| c.to_s }.join("\n") end - # forces git log to run def size @@ -184,7 +189,7 @@ def run_log log = @base.lib.full_log_commits( count: @max_count, all: @all, object: @object, path_limiter: @path, since: @since, author: @author, grep: @grep, skip: @skip, until: @until, between: @between, - cherry: @cherry + cherry: @cherry, merges: @merges ) @commits = log.map { |c| Git::Object::Commit.new(@base, c['sha'], c) } end diff --git a/tests/test_helper.rb b/tests/test_helper.rb index 067fa633..f35a0fcd 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -131,7 +131,7 @@ def append_file(name, contents) # # @return [void] # - def assert_command_line_eq(expected_command_line, method: :command, mocked_output: nil, include_env: false) + def assert_command_line_eq(expected_command_line, method: :command, mocked_output: '', include_env: false) actual_command_line = nil command_output = '' diff --git a/tests/units/test_log.rb b/tests/units/test_log.rb index 1cab1a32..f18fabf2 100644 --- a/tests/units/test_log.rb +++ b/tests/units/test_log.rb @@ -128,4 +128,9 @@ def test_log_cherry l = @git.log.between( 'master', 'cherry').cherry assert_equal( 1, l.size ) end + + def test_log_merges + expected_command_line = ['log', '--max-count=30', '--no-color', '--pretty=raw', '--merges', {:chdir=>nil}] + assert_command_line_eq(expected_command_line) { |git| git.log.merges.size } + end end From f647a18c8a3ae78f49c8cd485db4660aa10a92fc Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 15 May 2025 11:11:16 -0700 Subject: [PATCH 70/72] build: skip continuous integration workflow for release PRs --- .github/workflows/continuous_integration.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 5bc83dd3..e54df88c 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -10,6 +10,11 @@ on: jobs: build: name: Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }} + + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'pull_request' && !startsWith(github.event.pull_request.head.ref, 'release-please--')) + runs-on: ${{ matrix.operating-system }} continue-on-error: ${{ matrix.experimental == 'Yes' }} env: { JAVA_OPTS: -Djdk.io.File.enableADS=true } From 3dab0b34e41393a43437c53a53b96895fd3d2cc5 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 15 May 2025 11:56:02 -0700 Subject: [PATCH 71/72] build: skip the experiemental build workflow if a release commit is pushed to master --- .github/workflows/continuous_integration.yml | 5 ++--- .../workflows/experimental_continuous_integration.yml | 9 ++++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index e54df88c..c21e97cd 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -1,16 +1,15 @@ name: CI on: - push: - branches: [master,v1] pull_request: - branches: [master,v1] + branches: [master] workflow_dispatch: jobs: build: name: Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }} + # Skip this job if triggered by a release PR if: >- github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && !startsWith(github.event.pull_request.head.ref, 'release-please--')) diff --git a/.github/workflows/experimental_continuous_integration.yml b/.github/workflows/experimental_continuous_integration.yml index 44dc7889..488ab797 100644 --- a/.github/workflows/experimental_continuous_integration.yml +++ b/.github/workflows/experimental_continuous_integration.yml @@ -2,12 +2,19 @@ name: CI Experimental on: push: - branches: [master,v1] + branches: [master] + workflow_dispatch: jobs: build: name: Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }} + + # Skip this job if triggered by pushing a release commit + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && !startsWith(github.event.head_commit.message, 'chore: release ')) + runs-on: ${{ matrix.operating-system }} continue-on-error: true env: { JAVA_OPTS: -Djdk.io.File.enableADS=true } From b7da131cd2946af9159d515667df4af33016a6ae Mon Sep 17 00:00:00 2001 From: James Couball Date: Sun, 18 May 2025 14:02:33 -0700 Subject: [PATCH 72/72] chore: release v3.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ lib/git/version.rb | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e28eff59..ada7355e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "3.0.2" + ".": "3.1.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fec2948..5602c70e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ # Change Log +## [3.1.0](https://github.com/ruby-git/ruby-git/compare/v3.0.2...v3.1.0) (2025-05-18) + + +### Features + +* Make Git::Log support the git log --merges option ([df3b07d](https://github.com/ruby-git/ruby-git/commit/df3b07d0f14d79c6c77edc04550c1ad0207c920a)) + + +### Other Changes + +* Announce and document guidelines for using Conventional Commits ([a832259](https://github.com/ruby-git/ruby-git/commit/a832259314aa9c8bdd7719e50d425917df1df831)) +* Skip continuous integration workflow for release PRs ([f647a18](https://github.com/ruby-git/ruby-git/commit/f647a18c8a3ae78f49c8cd485db4660aa10a92fc)) +* Skip the experiemental build workflow if a release commit is pushed to master ([3dab0b3](https://github.com/ruby-git/ruby-git/commit/3dab0b34e41393a43437c53a53b96895fd3d2cc5)) + ## [3.0.2](https://github.com/ruby-git/ruby-git/compare/v3.0.1...v3.0.2) (2025-05-15) diff --git a/lib/git/version.rb b/lib/git/version.rb index 6831d2c1..0a293cc1 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -3,5 +3,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='3.0.2' + VERSION='3.1.0' end