diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 302c5eed..3a2cd0df 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -2,35 +2,39 @@ name: CI on: push: - branches: [master] + branches: [master,v1] pull_request: - branches: [master] + branches: [master,v1] workflow_dispatch: jobs: - continuous_integration_build: - continue-on-error: true + build: + name: Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }} + runs-on: ${{ matrix.operating-system }} + continue-on-error: ${{ matrix.experimental == 'Yes' }} + env: { JAVA_OPTS: -Djdk.io.File.enableADS=true } + strategy: fail-fast: false matrix: - ruby: [2.7, 3.0, 3.1, 3.2] + # Only the latest versions of JRuby and TruffleRuby are tested + ruby: ["3.0", "3.1", "3.2", "3.3", "truffleruby-23.1.1", "jruby-9.4.5.0"] operating-system: [ubuntu-latest] + experimental: [No] include: - - ruby: head + - # Building against head version of Ruby is considered experimental + ruby: head operating-system: ubuntu-latest - - ruby: truffleruby-head - operating-system: ubuntu-latest - - ruby: 2.7 - operating-system: windows-latest - - ruby: jruby-head - operating-system: windows-latest + experimental: Yes - name: Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }} - - runs-on: ${{ matrix.operating-system }} + - # Only test with minimal Ruby version on Windows + ruby: 3.0 + operating-system: windows-latest - env: - JAVA_OPTS: -Djdk.io.File.enableADS=true + - # Since JRuby on Windows is known to not work, consider this experimental + ruby: jruby-9.4.5.0 + operating-system: windows-latest + experimental: Yes steps: - name: Checkout Code diff --git a/CHANGELOG.md b/CHANGELOG.md index bb147268..eb37889d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ # Change Log +## v2.0.0.pre1 (2024-01-15) + +[Full Changelog](https://github.com/ruby-git/ruby-git/compare/v1.19.1..v2.0.0.pre1) + +Changes since v1.19.1: + +* 7585c39 Change how the git CLI subprocess is executed (#684) +* f93e042 Update instructions for releasing a new version of the git gem (#686) +* f48930d Update minimum required version of Ruby and Git (#685) + ## v1.19.1 (2024-01-13) [Full Changelog](https://github.com/ruby-git/ruby-git/compare/v1.19.0..v1.19.1) diff --git a/README.md b/README.md index 5597228d..f0c42db7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,14 @@ 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. +Get started by obtaining a repository object by: + +* opening an existing working copy with [Git.open](https://rubydoc.info/gems/git/Git#open-class_method) +* initializing a new repository with [Git.init](https://rubydoc.info/gems/git/Git#init-class_method) +* cloning a repository with [Git.clone](https://rubydoc.info/gems/git/Git#clone-class_method) + +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 git 2.0.0 is available as a pre-release version for testing! Please give it a try. @@ -32,7 +40,7 @@ The changes coming in this major release include: * 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 #617](https://github.com/ruby-git/ruby-git/pull/617) for more details + 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 @@ -41,36 +49,19 @@ 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. -## Homepage - -The project source code is at: - -http://github.com/ruby-git/ruby-git - -## Documentation - -Detailed documentation can be found at: - -https://rubydoc.info/gems/git/Git.html - -Get started by obtaining a repository object by: - -* opening an existing working copy with [Git.open](https://rubydoc.info/gems/git/Git#open-class_method) -* initializing a new repository with [Git.init](https://rubydoc.info/gems/git/Git#init-class_method) -* cloning a repository with [Git.clone](https://rubydoc.info/gems/git/Git#clone-class_method) - -Methods that can be called on a repository object are documented in [Git::Base](https://rubydoc.info/gems/git/Git/Base) - ## Install -You can install Ruby/Git like this: +Install the gem and add to the application's Gemfile by executing: -``` -sudo gem install git +```shell +bundle add git ``` -## Code Status +If bundler is not being used to manage dependencies, install the gem by executing: +```shell +gem install git +``` ## Major Objects @@ -103,12 +94,6 @@ Pass the `--all` option to `git log` as follows: Here are a bunch of examples of how to use the Ruby/Git package. -Ruby < 1.9 will require rubygems to be loaded. - -```ruby -require 'rubygems' -``` - Require the 'git' gem. ```ruby require 'git' @@ -422,6 +407,14 @@ g.with_temp_working(dir) do end ``` +## Ruby version support policy + +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 Truffle Ruby on Linus + ## License licensed under MIT License Copyright (c) 2008 Scott Chacon. See LICENSE for further details. diff --git a/RELEASING.md b/RELEASING.md index 04e11984..ead6293a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -7,64 +7,79 @@ Releasing a new version of the `git` gem requires these steps: -- [How to release a new git.gem](#how-to-release-a-new-gitgem) - - [Install Prerequisites](#install-prerequisites) - - [Prepare the Release](#prepare-the-release) - - [Review and Merge the Release](#review-and-merge-the-release) - - [Build and Release the Gem](#build-and-release-the-gem) - -These instructions use an example where: - -- The default branch is `master` -- The current release version is `1.5.0` -- You want to create a new *minor* release, `1.6.0` +* [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: -- [git](https://git-scm.com) is used to interact with the local and remote repositories -- [gh](https://cli.github.com) is used to create the release and PR in GitHub -- [Docker](https://www.docker.com) is used to run the script to create the release notes +* [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 [brew](https://brew.sh): +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 ... -$ brew install --cask docker -... $ ``` -## Prepare the Release +## Determine the SemVer release type -Bump the version, create release notes, tag the release and create a GitHub release and PR which can be used to review the release. +Determine the SemVer version increment that should be applied for the new release: -Steps: +* `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. -- Check out the code with `git clone https://github.com/ruby-git/ruby-git ruby-git-v1.6.0 && cd ruby-git-v1.6.0` -- Install development dependencies using bundle `bundle install` -- Based upon the nature of the changes, decide on the type of release: `major`, `minor`, or `patch` (in this example we will use `minor`) -- Run the release script `bundle exec create-github-release minor` +## Create the release -## Review and Merge the Release +Create the release using the `create-github-release` command. If the release type +is `major`, the command is: -Have the release PR approved and merge the changes into the `master` branch. +```shell +create-github-release major +``` -**IMPORTANT** DO NOT merge to the `master` branch using the GitHub UI. Instead use the instructions below. +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. -Steps: +## Review the CHANGELOG and release PR -- Get the release PR reviewed and approved in GitHub -- Merge the changes with the command `git checkout master && git merge --ff-only v1.6.0 && git push` +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. -## Build and Release the Gem +## Manually merge the release PR -Build the gem and publish it to [rubygems.org](https://rubygems.org/gems/git) +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: -Steps: +```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 -- Build and release the gem using rake `bundle exec rake release` +Finally, publish the git gem to RubyGems.org using the following command: + +```shell +rake release:rubygem_push +``` diff --git a/bin/command_line_test b/bin/command_line_test new file mode 100755 index 00000000..a88893a2 --- /dev/null +++ b/bin/command_line_test @@ -0,0 +1,180 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'optparse' + +# A script used to test calling a command line program from Ruby +# +# This script is used to test the `Git::CommandLine` class. It is called +# from the `test_command_line` unit test. +# +# --stdout: string to output to stdout +# --stderr: string to output to stderr +# --exitstatus: exit status to return (default is zero) +# --signal: uncaught signal to raise (default is not to signal) +# +# Both --stdout and --stderr can be given. +# +# If --signal is given, --exitstatus is ignored. +# +# Examples: +# Output "Hello, world!" to stdout and exit with status 0 +# $ bin/command_line_test --stdout="Hello, world!" --exitstatus=0 +# +# Output "ERROR: timeout" to stderr and exit with status 1 +# $ bin/command_line_test --stderr="ERROR: timeout" --exitstatus=1 +# +# Output "Fatal: killed by parent" to stderr and signal 9 +# $ bin/command_line_test --stderr="Fatal: killed by parent" --signal=9 +# +# Output to both stdout and stderr return default exitstatus 0 +# $ bin/command_line_test --stdout="Hello, world!" --stderr="ERROR: timeout" +# + + +class CommandLineParser + def initialize + @option_parser = OptionParser.new + define_options + end + + attr_reader :stdout, :stderr, :exitstatus, :signal + + # Parse the command line arguements returning the options + # + # @example + # parser = CommandLineParser.new + # options = parser.parse(['major']) + # + # @param args [Array] the command line arguments + # + # @return [CreateGithubRelease::Options] the options + # + def parse(*args) + begin + option_parser.parse!(remaining_args = args.dup) + rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e + report_errors(e.message) + end + parse_remaining_args(remaining_args) + # puts options unless options.quiet + # report_errors(*options.errors) unless options.valid? + self + end + + private + + # @!attribute [rw] option_parser + # + # The option parser + # + # @return [OptionParser] the option parser + # + # @api private + # + attr_reader :option_parser + + def define_options + option_parser.banner = "Usage:\n#{command_template}" + option_parser.separator '' + option_parser.separator "Both --stdout and --stderr can be given." + option_parser.separator 'If --signal is given, --exitstatus is ignored.' + option_parser.separator 'If nothing is given, the script will exit with exitstatus 0.' + option_parser.separator '' + option_parser.separator 'Options:' + %i[ + define_help_option define_stdout_option define_stderr_option + define_exitstatus_option define_signal_option + ].each { |m| send(m) } + end + + # The command line template as a string + # @return [String] + # @api private + def command_template + <<~COMMAND + #{File.basename($PROGRAM_NAME)} \ + --help | \ + [--stdout="string to stdout"] [--stderr="string to stderr"] [--exitstatus=1] [--signal=9] + COMMAND + end + + # Define the stdout option + # @return [void] + # @api private + def define_stdout_option + option_parser.on('--stdout="string to stdout"', 'A string to send to stdout') do |string| + @stdout = string + end + end + + # Define the stderr option + # @return [void] + # @api private + def define_stderr_option + option_parser.on('--stderr="string to stderr"', 'A string to send to stderr') do |string| + @stderr = string + end + end + + # Define the exitstatus option + # @return [void] + # @api private + def define_exitstatus_option + option_parser.on('--exitstatus=1', 'The exitstatus to return') do |exitstatus| + @exitstatus = Integer(exitstatus) + end + end + + # Define the signal option + # @return [void] + # @api private + def define_signal_option + option_parser.on('--signal=9', 'The signal to raise') do |signal| + @signal = Integer(signal) + end + end + + # Define the help option + # @return [void] + # @api private + def define_help_option + option_parser.on_tail('-h', '--help', 'Show this message') do + puts option_parser + exit 0 + end + end + + # An error message constructed from the given errors array + # @return [String] + # @api private + def error_message(errors) + <<~MESSAGE + #{errors.map { |e| "ERROR: #{e}" }.join("\n")} + + Use --help for usage + MESSAGE + end + + # Output an error message and useage to stderr and exit + # @return [void] + # @api private + def report_errors(*errors) + warn error_message(errors) + exit 1 + end + + # Parse non-option arguments (there are none for this parser) + # @return [void] + # @api private + def parse_remaining_args(remaining_args) + report_errors('Too many args') unless remaining_args.empty? + end +end + +options = CommandLineParser.new.parse(*ARGV) + +STDOUT.puts options.stdout if options.stdout +STDERR.puts options.stderr if options.stderr +Process.kill(options.signal, Process.pid) if options.signal +exit(options.exitstatus) if options.exitstatus diff --git a/git.gemspec b/git.gemspec index daff7915..5ba540c0 100644 --- a/git.gemspec +++ b/git.gemspec @@ -24,22 +24,20 @@ Gem::Specification.new do |s| s.metadata['documentation_uri'] = "https://rubydoc.info/gems/#{s.name}/#{s.version}" s.require_paths = ['lib'] - s.required_ruby_version = '>= 2.3' - s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=) - s.requirements = ['git 1.6.0.0, or greater'] + s.required_ruby_version = '>= 3.0.0' + s.requirements = ['git 2.28.0 or greater'] s.add_runtime_dependency 'addressable', '~> 2.8' + s.add_runtime_dependency 'process_executer', '~> 0.7' s.add_runtime_dependency 'rchardet', '~> 1.8' - s.add_development_dependency 'bump', '~> 0.10' - s.add_development_dependency 'create_github_release', '~> 0.2' s.add_development_dependency 'minitar', '~> 0.9' s.add_development_dependency 'mocha', '~> 2.1' - s.add_development_dependency 'rake', '~> 13.0' - s.add_development_dependency 'test-unit', '~> 3.3' + s.add_development_dependency 'rake', '~> 13.1' + s.add_development_dependency 'test-unit', '~> 3.6' unless RUBY_PLATFORM == 'java' - s.add_development_dependency 'redcarpet', '~> 3.5' + s.add_development_dependency 'redcarpet', '~> 3.6' s.add_development_dependency 'yard', '~> 0.9', '>= 0.9.28' s.add_development_dependency 'yardstick', '~> 0.9' end diff --git a/lib/git.rb b/lib/git.rb index e75ff189..f4825206 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -8,6 +8,7 @@ require 'git/branch' require 'git/branches' require 'git/command_line_result' +require 'git/command_line' require 'git/config' require 'git/diff' require 'git/encoding_utils' @@ -23,6 +24,7 @@ require 'git/repository' require 'git/signaled_error' require 'git/status' +require 'git/signaled_error' require 'git/stash' require 'git/stashes' require 'git/url' diff --git a/lib/git/command_line.rb b/lib/git/command_line.rb new file mode 100644 index 00000000..3001c55d --- /dev/null +++ b/lib/git/command_line.rb @@ -0,0 +1,342 @@ +# frozen_string_literal: true + +require 'git/base' +require 'git/command_line_result' +require 'git/failed_error' +require 'git/signaled_error' +require 'stringio' + +module Git + # Runs a git command and returns the result + # + # @api public + # + class CommandLine + # Create a Git::CommandLine object + # + # @example + # env = { 'GIT_DIR' => '/path/to/git/dir' } + # binary_path = '/usr/bin/git' + # global_opts = %w[--git-dir /path/to/git/dir] + # logger = Logger.new(STDOUT) + # cli = CommandLine.new(env, binary_path, global_opts, logger) + # cli.run('version') #=> #] environment variables to set + # @param global_opts [Array] global options to pass to git + # @param logger [Logger] the logger to use + # + def initialize(env, binary_path, global_opts, logger) + @env = env + @binary_path = binary_path + @global_opts = global_opts + @logger = logger + end + + # @attribute [r] env + # + # Variables to set (or unset) in the git command's environment + # + # @example + # env = { 'GIT_DIR' => '/path/to/git/dir' } + # command_line = Git::CommandLine.new(env, '/usr/bin/git', [], Logger.new(STDOUT)) + # command_line.env #=> { 'GIT_DIR' => '/path/to/git/dir' } + # + # @return [Hash] + # + # @see https://ruby-doc.org/3.2.1/Process.html#method-c-spawn Process.spawn + # for details on how to set environment variables using the `env` parameter + # + attr_reader :env + + # @attribute [r] binary_path + # + # The path to the command line binary to run + # + # @example + # binary_path = '/usr/bin/git' + # command_line = Git::CommandLine.new({}, binary_path, ['version'], Logger.new(STDOUT)) + # command_line.binary_path #=> '/usr/bin/git' + # + # @return [String] + # + attr_reader :binary_path + + # @attribute [r] global_opts + # + # The global options to pass to git + # + # These are options that are passed to git before the command name and + # arguments. For example, in `git --git-dir /path/to/git/dir version`, the + # global options are %w[--git-dir /path/to/git/dir]. + # + # @example + # env = {} + # global_opts = %w[--git-dir /path/to/git/dir] + # logger = Logger.new(nil) + # cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger) + # cli.global_opts #=> %w[--git-dir /path/to/git/dir] + # + # @return [Array] + # + attr_reader :global_opts + + # @attribute [r] logger + # + # The logger to use for logging git commands and results + # + # @example + # env = {} + # global_opts = %w[] + # logger = Logger.new(STDOUT) + # cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger) + # cli.logger == logger #=> true + # + # @return [Logger] + # + attr_reader :logger + + # Execute a git command, wait for it to finish, and return the result + # + # NORMALIZATION + # + # The command output is returned as a Unicde string containing the binary output + # from the command. If the binary output is not valid UTF-8, the output will + # cause problems because the encoding will be invalid. + # + # Normalization is a process that trys to convert the binary output to a valid + # UTF-8 string. It uses the `rchardet` gem to detect the encoding of the binary + # output and then converts it to UTF-8. + # + # Normalization is not enabled by default. Pass `normalize: true` to Git::CommandLine#run + # to enable it. Normalization will only be performed on stdout and only if the `out:`` option + # is nil or is a StringIO object. If the out: option is set to a file or other IO object, + # 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 + # args = %w[log -n 1 --oneline] + # cli.run(*args) #=> "f5baa11 beginning of Ruby/Git project\n" + # + # @example Run a command and return the chomped output + # cli.run('version', chomp: true) #=> "git version 2.39.1" + # + # @example Run a command and without normalizing the output + # cli.run('version', normalize: false) #=> "git version 2.39.1\n" + # + # @example Capture stdout in a temporary file + # require 'tempfile' + # tempfile = Tempfile.create('git') do |file| + # cli.run('version', out: file) + # file.rewind + # file.read #=> "git version 2.39.1\n" + # end + # + # @example Capture stderr in a StringIO object + # require 'stringio' + # stderr = StringIO.new + # begin + # cli.run('log', 'nonexistent-branch', err: stderr) + # rescue Git::FailedError => e + # stderr.string #=> "unknown revision or path not in the working tree.\n" + # end + # + # @param args [Array] the command line arguements to pass to git + # + # This array should be splatted into the parameter list. + # + # @param out [#write, nil] the object to write stdout to or nil to ignore stdout + # + # If this is a 'StringIO' object, then `stdout_writer.string` will be returned. + # + # In general, only specify a `stdout_writer` object when you want to redirect + # stdout to a file or some other object that responds to `#write`. The default + # behavior will return the output of the command. + # + # @param err [#write] the object to write stderr to or nil to ignore stderr + # + # If this is a 'StringIO' object and `merged_output` is `true`, then + # `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 + # + # @return [Git::CommandLineResult] the output of the command + # + # 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 + # + def run(*args, out:, err:, normalize:, chomp:, merge:, chdir: 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)) + + process_result(git_cmd, status, out, err, normalize, chomp) + end + + private + + # Build the git command line from the available sources to send to `Process.spawn` + # @return [Array] + # @api private + # + def build_git_cmd(args) + raise ArgumentError.new('The args array can not contain an array') if args.any? { |a| a.is_a?(Array) } + + [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::GitExecuteError] + # + # @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 + 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 + # + # @raise [Git::GitExecuteError] if an exception was raised while collecting subprocess output + # + # @return [Process::Status] the status of the completed subprocess + # + # @api private + # + def spawn(cmd, out_writers, err_writers, chdir:) + 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) + 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 normalize [Boolean] whether to normalize the output of each writer + # @param chomp [Boolean] whether to chomp the output of each writer + # + # @return [Git::CommandLineResult] the result of the command to return to the caller + # + # @raise [Git::FailedError] if the command failed + # @raise [Git::SignaledError] if the command was signaled + # + # @api private + # + def process_result(git_cmd, status, out, err, normalize, chomp) + 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::SignaledError.new(result) if status.signaled? + raise Git::FailedError.new(result) unless status.success? + end + end + + # Execute the git command and write the command output to out and err + # + # @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 + # + # @return [Git::CommandLineResult] the result of the command to return to the caller + # + # @api private + # + def execute(git_cmd, out, err, chdir:) + out_writers, err_writers = writers(out, err) + spawn(git_cmd, out_writers, err_writers, chdir: chdir) + end + end +end diff --git a/lib/git/failed_error.rb b/lib/git/failed_error.rb index 27aa6ed9..75973f6f 100644 --- a/lib/git/failed_error.rb +++ b/lib/git/failed_error.rb @@ -14,20 +14,18 @@ module Git class FailedError < Git::GitExecuteError # Create a FailedError object # - # Since this gem redirects stderr to stdout, the stdout of the process is used. - # # @example # `exit 1` # set $? appropriately for this example # result = Git::CommandLineResult.new(%w[git status], $?, 'stdout', 'stderr') # error = Git::FailedError.new(result) # error.message #=> - # "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\noutput: \"stdout\"" + # "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\nstderr: \"stderr\"" # # @param result [Git::CommandLineResult] the result of the git command including # the git command, status, stdout, and stderr # def initialize(result) - super("#{result.git_cmd}\nstatus: #{result.status}\noutput: #{result.stdout.inspect}") + super("#{result.git_cmd}\nstatus: #{result.status}\nstderr: #{result.stderr.inspect}") @result = result end diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 06f3a2a1..9a6be282 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -1,14 +1,15 @@ require 'git/failed_error' +require 'git/command_line' require 'logger' +require 'pp' +require 'process_executer' +require 'stringio' require 'tempfile' require 'zlib' require 'open3' module Git class Lib - - @@semaphore = Mutex.new - # The path to the Git working copy. The default is '"./.git"'. # # @return [Pathname] the path to the Git working copy. @@ -337,7 +338,19 @@ def process_commit_log_data(data) end def object_contents(sha, &block) - command('cat-file', '-p', 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) @@ -474,11 +487,15 @@ def grep(string, opts = {}) grep_opts.push('--', *opts[:path_limiter]) if opts[:path_limiter].is_a?(Array) hsh = {} - command_lines('grep', *grep_opts).each do |line| - if m = /(.*?)\:(\d+)\:(.*)/.match(line) - hsh[m[1]] ||= [] - hsh[m[1]] << [m[2].to_i, m[3]] + begin + command_lines('grep', *grep_opts).each do |line| + if m = /(.*?)\:(\d+)\:(.*)/.match(line) + hsh[m[1]] ||= [] + hsh[m[1]] << [m[2].to_i, m[3]] + end end + rescue Git::FailedError => e + raise unless e.result.status.exitstatus == 1 && e.result.stderr == '' end hsh end @@ -865,16 +882,17 @@ def unmerged def conflicts # :yields: file, your, their self.unmerged.each do |f| - your_tempfile = Tempfile.new("YOUR-#{File.basename(f)}") - your = your_tempfile.path - your_tempfile.close # free up file for git command process - command('show', ":2:#{f}", redirect: "> #{escape your}") - - their_tempfile = Tempfile.new("THEIR-#{File.basename(f)}") - their = their_tempfile.path - their_tempfile.close # free up file for git command process - command('show', ":3:#{f}", redirect: "> #{escape their}") - yield(f, your, their) + Tempfile.create("YOUR-#{File.basename(f)}") do |your| + command('show', ":2:#{f}", out: your) + your.close + + Tempfile.create("THEIR-#{File.basename(f)}") do |their| + command('show', ":3:#{f}", out: their) + their.close + + yield(f, your.path, their.path) + end + end end end @@ -948,7 +966,7 @@ def fetch(remote, opts) arr_opts << remote if remote arr_opts << opts[:ref] if opts[:ref] - command('fetch', *arr_opts) + command('fetch', *arr_opts, merge: true) end def push(remote = nil, branch = nil, opts = nil) @@ -1001,7 +1019,13 @@ def tag_sha(tag_name) head = File.join(@git_dir, 'refs', 'tags', tag_name) return File.read(head).chomp if File.exist?(head) - command('show-ref', '--tags', '-s', tag_name) + begin + command('show-ref', '--tags', '-s', tag_name) + rescue Git::FailedError => e + raise unless e.result.status.exitstatus == 1 && e.result.stderr == '' + + '' + end end def repack @@ -1026,15 +1050,12 @@ def write_tree def commit_tree(tree, opts = {}) opts[:message] ||= "commit tree #{tree}" - t = Tempfile.new('commit-message') - t.write(opts[:message]) - t.close - arr_opts = [] arr_opts << tree arr_opts << '-p' << opts[:parent] if opts[:parent] - arr_opts += Array(opts[:parents]).map { |p| ['-p', p] }.flatten if opts[:parents] - command('commit-tree', *arr_opts, redirect: "< #{escape t.path}") + Array(opts[:parents]).each { |p| arr_opts << '-p' << p } if opts[:parents] + arr_opts << '-m' << opts[:message] + command('commit-tree', *arr_opts) end def update_ref(ref, commit) @@ -1080,7 +1101,11 @@ def archive(sha, file = nil, opts = {}) arr_opts << "--remote=#{opts[:remote]}" if opts[:remote] arr_opts << sha arr_opts << '--' << opts[:path] if opts[:path] - command('archive', *arr_opts, redirect: " > #{escape file}") + + f = File.open(file, 'wb') + command('archive', *arr_opts, out: f) + f.close + if opts[:add_gzip] file_content = File.read(file) Zlib::GzipWriter.open(file) do |gz| @@ -1115,7 +1140,7 @@ def compare_version_to(*other_version) end def required_command_version - [1, 6] + [2, 28] end def meets_required_version? @@ -1133,11 +1158,6 @@ def self.warn_if_old_command(lib) private - # Systen ENV variables involved in the git commands. - # - # @return [] the names of the EVN variables involved in the git commands - ENV_VARIABLE_NAMES = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_SSH'] - def command_lines(cmd, *opts, chdir: nil) cmd_op = command(cmd, *opts, chdir: chdir) if cmd_op.encoding.name != "UTF-8" @@ -1148,84 +1168,32 @@ def command_lines(cmd, *opts, chdir: nil) op.split("\n") end - # Takes the current git's system ENV variables and store them. - def store_git_system_env_variables - @git_system_env_variables = {} - ENV_VARIABLE_NAMES.each do |env_variable_name| - @git_system_env_variables[env_variable_name] = ENV[env_variable_name] - end + 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 + } end - # Takes the previously stored git's ENV variables and set them again on ENV. - def restore_git_system_env_variables - ENV_VARIABLE_NAMES.each do |env_variable_name| - ENV[env_variable_name] = @git_system_env_variables[env_variable_name] + def global_opts + Array.new.tap do |global_opts| + global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil? + global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil? + global_opts << '-c' << 'core.quotePath=true' + global_opts << '-c' << 'color.ui=false' end end - # Sets git's ENV variables to the custom values for the current instance. - def set_custom_git_env_variables - ENV['GIT_DIR'] = @git_dir - ENV['GIT_WORK_TREE'] = @git_work_dir - ENV['GIT_INDEX_FILE'] = @git_index_file - ENV['GIT_SSH'] = Git::Base.config.git_ssh + def command_line + @command_line ||= + Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger) end - # Runs a block inside an environment with customized ENV variables. - # It restores the ENV after execution. - # - # @param [Proc] block block to be executed within the customized environment - def with_custom_env_variables(&block) - @@semaphore.synchronize do - store_git_system_env_variables() - set_custom_git_env_variables() - return block.call() - end - ensure - restore_git_system_env_variables() - end - - def command(*cmd, redirect: '', chomp: true, chdir: nil, &block) - Git::Lib.warn_if_old_command(self) - - raise 'cmd can not include a nested array' if cmd.any? { |o| o.is_a? Array } - - global_opts = [] - global_opts << "--git-dir=#{@git_dir}" if !@git_dir.nil? - global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil? - global_opts << '-c' << 'core.quotePath=true' - global_opts << '-c' << 'color.ui=false' - - escaped_cmd = cmd.map { |part| escape(part) }.join(' ') - - global_opts = global_opts.map { |s| escape(s) }.join(' ') - - git_cmd = "#{Git::Base.config.binary_path} #{global_opts} #{escaped_cmd} #{redirect} 2>&1" - - output = nil - - command_thread = nil; - - status = nil - - with_custom_env_variables do - command_thread = Thread.new do - output, status = run_command(git_cmd, chdir, &block) - end - command_thread.join - end - - @logger.info(git_cmd) - @logger.debug(output) - - if status.exitstatus > 1 || (status.exitstatus == 1 && output != '') - result = Git::CommandLineResult.new(git_cmd, status, output, '') - raise Git::FailedError.new(result) - end - - output.chomp! if output && chomp && !block_given? - - output + def command(*args, out: nil, err: nil, normalize: true, chomp: true, merge: false, chdir: nil) + result = command_line.run(*args, out: out, err: err, normalize: normalize, chomp: chomp, merge: merge, chdir: chdir) + result.stdout end # Takes the diff command line output (as Array) and parse it into a Hash @@ -1291,38 +1259,5 @@ def log_path_options(opts) end arr_opts end - - def run_command(git_cmd, chdir=nil, &block) - block ||= Proc.new do |io| - io.readlines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join - end - - opts = {} - opts[:chdir] = File.expand_path(chdir) if chdir - - Open3.popen2(git_cmd, opts) do |stdin, stdout, wait_thr| - [block.call(stdout), wait_thr.value] - end - end - - def escape(s) - windows_platform? ? escape_for_windows(s) : escape_for_sh(s) - end - - def escape_for_sh(s) - "'#{s && s.to_s.gsub('\'','\'"\'"\'')}'" - end - - def escape_for_windows(s) - # Escape existing double quotes in s and then wrap the result with double quotes - escaped_string = s.to_s.gsub('"','\\"') - %Q{"#{escaped_string}"} - end - - def windows_platform? - # Check if on Windows via RUBY_PLATFORM (CRuby) and RUBY_DESCRIPTION (JRuby) - win_platform_regex = /mingw|mswin/ - RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex - end end end diff --git a/lib/git/version.rb b/lib/git/version.rb index 6ab7e075..120657f0 100644 --- a/lib/git/version.rb +++ b/lib/git/version.rb @@ -1,5 +1,5 @@ module Git # The current gem version # @return [String] the current gem version. - VERSION='1.19.1' + VERSION='2.0.0.pre1' end diff --git a/tests/test_helper.rb b/tests/test_helper.rb index 9bf44d6b..f5b08ee3 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -7,6 +7,9 @@ require "git" +$stdout.sync = true +$stderr.sync = true + class Test::Unit::TestCase TEST_ROOT = File.expand_path(__dir__) @@ -101,65 +104,32 @@ def append_file(name, contents) end end - # Runs a block inside an environment with customized ENV variables. - # It restores the ENV after execution. - # - # @param [Proc] block block to be executed within the customized environment - # - def with_custom_env_variables(&block) - saved_env = {} - begin - Git::Lib::ENV_VARIABLE_NAMES.each { |k| saved_env[k] = ENV[k] } - return block.call - ensure - Git::Lib::ENV_VARIABLE_NAMES.each { |k| ENV[k] = saved_env[k] } - end - end - - # Assert that the expected command line args are generated for a given Git::Lib method + # Assert that the expected command line is generated by a given Git::Base method # - # This assertion generates an empty git repository and then runs calls - # Git::Base method named by `git_cmd` passing that method `git_cmd_args`. + # This assertion generates an empty git repository and then yields to the + # given block passing the Git::Base instance for the empty repository. The + # current directory is set to the root of the repository's working tree. # - # Before calling `git_cmd`, this method stubs the `Git::Lib#command` method to - # capture the args sent to it by `git_cmd`. These args are captured into - # `actual_command_line`. # - # assert_equal is called comparing the given `expected_command_line` to - # `actual_command_line`. + # @example Test that calling `git.fetch` generates the command line `git fetch` + # # Only need to specify the arguments to the git command + # expected_command_line = ['fetch'] + # assert_command_line_eq(expected_command_line) { |git| git.fetch } # - # @example Fetch with no args - # expected_command_line = ['fetch', '--', 'origin'] - # git_cmd = :fetch - # git_cmd_args = [] - # assert_command_line(expected_command_line, git_cmd, git_cmd_args) - # - # @example Fetch with some args + # @example Test that calling `git.fetch('origin', { ref: 'master', depth: '2' })` generates the command line `git fetch --depth 2 -- origin master` # expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master'] - # git_cmd = :fetch - # git_cmd_args = ['origin', ref: 'master', depth: '2'] - # assert_command_line(expected_command_line, git_cmd, git_cmd_args) - # - # @example Fetch all - # expected_command_line = ['fetch', '--all'] - # git_cmd = :fetch - # git_cmd_args = [all: true] - # assert_command_line(expected_command_line, git_cmd, git_cmd_args) + # assert_command_line_eq(expected_command_line) { |git| git.fetch('origin', { ref: 'master', depth: '2' }) } # # @param expected_command_line [Array] The expected arguments to be sent to Git::Lib#command - # @param git_cmd [Symbol] the method to be called on the Git::Base object - # @param git_cmd_args [Array] The arguments to be sent to the git_cmd method - # @param git_output [String] The output to be returned by the Git::Lib#command method + # @param git_output [String] The mocked output to be returned by the Git::Lib#command method # - # @yield [git] An initialization block - # The initialization block is called after a test project is created with Git.init. - # The current working directory is set to the root of the test project's working tree. + # @yield [git] a block to call the method to be tested # @yieldparam git [Git::Base] The Git::Base object resulting from initializing the test project # @yieldreturn [void] the return value of the block is ignored # # @return [void] # - def assert_command_line(expected_command_line, git_cmd, git_cmd_args, git_output = nil) + def assert_command_line_eq(expected_command_line, method: :command, mocked_output: nil) actual_command_line = nil command_output = '' @@ -167,16 +137,13 @@ def assert_command_line(expected_command_line, git_cmd, git_cmd_args, git_output in_temp_dir do |path| git = Git.init('test_project') + git.lib.define_singleton_method(method) do |*cmd, **opts, &block| + actual_command_line = [*cmd, opts] + mocked_output + end + Dir.chdir 'test_project' do yield(git) if block_given? - - # Mock the Git::Lib#command method to capture the actual command line args - git.lib.define_singleton_method(:command) do |cmd, *opts, &block| - actual_command_line = [cmd, *opts.flatten] - git_output - end - - command_output = git.send(git_cmd, *git_cmd_args) end end diff --git a/tests/units/test_checkout.rb b/tests/units/test_checkout.rb index 0c761e83..a30b3fcc 100644 --- a/tests/units/test_checkout.rb +++ b/tests/units/test_checkout.rb @@ -1,67 +1,41 @@ require 'test_helper' - # Runs checkout command to checkout or create branch - # - # accepts options: - # :new_branch - # :force - # :start_point - # - # @param [String] branch - # @param [Hash] opts - # def checkout(branch, opts = {}) - class TestCheckout < Test::Unit::TestCase test 'checkout with no args' do - expected_command_line = ['checkout'] - git_cmd = :checkout - git_cmd_args = [] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout } end test 'checkout with no args and options' do - expected_command_line = ['checkout', '--force'] - git_cmd = :checkout - git_cmd_args = [force: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', '--force', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout(force: true) } end test 'checkout with branch' do - expected_command_line = ['checkout', 'feature1'] - git_cmd = :checkout - git_cmd_args = ['feature1'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', 'feature1', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout('feature1') } end test 'checkout with branch and options' do - expected_command_line = ['checkout', '--force', 'feature1'] - git_cmd = :checkout - git_cmd_args = ['feature1', force: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', '--force', 'feature1', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout('feature1', force: true) } end test 'checkout with branch name and new_branch: true' do - expected_command_line = ['checkout', '-b', 'feature1'] - git_cmd = :checkout - git_cmd_args = ['feature1', new_branch: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', '-b', 'feature1', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout('feature1', new_branch: true) } end test 'checkout with force: true' do - expected_command_line = ['checkout', '--force', 'feature1'] - git_cmd = :checkout - git_cmd_args = ['feature1', force: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', '--force', 'feature1', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout('feature1', force: true) } end test 'checkout with branch name and new_branch: true and start_point: "sha"' do - expected_command_line = ['checkout', '-b', 'feature1', 'sha'] - git_cmd = :checkout - git_cmd_args = ['feature1', new_branch: true, start_point: 'sha'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['checkout', '-b', 'feature1', 'sha', {}] + assert_command_line_eq(expected_command_line) { |git| git.checkout('feature1', new_branch: true, start_point: 'sha') } end - test 'when checkout succeeds an error should not be raised' do in_temp_dir do git = Git.init('.', initial_branch: 'master') diff --git a/tests/units/test_command_line.rb b/tests/units/test_command_line.rb new file mode 100644 index 00000000..81f48bb9 --- /dev/null +++ b/tests/units/test_command_line.rb @@ -0,0 +1,261 @@ +require 'test_helper' +require 'tempfile' + +class TestCommamndLine < Test::Unit::TestCase + test "initialize" do + global_opts = %q[--opt1=test --opt2] + + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + + assert_equal(env, command_line.env) + assert_equal(global_opts, command_line.global_opts) + assert_equal(logger, command_line.logger) + end + + # DEFAULT VALUES + # + # These are used by tests so the test can just change the value it wants to test. + # + def env + {} + end + + def binary_path + @binary_path ||= 'ruby' + end + + def global_opts + @global_opts ||= ['bin/command_line_test'] + end + + def logger + @logger ||= Logger.new(nil) + end + + def out_writer + nil + end + + def err_writer + nil + end + + def normalize + false + end + + def chomp + false + end + + def merge + false + end + + # END DEFAULT VALUES + + test "run should return a result that includes the command ran, its output, and resulting status" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + 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('stdout output', result.stdout.chomp) + assert_equal('stderr output', result.stderr.chomp) + assert(result.status.is_a? Process::Status) + assert_equal(0, result.status.exitstatus) + end + + test "run should raise FailedError if command fails" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--exitstatus=1', '--stdout=O1', '--stderr=O2'] + error = assert_raise Git::FailedError do + command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + end + + # 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('O1', result.stdout.chomp) + assert_equal('O2', result.stderr.chomp) + assert_equal(1, result.status.exitstatus) + end + + unless Gem.win_platform? + # Ruby on Windows doesn't support signals fully (at all?) + # See https://blog.simplificator.com/2016/01/18/how-to-kill-processes-on-windows-using-ruby/ + test "run should raise SignaledError if command exits because of an uncaught signal" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--signal=9', '--stdout=O1', '--stderr=O2'] + error = assert_raise Git::SignaledError do + command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + end + + # 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) + # 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) + assert_equal(9, result.status.termsig) + end + end + + test "run should chomp output if chomp is true" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--stdout=stdout output'] + chomp = true + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + + assert_equal('stdout output', result.stdout) + end + + 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) + out_writers.each { |w| w.write(File.read('tests/files/encoding/test1.txt')) } + `true` + $? # return status + end + + normalize = true + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + + expected_output = <<~OUTPUT + Λορεμ ιπσθμ δολορ σιτ + Ηισ εξ τοτα σθαvιτατε + Νο θρβανιτασ + Φεθγιατ θρβανιτασ ρεπριμιqθε + OUTPUT + + assert_equal(expected_output, result.stdout) + 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) + out_writers.each { |w| w.write(File.read('tests/files/encoding/test1.txt')) } + `true` + $? # return status + end + + 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 + + assert_equal(expected_output, result.stdout) + end + + test "run should redirect stderr to stdout if merge is true" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--stdout=stdout output', '--stderr=stderr output'] + merge = true + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + + # The output should be merged, but the order depends on a number of + # external factors + assert_include(result.stdout, 'stdout output') + assert_include(result.stdout, 'stderr output') + end + + test "run should log command and output if logger is given" do + log_output = StringIO.new + logger = Logger.new(log_output, level: Logger::DEBUG) + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--stdout=stdout output'] + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + + # The command and its exitstatus should be logged on INFO level + assert_match(/^I, .*exited with status pid \d+ exit \d+$/, log_output.string) + + # The command's stdout and stderr should be logged on DEBUG level + assert_match(/^D, .*stdout:\n.*\nstderr:\n.*$/, log_output.string) + end + + test "run should be able to redirect stdout to a file" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--stdout=stdout output'] + Tempfile.create do |f| + out_writer = f + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + f.rewind + assert_equal('stdout output', f.read.chomp) + end + end + + test "run should raise a GitExecuteError 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 + def write(*args) + raise IOError, 'error writing to file' + end + end.new + + error = assert_raise Git::GitExecuteError 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(IOError, error.cause) + assert_equal('error writing to file', error.cause.message) + end + + test "run should be able to redirect stderr to a file" do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--stderr=ERROR: fatal error', '--stdout=STARTING PROCESS'] + Tempfile.create do |f| + err_writer = f + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + f.rewind + assert_equal('ERROR: fatal error', f.read.chomp) + end + end + + test "run should raise a GitExecuteError 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 + def write(*args) + raise IOError, 'error writing to stderr file' + end + end.new + + error = assert_raise Git::GitExecuteError 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(IOError, error.cause) + assert_equal('error writing to stderr file', error.cause.message) + end + + test 'run should be able to redirect stdout and stderr to the same file' do + command_line = Git::CommandLine.new(env, binary_path, global_opts, logger) + args = ['--stderr=ERROR: fatal error', '--stdout=STARTING PROCESS'] + Tempfile.create do |f| + out_writer = f + merge = true + result = command_line.run(*args, out: out_writer, err: err_writer, normalize: normalize, chomp: chomp, merge: merge) + f.rewind + output = f.read + + # The output should be merged, but the order depends on a number of + # external factors + assert_include(output, 'ERROR: fatal error') + assert_include(output, 'STARTING PROCESS') + end + end +end diff --git a/tests/units/test_commit_with_gpg.rb b/tests/units/test_commit_with_gpg.rb index 10eae678..b8a3e1ec 100644 --- a/tests/units/test_commit_with_gpg.rb +++ b/tests/units/test_commit_with_gpg.rb @@ -8,45 +8,22 @@ def setup end def test_with_configured_gpg_keyid - Dir.mktmpdir do |dir| - git = Git.init(dir) - actual_cmd = nil - git.lib.define_singleton_method(:run_command) do |git_cmd, chdir, &block| - actual_cmd = git_cmd - [`true`, $?] - end - message = 'My commit message' - git.commit(message, gpg_sign: true) - assert_match(/commit.*--gpg-sign['"]/, actual_cmd) - end + message = 'My commit message' + expected_command_line = ["commit", "--message=#{message}", "--gpg-sign", {}] + assert_command_line_eq(expected_command_line) { |g| g.commit(message, gpg_sign: true) } end def test_with_specific_gpg_keyid - Dir.mktmpdir do |dir| - git = Git.init(dir) - actual_cmd = nil - git.lib.define_singleton_method(:run_command) do |git_cmd, chdir, &block| - actual_cmd = git_cmd - [`true`, $?] - end - message = 'My commit message' - git.commit(message, gpg_sign: 'keykeykey') - assert_match(/commit.*--gpg-sign=keykeykey['"]/, actual_cmd) - end + message = 'My commit message' + key = 'keykeykey' + expected_command_line = ["commit", "--message=#{message}", "--gpg-sign=#{key}", {}] + assert_command_line_eq(expected_command_line) { |g| g.commit(message, gpg_sign: key) } end def test_disabling_gpg_sign - Dir.mktmpdir do |dir| - git = Git.init(dir) - actual_cmd = nil - git.lib.define_singleton_method(:run_command) do |git_cmd, chdir, &block| - actual_cmd = git_cmd - [`true`, $?] - end - message = 'My commit message' - git.commit(message, no_gpg_sign: true) - assert_match(/commit.*--no-gpg-sign['"]/, actual_cmd) - end + message = 'My commit message' + expected_command_line = ["commit", "--message=#{message}", "--no-gpg-sign", {}] + assert_command_line_eq(expected_command_line) { |g| g.commit(message, no_gpg_sign: true) } end def test_conflicting_gpg_sign_options diff --git a/tests/units/test_config.rb b/tests/units/test_config.rb index 35208d24..b60e6c83 100644 --- a/tests/units/test_config.rb +++ b/tests/units/test_config.rb @@ -38,34 +38,32 @@ def test_set_config_with_custom_file end def test_env_config - with_custom_env_variables do - begin - assert_equal(Git::Base.config.binary_path, 'git') - assert_equal(Git::Base.config.git_ssh, nil) + begin + assert_equal(Git::Base.config.binary_path, 'git') + assert_equal(Git::Base.config.git_ssh, nil) - ENV['GIT_PATH'] = '/env/bin' - ENV['GIT_SSH'] = '/env/git/ssh' + ENV['GIT_PATH'] = '/env/bin' + ENV['GIT_SSH'] = '/env/git/ssh' - assert_equal(Git::Base.config.binary_path, '/env/bin/git') - assert_equal(Git::Base.config.git_ssh, '/env/git/ssh') + assert_equal(Git::Base.config.binary_path, '/env/bin/git') + assert_equal(Git::Base.config.git_ssh, '/env/git/ssh') - Git.configure do |config| - config.binary_path = '/usr/bin/git' - config.git_ssh = '/path/to/ssh/script' - end + Git.configure do |config| + config.binary_path = '/usr/bin/git' + config.git_ssh = '/path/to/ssh/script' + end - assert_equal(Git::Base.config.binary_path, '/usr/bin/git') - assert_equal(Git::Base.config.git_ssh, '/path/to/ssh/script') + assert_equal(Git::Base.config.binary_path, '/usr/bin/git') + assert_equal(Git::Base.config.git_ssh, '/path/to/ssh/script') - @git.log - ensure - ENV['GIT_SSH'] = nil - ENV['GIT_PATH'] = nil + @git.log + ensure + ENV['GIT_SSH'] = nil + ENV['GIT_PATH'] = nil - Git.configure do |config| - config.binary_path = nil - config.git_ssh = nil - end + Git.configure do |config| + config.binary_path = nil + config.git_ssh = nil end end end diff --git a/tests/units/test_failed_error.rb b/tests/units/test_failed_error.rb index 4833c6df..ea4ad4b2 100644 --- a/tests/units/test_failed_error.rb +++ b/tests/units/test_failed_error.rb @@ -17,7 +17,7 @@ def test_message error = Git::FailedError.new(result) - expected_message = "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\noutput: \"stdout\"" + expected_message = "[\"git\", \"status\"]\nstatus: pid 89784 exit 1\nstderr: \"stderr\"" assert_equal(expected_message, error.message) end end diff --git a/tests/units/test_lib.rb b/tests/units/test_lib.rb index b5502efd..9cf52923 100644 --- a/tests/units/test_lib.rb +++ b/tests/units/test_lib.rb @@ -90,14 +90,10 @@ def test_checkout def test_checkout_with_start_point assert(@lib.reset(nil, hard: true)) # to get around worktree status on windows - actual_cmd = nil - @lib.define_singleton_method(:run_command) do |git_cmd, chdir, &block| - actual_cmd = git_cmd - super(git_cmd, &block) + expected_command_line = ["checkout", "-b", "test_checkout_b2", "master", {}] + assert_command_line_eq(expected_command_line) do |git| + git.checkout('test_checkout_b2', {new_branch: true, start_point: 'master'}) end - - assert(@lib.checkout('test_checkout_b2', {new_branch: true, start_point: 'master'})) - assert_match(%r/['"]checkout['"] ['"]-b['"] ['"]test_checkout_b2['"] ['"]master['"]/, actual_cmd) end # takes parameters, returns array of appropriate commit objects @@ -127,41 +123,27 @@ def test_log_commits assert_equal(20, a.size) end - def test_environment_reset - with_custom_env_variables do - ENV['GIT_DIR'] = '/my/git/dir' - ENV['GIT_WORK_TREE'] = '/my/work/tree' - ENV['GIT_INDEX_FILE'] = 'my_index' - - @lib.log_commits :count => 10 - - assert_equal(ENV['GIT_DIR'], '/my/git/dir') - assert_equal(ENV['GIT_WORK_TREE'], '/my/work/tree') - assert_equal(ENV['GIT_INDEX_FILE'],'my_index') - end - end - def test_git_ssh_from_environment_is_passed_to_binary - with_custom_env_variables do - begin - Dir.mktmpdir do |dir| - output_path = File.join(dir, 'git_ssh_value') - binary_path = File.join(dir, 'git.bat') # .bat so it works in Windows too - Git::Base.config.binary_path = binary_path - File.open(binary_path, 'w') { |f| - f << "echo \"my/git-ssh-wrapper\" > #{output_path}" - } - FileUtils.chmod(0700, binary_path) - @lib.checkout('something') - assert(File.read(output_path).include?("my/git-ssh-wrapper")) - end - ensure - Git.configure do |config| - config.binary_path = nil - config.git_ssh = nil - end - end + saved_binary_path = Git::Base.config.binary_path + saved_git_ssh = Git::Base.config.git_ssh + + Dir.mktmpdir do |dir| + output_path = File.join(dir, 'git_ssh_value') + binary_path = File.join(dir, 'my_own_git.bat') # .bat so it works in Windows too + Git::Base.config.binary_path = binary_path + Git::Base.config.git_ssh = 'GIT_SSH_VALUE' + File.write(binary_path, <<~SCRIPT) + #!/bin/sh + set > "#{output_path}" + SCRIPT + FileUtils.chmod(0700, binary_path) + @lib.checkout('something') + env = File.read(output_path) + assert_match(/^GIT_SSH=(["']?)GIT_SSH_VALUE\1$/, env, 'GIT_SSH should be set in the environment') end + ensure + Git::Base.config.binary_path = saved_binary_path + Git::Base.config.git_ssh = saved_git_ssh end def test_revparse diff --git a/tests/units/test_logger.rb b/tests/units/test_logger.rb index 7c070e1d..470a2ed8 100644 --- a/tests/units/test_logger.rb +++ b/tests/units/test_logger.rb @@ -28,10 +28,10 @@ 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 -- : cherry/ + expected_log_entry = /DEBUG -- : stdout:\n" cherry/ assert_match(expected_log_entry, logc, missing_log_entry) end @@ -46,10 +46,10 @@ 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 -- : cherry/ + expected_log_entry = /DEBUG -- : stdout:\n" cherry/ assert_not_match(expected_log_entry, logc, unexpected_log_entry) end end diff --git a/tests/units/test_push.rb b/tests/units/test_push.rb index 83c227b7..78cc9396 100644 --- a/tests/units/test_push.rb +++ b/tests/units/test_push.rb @@ -2,52 +2,36 @@ class TestPush < Test::Unit::TestCase test 'push with no args' do - expected_command_line = ['push'] - git_cmd = :push - git_cmd_args = [] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', {}] + assert_command_line_eq(expected_command_line) { |git| git.push } end test 'push with no args and options' do - expected_command_line = ['push', '--force'] - git_cmd = :push - git_cmd_args = [force: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--force', {}] + assert_command_line_eq(expected_command_line) { |git| git.push(force: true) } end test 'push with only a remote name' do - expected_command_line = ['push', 'origin'] - git_cmd = :push - git_cmd_args = ['origin'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', 'origin', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin') } end test 'push with a single push option' do - expected_command_line = ['push', '--push-option', 'foo'] - git_cmd = :push - git_cmd_args = [push_option: 'foo'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--push-option', 'foo', {}] + assert_command_line_eq(expected_command_line) { |git| git.push(push_option: 'foo') } end test 'push with an array of push options' do - expected_command_line = ['push', '--push-option', 'foo', '--push-option', 'bar', '--push-option', 'baz'] - git_cmd = :push - git_cmd_args = [push_option: ['foo', 'bar', 'baz']] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--push-option', 'foo', '--push-option', 'bar', '--push-option', 'baz', {}] + assert_command_line_eq(expected_command_line) { |git| git.push(push_option: ['foo', 'bar', 'baz']) } end test 'push with only a remote name and options' do - expected_command_line = ['push', '--force', 'origin'] - git_cmd = :push - git_cmd_args = ['origin', force: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--force', 'origin', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', force: true) } end test 'push with only a branch name' do - expected_command_line = ['push', 'master'] - git_cmd = :push - git_cmd_args = [nil, 'origin'] - in_temp_dir do git = Git.init('.', initial_branch: 'master') assert_raises(ArgumentError) { git.push(nil, 'master') } @@ -55,52 +39,38 @@ class TestPush < Test::Unit::TestCase end test 'push with both remote and branch name' do - expected_command_line = ['push', 'origin', 'master'] - git_cmd = :push - git_cmd_args = ['origin', 'master'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', 'origin', 'master', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', 'master') } end test 'push with force: true' do - expected_command_line = ['push', '--force', 'origin', 'master'] - git_cmd = :push - git_cmd_args = ['origin', 'master', force: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--force', 'origin', 'master', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', 'master', force: true) } end test 'push with f: true' do - expected_command_line = ['push', '--force', 'origin', 'master'] - git_cmd = :push - git_cmd_args = ['origin', 'master', f: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--force', 'origin', 'master', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', 'master', f: true) } end test 'push with mirror: true' do - expected_command_line = ['push', '--force', 'origin', 'master'] - git_cmd = :push - git_cmd_args = ['origin', 'master', f: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--mirror', 'origin', 'master', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', 'master', mirror: true) } end test 'push with delete: true' do - expected_command_line = ['push', '--delete', 'origin', 'master'] - git_cmd = :push - git_cmd_args = ['origin', 'master', delete: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--delete', 'origin', 'master', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', 'master', delete: true) } end test 'push with tags: true' do - expected_command_line = ['push', '--tags', 'origin'] - git_cmd = :push - git_cmd_args = ['origin', nil, tags: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--tags', 'origin', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', 'master', tags: true) } end test 'push with all: true' do - expected_command_line = ['push', '--all', 'origin'] - git_cmd = :push - git_cmd_args = ['origin', all: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['push', '--all', 'origin', {}] + assert_command_line_eq(expected_command_line) { |git| git.push('origin', all: true) } end test 'when push succeeds an error should not be raised' do diff --git a/tests/units/test_remotes.rb b/tests/units/test_remotes.rb index 39374950..b134afbc 100644 --- a/tests/units/test_remotes.rb +++ b/tests/units/test_remotes.rb @@ -120,38 +120,28 @@ def test_fetch end def test_fetch_cmd_with_no_args - expected_command_line = ['fetch', '--', 'origin'] - git_cmd = :fetch - git_cmd_args = [] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['fetch', '--', 'origin', { merge: true }] + assert_command_line_eq(expected_command_line) { |git| git.fetch } end def test_fetch_cmd_with_origin_and_branch - expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master'] - git_cmd = :fetch - git_cmd_args = ['origin', ref: 'master', depth: '2'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['fetch', '--depth', '2', '--', 'origin', 'master', { merge: true }] + assert_command_line_eq(expected_command_line) { |git| git.fetch('origin', { ref: 'master', depth: '2' }) } end def test_fetch_cmd_with_all - expected_command_line = ['fetch', '--all'] - git_cmd = :fetch - git_cmd_args = [all: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['fetch', '--all', { merge: true }] + assert_command_line_eq(expected_command_line) { |git| git.fetch({ all: true }) } end def test_fetch_cmd_with_all_with_other_args - expected_command_line = ['fetch', '--all', '--force', '--depth', '2'] - git_cmd = :fetch - git_cmd_args = [all: true, force: true, depth: '2'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['fetch', '--all', '--force', '--depth', '2', { merge: true }] + assert_command_line_eq(expected_command_line) { |git| git.fetch({all: true, force: true, depth: '2'}) } end def test_fetch_cmd_with_update_head_ok - expected_command_line = ['fetch', '--update-head-ok'] - git_cmd = :fetch - git_cmd_args = [:'update-head-ok' => true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['fetch', '--update-head-ok', { merge: true }] + assert_command_line_eq(expected_command_line) { |git| git.fetch({:'update-head-ok' => true}) } end def test_fetch_command_injection @@ -162,10 +152,10 @@ def test_fetch_command_injection origin = "--upload-pack=touch #{test_file};" begin git.fetch(origin, { ref: 'some/ref/head' }) - rescue Git::FailedError + rescue Git::GitExecuteError # This is expected else - raise 'Expected Git::Failed to be raised' + raise 'Expected Git::FailedError to be raised' end vulnerability_exists = File.exist?(test_file) @@ -179,24 +169,28 @@ def test_fetch_ref_adds_ref_option rem = Git.clone(BARE_REPO_PATH, 'remote', :config => 'receive.denyCurrentBranch=ignore') loc.add_remote('testrem', rem) - loc.chdir do + first_commit_sha = second_commit_sha = nil + + rem.chdir do new_file('test-file1', 'gonnaCommitYou') - loc.add - loc.commit('master commit 1') - first_commit_sha = loc.log.first.sha + rem.add + rem.commit('master commit 1') + first_commit_sha = rem.log.first.sha new_file('test-file2', 'gonnaCommitYouToo') - loc.add - loc.commit('master commit 2') - second_commit_sha = loc.log.first.sha + rem.add + rem.commit('master commit 2') + second_commit_sha = rem.log.first.sha + end + loc.chdir do # Make sure fetch message only has the first commit when we fetch the first commit - assert(loc.fetch('origin', {:ref => first_commit_sha}).include?(first_commit_sha)) - assert(!loc.fetch('origin', {:ref => first_commit_sha}).include?(second_commit_sha)) + assert(loc.fetch('testrem', {:ref => first_commit_sha}).include?(first_commit_sha)) + assert(!loc.fetch('testrem', {:ref => first_commit_sha}).include?(second_commit_sha)) # Make sure fetch message only has the second commit when we fetch the second commit - assert(loc.fetch('origin', {:ref => second_commit_sha}).include?(second_commit_sha)) - assert(!loc.fetch('origin', {:ref => second_commit_sha}).include?(first_commit_sha)) + assert(loc.fetch('testrem', {:ref => second_commit_sha}).include?(second_commit_sha)) + assert(!loc.fetch('testrem', {:ref => second_commit_sha}).include?(first_commit_sha)) end end end diff --git a/tests/units/test_repack.rb b/tests/units/test_repack.rb index da7be542..4a27e8f8 100644 --- a/tests/units/test_repack.rb +++ b/tests/units/test_repack.rb @@ -4,17 +4,7 @@ class TestRepack < Test::Unit::TestCase test 'should be able to call repack with the right args' do - in_bare_repo_clone do |r1| - new_file('new_file', 'new content') - r1.add - r1.commit('my commit') - - # assert_nothing_raised { r1.repack } - - expected_command_line = ['repack', '-a', '-d'] - git_cmd = :repack - git_cmd_args = [] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) - end + expected_command_line = ['repack', '-a', '-d', {}] + assert_command_line_eq(expected_command_line) { |git| git.repack } end end diff --git a/tests/units/test_rm.rb b/tests/units/test_rm.rb index 9b205d11..658ce9ca 100644 --- a/tests/units/test_rm.rb +++ b/tests/units/test_rm.rb @@ -9,39 +9,31 @@ # because right now it forks for every call class TestRm < Test::Unit::TestCase - test 'rm with no options should specific "." for the pathspec' do - expected_command_line = ['rm', '-f', '--', '.'] - git_cmd = :rm - git_cmd_args = [] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + test 'rm with no options should specify "." for the pathspec' do + expected_command_line = ['rm', '-f', '--', '.', {}] + assert_command_line_eq(expected_command_line) { |git| git.rm } end test 'rm with one pathspec' do - expected_command_line = ['rm', '-f', '--', 'pathspec'] - git_cmd = :rm - git_cmd_args = ['pathspec'] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['rm', '-f', '--', 'pathspec', {}] + assert_command_line_eq(expected_command_line) { |git| git.rm('pathspec') } end test 'rm with multiple pathspecs' do - expected_command_line = ['rm', '-f', '--', 'pathspec1', 'pathspec2'] - git_cmd = :rm - git_cmd_args = [['pathspec1', 'pathspec2']] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['rm', '-f', '--', 'pathspec1', 'pathspec2', {}] + assert_command_line_eq(expected_command_line) { |git| git.rm(['pathspec1', 'pathspec2']) } end test 'rm with the recursive option' do - expected_command_line = ['rm', '-f', '-r', '--', 'pathspec'] - git_cmd = :rm - git_cmd_args = ['pathspec', recursive: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['rm', '-f', '-r', '--', 'pathspec', {}] + assert_command_line_eq(expected_command_line) { |git| git.rm('pathspec', recursive: true) } end test 'rm with the cached option' do - expected_command_line = ['rm', '-f', '--cached', '--', 'pathspec'] + expected_command_line = ['rm', '-f', '--cached', '--', 'pathspec', {}] git_cmd = :rm git_cmd_args = ['pathspec', cached: true] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + assert_command_line_eq(expected_command_line) { |git| git.rm('pathspec', cached: true) } end test 'when rm succeeds an error should not be raised' do diff --git a/tests/units/test_tree_ops.rb b/tests/units/test_tree_ops.rb index 02d0b43a..82e65b49 100644 --- a/tests/units/test_tree_ops.rb +++ b/tests/units/test_tree_ops.rb @@ -6,67 +6,45 @@ class TestTreeOps < Test::Unit::TestCase def test_read_tree treeish = 'testbranch1' - expected_command_line = ['read-tree', treeish] - git_cmd = :read_tree - git_cmd_args = [treeish] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['read-tree', treeish, {}] + assert_command_line_eq(expected_command_line) { |git| git.read_tree(treeish) } end def test_read_tree_with_prefix treeish = 'testbranch1' prefix = 'foo' - expected_command_line = ['read-tree', "--prefix=#{prefix}", treeish] - git_cmd = :read_tree - git_cmd_args = [treeish, prefix: prefix] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + expected_command_line = ['read-tree', "--prefix=#{prefix}", treeish, {}] + assert_command_line_eq(expected_command_line) { |git| git.read_tree(treeish, prefix: prefix) } end def test_write_tree - expected_command_line = ['write-tree'] - git_cmd = :write_tree - git_cmd_args = [] - git_output = 'aa7349e' - result = assert_command_line(expected_command_line, git_cmd, git_cmd_args, git_output) + expected_output = 'aa7349e' + actual_output = nil + expected_command_line = ['write-tree', {}] + assert_command_line_eq(expected_command_line, mocked_output: expected_output) do |git| + actual_output = git.write_tree + end + # the git output should be returned from Git::Base#write_tree - assert_equal(git_output, result) + assert_equal(expected_output, actual_output) end def test_commit_tree_with_default_message tree = 'tree-ref' + message = 'commit tree tree-ref' - expected_message = 'commit tree tree-ref' - tempfile_path = 'foo' - mock_tempfile = mock('tempfile') - Tempfile.stubs(:new).returns(mock_tempfile) - mock_tempfile.stubs(:path).returns(tempfile_path) - mock_tempfile.expects(:write).with(expected_message) - mock_tempfile.expects(:close) - - redirect_value = windows_platform? ? "< \"#{tempfile_path}\"" : "< '#{tempfile_path}'" + expected_command_line = ['commit-tree', tree, '-m', message, {}] - expected_command_line = ['commit-tree', tree, redirect: redirect_value] - git_cmd = :commit_tree - git_cmd_args = [tree] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + assert_command_line_eq(expected_command_line) { |git| git.commit_tree(tree) } end def test_commit_tree_with_message tree = 'tree-ref' message = 'this is my message' - tempfile_path = 'foo' - mock_tempfile = mock('tempfile') - Tempfile.stubs(:new).returns(mock_tempfile) - mock_tempfile.stubs(:path).returns(tempfile_path) - mock_tempfile.expects(:write).with(message) - mock_tempfile.expects(:close) - - redirect_value = windows_platform? ? "< \"#{tempfile_path}\"" : "< '#{tempfile_path}'" + expected_command_line = ['commit-tree', tree, '-m', message, {}] - expected_command_line = ['commit-tree', tree, redirect: redirect_value] - git_cmd = :commit_tree - git_cmd_args = [tree, message: message] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + assert_command_line_eq(expected_command_line) { |git| git.commit_tree(tree, message: message) } end def test_commit_tree_with_parent @@ -74,20 +52,9 @@ def test_commit_tree_with_parent message = 'this is my message' parent = 'parent-commit' - tempfile_path = 'foo' - mock_tempfile = mock('tempfile') - Tempfile.stubs(:new).returns(mock_tempfile) - mock_tempfile.stubs(:path).returns(tempfile_path) - mock_tempfile.expects(:write).with(message) - mock_tempfile.expects(:close) - - redirect_value = windows_platform? ? "< \"#{tempfile_path}\"" : "< '#{tempfile_path}'" - - expected_command_line = ['commit-tree', tree, "-p", parent, redirect: redirect_value] - git_cmd = :commit_tree - git_cmd_args = [tree, parent: parent, message: message] + expected_command_line = ['commit-tree', tree, "-p", parent, '-m', message, {}] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + assert_command_line_eq(expected_command_line) { |git| git.commit_tree(tree, parent: parent, message: message) } end def test_commit_tree_with_parents @@ -95,20 +62,9 @@ def test_commit_tree_with_parents message = 'this is my message' parents = 'commit1' - tempfile_path = 'foo' - mock_tempfile = mock('tempfile') - Tempfile.stubs(:new).returns(mock_tempfile) - mock_tempfile.stubs(:path).returns(tempfile_path) - mock_tempfile.expects(:write).with(message) - mock_tempfile.expects(:close) + expected_command_line = ['commit-tree', tree, '-p', 'commit1', '-m', message, {}] - redirect_value = windows_platform? ? "< \"#{tempfile_path}\"" : "< '#{tempfile_path}'" - - expected_command_line = ['commit-tree', tree, '-p', 'commit1', redirect: redirect_value] - git_cmd = :commit_tree - git_cmd_args = [tree, parents: parents, message: message] - - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + assert_command_line_eq(expected_command_line) { |git| git.commit_tree(tree, parents: parents, message: message) } end def test_commit_tree_with_multiple_parents @@ -116,20 +72,9 @@ def test_commit_tree_with_multiple_parents message = 'this is my message' parents = ['commit1', 'commit2'] - tempfile_path = 'foo' - mock_tempfile = mock('tempfile') - Tempfile.stubs(:new).returns(mock_tempfile) - mock_tempfile.stubs(:path).returns(tempfile_path) - mock_tempfile.expects(:write).with(message) - mock_tempfile.expects(:close) - - redirect_value = windows_platform? ? "< \"#{tempfile_path}\"" : "< '#{tempfile_path}'" - - expected_command_line = ['commit-tree', tree, '-p', 'commit1', '-p', 'commit2', redirect: redirect_value] - git_cmd = :commit_tree - git_cmd_args = [tree, parents: parents, message: message] + expected_command_line = ['commit-tree', tree, '-p', 'commit1', '-p', 'commit2', '-m', message, {}] - assert_command_line(expected_command_line, git_cmd, git_cmd_args) + assert_command_line_eq(expected_command_line) { |git| git.commit_tree(tree, parents: parents, message: message) } end # Examples of how to use Git::Base#commit_tree, write_tree, and commit_tree pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy