git 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/CONTRIBUTING.md +185 -165
  4. data/LICENSE +1 -1
  5. data/README.md +151 -151
  6. data/git.gemspec +35 -3
  7. data/lib/git/command_line/base.rb +1 -2
  8. data/lib/git/commands/base.rb +1 -1
  9. data/lib/git/commands/cat_file/raw.rb +8 -7
  10. data/lib/git/repository/branching.rb +79 -5
  11. data/lib/git/version.rb +1 -1
  12. metadata +5 -94
  13. data/.claude/commands/address-copilot-reviews.md +0 -14
  14. data/.claude/settings.json +0 -16
  15. data/.claude/skills +0 -1
  16. data/.commitlintrc.yml +0 -38
  17. data/.dockerignore +0 -27
  18. data/.github/copilot-instructions.md +0 -30
  19. data/.github/hooks/bin-setup-on-worktree.json +0 -11
  20. data/.github/hooks/run-bin-setup-once.sh +0 -20
  21. data/.github/issue_template.md +0 -15
  22. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +0 -188
  23. data/.github/pull_request_template.md +0 -21
  24. data/.github/skills/breaking-change-analysis/SKILL.md +0 -99
  25. data/.github/skills/ci-cd-troubleshooting/SKILL.md +0 -264
  26. data/.github/skills/command-implementation/REFERENCE.md +0 -994
  27. data/.github/skills/command-implementation/SKILL.md +0 -230
  28. data/.github/skills/command-test-conventions/SKILL.md +0 -664
  29. data/.github/skills/command-yard-documentation/SKILL.md +0 -434
  30. data/.github/skills/dependency-management/SKILL.md +0 -72
  31. data/.github/skills/development-workflow/SKILL.md +0 -512
  32. data/.github/skills/facade-implementation/REFERENCE.md +0 -837
  33. data/.github/skills/facade-implementation/SKILL.md +0 -269
  34. data/.github/skills/facade-test-conventions/SKILL.md +0 -391
  35. data/.github/skills/facade-yard-documentation/SKILL.md +0 -435
  36. data/.github/skills/make-skill-template/SKILL.md +0 -226
  37. data/.github/skills/pr-readiness-review/SKILL.md +0 -205
  38. data/.github/skills/project-context/SKILL.md +0 -306
  39. data/.github/skills/pull-request-review/SKILL.md +0 -168
  40. data/.github/skills/rebase/SKILL.md +0 -148
  41. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +0 -131
  42. data/.github/skills/release-management/SKILL.md +0 -125
  43. data/.github/skills/resolve-feedback/SKILL.md +0 -288
  44. data/.github/skills/review-arguments-dsl/CHECKLIST.md +0 -788
  45. data/.github/skills/review-arguments-dsl/SKILL.md +0 -214
  46. data/.github/skills/review-cross-command-consistency/SKILL.md +0 -139
  47. data/.github/skills/reviewing-skills/SKILL.md +0 -214
  48. data/.github/skills/rspec-unit-testing-standards/SKILL.md +0 -685
  49. data/.github/skills/tdd-refactor-step/SKILL.md +0 -236
  50. data/.github/skills/test-debugging/SKILL.md +0 -161
  51. data/.github/skills/yard-documentation/SKILL.md +0 -981
  52. data/.github/skills/yard-documentation/element-rules.md +0 -162
  53. data/.github/skills-deprecated/README.md +0 -21
  54. data/.github/skills-deprecated/extract-command-from-lib/SKILL.md +0 -487
  55. data/.github/skills-deprecated/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +0 -22
  56. data/.github/skills-deprecated/extract-facade-from-base-lib/SKILL.md +0 -600
  57. data/.github/skills-deprecated/review-backward-compatibility/SKILL.md +0 -275
  58. data/.github/workflows/continuous_integration.yml +0 -358
  59. data/.github/workflows/enforce_conventional_commits.yml +0 -35
  60. data/.github/workflows/experimental_continuous_integration.yml +0 -59
  61. data/.github/workflows/release.yml +0 -52
  62. data/.github/workflows/warm_bundler_caches.yml +0 -82
  63. data/.gitignore +0 -30
  64. data/.husky/commit-msg +0 -1
  65. data/.husky/pre-commit +0 -13
  66. data/.release-please-config.json +0 -36
  67. data/.release-please-manifest.json +0 -3
  68. data/.rspec +0 -2
  69. data/.rubocop.yml +0 -44
  70. data/.rubocop_todo.yml +0 -30
  71. data/.yard-lint.yml +0 -75
  72. data/CLAUDE.md +0 -11
  73. data/Gemfile +0 -22
  74. data/Rakefile +0 -41
  75. data/docker/test/Dockerfile +0 -32
  76. data/docker/test/docker-compose.yml +0 -0
  77. data/package.json +0 -10
  78. data/redesign/1_architecture_existing.md +0 -102
  79. data/redesign/2_architecture_redesign.md +0 -449
  80. data/redesign/3_architecture_implementation.md +0 -1623
  81. data/redesign/Phase 4 - Step A.md +0 -366
  82. data/redesign/Phase 4 - Step B.md +0 -921
  83. data/redesign/Phase 4 - Step C.md +0 -833
  84. data/redesign/beta_release.md +0 -107
  85. data/redesign/branch_parse_refactor_plan.md +0 -163
  86. data/redesign/c1a-public-api-scope.tsv +0 -256
  87. data/redesign/c1c2_audit.md +0 -566
  88. data/redesign/c1c2_bucket6_lib_orphans.md +0 -626
  89. data/redesign/config_design.rb +0 -501
  90. data/redesign/index.md +0 -34
  91. data/redesign/info_object_migration_plan.md +0 -126
  92. data/redesign/integration_test_analysis.md +0 -521
  93. data/redesign/phase-4-step-b-test-audit.tsv +0 -485
  94. data/redesign/remote_refactor_plan.md +0 -164
  95. data/redesign/reverse_dependencies.sql +0 -44
  96. data/tasks/gem_tasks.rake +0 -14
  97. data/tasks/npm_tasks.rake +0 -7
  98. data/tasks/rspec.rake +0 -111
  99. data/tasks/rubocop.rake +0 -5
  100. data/tasks/test_gem.rake +0 -12
  101. data/tasks/yard.rake +0 -57
