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.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +486 -410
  52. data/lib/git/branch.rb +380 -43
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +224 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +114 -18
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +349 -20
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,993 @@
1
+ # Command Implementation — Reference
2
+
3
+ Detailed reference for `Git::Commands::Base` command classes. This file is loaded
4
+ by subagents during the [Command Implementation](SKILL.md) workflow.
5
+
6
+ ## Contents
7
+
8
+ - [Contents](#contents)
9
+ - [Files to generate](#files-to-generate)
10
+ - [Single class vs. sub-command namespace](#single-class-vs-sub-command-namespace)
11
+ - [When to use sub-commands](#when-to-use-sub-commands)
12
+ - [Do NOT split by output format / output mode](#do-not-split-by-output-format--output-mode)
13
+ - [When to keep a single class](#when-to-keep-a-single-class)
14
+ - [Naming sub-command classes](#naming-sub-command-classes)
15
+ - [Namespace module template](#namespace-module-template)
16
+ - [Architecture contract](#architecture-contract)
17
+ - [Command template (Base pattern)](#command-template-base-pattern)
18
+ - [`#call` override guidance](#call-override-guidance)
19
+ - [Overriding `call` — inline example](#overriding-call--inline-example)
20
+ - [Action-option-with-optional-value commands](#action-option-with-optional-value-commands)
21
+ - [When to use `skip_cli` on `operand`](#when-to-use-skip_cli-on-operand)
22
+ - [`Base#with_stdin` mechanics](#basewith_stdin-mechanics)
23
+ - [Options completeness — consult the latest-version docs first](#options-completeness--consult-the-latest-version-docs-first)
24
+ - [`requires_git_version` convention](#requires_git_version-convention)
25
+ - [Scoping options to sub-command classes](#scoping-options-to-sub-command-classes)
26
+ - [Execution-model conflicts](#execution-model-conflicts)
27
+ - [`end_of_options` placement](#end_of_options-placement)
28
+ - [Rule 1 — SYNOPSIS shows `--`: mirror the SYNOPSIS](#rule-1--synopsis-shows----mirror-the-synopsis)
29
+ - [Rule 2 — SYNOPSIS does NOT show `--`: protect operands from flag misinterpretation](#rule-2--synopsis-does-not-show----protect-operands-from-flag-misinterpretation)
30
+ - [Exit status guidance](#exit-status-guidance)
31
+ - [Facade delegation and policy options](#facade-delegation-and-policy-options)
32
+ - [Internal compatibility contract](#internal-compatibility-contract)
33
+ - [Phased rollout requirements](#phased-rollout-requirements)
34
+ - [Common failures](#common-failures)
35
+ - [Policy/output-control flag hardcoded as `literal` (neutrality violation)](#policyoutput-control-flag-hardcoded-as-literal-neutrality-violation)
36
+ - [Unnecessary `def call` override](#unnecessary-def-call-override)
37
+ - [`execution_option` for fixed kwargs](#execution_option-for-fixed-kwargs)
38
+ - [Unnecessary `require` statements](#unnecessary-require-statements)
39
+ - [Other common failures](#other-common-failures)
40
+
41
+ ## Files to generate
42
+
43
+ For `Git::Commands::Foo::Bar`, **all three files are required and must be created**:
44
+
45
+ - `lib/git/commands/foo/bar.rb` — the command class
46
+ - `spec/unit/git/commands/foo/bar_spec.rb` — unit tests
47
+ - `spec/integration/git/commands/foo/bar_spec.rb` — integration tests (mandatory, not
48
+ optional)
49
+
50
+ Optional (first command in module):
51
+
52
+ - `lib/git/commands/foo.rb`
53
+
54
+ ## Single class vs. sub-command namespace
55
+
56
+ Most git commands map to a single class. Split into a namespace module with multiple
57
+ sub-command classes when the git command surfaces **meaningfully different
58
+ operations** that have distinct call shapes or protocols.
59
+
60
+ ### When to use sub-commands
61
+
62
+ **Split by operation** — when the git command has named sub-actions whose option sets
63
+ have little overlap (each sub-action would have mostly dead options if they shared
64
+ one class):
65
+
66
+ ```
67
+ git stash push / pop / apply / drop / list / show
68
+ git tag --create / --delete / --list
69
+ git worktree add / list / remove / move
70
+ ```
71
+
72
+ **Split by stdin protocol** — when one variant reads from stdin and another does not
73
+ (even if the git command is the same). The stdin variant needs a `call` override that
74
+ uses `Base#with_stdin`; mixing that with a no-stdin path in one class produces an
75
+ awkward interface.
76
+
77
+ ### Do NOT split by output format / output mode
78
+
79
+ **Output-mode flags are NOT a reason to create separate subclasses.** When a git
80
+ command supports multiple output formats via flags (`--patch`, `--numstat`, `--raw`,
81
+ `--format=…`, etc.), express them as `flag_option` or `value_option` entries in a
82
+ **single class**. The facade passes the desired format flags explicitly at call time:
83
+
84
+ ```ruby
85
+ # ❌ Anti-pattern: one class per output format
86
+ class Diff::Patch < Git::Commands::Base; end # literal '--patch'
87
+ class Diff::Numstat < Git::Commands::Base; end # literal '--numstat'
88
+ class Diff::Raw < Git::Commands::Base; end # literal '--raw'
89
+
90
+ # ✅ Correct: one class, output mode as options
91
+ class Diff < Git::Commands::Base
92
+ arguments do
93
+ literal 'diff'
94
+ flag_option :patch
95
+ flag_option :numstat
96
+ flag_option :raw
97
+ ...
98
+ end
99
+ end
100
+
101
+ # lib/git/lib.rb — parser contract is visible and auditable:
102
+ Git::Commands::Diff.new(self).call(patch: true, numstat: true, ...)
103
+ ```
104
+
105
+ The same applies for `--format=<string>`, `--pretty=<fmt>`, `--no-color`, and all
106
+ other parser-contract options. Declare them in the DSL; the facade passes them.
107
+
108
+ Remember: **`literal` entries are only for operation selectors** — fixed flags that
109
+ define which git sub-operation the class represents (e.g., `literal 'stash'`,
110
+ `literal 'show'`, `literal '--delete'`). Output-format flags are not operation
111
+ selectors.
112
+
113
+ ### When to keep a single class
114
+
115
+ - Different output modes (`--patch`, `--numstat`, `--raw`): **always** use a single
116
+ class; expose modes as DSL options.
117
+ - Minor option variations that share the same argument set.
118
+ - When the "special mode" is just 1–2 flags — use `flag_option`/`value_option`.
119
+ - When callers would always need multiple modes together (the facade composes them).
120
+
121
+ ### Naming sub-command classes
122
+
123
+ Prefer **user-oriented names** (what the caller gets back) over flag names
124
+ (implementation detail the caller shouldn't need to know):
125
+
126
+ ```
127
+ # Avoid — leaks implementation detail
128
+ CatFile::BatchCheck / CatFile::Batch
129
+
130
+ # Prefer — describes the result from the caller's perspective
131
+ CatFile::ObjectMeta / CatFile::ObjectContent
132
+ ```
133
+
134
+ Two hard constraints:
135
+
136
+ - **Never name a sub-command class `Object`** — it shadows Ruby's `::Object` base
137
+ class anywhere that constant is looked up inside the namespace.
138
+ - **Never use the `*Info` or `*Result` suffix** on command classes — those suffixes
139
+ are reserved for parsed result structs (`BranchInfo`, `TagInfo`,
140
+ `BranchDeleteResult`) which live in the top-level `Git::` namespace, not in
141
+ `Git::Commands::*`. A reader seeing `CommandFoo::BarInfo` expects a data struct,
142
+ not a class that runs a subprocess.
143
+
144
+ ### Namespace module template
145
+
146
+ When splitting, create a bare namespace module file (`foo.rb`) — no class — matching
147
+ the pattern of `cat_file.rb`. The file has three required sections in this order:
148
+ `require_relative` lines → module body with YARD → empty `module Foo` block.
149
+
150
+ **Required elements (all mandatory):**
151
+
152
+ 1. `# frozen_string_literal: true` magic comment
153
+ 2. One `require_relative` line per sub-command file, in the order the sub-commands
154
+ appear in the `@see` bullet list
155
+ 3. A one-line summary (what `git foo` does overall)
156
+ 4. A "This module contains command classes split by…" paragraph with a bullet for
157
+ every sub-command class using `{Foo::Bar}` YARD links followed by ` — ` and a
158
+ short description
159
+ 5. `@api private`
160
+ 6. `@see https://git-scm.com/docs/git-foo git-foo documentation`
161
+ 7. At least two `@example` blocks — one per sub-command class; each example should
162
+ demonstrate the most common (non-error-path) call using a local variable named
163
+ `cmd` and `lib` as the constructor argument
164
+ 8. Empty `module Foo` + `end` block (no methods, no constants)
165
+
166
+ **Tag ordering inside the YARD comment block:**
167
+
168
+ ```
169
+ # One-line summary.
170
+ #
171
+ # This module contains command classes split by ...:
172
+ #
173
+ # - {Foo::Bar} — short description
174
+ # - {Foo::Baz} — short description
175
+ #
176
+ # @api private
177
+ #
178
+ # @see https://git-scm.com/docs/git-foo git-foo documentation
179
+ #
180
+ # @example <Short description of the Bar use case>
181
+ # cmd = Git::Commands::Foo::Bar.new(lib)
182
+ # cmd.call(...)
183
+ #
184
+ # @example <Short description of the Baz use case>
185
+ # cmd = Git::Commands::Foo::Baz.new(lib)
186
+ # cmd.call(...)
187
+ ```
188
+
189
+ **Full template:**
190
+
191
+ ```ruby
192
+ # frozen_string_literal: true
193
+
194
+ require_relative 'foo/bar'
195
+ require_relative 'foo/baz'
196
+
197
+ module Git
198
+ module Commands
199
+ # One-line summary of what `git foo` does.
200
+ #
201
+ # This module contains command classes split by [reason for split]:
202
+ #
203
+ # - {Foo::Bar} — what Bar does
204
+ # - {Foo::Baz} — what Baz does
205
+ #
206
+ # @api private
207
+ #
208
+ # @see https://git-scm.com/docs/git-foo git-foo documentation
209
+ #
210
+ # @example <Short description for bar>
211
+ # cmd = Git::Commands::Foo::Bar.new(lib)
212
+ # cmd.call(...)
213
+ #
214
+ # @example <Short description for baz>
215
+ # cmd = Git::Commands::Foo::Baz.new(lib)
216
+ # cmd.call(...)
217
+ #
218
+ module Foo
219
+ end
220
+ end
221
+ end
222
+ ```
223
+
224
+ Each sub-command file adds `@see Git::Commands::Foo` to link back to the parent
225
+ module's overview.
226
+
227
+ **Checklist for reviewing an existing namespace module:**
228
+
229
+ - [ ] `# frozen_string_literal: true` is present
230
+ - [ ] All sub-command files are `require_relative`'d (no `require 'git/commands/...'`)
231
+ - [ ] Bullet list covers every sub-command class in the namespace with `{Foo::Bar}` YARD links
232
+ - [ ] `@api private` is present
233
+ - [ ] `@see` link points to `git-scm.com/docs/git-foo` documentation
234
+ - [ ] At least one `@example` block per sub-command class
235
+ - [ ] Each example uses `cmd = Git::Commands::Foo::Bar.new(lib)` form (variable `cmd`, arg `lib`)
236
+ - [ ] Tag order: summary → bullet list → `@api private` → `@see` → `@examples`
237
+ - [ ] No class is defined inside the module file; the `module Foo` block is empty
238
+
239
+ ## Architecture contract
240
+
241
+ For migrated commands, the expected structure is:
242
+
243
+ ```ruby
244
+ require 'git/commands/base'
245
+
246
+ class SomeCommand < Git::Commands::Base
247
+ arguments do
248
+ ...
249
+ end
250
+
251
+ # optional — only when introduced after Git::MINIMUM_GIT_VERSION
252
+ requires_git_version '2.29.0'
253
+
254
+ # optional for non-zero successful exits
255
+ # reason comment
256
+ allow_exit_status 0..1
257
+
258
+ # @!method call(*, **)
259
+ #
260
+ # @overload call(**options)
261
+ #
262
+ # YARD docs for this command's call signature.
263
+ #
264
+ # @return [Git::CommandLineResult]
265
+ end
266
+ ```
267
+
268
+ The `@!method` directive is the correct YARD form when the class contains **no
269
+ explicit `def call`** — YARD uses it to render per-command docs on the inherited
270
+ `call` method. When the class **does** define `def call` explicitly, place YARD
271
+ docs directly above `def call` and omit the `@!method` directive.
272
+
273
+ Shared behavior lives in `Base`:
274
+
275
+ - binds arguments
276
+ - calls `@execution_context.command_capturing(*args, **args.execution_options, raise_on_failure: false)`
277
+ - raises `Git::FailedError` unless exit status is in allowed range (`0..0` default)
278
+
279
+ Structural requirements:
280
+
281
+ - Class inherits from `Git::Commands::Base`
282
+ - File requires `git/commands/base` (not `git/commands/arguments`)
283
+ - Has exactly one `arguments do` declaration
284
+ - Does not define command-specific `initialize` that only assigns
285
+ `@execution_context`
286
+ - `require` statements are limited to files actually used within the command
287
+ class file itself — do not carry over `require` entries that belong only to
288
+ the facade (`Git::Lib`) or parser layer
289
+
290
+ ## Command template (Base pattern)
291
+
292
+ The `@note` annotation in the class-level docs must record the **latest git release**
293
+ at the time of audit, not the "last updated in" version shown in the git-scm.com page
294
+ footer (which only tracks when that command's docs last changed and can be much older
295
+ than the current release). Determine the correct version by running:
296
+
297
+ ```sh
298
+ bin/latest-git-version # e.g. 2.53.0
299
+ ```
300
+
301
+ Substitute the output for `2.XX.0` in the template below. The URL will resolve to
302
+ the last docs update for that command even if the command docs did not change in that
303
+ exact release.
304
+
305
+ ```ruby
306
+ # frozen_string_literal: true
307
+
308
+ require 'git/commands/base'
309
+
310
+ module Git
311
+ module Commands
312
+ module Foo
313
+ # Summary...
314
+ #
315
+ # @example Typical usage
316
+ # bar = Git::Commands::Foo::Bar.new(execution_context)
317
+ # bar.call(...)
318
+ #
319
+ # @note `arguments` block audited against https://git-scm.com/docs/git-{command}/2.XX.0
320
+ #
321
+ # @see Git::Commands::Foo
322
+ #
323
+ # @see https://git-scm.com/docs/git-{command} git-{command}
324
+ #
325
+ # @api private
326
+ class Bar < Git::Commands::Base # never name the class Object
327
+ arguments do
328
+ # Group related options with section comments (e.g. # Output, # Safety)
329
+ # NEVER add trailing inline comments (e.g. `# --verbose`) to DSL entries.
330
+ # The DSL is self-documenting; inline comments duplicate YARD docs and
331
+ # were removed project-wide in commit 370dffb.
332
+ end
333
+
334
+ # Optional: for commands where non-zero exits are valid
335
+ # rationale comment
336
+ # allow_exit_status 0..1
337
+
338
+ # @!method call(*, **)
339
+ #
340
+ # @overload call(operand = nil, *rest, **options)
341
+ #
342
+ # Execute the git ... command.
343
+ #
344
+ # @param operand [String, nil] (nil) short description without trailing period
345
+ #
346
+ # Continuation paragraph separated by a blank comment line. Only needed
347
+ # when the short description alone is insufficient.
348
+ #
349
+ # @param options [Hash] command options
350
+ #
351
+ # @option options [Boolean, nil] :simple_flag (nil) one-sentence description without period
352
+ #
353
+ # @option options [Boolean, Integer, nil] :force (nil) short description without period
354
+ #
355
+ # When an integer is given, the flag is repeated that many times (up to the
356
+ # configured `max_times:` limit).
357
+ #
358
+ # Alias: :f
359
+ #
360
+ # @option options [Boolean, String, nil] :complex_flag (nil) short description without period
361
+ #
362
+ # Continuation: explain the `true`/`false`/string forms here, each separated by
363
+ # a blank comment line from the short description above.
364
+ #
365
+ # Alias: :f
366
+ #
367
+ # @return [Git::CommandLineResult] the result of calling `git ...`
368
+ #
369
+ # @raise [ArgumentError] if unsupported options are provided
370
+ #
371
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
372
+ end
373
+ end
374
+ end
375
+ end
376
+ ```
377
+
378
+ This template uses no explicit `def call` — the `@!method` YARD directive attaches
379
+ per-command docs to the inherited `call` method. Use this form for simple commands
380
+ where no pre-call logic is needed.
381
+
382
+ > **`@raise` wording** — always use the canonical generic form that matches the
383
+ > command's declared exit-status range. **Never** enumerate specific failure causes
384
+ > (e.g. "if the branch doesn't exist"). Use:
385
+ >
386
+ > | `allow_exit_status` | Canonical `@raise` wording |
387
+ > | --- | --- |
388
+ > | none declared (default `0..0`) | `if git exits with a non-zero exit status` |
389
+ > | `allow_exit_status 0..1` | `if git exits outside the allowed range (exit code > 1)` |
390
+ > | `allow_exit_status 0..N` | `if git exits outside the allowed range (exit code > N)` |
391
+
392
+ YARD tag formatting rules (short descriptions, continuation paragraphs, punctuation)
393
+ are defined in the [YARD Documentation](../yard-documentation/SKILL.md) skill. The
394
+ template above demonstrates the correct form.
395
+
396
+ ## `#call` override guidance
397
+
398
+ Most commands use only a `# @!method call(*, **)` YARD directive with no
399
+ explicit `def call` — the inherited `Base#call` handles binding, execution,
400
+ and exit-status validation automatically. Do **not** add `def call(*, **) = super`
401
+ or `def call(*, **) / super / end` for commands that need no custom logic; it
402
+ adds no behavior and conflicts with the `@!method` directive.
403
+
404
+ **Override `call` only when the command needs:**
405
+
406
+ 1. **Input validation the DSL cannot express** — per-argument validation parameters
407
+ (`required:`, `type:`, `allow_nil:`, etc.) and operand format validation belong
408
+ in `arguments do`. Cross-argument constraint methods are generally **not** declared;
409
+ git validates its own option semantics. The narrow exception is **arguments git
410
+ cannot observe in its argv**: if an argument is `skip_cli: true` and never
411
+ reaches git's argv, git cannot detect incompatibilities — use `conflicts` and/or
412
+ `requires_one_of` in the DSL (e.g., `cat-file --batch` uses both because
413
+ `:objects` is `skip_cli: true`). Do not raise `ArgumentError` manually for things
414
+ the DSL can express via a constraint declaration.
415
+ 2. **stdin feeding** — batch protocols (`--batch`, `--batch-check`) via
416
+ `Base#with_stdin`
417
+ 3. **Non-trivial option routing** — build different argument sets based on
418
+ which options are present
419
+ 4. **Action-option-with-optional-value** — when the command's primary action is
420
+ expressed as an option with an optional value (man-page notation:
421
+ `--flag[=<value>]`). The DSL entry uses `flag_or_value_option :name, inline:
422
+ true, type: [TrueClass, String]` and the override maps a positional `call` API
423
+ onto the keyword:
424
+
425
+ ```ruby
426
+ def call(value = true, *, **)
427
+ super(*, **, option_name: value)
428
+ end
429
+ ```
430
+
431
+ **When overriding:**
432
+
433
+ - Bind arguments via `args_definition.bind(...)` — do not reimplement binding
434
+ - Delegate exit-status handling to `validate_exit_status!` — do not reimplement
435
+ - Do not call `super` after manual binding; use `@execution_context.command_capturing` directly
436
+
437
+ **DSL defaults:**
438
+
439
+ Defaults defined in the DSL (e.g., `operand :paths, default: ['.']`) are applied
440
+ automatically during `args_definition.bind(...)` — do not set defaults manually in
441
+ `call`.
442
+
443
+ When the command requires an explicit `def call` override, place YARD doc comments
444
+ **directly above** `def call` — do **not** use `# @!method call(*, **)` alongside
445
+ an explicit override.
446
+
447
+ ### Overriding `call` — inline example
448
+
449
+ ```ruby
450
+ # @overload call(*objects, **options)
451
+ #
452
+ # Execute the `git cat-file --batch` command.
453
+ #
454
+ # @param objects [Array<String>] one or more object names
455
+ #
456
+ # @param options [Hash] command options
457
+ #
458
+ # @option options [Boolean, nil] :unordered (nil) unordered output
459
+ #
460
+ # @return [Git::CommandLineResult] the result of calling `git cat-file --batch`
461
+ #
462
+ # @raise [ArgumentError] if unsupported options are provided
463
+ #
464
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
465
+ def call(*objects, **options)
466
+ bound = args_definition.bind(*objects, **options)
467
+ with_stdin(Array(bound.objects).map { |o| "#{o}\n" }.join) do |reader|
468
+ run_batch(bound, reader)
469
+ end
470
+ end
471
+
472
+ def run_batch(bound, reader)
473
+ result = @execution_context.command_capturing(*bound, in: reader, **bound.execution_options, raise_on_failure: false)
474
+ validate_exit_status!(result)
475
+ result
476
+ end
477
+ ```
478
+
479
+ For stdin-driven commands where objects are domain inputs but not CLI argv, prefer
480
+ modeling that contract in the DSL:
481
+
482
+ ```ruby
483
+ arguments do
484
+ literal 'cat-file'
485
+ literal '--batch-check'
486
+ flag_option :batch_all_objects
487
+ operand :objects, repeatable: true, skip_cli: true
488
+
489
+ conflicts :objects, :batch_all_objects
490
+ requires_one_of :objects, :batch_all_objects
491
+ end
492
+ ```
493
+
494
+ ### Action-option-with-optional-value commands
495
+
496
+ When a git command's primary action is an option with an optional value (man-page
497
+ notation: `--flag[=<value>]`, e.g. `git am --show-current-patch[=(diff|raw)]`), use
498
+ this pattern:
499
+
500
+ **DSL:**
501
+ ```ruby
502
+ arguments do
503
+ literal 'am'
504
+ flag_or_value_option :show_current_patch, inline: true, type: [TrueClass, String]
505
+ end
506
+ ```
507
+
508
+ **`#call` override** — required to give callers a natural positional API:
509
+ ```ruby
510
+ # Show the patch currently being applied by `git am`
511
+ #
512
+ # @param value [true, String] when +true+ (default), emits +--show-current-patch+
513
+ # (git's default behavior). Pass +"diff"+ or +"raw"+ to emit
514
+ # +--show-current-patch=diff+ / +--show-current-patch=raw+.
515
+ #
516
+ # @return [Git::CommandLineResult] the result of the command
517
+ #
518
+ # @raise [Git::FailedError] if no am session is in progress
519
+ #
520
+ def call(value = true, *, **)
521
+ super(*, **, option_name: value)
522
+ end
523
+ ```
524
+
525
+ Where:
526
+ - `value = true` — positional default; `true` emits `--flag`; a String emits
527
+ `--flag=value`
528
+ - `*` — forwards positional operands declared in the DSL (omit when the command has
529
+ none)
530
+ - `**` — forwards keyword options to the DSL binder; unknown keywords raise
531
+ `ArgumentError`
532
+ - `option_name: value` placed last so the positional arg always takes precedence
533
+
534
+ The `type: [TrueClass, String]` on the DSL entry rejects `false` at bind time,
535
+ removing the need for manual validation in the override.
536
+
537
+ ### When to use `skip_cli` on `operand`
538
+
539
+ Use `operand ..., skip_cli: true` when all of the following are true:
540
+
541
+ - The value is part of the Ruby `#call` interface and should be bound/validated by
542
+ the DSL
543
+ - The value should remain accessible on `bound` (for cross-field constraints, helper
544
+ methods, and documentation)
545
+ - The value must **not** be emitted into CLI argv (for example, it is sent via stdin
546
+ protocol)
547
+
548
+ Do **not** use `skip_cli` for execution-engine kwargs (`timeout:`, `chdir:`, etc.) —
549
+ those belong to `execution_option`.
550
+
551
+ Key points:
552
+
553
+ - **`in:` requires a real IO object.** `Process.spawn` only accepts objects with a
554
+ file descriptor; `StringIO` does not work. `Base#with_stdin` handles this by
555
+ opening an `IO.pipe` and spawning a background `Thread` that writes the content to
556
+ the write end (then closes it). The threaded write prevents deadlocks when content
557
+ exceeds the OS pipe buffer — the subprocess can drain the pipe concurrently. The
558
+ thread rescues `Errno::EPIPE` / `IOError` so it exits cleanly if the subprocess
559
+ closes stdin early. Pass an empty string when the process should receive no input
560
+ (e.g. when a `--batch-all-objects`-style flag makes git enumerate objects itself).
561
+ - **Extract helpers** like `run_batch` to stay within Rubocop `Metrics/MethodLength`
562
+ and `Metrics/AbcSize` thresholds. Aim to keep `call` under ~10 lines.
563
+
564
+ ## `Base#with_stdin` mechanics
565
+
566
+ `Base#with_stdin(content)` opens an `IO.pipe`, spawns a background `Thread` that
567
+ writes `content` to the write end (then closes it), and yields the read end as
568
+ `in:` to the execution context. The threaded write prevents deadlocks when
569
+ `content` exceeds the OS pipe buffer — the subprocess can drain the pipe
570
+ concurrently. The thread also rescues `Errno::EPIPE` / `IOError` so it exits
571
+ cleanly if the subprocess closes stdin early.
572
+
573
+ Use `with_stdin` instead of manual pipe management. `StringIO` cannot be used
574
+ because `Process.spawn` requires a real file descriptor.
575
+
576
+ Example — batch stdin protocol (as used by `git cat-file --batch`):
577
+
578
+ ```ruby
579
+ def call(*, **)
580
+ bound = args_definition.bind(*, **)
581
+ with_stdin(Array(bound.objects).map { |object| "#{object}\n" }.join) do |stdin_r|
582
+ run_batch(bound, stdin_r)
583
+ end
584
+ end
585
+ ```
586
+
587
+ ## Options completeness — consult the latest-version docs first
588
+
589
+ **Before writing any DSL entries**, use the documentation fetched during the
590
+ [Input](SKILL.md#git-documentation-for-the-git-command) phase and enumerate every
591
+ option the latest-version docs describe.
592
+
593
+ ### `requires_git_version` convention
594
+
595
+ `requires_git_version` is a **class-level** declaration only. Individual options do
596
+ **not** carry version annotations. The declaration must use a `'major.minor.patch'`
597
+ string (e.g., `'2.29.0'`), not a `Git::Version` or `Range` — pre-release versions
598
+ are not supported.
599
+
600
+ | Scenario | Action |
601
+ | --- | --- |
602
+ | Command exists in `Git::MINIMUM_GIT_VERSION` | Do **not** add `requires_git_version` |
603
+ | Command was introduced after `Git::MINIMUM_GIT_VERSION` | Add `requires_git_version '<version>'` at the version the command was introduced |
604
+
605
+ Options that were added to a command after `Git::MINIMUM_GIT_VERSION` are still
606
+ scaffolded in the DSL — they are **not** omitted. When a caller passes such an option
607
+ on an older git installation, git itself will produce its native "unknown option"
608
+ error. This is acceptable and expected; the ruby-git library does not gate individual
609
+ options by version.
610
+
611
+ For each option, make one of three decisions:
612
+
613
+ | Decision | Reason | Action |
614
+ | --- | --- | --- |
615
+ | **Include** | All behavioral options — including output-format flags (`--pretty=`, `--patch`, `--numstat`, `--name-only`, etc.) and filtering/selection flags | Add to `arguments do` |
616
+ | **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](#scoping-options-to-sub-command-classes) below |
617
+ | **Exclude (execution-model conflict)** | Requires TTY input or otherwise makes the command incompatible with non-interactive subprocess execution | Omit — see [Execution-model conflicts](#execution-model-conflicts) below |
618
+
619
+ Group related options with a comment in the DSL (e.g. `# Ref inclusion`, `# Date
620
+ filtering`, `# Commit ordering`). Follow the section groupings from the
621
+ latest-version documentation — this makes it easy for a reviewer to cross-check
622
+ against the docs.
623
+
624
+ **Pairs and opposites:** when the latest-version docs document `--foo` / `--no-foo`
625
+ as explicit flags, model them as a single `flag_option :foo, negatable: true` rather
626
+ than two separate DSL declarations. This registers both `:foo` (positive) and `:no_foo`
627
+ (negative) as independent boolean keys that follow standard boolean semantics:
628
+ `true` emits the flag, `false`/`nil` emits nothing. Use `no_foo: true` at the call
629
+ site to emit `--no-foo`.
630
+
631
+ **Short-flag aliases — cross-check the latest-version docs/source:** before adding
632
+ any short-flag alias (e.g. `%i[dry_run n]`), verify the alias character appears on
633
+ the same option heading in the documentation or parser for the latest supported
634
+ version (`-n, --dry-run`). Do **not** invent an alias that the latest-version sources
635
+ do not document — in this DSL, short aliases are Ruby-keyword aliases for ergonomics
636
+ and documentation parity, while CLI emission still follows the primary option name's
637
+ flag spec. Symmetrically, every option the latest-version docs document with a short
638
+ form **must** have an alias in the DSL (long name first: `%i[dry_run n]`).
639
+
640
+ **`as:` is an escape hatch, not a default tool:** treat `as:` as suspicious by
641
+ default and use it only when the required argv cannot be expressed by the normal DSL
642
+ mapping plus existing modifiers (`negatable:`, `inline:`, `as_operand:`,
643
+ `max_times:`, etc.). If a plain symbol, alias, or first-class modifier can express
644
+ the same behavior, prefer that. In particular, do not use `as:` to encode repeated
645
+ flags now that `max_times:` exists.
646
+
647
+ **`inline: true` for `=<value>` options:** when the latest-version docs show
648
+ `--option=<value>` (with an `=`), the DSL entry must include `inline: true`
649
+ regardless of whether the DSL method is `value_option` or `flag_or_value_option`.
650
+ Without it, the value is emitted as a separate token (`--option value`) instead of
651
+ the expected inline form (`--option=value`). Check every `value_option` and
652
+ `flag_or_value_option` entry against the latest-version signature.
653
+
654
+ **Constraint declarations are generally not used in command classes.** Do not add
655
+ `conflicts`, `requires`, `requires_one_of`, `requires_exactly_one_of`,
656
+ `forbid_values`, or `allowed_values` declarations to command classes. Git is the
657
+ single source of truth for its own option semantics. There are two narrow exceptions:
658
+
659
+ 1. **`skip_cli: true` arguments** — the argument never reaches git's argv, so git
660
+ cannot detect incompatibilities and constraint declarations are appropriate (see
661
+ the `cat-file --batch` example above: `:objects` is `skip_cli: true`, so git never
662
+ sees it and cannot detect the conflict or the absent-both case).
663
+ 2. **Git-visible arguments that cause silent data loss** — if a combination of
664
+ git-visible arguments causes git to silently discard data (no error, wrong
665
+ result), a `conflicts` declaration MAY be added with: a code comment explaining
666
+ why, a reference to the git version(s) where the behavior was verified, and a
667
+ test. As of this writing, no such case has been identified.
668
+ 3. **Mode-scoped flags explicitly constrained by the git docs** — if the git
669
+ documentation explicitly states that a flag only applies to certain modes or
670
+ option combinations (e.g., `--allow-unknown-type` is documented as "Allow
671
+ `-s` or `-t` to query broken/corrupt objects of unknown type"), a
672
+ `requires_one_of :mode_a, :mode_b, when: :flag` declaration is appropriate. Add
673
+ a DSL comment noting the constraint and a unit test asserting the ArgumentError.
674
+
675
+ ```ruby
676
+ # Allow -t and -s to query broken or corrupt objects of unknown type;
677
+ # rejected by git in any other mode — enforced by constraint below
678
+ flag_option :allow_unknown_type
679
+ # ...
680
+ requires_one_of :t, :s, when: :allow_unknown_type
681
+ ```
682
+
683
+ See `redesign/3_architecture_implementation.md` Insight 6 for the full policy.
684
+
685
+ This step is required. A command class that only exposes the options that happen to
686
+ be used today in `Git::Lib` is incomplete — callers of the future API should not need
687
+ to re-open the docs just because the scaffold only covered current usage.
688
+
689
+ ### Scoping options to sub-command classes
690
+
691
+ When a git command is split into sub-command classes (e.g., `Branch::Create`,
692
+ `Branch::List`, `Branch::Delete`), each class must include **only** the options that
693
+ apply to the sub-action it implements. Do **not** enumerate every option on the man
694
+ page — most git commands document options for all modes on a single page, and adding
695
+ options that belong to a different mode produces a class that accepts arguments git
696
+ will reject or misinterpret.
697
+
698
+ **How to determine which options belong to a sub-action:**
699
+
700
+ 1. **Read the SYNOPSIS** — git man pages list separate SYNOPSIS lines per mode
701
+ (e.g., `git branch [--list]`, `git branch -d`, `git branch -m`). Only options
702
+ shown on the SYNOPSIS line for the target sub-action are candidates.
703
+
704
+ 2. **Cross-reference the DESCRIPTION and OPTIONS sections** — some options are
705
+ described generally but only apply to specific modes. Check each option's
706
+ description for phrases like "only useful with `--list`" or "when used with
707
+ `-d`". If the docs explicitly tie an option to a different mode, exclude it.
708
+
709
+ 3. **Common/shared options** — options that appear on every SYNOPSIS line or are
710
+ described as applying to the command as a whole (e.g., `--quiet`, `--verbose`)
711
+ should be included in every sub-command class where they are meaningful.
712
+
713
+ **Example — `git branch`:**
714
+
715
+ | Option | Create | List | Delete | Move/Copy |
716
+ | --- | --- | --- | --- | --- |
717
+ | `--track` | Yes | — | — | — |
718
+ | `--force` | Yes | — | Yes | Yes |
719
+ | `--sort` | — | Yes | — | — |
720
+ | `--format` | — | Yes | — | — |
721
+ | `--merged` | — | Yes | — | — |
722
+ | `--quiet` | Yes | — | Yes | — |
723
+ | `--color` | — | Yes | — | — |
724
+
725
+ This rule applies **only** when the command is split into sub-command classes. For
726
+ single-class commands, include all options as described in the decision table above.
727
+
728
+ ### Execution-model conflicts
729
+
730
+ Command classes are neutral — they never hardcode policy choices. Policy defaults
731
+ (`no_edit: true`, `no_progress: true`, etc.) belong to the facade (`Git::Lib`).
732
+
733
+ > **Anti-pattern:** `literal '--no-edit'` inside a command class.
734
+ >
735
+ > **Correct pattern:** `flag_option :edit, negatable: true` in the command; `no_edit:
736
+ > true` passed from the facade call site.
737
+
738
+ The **only** options to exclude from the DSL are those that conflict with
739
+ non-interactive subprocess execution:
740
+
741
+ - `--interactive` / `-i` — requires a TTY
742
+ - `--patch` (interactive form, e.g. `git add -p`) — requires TTY prompts
743
+ - Any option requiring stdin/TTY interaction the library cannot provide
744
+
745
+ > **Note on `--patch`:** In `git add -p` it opens an interactive session (exclude).
746
+ > In `git diff --patch` it selects a non-interactive output format (include).
747
+ > Evaluate per-command, not globally.
748
+
749
+ **`--edit` / `--no-edit`:** Model as `flag_option :edit, negatable: true`. Do
750
+ **not** hardcode `literal '--no-edit'`. Pass `no_edit: true` from the facade call
751
+ site. See "Command-layer neutrality" in CONTRIBUTING.md.
752
+
753
+ **`--verbose`/`-v` and `--quiet`/`-q`:** include these unless their git
754
+ implementation requires interactive I/O.
755
+
756
+ ## `end_of_options` placement
757
+
758
+ Determine placement based on whether the SYNOPSIS explicitly shows `--`. See the
759
+ Review Arguments DSL checklist for the full decision tree.
760
+
761
+ ### Rule 1 — SYNOPSIS shows `--`: mirror the SYNOPSIS
762
+
763
+ When the SYNOPSIS explicitly shows `--` between operand groups (e.g., `[<tree-ish>]
764
+ [--] [<pathspec>...]`), place `end_of_options` in the same position the SYNOPSIS
765
+ shows it — after the pre-`--` operands, before the post-`--` group. See the Review
766
+ Arguments DSL checklist ("Choosing the correct pathspec form") for how to model the
767
+ post-`--` group (`value_option ... as_operand: true`).
768
+
769
+ **Do not apply Rule 2** when Rule 1 applies.
770
+
771
+ ```ruby
772
+ # git diff [<tree-ish>] [--] [<pathspec>...]
773
+ operand :tree_ish # BEFORE end_of_options
774
+ end_of_options # mirrors SYNOPSIS position
775
+ value_option :pathspec, as_operand: true, repeatable: true # AFTER end_of_options
776
+ ```
777
+
778
+ ### Rule 2 — SYNOPSIS does NOT show `--`: protect operands from flag misinterpretation
779
+
780
+ Insert `end_of_options` immediately before the first `operand` whenever any
781
+ `flag_option`, `value_option`, `flag_or_value_option`, `key_value_option`, or
782
+ `custom_option` appears earlier in the `arguments do` block. This prevents operands
783
+ from being misinterpreted as flags when a caller passes a value that starts with `-`.
784
+
785
+ `literal` entries are **never** the trigger — regardless of whether their value is
786
+ option-style (e.g. `literal '--delete'`) or a plain subcommand word (e.g. `literal
787
+ 'remove'`). Only the five DSL option methods above matter.
788
+
789
+ ```ruby
790
+ # ✅ Correct — flag_option triggers Rule 2; end_of_options inserted before first operand
791
+ arguments do
792
+ literal 'remote'
793
+ literal 'rename'
794
+ flag_option :progress, negatable: true # ← this triggers Rule 2
795
+
796
+ end_of_options
797
+
798
+ operand :old, required: true
799
+ operand :new, required: true
800
+ end
801
+
802
+ # ✅ Not needed — only literal entries precede the operand; no DSL option-flag methods
803
+ arguments do
804
+ literal 'remote'
805
+ literal 'remove'
806
+ operand :name, required: true # no DSL option methods → not required
807
+ end
808
+ ```
809
+
810
+ `end_of_options` is always safe to add even when not strictly required. Omit it by
811
+ convention when neither rule applies: it adds no defensive value and produces
812
+ unnecessarily verbose command lines (e.g. `git remote remove -- origin` instead of
813
+ `git remote remove origin`). When in doubt, add it — the Review Arguments DSL skill
814
+ flags a missing `end_of_options` as an error when options appear before operands.
815
+
816
+ ## Exit status guidance
817
+
818
+ - Default: no declaration needed (`0..0` from `Base`)
819
+ - Non-default: declare `allow_exit_status <range>` and add rationale comment
820
+
821
+ Examples:
822
+
823
+ ```ruby
824
+ # git diff exits 1 when differences are found (not an error)
825
+ allow_exit_status 0..1
826
+ ```
827
+
828
+ ```ruby
829
+ # fsck uses exit codes 0-7 as bit flags for findings
830
+ allow_exit_status 0..7
831
+ ```
832
+
833
+ ## Facade delegation and policy options
834
+
835
+ The command class is only half the story. After scaffolding the command, you must
836
+ also write (or update) the `Git::Lib` method that **delegates** to it. The facade
837
+ sets safe policy defaults at each call site — `no_edit: true`, `no_progress: true`,
838
+ etc. — not as `literal` entries inside the command class. See "Command-layer
839
+ neutrality" in CONTRIBUTING.md.
840
+
841
+ Policy defaults fall into two categories (see also
842
+ [facade-implementation/REFERENCE.md](../facade-implementation/REFERENCE.md)):
843
+
844
+ - **Fixed policy defaults** (`no_edit: true`, `no_progress: true`, `no_color: true`,
845
+ format strings): set unconditionally and **not** included in `ALLOWED_OPTS`.
846
+ `assert_valid_opts` rejects any caller-supplied value for these keys, enforcing
847
+ the policy. They are not part of the public API.
848
+ - **Overridable policy defaults** (e.g., `verbose: false`): included in `ALLOWED_OPTS`.
849
+ The facade sets a sensible default but callers may override it by passing a value
850
+ that goes through `**opts`.
851
+
852
+ ```ruby
853
+ # lib/git/lib.rb — facade method for `git pull`
854
+
855
+ PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
856
+
857
+ def pull(remote = nil, branch = nil, opts = {})
858
+ raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
859
+
860
+ assert_valid_opts(opts, PULL_ALLOWED_OPTS)
861
+ allowed_opts = opts.slice(*PULL_ALLOWED_OPTS)
862
+ positional_args = [remote, branch].compact
863
+ # no_edit: true is the non-interactive default (see CONTRIBUTING.md)
864
+ Git::Commands::Pull.new(self).call(*positional_args, no_edit: true, **allowed_opts).stdout
865
+ end
866
+ ```
867
+
868
+ Key points for the facade method:
869
+
870
+ - **Filter options** — declare an `ALLOWED_OPTS` constant listing only the options
871
+ the public API accepted at v4.3.0. Use `assert_valid_opts` + `opts.slice` to
872
+ prevent accidental API expansion.
873
+ - **Pass policy options as safe defaults** — `no_edit: true`, `no_progress: true`, etc.
874
+ Fixed policy defaults go directly in the command call (not in `ALLOWED_OPTS`).
875
+ Overridable policy defaults are placed before `**opts` in the command call so the
876
+ caller's value wins on key collision, and are included in `ALLOWED_OPTS`. Add a
877
+ comment explaining *why* (e.g., `# non-interactive default`).
878
+ - **Return the legacy type** — typically `.stdout` or a parsed struct, not
879
+ `CommandLineResult`.
880
+
881
+ See [Extract Command from Lib](../extract-command-from-lib/SKILL.md) for the complete
882
+ delegation workflow and additional patterns (stdout passthrough, parsed return
883
+ values, opts-hash normalization).
884
+
885
+ ## Internal compatibility contract
886
+
887
+ This is the canonical location for the internal compatibility contract. Other
888
+ skills reference this section rather than duplicating it.
889
+
890
+ Ensure refactors preserve these contract expectations:
891
+
892
+ - constructor shape remains `initialize(execution_context)` (inherited from `Base`)
893
+ - command entrypoint remains `call(*, **)` at runtime (via `Base#call`)
894
+ - argument-definition metadata remains available via `args_definition`
895
+
896
+ If an intentional deviation exists, require migration notes/changelog documentation.
897
+
898
+ ## Phased rollout requirements
899
+
900
+ This is the canonical location for phased rollout requirements. Other skills
901
+ reference this section rather than duplicating the full checklist.
902
+
903
+ For migration PRs, verify process constraints:
904
+
905
+ - changes are on a feature branch — **never commit or push directly to `main`**
906
+ - migration slice is scoped (pilot or one family), not all commands at once
907
+ - each slice is independently revertible
908
+ - refactor-only changes are not mixed with unrelated behavior changes
909
+ - quality gates pass for the slice — discover tasks via
910
+ `bundle exec ruby -e "require 'rake'; load 'Rakefile'; puts Rake::Task['default:parallel'].prerequisites"`
911
+ and run each individually via `bundle exec rake <task>`, fixing failures before
912
+ advancing
913
+
914
+ ## Common failures
915
+
916
+ ### Policy/output-control flag hardcoded as `literal` (neutrality violation)
917
+
918
+ `literal` entries for output-control, editor-suppression, progress, or verbose
919
+ flags inside a command class violate the neutrality principle. The command class
920
+ must model the git CLI faithfully; the facade sets safe defaults and callers may
921
+ override them.
922
+
923
+ Symptom: the command class contains one or more of:
924
+
925
+ ```ruby
926
+ # ❌ Any of these are neutrality violations
927
+ literal '--no-edit'
928
+ literal '--verbose'
929
+ literal '--no-progress'
930
+ literal '--no-color'
931
+ literal '--porcelain'
932
+ ```
933
+
934
+ Fix: convert each to a DSL option and pass the policy value from the facade:
935
+
936
+ ```ruby
937
+ # ✅ In the command class — neutral DSL declaration
938
+ flag_option :edit, negatable: true
939
+ flag_option :progress, negatable: true
940
+ flag_option :verbose
941
+ value_option :format
942
+
943
+ # ✅ In Git::Lib — facade passes the policy value explicitly
944
+ Git::Commands::Pull.new(self).call(no_edit: true, no_progress: true)
945
+ Git::Commands::Mv.new(self).call(*args, verbose: true)
946
+ Git::Commands::Fsck.new(self).call(no_progress: true)
947
+ ```
948
+
949
+ See "Command-layer neutrality" in CONTRIBUTING.md for the full policy.
950
+
951
+ ### Unnecessary `def call` override
952
+
953
+ Do **not** add `def call(*, **) = super` or `def call(*, **) / super / end` for
954
+ commands that need no custom logic; it adds no behavior and conflicts with the
955
+ `@!method` directive.
956
+
957
+ ### `execution_option` for fixed kwargs
958
+
959
+ `execution_option` must **not** be used for kwargs whose value must be
960
+ unconditionally fixed regardless of caller input. If a kwarg always has a specific
961
+ required value (e.g. `chomp: false` for commands returning raw content where trailing
962
+ newlines are data), hardcode it in a `def call` override instead — exposing it via
963
+ `execution_option` would allow callers to override a value that must never change.
964
+
965
+ ### Unnecessary `require` statements
966
+
967
+ A command class file should only `require` what it actually uses. The canonical
968
+ example is parser requires: `require 'git/parsers/foo'` is needed by the facade
969
+ (`Git::Lib`) but not by the command class itself — the command class just runs git
970
+ and returns `CommandLineResult`.
971
+
972
+ ```ruby
973
+ # ❌ Command class does not use Git::Parsers::Branch
974
+ require 'git/parsers/branch'
975
+ require 'git/commands/base'
976
+
977
+ # ✅ Only what the file actually uses
978
+ require 'git/commands/base'
979
+ ```
980
+
981
+ **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.
982
+
983
+ **Update mode:** remove flagged `require` statements.
984
+
985
+ ### Other common failures
986
+
987
+ - lingering `ARGS = Arguments.define` constant and custom `#call`
988
+ - command-specific duplicated exit-status checks instead of `allow_exit_status`
989
+ - missing rationale comment for `allow_exit_status`
990
+ - missing YARD directive (`# @!method call(*, **)`)
991
+ - `call` override that reimplements `Base#call` logic instead of delegating to `validate_exit_status!`
992
+ - using a manual `IO.pipe` inline instead of `Base#with_stdin` for stdin-feeding commands
993
+ - migration PR scope too broad (not phased)