git 4.3.2 → 5.0.0.beta.1
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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +67 -2705
- data/.github/pull_request_template.md +3 -1
- data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
- data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
- data/.github/skills/command-implementation/REFERENCE.md +993 -0
- data/.github/skills/command-implementation/SKILL.md +229 -0
- data/.github/skills/command-test-conventions/SKILL.md +660 -0
- data/.github/skills/command-yard-documentation/SKILL.md +426 -0
- data/.github/skills/dependency-management/SKILL.md +72 -0
- data/.github/skills/development-workflow/SKILL.md +506 -0
- data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
- data/.github/skills/facade-implementation/REFERENCE.md +840 -0
- data/.github/skills/facade-implementation/SKILL.md +260 -0
- data/.github/skills/facade-test-conventions/SKILL.md +380 -0
- data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
- data/.github/skills/make-skill-template/SKILL.md +176 -0
- data/.github/skills/pr-readiness-review/SKILL.md +185 -0
- data/.github/skills/project-context/SKILL.md +313 -0
- data/.github/skills/pull-request-review/SKILL.md +168 -0
- data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
- data/.github/skills/release-management/SKILL.md +125 -0
- data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
- data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
- data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
- data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
- data/.github/skills/reviewing-skills/SKILL.md +189 -0
- data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
- data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
- data/.github/skills/test-debugging/SKILL.md +160 -0
- data/.github/skills/yard-documentation/SKILL.md +793 -0
- data/.github/workflows/continuous_integration.yml +3 -2
- data/.github/workflows/enforce_conventional_commits.yml +1 -1
- data/.github/workflows/experimental_continuous_integration.yml +2 -2
- data/.github/workflows/release.yml +3 -4
- data/.gitignore +8 -0
- data/.husky/pre-commit +13 -0
- data/.release-please-manifest.json +1 -1
- data/.rspec +3 -0
- data/.rubocop.yml +7 -3
- data/.rubocop_todo.yml +23 -5
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -40
- data/CONTRIBUTING.md +694 -53
- data/README.md +17 -5
- data/Rakefile +61 -9
- data/commitlint.test +4 -0
- data/git.gemspec +14 -8
- data/lib/git/args_builder.rb +0 -8
- data/lib/git/base.rb +486 -410
- data/lib/git/branch.rb +380 -43
- data/lib/git/branch_delete_failure.rb +31 -0
- data/lib/git/branch_delete_result.rb +63 -0
- data/lib/git/branch_info.rb +178 -0
- data/lib/git/branches.rb +130 -24
- data/lib/git/command_line/base.rb +245 -0
- data/lib/git/command_line/capturing.rb +249 -0
- data/lib/git/command_line/result.rb +96 -0
- data/lib/git/command_line/streaming.rb +194 -0
- data/lib/git/command_line.rb +43 -322
- data/lib/git/command_line_result.rb +4 -88
- data/lib/git/commands/add.rb +131 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +252 -0
- data/lib/git/commands/am/continue.rb +43 -0
- data/lib/git/commands/am/quit.rb +43 -0
- data/lib/git/commands/am/retry.rb +47 -0
- data/lib/git/commands/am/show_current_patch.rb +64 -0
- data/lib/git/commands/am/skip.rb +42 -0
- data/lib/git/commands/am.rb +33 -0
- data/lib/git/commands/apply.rb +237 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +140 -0
- data/lib/git/commands/arguments.rb +3510 -0
- data/lib/git/commands/base.rb +403 -0
- data/lib/git/commands/branch/copy.rb +94 -0
- data/lib/git/commands/branch/create.rb +173 -0
- data/lib/git/commands/branch/delete.rb +80 -0
- data/lib/git/commands/branch/list.rb +162 -0
- data/lib/git/commands/branch/move.rb +94 -0
- data/lib/git/commands/branch/set_upstream.rb +86 -0
- data/lib/git/commands/branch/show_current.rb +49 -0
- data/lib/git/commands/branch/unset_upstream.rb +57 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +364 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +210 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +151 -0
- data/lib/git/commands/checkout/files.rb +115 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +105 -0
- data/lib/git/commands/clean.rb +100 -0
- data/lib/git/commands/clone.rb +240 -0
- data/lib/git/commands/commit.rb +272 -0
- data/lib/git/commands/commit_tree.rb +100 -0
- data/lib/git/commands/config_option_syntax/add.rb +83 -0
- data/lib/git/commands/config_option_syntax/get.rb +117 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
- data/lib/git/commands/config_option_syntax/list.rb +107 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
- data/lib/git/commands/config_option_syntax/set.rb +114 -0
- data/lib/git/commands/config_option_syntax/unset.rb +89 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +155 -0
- data/lib/git/commands/diff.rb +656 -0
- data/lib/git/commands/diff_files.rb +518 -0
- data/lib/git/commands/diff_index.rb +496 -0
- data/lib/git/commands/fetch.rb +352 -0
- data/lib/git/commands/fsck.rb +136 -0
- data/lib/git/commands/gc.rb +132 -0
- data/lib/git/commands/grep.rb +338 -0
- data/lib/git/commands/init.rb +99 -0
- data/lib/git/commands/log.rb +632 -0
- data/lib/git/commands/ls_files.rb +191 -0
- data/lib/git/commands/ls_remote.rb +155 -0
- data/lib/git/commands/ls_tree.rb +131 -0
- data/lib/git/commands/maintenance/register.rb +75 -0
- data/lib/git/commands/maintenance/run.rb +104 -0
- data/lib/git/commands/maintenance/start.rb +66 -0
- data/lib/git/commands/maintenance/stop.rb +55 -0
- data/lib/git/commands/maintenance/unregister.rb +79 -0
- data/lib/git/commands/maintenance.rb +31 -0
- data/lib/git/commands/merge/abort.rb +44 -0
- data/lib/git/commands/merge/continue.rb +44 -0
- data/lib/git/commands/merge/quit.rb +46 -0
- data/lib/git/commands/merge/start.rb +245 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +86 -0
- data/lib/git/commands/mv.rb +77 -0
- data/lib/git/commands/name_rev.rb +114 -0
- data/lib/git/commands/pull.rb +377 -0
- data/lib/git/commands/push.rb +246 -0
- data/lib/git/commands/read_tree.rb +149 -0
- data/lib/git/commands/remote/add.rb +91 -0
- data/lib/git/commands/remote/get_url.rb +66 -0
- data/lib/git/commands/remote/list.rb +54 -0
- data/lib/git/commands/remote/prune.rb +61 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +69 -0
- data/lib/git/commands/remote/set_branches.rb +63 -0
- data/lib/git/commands/remote/set_head.rb +82 -0
- data/lib/git/commands/remote/set_url.rb +71 -0
- data/lib/git/commands/remote/set_url_add.rb +61 -0
- data/lib/git/commands/remote/set_url_delete.rb +64 -0
- data/lib/git/commands/remote/show.rb +71 -0
- data/lib/git/commands/remote/update.rb +72 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +277 -0
- data/lib/git/commands/reset.rb +147 -0
- data/lib/git/commands/rev_parse.rb +297 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +57 -0
- data/lib/git/commands/revert/quit.rb +47 -0
- data/lib/git/commands/revert/skip.rb +44 -0
- data/lib/git/commands/revert/start.rb +153 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +114 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
- data/lib/git/commands/show_ref/exists.rb +78 -0
- data/lib/git/commands/show_ref/list.rb +145 -0
- data/lib/git/commands/show_ref/verify.rb +120 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +75 -0
- data/lib/git/commands/stash/branch.rb +65 -0
- data/lib/git/commands/stash/clear.rb +41 -0
- data/lib/git/commands/stash/create.rb +58 -0
- data/lib/git/commands/stash/drop.rb +67 -0
- data/lib/git/commands/stash/list.rb +39 -0
- data/lib/git/commands/stash/pop.rb +78 -0
- data/lib/git/commands/stash/push.rb +103 -0
- data/lib/git/commands/stash/show.rb +149 -0
- data/lib/git/commands/stash/store.rb +63 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +169 -0
- data/lib/git/commands/symbolic_ref/delete.rb +68 -0
- data/lib/git/commands/symbolic_ref/read.rb +95 -0
- data/lib/git/commands/symbolic_ref/update.rb +76 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +139 -0
- data/lib/git/commands/tag/delete.rb +55 -0
- data/lib/git/commands/tag/list.rb +143 -0
- data/lib/git/commands/tag/verify.rb +71 -0
- data/lib/git/commands/tag.rb +26 -0
- data/lib/git/commands/update_ref/batch.rb +140 -0
- data/lib/git/commands/update_ref/delete.rb +92 -0
- data/lib/git/commands/update_ref/update.rb +106 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +52 -0
- data/lib/git/commands/worktree/add.rb +140 -0
- data/lib/git/commands/worktree/list.rb +64 -0
- data/lib/git/commands/worktree/lock.rb +58 -0
- data/lib/git/commands/worktree/management_base.rb +51 -0
- data/lib/git/commands/worktree/move.rb +66 -0
- data/lib/git/commands/worktree/prune.rb +67 -0
- data/lib/git/commands/worktree/remove.rb +63 -0
- data/lib/git/commands/worktree/repair.rb +76 -0
- data/lib/git/commands/worktree/unlock.rb +47 -0
- data/lib/git/commands/worktree.rb +43 -0
- data/lib/git/commands/write_tree.rb +68 -0
- data/lib/git/commands.rb +89 -0
- data/lib/git/detached_head_info.rb +54 -0
- data/lib/git/diff.rb +297 -7
- data/lib/git/diff_file_numstat_info.rb +29 -0
- data/lib/git/diff_file_patch_info.rb +134 -0
- data/lib/git/diff_file_raw_info.rb +127 -0
- data/lib/git/diff_info.rb +169 -0
- data/lib/git/diff_path_status.rb +78 -19
- data/lib/git/diff_result.rb +32 -0
- data/lib/git/diff_stats.rb +59 -14
- data/lib/git/dirstat_info.rb +86 -0
- data/lib/git/errors.rb +65 -2
- data/lib/git/execution_context/global.rb +56 -0
- data/lib/git/execution_context/repository.rb +147 -0
- data/lib/git/execution_context.rb +482 -0
- data/lib/git/file_ref.rb +74 -0
- data/lib/git/fsck_object.rb +9 -9
- data/lib/git/fsck_result.rb +1 -1
- data/lib/git/lib.rb +1606 -1028
- data/lib/git/log.rb +15 -2
- data/lib/git/object.rb +92 -22
- data/lib/git/parsers/branch.rb +224 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/diff.rb +585 -0
- data/lib/git/parsers/fsck.rb +133 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/stash.rb +208 -0
- data/lib/git/parsers/tag.rb +257 -0
- data/lib/git/remote.rb +133 -9
- data/lib/git/repository/branching.rb +572 -0
- data/lib/git/repository/committing.rb +191 -0
- data/lib/git/repository/configuring.rb +156 -0
- data/lib/git/repository/diffing.rb +775 -0
- data/lib/git/repository/inspecting.rb +153 -0
- data/lib/git/repository/logging.rb +247 -0
- data/lib/git/repository/merging.rb +295 -0
- data/lib/git/repository/object_operations.rb +1101 -0
- data/lib/git/repository/path_resolver.rb +207 -0
- data/lib/git/repository/remote_operations.rb +753 -0
- data/lib/git/repository/shared_private.rb +51 -0
- data/lib/git/repository/staging.rb +390 -0
- data/lib/git/repository/stashing.rb +107 -0
- data/lib/git/repository/status_operations.rb +180 -0
- data/lib/git/repository/worktree_operations.rb +159 -0
- data/lib/git/repository.rb +264 -1
- data/lib/git/stash.rb +85 -4
- data/lib/git/stash_info.rb +104 -0
- data/lib/git/stashes.rb +130 -13
- data/lib/git/status.rb +224 -18
- data/lib/git/tag_delete_failure.rb +31 -0
- data/lib/git/tag_delete_result.rb +63 -0
- data/lib/git/tag_info.rb +105 -0
- data/lib/git/version.rb +109 -2
- data/lib/git/version_constraint.rb +81 -0
- data/lib/git/worktree.rb +120 -5
- data/lib/git/worktrees.rb +107 -7
- data/lib/git.rb +114 -18
- data/redesign/1_architecture_existing.md +54 -18
- data/redesign/2_architecture_redesign.md +365 -46
- data/redesign/3_architecture_implementation.md +1451 -54
- data/tasks/gem_tasks.rake +4 -0
- data/tasks/npm_tasks.rake +7 -0
- data/tasks/rspec.rake +48 -0
- data/tasks/test.rake +13 -1
- data/tasks/yard.rake +34 -7
- metadata +349 -20
- data/lib/git/index.rb +0 -6
- data/lib/git/path.rb +0 -38
- data/lib/git/working_directory.rb +0 -6
- /data/{release-please-config.json → .release-please-config.json} +0 -0
|
@@ -0,0 +1,660 @@
|
|
|
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 (`lib_command_spec.rb`).
|
|
245
|
+
|
|
246
|
+
**Where to test policy enforcement:** Policy tests belong in the facade layer,
|
|
247
|
+
not in command specs. When a `Git::Lib` method sets policy defaults like
|
|
248
|
+
`no_edit: true` or `no_progress: true`, the corresponding `lib_command_spec.rb`
|
|
249
|
+
(or `lib_spec.rb`) test should verify those defaults reach the command:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
# spec/unit/git/lib_command_spec.rb — facade policy-default test
|
|
253
|
+
describe '#pull' do
|
|
254
|
+
it 'defaults to no_edit: true for non-interactive execution' do
|
|
255
|
+
expect_any_instance_of(Git::Commands::Pull)
|
|
256
|
+
.to receive(:call).with(anything, no_edit: true).and_call_original
|
|
257
|
+
lib.pull('origin', 'main')
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
This separation ensures:
|
|
263
|
+
- Command specs verify the **neutral interface** (every option works correctly)
|
|
264
|
+
- Facade specs verify the **policy** (the right options are passed and why)
|
|
265
|
+
- An AI sees exactly where each concern is tested and does not conflate them
|
|
266
|
+
|
|
267
|
+
See "Command-layer neutrality" in CONTRIBUTING.md.
|
|
268
|
+
|
|
269
|
+
#### `#initialize` — omit from command specs
|
|
270
|
+
|
|
271
|
+
**Do not write a `describe '#initialize'` block in command specs.** This is a
|
|
272
|
+
deliberate exception to Rule 2's SHOULD guidance for concrete subclasses. The full
|
|
273
|
+
reasoning chain:
|
|
274
|
+
|
|
275
|
+
1. **Rule 2's `have_attributes` form requires public attributes.** `Base#initialize`
|
|
276
|
+
stores `@execution_context` as a private instance variable with no `attr_reader`,
|
|
277
|
+
so there is nothing to pass to `have_attributes`. The form that Rule 2 uses cannot
|
|
278
|
+
be applied.
|
|
279
|
+
|
|
280
|
+
2. **The only fallback is `not_to raise_error`, which is a Rule 24 violation.**
|
|
281
|
+
Asserting that `described_class.new(execution_context)` does not raise merely
|
|
282
|
+
confirms the code runs — it is not an observable behavioral assertion.
|
|
283
|
+
|
|
284
|
+
3. **Both Rule 2 purposes are already satisfied by other means:**
|
|
285
|
+
- *Documentation:* the `let(:command) { described_class.new(execution_context) }`
|
|
286
|
+
declaration at the top of every spec documents the constructor signature
|
|
287
|
+
as clearly as a dedicated block would.
|
|
288
|
+
- *Accidental-override guard:* `let(:command)` is evaluated before every example.
|
|
289
|
+
If a subclass accidentally introduced a `def initialize` with a different
|
|
290
|
+
signature, every test in the file would immediately raise `ArgumentError` —
|
|
291
|
+
providing the same protection a dedicated block would.
|
|
292
|
+
|
|
293
|
+
4. **`Base#initialize` is covered by `base_spec.rb`.** Command subclasses that do
|
|
294
|
+
not override `#initialize` gain nothing from repeating it.
|
|
295
|
+
|
|
296
|
+
**Required fix if found:** Remove any `describe '#initialize'` block that contains
|
|
297
|
+
only `expect { described_class.new(execution_context) }.not_to raise_error` — it is
|
|
298
|
+
a Rule 24 violation and provides no coverage value.
|
|
299
|
+
|
|
300
|
+
#### Unit test grouping
|
|
301
|
+
|
|
302
|
+
Unit tests are organized under `describe '#call'` with three sections:
|
|
303
|
+
|
|
304
|
+
1. **Argument building** (the bulk) — flat `context` blocks, one per option/operand
|
|
305
|
+
variation. These are always present and come first.
|
|
306
|
+
2. **`context 'exit code handling'`** — only for commands with `allow_exit_status`
|
|
307
|
+
ranges beyond `0..0`. Uses mocked exit codes via `command_result` helper to test
|
|
308
|
+
that exit codes within the allowed range return a result and exit codes outside
|
|
309
|
+
the range raise `FailedError`.
|
|
310
|
+
3. **`context 'input validation'`** — only for commands with validation rules. Covers
|
|
311
|
+
unsupported options and required arguments that raise `ArgumentError`.
|
|
312
|
+
Cross-argument constraints for git-visible arguments are not tested because
|
|
313
|
+
command classes do not declare them. The exception is constraints on `skip_cli:
|
|
314
|
+
true` arguments (e.g., `conflicts :objects, :batch_all_objects` and
|
|
315
|
+
`requires_one_of :objects, :batch_all_objects`), which should be tested.
|
|
316
|
+
|
|
317
|
+
The exit code and input validation blocks are optional — include them only when the
|
|
318
|
+
command has those behaviors. They always appear at the end of `#call`, in that order.
|
|
319
|
+
|
|
320
|
+
**Required fix if found:** The section names `'exit code handling'` and `'input
|
|
321
|
+
validation'` are exact string literals — do not paraphrase. A context named
|
|
322
|
+
`'with an unsupported option'` or `'when the option is invalid'` instead of
|
|
323
|
+
`'input validation'` MUST be renamed. These names are load-bearing identifiers: they
|
|
324
|
+
signal to reviewers at a glance which structural section they are looking at and what
|
|
325
|
+
it may or may not contain.
|
|
326
|
+
|
|
327
|
+
Unit test descriptions should be concise and action-oriented. Use descriptions like
|
|
328
|
+
"includes the --cached flag", "passes both commits as operands", "combines commit
|
|
329
|
+
with pathspecs".
|
|
330
|
+
|
|
331
|
+
**Always use the emitted long-flag form in descriptions, never a short alias.** The
|
|
332
|
+
DSL canonicalises aliases to the long form (e.g. `:q` → `--quiet`, `:f` → `--force`).
|
|
333
|
+
Writing `'adds -q flag'` in an `it` description is misleading because the actual token
|
|
334
|
+
asserted in the expectation is `'--quiet'`. Use `'adds --quiet flag'` instead.
|
|
335
|
+
|
|
336
|
+
> **Exception to RSpec Unit Testing Standards Rules 11–12 (subject and let
|
|
337
|
+
> ordering):** Command unit tests intentionally omit `subject` within `describe
|
|
338
|
+
> '#call'`. Because each test exercises a different argument combination, there is no
|
|
339
|
+
> single fixed call expressible as a shared `subject`. Use `let(:command)` at the
|
|
340
|
+
> `RSpec.describe` level and call `command.call(...)` directly inside each `it`
|
|
341
|
+
> block, overriding `let` inputs per `context` block as needed.
|
|
342
|
+
|
|
343
|
+
**Example with all three sections:**
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
RSpec.describe Git::Commands::Branch::Delete do
|
|
347
|
+
# Duck-type collaborator: command specs depend on the #command_capturing interface,
|
|
348
|
+
# not a single concrete ExecutionContext class.
|
|
349
|
+
let(:execution_context) { double('ExecutionContext') }
|
|
350
|
+
let(:command) { described_class.new(execution_context) }
|
|
351
|
+
|
|
352
|
+
describe '#call' do
|
|
353
|
+
# Argument building — flat contexts
|
|
354
|
+
context 'with single branch name' do
|
|
355
|
+
it 'passes the branch name' do
|
|
356
|
+
expected_result = command_result('Deleted branch feature.')
|
|
357
|
+
expect_command_capturing('branch', '-d', 'feature').and_return(expected_result)
|
|
358
|
+
result = command.call('feature')
|
|
359
|
+
expect(result).to eq(expected_result)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
context 'with :force option' do
|
|
364
|
+
# ...
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Exit code handling — only when command declares allow_exit_status
|
|
368
|
+
context 'exit code handling' do
|
|
369
|
+
it 'returns result for exit code 0' do
|
|
370
|
+
# ... mock exit code 0, assert result returned ...
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
it 'returns result for exit code 1 (partial failure)' do
|
|
374
|
+
# ... mock exit code 1, assert result returned ...
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
it 'raises FailedError for exit code > 1' do
|
|
378
|
+
# ... mock exit code 128, assert FailedError raised ...
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Input validation — only when command validates input
|
|
383
|
+
context 'input validation' do
|
|
384
|
+
it 'raises ArgumentError for unsupported options' do
|
|
385
|
+
expect { command.call('branch', invalid: true) }
|
|
386
|
+
.to raise_error(ArgumentError, /Unsupported options/)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Example with argument building only** (no custom exit codes, no validation):
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
RSpec.describe Git::Commands::Stash::Pop do
|
|
397
|
+
# Duck-type collaborator: command specs depend on the #command_capturing interface,
|
|
398
|
+
# not a single concrete ExecutionContext class.
|
|
399
|
+
let(:execution_context) { double('ExecutionContext') }
|
|
400
|
+
let(:command) { described_class.new(execution_context) }
|
|
401
|
+
|
|
402
|
+
describe '#call' do
|
|
403
|
+
context 'with no arguments' do
|
|
404
|
+
# ...
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
context 'with stash reference' do
|
|
408
|
+
# ...
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
context 'with :index option' do
|
|
412
|
+
# ...
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Integration tests
|
|
419
|
+
|
|
420
|
+
Integration tests are minimal smoke tests that confirm the command executes
|
|
421
|
+
successfully against a real git repository. They should NOT test git's output format,
|
|
422
|
+
parsing behavior, or specific content of stdout — those concerns belong in parser
|
|
423
|
+
specs and facade/end-to-end specs.
|
|
424
|
+
|
|
425
|
+
Each integration spec file tests exactly **one command class**. Do not create
|
|
426
|
+
multi-command workflow specs that chain commands together — that is the concern of
|
|
427
|
+
facade or end-to-end tests.
|
|
428
|
+
|
|
429
|
+
Integration tests should only cover:
|
|
430
|
+
|
|
431
|
+
- A smoke test: calling with valid arguments returns a `CommandLineResult` with
|
|
432
|
+
expected output (e.g., non-empty for commands that produce output)
|
|
433
|
+
- Exit codes from real git: one test per success exit code, exercised through real
|
|
434
|
+
git invocations that naturally produce each code. For example, for `git diff`:
|
|
435
|
+
identical refs produce exit code 0 with empty output; differing refs produce exit
|
|
436
|
+
code ≤1 with non-empty output. This confirms that real git returns the exit codes
|
|
437
|
+
the command's `allow_exit_status` range expects.
|
|
438
|
+
- Error handling: invalid input (e.g., a nonexistent ref) raises `FailedError`.
|
|
439
|
+
**Every command must have at least one error handling test.** Even commands with
|
|
440
|
+
non-default `allow_exit_status` ranges can be forced to fail (e.g., by removing
|
|
441
|
+
`.git` to trigger exit code 128).
|
|
442
|
+
|
|
443
|
+
**Do not** write integration tests that assert on git's output format (e.g., matching
|
|
444
|
+
specific line patterns, status letters, or header syntax). The command's job is to
|
|
445
|
+
pass the correct arguments to git and return the result — verifying git's formatting
|
|
446
|
+
behavior is testing git, not the command. If a particular flag needs to be tested
|
|
447
|
+
(e.g., `-M` for rename detection), verify the flag appears in the arguments via a
|
|
448
|
+
unit test.
|
|
449
|
+
|
|
450
|
+
> **Branch workflow:** Implement any new or updated tests on a feature branch. Never
|
|
451
|
+
> commit or push directly to `main` — open a pull request when changes are ready to
|
|
452
|
+
> merge.
|
|
453
|
+
|
|
454
|
+
#### Integration test grouping
|
|
455
|
+
|
|
456
|
+
Integration tests must be organized into two `context` blocks under `#call`:
|
|
457
|
+
|
|
458
|
+
- `context 'when the command succeeds'` — smoke tests, option variations, and exit
|
|
459
|
+
code variants
|
|
460
|
+
- `context 'when the command fails'` — error handling tests (`FailedError`)
|
|
461
|
+
|
|
462
|
+
This grouping provides a consistent structure across all command specs and makes it
|
|
463
|
+
immediately clear which tests cover the happy path vs. error conditions.
|
|
464
|
+
|
|
465
|
+
**Simple command example** (default exit code handling):
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
RSpec.describe Git::Commands::Add, :integration do
|
|
469
|
+
include_context 'in an empty repository'
|
|
470
|
+
|
|
471
|
+
subject(:command) { described_class.new(execution_context) }
|
|
472
|
+
|
|
473
|
+
describe '#call' do
|
|
474
|
+
context 'when the command succeeds' do
|
|
475
|
+
it 'returns a CommandLineResult' do
|
|
476
|
+
# ... valid invocation ...
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
context 'when the command fails' do
|
|
481
|
+
it 'raises FailedError with a nonexistent path' do
|
|
482
|
+
# git's error message phrasing varies by version — anchor on the stable input value
|
|
483
|
+
expect { command.call('nonexistent.txt') }
|
|
484
|
+
.to raise_error(Git::FailedError, /nonexistent\.txt/)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
**Custom exit code example** (command declares `allow_exit_status`):
|
|
492
|
+
|
|
493
|
+
```ruby
|
|
494
|
+
RSpec.describe Git::Commands::Diff::Numstat, :integration do
|
|
495
|
+
include_context 'in a diff test repository'
|
|
496
|
+
|
|
497
|
+
subject(:command) { described_class.new(execution_context) }
|
|
498
|
+
|
|
499
|
+
describe '#call' do
|
|
500
|
+
context 'when the command succeeds' do
|
|
501
|
+
it 'returns exit code 0 with no differences' do
|
|
502
|
+
result = command.call('initial', 'initial')
|
|
503
|
+
expect(result.status.exitstatus).to eq(0)
|
|
504
|
+
expect(result.stdout).to be_empty
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
it 'succeeds with differences found' do
|
|
508
|
+
result = command.call('initial', 'after_modify')
|
|
509
|
+
expect(result.status.exitstatus).to eq(1)
|
|
510
|
+
expect(result.stdout).not_to be_empty
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
context 'when the command fails' do
|
|
515
|
+
it 'raises FailedError for invalid revision' do
|
|
516
|
+
# git's error message phrasing varies by version — anchor on the stable input value
|
|
517
|
+
expect { command.call('nonexistent-ref') }
|
|
518
|
+
.to raise_error(Git::FailedError, /nonexistent-ref/)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
#### Guard tests for options introduced after the minimum supported Git version
|
|
526
|
+
|
|
527
|
+
When an integration test exercises an option that was introduced after the minimum
|
|
528
|
+
supported Git version (2.28.0), guard the example with
|
|
529
|
+
`skip: unless_git(minimum_version, feature_description)` to prevent failures on
|
|
530
|
+
installations that do not yet have the required Git version. The `unless_git` helper
|
|
531
|
+
is defined in `spec/spec_helper.rb`:
|
|
532
|
+
|
|
533
|
+
- Returns `false` when the installed Git meets the minimum version (tests run normally).
|
|
534
|
+
- Returns a human-readable skip reason string when the installed Git is too old
|
|
535
|
+
(RSpec skips the example).
|
|
536
|
+
|
|
537
|
+
Apply the guard to individual `it` blocks when only some tests in a context require a
|
|
538
|
+
newer version. Apply it to a `context` or `describe` block when **all** tests in that
|
|
539
|
+
group require the same minimum version.
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
# ✅ Different options introduced in different git versions — guard each `it` individually
|
|
543
|
+
it 'returns a CommandLineResult with the :verbose option',
|
|
544
|
+
skip: unless_git('2.33.0', 'git worktree list --verbose') do
|
|
545
|
+
# ...
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
it 'returns a CommandLineResult with the :z option combined with :porcelain',
|
|
549
|
+
skip: unless_git('2.36.0', 'git worktree list --porcelain -z') do
|
|
550
|
+
# ...
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# ✅ All tests in the group require the same version — guard the context/describe block
|
|
554
|
+
RSpec.describe Git::Commands::ShowRef::Exists, :integration,
|
|
555
|
+
skip: unless_git('2.43.0', 'git show-ref --exists') do
|
|
556
|
+
# ...
|
|
557
|
+
end
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Determine the correct minimum version by checking version-matched upstream git
|
|
561
|
+
documentation (e.g., `https://git-scm.com/docs/git-worktree/2.33.0`) rather than
|
|
562
|
+
relying only on the locally installed git binary.
|
|
563
|
+
|
|
564
|
+
#### Additional integration conventions
|
|
565
|
+
|
|
566
|
+
**Always specify `initial_branch: 'main'` when calling `Git.init` in test setup.**
|
|
567
|
+
The `in an empty repository` shared context already does this for the primary repo,
|
|
568
|
+
but tests that create *additional* repositories in a `before` block (e.g., a bare
|
|
569
|
+
remote, a second clone target) must pass `initial_branch: 'main'` explicitly to
|
|
570
|
+
`Git.init`. Without it, the repo's `HEAD` points to whatever `init.defaultBranch`
|
|
571
|
+
is set to on the CI runner or developer's machine, making the test non-deterministic:
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
# ❌ Fragile — HEAD points to the system default branch name
|
|
575
|
+
Git.init(bare_dir, bare: true)
|
|
576
|
+
|
|
577
|
+
# ✅ Correct — HEAD always points to 'main'
|
|
578
|
+
Git.init(bare_dir, bare: true, initial_branch: 'main')
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**No shell-outs in tests.** Never use backticks, `system()`, or `%x[]` in tests. For
|
|
582
|
+
git commands (including setup steps), use `execution_context.command_capturing` — it
|
|
583
|
+
is portable across platforms, handles paths with spaces, and uses the same mechanism
|
|
584
|
+
the command classes themselves use. For example:
|
|
585
|
+
`execution_context.command_capturing('rev-parse', 'HEAD').stdout.strip`. For non-git
|
|
586
|
+
operations (file creation, directory manipulation, etc.), use Ruby's standard library
|
|
587
|
+
(`FileUtils`, `File`, `Dir`) instead of shelling out.
|
|
588
|
+
|
|
589
|
+
**Write cross-platform tests.** Avoid Unix-specific paths like `/dev/null`,
|
|
590
|
+
`/dev/zero`, or hardcoded `/tmp`. Use Ruby's standard library for temporary files and
|
|
591
|
+
directories (`Dir.mktmpdir`, `Tempfile`), and use `File.join` for path construction.
|
|
592
|
+
When creating failure scenarios, use portable approaches (e.g., create a regular file
|
|
593
|
+
and try to use it where a directory is expected) rather than platform-specific
|
|
594
|
+
tricks.
|
|
595
|
+
|
|
596
|
+
### Shared conventions
|
|
597
|
+
|
|
598
|
+
**Do not use other Commands classes in tests.** Each spec tests exactly one command
|
|
599
|
+
class. Use `execution_context.command_capturing`, `repo`, or standard library methods
|
|
600
|
+
for setup instead of instantiating other Commands classes. This maintains test
|
|
601
|
+
isolation and prevents bugs in one command from breaking another command's tests.
|
|
602
|
+
|
|
603
|
+
**Require only the command under test.** See [Rule
|
|
604
|
+
5](../rspec-unit-testing-standards/SKILL.md#rule-5-must-require-spec_helper-and-only-the-files-under-test)
|
|
605
|
+
in RSpec Unit Testing Standards (MUST). For command specs specifically: do not
|
|
606
|
+
require other command classes even if they are not instantiated — unused requires
|
|
607
|
+
create false coupling between specs.
|
|
608
|
+
|
|
609
|
+
**Version-dependent tests.** When a test's behavior varies by git version, use `skip`
|
|
610
|
+
inside the `it` block — not the `skip:` metadata on `it`. The metadata form evaluates
|
|
611
|
+
at the describe level where helpers like `repo` are not available, causing a load
|
|
612
|
+
error. For example:
|
|
613
|
+
|
|
614
|
+
```ruby
|
|
615
|
+
it 'succeeds when no merge is in progress' do
|
|
616
|
+
skip 'requires git 2.35.0 or later' unless Git.git_version >= Git::Version.new(2, 35, 0)
|
|
617
|
+
|
|
618
|
+
expect { command.call }.not_to raise_error
|
|
619
|
+
end
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
**Test descriptions must match assertions.** See [Rule
|
|
623
|
+
9](../rspec-unit-testing-standards/SKILL.md#rule-9-must-it-blocks-assert-one-concept-and-the-description-must-match-the-assertion)
|
|
624
|
+
in RSpec Unit Testing Standards (MUST). This applies equally to command specs: a test
|
|
625
|
+
described as "includes the --force flag" must assert that the flag appears in the
|
|
626
|
+
arguments, not merely that `#call` returns a result.
|
|
627
|
+
|
|
628
|
+
**Regex patterns** in test assertions should not use Ruby's `/m` modifier unless
|
|
629
|
+
intentionally matching across newlines. Git output is line-based, so patterns should
|
|
630
|
+
match within single lines.
|
|
631
|
+
|
|
632
|
+
## Workflow
|
|
633
|
+
|
|
634
|
+
1. Load the [RSpec Unit Testing Standards](../rspec-unit-testing-standards/SKILL.md)
|
|
635
|
+
skill (line 1 through EOF)
|
|
636
|
+
2. Read the spec file(s) under review and the corresponding command source file
|
|
637
|
+
3. Determine the minimum supported Git version
|
|
638
|
+
(see [Version-aware test scope](#version-aware-test-scope))
|
|
639
|
+
4. Audit each spec against the rules in [Reference](#reference), checking unit and
|
|
640
|
+
integration tests separately
|
|
641
|
+
5. Produce the [Output](#output)
|
|
642
|
+
|
|
643
|
+
## Output
|
|
644
|
+
|
|
645
|
+
Report only anomalies — skip items that comply. For each issue found, provide:
|
|
646
|
+
|
|
647
|
+
- **Rule or guideline violated** — cite by name and source skill (e.g., "Rule 22,
|
|
648
|
+
RSpec Unit Testing Standards" or "What not to test, Command Test Conventions")
|
|
649
|
+
- **Location** — spec file and block path (e.g., `describe '#call' > context 'with
|
|
650
|
+
:force option' > it '...'`)
|
|
651
|
+
- **Issue** — one sentence describing what is wrong
|
|
652
|
+
- **Fix** — the minimal change needed
|
|
653
|
+
|
|
654
|
+
Group findings under two headings:
|
|
655
|
+
|
|
656
|
+
**Required fixes** — MUST-level violations from either skill
|
|
657
|
+
|
|
658
|
+
**Suggested improvements** — SHOULD-level deviations, ordered by impact
|
|
659
|
+
|
|
660
|
+
If no issues are found, say so in one sentence and stop.
|