@@ -1,664 +0,0 @@
1
- ---
2
- name: command-test-conventions
3
- description: "Conventions for writing and reviewing unit and integration tests for Git::Commands::* classes. Use when scaffolding new command tests or auditing existing ones."
4
- ---
5
-
6
- # Command Test Conventions
7
-
8
- Conventions for writing and reviewing unit and integration tests for
9
- `Git::Commands::*` classes.
10
-
11
- - [Related skills](#related-skills)
12
- - [Input](#input)
13
- - [Version-aware test scope](#version-aware-test-scope)
14
- - [Reference](#reference)
15
- - [Unit tests](#unit-tests)
16
- - [Cover these cases](#cover-these-cases)
17
- - [Expectations for command invocation](#expectations-for-command-invocation)
18
- - [`#initialize` — omit from command specs](#initialize--omit-from-command-specs)
19
- - [Unit test grouping](#unit-test-grouping)
20
- - [Integration tests](#integration-tests)
21
- - [Integration test grouping](#integration-test-grouping)
22
- - [Guard tests for options introduced after the minimum supported Git version](#guard-tests-for-options-introduced-after-the-minimum-supported-git-version)
23
- - [Additional integration conventions](#additional-integration-conventions)
24
- - [Shared conventions](#shared-conventions)
25
- - [Workflow](#workflow)
26
- - [Output](#output)
27
-
28
- ## Related skills
29
-
30
- - [RSpec Unit Testing Standards](../rspec-unit-testing-standards/SKILL.md) — baseline
31
- RSpec rules that govern all unit test structure, naming, setup, stubbing, and
32
- coverage; this skill adds command-specific conventions on top
33
- - [Review Arguments DSL](../review-arguments-dsl/SKILL.md) — verifying DSL entries
34
- match git CLI
35
- - [Command Implementation](../command-implementation/SKILL.md) — class
36
- structure, phased rollout gates, and internal compatibility contracts
37
- - [Command YARD Documentation](../command-yard-documentation/SKILL.md)
38
- — documentation completeness for command classes
39
-
40
- ## Input
41
-
42
- The invocation needs the unit and/or integration spec file(s) to review. Including
43
- the corresponding command source file provides useful context for verifying argument
44
- coverage.
45
-
46
- **Prerequisite:** Read the **entire** [RSpec Unit Testing
47
- Standards](../rspec-unit-testing-standards/SKILL.md) skill (line 1 through EOF)
48
- before beginning. It defines the baseline Rules 1–28 that this skill extends. Without
49
- it, MUST-level structural, naming, stubbing, and coverage checks will not be applied.
50
-
51
- ### Version-aware test scope
52
-
53
- Before deciding that test coverage is missing for an option, alias, or flag
54
- form, determine the repository's minimum supported Git version from project
55
- metadata. In this repository, `git.gemspec` declares `git 2.28.0 or greater`.
56
-
57
- Coverage expectations for CLI forms must be based on the minimum supported Git
58
- version, not only on the locally installed Git. Use version-matched upstream
59
- documentation first, version-matched upstream source when needed, and local
60
- `git <command> -h` output only as a supplemental check.
61
-
62
- Do not require tests for newer-version-only forms that are not supported by the
63
- minimum supported Git version. Symmetrically, if the local Git omits or
64
- abbreviates a form that is supported in the minimum version, tests should still
65
- cover the minimum-version behavior.
66
-
67
- ## Reference
68
-
69
- ### Unit tests
70
-
71
- Unit tests verify CLI argument building and command-layer behavior for each command.
72
-
73
- #### Cover these cases
74
-
75
- - Base invocation (no options): verify literals and return pass-through. Store the
76
- `.and_return` value in an `expected_result` variable and assert `expect(result).to
77
- eq(expected_result)` to verify that `#call` passes through what
78
- `execution_context.command_capturing` returns. This assertion belongs only in the
79
- base invocation test — do not repeat it in every test.
80
- - Each positional operand variation (e.g., single value, multiple values)
81
- - Each flag option, including aliases (e.g., `:force` and `:f`)
82
- - `max_times:` flag options: test with `true` (emits once) and the maximum integer
83
- (emits N times), plus each alias with `true`
84
- - Flag options combined with operands where meaningful (e.g., an option that modifies
85
- how operands are interpreted)
86
- - Value options with each accepted form (e.g., boolean `true` vs a string value like
87
- `'lines,cumulative'`)
88
- - Pathspecs or other repeatable/`end_of_options`-based operands, both alone and
89
- combined with preceding operands
90
- - Execution options forwarding where applicable (e.g., `timeout:`)
91
- - Exit-status behavior for commands using `allow_exit_status` with a non-default
92
- range: test that exit codes within the declared range return a result without
93
- raising, and that exit codes outside the range raise `FailedError`. For example, if
94
- the command declares `allow_exit_status 0..1`, test that exit codes 0 and 1
95
- succeed, and that exit codes 2 and 128 raise `FailedError`. Commands that only
96
- succeed at exit code 0 (the default) do not need a unit-level exit code test — the
97
- integration error-handling test covers that path.
98
- - Input validation (`ArgumentError`) for per-argument validation failures: unknown
99
- options, `required:` violations, `type:` mismatches, etc. Command classes generally
100
- do **not** declare cross-argument constraint methods (`conflicts`, `requires`,
101
- `requires_one_of`, `requires_exactly_one_of`, `forbid_values`, `allowed_values`,
102
- etc.) — git validates its own option semantics. The narrow exception is **arguments
103
- git cannot observe in its argv**: if an argument is `skip_cli: true`, it never
104
- reaches git's argv and git cannot detect incompatibilities — constraint
105
- declarations are appropriate and the resulting `ArgumentError` should be tested.
106
- See the validation delegation policy in `redesign/3_architecture_implementation.md`
107
- Insight 6.
108
-
109
- #### Expectations for command invocation
110
-
111
- Use the `expect_command_capturing` helper from `spec_helper.rb` (or
112
- `expect_command_streaming` for streaming commands) which automatically includes
113
- `raise_on_failure: false`:
114
-
115
- ```ruby
116
- expect_command_capturing('clone', '--', url, dir).and_return(command_result)
117
- ```
118
-
119
- When testing execution options, include forwarded keywords:
120
-
121
- ```ruby
122
- expect_command_capturing('clone', '--', url, dir, timeout: 30).and_return(command_result)
123
- ```
124
-
125
- These helpers expand to `expect(execution_context).to receive(:command_capturing)...`
126
- — `expect` rather than `allow` because the call itself (the correct arguments
127
- reaching git) is the behavior under test. See **Rule 19** in the [RSpec Unit Testing
128
- Standards](../rspec-unit-testing-standards/SKILL.md).
129
-
130
- ##### Expectations for stdin-feeding commands
131
-
132
- Commands that use `Base#with_stdin` pass an `IO` pipe read end as `in:` to
133
- `execution_context.command_capturing`. Unit tests must capture that IO object and
134
- assert its content. Use a block form on the `expect` to intercept keyword arguments:
135
-
136
- ```ruby
137
- # Helper defined in the spec file:
138
- def expect_batch_command(*extra_args, stdin_content: nil, **extra_opts) # rubocop:disable Metrics/AbcSize
139
- expect(execution_context).to receive(:command_capturing) do |*args, **kwargs|
140
- expect(args).to eq(['cat-file', '--batch-check', *extra_args])
141
- expect(kwargs).to include(raise_on_failure: false, **extra_opts)
142
- expect(kwargs[:in].read).to eq(stdin_content) if stdin_content
143
- command_result
144
- end
145
- end
146
-
147
- # Usage:
148
- it 'passes the object via stdin and runs --batch-check' do
149
- expect_batch_command(stdin_content: "HEAD\n")
150
- command.call('HEAD')
151
- end
152
-
153
- it 'writes each object on its own line to stdin' do
154
- expect_batch_command(stdin_content: "HEAD\nv1.0\nabc123\n")
155
- command.call('HEAD', 'v1.0', 'abc123')
156
- end
157
-
158
- it 'includes --batch-all-objects and writes nothing to stdin' do
159
- expect_batch_command('--batch-all-objects', stdin_content: '')
160
- command.call(batch_all_objects: true)
161
- end
162
-
163
- # git-invisible argument exception: :objects is skip_cli: true, so git never sees
164
- # it in argv and cannot detect these incompatibilities. Ruby must enforce them.
165
- # conflicts: can't pass objects AND bypass stdin; requires_one_of: must choose one.
166
- it 'raises when mutually exclusive DSL inputs are combined' do
167
- expect { command.call('HEAD', batch_all_objects: true) }
168
- .to raise_error(ArgumentError, /cannot specify :objects and :batch_all_objects/)
169
- end
170
- ```
171
-
172
- `kwargs[:in].read` works because `Base#with_stdin` writes to stdin on a background
173
- thread and yields the read end immediately; the `read` call blocks until the writer
174
- thread closes the pipe and EOF is reached, so the full content is returned. Test
175
- `stdin_content: ''` explicitly for the no-input case (e.g. `--batch-all-objects`) to
176
- confirm nothing is written.
177
-
178
- ##### What not to test
179
-
180
- Unit tests should exercise each **code path** through the command, not each possible
181
- **input value**. Avoid these patterns:
182
-
183
- - **`option: false` for any `flag_option`.** Passing `false` to a `flag_option`
184
- (negatable or non-negatable) produces no output — identical to the base invocation
185
- with no options. The "no arguments" test already covers this path. To exercise the
186
- negative form of a negatable flag, use the `no_` companion key: e.g.:
187
- `no_single_branch: true` emits `--no-single-branch`, which is a distinct code path
188
- worth testing.
189
- - **Repeating the return value assertion.** The base invocation test asserts
190
- `expect(result).to eq(expected_result)` once as a contract check. Do not repeat
191
- this assertion in other tests — one check per file is sufficient.
192
- - **Intermediate integers for `max_times:` flags.** When a flag declares
193
- `max_times: N`, test only `true` and the max integer N. Do not test intermediate
194
- values (e.g. `force: 1` when `max_times: 2`) — the DSL handles all valid integers
195
- uniformly and intermediate values exercise the same code path.
196
- - **String-variant pass-through tests.** Do not write multiple tests that pass
197
- different string values through the same positional argument or value option. Tests
198
- like "handles paths with spaces" and "handles paths with unicode" exercise the same
199
- code path — the command passes strings unchanged. One test per operand/option is
200
- sufficient.
201
- - **Multiple format variants for the same operand.** For example, a stash command
202
- that accepts a stash reference does not need separate tests for `stash@{0}`,
203
- `stash@{2}`, and `1` — they all flow through the same positional argument.
204
- - **Varying mocked stdout for the same invocation.** If the command has no output
205
- parsing, testing the same `#call` with different mocked stdout values exercises
206
- identical code. One test is sufficient unless the command parses or branches on the
207
- output.
208
-
209
- The `Arguments` DSL has its own comprehensive spec (`arguments_spec.rb`) that tests
210
- flag handling, value options, positionals, `end_of_options`, edge cases, and error
211
- conditions. Command specs should test that the command **uses** the DSL correctly
212
- (i.e., the right arguments reach `execution_context.command_capturing`), not re-test
213
- the DSL's own behavior.
214
-
215
- Two specific DSL re-test patterns that commonly appear but should be avoided:
216
-
217
- - **`end_of_options` protection tests (dash-prefixed operands).** When a command
218
- declares `end_of_options`, the existing operand tests already verify that `'--'`
219
- appears before operands in the expected argv sequence. Do **not** add a separate
220
- test that passes a dash-prefixed operand (e.g. `'-feature'`) to prove the
221
- separator prevents misinterpretation: a dash-prefixed string exercises the
222
- identical code path as any other string, and the DSL spec (`arguments_spec.rb`)
223
- already covers `end_of_options` protection. The command spec only needs to show
224
- `'--'` at the right position; the DSL spec demonstrates why that matters.
225
- - **`required:` operand rejection tests.** When a command declares
226
- `operand :name, required: true`, do not test that calling with no arguments
227
- raises `ArgumentError` — the DSL spec covers required-operand validation. The
228
- command spec should test what happens when the operand IS provided, not that
229
- the DSL reports missing-argument errors correctly.
230
-
231
- **Policy vs. interface testing:** Command classes are neutral, faithful
232
- representations of the git CLI. Their unit tests verify CLI argument building (the
233
- neutral interface), not policy enforcement. Tests should **not** hardcode policy
234
- assumptions — for example, a command spec should not always pass `no_edit: true` or
235
- expect `--no-edit` unless the test is specifically exercising that option.
236
-
237
- > **Anti-pattern:** every `it` block in a command spec passes `no_edit: true`,
238
- > `no_progress: true`, or `no_color: true` — this tests the facade's policy, not
239
- > the command's interface.
240
- >
241
- > **Correct pattern:** test each option independently (`it 'passes --no-edit
242
- > when no_edit is true'`); test the default (no option passed) separately. Policy
243
- > enforcement (which options the facade passes and why) is tested at the facade
244
- > layer (for example, `spec/unit/git/repository/remote_operations_spec.rb`).
245
-
246
- **Where to test policy enforcement:** Policy tests belong in the facade layer,
247
- not in command specs. When a `Git::Repository::*` facade method sets policy defaults like
248
- `no_edit: true` or `no_progress: true`, the corresponding facade unit spec
249
- (e.g. `spec/unit/git/repository/remote_operations_spec.rb`) should verify those defaults reach the command:
250
-
251
- ```ruby
252
- # spec/unit/git/repository/remote_operations_spec.rb — facade policy-default test
253
- describe '#pull' do
254
- let(:pull_command) { instance_double(Git::Commands::Pull) }
255
- let(:pull_result) { command_result('') }
256
-
257
- it 'adds no_edit: true and no_progress: true for non-interactive execution' do
258
- expect(Git::Commands::Pull).to receive(:new).with(execution_context).and_return(pull_command)
259
- expect(pull_command)
260
- .to receive(:call).with('origin', 'main', no_edit: true, no_progress: true).and_return(pull_result)
261
- described_instance.pull('origin', 'main')
262
- end
263
- end
264
- ```
265
-
266
- This separation ensures:
267
- - Command specs verify the **neutral interface** (every option works correctly)
268
- - Facade specs verify the **policy** (the right options are passed and why)
269
- - An AI sees exactly where each concern is tested and does not conflate them
270
-
271
- See "Command-layer neutrality" in CONTRIBUTING.md.
272
-
273
- #### `#initialize` — omit from command specs
274
-
275
- **Do not write a `describe '#initialize'` block in command specs.** This is a
276
- deliberate exception to Rule 2's SHOULD guidance for concrete subclasses. The full
277
- reasoning chain:
278
-
279
- 1. **Rule 2's `have_attributes` form requires public attributes.** `Base#initialize`
280
- stores `@execution_context` as a private instance variable with no `attr_reader`,
281
- so there is nothing to pass to `have_attributes`. The form that Rule 2 uses cannot
282
- be applied.
283
-
284
- 2. **The only fallback is `not_to raise_error`, which is a Rule 24 violation.**
285
- Asserting that `described_class.new(execution_context)` does not raise merely
286
- confirms the code runs — it is not an observable behavioral assertion.
287
-
288
- 3. **Both Rule 2 purposes are already satisfied by other means:**
289
- - *Documentation:* the `let(:command) { described_class.new(execution_context) }`
290
- declaration at the top of every spec documents the constructor signature
291
- as clearly as a dedicated block would.
292
- - *Accidental-override guard:* `let(:command)` is evaluated before every example.
293
- If a subclass accidentally introduced a `def initialize` with a different
294
- signature, every test in the file would immediately raise `ArgumentError` —
295
- providing the same protection a dedicated block would.
296
-
297
- 4. **`Base#initialize` is covered by `base_spec.rb`.** Command subclasses that do
298
- not override `#initialize` gain nothing from repeating it.
299
-
300
- **Required fix if found:** Remove any `describe '#initialize'` block that contains
301
- only `expect { described_class.new(execution_context) }.not_to raise_error` — it is
302
- a Rule 24 violation and provides no coverage value.
303
-
304
- #### Unit test grouping
305
-
306
- Unit tests are organized under `describe '#call'` with three sections:
307
-
308
- 1. **Argument building** (the bulk) — flat `context` blocks, one per option/operand
309
- variation. These are always present and come first.
310
- 2. **`context 'exit code handling'`** — only for commands with `allow_exit_status`
311
- ranges beyond `0..0`. Uses mocked exit codes via `command_result` helper to test
312
- that exit codes within the allowed range return a result and exit codes outside
313
- the range raise `FailedError`.
314
- 3. **`context 'input validation'`** — only for commands with validation rules. Covers
315
- unsupported options and required arguments that raise `ArgumentError`.
316
- Cross-argument constraints for git-visible arguments are not tested because
317
- command classes do not declare them. The exception is constraints on `skip_cli:
318
- true` arguments (e.g., `conflicts :objects, :batch_all_objects` and
319
- `requires_one_of :objects, :batch_all_objects`), which should be tested.
320
-
321
- The exit code and input validation blocks are optional — include them only when the
322
- command has those behaviors. They always appear at the end of `#call`, in that order.
323
-
324
- **Required fix if found:** The section names `'exit code handling'` and `'input
325
- validation'` are exact string literals — do not paraphrase. A context named
326
- `'with an unsupported option'` or `'when the option is invalid'` instead of
327
- `'input validation'` MUST be renamed. These names are load-bearing identifiers: they
328
- signal to reviewers at a glance which structural section they are looking at and what
329
- it may or may not contain.
330
-
331
- Unit test descriptions should be concise and action-oriented. Use descriptions like
332
- "includes the --cached flag", "passes both commits as operands", "combines commit
333
- with pathspecs".
334
-
335
- **Always use the emitted long-flag form in descriptions, never a short alias.** The
336
- DSL canonicalises aliases to the long form (e.g. `:q` → `--quiet`, `:f` → `--force`).
337
- Writing `'adds -q flag'` in an `it` description is misleading because the actual token
338
- asserted in the expectation is `'--quiet'`. Use `'adds --quiet flag'` instead.
339
-
340
- > **Exception to RSpec Unit Testing Standards Rules 11–12 (subject and let
341
- > ordering):** Command unit tests intentionally omit `subject` within `describe
342
- > '#call'`. Because each test exercises a different argument combination, there is no
343
- > single fixed call expressible as a shared `subject`. Use `let(:command)` at the
344
- > `RSpec.describe` level and call `command.call(...)` directly inside each `it`
345
- > block, overriding `let` inputs per `context` block as needed.
346
-
347
- **Example with all three sections:**
348
-
349
- ```ruby
350
- RSpec.describe Git::Commands::Branch::Delete do
351
- # Duck-type collaborator: command specs depend on the #command_capturing interface,
352
- # not a single concrete ExecutionContext class.
353
- let(:execution_context) { double('ExecutionContext') }
354
- let(:command) { described_class.new(execution_context) }
355
-
356
- describe '#call' do
357
- # Argument building — flat contexts
358
- context 'with single branch name' do
359
- it 'passes the branch name' do
360
- expected_result = command_result('Deleted branch feature.')
361
- expect_command_capturing('branch', '-d', 'feature').and_return(expected_result)
362
- result = command.call('feature')
363
- expect(result).to eq(expected_result)
364
- end
365
- end
366
-
367
- context 'with :force option' do
368
- # ...
369
- end
370
-
371
- # Exit code handling — only when command declares allow_exit_status
372
- context 'exit code handling' do
373
- it 'returns result for exit code 0' do
374
- # ... mock exit code 0, assert result returned ...
375
- end
376
-
377
- it 'returns result for exit code 1 (partial failure)' do
378
- # ... mock exit code 1, assert result returned ...
379
- end
380
-
381
- it 'raises FailedError for exit code > 1' do
382
- # ... mock exit code 128, assert FailedError raised ...
383
- end
384
- end
385
-
386
- # Input validation — only when command validates input
387
- context 'input validation' do
388
- it 'raises ArgumentError for unsupported options' do
389
- expect { command.call('branch', invalid: true) }
390
- .to raise_error(ArgumentError, /Unsupported options/)
391
- end
392
- end
393
- end
394
- end
395
- ```
396
-
397
- **Example with argument building only** (no custom exit codes, no validation):
398
-
399
- ```ruby
400
- RSpec.describe Git::Commands::Stash::Pop do
401
- # Duck-type collaborator: command specs depend on the #command_capturing interface,
402
- # not a single concrete ExecutionContext class.
403
- let(:execution_context) { double('ExecutionContext') }
404
- let(:command) { described_class.new(execution_context) }
405
-
406
- describe '#call' do
407
- context 'with no arguments' do
408
- # ...
409
- end
410
-
411
- context 'with stash reference' do
412
- # ...
413
- end
414
-
415
- context 'with :index option' do
416
- # ...
417
- end
418
- end
419
- end
420
- ```
421
-
422
- ### Integration tests
423
-
424
- Integration tests are minimal smoke tests that confirm the command executes
425
- successfully against a real git repository. They should NOT test git's output format,
426
- parsing behavior, or specific content of stdout — those concerns belong in parser
427
- specs and facade/end-to-end specs.
428
-
429
- Each integration spec file tests exactly **one command class**. Do not create
430
- multi-command workflow specs that chain commands together — that is the concern of
431
- facade or end-to-end tests.
432
-
433
- Integration tests should only cover:
434
-
435
- - A smoke test: calling with valid arguments returns a `CommandLineResult` with
436
- expected output (e.g., non-empty for commands that produce output)
437
- - Exit codes from real git: one test per success exit code, exercised through real
438
- git invocations that naturally produce each code. For example, for `git diff`:
439
- identical refs produce exit code 0 with empty output; differing refs produce exit
440
- code ≤1 with non-empty output. This confirms that real git returns the exit codes
441
- the command's `allow_exit_status` range expects.
442
- - Error handling: invalid input (e.g., a nonexistent ref) raises `FailedError`.
443
- **Every command must have at least one error handling test.** Even commands with
444
- non-default `allow_exit_status` ranges can be forced to fail (e.g., by removing
445
- `.git` to trigger exit code 128).
446
-
447
- **Do not** write integration tests that assert on git's output format (e.g., matching
448
- specific line patterns, status letters, or header syntax). The command's job is to
449
- pass the correct arguments to git and return the result — verifying git's formatting
450
- behavior is testing git, not the command. If a particular flag needs to be tested
451
- (e.g., `-M` for rename detection), verify the flag appears in the arguments via a
452
- unit test.
453
-
454
- > **Branch workflow:** Implement any new or updated tests on a feature branch. Never
455
- > commit or push directly to `main` — open a pull request when changes are ready to
456
- > merge.
457
-
458
- #### Integration test grouping
459
-
460
- Integration tests must be organized into two `context` blocks under `#call`:
461
-
462
- - `context 'when the command succeeds'` — smoke tests, option variations, and exit
463
- code variants
464
- - `context 'when the command fails'` — error handling tests (`FailedError`)
465
-
466
- This grouping provides a consistent structure across all command specs and makes it
467
- immediately clear which tests cover the happy path vs. error conditions.
468
-
469
- **Simple command example** (default exit code handling):
470
-
471
- ```ruby
472
- RSpec.describe Git::Commands::Add, :integration do
473
- include_context 'in an empty repository'
474
-
475
- subject(:command) { described_class.new(execution_context) }
476
-
477
- describe '#call' do
478
- context 'when the command succeeds' do
479
- it 'returns a CommandLineResult' do
480
- # ... valid invocation ...
481
- end
482
- end
483
-
484
- context 'when the command fails' do
485
- it 'raises FailedError with a nonexistent path' do
486
- # git's error message phrasing varies by version — anchor on the stable input value
487
- expect { command.call('nonexistent.txt') }
488
- .to raise_error(Git::FailedError, /nonexistent\.txt/)
489
- end
490
- end
491
- end
492
- end
493
- ```
494
-
495
- **Custom exit code example** (command declares `allow_exit_status`):
496
-
497
- ```ruby
498
- RSpec.describe Git::Commands::Diff::Numstat, :integration do
499
- include_context 'in a diff test repository'
500
-
501
- subject(:command) { described_class.new(execution_context) }
502
-
503
- describe '#call' do
504
- context 'when the command succeeds' do
505
- it 'returns exit code 0 with no differences' do
506
- result = command.call('initial', 'initial')
507
- expect(result.status.exitstatus).to eq(0)
508
- expect(result.stdout).to be_empty
509
- end
510
-
511
- it 'succeeds with differences found' do
512
- result = command.call('initial', 'after_modify')
513
- expect(result.status.exitstatus).to eq(1)
514
- expect(result.stdout).not_to be_empty
515
- end
516
- end
517
-
518
- context 'when the command fails' do
519
- it 'raises FailedError for invalid revision' do
520
- # git's error message phrasing varies by version — anchor on the stable input value
521
- expect { command.call('nonexistent-ref') }
522
- .to raise_error(Git::FailedError, /nonexistent-ref/)
523
- end
524
- end
525
- end
526
- end
527
- ```
528
-
529
- #### Guard tests for options introduced after the minimum supported Git version
530
-
531
- When an integration test exercises an option that was introduced after the minimum
532
- supported Git version (2.28.0), guard the example with
533
- `skip: unless_git(minimum_version, feature_description)` to prevent failures on
534
- installations that do not yet have the required Git version. The `unless_git` helper
535
- is defined in `spec/spec_helper.rb`:
536
-
537
- - Returns `false` when the installed Git meets the minimum version (tests run normally).
538
- - Returns a human-readable skip reason string when the installed Git is too old
539
- (RSpec skips the example).
540
-
541
- Apply the guard to individual `it` blocks when only some tests in a context require a
542
- newer version. Apply it to a `context` or `describe` block when **all** tests in that
543
- group require the same minimum version.
544
-
545
- ```ruby
546
- # ✅ Different options introduced in different git versions — guard each `it` individually
547
- it 'returns a CommandLineResult with the :verbose option',
548
- skip: unless_git('2.33.0', 'git worktree list --verbose') do
549
- # ...
550
- end
551
-
552
- it 'returns a CommandLineResult with the :z option combined with :porcelain',
553
- skip: unless_git('2.36.0', 'git worktree list --porcelain -z') do
554
- # ...
555
- end
556
-
557
- # ✅ All tests in the group require the same version — guard the context/describe block
558
- RSpec.describe Git::Commands::ShowRef::Exists, :integration,
559
- skip: unless_git('2.43.0', 'git show-ref --exists') do
560
- # ...
561
- end
562
- ```
563
-
564
- Determine the correct minimum version by checking version-matched upstream git
565
- documentation (e.g., `https://git-scm.com/docs/git-worktree/2.33.0`) rather than
566
- relying only on the locally installed git binary.
567
-
568
- #### Additional integration conventions
569
-
570
- **Always specify `initial_branch: 'main'` when calling `Git.init` in test setup.**
571
- The `in an empty repository` shared context already does this for the primary repo,
572
- but tests that create *additional* repositories in a `before` block (e.g., a bare
573
- remote, a second clone target) must pass `initial_branch: 'main'` explicitly to
574
- `Git.init`. Without it, the repo's `HEAD` points to whatever `init.defaultBranch`
575
- is set to on the CI runner or developer's machine, making the test non-deterministic:
576
-
577
- ```ruby
578
- # ❌ Fragile — HEAD points to the system default branch name
579
- Git.init(bare_dir, bare: true)
580
-
581
- # ✅ Correct — HEAD always points to 'main'
582
- Git.init(bare_dir, bare: true, initial_branch: 'main')
583
- ```
584
-
585
- **No shell-outs in tests.** Never use backticks, `system()`, or `%x[]` in tests. For
586
- git commands (including setup steps), use `execution_context.command_capturing` — it
587
- is portable across platforms, handles paths with spaces, and uses the same mechanism
588
- the command classes themselves use. For example:
589
- `execution_context.command_capturing('rev-parse', 'HEAD').stdout.strip`. For non-git
590
- operations (file creation, directory manipulation, etc.), use Ruby's standard library
591
- (`FileUtils`, `File`, `Dir`) instead of shelling out.
592
-
593
- **Write cross-platform tests.** Avoid Unix-specific paths like `/dev/null`,
594
- `/dev/zero`, or hardcoded `/tmp`. Use Ruby's standard library for temporary files and
595
- directories (`Dir.mktmpdir`, `Tempfile`), and use `File.join` for path construction.
596
- When creating failure scenarios, use portable approaches (e.g., create a regular file
597
- and try to use it where a directory is expected) rather than platform-specific
598
- tricks.
599
-
600
- ### Shared conventions
601
-
602
- **Do not use other Commands classes in tests.** Each spec tests exactly one command
603
- class. Use `execution_context.command_capturing`, `repo`, or standard library methods
604
- for setup instead of instantiating other Commands classes. This maintains test
605
- isolation and prevents bugs in one command from breaking another command's tests.
606
-
607
- **Require only the command under test.** See [Rule
608
- 5](../rspec-unit-testing-standards/SKILL.md#rule-5-must-require-spec_helper-and-only-the-files-under-test)
609
- in RSpec Unit Testing Standards (MUST). For command specs specifically: do not
610
- require other command classes even if they are not instantiated — unused requires
611
- create false coupling between specs.
612
-
613
- **Version-dependent tests.** When a test's behavior varies by git version, use `skip`
614
- inside the `it` block — not the `skip:` metadata on `it`. The metadata form evaluates
615
- at the describe level where helpers like `repo` are not available, causing a load
616
- error. For example:
617
-
618
- ```ruby
619
- it 'succeeds when no merge is in progress' do
620
- skip 'requires git 2.35.0 or later' unless Git.git_version >= Git::Version.new(2, 35, 0)
621
-
622
- expect { command.call }.not_to raise_error
623
- end
624
- ```
625
-
626
- **Test descriptions must match assertions.** See [Rule
627
- 9](../rspec-unit-testing-standards/SKILL.md#rule-9-must-it-blocks-assert-one-concept-and-the-description-must-match-the-assertion)
628
- in RSpec Unit Testing Standards (MUST). This applies equally to command specs: a test
629
- described as "includes the --force flag" must assert that the flag appears in the
630
- arguments, not merely that `#call` returns a result.
631
-
632
- **Regex patterns** in test assertions should not use Ruby's `/m` modifier unless
633
- intentionally matching across newlines. Git output is line-based, so patterns should
634
- match within single lines.
635
-
636
- ## Workflow
637
-
638
- 1. Load the [RSpec Unit Testing Standards](../rspec-unit-testing-standards/SKILL.md)
639
- skill (line 1 through EOF)
640
- 2. Read the spec file(s) under review and the corresponding command source file
641
- 3. Determine the minimum supported Git version
642
- (see [Version-aware test scope](#version-aware-test-scope))
643
- 4. Audit each spec against the rules in [Reference](#reference), checking unit and
644
- integration tests separately
645
- 5. Produce the [Output](#output)
646
-
647
- ## Output
648
-
649
- Report only anomalies — skip items that comply. For each issue found, provide:
650
-
651
- - **Rule or guideline violated** — cite by name and source skill (e.g., "Rule 22,
652
- RSpec Unit Testing Standards" or "What not to test, Command Test Conventions")
653
- - **Location** — spec file and block path (e.g., `describe '#call' > context 'with
654
- :force option' > it '...'`)
655
- - **Issue** — one sentence describing what is wrong
656
- - **Fix** — the minimal change needed
657
-
658
- Group findings under two headings:
659
-
660
- **Required fixes** — MUST-level violations from either skill
661
-
662
- **Suggested improvements** — SHOULD-level deviations, ordered by impact
663
-
664
- If no issues are found, say so in one sentence and stop.