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,487 @@
1
+ ---
2
+ name: extract-command-from-lib
3
+ description: "Migrates a direct #command call in Git::Lib to a Git::Commands::* class as part of the architectural redesign. Use when extracting a specific command during the Strangler Fig migration."
4
+ ---
5
+
6
+ # Extract Command from Lib
7
+
8
+ Replace a direct `#command` call in `Git::Lib` with a call to a `Git::Commands::*`
9
+ class. The git subcommand is determined by the first (or first few) arguments to the
10
+ `#command` method call.
11
+
12
+ ## Contents
13
+
14
+ - [How to use this skill](#how-to-use-this-skill)
15
+ - [Prerequisites](#prerequisites)
16
+ - [Related skills](#related-skills)
17
+ - [Input](#input)
18
+ - [Workflow](#workflow)
19
+ - [Branch setup](#branch-setup)
20
+ - [Step 1 — Identify the `#command` call](#step-1-identify-the-command-call)
21
+ - [Step 2 — Plan the migration and get approval](#step-2-plan-the-migration-and-get-approval)
22
+ - [Step 3 — Ensure adequate legacy tests](#step-3-ensure-adequate-legacy-tests)
23
+ - [Step 4 — Ensure the `Git::Commands::*` class exists](#step-4-ensure-the-gitcommands-class-exists)
24
+ - [Step 5 — Update `Git::Lib` to delegate to the command class](#step-5-update-gitlib-to-delegate-to-the-command-class)
25
+ - [Commit discipline](#commit-discipline)
26
+ - [Create a pull request](#create-a-pull-request)
27
+ - [Quality gates (run at every step)](#quality-gates-run-at-every-step)
28
+ - [Common patterns](#common-patterns)
29
+ - [Simple delegation (stdout passthrough)](#simple-delegation-stdout-passthrough)
30
+ - [Delegation with post-processing](#delegation-with-post-processing)
31
+ - [Delegation with parsed return value](#delegation-with-parsed-return-value)
32
+ - [Delegation with opts-hash key normalization](#delegation-with-opts-hash-key-normalization)
33
+ - [Delegation with option filtering (preventing API expansion)](#delegation-with-option-filtering-preventing-api-expansion)
34
+ - [What stays in `Git::Lib`](#what-stays-in-gitlib)
35
+ - [What moves to `Git::Commands::*`](#what-moves-to-gitcommands)
36
+
37
+ ## How to use this skill
38
+
39
+ Attach this file to your Copilot Chat context, then invoke it with a short message
40
+ identifying the `Git::Lib` method or `#command` call to migrate. Examples:
41
+
42
+ ```text
43
+ Using the Extract Command from Lib skill, migrate Git::Lib#worktree_add —
44
+ it calls command('worktree', 'add', ...).
45
+ ```
46
+
47
+ ```text
48
+ Extract Command from Lib: command('ls-tree', ...)
49
+ ```
50
+
51
+ The invocation needs either the `Git::Lib` method name or the git subcommand string
52
+ from the `#command` call (or both).
53
+
54
+ ## Prerequisites
55
+
56
+ Before starting, you **MUST** load the following skill(s) in their entirety:
57
+
58
+ - [YARD Documentation](../yard-documentation/SKILL.md) — authoritative
59
+ source for YARD formatting rules and writing standards;
60
+
61
+ ## Related skills
62
+
63
+ Run or reference these skills during the workflow:
64
+
65
+ - [Command Implementation](../command-implementation/SKILL.md) — generates and reviews `Git::Commands::*`
66
+ classes, unit tests, integration tests, and YARD docs (used in Step 4 if the
67
+ command class does not exist yet); also the canonical class-shape checklist,
68
+ phased rollout gates, and internal compatibility contracts
69
+ - [Review Arguments DSL](../review-arguments-dsl/SKILL.md) — verifying DSL entries match git CLI
70
+ - [Command Test Conventions](../command-test-conventions/SKILL.md) — unit/integration test conventions for command classes
71
+ - [Command YARD Documentation](../command-yard-documentation/SKILL.md) — documentation completeness for command classes
72
+ - [Review Cross-Command Consistency](../review-cross-command-consistency/SKILL.md) — sibling consistency within a command family
73
+ - [Review Backward Compatibility](../review-backward-compatibility/SKILL.md) — preserving `Git::Lib` return-value contracts
74
+ - [Extract Facade from Base/Lib](../extract-facade-from-base-lib/SKILL.md) — the
75
+ follow-on extraction that moves the public method from `Git::Base` /
76
+ `Git::Lib` into a `Git::Repository::*` facade method (Phase 4 deletes both
77
+ `Git::Base` and `Git::Lib`)
78
+
79
+ ## Input
80
+
81
+ Required:
82
+
83
+ 1. A `Git::Lib` method that contains one or more `command(...)` calls to replace
84
+ 2. The git subcommand name (derived from the first arguments to `#command`)
85
+
86
+ ## Workflow
87
+
88
+ ### Branch setup
89
+
90
+ All work must be done on a feature branch. **Never commit or push directly to
91
+ `main`.**
92
+
93
+ Before starting, create a new branch:
94
+
95
+ ```bash
96
+ git checkout -b <feature-branch-name>
97
+ ```
98
+
99
+ All commits in this workflow go on the feature branch. When work is complete,
100
+ open a pull request — do not merge or push directly into `main`.
101
+
102
+ ### Step 1 — Identify the `#command` call
103
+
104
+ 1. Locate the `Git::Lib` method that calls `command(...)`.
105
+ 2. Note:
106
+ - the git subcommand (first argument(s) to `#command`)
107
+ - the options/arguments passed after the subcommand
108
+ - execution options (e.g., `timeout:`, `out:`, `err:`, `env:`)
109
+ - the return value and any post-processing (`.stdout`, parsing, regex matching)
110
+ 3. Document the method's current **public contract**: signature, return type, and
111
+ return-value format (String, Array, Hash, Boolean, etc.)
112
+ 4. Run linters and rubocop to confirm a clean baseline:
113
+
114
+ ```bash
115
+ bundle exec rubocop
116
+ ```
117
+
118
+ Fix any issues before continuing.
119
+
120
+ ### Step 2 — Plan the migration and get approval
121
+
122
+ Before writing or changing any code, present a migration plan and **wait for
123
+ explicit confirmation** from the user. Do not proceed until they approve.
124
+
125
+ The plan must cover every `#command` call identified above. For each one, state:
126
+
127
+ | `Git::Lib` method | `#command` call | Target `Git::Commands` class | Class exists? | Notes |
128
+ | --- | --- | --- | --- | --- |
129
+ | `some_method` | `command('sub', '--flag', arg)` | `Git::Commands::Sub` (new) or existing | ✅ / 🆕 | any mapping decisions |
130
+
131
+ Also state:
132
+
133
+ - Which (if any) new `Git::Commands::*` classes need to be created
134
+ - How optional or empty arguments will be handled (e.g., nil vs `''` operands)
135
+ - Any return-value post-processing that stays in `Git::Lib`
136
+
137
+ Then ask:
138
+
139
+ > Does this mapping look correct? Any changes before I start implementing?
140
+
141
+ **Do not move to Step 3 until the user confirms the plan.**
142
+
143
+ ### Step 3 — Ensure adequate legacy tests
144
+
145
+ Before making any changes, verify that `tests/units/` has adequate tests for the
146
+ `Git::Lib` method being migrated.
147
+
148
+ 1. Search existing legacy tests for coverage of the method:
149
+
150
+ ```bash
151
+ grep -rn '<method_name>' tests/units/
152
+ ```
153
+
154
+ 2. If coverage is insufficient, add **minimal new tests** to the legacy test suite
155
+ that exercise the method's current behavior. These tests ensure the refactor does
156
+ not break backward compatibility.
157
+ - Do **not** change existing tests.
158
+ - Follow existing legacy test conventions (`Test::Unit::TestCase`,
159
+ `assert_command_line_eq`, `in_temp_dir`, etc.).
160
+ - Verify new tests pass:
161
+
162
+ ```bash
163
+ bundle exec bin/test <test-file-basename>
164
+ ```
165
+
166
+ - Run rubocop against the new test file:
167
+
168
+ ```bash
169
+ bundle exec rubocop tests/units/<test-file>
170
+ ```
171
+
172
+ - Fix any issues before continuing.
173
+ 3. Commit the new legacy tests:
174
+
175
+ ```bash
176
+ git add tests/units/<test-file>
177
+ git commit -m "refactor(test): add legacy tests for <method_name>"
178
+ ```
179
+
180
+ ### Step 4 — Ensure the `Git::Commands::*` class exists
181
+
182
+ 1. Search `lib/git/commands/` for an existing command class that matches the git
183
+ subcommand:
184
+
185
+ ```bash
186
+ find lib/git/commands -name '*.rb' | sort
187
+ ```
188
+
189
+ Also check the class contents to confirm the existing class covers the same
190
+ subcommand variation (e.g., `branch --show-current` vs. `branch --list`).
191
+
192
+ 2. **If the command class already exists**, skip to Step 5.
193
+
194
+ 3. **If the command class does not exist**, scaffold it using the
195
+ [Command Implementation](../command-implementation/SKILL.md) skill. This produces:
196
+
197
+ - `lib/git/commands/<command>.rb` (or `lib/git/commands/<family>/<action>.rb`)
198
+ - `spec/unit/git/commands/<command>_spec.rb`
199
+ - `spec/integration/git/commands/<command>_spec.rb`
200
+
201
+ 4. Verify the new command class:
202
+
203
+ ```bash
204
+ bundle exec rspec spec/unit/git/commands/<command>_spec.rb
205
+ bundle exec rspec spec/integration/git/commands/<command>_spec.rb
206
+ bundle exec rubocop lib/git/commands/<command>.rb
207
+ bundle exec rake yard
208
+ ```
209
+
210
+ Fix any issues before continuing.
211
+
212
+ 5. Commit the new command class and its tests:
213
+
214
+ ```bash
215
+ git add lib/git/commands/<command>*.rb spec/
216
+ git commit -m "refactor(command): add Git::Commands::<Command> class"
217
+ ```
218
+
219
+ ### Step 5 — Update `Git::Lib` to delegate to the command class
220
+
221
+ 1. Replace the `command(...)` call with a call to the `Git::Commands::*` class:
222
+
223
+ ```ruby
224
+ # Before
225
+ def some_method(args)
226
+ command('some-command', '--flag', args).stdout
227
+ end
228
+
229
+ # After
230
+ def some_method(args)
231
+ Git::Commands::SomeCommand.new(self).call(args, flag: true).stdout
232
+ end
233
+ ```
234
+
235
+ 2. Preserve the method's **exact return value contract** — apply any parsing or
236
+ transformation after `.stdout` / `.stderr` / `.status` to match the original
237
+ return type.
238
+
239
+ 3. **Prevent API expansion** — the command class may accept many more options than
240
+ the legacy `Git::Lib` method ever exposed. Only forward the options that were
241
+ part of the original `Git::Lib` method's public API. Use a `<COMMAND>_ALLOWED_OPTS`
242
+ constant to whitelist permitted option keys, call `assert_valid_opts` to raise
243
+ on unknown keys, then filter with `opts.slice` before forwarding:
244
+
245
+ ```ruby
246
+ PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
247
+
248
+ def pull(remote = nil, branch = nil, opts = {})
249
+ assert_valid_opts(opts, PULL_ALLOWED_OPTS)
250
+ allowed_opts = opts.slice(*PULL_ALLOWED_OPTS)
251
+ Git::Commands::Pull.new(self).call(remote, branch, **allowed_opts).stdout
252
+ end
253
+ ```
254
+
255
+ `assert_valid_opts` raises `ArgumentError` for any unrecognised key, giving
256
+ callers a clear error instead of silently ignoring unknown options. This
257
+ ensures that callers cannot accidentally pass options that happen to match
258
+ command DSL option names but were never part of the public contract.
259
+
260
+ 4. Add the appropriate `require_relative` at the top of `lib/git/lib.rb` if not
261
+ already present.
262
+
263
+ 4. Verify:
264
+
265
+ ```bash
266
+ bundle exec bin/test <legacy-test-file-basename>
267
+ bundle exec rspec
268
+ bundle exec rubocop
269
+ bundle exec rake yard
270
+ ```
271
+
272
+ Fix any issues before continuing.
273
+
274
+ 5. Commit the `Git::Lib` change:
275
+
276
+ ```bash
277
+ git add lib/git/lib.rb
278
+ git commit -m "refactor(lib): delegate <method_name> to Git::Commands::<Command>"
279
+ ```
280
+
281
+ ## Commit discipline
282
+
283
+ Keep work organized into **three logical commit categories** (each optional if no
284
+ changes were needed for that step):
285
+
286
+ 1. `refactor(test): add legacy tests for <method_name>` — new tests in
287
+ `tests/units/`
288
+ 2. `refactor(command): add Git::Commands::<Command> class` — new command class,
289
+ unit specs, and integration specs
290
+ 3. `refactor(lib): delegate <method_name> to Git::Commands::<Command>` — `Git::Lib`
291
+ changes only
292
+
293
+ During implementation, you may use multiple task-level commits. Before opening a
294
+ PR, follow the repository finalize workflow (see
295
+ [Development Workflow](../development-workflow/SKILL.md)) and squash commits as
296
+ required.
297
+
298
+ **Issue and PR references in commit bodies:** Do not use `#<number>` in the
299
+ commit body — write `issue 1000` not `issue #1000`. A commitlint parser flaw
300
+ treats any line containing `#<number>` as a footer token, breaking the
301
+ body/footer split. To close an issue/PR, use `Closes`/`Fixes`/`Resolves #<number>`
302
+ in the footer. To merely mention one for context, omit the `#` and no footer line
303
+ is needed.
304
+
305
+ If further changes are needed after task commits are created:
306
+
307
+ - Amend the change to the **appropriate commit** (e.g., a command class fix goes
308
+ into the `refactor(command)` commit).
309
+ - Rebase the later commits on top:
310
+
311
+ ```bash
312
+ git rebase -i <base-commit>
313
+ ```
314
+
315
+ - After rebasing, verify all quality gates still pass:
316
+
317
+ ```bash
318
+ bundle exec rspec && bundle exec rake test && bundle exec rubocop && bundle exec rake yard
319
+ ```
320
+
321
+ ## Create a pull request
322
+
323
+ Once all commits are clean and quality gates pass, create a PR for the branch.
324
+
325
+ If changes are made after the PR is created:
326
+
327
+ - Amend the change to the appropriate commit.
328
+ - Rebase later commits on top.
329
+ - Force-push the branch:
330
+
331
+ ```bash
332
+ git push --force-with-lease
333
+ ```
334
+
335
+ ## Quality gates (run at every step)
336
+
337
+ ```bash
338
+ bundle exec rspec
339
+ bundle exec rake test
340
+ bundle exec rubocop
341
+ bundle exec rake yard
342
+ ```
343
+
344
+ All four must pass before committing at each step. If errors are found, fix them
345
+ before continuing.
346
+
347
+ ## Common patterns
348
+
349
+ ### Simple delegation (stdout passthrough)
350
+
351
+ ```ruby
352
+ # Before
353
+ def symbolic_ref(branch_name)
354
+ command('symbolic-ref', 'HEAD', "refs/heads/#{branch_name}")
355
+ end
356
+
357
+ # After
358
+ def symbolic_ref(branch_name)
359
+ Git::Commands::SymbolicRef.new(self).call(branch_name).stdout
360
+ end
361
+ ```
362
+
363
+ ### Delegation with post-processing
364
+
365
+ ```ruby
366
+ # Before
367
+ def cat_file_type(object)
368
+ command('cat-file', '-t', object).stdout
369
+ end
370
+
371
+ # After
372
+ def cat_file_type(object)
373
+ Git::Commands::CatFile::Type.new(self).call(object).stdout
374
+ end
375
+ ```
376
+
377
+ ### Delegation with parsed return value
378
+
379
+ ```ruby
380
+ # Before
381
+ def worktree_list
382
+ worktrees = {}
383
+ command('worktree', 'list', '--porcelain').stdout.split("\n").each do |w|
384
+ # ... parsing ...
385
+ end
386
+ worktrees
387
+ end
388
+
389
+ # After
390
+ def worktree_list
391
+ result = Git::Commands::Worktree::List.new(self).call
392
+ worktrees = {}
393
+ result.stdout.split("\n").each do |w|
394
+ # ... parsing stays in Git::Lib ...
395
+ end
396
+ worktrees
397
+ end
398
+ ```
399
+
400
+ ### Delegation with opts-hash key normalization
401
+
402
+ When the legacy method accepted a flat `opts` hash and uses a `KEY_NORMALIZATIONS`
403
+ constant to rename option keys before forwarding them, the constant's keys must be
404
+ the same type as the keys callers actually pass.
405
+
406
+ **Ruby's `'key':` symbol-literal syntax creates a *symbol* key** — `{ 'update-head-ok': :x }`
407
+ stores the key `:'update-head-ok'`, not the string `'update-head-ok'`. If legacy
408
+ callers pass string keys, the lookup misses and the raw key is forwarded unchanged,
409
+ causing a git "unsupported option" error at runtime.
410
+
411
+ Always symbolize keys before the normalization lookup:
412
+
413
+ ```ruby
414
+ # ❌ Bug — string key 'update-head-ok' misses the symbol key :'update-head-ok'
415
+ opts = opts.transform_keys { |k| KEY_NORMALIZATIONS.fetch(k, k) }
416
+
417
+ # ✅ Correct — symbolize first so both string and symbol callers match
418
+ opts = opts.transform_keys do |k|
419
+ sym = k.is_a?(Symbol) ? k : k.to_sym
420
+ KEY_NORMALIZATIONS.fetch(sym, sym)
421
+ end
422
+ ```
423
+
424
+ **When to apply this pattern:** Whenever `transform_keys` is combined with a
425
+ normalization constant whose keys use the `'hyphenated-name':` symbol-literal
426
+ syntax. Scan the constant's definition to confirm its keys are symbols, then
427
+ confirm whether existing callers pass strings or symbols. If callers are a mix —
428
+ or if callers are `Git::Base` or `Git::Lib` methods that forward user-supplied
429
+ hashes — add the symbolization guard.
430
+
431
+ ### Delegation with option filtering (preventing API expansion)
432
+
433
+ A command class may expose many more options than the legacy `Git::Lib` method
434
+ ever accepted. Without filtering, callers could accidentally pass options that
435
+ happen to match command DSL names but were never part of the public contract.
436
+
437
+ The facade is also where **policy options** are set as safe defaults — options
438
+ that support non-interactive execution, control output format for parsing, or
439
+ set other command-level defaults. The command class stays neutral; the facade
440
+ makes the defaults explicit. Some defaults are **fixed** (not in `ALLOWED_OPTS` —
441
+ `assert_valid_opts` rejects them if a caller supplies them); others are
442
+ **overridable** (in `ALLOWED_OPTS`, placed before `**opts` so the caller's value
443
+ wins on collision). Examples: `no_edit: true`, `verbose: true`,
444
+ `no_progress: true`, `no_color: true`.
445
+ See "Command-layer neutrality" in CONTRIBUTING.md.
446
+
447
+ Declare an `<COMMAND>_ALLOWED_OPTS` constant listing only the options that were
448
+ present in the original method. Call `assert_valid_opts` first to raise
449
+ `ArgumentError` on unrecognised keys, then use `opts.slice` to filter before
450
+ forwarding:
451
+
452
+ ```ruby
453
+ # Only :allow_unrelated_histories was accepted by the original Git::Lib#pull
454
+ PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
455
+
456
+ def pull(remote = nil, branch = nil, opts = {})
457
+ raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
458
+
459
+ assert_valid_opts(opts, PULL_ALLOWED_OPTS)
460
+ allowed_opts = opts.slice(*PULL_ALLOWED_OPTS)
461
+ positional_args = [remote, branch].compact
462
+ # no_edit: true is the non-interactive default (see CONTRIBUTING.md)
463
+ Git::Commands::Pull.new(self).call(*positional_args, no_edit: true, **allowed_opts).stdout
464
+ end
465
+ ```
466
+
467
+ `assert_valid_opts` is a private helper already defined in `Git::Lib` — no extra
468
+ require is needed. It raises `ArgumentError: Unknown options: <key>` when any
469
+ unrecognised key is present, giving callers a clear error rather than silently
470
+ dropping the option.
471
+
472
+ Name the constant after the git subcommand (`PULL_ALLOWED_OPTS`, `FETCH_ALLOWED_OPTS`,
473
+ etc.) and place it immediately before the method definition.
474
+
475
+ ## What stays in `Git::Lib`
476
+
477
+ - Output parsing and transformation (until a parser class is created)
478
+ - Return-value adaptation to preserve backward compatibility
479
+ - Option validation and filtering to prevent API expansion (see `<COMMAND>_ALLOWED_OPTS` + `assert_valid_opts` pattern)
480
+ - Deprecation shims (e.g., option renames)
481
+ - Method signatures and public API surface
482
+
483
+ ## What moves to `Git::Commands::*`
484
+
485
+ - Argument building and CLI flag generation
486
+ - `#command` invocation
487
+ - Exit-status handling via `allow_exit_status`