Command Implementation — Reference

Detailed reference for Git::Commands::Base command classes. This file is loaded by subagents during the Command Implementation workflow.

Contents

Files to generate

For Git::Commands::Foo::Bar, all three files are required and must be created:

  • lib/git/commands/foo/bar.rb — the command class
  • spec/unit/git/commands/foo/bar_spec.rb — unit tests
  • spec/integration/git/commands/foo/bar_spec.rb — integration tests (mandatory, not optional)

Optional (first command in module):

  • lib/git/commands/foo.rb

Single class vs. sub-command namespace

Most git commands map to a single class. Split into a namespace module with multiple sub-command classes when the git command surfaces meaningfully different operations that have distinct call shapes or protocols.

When to use sub-commands

Split by operation — when the git command has named sub-actions whose option sets have little overlap (each sub-action would have mostly dead options if they shared one class):

git stash push / pop / apply / drop / list / show
git tag --create / --delete / --list
git worktree add / list / remove / move

Split by stdin protocol — when one variant reads from stdin and another does not (even if the git command is the same). The stdin variant needs a call override that uses Base#with_stdin; mixing that with a no-stdin path in one class produces an awkward interface.

Do NOT split by output format / output mode

Output-mode flags are NOT a reason to create separate subclasses. When a git command supports multiple output formats via flags (--patch, --numstat, --raw, --format=…, etc.), express them as flag_option or value_option entries in a single class. The facade passes the desired format flags explicitly at call time:

# ❌ Anti-pattern: one class per output format
class Diff::Patch < Git::Commands::Base; end    # literal '--patch'
class Diff::Numstat < Git::Commands::Base; end  # literal '--numstat'
class Diff::Raw < Git::Commands::Base; end      # literal '--raw'

# ✅ Correct: one class, output mode as options
class Diff < Git::Commands::Base
  arguments do
    literal 'diff'
    flag_option :patch
    flag_option :numstat
    flag_option :raw
    ...
  end
end

# lib/git/repository/diffing.rb — parser contract is visible and auditable:
Git::Commands::Diff.new(@execution_context).call(patch: true, numstat: true, ...)

The same applies for --format=<string>, --pretty=<fmt>, --no-color, and all other parser-contract options. Declare them in the DSL; the facade passes them.

Remember: literal entries are only for operation selectors — fixed flags that define which git sub-operation the class represents (e.g., literal 'stash', literal 'show', literal '--delete'). Output-format flags are not operation selectors.

When to keep a single class

  • Different output modes (--patch, --numstat, --raw): always use a single class; expose modes as DSL options.
  • Minor option variations that share the same argument set.
  • When the "special mode" is just 1–2 flags — use flag_option/value_option.
  • When callers would always need multiple modes together (the facade composes them).

Naming sub-command classes

Prefer user-oriented names (what the caller gets back) over flag names (implementation detail the caller shouldn't need to know):

# Avoid — leaks implementation detail
CatFile::BatchCheck / CatFile::Batch

# Prefer — describes the result from the caller's perspective
CatFile::ObjectMeta / CatFile::ObjectContent

Two hard constraints:

  • Never name a sub-command class Object — it shadows Ruby's ::Object base class anywhere that constant is looked up inside the namespace.
  • Never use the *Info or *Result suffix on command classes — those suffixes are reserved for parsed result structs (BranchInfo, TagInfo, BranchDeleteResult) which live in the top-level Git:: namespace, not in Git::Commands::*. A reader seeing CommandFoo::BarInfo expects a data struct, not a class that runs a subprocess.

Namespace module template

When splitting, create a bare namespace module file (foo.rb) — no class — matching the pattern of cat_file.rb. The file has three required sections in this order: require_relative lines → module body with YARD → empty module Foo block.

Required elements (all mandatory):

  1. # frozen_string_literal: true magic comment
  2. One require_relative line per sub-command file, in the order the sub-commands appear in the @see bullet list
  3. A one-line summary (what git foo does overall)
  4. A "This module contains command classes split by…" paragraph with a bullet for every sub-command class using {Foo::Bar} YARD links followed by and a short description
  5. At least two @example blocks — one per sub-command class; each example should demonstrate the most common (non-error-path) call using a local variable named cmd and lib as the constructor argument
  6. @see https://git-scm.com/docs/git-foo git-foo documentation
  7. @api private
  8. Empty module Foo + end block (no methods, no constants)

Tag ordering inside the YARD comment block:

# One-line summary.
#
# This module contains command classes split by ...:
#
# - {Foo::Bar} — short description
# - {Foo::Baz} — short description
#
# @example <Short description of the Bar use case>
#   cmd = Git::Commands::Foo::Bar.new(lib)
#   cmd.call(...)
#
# @example <Short description of the Baz use case>
#   cmd = Git::Commands::Foo::Baz.new(lib)
#   cmd.call(...)
#
# @see https://git-scm.com/docs/git-foo git-foo documentation
#
# @api private

Full template:

# frozen_string_literal: true

require_relative 'foo/bar'
require_relative 'foo/baz'

module Git
  module Commands
    # One-line summary of what `git foo` does.
    #
    # This module contains command classes split by [reason for split]:
    #
    # - {Foo::Bar} — what Bar does
    # - {Foo::Baz} — what Baz does
    #
    # @example <Short description for bar>
    #   cmd = Git::Commands::Foo::Bar.new(lib)
    #   cmd.call(...)
    #
    # @example <Short description for baz>
    #   cmd = Git::Commands::Foo::Baz.new(lib)
    #   cmd.call(...)
    #
    # @see https://git-scm.com/docs/git-foo git-foo documentation
    #
    # @api private
    #
    module Foo
    end
  end
end

Each sub-command file adds @see Git::Commands::Foo to link back to the parent module's overview.

Checklist for reviewing an existing namespace module:

  • [ ] # frozen_string_literal: true is present
  • [ ] All sub-command files are require_relative'd (no require 'git/commands/...')
  • [ ] Bullet list covers every sub-command class in the namespace with {Foo::Bar} YARD links
  • [ ] @api private is present
  • [ ] @see link points to git-scm.com/docs/git-foo documentation
  • [ ] At least one @example block per sub-command class
  • [ ] Each example uses cmd = Git::Commands::Foo::Bar.new(lib) form (variable cmd, arg lib)
  • [ ] Tag order: summary → bullet list → @example blocks → @see@api private
  • [ ] No class is defined inside the module file; the module Foo block is empty

Architecture contract

For migrated commands, the expected structure is:

require 'git/commands/base'

class SomeCommand < Git::Commands::Base
  arguments do
    ...
  end

  # optional — only when introduced after Git::MINIMUM_GIT_VERSION
  requires_git_version '2.29.0'

  # optional for non-zero successful exits
  # reason comment
  allow_exit_status 0..1

  # @!method call(*, **)
  #
  #   @overload call(**options)
  #
  #     YARD docs for this command's call signature.
  #
  #     @return [Git::CommandLineResult]
end

The @!method directive is the correct YARD form when the class contains no explicit def call — YARD uses it to render per-command docs on the inherited call method. When the class does define def call explicitly, place YARD docs directly above def call and omit the @!method directive.

Shared behavior lives in Base:

  • binds arguments
  • calls @execution_context.command_capturing(*args, **args.execution_options, raise_on_failure: false)
  • raises Git::FailedError unless exit status is in allowed range (0..0 default)

Structural requirements:

  • Class inherits from Git::Commands::Base
  • File requires git/commands/base (not git/commands/arguments)
  • Has exactly one arguments do declaration
  • Does not define command-specific initialize that only assigns @execution_context
  • require statements are limited to files actually used within the command class file itself — do not carry over require entries that belong only to the Git::Repository::* facade or parser layer

Command template (Base pattern)

The @note annotation in the class-level docs must record the latest git release at the time of audit, not the "last updated in" version shown in the git-scm.com page footer (which only tracks when that command's docs last changed and can be much older than the current release). Determine the correct version by running:

bin/latest-git-version   # e.g. 2.53.0

Substitute the output for 2.XX.0 in the template below. The URL will resolve to the last docs update for that command even if the command docs did not change in that exact release.

# frozen_string_literal: true

require 'git/commands/base'

module Git
  module Commands
    module Foo
      # Summary...
      #
      # @example Typical usage
      #   bar = Git::Commands::Foo::Bar.new(execution_context)
      #   bar.call(...)
      #
      # @note `arguments` block audited against https://git-scm.com/docs/git-{command}/2.XX.0
      #
      # @see Git::Commands::Foo
      #
      # @see https://git-scm.com/docs/git-{command} git-{command}
      #
      # @api private
      class Bar < Git::Commands::Base  # never name the class Object
        arguments do
          # Group related options with section comments (e.g. # Output, # Safety)
          # NEVER add trailing inline comments (e.g. `# --verbose`) to DSL entries;
          # the DSL is self-documenting and inline comments duplicate the YARD docs.
          # NEVER put YARD tags (@see, @param, @return, etc.) in comments inside this
          # block. A DSL call such as `flag_option :x` is not a documentable construct,
          # so YARD silently drops the whole comment (an orphaned doc comment that
          # Documentation/OrphanedDocComment flags). Put per-option references in the
          # matching @option tag (use continuation text for URLs) and command-wide
          # references in a class-level @see.
        end

        # Optional: for commands where non-zero exits are valid
        # rationale comment
        # allow_exit_status 0..1

        # @!method call(*, **)
        #
        #   @overload call(operand = nil, *rest, **options)
        #
        #     Execute the git ... command.
        #
        #     @param operand [String, nil] (nil) short description without trailing period
        #
        #       Continuation paragraph separated by a blank comment line. Only needed
        #       when the short description alone is insufficient.
        #
        #     @param options [Hash] command options
        #
        #     @option options [Boolean, nil] :simple_flag (nil) one-sentence description without period
        #
        #     @option options [Boolean, Integer, nil] :force (nil) short description without period
        #
        #       When an integer is given, the flag is repeated that many times (up to the
        #       configured `max_times:` limit).
        #
        #       Alias: :f
        #
        #     @option options [Boolean, String, nil] :complex_flag (nil) short description without period
        #
        #       Continuation: explain the `true`/`false`/string forms here, each separated by
        #       a blank comment line from the short description above.
        #
        #       Alias: :f
        #
        #     @return [Git::CommandLineResult] the result of calling `git ...`
        #
        #     @raise [ArgumentError] if unsupported options are provided
        #
        #     @raise [Git::FailedError] if git exits with a non-zero exit status
      end
    end
  end
end

This template uses no explicit def call — the @!method YARD directive attaches per-command docs to the inherited call method. Use this form for simple commands where no pre-call logic is needed.

@raise wording — always use the canonical generic form that matches the command's declared exit-status range. Never enumerate specific failure causes (e.g. "if the branch doesn't exist"). Use:

allow_exit_status Canonical @raise wording
none declared (default 0..0) if git exits with a non-zero exit status
allow_exit_status 0..1 if git exits outside the allowed range (exit code > 1)
allow_exit_status 0..N if git exits outside the allowed range (exit code > N)

YARD tag formatting rules (short descriptions, continuation paragraphs, punctuation) are defined in the YARD Documentation skill. The template above demonstrates the correct form.

#call override guidance

Most commands use only a # @!method call(*, **) YARD directive with no explicit def call — the inherited Base#call handles binding, execution, and exit-status validation automatically. Do not add def call(*, **) = super or def call(*, **) / super / end for commands that need no custom logic; it adds no behavior and conflicts with the @!method directive.

Override call only when the command needs:

  1. Input validation the DSL cannot express — per-argument validation parameters (required:, type:, allow_nil:, etc.) and operand format validation belong in arguments do. Cross-argument constraint methods are generally not declared; git validates its own option semantics. The narrow exception is arguments git cannot observe in its argv: if an argument is skip_cli: true and never reaches git's argv, git cannot detect incompatibilities — use conflicts and/or requires_one_of in the DSL (e.g., cat-file --batch uses both because :objects is skip_cli: true). Do not raise ArgumentError manually for things the DSL can express via a constraint declaration.

  2. stdin feeding — batch protocols (--batch, --batch-check) via Base#with_stdin

  3. Non-trivial option routing — build different argument sets based on which options are present

  4. Action-option-with-optional-value — when the command's primary action is expressed as an option with an optional value (man-page notation: --flag[=<value>]). The DSL entry uses flag_or_value_option :name, inline: true, type: [TrueClass, String] and the override maps a positional call API onto the keyword:

    def call(value = true, *, **)
      super(*, **, option_name: value)
    end
    

When overriding:

  • Bind arguments via args_definition.bind(...) — do not reimplement binding
  • Delegate exit-status handling to validate_exit_status! — do not reimplement
  • Do not call super after manual binding; use @execution_context.command_capturing directly

DSL defaults:

Defaults defined in the DSL (e.g., operand :paths, default: ['.']) are applied automatically during args_definition.bind(...) — do not set defaults manually in call.

When the command requires an explicit def call override, place YARD doc comments directly above def call — do not use # @!method call(*, **) alongside an explicit override.

Overriding call — inline example

# @overload call(*objects, **options)
#
#   Execute the `git cat-file --batch` command.
#
#   @param objects [Array<String>] one or more object names
#
#   @param options [Hash] command options
#
#   @option options [Boolean, nil] :unordered (nil) unordered output
#
#   @return [Git::CommandLineResult] the result of calling `git cat-file --batch`
#
#   @raise [ArgumentError] if unsupported options are provided
#
#   @raise [Git::FailedError] if git exits with a non-zero exit status
def call(*objects, **options)
  bound = args_definition.bind(*objects, **options)
  with_stdin(Array(bound.objects).map { |o| "#{o}\n" }.join) do |reader|
    run_batch(bound, reader)
  end
end

def run_batch(bound, reader)
  result = @execution_context.command_capturing(*bound, in: reader, **bound.execution_options, raise_on_failure: false)
  validate_exit_status!(result)
  result
end

For stdin-driven commands where objects are domain inputs but not CLI argv, prefer modeling that contract in the DSL:

arguments do
  literal 'cat-file'
  literal '--batch-check'
  flag_option :batch_all_objects
  operand :objects, repeatable: true, skip_cli: true

  conflicts :objects, :batch_all_objects
  requires_one_of :objects, :batch_all_objects
end

Action-option-with-optional-value commands

When a git command's primary action is an option with an optional value (man-page notation: --flag[=<value>], e.g. git am --show-current-patch[=(diff|raw)]), use this pattern:

DSL:

arguments do
  literal 'am'
  flag_or_value_option :show_current_patch, inline: true, type: [TrueClass, String]
end

#call override — required to give callers a natural positional API:

# Show the patch currently being applied by `git am`
#
# @param value [true, String] when +true+ (default), emits +--show-current-patch+
#   (git's default behavior). Pass +"diff"+ or +"raw"+ to emit
#   +--show-current-patch=diff+ / +--show-current-patch=raw+.
#
# @return [Git::CommandLineResult] the result of the command
#
# @raise [Git::FailedError] if no am session is in progress
#
def call(value = true, *, **)
  super(*, **, option_name: value)
end

Where:

  • value = true — positional default; true emits --flag; a String emits --flag=value
  • * — forwards positional operands declared in the DSL (omit when the command has none)
  • ** — forwards keyword options to the DSL binder; unknown keywords raise ArgumentError
  • option_name: value placed last so the positional arg always takes precedence

The type: [TrueClass, String] on the DSL entry rejects false at bind time, removing the need for manual validation in the override.

When to use skip_cli on operand

Use operand ..., skip_cli: true when all of the following are true:

  • The value is part of the Ruby #call interface and should be bound/validated by the DSL
  • The value should remain accessible on bound (for cross-field constraints, helper methods, and documentation)
  • The value must not be emitted into CLI argv (for example, it is sent via stdin protocol)

Do not use skip_cli for execution-engine kwargs (timeout:, chdir:, etc.) — those belong to execution_option.

Key points:

  • in: requires a real IO object. Process.spawn only accepts objects with a file descriptor; StringIO does not work. Base#with_stdin handles this by opening an IO.pipe and spawning a background Thread that writes the content to the write end (then closes it). The threaded write prevents deadlocks when content exceeds the OS pipe buffer — the subprocess can drain the pipe concurrently. The thread rescues Errno::EPIPE / IOError so it exits cleanly if the subprocess closes stdin early. Pass an empty string when the process should receive no input (e.g. when a --batch-all-objects-style flag makes git enumerate objects itself).
  • Extract helpers like run_batch to stay within Rubocop Metrics/MethodLength and Metrics/AbcSize thresholds. Aim to keep call under ~10 lines.

Base#with_stdin mechanics

Base#with_stdin(content) opens an IO.pipe, spawns a background Thread that writes content to the write end (then closes it), and yields the read end as in: to the execution context. The threaded write prevents deadlocks when content exceeds the OS pipe buffer — the subprocess can drain the pipe concurrently. The thread also rescues Errno::EPIPE / IOError so it exits cleanly if the subprocess closes stdin early.

Use with_stdin instead of manual pipe management. StringIO cannot be used because Process.spawn requires a real file descriptor.

Example — batch stdin protocol (as used by git cat-file --batch):

def call(*, **)
  bound = args_definition.bind(*, **)
  with_stdin(Array(bound.objects).map { |object| "#{object}\n" }.join) do |stdin_r|
    run_batch(bound, stdin_r)
  end
end

Options completeness — consult the latest-version docs first

Before writing any DSL entries, use the documentation fetched during the Input phase and enumerate every option the latest-version docs describe.

requires_git_version convention

requires_git_version is a class-level declaration only. Individual options do not carry version annotations. The declaration must use a 'major.minor.patch' string (e.g., '2.29.0'), not a Git::Version or Range — pre-release versions are not supported.

Scenario Action
Command exists in Git::MINIMUM_GIT_VERSION Do not add requires_git_version
Command was introduced after Git::MINIMUM_GIT_VERSION Add requires_git_version '<version>' at the version the command was introduced

Options that were added to a command after Git::MINIMUM_GIT_VERSION are still scaffolded in the DSL — they are not omitted. When a caller passes such an option on an older git installation, git itself will produce its native "unknown option" error. This is acceptable and expected; the ruby-git library does not gate individual options by version.

For each option, make one of three decisions:

Decision Reason Action
Include All behavioral options — including output-format flags (--pretty=, --patch, --numstat, --name-only, etc.) and filtering/selection flags Add to arguments do
Exclude (wrong sub-action) Option belongs to a different sub-action than the one this class implements Omit — see Scoping options to sub-command classes below
Exclude (execution-model conflict) Requires TTY input or otherwise makes the command incompatible with non-interactive subprocess execution Omit — see Execution-model conflicts below

Group related options with a comment in the DSL (e.g. # Ref inclusion, # Date filtering, # Commit ordering). Follow the section groupings from the latest-version documentation — this makes it easy for a reviewer to cross-check against the docs.

Pairs and opposites: when the latest-version docs document --foo / --no-foo as explicit flags, model them as a single flag_option :foo, negatable: true rather than two separate DSL declarations. This registers both :foo (positive) and :no_foo (negative) as independent boolean keys that follow standard boolean semantics: true emits the flag, false/nil emits nothing. Use no_foo: true at the call site to emit --no-foo.

Short-flag aliases — cross-check the latest-version docs/source: before adding any short-flag alias (e.g. %i[dry_run n]), verify the alias character appears on the same option heading in the documentation or parser for the latest supported version (-n, --dry-run). Do not invent an alias that the latest-version sources do not document — in this DSL, short aliases are Ruby-keyword aliases for ergonomics and documentation parity, while CLI emission still follows the primary option name's flag spec. Symmetrically, every option the latest-version docs document with a short form must have an alias in the DSL (long name first: %i[dry_run n]).

as: is an escape hatch, not a default tool: treat as: as suspicious by default and use it only when the required argv cannot be expressed by the normal DSL mapping plus existing modifiers (negatable:, inline:, as_operand:, max_times:, etc.). If a plain symbol, alias, or first-class modifier can express the same behavior, prefer that. In particular, do not use as: to encode repeated flags now that max_times: exists.

inline: true for =<value> options: when the latest-version docs show --option=<value> (with an =), the DSL entry must include inline: true regardless of whether the DSL method is value_option or flag_or_value_option. Without it, the value is emitted as a separate token (--option value) instead of the expected inline form (--option=value). Check every value_option and flag_or_value_option entry against the latest-version signature.

Constraint declarations are generally not used in command classes. Do not add conflicts, requires, requires_one_of, requires_exactly_one_of, forbid_values, or allowed_values declarations to command classes. Git is the single source of truth for its own option semantics. There are two narrow exceptions:

  1. skip_cli: true arguments — the argument never reaches git's argv, so git cannot detect incompatibilities and constraint declarations are appropriate (see the cat-file --batch example above: :objects is skip_cli: true, so git never sees it and cannot detect the conflict or the absent-both case).

  2. Git-visible arguments that cause silent data loss — if a combination of git-visible arguments causes git to silently discard data (no error, wrong result), a conflicts declaration MAY be added with: a code comment explaining why, a reference to the git version(s) where the behavior was verified, and a test. As of this writing, no such case has been identified.

  3. Mode-scoped flags explicitly constrained by the git docs — if the git documentation explicitly states that a flag only applies to certain modes or option combinations (e.g., --allow-unknown-type is documented as "Allow -s or -t to query broken/corrupt objects of unknown type"), a requires_one_of :mode_a, :mode_b, when: :flag declaration is appropriate. Add a DSL comment noting the constraint and a unit test asserting the ArgumentError.

    # Allow -t and -s to query broken or corrupt objects of unknown type;
    # rejected by git in any other mode — enforced by constraint below
    flag_option :allow_unknown_type
    # ...
    requires_one_of :t, :s, when: :allow_unknown_type
    

See redesign/3_architecture_implementation.md Insight 6 for the full policy.

This step is required. A command class that only exposes the options that happen to be used today in the Git::Repository::* facade is incomplete — callers of the future API should not need to re-open the docs just because the scaffold only covered current usage.

Scoping options to sub-command classes

When a git command is split into sub-command classes (e.g., Branch::Create, Branch::List, Branch::Delete), each class must include only the options that apply to the sub-action it implements. Do not enumerate every option on the man page — most git commands document options for all modes on a single page, and adding options that belong to a different mode produces a class that accepts arguments git will reject or misinterpret.

How to determine which options belong to a sub-action:

  1. Read the SYNOPSIS — git man pages list separate SYNOPSIS lines per mode (e.g., git branch [--list], git branch -d, git branch -m). Only options shown on the SYNOPSIS line for the target sub-action are candidates.

  2. Cross-reference the DESCRIPTION and OPTIONS sections — some options are described generally but only apply to specific modes. Check each option's description for phrases like "only useful with --list" or "when used with -d". If the docs explicitly tie an option to a different mode, exclude it.

  3. Common/shared options — options that appear on every SYNOPSIS line or are described as applying to the command as a whole (e.g., --quiet, --verbose) should be included in every sub-command class where they are meaningful.

Example — git branch:

Option Create List Delete Move/Copy
--track Yes
--force Yes Yes Yes
--sort Yes
--format Yes
--merged Yes
--quiet Yes Yes
--color Yes

This rule applies only when the command is split into sub-command classes. For single-class commands, include all options as described in the decision table above.

Execution-model conflicts

Command classes are neutral — they never hardcode policy choices. Policy defaults (no_edit: true, no_progress: true, etc.) belong to the Git::Repository::* facade.

Anti-pattern: literal '--no-edit' inside a command class.

Correct pattern: flag_option :edit, negatable: true in the command; no_edit: true passed from the facade call site.

The only options to exclude from the DSL are those that conflict with non-interactive subprocess execution:

  • --interactive / -i — requires a TTY
  • --patch (interactive form, e.g. git add -p) — requires TTY prompts
  • Any option requiring stdin/TTY interaction the library cannot provide

Note on --patch: In git add -p it opens an interactive session (exclude). In git diff --patch it selects a non-interactive output format (include). Evaluate per-command, not globally.

--edit / --no-edit: Model as flag_option :edit, negatable: true. Do not hardcode literal '--no-edit'. Pass no_edit: true from the facade call site. See "Command-layer neutrality" in CONTRIBUTING.md.

--verbose/-v and --quiet/-q: include these unless their git implementation requires interactive I/O.

end_of_options placement

Determine placement based on whether the SYNOPSIS explicitly shows --. See the Review Arguments DSL checklist for the full decision tree.

Rule 1 — SYNOPSIS shows --: mirror the SYNOPSIS

When the SYNOPSIS explicitly shows -- between operand groups (e.g., [<tree-ish>] [--] [<pathspec>...]), place end_of_options in the same position the SYNOPSIS shows it — after the pre--- operands, before the post--- group. See the Review Arguments DSL checklist ("Choosing the correct pathspec form") for how to model the post--- group (value_option ... as_operand: true).

Do not apply Rule 2 when Rule 1 applies.

# git diff [<tree-ish>] [--] [<pathspec>...]
operand :tree_ish                                             # BEFORE end_of_options
end_of_options                                                # mirrors SYNOPSIS position
value_option :pathspec, as_operand: true, repeatable: true    # AFTER end_of_options

Rule 2 — SYNOPSIS does NOT show --: protect operands from flag misinterpretation

Insert end_of_options immediately before the first operand whenever any flag_option, value_option, flag_or_value_option, key_value_option, or custom_option appears earlier in the arguments do block. This prevents operands from being misinterpreted as flags when a caller passes a value that starts with -.

literal entries are never the trigger — regardless of whether their value is option-style (e.g. literal '--delete') or a plain subcommand word (e.g. literal 'remove'). Only the five DSL option methods above matter.

# ✅ Correct — flag_option triggers Rule 2; end_of_options inserted before first operand
arguments do
  literal 'remote'
  literal 'rename'
  flag_option :progress, negatable: true   # ← this triggers Rule 2

  end_of_options

  operand :old, required: true
  operand :new, required: true
end

# ✅ Not needed — only literal entries precede the operand; no DSL option-flag methods
arguments do
  literal 'remote'
  literal 'remove'
  operand :name, required: true  # no DSL option methods → not required
end

end_of_options is always safe to add even when not strictly required. Omit it by convention when neither rule applies: it adds no defensive value and produces unnecessarily verbose command lines (e.g. git remote remove -- origin instead of git remote remove origin). When in doubt, add it — the Review Arguments DSL skill flags a missing end_of_options as an error when options appear before operands.

Exit status guidance

  • Default: no declaration needed (0..0 from Base)
  • Non-default: declare allow_exit_status <range> and add rationale comment

Examples:

# git diff exits 1 when differences are found (not an error)
allow_exit_status 0..1
# fsck uses exit codes 0-7 as bit flags for findings
allow_exit_status 0..7

Facade delegation and policy options

The command class is only half the story. After scaffolding the command, you must also write (or update) the Git::Repository::* facade method that delegates to it. The facade sets safe policy defaults at each call site — no_edit: true, no_progress: true, etc. — not as literal entries inside the command class. See "Command-layer neutrality" in CONTRIBUTING.md.

Policy defaults fall into two categories (see also facade-implementation/REFERENCE.md):

  • Fixed policy defaults (no_edit: true, no_progress: true, no_color: true, format strings): set unconditionally and not included in ALLOWED_OPTS. assert_valid_opts rejects any caller-supplied value for these keys, enforcing the policy. They are not part of the public API.
  • Overridable policy defaults (e.g., verbose: false): included in ALLOWED_OPTS. The facade sets a sensible default but callers may override it by passing a value that goes through **opts.
# lib/git/repository/remote_operations.rb — facade method for `git pull`

PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze

def pull(remote = nil, branch = nil, opts = {})
  raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?

  assert_valid_opts(opts, PULL_ALLOWED_OPTS)
  allowed_opts = opts.slice(*PULL_ALLOWED_OPTS)
  positional_args = [remote, branch].compact
  # no_edit: true is the non-interactive default (see CONTRIBUTING.md)
  Git::Commands::Pull.new(@execution_context).call(*positional_args, no_edit: true, **allowed_opts).stdout
end

Key points for the facade method:

  • Filter options — declare an ALLOWED_OPTS constant listing only the options the public API accepted at v4.3.0. Use assert_valid_opts + opts.slice to prevent accidental API expansion.
  • Pass policy options as safe defaultsno_edit: true, no_progress: true, etc. Fixed policy defaults go directly in the command call (not in ALLOWED_OPTS). Overridable policy defaults are placed before **opts in the command call so the caller's value wins on key collision, and are included in ALLOWED_OPTS. Add a comment explaining why (e.g., # non-interactive default).
  • Return the legacy type — typically .stdout or a parsed struct, not CommandLineResult.

Internal compatibility contract

This is the canonical location for the internal compatibility contract. Other skills reference this section rather than duplicating it.

Ensure refactors preserve these contract expectations:

  • constructor shape remains initialize(execution_context) (inherited from Base)
  • command entrypoint remains call(*, **) at runtime (via Base#call)
  • argument-definition metadata remains available via args_definition

If an intentional deviation exists, require migration notes/changelog documentation.

Phased rollout requirements

This is the canonical location for phased rollout requirements. Other skills reference this section rather than duplicating the full checklist.

For migration PRs, verify process constraints:

  • changes are on a feature branch — never commit or push directly to main
  • migration slice is scoped (pilot or one family), not all commands at once
  • each slice is independently revertible
  • refactor-only changes are not mixed with unrelated behavior changes
  • quality gates pass for the slice — discover tasks via bundle exec ruby -e "require 'rake'; load 'Rakefile'; puts Rake::Task['default:parallel'].prerequisites" and run each individually via bundle exec rake <task>, fixing failures before advancing

Common failures

Policy/output-control flag hardcoded as literal (neutrality violation)

literal entries for output-control, editor-suppression, progress, or verbose flags inside a command class violate the neutrality principle. The command class must model the git CLI faithfully; the facade sets safe defaults and callers may override them.

Symptom: the command class contains one or more of:

# ❌ Any of these are neutrality violations
literal '--no-edit'
literal '--verbose'
literal '--no-progress'
literal '--no-color'
literal '--porcelain'

Fix: convert each to a DSL option and pass the policy value from the facade:

# ✅ In the command class — neutral DSL declaration
flag_option :edit, negatable: true
flag_option :progress, negatable: true
flag_option :verbose
value_option :format

# ✅ In Git::Repository::* — facade passes the policy value explicitly
Git::Commands::Pull.new(@execution_context).call(no_edit: true, no_progress: true)
Git::Commands::Mv.new(@execution_context).call(*args, verbose: true)
Git::Commands::Fsck.new(@execution_context).call(no_progress: true)

See "Command-layer neutrality" in CONTRIBUTING.md for the full policy.

Unnecessary def call override

Do not add def call(*, **) = super or def call(*, **) / super / end for commands that need no custom logic; it adds no behavior and conflicts with the @!method directive.

execution_option for fixed kwargs

execution_option must not be used for kwargs whose value must be unconditionally fixed regardless of caller input. If a kwarg always has a specific required value (e.g. chomp: false for commands returning raw content where trailing newlines are data), hardcode it in a def call override instead — exposing it via execution_option would allow callers to override a value that must never change.

Unnecessary require statements

A command class file should only require what it actually uses. The canonical example is parser requires: require 'git/parsers/foo' is needed by the Git::Repository::* facade but not by the command class itself — the command class just runs git and returns CommandLineResult.

# ❌ Command class does not use Git::Parsers::Branch
require 'git/parsers/branch'
require 'git/commands/base'

# ✅ Only what the file actually uses
require 'git/commands/base'

Review mode: flag any require beyond git/commands/base (and git/commands/branch for sub-command files) unless a constant from that file is referenced in the command class body.

Update mode: remove flagged require statements.

Other common failures

  • lingering ARGS = Arguments.define constant and custom #call
  • command-specific duplicated exit-status checks instead of allow_exit_status
  • missing rationale comment for allow_exit_status
  • missing YARD directive (# @!method call(*, **))
  • call override that reimplements Base#call logic instead of delegating to validate_exit_status!
  • using a manual IO.pipe inline instead of Base#with_stdin for stdin-feeding commands
  • migration PR scope too broad (not phased)