From 8df062ddcfed07fa806958ab97cd9f1d45ce4c03 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 15 Mar 2024 13:22:01 -0700 Subject: [PATCH 01/81] Release v2.0.0.pre3 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 073223fd..3f20ddb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ # Change Log +## 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) + +Changes since v2.0.0.pre2: + +* 5d4b34e Allow allow_unrelated_histories option for Base#pull + ## v2.0.0.pre2 (2024-02-24) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v2.0.0.pre1..v2.0.0.pre2) diff --git a/lib/git/version.rb b/lib/git/version.rb index d50f3c40..35580479 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.pre2' + VERSION='2.0.0.pre3' end From e4d6a773a5105e98558dd802d0faff013cd71635 Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 19 Mar 2024 16:34:05 -0700 Subject: [PATCH 02/81] Show log(x).since combination in README Signed-off-by: James Couball --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 64f05cac..2715d2f6 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,12 @@ g.index.writable? g.repo g.dir -g.log # returns a Git::Log object, which is an Enumerator of Git::Commit objects -g.log(200) -g.log.since('2 weeks ago') +# log - returns a Git::Log object, which is an Enumerator of Git::Commit objects +# default configuration returns a max of 30 commits +g.log +g.log(200) # 200 most recent commits +g.log.since('2 weeks ago') # default count of commits since 2 weeks ago. +g.log(200).since('2 weeks ago') # commits since 2 weeks ago, limited to 200. g.log.between('v2.5', 'v2.6') g.log.each {|l| puts l.sha } g.gblob('v2.5:Makefile').log.since('2 weeks ago') From d9570ab6191aa79a02ea09556cb08d9b8388ce8a Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 19 Mar 2024 18:16:31 -0700 Subject: [PATCH 03/81] Move issue and pull request templates to the .github directory Signed-off-by: James Couball --- ISSUE_TEMPLATE.md => .github/issue_template.md | 0 PULL_REQUEST_TEMPLATE.md => .github/pull_request_template.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename ISSUE_TEMPLATE.md => .github/issue_template.md (100%) rename PULL_REQUEST_TEMPLATE.md => .github/pull_request_template.md (100%) diff --git a/ISSUE_TEMPLATE.md b/.github/issue_template.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/issue_template.md diff --git a/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 100% rename from PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md From ec7c257e4e0301175c3d8fe9337e6f1fbdd635c0 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 30 Mar 2024 13:35:55 -0700 Subject: [PATCH 04/81] Remove unneeded scripts to create a new release Signed-off-by: James Couball --- Dockerfile.changelog-rs | 12 - bin/create-release | 506 ---------------------------------------- 2 files changed, 518 deletions(-) delete mode 100644 Dockerfile.changelog-rs delete mode 100755 bin/create-release diff --git a/Dockerfile.changelog-rs b/Dockerfile.changelog-rs deleted file mode 100644 index 75c35d93..00000000 --- a/Dockerfile.changelog-rs +++ /dev/null @@ -1,12 +0,0 @@ -FROM rust - -# Build the docker image (from this project's root directory): -# docker build --file Dockerfile.changelog-rs --tag changelog-rs . -# -# Use this image to output a changelog (from this project's root directory): -# docker run --rm --volume "$PWD:/worktree" changelog-rs v1.9.1 v1.10.0 - -RUN cargo install changelog-rs -WORKDIR /worktree - -ENTRYPOINT ["/usr/local/cargo/bin/changelog-rs", "/worktree"] diff --git a/bin/create-release b/bin/create-release deleted file mode 100755 index fdc8aa83..00000000 --- a/bin/create-release +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env ruby - -# Run this script while in the root directory of the project with the default -# branch checked out. - -require 'bump' -require 'English' -require 'fileutils' -require 'optparse' -require 'tempfile' - -# TODO: Right now the default branch and the remote name are hard coded - -class Options - attr_accessor :current_version, :next_version, :tag, :current_tag, :next_tag, :branch, :quiet - - def initialize - yield self if block_given? - end - - def release_type - raise "release_type not set" if @release_type.nil? - @release_type - end - - VALID_RELEASE_TYPES = %w(major minor patch) - - def release_type=(release_type) - raise 'release_type must be one of: ' + VALID_RELEASE_TYPES.join(', ') unless VALID_RELEASE_TYPES.include?(release_type) - @release_type = release_type - end - - def quiet - @quiet = false unless instance_variable_defined?(:@quiet) - @quiet - end - - def current_version - @current_version ||= Bump::Bump.current - end - - def next_version - current_version # Save the current version before bumping - @next_version ||= Bump::Bump.next_version(release_type) - end - - def tag - @tag ||= "v#{next_version}" - end - - def current_tag - @current_tag ||= "v#{current_version}" - end - - def next_tag - tag - end - - def branch - @branch ||= "release-#{tag}" - end - - def default_branch - @default_branch ||= `git remote show '#{remote}'`.match(/HEAD branch: (.*?)$/)[1] - end - - def remote - @remote ||= 'origin' - end - - def to_s - <<~OUTPUT - release_type='#{release_type}' - current_version='#{current_version}' - next_version='#{next_version}' - tag='#{tag}' - branch='#{branch}' - quiet=#{quiet} - OUTPUT - end -end - -class CommandLineParser - attr_reader :options - - def initialize - @option_parser = OptionParser.new - define_options - @options = Options.new - end - - def parse(args) - option_parser.parse!(remaining_args = args.dup) - parse_remaining_args(remaining_args) - # puts options unless options.quiet - options - end - - private - - attr_reader :option_parser - - def parse_remaining_args(remaining_args) - error_with_usage('No release type specified') if remaining_args.empty? - @options.release_type = remaining_args.shift || nil - error_with_usage('Too many args') unless remaining_args.empty? - end - - def error_with_usage(message) - warn <<~MESSAGE - ERROR: #{message} - #{option_parser} - MESSAGE - exit 1 - end - - def define_options - option_parser.banner = 'Usage: create_release --help | release-type' - option_parser.separator '' - option_parser.separator 'Options:' - - define_quiet_option - define_help_option - end - - def define_quiet_option - option_parser.on('-q', '--[no-]quiet', 'Do not show output') do |quiet| - options.quiet = quiet - end - end - - def define_help_option - option_parser.on_tail('-h', '--help', 'Show this message') do - puts option_parser - exit 0 - end - end -end - -class ReleaseAssertions - attr_reader :options - - def initialize(options) - @options = options - end - - def make_assertions - bundle_is_up_to_date - in_git_repo - in_repo_toplevel_directory - on_default_branch - no_uncommitted_changes - local_and_remote_on_same_commit - tag_does_not_exist - branch_does_not_exist - docker_is_running - changelog_docker_container_exists - gh_command_exists - end - - private - - def gh_command_exists - print "Checking that the gh command exists..." - `which gh > /dev/null 2>&1` - if $CHILD_STATUS.success? - puts "OK" - else - error "The gh command was not found" - end - end - - def docker_is_running - print "Checking that docker is installed and running..." - `docker info > /dev/null 2>&1` - if $CHILD_STATUS.success? - puts "OK" - else - error "Docker is not installed or not running" - end - end - - - def changelog_docker_container_exists - print "Checking that the changelog docker container exists (might take time to build)..." - `docker build --file Dockerfile.changelog-rs --tag changelog-rs . 1>/dev/null` - if $CHILD_STATUS.success? - puts "OK" - else - error "Failed to build the changelog-rs docker container" - end - end - - def bundle_is_up_to_date - print "Checking that the bundle is up to date..." - if File.exist?('Gemfile.lock') - print "Running bundle update..." - `bundle update --quiet` - if $CHILD_STATUS.success? - puts "OK" - else - error "bundle update failed" - end - else - print "Running bundle install..." - `bundle install --quiet` - if $CHILD_STATUS.success? - puts "OK" - else - error "bundle install failed" - end - end - end - - def in_git_repo - print "Checking that you are in a git repo..." - `git rev-parse --is-inside-work-tree --quiet > /dev/null 2>&1` - if $CHILD_STATUS.success? - puts "OK" - else - error "You are not in a git repo" - end - end - - def in_repo_toplevel_directory - print "Checking that you are in the repo's toplevel directory..." - toplevel_directory = `git rev-parse --show-toplevel`.chomp - if toplevel_directory == FileUtils.pwd - puts "OK" - else - error "You are not in the repo's toplevel directory" - end - end - - def on_default_branch - print "Checking that you are on the default branch..." - current_branch = `git branch --show-current`.chomp - if current_branch == options.default_branch - puts "OK" - else - error "You are not on the default branch '#{default_branch}'" - end - end - - def no_uncommitted_changes - print "Checking that there are no uncommitted changes..." - if `git status --porcelain | wc -l`.to_i == 0 - puts "OK" - else - error "There are uncommitted changes" - end - end - - def no_staged_changes - print "Checking that there are no staged changes..." - if `git diff --staged --name-only | wc -l`.to_i == 0 - puts "OK" - else - error "There are staged changes" - end - end - - def local_and_remote_on_same_commit - print "Checking that local and remote are on the same commit..." - local_commit = `git rev-parse HEAD`.chomp - remote_commit = `git ls-remote '#{options.remote}' '#{options.default_branch}' | cut -f 1`.chomp - if local_commit == remote_commit - puts "OK" - else - error "Local and remote are not on the same commit" - end - end - - def local_tag_does_not_exist - print "Checking that local tag '#{options.tag}' does not exist..." - - tags = `git tag --list "#{options.tag}"`.chomp - error 'Could not list tags' unless $CHILD_STATUS.success? - - if tags.split.empty? - puts 'OK' - else - error "'#{options.tag}' already exists" - end - end - - def remote_tag_does_not_exist - print "Checking that the remote tag '#{options.tag}' does not exist..." - `git ls-remote --tags --exit-code '#{options.remote}' #{options.tag} >/dev/null 2>&1` - unless $CHILD_STATUS.success? - puts "OK" - else - error "'#{options.tag}' already exists" - end - end - - def tag_does_not_exist - local_tag_does_not_exist - remote_tag_does_not_exist - end - - def local_branch_does_not_exist - print "Checking that local branch '#{options.branch}' does not exist..." - - if `git branch --list "#{options.branch}" | wc -l`.to_i.zero? - puts "OK" - else - error "'#{options.branch}' already exists." - end - end - - def remote_branch_does_not_exist - print "Checking that the remote branch '#{options.branch}' does not exist..." - `git ls-remote --heads --exit-code '#{options.remote}' '#{options.branch}' >/dev/null 2>&1` - unless $CHILD_STATUS.success? - puts "OK" - else - error "'#{options.branch}' already exists" - end - end - - def branch_does_not_exist - local_branch_does_not_exist - remote_branch_does_not_exist - end - - private - - def print(*args) - super unless options.quiet - end - - def puts(*args) - super unless options.quiet - end - - def error(message) - warn "ERROR: #{message}" - exit 1 - end -end - -class ReleaseCreator - attr_reader :options - - def initialize(options) - @options = options - end - - def create_release - create_branch - update_changelog - update_version - make_release_commit - create_tag - push_release_commit_and_tag - create_github_release - create_release_pull_request - end - - private - - def create_branch - print "Creating branch '#{options.branch}'..." - `git checkout -b "#{options.branch}" > /dev/null 2>&1` - if $CHILD_STATUS.success? - puts "OK" - else - error "Could not create branch '#{options.branch}'" unless $CHILD_STATUS.success? - end - end - - def update_changelog - print 'Updating CHANGELOG.md...' - changelog_lines = File.readlines('CHANGELOG.md') - first_entry = changelog_lines.index { |e| e =~ /^## / } - error "Could not find changelog insertion point" unless first_entry - FileUtils.rm('CHANGELOG.md') - File.write('CHANGELOG.md', <<~CHANGELOG.chomp) - #{changelog_lines[0..first_entry - 1].join}## #{options.tag} - - See https://github.com/ruby-git/ruby-git/releases/tag/#{options.tag} - - #{changelog_lines[first_entry..].join} - CHANGELOG - `git add CHANGELOG.md` - if $CHILD_STATUS.success? - puts 'OK' - else - error 'Could not stage changes to CHANGELOG.md' - end - end - - def update_version - print 'Updating version...' - message, status = Bump::Bump.run(options.release_type, commit: false) - error 'Could not bump version' unless status == 0 - `git add lib/git/version.rb` - if $CHILD_STATUS.success? - puts 'OK' - else - error 'Could not stage changes to lib/git/version.rb' - end - end - - def make_release_commit - print 'Making release commit...' - `git commit -s -m 'Release #{options.tag}'` - error 'Could not make release commit' unless $CHILD_STATUS.success? - end - - def create_tag - print "Creating tag '#{options.tag}'..." - `git tag '#{options.tag}'` - if $CHILD_STATUS.success? - puts 'OK' - else - error "Could not create tag '#{options.tag}'" - end - end - - def push_release_commit_and_tag - print "Pushing branch '#{options.branch}' to remote..." - `git push --tags --set-upstream '#{options.remote}' '#{options.branch}' > /dev/null 2>&1` - if $CHILD_STATUS.success? - puts 'OK' - else - error 'Could not push release commit' - end - end - - def changelog - @changelog ||= begin - print "Generating changelog..." - pwd = FileUtils.pwd - from = options.current_tag - to = options.next_tag - command = "docker run --rm --volume '#{pwd}:/worktree' changelog-rs '#{from}' '#{to}'" - changelog = `#{command}` - if $CHILD_STATUS.success? - puts 'OK' - changelog.rstrip.lines[1..].join - else - error 'Could not generate the changelog' - end - end - end - - def create_github_release - Tempfile.create do |f| - f.write changelog - f.close - - print "Creating GitHub release '#{options.tag}'..." - tag = options.tag - `gh release create #{tag} --title 'Release #{tag}' --notes-file '#{f.path}' --target #{options.default_branch}` - if $CHILD_STATUS.success? - puts 'OK' - else - error 'Could not create release' - end - end - end - - def create_release_pull_request - Tempfile.create do |f| - f.write <<~PR - ### Your checklist for this pull request - 🚨Please review the [guidelines for contributing](https://github.com/ruby-git/ruby-git/blob/#{options.default_branch}/CONTRIBUTING.md) to this repository. - - - [X] Ensure all commits include DCO sign-off. - - [X] Ensure that your contributions pass unit testing. - - [X] Ensure that your contributions contain documentation if applicable. - - ### Description - #{changelog} - PR - f.close - - print "Creating GitHub pull request..." - `gh pr create --title 'Release #{options.tag}' --body-file '#{f.path}' --base '#{options.default_branch}'` - if $CHILD_STATUS.success? - puts 'OK' - else - error 'Could not create release pull request' - end - end - end - - def error(message) - warn "ERROR: #{message}" - exit 1 - end - - def print(*args) - super unless options.quiet - end - - def puts(*args) - super unless options.quiet - end -end - -options = CommandLineParser.new.parse(ARGV) -ReleaseAssertions.new(options).make_assertions -ReleaseCreator.new(options).create_release From e056d64bd1adba60cf22c0297e106a30434b55c1 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 30 Mar 2024 14:11:18 -0700 Subject: [PATCH 05/81] Build with jruby-head on Windows until jruby/jruby#7515 is fixed Signed-off-by: James Couball --- .github/workflows/continuous_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index bc207a9e..a3d49058 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -32,7 +32,7 @@ jobs: operating-system: windows-latest - # Since JRuby on Windows is known to not work, consider this experimental - ruby: jruby-9.4.5.0 + ruby: jruby-head operating-system: windows-latest experimental: Yes From 705e98309cc1d8942fb5851145bdf7b4013d67f9 Mon Sep 17 00:00:00 2001 From: James Couball Date: Sat, 30 Mar 2024 14:36:22 -0700 Subject: [PATCH 06/81] Move experimental builds to a separate workflow that only runs when pushed to master Signed-off-by: James Couball --- .github/workflows/continuous_integration.yml | 10 ----- .../experimental_continuous_integration.yml | 43 +++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/experimental_continuous_integration.yml diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index a3d49058..52c6c4ea 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -22,20 +22,10 @@ jobs: operating-system: [ubuntu-latest] experimental: [No] include: - - # Building against head version of Ruby is considered experimental - ruby: head - operating-system: ubuntu-latest - experimental: Yes - - # Only test with minimal Ruby version on Windows ruby: 3.0 operating-system: windows-latest - - # Since JRuby on Windows is known to not work, consider this experimental - ruby: jruby-head - operating-system: windows-latest - experimental: Yes - steps: - name: Checkout Code uses: actions/checkout@v4 diff --git a/.github/workflows/experimental_continuous_integration.yml b/.github/workflows/experimental_continuous_integration.yml new file mode 100644 index 00000000..44dc7889 --- /dev/null +++ b/.github/workflows/experimental_continuous_integration.yml @@ -0,0 +1,43 @@ +name: CI Experimental + +on: + push: + branches: [master,v1] + workflow_dispatch: + +jobs: + build: + name: Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + continue-on-error: true + env: { JAVA_OPTS: -Djdk.io.File.enableADS=true } + + strategy: + fail-fast: false + matrix: + include: + - # Building against head version of Ruby is considered experimental + ruby: head + operating-system: ubuntu-latest + experimental: Yes + + - # Since JRuby on Windows is known to not work, consider this experimental + ruby: jruby-head + operating-system: windows-latest + experimental: Yes + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - name: Run Build + run: bundle exec rake default + + - name: Test Gem + run: bundle exec rake test:gem From 7e99b175eed5228f8253ea696c3c17383bff42c3 Mon Sep 17 00:00:00 2001 From: James Couball Date: Thu, 25 Apr 2024 15:52:26 -0700 Subject: [PATCH 07/81] Update documentation for new timeout functionality Signed-off-by: James Couball --- README.md | 34 +++++++++++++++++-------------- bin/command_line_test | 9 ++++++++- lib/git/command_line.rb | 18 ++++++++++++----- lib/git/lib.rb | 45 +++++++++++++++++++++++++++++------------ 4 files changed, 72 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 2715d2f6..ab1ce6bb 100644 --- a/README.md +++ b/README.md @@ -158,22 +158,25 @@ rescue Git::Error => e The timeout feature was added in git gem version `2.0.0`. -A timeout for git operations can be set either globally or for specific method calls -that accept a `:timeout` parameter. +A timeout for git command line operations can be set either globally or for specific +method calls that accept a `:timeout` parameter. The timeout value must be a real, non-negative `Numeric` value that specifies a number of seconds a `git` command will be given to complete before being sent a KILL signal. This library may hang if the `git` command does not terminate after receiving the KILL signal. -When a command times out, a `Git::TimeoutError` is raised. +When a command times out, it is killed by sending it the `SIGKILL` signal and a +`Git::TimeoutError` is raised. This error derives from the `Git::SignaledError` and +`Git::Error`. If the timeout value is `0` or `nil`, no timeout will be enforced. -If a method accepts a `:timeout` parameter and a receives a non-nil value, it will -override the global timeout value. In this context, a value of `nil` (which is -usually the default) will use the global timeout value and a value of `0` will turn -off timeout enforcement for that method call no matter what the global value is. +If a method accepts a `:timeout` parameter and a receives a non-nil value, the value +of this parameter will override the global timeout value. In this context, a value of +`nil` (which is usually the default) will use the global timeout value and a value of +`0` will turn off timeout enforcement for that method call no matter what the global +value is. To set a global timeout, use the `Git.config` object: @@ -193,19 +196,20 @@ Git.clone(repo_url, timeout: 0) # Do not enforce a timeout Git.clone(repo_url, timeout: 10.5) # Timeout after 10.5 seconds raising Git::SignaledError ``` -If the command takes too long, a `Git::SignaledError` will be raised: +If the command takes too long, a `Git::TimeoutError` will be raised: ```ruby begin Git.clone(repo_url, timeout: 10) rescue Git::TimeoutError => e - result = e.result - result.class #=> Git::CommandLineResult - result.status #=> # - result.status.timeout? #=> true - result.git_cmd # The git command ran as an array of strings - result.stdout # The command's output to stdout until it was terminated - result.stderr # The command's output to stderr until it was terminated + e.result.tap do |r| + r.class #=> Git::CommandLineResult + r.status #=> # + r.status.timeout? #=> true + r.git_cmd # The git command ran as an array of strings + r.stdout # The command's output to stdout until it was terminated + r.stderr # The command's output to stderr until it was terminated + end end ``` diff --git a/bin/command_line_test b/bin/command_line_test index 1827da2b..918e2024 100755 --- a/bin/command_line_test +++ b/bin/command_line_test @@ -12,6 +12,7 @@ require 'optparse' # --stderr: string to output to stderr # --exitstatus: exit status to return (default is zero) # --signal: uncaught signal to raise (default is not to signal) +# --duration: number of seconds to sleep before exiting (default is zero) # # Both --stdout and --stderr can be given. # @@ -31,7 +32,13 @@ require 'optparse' # $ bin/command_line_test --stdout="Hello, world!" --stderr="ERROR: timeout" # - +# The command line parser for this script +# +# @example +# parser = CommandLineParser.new +# options = parser.parse(['--exitstatus', '1', '--stderr', 'ERROR: timeout', '--duration', '5']) +# +# @api private class CommandLineParser def initialize @option_parser = OptionParser.new diff --git a/lib/git/command_line.rb b/lib/git/command_line.rb index ed81cba6..f52ff556 100644 --- a/lib/git/command_line.rb +++ b/lib/git/command_line.rb @@ -114,7 +114,6 @@ def initialize(env, binary_path, global_opts, logger) # the normalize option will be ignored. # # @example Run a command and return the output - # # cli.run('version') #=> "git version 2.39.1\n" # # @example The args array should be splatted into the parameter list @@ -162,14 +161,18 @@ def initialize(env, binary_path, global_opts, logger) # `stderr_writer.string` will be merged into the output returned by this method. # # @param normalize [Boolean] whether to normalize the output to a valid encoding + # # @param chomp [Boolean] whether to chomp the output + # # @param merge [Boolean] whether to merge stdout and stderr in the string returned + # # @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 or 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 timeout is zero, the timeout will not be enforced. + # + # 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. # @@ -178,9 +181,13 @@ def initialize(env, binary_path, global_opts, logger) # This result of running the command. # # @raise [ArgumentError] if `args` is not an array of strings + # # @raise [Git::SignaledError] if the command was terminated because of an uncaught signal + # # @raise [Git::FailedError] if the command returned a non-zero exitstatus + # # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output + # # @raise [Git::TimeoutError] if the command times out # def run(*args, out:, err:, normalize:, chomp:, merge:, chdir: nil, timeout: nil) @@ -267,7 +274,7 @@ def raise_pipe_error(git_cmd, pipe_name, pipe) # # @param cmd [Array] the git command to execute # @param chdir [String] the directory to run the command in - # @param timeout [Float, Integer, nil] the maximum seconds to wait for the command to complete + # @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. @@ -321,6 +328,7 @@ def writers(out, err) # @param err [#write] the object that stderr was written to # @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 # # @return [Git::CommandLineResult] the result of the command to return to the caller # @@ -346,7 +354,7 @@ def process_result(git_cmd, status, out, err, normalize, chomp, timeout) # @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 [Float, Integer, nil] the maximum seconds to wait for the command to complete + # @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. diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 28c32b63..9d4fbe5c 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -80,22 +80,34 @@ def init(opts={}) command('init', *arr_opts) end - # tries to clone the given repo + # Clones a repository into a newly created directory # - # accepts options: - # :bare:: no working directory - # :branch:: name of branch to track (rather than 'master') - # :depth:: the number of commits back to pull - # :filter:: specify partial clone - # :origin:: name of remote (same as remote) - # :path:: directory where the repo will be cloned - # :remote:: name of remote (rather than 'origin') - # :recursive:: after the clone is created, initialize all submodules within, using their default settings. + # @param [String] repository_url the URL of the repository to clone + # @param [String, nil] directory the directory to clone into + # + # If nil, the repository is cloned into a directory with the same name as + # the repository. + # + # @param [Hash] opts the options for this command # - # TODO - make this work with SSH password or auth_key + # @option opts [Boolean] :bare (false) if true, clone as a bare repository + # @option opts [String] :branch the branch to checkout + # @option opts [String, Array] :config one or more configuration options to set + # @option opts [Integer] :depth the number of commits back to pull + # @option opts [String] :filter specify partial clone + # @option opts [String] :mirror set up a mirror of the source repository + # @option opts [String] :origin the name of the remote + # @option opts [String] :path an optional prefix for the directory parameter + # @option opts [String] :remote the name of the remote + # @option opts [Boolean] :recursive after the clone is created, initialize all submodules within, using their default settings + # @option opts [Numeric, nil] :timeout the number of seconds to wait for the command to complete + # + # See {Git::Lib#command} for more information about :timeout # # @return [Hash] the options to pass to {Git::Base.new} # + # @todo make this work with SSH password or auth_key + # def clone(repository_url, directory, opts = {}) @path = opts[:path] || '.' clone_dir = opts[:path] ? File.join(@path, directory) : directory @@ -1215,8 +1227,15 @@ def command_line # # @param chdir [String, nil] the directory to run the command in # - # @param timeout [Numeric, nil] the maximum time to wait for the command to - # complete + # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete + # + # If timeout is nil, the global timeout from {Git::Config} is used. + # + # If timeout is zero, the timeout will not be enforced. + # + # 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. # # @see Git::CommandLine#run # From 7376d76d99f36e88ba09d71d6a107955f830ec97 Mon Sep 17 00:00:00 2001 From: James Couball Date: Mon, 6 May 2024 08:43:29 -0700 Subject: [PATCH 08/81] Refactor errors that are raised by this gem Signed-off-by: James Couball --- README.md | 52 +---- git.gemspec | 1 + lib/git.rb | 17 +- lib/git/command_line.rb | 15 +- lib/git/command_line_error.rb | 59 ----- lib/git/error.rb | 7 - lib/git/errors.rb | 206 ++++++++++++++++++ lib/git/failed_error.rb | 14 -- lib/git/git_execute_error.rb | 14 -- lib/git/lib.rb | 13 +- lib/git/object.rb | 136 ++++++------ lib/git/signaled_error.rb | 14 -- lib/git/timeout_error.rb | 60 ----- tests/units/test_command_line.rb | 16 +- tests/units/test_git_execute_error.rb | 7 - .../test_lib_repository_default_branch.rb | 2 +- tests/units/test_remotes.rb | 6 +- tests/units/test_tags.rb | 10 +- 18 files changed, 328 insertions(+), 321 deletions(-) delete mode 100644 lib/git/command_line_error.rb delete mode 100644 lib/git/error.rb create mode 100644 lib/git/errors.rb delete mode 100644 lib/git/failed_error.rb delete mode 100644 lib/git/git_execute_error.rb delete mode 100644 lib/git/signaled_error.rb delete mode 100644 lib/git/timeout_error.rb delete mode 100644 tests/units/test_git_execute_error.rb diff --git a/README.md b/README.md index ab1ce6bb..23efa669 100644 --- a/README.md +++ b/README.md @@ -104,56 +104,22 @@ Pass the `--all` option to `git log` as follows: ## Errors Raised By This Gem -This gem raises custom errors that derive from `Git::Error`. These errors are -arranged in the following class heirarchy: +The git gem will only raise an `ArgumentError` or an error that is a subclass of +`Git::Error`. It does not explicitly raise any other types of errors. -Error heirarchy: - -```text -Error -└── CommandLineError - β”œβ”€β”€ FailedError - └── SignaledError - └── TimeoutError -``` - -Other standard errors may also be raised like `ArgumentError`. Each method should -document the errors it may raise. - -Description of each Error class: - -* `Error`: This catch-all error serves as the base class for other custom errors in this - gem. Errors of this class are raised when no more approriate specific error to - raise. -* `CommandLineError`: This error is raised when there's a problem executing the git - command line. This gem will raise a more specific error depending on how the - command line failed. -* `FailedError`: This error is raised when the git command line exits with a non-zero - status code that is not expected by the git gem. -* `SignaledError`: This error is raised when the git command line is terminated as a - result of receiving a signal. This could happen if the process is forcibly - terminated or if there is a serious system error. -* `TimeoutError`: This is a specific type of `SignaledError` that is raised when the - git command line operation times out and is killed via the SIGKILL signal. This - happens if the operation takes longer than the timeout duration configured in - `Git.config.timeout` or via the `:timeout` parameter given in git methods that - support this parameter. - -`Git::GitExecuteError` remains as an alias for `Git::Error`. It is considered -deprecated as of git-2.0.0. - -Here is an example of catching errors when using the git gem: +It is recommended to rescue `Git::Error` to catch any runtime error raised by +this gem unless you need more specific error handling. ```ruby begin - timeout_duration = 0.001 # seconds - repo = Git.clone('https://github.com/ruby-git/ruby-git', 'ruby-git-temp', timeout: timeout_duration) -rescue Git::TimeoutError => e # Catch the more specific error first! - puts "Git clone took too long and timed out #{e}" + # some git operation rescue Git::Error => e - puts "Received the following error: #{e}" + puts "An error occurred: #{e.message}" +end ``` +See [`Git::Error`](https://rubydoc.info/gems/git/Git/Error) for more information. + ## Specifying And Handling Timeouts The timeout feature was added in git gem version `2.0.0`. diff --git a/git.gemspec b/git.gemspec index 8a2af4e4..14470c00 100644 --- a/git.gemspec +++ b/git.gemspec @@ -27,6 +27,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 3.0.0' s.requirements = ['git 2.28.0 or greater'] + 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' diff --git a/lib/git.rb b/lib/git.rb index 4b41a393..e995e96c 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -1,22 +1,21 @@ -# Add the directory containing this file to the start of the load path if it -# isn't there already. -$:.unshift(File.dirname(__FILE__)) unless - $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) +require 'active_support' +require 'active_support/deprecation' + +module Git + Deprecation = ActiveSupport::Deprecation.new('3.0', 'Git') +end require 'git/author' require 'git/base' require 'git/branch' require 'git/branches' -require 'git/command_line_error' require 'git/command_line_result' require 'git/command_line' require 'git/config' require 'git/diff' require 'git/encoding_utils' -require 'git/error' +require 'git/errors' require 'git/escaped_path' -require 'git/failed_error' -require 'git/git_execute_error' require 'git/index' require 'git/lib' require 'git/log' @@ -24,11 +23,9 @@ require 'git/path' require 'git/remote' require 'git/repository' -require 'git/signaled_error' require 'git/status' require 'git/stash' require 'git/stashes' -require 'git/timeout_error' require 'git/url' require 'git/version' require 'git/working_directory' diff --git a/lib/git/command_line.rb b/lib/git/command_line.rb index f52ff556..276cdc78 100644 --- a/lib/git/command_line.rb +++ b/lib/git/command_line.rb @@ -2,8 +2,7 @@ require 'git/base' require 'git/command_line_result' -require 'git/failed_error' -require 'git/signaled_error' +require 'git/errors' require 'stringio' module Git @@ -186,7 +185,7 @@ def initialize(env, binary_path, global_opts, logger) # # @raise [Git::FailedError] if the command returned a non-zero exitstatus # - # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output + # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output # # @raise [Git::TimeoutError] if the command times out # @@ -260,14 +259,14 @@ def post_process_all(writers, normalize, chomp) # @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::GitExecuteError] + # @raise [Git::ProcessIOError] # # @return [void] this method always raises an error # # @api private # def raise_pipe_error(git_cmd, pipe_name, pipe) - raise Git::GitExecuteError.new("Pipe Exception for #{git_cmd}: #{pipe_name}"), cause: pipe.exception + raise Git::ProcessIOError.new("Pipe Exception for #{git_cmd}: #{pipe_name}"), cause: pipe.exception end # Execute the git command and collect the output @@ -281,7 +280,7 @@ def raise_pipe_error(git_cmd, pipe_name, pipe) # # If the command does not respond to SIGKILL, it will hang this method. # - # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output + # @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 @@ -334,6 +333,8 @@ def writers(out, err) # # @raise [Git::FailedError] if the command failed # @raise [Git::SignaledError] if the command was signaled + # @raise [Git::TimeoutError] if the command times out + # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output # # @api private # @@ -361,7 +362,7 @@ def process_result(git_cmd, status, out, err, normalize, chomp, timeout) # # If the command does not respond to SIGKILL, it will hang this method. # - # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output + # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output # @raise [Git::TimeoutError] if the command times out # # @return [Git::CommandLineResult] the result of the command to return to the caller diff --git a/lib/git/command_line_error.rb b/lib/git/command_line_error.rb deleted file mode 100644 index 269ef3cd..00000000 --- a/lib/git/command_line_error.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require_relative 'error' - -module Git - # Raised when a git command fails or exits because of an uncaught signal - # - # The git command executed, status, stdout, and stderr are available from this - # object. - # - # Rather than creating a CommandLineError object directly, it is recommended to use - # one of the derived classes for the appropriate type of error: - # - # * {Git::FailedError}: when the git command exits with a non-zero status - # * {Git::SignaledError}: when the git command exits because of an uncaught signal - # * {Git::TimeoutError}: when the git command times out - # - # @api public - # - class CommandLineError < Git::Error - # Create a CommandLineError object - # - # @example - # `exit 1` # set $? appropriately for this example - # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr') - # error = Git::CommandLineError.new(result) - # error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' - # - # @param result [Git::CommandLineResult] the result of the git command including - # the git command, status, stdout, and stderr - # - def initialize(result) - @result = result - super() - end - - # The human readable representation of this error - # - # @example - # error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' - # - # @return [String] - # - def to_s = <<~MESSAGE.chomp - #{result.git_cmd}, status: #{result.status}, stderr: #{result.stderr.inspect} - MESSAGE - - # @attribute [r] result - # - # The result of the git command including the git command and its status and output - # - # @example - # error.result #=> # - # - # @return [Git::CommandLineResult] - # - attr_reader :result - end -end diff --git a/lib/git/error.rb b/lib/git/error.rb deleted file mode 100644 index 1b2e44be..00000000 --- a/lib/git/error.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module Git - # Base class for all custom git module errors - # - class Error < StandardError; end -end \ No newline at end of file diff --git a/lib/git/errors.rb b/lib/git/errors.rb new file mode 100644 index 00000000..900f858a --- /dev/null +++ b/lib/git/errors.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +module Git + # Base class for all custom git module errors + # + # The git gem will only raise an `ArgumentError` or an error that is a subclass of + # `Git::Error`. It does not explicitly raise any other types of errors. + # + # It is recommended to rescue `Git::Error` to catch any runtime error raised by + # this gem unless you need more specific error handling. + # + # Git's custom errors are arranged in the following class heirarchy: + # + # ```text + # StandardError + # └─> Git::Error + # β”œβ”€> Git::CommandLineError + # β”‚ β”œβ”€> Git::FailedError + # β”‚ └─> Git::SignaledError + # β”‚ └─> Git::TimeoutError + # β”œβ”€> Git::ProcessIOError + # └─> Git::UnexpectedResultError + # ``` + # + # | Error Class | Description | + # | --- | --- | + # | `Error` | This catch-all error serves as the base class for other custom errors raised by the git gem. | + # | `CommandLineError` | A subclass of this error is raised when there is a problem executing the git command line. | + # | `FailedError` | This error is raised when the git command line exits with a non-zero status code that is not expected by the git gem. | + # | `SignaledError` | This error is raised when the git command line is terminated as a result of receiving a signal. This could happen if the process is forcibly terminated or if there is a serious system error. | + # | `TimeoutError` | This is a specific type of `SignaledError` that is raised when the git command line operation times out and is killed via the SIGKILL signal. This happens if the operation takes longer than the timeout duration configured in `Git.config.timeout` or via the `:timeout` parameter given in git methods that support timeouts. | + # | `ProcessIOError` | An error was encountered reading or writing to a subprocess. | + # | `UnexpectedResultError` | The command line ran without error but did not return the expected results. | + # + # @example Rescuing a generic error + # begin + # # some git operation + # rescue Git::Error => e + # puts "An error occurred: #{e.message}" + # end + # + # @example Rescuing a timeout error + # begin + # timeout_duration = 0.001 # seconds + # repo = Git.clone('https://github.com/ruby-git/ruby-git', 'ruby-git-temp', timeout: timeout_duration) + # rescue Git::TimeoutError => e # Catch the more specific error first! + # puts "Git clone took too long and timed out #{e}" + # rescue Git::Error => e + # puts "Received the following error: #{e}" + # end + # + # @see Git::CommandLineError + # @see Git::FailedError + # @see Git::SignaledError + # @see Git::TimeoutError + # @see Git::ProcessIOError + # @see Git::UnexpectedResultError + # + # @api public + # + class Error < StandardError; end + + # An alias for Git::Error + # + # Git::GitExecuteError error class is an alias for Git::Error for backwards + # compatibility. It is recommended to use Git::Error directly. + # + # @deprecated Use Git::Error instead + # + GitExecuteError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Git::GitExecuteError', 'Git::Error', Git::Deprecation) + + # Raised when a git command fails or exits because of an uncaught signal + # + # The git command executed, status, stdout, and stderr are available from this + # object. + # + # The Gem will raise a more specific error for each type of failure: + # + # * {Git::FailedError}: when the git command exits with a non-zero status + # * {Git::SignaledError}: when the git command exits because of an uncaught signal + # * {Git::TimeoutError}: when the git command times out + # + # @api public + # + class CommandLineError < Git::Error + # Create a CommandLineError object + # + # @example + # `exit 1` # set $? appropriately for this example + # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr') + # error = Git::CommandLineError.new(result) + # error.to_s #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' + # + # @param result [Git::CommandLineResult] the result of the git command including + # the git command, status, stdout, and stderr + # + def initialize(result) + @result = result + super(error_message) + end + + # The human readable representation of this error + # + # @example + # error.error_message #=> '["git", "status"], status: pid 89784 exit 1, stderr: "stderr"' + # + # @return [String] + # + def error_message = <<~MESSAGE.chomp + #{result.git_cmd}, status: #{result.status}, stderr: #{result.stderr.inspect} + MESSAGE + + # @attribute [r] result + # + # The result of the git command including the git command and its status and output + # + # @example + # error.result #=> # + # + # @return [Git::CommandLineResult] + # + attr_reader :result + end + + # This error is raised when a git command returns a non-zero exitstatus + # + # The git command executed, status, stdout, and stderr are available from this + # object. + # + # @api public + # + class FailedError < Git::CommandLineError; end + + # This error is raised when a git command exits because of an uncaught signal + # + # @api public + # + class SignaledError < Git::CommandLineError; end + + # This error is raised when a git command takes longer than the configured timeout + # + # The git command executed, status, stdout, and stderr, and the timeout duration + # are available from this object. + # + # result.status.timeout? will be `true` + # + # @api public + # + class TimeoutError < Git::SignaledError + # Create a TimeoutError object + # + # @example + # command = %w[sleep 10] + # timeout_duration = 1 + # status = ProcessExecuter.spawn(*command, timeout: timeout_duration) + # result = Git::CommandLineResult.new(command, status, 'stdout', 'err output') + # error = Git::TimeoutError.new(result, timeout_duration) + # error.error_message #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s' + # + # @param result [Git::CommandLineResult] the result of the git command including + # the git command, status, stdout, and stderr + # + # @param timeout_duration [Numeric] the amount of time the subprocess was allowed + # to run before being killed + # + def initialize(result, timeout_duration) + @timeout_duration = timeout_duration + super(result) + end + + # The human readable representation of this error + # + # @example + # error.error_message #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s' + # + # @return [String] + # + def error_message = <<~MESSAGE.chomp + #{super}, timed out after #{timeout_duration}s + MESSAGE + + # The amount of time the subprocess was allowed to run before being killed + # + # @example + # `kill -9 $$` # set $? appropriately for this example + # result = Git::CommandLineResult.new(%w[git status], $?, '', "killed") + # error = Git::TimeoutError.new(result, 10) + # error.timeout_duration #=> 10 + # + # @return [Numeric] + # + attr_reader :timeout_duration + end + + # Raised when the output of a git command can not be read + # + # @api public + # + class ProcessIOError < Git::Error; end + + # Raised when the git command result was not as expected + # + # @api public + # + class UnexpectedResultError < Git::Error; end +end diff --git a/lib/git/failed_error.rb b/lib/git/failed_error.rb deleted file mode 100644 index 5c6e1f62..00000000 --- a/lib/git/failed_error.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require_relative 'command_line_error' - -module Git - # This error is raised when a git command returns a non-zero exitstatus - # - # The git command executed, status, stdout, and stderr are available from this - # object. - # - # @api public - # - class FailedError < Git::CommandLineError; end -end diff --git a/lib/git/git_execute_error.rb b/lib/git/git_execute_error.rb deleted file mode 100644 index 654dfc5b..00000000 --- a/lib/git/git_execute_error.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require_relative 'error' - -module Git - # This error is raised when a git command fails - # - # This error class is used as an alias for Git::Error for backwards compatibility. - # It is recommended to use Git::Error directly. - # - # @deprecated Use Git::Error instead - # - GitExecuteError = Git::Error -end \ No newline at end of file diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 9d4fbe5c..bfb1c66d 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1,5 +1,5 @@ -require 'git/failed_error' require 'git/command_line' +require 'git/errors' require 'logger' require 'pp' require 'process_executer' @@ -155,7 +155,7 @@ def repository_default_branch(repository) match_data = output.match(%r{^ref: refs/heads/(?[^\t]+)\tHEAD$}) return match_data[:default_branch] if match_data - raise 'Unable to determine the default branch' + raise Git::UnexpectedResultError, 'Unable to determine the default branch' end ## READ COMMANDS ## @@ -420,7 +420,7 @@ def change_head_branch(branch_name) def branches_all command_lines('branch', '-a').map do |line| match_data = line.match(BRANCH_LINE_REGEXP) - raise GitExecuteError, 'Unexpected branch line format' unless match_data + raise Git::UnexpectedResultError, 'Unexpected branch line format' unless match_data next nil if match_data[:not_a_branch] || match_data[:detached_ref] [ match_data[:refname], @@ -945,7 +945,7 @@ def tag(name, *opts) opts = opts.last.instance_of?(Hash) ? opts.last : {} if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message]) - raise "Can not create an [:a|:annotate] tag without the precense of [:m|:message]." + raise ArgumentError, 'Cannot create an annotated tag without a message.' end arr_opts = [] @@ -1242,7 +1242,10 @@ def command_line # @return [String] the command's stdout (or merged stdout and stderr if `merge` # is true) # - # @raise [Git::GitExecuteError] if the command fails + # @raise [Git::FailedError] if the command failed + # @raise [Git::SignaledError] if the command was signaled + # @raise [Git::TimeoutError] if the command times out + # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output # # The exception's `result` attribute is a {Git::CommandLineResult} which will # contain the result of the command including the exit status, stdout, and diff --git a/lib/git/object.rb b/lib/git/object.rb index 30258e92..1ffc1013 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -1,16 +1,18 @@ +require 'git/author' +require 'git/diff' +require 'git/errors' +require 'git/log' + module Git - - class GitTagNameDoesNotExist< StandardError - end - + # represents a git object class Object - + class AbstractObject attr_accessor :objectish, :type, :mode attr_writer :size - + def initialize(base, objectish) @base = base @objectish = objectish.to_s @@ -23,11 +25,11 @@ def initialize(base, objectish) def sha @sha ||= @base.lib.revparse(@objectish) end - + def size @size ||= @base.lib.object_size(@objectish) end - + # Get the object's contents. # If no block is given, the contents are cached in memory and returned as a string. # If a block is given, it yields an IO object (via IO::popen) which could be used to @@ -41,108 +43,108 @@ def contents(&block) @contents ||= @base.lib.object_contents(@objectish) end end - + def contents_array self.contents.split("\n") end - + def to_s @objectish end - + def grep(string, path_limiter = nil, opts = {}) opts = {:object => sha, :path_limiter => path_limiter}.merge(opts) @base.lib.grep(string, opts) end - + def diff(objectish) Git::Diff.new(@base, @objectish, objectish) end - + def log(count = 30) Git::Log.new(@base, count).object(@objectish) end - + # creates an archive of this object (tree) def archive(file = nil, opts = {}) @base.lib.archive(@objectish, file, opts) end - + def tree?; false; end - + def blob?; false; end - + def commit?; false; end def tag?; false; end - + end - - + + class Blob < AbstractObject - + def initialize(base, sha, mode = nil) super(base, sha) @mode = mode end - + def blob? true end end - + class Tree < AbstractObject - + def initialize(base, sha, mode = nil) super(base, sha) @mode = mode @trees = nil @blobs = nil end - + def children blobs.merge(subtrees) end - + def blobs @blobs ||= check_tree[:blobs] end alias_method :files, :blobs - + def trees @trees ||= check_tree[:trees] end alias_method :subtrees, :trees alias_method :subdirectories, :trees - + def full_tree @base.lib.full_tree(@objectish) end - + def depth @base.lib.tree_depth(@objectish) end - + def tree? true end - + private # actually run the git command def check_tree @trees = {} @blobs = {} - + data = @base.lib.ls_tree(@objectish) - data['tree'].each do |key, tree| - @trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode]) + data['tree'].each do |key, tree| + @trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode]) end - - data['blob'].each do |key, blob| - @blobs[key] = Git::Object::Blob.new(@base, blob[:sha], blob[:mode]) + + data['blob'].each do |key, blob| + @blobs[key] = Git::Object::Blob.new(@base, blob[:sha], blob[:mode]) end { @@ -150,11 +152,11 @@ def check_tree :blobs => @blobs } end - + end - + class Commit < AbstractObject - + def initialize(base, sha, init = nil) super(base, sha) @tree = nil @@ -166,48 +168,48 @@ def initialize(base, sha, init = nil) set_commit(init) end end - + def message check_commit @message end - + def name @base.lib.namerev(sha) end - + def gtree check_commit Tree.new(@base, @tree) end - + def parent parents.first end - + # array of all parent commits def parents check_commit - @parents + @parents end - + # git author - def author + def author check_commit @author end - + def author_date author.date end - + # git author def committer check_commit @committer end - - def committer_date + + def committer_date committer.date end alias_method :date, :committer_date @@ -215,7 +217,7 @@ def committer_date def diff_parent diff(parent) end - + def set_commit(data) @sha ||= data['sha'] @committer = Git::Author.new(data['committer']) @@ -224,26 +226,26 @@ def set_commit(data) @parents = data['parent'].map{ |sha| Git::Object::Commit.new(@base, sha) } @message = data['message'].chomp end - + def commit? true end private - + # see if this object has been initialized and do so if not def check_commit return if @tree - + data = @base.lib.commit_data(@objectish) set_commit(data) end - + end - + class Tag < AbstractObject attr_accessor :name - + def initialize(base, sha, name) super(base, sha) @name = name @@ -259,7 +261,7 @@ def message check_tag() return @message end - + def tag? true end @@ -274,7 +276,7 @@ def tagger def check_tag return if @loaded - if !self.annotated? + if !self.annotated? @message = @tagger = nil else tdata = @base.lib.tag_data(@name) @@ -284,29 +286,29 @@ def check_tag @loaded = true end - + end - + # if we're calling this, we don't know what type it is yet # so this is our little factory method def self.new(base, objectish, type = nil, is_tag = false) if is_tag sha = base.lib.tag_sha(objectish) if sha == '' - raise Git::GitTagNameDoesNotExist.new(objectish) + raise Git::UnexpectedResultError.new("Tag '#{objectish}' does not exist.") end return Git::Object::Tag.new(base, sha, objectish) end - + type ||= base.lib.object_type(objectish) klass = case type - when /blob/ then Blob + when /blob/ then Blob when /commit/ then Commit when /tree/ then Tree end klass.new(base, objectish) end - + end end diff --git a/lib/git/signaled_error.rb b/lib/git/signaled_error.rb deleted file mode 100644 index cb24ea30..00000000 --- a/lib/git/signaled_error.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require_relative 'command_line_error' - -module Git - # This error is raised when a git command exits because of an uncaught signal - # - # The git command executed, status, stdout, and stderr are available from this - # object. - # - # @api public - # - class SignaledError < Git::CommandLineError; end -end diff --git a/lib/git/timeout_error.rb b/lib/git/timeout_error.rb deleted file mode 100644 index ed482e73..00000000 --- a/lib/git/timeout_error.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require_relative 'signaled_error' - -module Git - # This error is raised when a git command takes longer than the configured timeout - # - # The git command executed, status, stdout, and stderr, and the timeout duration - # are available from this object. - # - # result.status.timeout? will be `true` - # - # @api public - # - class TimeoutError < Git::SignaledError - # Create a TimeoutError object - # - # @example - # command = %w[sleep 10] - # timeout_duration = 1 - # status = ProcessExecuter.spawn(*command, timeout: timeout_duration) - # result = Git::CommandLineResult.new(command, status, 'stdout', 'err output') - # error = Git::TimeoutError.new(result, timeout_duration) - # error.to_s #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s' - # - # @param result [Git::CommandLineResult] the result of the git command including - # the git command, status, stdout, and stderr - # - # @param timeout_duration [Numeric] the amount of time the subprocess was allowed - # to run before being killed - # - def initialize(result, timeout_duration) - @timeout_duration = timeout_duration - super(result) - end - - # The human readable representation of this error - # - # @example - # error.to_s #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s' - # - # @return [String] - # - def to_s = <<~MESSAGE.chomp - #{super}, timed out after #{timeout_duration}s - MESSAGE - - # The amount of time the subprocess was allowed to run before being killed - # - # @example - # `kill -9 $$` # set $? appropriately for this example - # result = Git::CommandLineResult.new(%w[git status], $?, '', "killed") - # error = Git::TimeoutError.new(result, 10) - # error.timeout_duration #=> 10 - # - # @return [Numeric] - # - attr_reader :timeout_duration - end -end diff --git a/tests/units/test_command_line.rb b/tests/units/test_command_line.rb index c03df542..eac144fb 100644 --- a/tests/units/test_command_line.rb +++ b/tests/units/test_command_line.rb @@ -77,11 +77,11 @@ def merge args = ['--duration=5'] # Git::TimeoutError (alone with Git::FailedError and Git::SignaledError) is a - # subclass of Git::GitExecuteError + # subclass of Git::Error begin command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge, timeout: 0.01) - rescue Git::GitExecuteError => e + rescue Git::Error => e assert_equal(true, e.result.status.timeout?) end end @@ -228,7 +228,7 @@ def command_line.spawn(cmd, out_writers, err_writers, chdir: nil, timeout: nil) end end - test "run should raise a GitExecuteError if there was an error raised writing stdout" do + test "run should raise a Git::ProcessIOError if there was an error raised writing stdout" do command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) args = ['--stdout=stdout output'] out_writer = Class.new do @@ -237,11 +237,11 @@ def write(*args) end end.new - error = assert_raise Git::GitExecuteError do + error = assert_raise Git::ProcessIOError do command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) end - assert_kind_of(Git::GitExecuteError, error) + assert_kind_of(Git::ProcessIOError, error) assert_kind_of(IOError, error.cause) assert_equal('error writing to file', error.cause.message) end @@ -257,7 +257,7 @@ def write(*args) end end - test "run should raise a GitExecuteError if there was an error raised writing stderr" do + test "run should raise a Git::ProcessIOError if there was an error raised writing stderr" do command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) args = ['--stderr=ERROR: fatal error'] err_writer = Class.new do @@ -266,11 +266,11 @@ def write(*args) end end.new - error = assert_raise Git::GitExecuteError do + error = assert_raise Git::ProcessIOError do command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) end - assert_kind_of(Git::GitExecuteError, error) + assert_kind_of(Git::ProcessIOError, error) assert_kind_of(IOError, error.cause) assert_equal('error writing to stderr file', error.cause.message) end diff --git a/tests/units/test_git_execute_error.rb b/tests/units/test_git_execute_error.rb deleted file mode 100644 index b675a3b3..00000000 --- a/tests/units/test_git_execute_error.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'test_helper' - -class TestGitExecuteError < Test::Unit::TestCase - def test_is_a_standard_error - assert(Git::GitExecuteError < StandardError) - end -end diff --git a/tests/units/test_lib_repository_default_branch.rb b/tests/units/test_lib_repository_default_branch.rb index dea8bf0f..0e012895 100644 --- a/tests/units/test_lib_repository_default_branch.rb +++ b/tests/units/test_lib_repository_default_branch.rb @@ -71,7 +71,7 @@ def test_repository_with_no_commits # Local or remote, the result is the same repository = '.' mock_command(@lib, repository, '') - assert_raise_with_message(RuntimeError, 'Unable to determine the default branch') do + assert_raise_with_message(Git::UnexpectedResultError, 'Unable to determine the default branch') do @lib.repository_default_branch(repository) end end diff --git a/tests/units/test_remotes.rb b/tests/units/test_remotes.rb index b134afbc..00c4c31b 100644 --- a/tests/units/test_remotes.rb +++ b/tests/units/test_remotes.rb @@ -152,7 +152,7 @@ def test_fetch_command_injection origin = "--upload-pack=touch #{test_file};" begin git.fetch(origin, { ref: 'some/ref/head' }) - rescue Git::GitExecuteError + rescue Git::Error # This is expected else raise 'Expected Git::FailedError to be raised' @@ -221,10 +221,12 @@ def test_push assert(rem.status['test-file1']) assert(!rem.status['test-file3']) - assert_raise Git::GitTagNameDoesNotExist do + error = assert_raise Git::UnexpectedResultError do rem.tag('test-tag') end + assert_equal error.message, "Tag 'test-tag' does not exist." + loc.push('testrem', 'testbranch', true) rem.checkout('testbranch') diff --git a/tests/units/test_tags.rb b/tests/units/test_tags.rb index 31745bf8..242af137 100644 --- a/tests/units/test_tags.rb +++ b/tests/units/test_tags.rb @@ -12,9 +12,10 @@ def test_tags r2.config('user.name', 'Test User') r2.config('user.email', 'test@email.com') - assert_raise Git::GitTagNameDoesNotExist do + error = assert_raise Git::UnexpectedResultError do r1.tag('first') end + assert_equal error.message, "Tag 'first' does not exist." r1.add_tag('first') r1.chdir do @@ -31,10 +32,12 @@ def test_tags assert(r2.tags.any?{|t| t.name == 'third'}) assert(r2.tags.none?{|t| t.name == 'second'}) - assert_raise RuntimeError do + error = assert_raises ArgumentError do r2.add_tag('fourth', {:a => true}) end + assert_equal(error.message, 'Cannot create an annotated tag without a message.') + r2.add_tag('fourth', {:a => true, :m => 'test message'}) assert(r2.tags.any?{|t| t.name == 'fourth'}) @@ -51,9 +54,10 @@ def test_tags r2.delete_tag('third') - assert_raise Git::GitTagNameDoesNotExist do + error = assert_raise Git::UnexpectedResultError do r2.tag('third') end + assert_equal error.message, "Tag 'third' does not exist." tag1 = r2.tag('fourth') assert_true(tag1.annotated?) From 8566929f25bdcc8ee208904fa6f10b49d750e47e Mon Sep 17 00:00:00 2001 From: James Couball Date: Tue, 7 May 2024 07:43:41 -0700 Subject: [PATCH 09/81] Add dependency on create_github_release gem used for releasing the git gem Signed-off-by: James Couball --- git.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/git.gemspec b/git.gemspec index 14470c00..63042f0a 100644 --- a/git.gemspec +++ b/git.gemspec @@ -32,6 +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 'minitar', '~> 0.9' s.add_development_dependency 'mocha', '~> 2.1' s.add_development_dependency 'rake', '~> 13.1' From 56783e7d2381a40f8e05136ec4fce345a8c1b246 Mon Sep 17 00:00:00 2001 From: James Couball Date: Fri, 10 May 2024 09:30:12 -0700 Subject: [PATCH 10/81] 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 11/81] 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 12/81] 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 13/81] 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 14/81] 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 15/81] 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 16/81] 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 17/81] 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 18/81] 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 19/81] 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 20/81] 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 21/81] 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 22/81] 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 23/81] 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 24/81] 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 25/81] 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 26/81] 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 27/81] 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 28/81] 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 29/81] 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 30/81] 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 31/81] 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 32/81] 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 33/81] 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 34/81] 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 35/81] 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 36/81] 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 37/81] 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 38/81] 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 39/81] 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 40/81] 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 41/81] 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 42/81] 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 43/81] 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 44/81] 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 45/81] 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 46/81] 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 47/81] 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 48/81] 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 49/81] 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 50/81] 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 51/81] 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 52/81] 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 53/81] 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 54/81] 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 55/81] 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 56/81] 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 57/81] 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 58/81] 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 59/81] 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 60/81] 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 61/81] 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 62/81] 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 63/81] 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 64/81] 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 65/81] 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 66/81] 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 67/81] 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 68/81] 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 69/81] 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 70/81] 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 71/81] 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 72/81] 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 73/81] 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 74/81] 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 75/81] 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 76/81] 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 77/81] 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 78/81] 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 79/81] 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 80/81] 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 81/81] 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