diff --git a/.rubocop.yml b/.rubocop.yml index 15b15e0a..c85b9b91 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -3,6 +3,25 @@ inherit_from: .rubocop_todo.yml inherit_gem: main_branch_shared_rubocop_config: config/rubocop.yml +# Allow test data to have long lines +Layout/LineLength: + Exclude: + - "tests/test_helper.rb" + - "tests/units/**/*" + - "*.gemspec" + +# Testing and gemspec DSL results in large blocks +Metrics/BlockLength: + Exclude: + - "tests/test_helper.rb" + - "tests/units/**/*" + - "*.gemspec" + +# Don't force every test class to be described +Style/Documentation: + Exclude: + - "tests/units/**/*" + AllCops: # Pin this project to Ruby 3.1 in case the shared config above is upgraded to 3.2 # or later. diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 46c7f6c2..fbff4782 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -6,61 +6,15 @@ # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 2 -Lint/DuplicateMethods: - Exclude: - - 'lib/git/base.rb' - - 'lib/git/worktree.rb' - -# Offense count: 1 -# Configuration parameters: AllowComments, AllowEmptyLambdas. -Lint/EmptyBlock: - Exclude: - - 'tests/units/test_archive.rb' - -# Offense count: 3 -# Configuration parameters: AllowedParentClasses. -Lint/MissingSuper: - Exclude: - - 'lib/git/branch.rb' - - 'lib/git/remote.rb' - - 'lib/git/worktree.rb' - -# Offense count: 8 -Lint/StructNewOverride: - Exclude: - - 'tests/units/test_command_line_error.rb' - - 'tests/units/test_failed_error.rb' - - 'tests/units/test_signaled_error.rb' - - 'tests/units/test_timeout_error.rb' - -# Offense count: 2 -# Configuration parameters: AllowComments, AllowNil. -Lint/SuppressedException: - Exclude: - - 'lib/git/lib.rb' - - 'tests/units/test_each_conflict.rb' - -# Offense count: 1 -Lint/UselessConstantScoping: - Exclude: - - 'lib/git/branch.rb' - # Offense count: 68 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 109 -# Offense count: 8 -# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. -# AllowedMethods: refine -Metrics/BlockLength: - Max: 49 - # Offense count: 21 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 898 + Max: 925 # Offense count: 14 # Configuration parameters: AllowedMethods, AllowedPatterns. @@ -72,92 +26,7 @@ Metrics/CyclomaticComplexity: Metrics/MethodLength: Max: 51 -# Offense count: 2 -# Configuration parameters: CountKeywordArgs, MaxOptionalParameters. -Metrics/ParameterLists: - Max: 8 - # Offense count: 12 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 22 - -# Offense count: 1 -Naming/AccessorMethodName: - Exclude: - - 'lib/git/object.rb' - -# Offense count: 1 -# Configuration parameters: ForbiddenDelimiters. -# ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) -Naming/HeredocDelimiterNaming: - Exclude: - - 'tests/units/test_signed_commits.rb' - -# Offense count: 5 -# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods. -# AllowedMethods: call -Naming/PredicateMethod: - Exclude: - - 'lib/git/branch.rb' - - 'lib/git/lib.rb' - - 'tests/units/test_command_line.rb' - -# Offense count: 3 -# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. -# NamePrefix: is_, has_, have_, does_ -# ForbiddenPrefixes: is_, has_, have_, does_ -# AllowedMethods: is_a? -# MethodDefinitionMacros: define_method, define_singleton_method -Naming/PredicatePrefix: - Exclude: - - 'spec/**/*' - - 'lib/git/base.rb' - -# Offense count: 2 -# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. -# SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 -Naming/VariableNumber: - Exclude: - - 'tests/units/test_log.rb' - - 'tests/units/test_log_execute.rb' - -# Offense count: 1 -Style/ClassVars: - Exclude: - - 'lib/git/base.rb' - -# Offense count: 66 -# Configuration parameters: AllowedConstants. -Style/Documentation: - Enabled: false - -# Offense count: 4 -# This cop supports safe autocorrection (--autocorrect). -Style/IfUnlessModifier: - Exclude: - - 'lib/git/base.rb' - - 'lib/git/lib.rb' - -# Offense count: 2 -Style/MultilineBlockChain: - Exclude: - - 'lib/git/base.rb' - -# Offense count: 5 -# Configuration parameters: AllowedMethods. -# AllowedMethods: respond_to_missing? -Style/OptionalBooleanParameter: - Exclude: - - 'lib/git/base.rb' - - 'lib/git/object.rb' - - 'lib/git/path.rb' - - 'lib/git/stash.rb' - -# Offense count: 64 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. -# URISchemes: http, https -Layout/LineLength: - Max: 346 diff --git a/lib/git.rb b/lib/git.rb index c0537ff1..638b77d8 100644 --- a/lib/git.rb +++ b/lib/git.rb @@ -216,7 +216,8 @@ def self.clone(repository_url, directory = nil, options = {}) # @example with the logging option # logger = Logger.new(STDOUT, level: Logger::INFO) # Git.default_branch('.', log: logger) # => 'master' - # I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true' '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1 + # I, [2022-04-13T16:01:33.221596 #18415] INFO -- : git '-c' 'core.quotePath=true' + # '-c' 'color.ui=false' ls-remote '--symref' '--' '.' 'HEAD' 2>&1 # # @param repository [URI, Pathname, String] The (possibly remote) repository to get the default branch name for # diff --git a/lib/git/author.rb b/lib/git/author.rb index 1cc60832..ede74b34 100644 --- a/lib/git/author.rb +++ b/lib/git/author.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module Git + # An author in a Git commit class Author attr_accessor :name, :email, :date diff --git a/lib/git/base.rb b/lib/git/base.rb index 1193eae9..b75c63f4 100644 --- a/lib/git/base.rb +++ b/lib/git/base.rb @@ -35,7 +35,7 @@ def self.repository_default_branch(repository, options = {}) # # @return [Git::Config] the current config instance. def self.config - @@config ||= Config.new + @config ||= Config.new end def self.binary_version(binary_path) @@ -91,8 +91,10 @@ def self.root_of_worktree(working_dir) raise ArgumentError, "'#{working_dir}' does not exist" unless Dir.exist?(working_dir) begin - result, status = Open3.capture2e(Git::Base.config.binary_path, '-c', 'core.quotePath=true', '-c', - 'color.ui=false', 'rev-parse', '--show-toplevel', chdir: File.expand_path(working_dir)) + result, status = Open3.capture2e( + Git::Base.config.binary_path, '-c', 'core.quotePath=true', '-c', + 'color.ui=false', 'rev-parse', '--show-toplevel', chdir: File.expand_path(working_dir) + ) result = result.chomp rescue Errno::ENOENT raise ArgumentError, 'Failed to find the root of the worktree: git binary not found' @@ -182,29 +184,6 @@ def add_remote(name, url, opts = {}) Git::Remote.new(self, name) end - # Create a new git tag - # - # @example - # repo.add_tag('tag_name', object_reference) - # repo.add_tag('tag_name', object_reference, {:options => 'here'}) - # repo.add_tag('tag_name', {:options => 'here'}) - # - # @param [String] name The name of the tag to add - # @param [Hash] options Opstions to pass to `git tag`. - # See [git-tag](https://git-scm.com/docs/git-tag) for more details. - # @option options [boolean] :annotate Make an unsigned, annotated tag object - # @option options [boolean] :a An alias for the `:annotate` option - # @option options [boolean] :d Delete existing tag with the given names. - # @option options [boolean] :f Replace an existing tag with the given name (instead of failing) - # @option options [String] :message Use the given tag message - # @option options [String] :m An alias for the `:message` option - # @option options [boolean] :s Make a GPG-signed tag. - # - def add_tag(name, *options) - lib.tag(name, *options) - tag(name) - end - # changes current working directory for a block # to the git working directory # @@ -256,43 +235,77 @@ def repo # returns the repository size in bytes def repo_size - Dir.glob(File.join(repo.path, '**', '*'), File::FNM_DOTMATCH).reject do |f| - f.include?('..') - end.map do |f| - File.expand_path(f) - end.uniq.map do |f| - File.stat(f).size.to_i - end.reduce(:+) + all_files = Dir.glob(File.join(repo.path, '**', '*'), File::FNM_DOTMATCH) + + all_files.reject { |file| file.include?('..') } + .map { |file| File.expand_path(file) } + .uniq + .sum { |file| File.stat(file).size.to_i } end - def set_index(index_file, check = true) + def set_index(index_file, check = nil, must_exist: nil) + unless check.nil? + Git::Deprecation.warn( + 'The "check" argument is deprecated and will be removed in a future version. ' \ + 'Use "must_exist:" instead.' + ) + end + + # default is true + must_exist = must_exist.nil? && check.nil? ? true : must_exist | check + @lib = nil - @index = Git::Index.new(index_file.to_s, check) + @index = Git::Index.new(index_file.to_s, must_exist:) end - def set_working(work_dir, check = true) + def set_working(work_dir, check = nil, must_exist: nil) + unless check.nil? + Git::Deprecation.warn( + 'The "check" argument is deprecated and will be removed in a future version. ' \ + 'Use "must_exist:" instead.' + ) + end + + # default is true + must_exist = must_exist.nil? && check.nil? ? true : must_exist | check + @lib = nil - @working_directory = Git::WorkingDirectory.new(work_dir.to_s, check) + @working_directory = Git::WorkingDirectory.new(work_dir.to_s, must_exist:) end # returns +true+ if the branch exists locally - def is_local_branch?(branch) + def local_branch?(branch) branch_names = branches.local.map(&:name) branch_names.include?(branch) end + def is_local_branch?(branch) # rubocop:disable Naming/PredicatePrefix + Git.deprecation('Git::Base#is_local_branch? is deprecated. Use Git::Base#local_branch? instead.') + local_branch?(branch) + end + # returns +true+ if the branch exists remotely - def is_remote_branch?(branch) + def remote_branch?(branch) branch_names = branches.remote.map(&:name) branch_names.include?(branch) end + def is_remote_branch?(branch) # rubocop:disable Naming/PredicatePrefix + Git.deprecated('Git::Base#is_remote_branch? is deprecated. Use Git::Base#remote_branch? instead.') + remote_branch?(branch) + end + # returns +true+ if the branch exists - def is_branch?(branch) + def branch?(branch) branch_names = branches.map(&:name) branch_names.include?(branch) end + def is_branch?(branch) # rubocop:disable Naming/PredicatePrefix + Git.deprecated('Git::Base#is_branch? is deprecated. Use Git::Base#branch? instead.') + branch?(branch) + end + # this is a convenience method for accessing the class that wraps all the # actual 'git' forked system calls. At some point I hope to replace the Git::Lib # class with one that uses native methods or libgit C bindings @@ -768,7 +781,7 @@ def status # @return [Git::Object::Tag] a tag object def tag(tag_name) - Git::Object.new(self, tag_name, 'tag', true) + Git::Object::Tag.new(self, tag_name) end # Find as good common ancestors as possible for a merge @@ -874,7 +887,8 @@ def diff_path_status(objectish = 'HEAD', obj2 = nil) end if File.file?(repository) - repository = File.expand_path(File.read(repository)[8..].strip, options[:working_directory]) + repository = File.expand_path(File.read(repository)[8..].strip, + options[:working_directory]) end options[:repository] = repository diff --git a/lib/git/branch.rb b/lib/git/branch.rb index d1e60068..94e81b08 100644 --- a/lib/git/branch.rb +++ b/lib/git/branch.rb @@ -3,7 +3,8 @@ require 'git/path' module Git - class Branch < Path + # Represents a Git branch + class Branch attr_accessor :full, :remote, :name def initialize(base, name) @@ -56,8 +57,8 @@ def delete @base.lib.branch_delete(@name) end - def current - determine_current + def current # rubocop:disable Naming/PredicateMethod + @base.lib.branch_current == @name end def contains?(commit) @@ -93,18 +94,6 @@ def to_s @full end - private - - def check_if_create - @base.lib.branch_new(@name) - rescue StandardError - nil - end - - def determine_current - @base.lib.branch_current == @name - end - BRANCH_NAME_REGEXP = %r{ ^ # Optional 'refs/remotes/' at the beggining to specify a remote tracking branch @@ -116,6 +105,8 @@ def determine_current $ }x + private + # Given a full branch name return an Array containing the remote and branch names. # # Removes 'remotes' from the beggining of the name (if present). @@ -143,5 +134,11 @@ def parse_name(name) branch_name = match[:branch_name] [remote, branch_name] end + + def check_if_create + @base.lib.branch_new(@name) + rescue StandardError + nil + end end end diff --git a/lib/git/branches.rb b/lib/git/branches.rb index b490074e..85dfce19 100644 --- a/lib/git/branches.rb +++ b/lib/git/branches.rb @@ -51,7 +51,8 @@ def [](branch_name) branches[branch.full] ||= branch # This is how Git (version 1.7.9.5) works. - # Lets you ignore the 'remotes' if its at the beginning of the branch full name (even if is not a real remote branch). + # Lets you ignore the 'remotes' if its at the beginning of the branch full + # name (even if is not a real remote branch). branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ %r{^remotes/.+} end[branch_name.to_s] end diff --git a/lib/git/command_line.rb b/lib/git/command_line.rb index 372084cf..befa43fe 100644 --- a/lib/git/command_line.rb +++ b/lib/git/command_line.rb @@ -97,6 +97,10 @@ def initialize(env, binary_path, global_opts, logger) # Execute a git command, wait for it to finish, and return the result # + # Non-option the command line arguements to pass to git. If you collect + # the command line arguments in an array, make sure you splat the array + # into the parameter list. + # # NORMALIZATION # # The command output is returned as a Unicde string containing the binary output @@ -142,11 +146,9 @@ def initialize(env, binary_path, global_opts, logger) # 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 options_hash [Hash] the options to pass to the command # - # @param out [#write, nil] the object to write stdout to or nil to ignore stdout + # @option options_hash [#write, nil] :out the object to write stdout to or nil to ignore stdout # # If this is a 'StringIO' object, then `stdout_writer.string` will be returned. # @@ -154,20 +156,20 @@ def initialize(env, binary_path, global_opts, logger) # 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 + # @option options_hash [#write, nil] :err 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 + # @option options_hash [Boolean] :normalize whether to normalize the output of stdout and stderr # - # @param chomp [Boolean] whether to chomp the output + # @option options_hash [Boolean] :chomp whether to chomp both stdout and stderr output # - # @param merge [Boolean] whether to merge stdout and stderr in the string returned + # @option options_hash [Boolean] :merge whether to merge stdout and stderr in the string returned # - # @param chdir [String] the directory to run the command in + # @option options_hash [String, nil] :chdir the directory to run the command in # - # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete + # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete # # If timeout is zero, the timeout will not be enforced. # @@ -189,21 +191,50 @@ def initialize(env, binary_path, global_opts, logger) # # @raise [Git::TimeoutError] if the command times out # - def run(*args, normalize:, chomp:, merge:, out: nil, err: nil, chdir: nil, timeout: nil) + def run(*, **options_hash) + options_hash = RUN_ARGS.merge(options_hash) + extra_options = options_hash.keys - RUN_ARGS.keys + raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any? + + result = run_with_capture(*, **options_hash) + process_result(result, options_hash[:normalize], options_hash[:chomp], options_hash[:timeout]) + end + + # @return [Git::CommandLineResult] the result of running the command + # + # @api private + # + def run_with_capture(*args, **options_hash) git_cmd = build_git_cmd(args) - begin - options = { chdir: chdir || :not_set, timeout_after: timeout, raise_errors: false } + options = run_with_capture_options(**options_hash) + ProcessExecuter.run_with_capture(env, *git_cmd, **options) + rescue ProcessExecuter::ProcessIOError => e + raise Git::ProcessIOError.new(e.message), cause: e.exception.cause + end + + def run_with_capture_options(**options_hash) + chdir = options_hash[:chdir] || :not_set + timeout_after = options_hash[:timeout] + out = options_hash[:out] + err = options_hash[:err] + merge_output = options_hash[:merge] || false + + { chdir:, timeout_after:, merge_output:, raise_errors: false }.tap do |options| options[:out] = out unless out.nil? options[:err] = err unless err.nil? - options[:merge_output] = merge unless merge.nil? - - result = ProcessExecuter.run_with_capture(env, *git_cmd, **options) - rescue ProcessExecuter::ProcessIOError => e - raise Git::ProcessIOError.new(e.message), cause: e.exception.cause end - process_result(result, normalize, chomp, timeout) end + RUN_ARGS = { + normalize: false, + chomp: false, + merge: false, + out: nil, + err: nil, + chdir: nil, + timeout: nil + }.freeze + private # Build the git command line from the available sources to send to `Process.spawn` @@ -221,17 +252,27 @@ def build_git_cmd(args) # Post process output, log the command and result, and raise an error if the # command failed. # - # @param result [ProcessExecuter::Command::Result] the result it is a Process::Status and include command, stdout, and stderr + # @param result [ProcessExecuter::Command::Result] the result it is a + # Process::Status and include command, stdout, and stderr + # # @param normalize [Boolean] whether to normalize the output of each writer + # # @param chomp [Boolean] whether to chomp the output of each writer - # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete # - # @return [Git::CommandLineResult] the result of the command to return to the caller + # @param timeout [Numeric, nil] the maximum seconds to wait for the command to + # complete + # + # @return [Git::CommandLineResult] the result of the command to return to the + # caller # # @raise [Git::FailedError] if the command failed + # # @raise [Git::SignaledError] if the command was signaled + # # @raise [Git::TimeoutError] if the command times out - # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output + # + # @raise [Git::ProcessIOError] if an exception was raised while collecting + # subprocess output # # @api private # diff --git a/lib/git/config.rb b/lib/git/config.rb index fbd49b6c..115f0be3 100644 --- a/lib/git/config.rb +++ b/lib/git/config.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module Git + # The global configuration for this gem class Config attr_writer :binary_path, :git_ssh, :timeout diff --git a/lib/git/diff.rb b/lib/git/diff.rb index 899802c6..036fbc29 100644 --- a/lib/git/diff.rb +++ b/lib/git/diff.rb @@ -76,6 +76,7 @@ def stats } end + # The changes for a single file within a diff class DiffFile attr_accessor :patch, :path, :mode, :src, :dst, :type diff --git a/lib/git/diff_path_status.rb b/lib/git/diff_path_status.rb index 57400c8e..726e512d 100644 --- a/lib/git/diff_path_status.rb +++ b/lib/git/diff_path_status.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module Git + # The files and their status (e.g., added, modified, deleted) between two commits class DiffPathStatus include Enumerable diff --git a/lib/git/errors.rb b/lib/git/errors.rb index 900f858a..02bf022d 100644 --- a/lib/git/errors.rb +++ b/lib/git/errors.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Git + # rubocop:disable Layout/LineLength + # Base class for all custom git module errors # # The git gem will only raise an `ArgumentError` or an error that is a subclass of @@ -60,6 +62,8 @@ module Git # class Error < StandardError; end + # rubocop:enable Layout/LineLength + # An alias for Git::Error # # Git::GitExecuteError error class is an alias for Git::Error for backwards @@ -155,7 +159,8 @@ class TimeoutError < Git::SignaledError # status = ProcessExecuter.spawn(*command, timeout: timeout_duration) # result = Git::CommandLineResult.new(command, status, 'stdout', 'err output') # error = Git::TimeoutError.new(result, timeout_duration) - # error.error_message #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s' + # error.error_message + # #=> '["sleep", "10"], status: pid 70144 SIGKILL (signal 9), stderr: "err output", timed out after 1s' # # @param result [Git::CommandLineResult] the result of the git command including # the git command, status, stdout, and stderr @@ -171,7 +176,8 @@ def initialize(result, timeout_duration) # The human readable representation of this error # # @example - # error.error_message #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s' + # error.error_message + # #=> '["sleep", "10"], status: pid 88811 SIGKILL (signal 9), stderr: "err output", timed out after 1s' # # @return [String] # diff --git a/lib/git/lib.rb b/lib/git/lib.rb index 44919fa4..a77dede5 100644 --- a/lib/git/lib.rb +++ b/lib/git/lib.rb @@ -11,6 +11,8 @@ require 'open3' module Git + # Internal git operations + # @api private class Lib # The path to the Git working copy. The default is '"./.git"'. # @@ -94,6 +96,7 @@ def init(opts = {}) # Clones a repository into a newly created directory # # @param [String] repository_url the URL of the repository to clone + # # @param [String, nil] directory the directory to clone into # # If nil, the repository is cloned into a directory with the same name as @@ -102,16 +105,28 @@ def init(opts = {}) # @param [Hash] opts the options for this command # # @option opts [Boolean] :bare (false) if true, clone as a bare repository + # # @option opts [String] :branch the branch to checkout + # # @option opts [String, Array] :config one or more configuration options to set + # # @option opts [Integer] :depth the number of commits back to pull + # # @option opts [String] :filter specify partial clone + # # @option opts [String] :mirror set up a mirror of the source repository + # # @option opts [String] :origin the name of the remote + # # @option opts [String] :path an optional prefix for the directory parameter + # # @option opts [String] :remote the name of the remote - # @option opts [Boolean] :recursive after the clone is created, initialize all submodules within, using their default settings - # @option opts [Numeric, nil] :timeout the number of seconds to wait for the command to complete + # + # @option opts [Boolean] :recursive after the clone is created, initialize all + # within, using their default settings + # + # @option opts [Numeric, nil] :timeout the number of seconds to wait for the + # command to complete # # See {Git::Lib#command} for more information about :timeout # @@ -266,14 +281,23 @@ def log_commits(opts = {}) # # @param opts [Hash] the given options # - # @option opts :count [Integer] the maximum number of commits to return (maps to max-count) + # @option opts :count [Integer] the maximum number of commits to return (maps to + # max-count) + # # @option opts :all [Boolean] + # # @option opts :cherry [Boolean] + # # @option opts :since [String] + # # @option opts :until [String] + # # @option opts :grep [String] + # # @option opts :author [String] - # @option opts :between [Array] an array of two commit-ish strings to specify a revision range + # + # @option opts :between [Array] an array of two commit-ish strings to + # specify a revision range # # Only :between or :object options can be used, not both. # @@ -281,22 +305,29 @@ def log_commits(opts = {}) # # Only :between or :object options can be used, not both. # - # @option opts :path_limiter [Array, String] only include commits that impact files from the specified paths + # @option opts :path_limiter [Array, String] only include commits that + # impact files from the specified paths + # # @option opts :skip [Integer] # # @return [Array] the log output parsed into an array of hashs for each commit # # Each hash contains the following keys: + # # * 'sha' [String] the commit sha # * 'author' [String] the author of the commit # * 'message' [String] the commit message # * 'parent' [Array] the commit shas of the parent commits # * 'tree' [String] the tree sha - # * 'author' [String] the author of the commit and timestamp of when the changes were created - # * 'committer' [String] the committer of the commit and timestamp of when the commit was applied - # * 'merges' [Boolean] if truthy, only include merge commits (aka commits with 2 or more parents) + # * 'author' [String] the author of the commit and timestamp of when the + # changes were created + # * 'committer' [String] the committer of the commit and timestamp of when the + # commit was applied + # * 'merges' [Boolean] if truthy, only include merge commits (aka commits with + # 2 or more parents) # - # @raise [ArgumentError] if the revision range (specified with :between or :object) is a string starting with a hyphen + # @raise [ArgumentError] if the revision range (specified with :between or + # :object) is a string starting with a hyphen # def full_log_commits(opts = {}) assert_args_are_not_options('between', opts[:between]&.first) @@ -319,7 +350,8 @@ def full_log_commits(opts = {}) # # @see https://git-scm.com/docs/git-rev-parse git-rev-parse # @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions - # @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem Ref disambiguation rules + # @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem + # Ref disambiguation rules # # @example # lib.rev_parse('HEAD') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d' @@ -490,10 +522,12 @@ def each_cat_file_header(data) # Return a hash of annotated tag data # - # Does not work with lightweight tags. List all annotated tags in your repository with the following command: + # Does not work with lightweight tags. List all annotated tags in your repository + # with the following command: # # ```sh - # git for-each-ref --format='%(refname:strip=2)' refs/tags | while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done + # git for-each-ref --format='%(refname:strip=2)' refs/tags | \ + # while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done # ``` # # @see https://git-scm.com/docs/git-cat-file git-cat-file @@ -518,7 +552,8 @@ def each_cat_file_header(data) # * object [String] the sha of the tag object # * type [String] # * tag [String] tag name - # * tagger [String] the name and email of the user who created the tag and the timestamp of when the tag was created + # * tagger [String] the name and email of the user who created the tag + # and the timestamp of when the tag was created # * message [String] the tag message # # @raise [ArgumentError] if object is a string starting with a hyphen @@ -707,12 +742,7 @@ def worktree_prune def list_files(ref_dir) dir = File.join(@git_dir, 'refs', ref_dir) - files = [] - begin - files = Dir.glob('**/*', base: dir).select { |f| File.file?(File.join(dir, f)) } - rescue StandardError - end - files + Dir.glob('**/*', base: dir).select { |f| File.file?(File.join(dir, f)) } end # The state and name of branch pointed to by `HEAD` @@ -1304,7 +1334,8 @@ def tag(name, *opts) opts = opts.last.instance_of?(Hash) ? opts.last : {} if (opts[:a] || opts[:annotate]) && !(opts[:m] || opts[:message]) - raise ArgumentError, 'Cannot create an annotated tag without a message.' + raise ArgumentError, + 'Cannot create an annotated tag without a message.' end arr_opts = [] @@ -1517,16 +1548,29 @@ def meets_required_version? (current_command_version <=> required_command_version) >= 0 end - def self.warn_if_old_command(lib) + def self.warn_if_old_command(lib) # rubocop:disable Naming/PredicateMethod + Git::Deprecation.warn('Git::Lib#warn_if_old_command is deprecated. Use meets_required_version?.') + return true if @version_checked @version_checked = true unless lib.meets_required_version? - warn "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, but only found #{lib.current_command_version.join('.')}. You should probably upgrade." + warn "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, " \ + "but only found #{lib.current_command_version.join('.')}. You should probably upgrade." end true end + COMMAND_ARG_DEFAULTS = { + out: nil, + err: nil, + normalize: true, + chomp: true, + merge: false, + chdir: nil, + timeout: nil # Don't set to Git.config.timeout here since it is mutable + }.freeze + private def command_lines(cmd, *opts, chdir: nil) @@ -1574,26 +1618,20 @@ def command_line # Runs a git command and returns the output # # Additional args are passed to the command line. They should exclude the 'git' - # command itself and global options. - # - # For example, to run `git log --pretty=oneline`, you would pass `['log', - # '--pretty=oneline']` - # - # @param out [String, nil] the path to a file or an IO to write the command's - # stdout to - # - # @param err [String, nil] the path to a file or an IO to write the command's - # stdout to + # command itself and global options. Remember to splat the the arguments if given + # as an array. # - # @param normalize [Boolean] true to normalize the output encoding + # For example, to run `git log --pretty=oneline`, you would create the array + # `args = ['log', '--pretty=oneline']` and call `command(*args)`. # - # @param chomp [Boolean] true to remove trailing newlines from the output - # - # @param merge [Boolean] true to merge stdout and stderr - # - # @param chdir [String, nil] the directory to run the command in - # - # @param timeout [Numeric, nil] the maximum seconds to wait for the command to complete + # @param options_hash [Hash] the options to pass to the command + # @option options_hash [IO, String, #write, nil] :out the destination for captured stdout + # @option options_hash [IO, String, #write, nil] :err the destination for captured stderr + # @option options_hash [Boolean] :normalize true to normalize the output encoding to UTF-8 + # @option options_hash [Boolean] :chomp true to remove trailing newlines from the output + # @option options_hash [Boolean] :merge true to merge stdout and stderr into a single output + # @option options_hash [String, nil] :chdir the directory to run the command in + # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete # # If timeout is nil, the global timeout from {Git::Config} is used. # @@ -1608,9 +1646,14 @@ def command_line # @return [String] the command's stdout (or merged stdout and stderr if `merge` # is true) # + # @raise [ArgumentError] if an unknown option is passed + # # @raise [Git::FailedError] if the command failed + # # @raise [Git::SignaledError] if the command was signaled + # # @raise [Git::TimeoutError] if the command times out + # # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output # # The exception's `result` attribute is a {Git::CommandLineResult} which will @@ -1619,10 +1662,14 @@ def command_line # # @api private # - def command(*, out: nil, err: nil, normalize: true, chomp: true, merge: false, chdir: nil, timeout: nil) - timeout ||= Git.config.timeout - result = command_line.run(*, out: out, err: err, normalize: normalize, chomp: chomp, merge: merge, - chdir: chdir, timeout: timeout) + def command(*, **options_hash) + options_hash = COMMAND_ARG_DEFAULTS.merge(options_hash) + options_hash[:timeout] ||= Git.config.timeout + + extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys + raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any? + + result = command_line.run(*, **options_hash) result.stdout end @@ -1657,7 +1704,8 @@ def log_common_options(opts) arr_opts = [] if opts[:count] && !opts[:count].is_a?(Integer) - raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}" + raise ArgumentError, + "The log count option must be an Integer but was #{opts[:count].inspect}" end arr_opts << "--max-count=#{opts[:count]}" if opts[:count] diff --git a/lib/git/log.rb b/lib/git/log.rb index 76d8b6c5..1dbfc8d8 100644 --- a/lib/git/log.rb +++ b/lib/git/log.rb @@ -264,7 +264,10 @@ def [](index) private def deprecate_method(method_name) - Git::Deprecation.warn("Calling Git::Log##{method_name} is deprecated and will be removed in a future version. Call #execute and then ##{method_name} on the result object.") + Git::Deprecation.warn( + "Calling Git::Log##{method_name} is deprecated and will be removed in a future version. " \ + "Call #execute and then ##{method_name} on the result object." + ) end def dirty_log diff --git a/lib/git/object.rb b/lib/git/object.rb index 0e4b1a99..6c5c235f 100644 --- a/lib/git/object.rb +++ b/lib/git/object.rb @@ -8,6 +8,7 @@ module Git # represents a git object class Object + # A base class for all Git objects class AbstractObject attr_accessor :objectish, :type, :mode @@ -79,6 +80,7 @@ def commit? = false def tag? = false end + # A Git blob object class Blob < AbstractObject def initialize(base, sha, mode = nil) super(base, sha) @@ -90,6 +92,7 @@ def blob? end end + # A Git tree object class Tree < AbstractObject def initialize(base, sha, mode = nil) super(base, sha) @@ -149,6 +152,7 @@ def check_tree end end + # A Git commit object class Commit < AbstractObject def initialize(base, sha, init = nil) super(base, sha) @@ -159,7 +163,7 @@ def initialize(base, sha, init = nil) @message = nil return unless init - set_commit(init) + from_data(init) end def message @@ -211,7 +215,12 @@ def diff_parent diff(parent) end - def set_commit(data) + def set_commit(data) # rubocop:disable Naming/AccessorMethodName + Git.deprecation('Git::Object::Commit#set_commit is deprecated. Use #from_data instead.') + from_data(data) + end + + def from_data(data) @sha ||= data['sha'] @committer = Git::Author.new(data['committer']) @author = Git::Author.new(data['author']) @@ -231,22 +240,47 @@ def check_commit return if @tree data = @base.lib.cat_file_commit(@objectish) - set_commit(data) + from_data(data) end end + # A Git tag object + # + # This class represents a tag in Git, which can be either annotated or lightweight. + # + # Annotated tags contain additional metadata such as the tagger's name, email, and + # the date when the tag was created, along with a message. + # + # TODO: Annotated tags are not objects + # class Tag < AbstractObject attr_accessor :name - def initialize(base, sha, name) + # @overload initialize(base, name) + # @param base [Git::Base] The Git base object + # @param name [String] The name of the tag + # + # @overload initialize(base, sha, name) + # @param base [Git::Base] The Git base object + # @param sha [String] The SHA of the tag object + # @param name [String] The name of the tag + # + def initialize(base, sha, name = nil) + if name.nil? + name = sha + sha = base.lib.tag_sha(name) + raise Git::UnexpectedResultError, "Tag '#{name}' does not exist." if sha == '' + end + super(base, sha) + @name = name @annotated = nil @loaded = false end def annotated? - @annotated ||= (@base.lib.cat_file_type(name) == 'tag') + @annotated = @annotated.nil? ? (@base.lib.cat_file_type(name) == 'tag') : @annotated end def message @@ -282,12 +316,10 @@ def check_tag # if we're calling this, we don't know what type it is yet # so this is our little factory method - def self.new(base, objectish, type = nil, is_tag = false) + def self.new(base, objectish, type = nil, is_tag = false) # rubocop:disable Style/OptionalBooleanParameter if is_tag - sha = base.lib.tag_sha(objectish) - raise Git::UnexpectedResultError, "Tag '#{objectish}' does not exist." if sha == '' - - return Git::Object::Tag.new(base, sha, objectish) + Git::Deprecation.warn('Git::Object.new with is_tag argument is deprecated. Use Git::Object::Tag.new instead.') + return Git::Object::Tag.new(base, objectish) end type ||= base.lib.cat_file_type(objectish) diff --git a/lib/git/path.rb b/lib/git/path.rb index 59d77e39..32b3baa4 100644 --- a/lib/git/path.rb +++ b/lib/git/path.rb @@ -1,13 +1,28 @@ # frozen_string_literal: true module Git + # A base class that represents and validates a filesystem path + # + # Use for tracking things relevant to a Git repository, such as the working + # directory or index file. + # class Path attr_accessor :path - def initialize(path, check_path = true) + def initialize(path, check_path = nil, must_exist: nil) + unless check_path.nil? + Git::Deprecation.warn( + 'The "check_path" argument is deprecated and ' \ + 'will be removed in a future version. Use "must_exist:" instead.' + ) + end + + # default is true + must_exist = must_exist.nil? && check_path.nil? ? true : must_exist || check_path + path = File.expand_path(path) - raise ArgumentError, 'path does not exist', [path] if check_path && !File.exist?(path) + raise ArgumentError, 'path does not exist', [path] if must_exist && !File.exist?(path) @path = path end diff --git a/lib/git/remote.rb b/lib/git/remote.rb index 178436cd..8eed519b 100644 --- a/lib/git/remote.rb +++ b/lib/git/remote.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module Git - class Remote < Path + # A remote in a Git repository + class Remote attr_accessor :name, :url, :fetch_opts def initialize(base, name) diff --git a/lib/git/stash.rb b/lib/git/stash.rb index bace354c..2e9af43b 100644 --- a/lib/git/stash.rb +++ b/lib/git/stash.rb @@ -1,11 +1,21 @@ # frozen_string_literal: true module Git + # A stash in a Git repository class Stash - def initialize(base, message, existing = false) + def initialize(base, message, existing = nil, save: nil) + unless existing.nil? + Git::Deprecation.warn( + 'The "existing" argument is deprecated and will be removed in a future version. Use "save:" instead.' + ) + end + + # default is false + save = existing.nil? && save.nil? ? false : save | existing + @base = base @message = message - save unless existing + self.save unless save end def save diff --git a/lib/git/status.rb b/lib/git/status.rb index 2e0e0b75..c18f23c2 100644 --- a/lib/git/status.rb +++ b/lib/git/status.rb @@ -250,7 +250,12 @@ def fetch_untracked def fetch_modified # Files changed between the index vs. the worktree # git diff-files - # { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } } + # { + # file => { + # path: file, type: 'M', mode_index: '100644', mode_repo: '100644', + # sha_index: '0000000', :sha_repo: '52c6c4e' + # } + # } @base.lib.diff_files.each do |path, data| @files[path] ? @files[path].merge!(data) : @files[path] = data end @@ -261,7 +266,12 @@ def fetch_added # Files changed between the repo HEAD vs. the worktree # git diff-index HEAD - # { file => { path: file, type: 'M', mode_index: '100644', mode_repo: '100644', sha_index: '0000000', :sha_repo: '52c6c4e' } } + # { + # file => { + # path: file, type: 'M', mode_index: '100644', mode_repo: '100644', + # sha_index: '0000000', :sha_repo: '52c6c4e' + # } + # } @base.lib.diff_index('HEAD').each do |path, data| @files[path] ? @files[path].merge!(data) : @files[path] = data end diff --git a/lib/git/worktree.rb b/lib/git/worktree.rb index c9dbea4d..b99db5c3 100644 --- a/lib/git/worktree.rb +++ b/lib/git/worktree.rb @@ -3,8 +3,9 @@ require 'git/path' module Git - class Worktree < Path - attr_accessor :full, :dir, :gcommit + # A worktree in a Git repository + class Worktree + attr_accessor :full, :dir def initialize(base, dir, gcommit = nil) @full = dir diff --git a/tests/test_helper.rb b/tests/test_helper.rb index e14d2f19..fb4ac4b3 100644 --- a/tests/test_helper.rb +++ b/tests/test_helper.rb @@ -17,6 +17,11 @@ module Test module Unit + # A base class for all test cases in this project + # + # This class provides utility methods for setting up and tearing down test + # environments, creating temporary repositories, and mocking the Git binary. + # class TestCase TEST_ROOT = File.expand_path(__dir__) TEST_FIXTURES = File.join(TEST_ROOT, 'files') diff --git a/tests/units/test_archive.rb b/tests/units/test_archive.rb index 53a7bf9e..0035017c 100644 --- a/tests/units/test_archive.rb +++ b/tests/units/test_archive.rb @@ -8,8 +8,16 @@ def setup @git = Git.open(@wdir) end + require 'securerandom' + require 'tmpdir' + + # Create a temporary file path without actually creating the file + # + # @return [String] the path to the temporary file + # def tempfile - Dir::Tmpname.create('test-archive') {} + random_string = SecureRandom.hex(8) + File.join(Dir.tmpdir, "test-archive-#{random_string}") end def test_archive diff --git a/tests/units/test_command_line.rb b/tests/units/test_command_line.rb index 7488b57b..61c148e4 100644 --- a/tests/units/test_command_line.rb +++ b/tests/units/test_command_line.rb @@ -42,15 +42,15 @@ def err_writer nil end - def normalize + def normalize # rubocop:disable Naming/PredicateMethod false end - def chomp + def chomp # rubocop:disable Naming/PredicateMethod false end - def merge + def merge # rubocop:disable Naming/PredicateMethod false end diff --git a/tests/units/test_command_line_error.rb b/tests/units/test_command_line_error.rb index 25c03765..22c2c21c 100644 --- a/tests/units/test_command_line_error.rb +++ b/tests/units/test_command_line_error.rb @@ -4,9 +4,8 @@ class TestCommandLineError < Test::Unit::TestCase def test_initializer - status = Struct.new(:to_s).new('pid 89784 exit 1') + status = Class.new { def to_s = 'pid 89784 exit 1' }.new result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') - error = Git::CommandLineError.new(result) assert(error.is_a?(Git::Error)) @@ -14,7 +13,7 @@ def test_initializer end def test_to_s - status = Struct.new(:to_s).new('pid 89784 exit 1') + status = Class.new { def to_s = 'pid 89784 exit 1' }.new result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') error = Git::CommandLineError.new(result) diff --git a/tests/units/test_each_conflict.rb b/tests/units/test_each_conflict.rb index 6bfb37df..f6983d8a 100644 --- a/tests/units/test_each_conflict.rb +++ b/tests/units/test_each_conflict.rb @@ -5,9 +5,10 @@ class TestEachConflict < Test::Unit::TestCase def test_conflicts in_temp_repo('working') do + # Setup a repository with a conflict g = Git.open('.') - g.branch('new_branch').in_branch('test') do + g.branch('new_branch').in_branch('commit message') do new_file('example.txt', "1\n2\n3") g.add true @@ -20,11 +21,17 @@ def test_conflicts end g.merge('new_branch') + begin g.merge('new_branch2') - rescue StandardError + rescue Git::FailedError => e + assert_equal(1, e.result.status.exitstatus) + assert_match(/CONFLICT/, e.result.stdout) end + assert_equal(1, g.lib.unmerged.size) + + # Check the conflict g.each_conflict do |file, your, their| assert_equal('example.txt', file) assert_equal("1\n2\n3\n", File.read(your)) diff --git a/tests/units/test_failed_error.rb b/tests/units/test_failed_error.rb index 16a7c855..2a2cd6e9 100644 --- a/tests/units/test_failed_error.rb +++ b/tests/units/test_failed_error.rb @@ -4,7 +4,7 @@ class TestFailedError < Test::Unit::TestCase def test_initializer - status = Struct.new(:to_s).new('pid 89784 exit 1') + status = Class.new { def to_s = 'pid 89784 exit 1' }.new result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') error = Git::FailedError.new(result) @@ -13,7 +13,7 @@ def test_initializer end def test_to_s - status = Struct.new(:to_s).new('pid 89784 exit 1') + status = Class.new { def to_s = 'pid 89784 exit 1' }.new result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') error = Git::FailedError.new(result) diff --git a/tests/units/test_log.rb b/tests/units/test_log.rb index 781d90ff..6f71fe29 100644 --- a/tests/units/test_log.rb +++ b/tests/units/test_log.rb @@ -15,7 +15,7 @@ def test_log_max_count_default end # In these tests, note that @git.log(n) is equivalent to @git.log.max_count(n) - def test_log_max_count_20 + def test_log_max_count_twenty assert_equal(20, @git.log(20).size) assert_equal(20, @git.log.max_count(20).size) end diff --git a/tests/units/test_log_execute.rb b/tests/units/test_log_execute.rb index d6a1ef40..b55e78e4 100644 --- a/tests/units/test_log_execute.rb +++ b/tests/units/test_log_execute.rb @@ -16,7 +16,7 @@ def test_log_max_count_default end # In these tests, note that @git.log(n) is equivalent to @git.log.max_count(n) - def test_log_max_count_20 + def test_log_max_count_twenty assert_equal(20, @git.log(20).execute.size) assert_equal(20, @git.log.max_count(20).execute.size) end diff --git a/tests/units/test_signaled_error.rb b/tests/units/test_signaled_error.rb index 9fb75c15..7400985a 100644 --- a/tests/units/test_signaled_error.rb +++ b/tests/units/test_signaled_error.rb @@ -4,7 +4,8 @@ class TestSignaledError < Test::Unit::TestCase def test_initializer - status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` + # `kill -9 $$` + status = Class.new { def to_s = 'pid 65628 SIGKILL (signal 9)' }.new result = Git::CommandLineResult.new(%w[git status], status, '', 'uncaught signal') error = Git::SignaledError.new(result) @@ -13,7 +14,8 @@ def test_initializer end def test_to_s - status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` + # `kill -9 $$` + status = Class.new { def to_s = 'pid 65628 SIGKILL (signal 9)' }.new result = Git::CommandLineResult.new(%w[git status], status, '', 'uncaught signal') error = Git::SignaledError.new(result) diff --git a/tests/units/test_signed_commits.rb b/tests/units/test_signed_commits.rb index 99be0852..5cc28ccf 100644 --- a/tests/units/test_signed_commits.rb +++ b/tests/units/test_signed_commits.rb @@ -4,11 +4,11 @@ require 'fileutils' class TestSignedCommits < Test::Unit::TestCase - SSH_SIGNATURE_REGEXP = Regexp.new(<<~EOS.chomp, Regexp::MULTILINE) + SSH_SIGNATURE_REGEXP = Regexp.new(<<~REGEXP.chomp, Regexp::MULTILINE) -----BEGIN SSH SIGNATURE----- .* -----END SSH SIGNATURE----- - EOS + REGEXP def in_repo_with_signing_config in_temp_dir do |_path| diff --git a/tests/units/test_timeout_error.rb b/tests/units/test_timeout_error.rb index e3e4999a..911eece8 100644 --- a/tests/units/test_timeout_error.rb +++ b/tests/units/test_timeout_error.rb @@ -4,7 +4,8 @@ class TestTimeoutError < Test::Unit::TestCase def test_initializer - status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` + # `kill -9 $$` + status = Class.new { def to_s = 'pid 65628 SIGKILL (signal 9)' }.new result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'stderr') timeout_diration = 10 @@ -14,7 +15,8 @@ def test_initializer end def test_to_s - status = Struct.new(:to_s).new('pid 65628 SIGKILL (signal 9)') # `kill -9 $$` + # `kill -9 $$` + status = Class.new { def to_s = 'pid 65628 SIGKILL (signal 9)' }.new result = Git::CommandLineResult.new(%w[git status], status, 'stdout', 'Waiting...') timeout_duration = 10