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