class Git::Commands::Base
@api private
Base class for git command implementations.
Provides default {#initialize} and {#call} methods so that simple commands only need to declare their arguments:
class Add < Git::Commands::Base arguments do literal 'add' flag_option :all flag_option :force end_of_options operand :paths, repeatable: true end # Execute the git add command # ...YARD docs... def call(...) = super end
Commands whose git process may exit with a non-zero status that is not an error can declare the acceptable range of exit codes:
class Delete < Git::Commands::Base arguments do literal 'branch' literal '--delete' operand :branch_names, repeatable: true, required: true end allow_exit_status 0..1 # Execute the git branch --delete command # ...YARD docs... def call(...) = super end
Commands with execution options (e.g., timeout) work with the default ‘call` — execution options are extracted and forwarded automatically.
Attributes
@return [Range, nil] range of exit status values accepted by this command
@return [Git::Commands::Arguments, nil] the frozen argument definition for this command
@return [Git::VersionConstraint, nil] version constraint for this command
Returns +nil+ if the command is available in all supported git versions.
Public Class Methods
Source
# File lib/git/commands/base.rb, line 85 def allow_exit_status(range) raise ArgumentError, 'allow_exit_status expects a Range' unless range.is_a?(Range) unless range.begin.is_a?(Integer) && range.end.is_a?(Integer) raise ArgumentError, 'allow_exit_status bounds must be Integers' end raise ArgumentError, 'allow_exit_status range must not be empty' if range.begin > range.end @allowed_exit_status_range = range end
Declare the acceptable range of exit status values for this command.
@example git-diff exits 1 when a diff is found (not an error)
allow_exit_status 0..1
@example git-fsck uses exit codes 0-7 as bit flags
allow_exit_status 0..7
@param range [Range] range of accepted exit status values
@return [void]
@raise [ArgumentError] if range is invalid
Source
# File lib/git/commands/base.rb, line 62 def arguments(&) raise ArgumentError, "arguments already defined for #{name}" if @args_definition @args_definition = Arguments.define(&).freeze end
Define the command’s arguments using the {Arguments} DSL.
@return [void]
@raise [ArgumentError] if called more than once on the same class
@yield the block passed to {Arguments.define}
Source
# File lib/git/commands/base.rb, line 241 def call(*, **) bound = args_definition.bind(*, **) validate_version!(bound.execution_options) result = execute_command(bound) validate_exit_status!(result) result end
Execute the git command.
@overload call(*args, **kwargs)
Bind arguments and execute the command.
Execution options (declared via `execution_option` in the Arguments
DSL) are extracted from the bound arguments via
{Git::Commands::Arguments::Bound#execution_options} and forwarded as
keyword arguments to the execution context via {#execute_command}.
When the `:out` execution option is present, stdout is streamed using
`@execution_context.command_streaming`. Otherwise, stdout is captured
using `@execution_context.command_capturing`.
@example
# In a command subclass:
# result = command.call('HEAD', timeout: 10)
@param args [Array] positional arguments forwarded to {Arguments#bind}
@param kwargs [Hash] keyword arguments forwarded to {Arguments#bind}
@return [Git::CommandLine::Result] the result of calling ‘git`
@raise [ArgumentError] if no arguments definition is declared on the command class
@raise [Git::FailedError] if git returns an exit code outside the allowed range
@raise [Git::VersionError] if the installed git version doesn’t meet requirements
Source
# File lib/git/commands/base.rb, line 208 def initialize(execution_context) @execution_context = execution_context end
@param execution_context [Git::ExecutionContext] context that provides
{Git::ExecutionContext#command_capturing} and {Git::ExecutionContext#command_streaming}
Source
# File lib/git/commands/base.rb, line 162 def normalize_version_constraint(min, before_version) raise ArgumentError, 'requires_git_version requires min or before:' unless min || before_version min_version = min ? parse_version(min, 'min') : nil before_parsed = before_version ? parse_version(before_version, 'before:') : nil Git::VersionConstraint.new(min: min_version, before: before_parsed) end
Normalize a version constraint to a VersionConstraint
@param min [String, nil] minimum version
@param before_version [String, nil] upper bound version
@return [Git::VersionConstraint] the normalized constraint
@raise [ArgumentError] if the constraint is invalid
Source
# File lib/git/commands/base.rb, line 181 def parse_version(version, key_name) validate_version_format!(version, key_name) Git::Version.parse(version) end
Parse a version string into a Git version object
@param version [String] version string to parse
@param key_name [String] argument name used in validation errors
@return [Git::Version] the parsed version
@raise [ArgumentError] if the version format is invalid
Source
# File lib/git/commands/base.rb, line 144 def requires_git_version(min = nil, before: nil) raise ArgumentError, 'requires_git_version already declared for this class' if @git_version_constraint @git_version_constraint = normalize_version_constraint(min, before) end
Declare the git version requirements for this command.
Use this when the command (or the specific sub-action this class wraps) was introduced in a git version later than Git::MINIMUM_GIT_VERSION, or when the command was removed in a later version. When not declared, the command is assumed to be available in all supported git versions.
@example git-am –retry requires git 2.46.0 or later
requires_git_version '2.46.0'
@example Feature with version range
requires_git_version '2.29.0', before: '2.50.0'
@example Feature available before git 2.50.0
requires_git_version before: '2.50.0'
@param min [String, nil] minimum version
@param before [String, nil] upper bound version (exclusive)
@return [void]
@raise [ArgumentError] if version format is invalid or called twice
Source
# File lib/git/commands/base.rb, line 116 def skip_version_validation @skip_version_validation = true end
Declare that this command should skip version validation.
This is intended for internal use only — specifically for the ‘git version` command, which cannot validate versions without causing infinite recursion.
@return [void]
@api private
Source
# File lib/git/commands/base.rb, line 104 def skip_version_validation? = !!@skip_version_validation # Declare that this command should skip version validation. # # This is intended for internal use only — specifically for the # `git version` command, which cannot validate versions without # causing infinite recursion. # # @return [void] # # @api private # def skip_version_validation @skip_version_validation = true end # Declare the git version requirements for this command. # # Use this when the command (or the specific sub-action this class wraps) was # introduced in a git version later than +Git::MINIMUM_GIT_VERSION+, or when # the command was removed in a later version. When not declared, the command # is assumed to be available in all supported git versions. # # @example git-am --retry requires git 2.46.0 or later # requires_git_version '2.46.0' # # @example Feature with version range # requires_git_version '2.29.0', before: '2.50.0' # # @example Feature available before git 2.50.0 # requires_git_version before: '2.50.0' # # @param min [String, nil] minimum version # # @param before [String, nil] upper bound version (exclusive) # # @return [void] # # @raise [ArgumentError] if version format is invalid or called twice # def requires_git_version(min = nil, before: nil) raise ArgumentError, 'requires_git_version already declared for this class' if @git_version_constraint @git_version_constraint = normalize_version_constraint(min, before) end private # Normalize a version constraint to a VersionConstraint # # @param min [String, nil] minimum version # # @param before_version [String, nil] upper bound version # # @return [Git::VersionConstraint] the normalized constraint # # @raise [ArgumentError] if the constraint is invalid # def normalize_version_constraint(min, before_version) raise ArgumentError, 'requires_git_version requires min or before:' unless min || before_version min_version = min ? parse_version(min, 'min') : nil before_parsed = before_version ? parse_version(before_version, 'before:') : nil Git::VersionConstraint.new(min: min_version, before: before_parsed) end # Parse a version string into a Git version object # # @param version [String] version string to parse # # @param key_name [String] argument name used in validation errors # # @return [Git::Version] the parsed version # # @raise [ArgumentError] if the version format is invalid # def parse_version(version, key_name) validate_version_format!(version, key_name) Git::Version.parse(version) end # Validate a version string uses the required format # # @param version [Object] value to validate # # @param context [String, nil] argument name included in validation errors # # @return [void] # # @raise [ArgumentError] if the value is not a `major.minor.patch` version string # def validate_version_format!(version, context = nil) return if version.is_a?(String) && version.match?(/\A\d+\.\d+\.\d+\z/) subject = context || 'a version' raise ArgumentError, "requires_git_version expects #{subject} to be a 'major.minor.patch' version string, " \ "got: #{version.inspect}" end end
@!attribute [r] skip_version_validation?
@return [Boolean] whether this command skips version validation
Source
# File lib/git/commands/base.rb, line 196 def validate_version_format!(version, context = nil) return if version.is_a?(String) && version.match?(/\A\d+\.\d+\.\d+\z/) subject = context || 'a version' raise ArgumentError, "requires_git_version expects #{subject} to be a 'major.minor.patch' version string, " \ "got: #{version.inspect}" end
Validate a version string uses the required format
@param version [Object] value to validate
@param context [String, nil] argument name included in validation errors
@return [void]
@raise [ArgumentError] if the value is not a ‘major.minor.patch` version string
Private Class Methods
Source
# File lib/git/commands/base.rb, line 296 def capturing_opts(exec_opts) opts = exec_opts opts = opts.merge(normalize: false) unless normalize_captured_stdout? opts = opts.merge(chomp: false) unless chomp_captured_stdout? opts end
Build options for captured command output
@param exec_opts [Hash] execution options for the command line
@return [Hash] options for captured command output
Source
# File lib/git/commands/base.rb, line 333 def chomp_captured_stdout? true end
Whether {#execute_command} should chomp trailing newlines from ‘result.stdout` on the capturing path.
When ‘true` (the default), `command_capturing` strips the trailing newline from captured stdout. Override to return `false` in subclasses whose output must preserve trailing whitespace (e.g. `git show`, which can return blob content with significant trailing newlines).
This hook only affects the capturing path — when an ‘out:` execution option is present, stdout is streamed directly to the caller-supplied IO object and is never chomped regardless of this setting.
@return [Boolean]
Source
# File lib/git/commands/base.rb, line 349 def env {} end
Environment variable overrides to pass to the subprocess.
Returns an empty hash by default. Subclasses may override this method to inject process-level environment changes required for correctness (e.g. unsetting ‘GIT_INDEX_FILE` for worktree management commands).
The returned hash is merged with any environment overrides the caller supplies via the ‘env:` execution option — this hook’s values win on conflict so that safety invariants cannot be accidentally overridden.
@return [Hash] environment variable overrides (key: variable name, value: new value or nil to unset)
Source
# File lib/git/commands/base.rb, line 267 def execute_command(bound) exec_opts = execution_opts(bound) if exec_opts.key?(:out) @execution_context.command_streaming(*bound, **exec_opts, raise_on_failure: false) else @execution_context.command_capturing(*bound, **capturing_opts(exec_opts), raise_on_failure: false) end end
Execute a bound git command through the execution context
@param bound [Git::Commands::Arguments::Bound] bound command arguments
@return [Git::CommandLine::Result] the command result
Source
# File lib/git/commands/base.rb, line 283 def execution_opts(bound) caller_env = bound.execution_options.fetch(:env, {}) || {} merged_env = caller_env.merge(env) opts = bound.execution_options.except(:env) merged_env.empty? ? opts : opts.merge(env: merged_env) end
Build execution options for a bound command
@param bound [Git::Commands::Arguments::Bound] bound command arguments
@return [Hash] execution options for the command line
Source
# File lib/git/commands/base.rb, line 316 def normalize_captured_stdout? true end
Whether {#execute_command} should apply Ruby string normalization to ‘result.stdout` on the capturing path.
When ‘true` (the default), `command_capturing` normalizes the encoding of captured stdout. Override to return `false` in subclasses whose output is intrinsically binary (e.g. `git archive`), so that stdout bytes in `result.stdout` are returned unchanged.
This hook only affects the capturing path — when an ‘out:` execution option is present, stdout is streamed directly to the caller-supplied IO object and is never normalized or chomped regardless of this setting.
@return [Boolean]
Source
# File lib/git/commands/base.rb, line 491 def start_stdin_writer(content, writer) Thread.new do writer.write(content) unless content.empty? rescue Errno::EPIPE, IOError nil # subprocess closed stdin early ensure writer.close unless writer.closed? end end
Spawns a thread that writes content to writer then closes it. Rescues EPIPE/IOError so the thread exits cleanly when the subprocess closes its stdin early (e.g. on error exit before reading all input).
@param content [String] text to write to the pipe
@param writer [IO] write end of the pipe
@return [Thread] thread writing content to the pipe
Source
# File lib/git/commands/base.rb, line 425 def validate_class_version_constraint!(actual_version) constraint = self.class.git_version_constraint return unless constraint return if constraint.satisfied_by?(actual_version) raise Git::VersionError.new( subject: self.class, constraint: constraint, actual_version: actual_version ) end
Validate that git satisfies this command class’s version constraint
@param actual_version [Git::Version] installed git version
@return [void]
@raise [Git::VersionError] if the installed git version is unsupported
Source
# File lib/git/commands/base.rb, line 369 def validate_exit_status!(result) raise Git::FailedError, result unless allowed_exit_status_range.include?(result.status.exitstatus) end
Validate that a command result has an accepted exit status
@param result [Git::CommandLine::Result] command result to validate
@return [void]
@raise [Git::FailedError] if the result exit status is not accepted
Source
# File lib/git/commands/base.rb, line 407 def validate_floor_version!(actual_version) return if actual_version >= Git::MINIMUM_GIT_VERSION raise Git::VersionError.new( subject: 'The git gem', constraint: Git::VersionConstraint.new(min: Git::MINIMUM_GIT_VERSION, before: nil), actual_version: actual_version ) end
Validate that git satisfies the gem-wide minimum version
@param actual_version [Git::Version] installed git version
@return [void]
@raise [Git::VersionError] if the installed git version is too old
Source
# File lib/git/commands/base.rb, line 387 def validate_version!(exec_opts = {}) return if self.class.skip_version_validation? actual_version = @execution_context.git_version(timeout: exec_opts[:timeout]) # Floor check: fail-fast if git is too old for the gem itself validate_floor_version!(actual_version) # Class-level constraint check validate_class_version_constraint!(actual_version) end
Validate that the installed git version meets requirements
Raises Git::VersionError if:
-
The installed version is below Git::MINIMUM_GIT_VERSION (floor check)
-
The command has a class-level constraint that isn’t satisfied
Floor check always runs first and fails fast.
@param exec_opts [Hash] execution options used to query git version
@return [void]
@raise [Git::VersionError] if the installed git version is unsupported
Source
# File lib/git/commands/base.rb, line 472 def with_stdin(content) reader, writer = IO.pipe writer_thread = start_stdin_writer(content, writer) yield reader ensure reader.close unless reader.closed? writer_thread&.join end
Opens an in-memory IO pipe, spawns a background thread to write ‘content` to the write end (then close it), and immediately yields the read end. The write and close happen concurrently with the block.
The read end can be passed as the ‘in:` keyword to {Git::ExecutionContext#command_capturing} / {Git::CommandLine#run_with_capture}, connecting it directly to the spawned git process’s stdin without an intermediate file or shell heredoc. This is required because ‘Process.spawn` only accepts real IO objects with a file descriptor — `StringIO` does not work.
The threaded write prevents deadlocks when ‘content` exceeds the OS pipe buffer: the subprocess can drain the pipe concurrently while the writer thread continues writing.
Pass an empty string when the process should receive no input (e.g. when ‘–batch-all-objects` is used and git enumerates objects itself).
@example Feed bound object names to a git batch command
bound = args_definition.bind(*args, **kwargs) stdin_content = Array(bound.objects).map { |object| "#{object}\n" }.join with_stdin(stdin_content) do |reader| @execution_context.command_capturing('cat-file', '--batch-check', in: reader, raise_on_failure: false) end
@param content [String] text to write to the process’s stdin
@return [Object] the value returned by the block
@yield [reader] the read end of the pipe
@yieldparam reader [IO] the read end of the pipe; valid only for the
duration of the block
@yieldreturn [Object] the block’s return value, which becomes the method’s return value