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,840 @@
1
+ # Facade Implementation — Reference
2
+
3
+ Detailed reference for `Git::Repository::*` facade modules and methods. This file
4
+ is loaded by subagents during the [Facade Implementation](SKILL.md) workflow.
5
+
6
+ ## Contents
7
+
8
+ - [Contents](#contents)
9
+ - [Files to generate](#files-to-generate)
10
+ - [Topic module selection](#topic-module-selection)
11
+ - [Existing modules](#existing-modules)
12
+ - [Decision rules for adding a new module](#decision-rules-for-adding-a-new-module)
13
+ - [One-at-a-time extraction from `Git::Base` / `Git::Lib`](#one-at-a-time-extraction-from-gitbase--gitlib)
14
+ - [Naming a new topic module](#naming-a-new-topic-module)
15
+ - [Designing a facade method](#designing-a-facade-method)
16
+ - [Choosing the return type](#choosing-the-return-type)
17
+ - [Choosing the method signature](#choosing-the-method-signature)
18
+ - [One-line delegator](#one-line-delegator)
19
+ - [Orchestration sequence](#orchestration-sequence)
20
+ - [Sequencing multiple commands](#sequencing-multiple-commands)
21
+ - [Topic module skeleton](#topic-module-skeleton)
22
+ - [The five facade responsibilities checklist](#the-five-facade-responsibilities-checklist)
23
+ - [Argument pre-processing patterns](#argument-pre-processing-patterns)
24
+ - [Path normalization](#path-normalization)
25
+ - [Option whitelisting (preventing API expansion)](#option-whitelisting-preventing-api-expansion)
26
+ - [Deprecation handling](#deprecation-handling)
27
+ - [Defaults and policy options](#defaults-and-policy-options)
28
+ - [Internal helpers and encapsulation](#internal-helpers-and-encapsulation)
29
+ - [The rule](#the-rule)
30
+ - [The pattern](#the-pattern)
31
+ - [Why this works](#why-this-works)
32
+ - [Naming rules](#naming-rules)
33
+ - [Growth path](#growth-path)
34
+ - [Decision 1 — Where does the helper live?](#decision-1--where-does-the-helper-live)
35
+ - [Decision 2 — How is state passed to the helper?](#decision-2--how-is-state-passed-to-the-helper)
36
+ - [Why not `ActiveSupport::Concern`?](#why-not-activesupportconcern)
37
+ - [Parser vs. raw stdout](#parser-vs-raw-stdout)
38
+ - [Result-class factory methods](#result-class-factory-methods)
39
+ - [Common failures](#common-failures)
40
+ - [One-line delegation when orchestration is needed](#one-line-delegation-when-orchestration-is-needed)
41
+ - [Leaking command-class types into the public API](#leaking-command-class-types-into-the-public-api)
42
+ - [Exposing command-DSL-shaped argv in the facade signature](#exposing-command-dsl-shaped-argv-in-the-facade-signature)
43
+ - [Changing the legacy return type or signature on extraction](#changing-the-legacy-return-type-or-signature-on-extraction)
44
+ - [Bypassing `@execution_context`](#bypassing-execution_context)
45
+ - [Placing an overridable policy default after caller options](#placing-an-overridable-policy-default-after-caller-options)
46
+ - [Skipping option whitelisting on opaque opts hashes](#skipping-option-whitelisting-on-opaque-opts-hashes)
47
+ - [Mixing facade and command responsibilities](#mixing-facade-and-command-responsibilities)
48
+ - [Adding a topic module whose methods fit an existing one](#adding-a-topic-module-whose-methods-fit-an-existing-one)
49
+
50
+ ## Files to generate
51
+
52
+ For a facade method on `Git::Repository::<Topic>`:
53
+
54
+ - `lib/git/repository/<topic>.rb` — the topic module (created on first method,
55
+ extended for subsequent methods)
56
+ - `spec/unit/git/repository/<topic>_spec.rb` — unit tests
57
+ - `spec/integration/git/repository/<topic>_spec.rb` — integration tests
58
+ (omit for true one-line delegators that add no orchestration)
59
+
60
+ When the topic module is new, also update:
61
+
62
+ - `lib/git/repository.rb` — add `require 'git/repository/<topic>'` and `include
63
+ Git::Repository::<Topic>` in alphabetical order with the existing entries.
64
+
65
+ ## Topic module selection
66
+
67
+ ### Existing modules
68
+
69
+ List `lib/git/repository/` to see all current topic modules. Add to one of those
70
+ modules whenever the new method fits the topic. Do not create a new module when
71
+ an existing one would do.
72
+
73
+ ### Decision rules for adding a new module
74
+
75
+ Create a new topic module only when **both** of the following are true:
76
+
77
+ 1. The topic is recognizable to a reader familiar with git — preferably matching
78
+ one of the categories at <https://git-scm.com/docs> (Working tree, Branching,
79
+ History, Sharing, Patching, Inspection, Configuration, Plumbing).
80
+ 2. The methods would be awkward to place in any existing module without diluting
81
+ that module's topic.
82
+
83
+ There is no fixed method-count requirement. A module that starts with one or two
84
+ methods is fine when the topic is genuinely distinct; the question is always
85
+ *fit*, not count. Default to extending an existing module whenever the new method
86
+ plausibly fits there.
87
+
88
+ #### One-at-a-time extraction from `Git::Base` / `Git::Lib`
89
+
90
+ When migrating a single method without a planned batch:
91
+
92
+ 1. Before deciding placement, scan `Git::Base` and `Git::Lib` for sibling methods
93
+ on the same git topic (e.g. when extracting `branches_all`, also look for
94
+ `branch`, `branch_current`, `branch_delete`, `current_branch_state`).
95
+ 2. If those siblings form a coherent topic that does not fit any existing module,
96
+ create the new module on this first extraction so subsequent extractions have
97
+ a home.
98
+ 3. Otherwise place the method in the closest existing module. Revisit module
99
+ organization later if a distinct topic emerges; promote the cluster to its own
100
+ module in a single `refactor(repository):` commit at that point.
101
+
102
+ For a one-off method that does not fit any existing module and does not justify a
103
+ new module yet, place it in the closest existing module and revisit the
104
+ organization when more methods join it.
105
+
106
+ ### Naming a new topic module
107
+
108
+ Topic modules follow a **two-tier** convention (documented in
109
+ [redesign/3_architecture_implementation.md §Facade module naming convention](../../../redesign/3_architecture_implementation.md#facade-module-naming-convention)):
110
+
111
+ - **Gerund** (`verb-ing`) when a single action word clearly names the whole module:
112
+ `Staging`, `Committing`, `Branching`, `Merging`, `Logging`, `Diffing`, `Stashing`.
113
+ - **Noun + `Operations`** when the module groups a mixed bag of methods by git
114
+ concept rather than a single action: `RemoteOperations`, `ObjectOperations`,
115
+ `StatusOperations`, `WorktreeOperations`.
116
+
117
+ Additional rules:
118
+
119
+ - Use PascalCase; the file name is the snake_case equivalent (`Branching` →
120
+ `branching.rb`).
121
+ - Do **not** use plain nouns that clash with existing public or domain-object class names such
122
+ as `Branch`, `Diff`, `Log`, `Object`, `Remote`, `Status`, `Worktree`, etc. Those names
123
+ belong to existing domain/query/value objects, not topic modules.
124
+ - Do not use the `*Info` or `*Result` suffix — reserved for parsed result structs.
125
+
126
+ ## Designing a facade method
127
+
128
+ Decide the return type and the signature first (together they are the public
129
+ contract), then choose the body shape — one-line delegator for the simplest
130
+ cases, orchestration sequence otherwise.
131
+
132
+ ### Choosing the return type
133
+
134
+ Apply these rules in order:
135
+
136
+ 1. **Extracting from `Git::Base` or `Git::Lib`?** The facade method **must**
137
+ return exactly what the legacy method returned (same type, same shape, same
138
+ nil/empty semantics). Backward compatibility for users who called
139
+ `g.foo` / `g.lib.foo` is the public contract being preserved during
140
+ migration; capture the legacy return in the Step 2 plan.
141
+ 2. **No legacy method (greenfield facade method)?** Choose the return type from
142
+ the public-API perspective, in this order of preference:
143
+ 1. A **domain object** (`Git::BranchInfo`, `Git::DiffResult`, …) when the
144
+ output has structure callers will inspect.
145
+ 2. A **primitive** (`String` of chomped stdout, `Boolean`, `Integer`) when
146
+ the output is a single value.
147
+ 3. `nil` or `self` when the method is called for its side effects.
148
+ 4. **`Git::CommandLineResult`** only when the topic module explicitly
149
+ documents that as its contract (rare — reserved for low-level escape
150
+ hatches). Do not return `CommandLineResult` by default just because the
151
+ command returns it.
152
+ 3. **Never return** a type from `Git::Commands::*` (e.g.
153
+ `Git::Commands::Foo::Bar::SomeResult`). Command-internal types are not part
154
+ of the facade's public API.
155
+
156
+ ### Choosing the method signature
157
+
158
+ The Ruby signature is part of the public contract — just as binding as the
159
+ return type. Apply these rules in order:
160
+
161
+ 1. **Extracting from `Git::Base` or `Git::Lib`?** The facade method **must**
162
+ preserve the legacy signature exactly: same positional arguments in the same
163
+ order, same defaults, same `opts = {}` vs. `**options` shape, same
164
+ nil/sentinel semantics. Capture the legacy signature in the Step 2 plan and
165
+ diff against it after implementation.
166
+ 2. **No legacy method (greenfield facade method)?** Design the signature from
167
+ the public-API perspective:
168
+ 1. **Positional arguments** for the natural domain identifiers the method
169
+ operates on (paths, refs, names, messages). At most two or three.
170
+ 2. **Keyword arguments with `**options`** for option hashes. Prefer
171
+ `**options` over `opts = {}` in greenfield code — it surfaces unknown
172
+ keys at the call site and reads better with the [whitelist
173
+ pattern](#option-whitelisting-preventing-api-expansion). When the body
174
+ forwards the options unchanged (the common case), use the **anonymous**
175
+ keyword splat (`**`) so RuboCop's `Style/ArgumentsForwarding` cop is
176
+ satisfied; name the splat (`**options`) only when the body must inspect
177
+ or mutate the hash before forwarding (e.g. merging a positional
178
+ argument into it, or applying a deprecation rewrite).
179
+ 3. **Named keyword arguments** (`force: false`, `all: true`) for a small,
180
+ fixed set of flags that are part of the documented API and unlikely to
181
+ grow. Switch to `**options` once the set exceeds ~3 keys.
182
+ 3. **Validate cross-argument constraints in the facade**, before calling the
183
+ command. Raise `ArgumentError` with a message that names the offending
184
+ arguments — for example, `pull` raises when `branch` is given without
185
+ `remote`. The command class stays neutral about Ruby-level argument
186
+ relationships.
187
+ 4. **Never expose** command-DSL-shaped arguments (`*argv`, raw `Hash` of CLI
188
+ flags) on the facade. The facade's job is to translate Ruby idioms into
189
+ command calls; passing through opaque argv defeats the layer.
190
+
191
+ Mechanical patterns for shaping the inputs (path coercion, option whitelisting,
192
+ deprecation handling) live in [Argument pre-processing
193
+ patterns](#argument-pre-processing-patterns).
194
+
195
+ ### One-line delegator
196
+
197
+ When the facade method takes no options hash, does no pre-processing, and only
198
+ a trivial post-processing step (such as `.stdout.chomp`), it is a single-line
199
+ delegation. For example, a hypothetical `Git::Repository::Inspection#current_branch`
200
+ preserving `Git::Lib#current_branch`'s `String` contract:
201
+
202
+ ```ruby
203
+ # Return the name of the currently checked-out branch
204
+ #
205
+ # @example Get the current branch name
206
+ # repo.current_branch #=> "main"
207
+ #
208
+ # @return [String] the current branch name
209
+ #
210
+ # @raise [Git::FailedError] when `git rev-parse` exits with a non-zero status
211
+ #
212
+ def current_branch
213
+ Git::Commands::RevParse.new(@execution_context).call('--abbrev-ref', 'HEAD').stdout.chomp
214
+ end
215
+ ```
216
+
217
+ Use the one-line form only when **all** of the following hold:
218
+
219
+ - The Ruby signature exactly matches the command's `#call` signature (with at most
220
+ trivial coercion like `Array(paths)` or `*[remote, branch].compact`).
221
+ - The method takes no options hash. Any facade method that accepts options must
222
+ whitelist them — see [Option whitelisting](#option-whitelisting-preventing-api-expansion) —
223
+ which makes it at minimum a two-line orchestration.
224
+ - The post-processing is at most a single chained call (`.stdout`,
225
+ `.stdout.chomp`, etc.) that produces the documented return type (see
226
+ [Choosing the return type](#choosing-the-return-type) above).
227
+
228
+ If the documented return type requires parsing, multiple commands, validation,
229
+ deprecation, option whitelisting, or any conditional logic, the facade needs an
230
+ orchestration sequence — not a one-line delegator.
231
+
232
+ ### Orchestration sequence
233
+
234
+ When the facade method needs pre-processing, multiple commands, parsing, or result
235
+ assembly, expand the body into explicit phases:
236
+
237
+ ```ruby
238
+ def branches_all
239
+ result = Git::Commands::Branch::List.new(@execution_context).call(
240
+ all: true,
241
+ format: Git::Parsers::Branch::FORMAT_STRING
242
+ )
243
+ Git::Parsers::Branch.parse_list(result.stdout)
244
+ end
245
+
246
+ def commit(message, opts = {})
247
+ SharedPrivate.assert_valid_opts!(COMMIT_ALLOWED_OPTS, **opts)
248
+ opts = opts.merge(message: message) if message
249
+ opts = deprecate_commit_no_gpg_sign_option(opts)
250
+ Git::Commands::Commit.new(@execution_context).call(no_edit: true, **opts).stdout
251
+ end
252
+ ```
253
+
254
+ Three phases — keep them in this order:
255
+
256
+ 1. **Pre-process** — validate, whitelist, normalize, deprecate, default.
257
+ 2. **Call** — invoke one or more `Git::Commands::*` instances, each via
258
+ `@execution_context`.
259
+ 3. **Assemble** — pass stdout/stderr/status through a parser or result-class
260
+ factory method, or return the raw `CommandLineResult` value the topic module
261
+ documents.
262
+
263
+ ### Sequencing multiple commands
264
+
265
+ When a facade method orchestrates more than one command, sequence the calls
266
+ explicitly with intermediate results in local variables:
267
+
268
+ ```ruby
269
+ def branch_status(name)
270
+ upstream_result = Git::Commands::RevParse.new(@execution_context).call("#{name}@{upstream}")
271
+ ahead_behind = Git::Commands::RevList.new(@execution_context).call(
272
+ "#{name}...#{upstream_result.stdout.chomp}",
273
+ left_right: true,
274
+ count: true
275
+ )
276
+ Git::BranchStatus.from_rev_list_output(name, ahead_behind.stdout)
277
+ end
278
+ ```
279
+
280
+ Do not build a generic dispatcher or a "run everything in parallel" abstraction.
281
+ Explicit sequential calls are the documented pattern.
282
+
283
+ ## Topic module skeleton
284
+
285
+ The full file layout for a topic module under `lib/git/repository/`:
286
+
287
+ ```ruby
288
+ # frozen_string_literal: true
289
+
290
+ require 'git/commands/<command_a>'
291
+ require 'git/commands/<command_b>'
292
+ # require 'git/parsers/<parser>' — when the module uses a parser
293
+
294
+ module Git
295
+ class Repository
296
+ # Short summary of the topic and the facade methods it provides
297
+ #
298
+ # Included by {Git::Repository}.
299
+ #
300
+ # @api public
301
+ #
302
+ module Topic
303
+ # YARD docs per facade-yard-documentation skill
304
+ def method_a(...)
305
+ # body
306
+ end
307
+
308
+ # YARD docs per facade-yard-documentation skill
309
+ def method_b(...)
310
+ # body
311
+ end
312
+ end
313
+ end
314
+ end
315
+ ```
316
+
317
+ Then wire into `lib/git/repository.rb`:
318
+
319
+ ```ruby
320
+ require 'git/repository/topic'
321
+ # ...
322
+
323
+ class Repository
324
+ include Git::Repository::Topic
325
+ # ...
326
+ end
327
+ ```
328
+
329
+ ## The five facade responsibilities checklist
330
+
331
+ From [redesign/2_architecture_redesign.md §2.1](../../../redesign/2_architecture_redesign.md).
332
+ For each facade method, confirm whether each responsibility applies and is handled:
333
+
334
+ - [ ] **Manage execution context** — calls `Git::Commands::*.new(@execution_context)`,
335
+ never builds CLI argv directly and never bypasses the execution context.
336
+ - [ ] **Pre-process arguments** — applies path expansion, Ruby-idiomatic defaults,
337
+ option whitelisting, deprecations.
338
+ - [ ] **Collect data** — gathers any additional information needed before or after
339
+ command execution to build the response (e.g., reading config, listing refs).
340
+ Most facade methods do not need this; flag explicitly when present.
341
+ - [ ] **Call commands** — invokes one or more `Git::Commands::*` classes; multiple
342
+ calls are sequenced explicitly with intermediate results held in local variables.
343
+ - [ ] **Build rich response objects** — passes stdout through a `Git::Parsers::*`
344
+ class or a result-class factory method to produce the documented return type.
345
+ Returning the raw `CommandLineResult` is acceptable only when that is the
346
+ documented public contract for the topic module.
347
+
348
+ ## Argument pre-processing patterns
349
+
350
+ ### Path normalization
351
+
352
+ Accept `String` or `Array<String>` for path arguments and splat into the command:
353
+
354
+ ```ruby
355
+ def add(paths = '.', **)
356
+ SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
357
+ Git::Commands::Add.new(@execution_context).call(*Array(paths), **).stdout
358
+ end
359
+ ```
360
+
361
+ For path arguments that must be absolute or relative to the worktree root, expand
362
+ with `File.expand_path` against `@execution_context.git_work_dir`.
363
+
364
+ ### Option whitelisting (preventing API expansion)
365
+
366
+ When the facade method accepts an options hash (positional `opts = {}` *or*
367
+ keyword `**options`) and forwards it to a command, the underlying command class
368
+ typically exposes many more options than the public facade contract. Without
369
+ filtering, callers could pass options that happen to match command DSL names but
370
+ were never part of the facade's public API — silently expanding the contract.
371
+
372
+ Use a per-method whitelist constant + `SharedPrivate.assert_valid_opts!`:
373
+
374
+ ```ruby
375
+ PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
376
+ private_constant :PULL_ALLOWED_OPTS
377
+
378
+ def pull(remote = nil, branch = nil, **)
379
+ raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
380
+
381
+ SharedPrivate.assert_valid_opts!(PULL_ALLOWED_OPTS, **)
382
+ positional_args = [remote, branch].compact
383
+ Git::Commands::Pull.new(@execution_context)
384
+ .call(*positional_args, no_edit: true, **)
385
+ .stdout
386
+ end
387
+ ```
388
+
389
+ The helper's signature is `assert_valid_opts!(allowed, **opts)` — the allowed
390
+ set comes first as a positional argument so callers can re-forward the
391
+ anonymous splat (`**`) into both the assertion and the command call. Name the
392
+ splat (`**options`) only when the body must inspect or mutate the options
393
+ hash before forwarding it (see the [`commit` example](#orchestration-sequence)
394
+ above for that case).
395
+
396
+ Rules:
397
+
398
+ - Name the constant `<METHOD>_ALLOWED_OPTS` and mark it `private_constant`. It
399
+ is implementation detail, not part of the public API.
400
+ - Place the constant immediately before the method definition.
401
+ - The whitelist must match the `@option` tags in the YARD doc exactly. Reviewers
402
+ should verify the two lists are equal in both directions.
403
+ - `SharedPrivate.assert_valid_opts!` raises
404
+ `ArgumentError: Unknown options: <key>` for any unrecognized key. Document this
405
+ with `@raise [ArgumentError]` on the facade method.
406
+ - Every facade method that accepts an options hash **must** have a unit test
407
+ that passes an unknown key and expects `ArgumentError`. That test — not a
408
+ defensive `slice` at the call site — is what guarantees the whitelist stays
409
+ load-bearing under future refactors. Forward `**options` directly after the
410
+ assertion; do not also `slice` it (the assertion already proves every key is
411
+ allowed, and a second mechanism invites cargo-culting and confusion about
412
+ which one enforces the contract).
413
+
414
+ Even when the facade uses `**options` keyword forwarding, whitelist explicitly.
415
+ Relying on the command's own `ArgumentError` couples the facade contract to the
416
+ command's argument DSL, which is exactly what this layer exists to prevent.
417
+
418
+ ### Deprecation handling
419
+
420
+ Handle deprecated option keys explicitly in the facade — never let deprecation
421
+ shims leak into the command class. Pattern:
422
+
423
+ ```ruby
424
+ def commit(message, opts = {})
425
+ opts = opts.merge(message: message) if message
426
+ opts = deprecate_commit_no_gpg_sign_option(opts)
427
+ opts = deprecate_commit_add_all_option(opts)
428
+ Git::Commands::Commit.new(@execution_context).call(no_edit: true, **opts).stdout
429
+ end
430
+
431
+ private
432
+
433
+ def deprecate_commit_no_gpg_sign_option(opts)
434
+ return opts unless opts.key?(:no_gpg_sign)
435
+
436
+ Git::Deprecation.warn(
437
+ "Git::Repository#commit's :no_gpg_sign option is deprecated. " \
438
+ 'Use gpg_sign: false instead.'
439
+ )
440
+ opts.dup.tap do |o|
441
+ o[:gpg_sign] = false unless o.key?(:gpg_sign)
442
+ o.delete(:no_gpg_sign)
443
+ end
444
+ end
445
+ ```
446
+
447
+ ### Defaults and policy options
448
+
449
+ The facade is where **policy defaults** are applied — options that support
450
+ non-interactive execution, control output format for parsing, or set safe
451
+ command-level defaults. The command class stays neutral; the facade makes the
452
+ defaults explicit.
453
+
454
+ There are two categories of policy defaults:
455
+
456
+ **Fixed policy defaults** are set unconditionally and are NOT included in the
457
+ method's `ALLOWED_OPTS` constant. `assert_valid_opts!` rejects any
458
+ caller-supplied value for these keys before it reaches the command call,
459
+ enforcing the policy. They are not part of the facade's public API and must
460
+ not be documented as `@option` tags.
461
+
462
+ | Policy option | Why facade sets it |
463
+ | --- | --- |
464
+ | `no_edit: true` | Subprocesses cannot launch `$EDITOR` |
465
+ | `no_progress: true` | Progress output goes to stderr and pollutes parsing |
466
+ | `no_color: true` | ANSI escapes interfere with parsers |
467
+ | `format: Git::Parsers::Foo::FORMAT_STRING` | Facade wants a parseable format |
468
+
469
+ **Overridable policy defaults** are included in `ALLOWED_OPTS`. The facade
470
+ sets a sensible default but callers may override it. Place these **before**
471
+ the caller's `**opts` in the command call so the caller's value wins on key
472
+ collision, and document them as `@option` tags since they are part of the
473
+ public API:
474
+
475
+ ```ruby
476
+ # :verbose is in ALLOWED_OPTS — caller can pass verbose: true to override
477
+ Git::Commands::Log.new(@execution_context).call(*args, verbose: false, **opts)
478
+ ```
479
+
480
+ ## Internal helpers and encapsulation
481
+
482
+ Topic modules under `lib/git/repository/` often share helper logic — option
483
+ validation, path normalization, deprecation warnings, error wrapping. These
484
+ helpers must be reachable from any topic module without leaking onto the public
485
+ `Git::Repository` API surface.
486
+
487
+ ### The rule
488
+
489
+ **Do not put shared helpers as private methods on `Git::Repository`** (directly
490
+ or via `include`). `include` copies private instance methods onto the host class,
491
+ so any caller with a `Git::Repository` instance can `repo.send(:helper, ...)`.
492
+ This:
493
+
494
+ 1. Re-creates the `Git::Lib` god-class problem the redesign is escaping.
495
+ 2. Couples every topic module silently to ambient mixin state.
496
+ 3. Is not actually private and not `@api`-marked, so YARD/tooling cannot enforce it.
497
+
498
+ ### The pattern
499
+
500
+ Put shared helpers in a sibling **internal module** under `lib/git/repository/`
501
+ that is **not** `include`d into `Git::Repository`. Use `module_function` so
502
+ methods are called as singleton methods from the topic modules:
503
+
504
+ ```ruby
505
+ # lib/git/repository/shared_private.rb
506
+ module Git
507
+ class Repository
508
+ # Namespace for internal helpers shared across facade topic modules
509
+ #
510
+ # @api private
511
+ #
512
+ module SharedPrivate
513
+ module_function
514
+
515
+ def assert_valid_opts!(allowed, **options)
516
+ unknown = options.keys - allowed
517
+ return if unknown.empty?
518
+
519
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}"
520
+ end
521
+ end
522
+
523
+ private_constant :SharedPrivate
524
+ end
525
+ end
526
+ ```
527
+
528
+ Call sites use the short unqualified form (since the constant is private,
529
+ fully-qualified external references are not possible):
530
+
531
+ ```ruby
532
+ # lib/git/repository/staging.rb
533
+ def add(paths = '.', **)
534
+ SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
535
+ Git::Commands::Add.new(@execution_context)
536
+ .call(*Array(paths), **)
537
+ .stdout
538
+ end
539
+ ```
540
+
541
+ ### Why this works
542
+
543
+ - **No mixin pollution.** `Git::Repository` instances do not gain
544
+ `assert_valid_opts!` as a method. The helper is namespaced and private-by-API.
545
+ - **Explicit dependency.** A reader of `staging.rb` sees exactly where the helper
546
+ comes from. No magic mixin chain.
547
+ - **Stateless by contract.** Without `include`, helpers cannot access
548
+ `@execution_context` or other instance state — they must take everything as
549
+ arguments. This keeps them pure and trivially unit-testable.
550
+ - **Truly private constant.** `private_constant :SharedPrivate` causes
551
+ fully-qualified external references (`Git::Repository::SharedPrivate`) to
552
+ raise a `NameError` at runtime. Callers inside the `Git::Repository` class
553
+ body (i.e. the topic modules) use the short `SharedPrivate.foo(...)` form
554
+ and are unaffected.
555
+
556
+ ### Naming rules
557
+
558
+ **Topic modules** (those `include`d in `Git::Repository`) follow the two-tier
559
+ naming convention in [Naming a new topic module](#naming-a-new-topic-module):
560
+ gerund for single-action modules, `Noun + Operations` for mixed-bag modules.
561
+
562
+ **Internal helper modules** (those **not** `include`d) use descriptive nouns,
563
+ never generic role-suffixes like `*Helpers`, `*Utils`, or `*Support`. The
564
+ `*Operations` suffix is a topic-module convention — it is not a generic role
565
+ suffix and is not prohibited here:
566
+
567
+ | Module | Distinguished by |
568
+ | -------------------------------------------------- | -------------------------------------------------- |
569
+ | `Git::Repository::Staging` | `include`d, `@api public` (gerund topic module) |
570
+ | `Git::Repository::Branching` | `include`d, `@api public` (gerund topic module) |
571
+ | `Git::Repository::RemoteOperations` | `include`d, `@api public` (`*Operations` topic module) |
572
+ | `Git::Repository::SharedPrivate` | not `include`d, `@api private`, `private_constant` |
573
+ | `Git::Repository::SharedPrivate::OptionValidation` | nested under `SharedPrivate`, `@api private` |
574
+
575
+ Reasons:
576
+
577
+ - The location (`lib/git/repository/`) plus the `@api` tag and absence of an
578
+ `include` line in `lib/git/repository.rb` already convey API status. A
579
+ `*Helpers` suffix is redundant signage.
580
+ - Symmetry with topic modules keeps the directory listing readable.
581
+ - "Helpers" invites junk-drawer dumping; responsibility-named modules invite
582
+ cohesion. Ruby stdlib follows the same convention (`URI::DEFAULT_PARSER`,
583
+ `ActiveSupport::Inflector`, not `*Helpers`).
584
+
585
+ ### Growth path
586
+
587
+ **Every time a new method is added to `SharedPrivate`**, count the total methods
588
+ and look for sub-themes. If **either** trigger fires, extract before committing
589
+ the new method:
590
+
591
+ 1. `SharedPrivate` would exceed ~5 methods after the addition.
592
+ 2. Clear sub-themes are visible (validation vs. normalization vs. error wrapping).
593
+
594
+ Then extract responsibility-named submodules **nested under `SharedPrivate`**:
595
+
596
+ ```text
597
+ Git::Repository::SharedPrivate # catch-all (initial)
598
+ ↓ grows / develops sub-themes
599
+ Git::Repository::SharedPrivate::OptionValidation # extracted by responsibility
600
+ Git::Repository::SharedPrivate::PathNormalization
601
+ Git::Repository::SharedPrivate # remaining miscellany (or deleted)
602
+ ```
603
+
604
+ Submodules live in `lib/git/repository/shared_private/option_validation.rb`.
605
+ They do not need their own `private_constant` since the parent is already
606
+ private. The extraction is mechanical — call sites change from
607
+ `SharedPrivate.foo(...)` to `SharedPrivate::OptionValidation.foo(...)`.
608
+
609
+ ### Decision 1 — Where does the helper live?
610
+
611
+ Choose placement by working through these questions in order:
612
+
613
+ 1. **Is it used by only one topic module?** → `module Private` nested inside
614
+ that topic module.
615
+ 2. **Is it used by two or more modules, and does an existing `SharedPrivate::*`
616
+ submodule cover this concern?** → Add the method to that submodule.
617
+ 3. **Is it used by two or more modules, and no fitting submodule exists?** →
618
+ Add it directly to `SharedPrivate`, then apply the growth-path check (see
619
+ [Growth path](#growth-path)) to decide whether a new submodule is now
620
+ warranted.
621
+
622
+ | Condition | Placement |
623
+ | --- | --- |
624
+ | Used by one topic module only | `module Private` nested inside that topic module |
625
+ | Shared; fitting `SharedPrivate::*` submodule exists | That submodule (e.g. `SharedPrivate::OptionValidation`) |
626
+ | Shared; no fitting submodule exists | `SharedPrivate` directly |
627
+
628
+ **Nested `module Private`** — for helpers local to one topic module.
629
+ Nest it inside the topic module, mark it `@api private`, and call its methods
630
+ as `Private.foo(...)`:
631
+
632
+ ```ruby
633
+ module Git
634
+ class Repository
635
+ module Branching
636
+ # ... public facade methods ...
637
+
638
+ # Helpers private to the `Branching` topic module
639
+ #
640
+ # @api private
641
+ module Private
642
+ module_function
643
+
644
+ # Translates checkout options into git command arguments
645
+ #
646
+ # @param branch [String, nil] the target branch name
647
+ #
648
+ # @param options [Hash] caller-supplied checkout options
649
+ #
650
+ # @return [Array] a two-element tuple of translated arguments
651
+ #
652
+ # @api private
653
+ def translate_checkout_opts(branch, options)
654
+ # ...
655
+ end
656
+ end
657
+ private_constant :Private
658
+ end
659
+ end
660
+ end
661
+ ```
662
+
663
+ **Existing `SharedPrivate::*` submodule** — check
664
+ `lib/git/repository/shared_private/` for a file whose name matches the
665
+ concern (e.g. `option_validation.rb`). If one exists, add the method there.
666
+
667
+ **`SharedPrivate` directly** — when no fitting submodule exists yet.
668
+ See [The pattern](#the-pattern) for the full skeleton. Call sites use
669
+ `SharedPrivate.foo(...)`. After adding, re-run the growth-path check.
670
+
671
+ ### Decision 2 — How is state passed to the helper?
672
+
673
+ `module_function` helpers (whether in `module Private` or `SharedPrivate`) are
674
+ stateless by design — they cannot access `@execution_context` or any other
675
+ instance state. This is intentional: stateless helpers are trivially
676
+ unit-testable and have no hidden dependencies.
677
+
678
+ When a helper needs state, pass it explicitly rather than making the helper
679
+ stateful:
680
+
681
+ 1. **Pass state as an argument** — the preferred approach. Add the execution
682
+ context (or whatever state is needed) as a positional argument:
683
+
684
+ ```ruby
685
+ module Private
686
+ module_function
687
+
688
+ def build_result(execution_context, name)
689
+ Git::Commands::Foo.new(execution_context).call(name)
690
+ end
691
+ end
692
+ ```
693
+
694
+ 2. **Extract a PORO** — when the helper has enough state and behavior to
695
+ justify its own object. Place it under `lib/git/repository/`, mark it
696
+ `@api private`, and do not `include` it. In `lib/git/repository/commit_operation.rb`:
697
+
698
+ ```ruby
699
+ # Callable helper for executing a git commit
700
+ #
701
+ # @api private
702
+ class Git::Repository::CommitOperation
703
+ def initialize(execution_context)
704
+ @execution_context = execution_context
705
+ end
706
+
707
+ def call(...)
708
+ # ...
709
+ end
710
+ end
711
+ ```
712
+
713
+ Avoid: inline private instance methods directly on the topic module (i.e.,
714
+ `def` after `private` in the module body without a `Private` namespace). This
715
+ pollutes the `Git::Repository` instance namespace with methods reachable via
716
+ `repo.send(:helper, ...)`, which is the exact problem these patterns exist to
717
+ prevent.
718
+
719
+ ### Why not `ActiveSupport::Concern`?
720
+
721
+ `Concern` does not solve the include-time leak: a `Concern` `include`d into a
722
+ class still copies its private instance methods onto that class. Rails tolerates
723
+ the leak via underscore prefixes and `:nodoc:`, or extracts service objects.
724
+ This project takes the stricter approach (sibling module + `module_function`)
725
+ without depending on `activesupport`.
726
+
727
+ ## Parser vs. raw stdout
728
+
729
+ | Situation | Use |
730
+ | --- | --- |
731
+ | The facade returns a `String` of git's stdout (chomped or as-is) | `.stdout` |
732
+ | The facade returns a structured object built from line-by-line parsing | A `Git::Parsers::*` class |
733
+ | The facade returns a single bool/int derived from output | Inline transformation in the facade |
734
+ | The facade returns a `Git::CommandLineResult` | Return the raw result |
735
+
736
+ If the parsing logic exceeds ~5 lines, extract it into a `Git::Parsers::*` class
737
+ and call it from the facade. The facade method remains an orchestration sequence,
738
+ not a parser.
739
+
740
+ ## Result-class factory methods
741
+
742
+ When the facade returns a domain object (e.g. `BranchInfo`, `BranchDeleteResult`,
743
+ `DiffResult`), use a factory method on the result class rather than constructing
744
+ it inline:
745
+
746
+ ```ruby
747
+ def branch_delete(name, force: false)
748
+ result = Git::Commands::Branch::Delete.new(@execution_context).call(name, force: force)
749
+ Git::BranchDeleteResult.from(name: name, command_result: result)
750
+ end
751
+ ```
752
+
753
+ This keeps result-object construction in one place per type and makes parsers
754
+ reusable across facade methods.
755
+
756
+ ## Common failures
757
+
758
+ ### One-line delegation when orchestration is needed
759
+
760
+ If the facade method discards information the caller documented as part of the
761
+ return type (e.g. returns `result.stdout` when the caller expects a parsed Hash),
762
+ the one-line form is wrong. Expand to the orchestration sequence and call the
763
+ appropriate parser.
764
+
765
+ ### Leaking command-class types into the public API
766
+
767
+ The public return type should never be `Git::Commands::Foo::Bar::SomeResult` or
768
+ any other type from `Git::Commands::*`. Returning `Git::CommandLineResult` is
769
+ acceptable when the topic module documents that as its contract, but is not the
770
+ default — see [Choosing the return type](#choosing-the-return-type). Returning
771
+ domain objects (`Git::BranchInfo`, `Git::DiffResult`, etc.) is preferred for
772
+ methods that produce structured data.
773
+
774
+ ### Exposing command-DSL-shaped argv in the facade signature
775
+
776
+ The facade signature is a Ruby API, not a transcription of the git CLI. Accepting
777
+ a free-form `*args` that is forwarded straight to the command, or naming keyword
778
+ arguments after CLI flags (e.g. `no_ff:`, `set_upstream_to:`) instead of
779
+ Ruby-idiomatic names, leaks the command DSL into the public contract. Define the
780
+ signature from the caller's perspective; translate to command DSL inside the
781
+ body.
782
+
783
+ ### Changing the legacy return type or signature on extraction
784
+
785
+ When a facade method is extracted from `Git::Base` / `Git::Lib`, returning a
786
+ different type, accepting a different positional/keyword shape, or changing
787
+ nil-handling silently breaks every caller. Capture the legacy contract in the
788
+ Step 2 plan and diff before/after — see
789
+ [Choosing the return type](#choosing-the-return-type) rule 1 and
790
+ [Choosing the method signature](#choosing-the-method-signature) rule 1.
791
+
792
+ ### Bypassing `@execution_context`
793
+
794
+ Constructing a command with anything other than `@execution_context` (e.g.
795
+ `Git::Commands::Add.new(self)` from inside a facade method) is wrong. The facade
796
+ holds a `Git::ExecutionContext::Repository`; commands must always be constructed
797
+ with it.
798
+
799
+ ### Placing an overridable policy default after caller options
800
+
801
+ This anti-pattern applies to **overridable** policy defaults (those in `ALLOWED_OPTS`).
802
+ Placing the default after `**opts` silently overwrites the caller's explicit value:
803
+
804
+ ```ruby
805
+ # ❌ Wrong — caller's :verbose is silently discarded
806
+ Git::Commands::Log.new(@execution_context).call(*args, **opts, verbose: false)
807
+
808
+ # ✅ Correct — caller's :verbose wins because opts is splatted last
809
+ Git::Commands::Log.new(@execution_context).call(*args, verbose: false, **opts)
810
+ ```
811
+
812
+ Place overridable policy defaults before the caller's `**opts` so the caller's
813
+ value wins on key collision. This does not apply to fixed policy options (not in
814
+ `ALLOWED_OPTS`): `assert_valid_opts!` prevents those keys from reaching the
815
+ command call at all.
816
+
817
+ ### Skipping option whitelisting on opaque opts hashes
818
+
819
+ If the facade accepts an options hash (positional `opts = {}` *or* keyword
820
+ `**options`), it must call `SharedPrivate.assert_valid_opts!` against a
821
+ `private_constant`-marked `<METHOD>_ALLOWED_OPTS` constant. Without it,
822
+ callers can silently pass any key the command DSL happens to accept, which is
823
+ API expansion that the facade did not commit to.
824
+
825
+ ### Mixing facade and command responsibilities
826
+
827
+ The facade does not build CLI argv. The command does not pre-process Ruby
828
+ arguments or parse output. If a facade method calls `command(...)` directly
829
+ (rather than `Git::Commands::*.new(...).call(...)`) it is bypassing the command
830
+ layer; refactor by introducing or extending the appropriate command class first.
831
+
832
+ ### Adding a topic module whose methods fit an existing one
833
+
834
+ A topic module is not justified when its method(s) would fit naturally in an
835
+ existing module. Before creating a new module, scan existing modules for a
836
+ plausible home. The absence of a method-count threshold is not an invitation
837
+ to fragment the API — place the method in the closest existing module unless
838
+ the topic is genuinely distinct and the method would be awkward there. See
839
+ [Decision rules for adding a new module](#decision-rules-for-adding-a-new-module)
840
+ for the full two-criteria test.