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
@@ -1,65 +1,1267 @@
1
1
  # Implementation Plan for Git Gem Redesign (v5.0.0)
2
2
 
3
- This document outlines a step-by-step plan to implement the proposed architectural redesign. The plan is structured to be incremental, ensuring that the gem remains functional and passes its test suite after each major step. This approach minimizes risk and allows for a gradual, controlled migration to the new architecture.
3
+ This document outlines a step-by-step plan to implement the proposed architectural
4
+ redesign. The plan is structured to be incremental, ensuring that the gem remains
5
+ functional and passes its test suite after each major step. This approach minimizes
6
+ risk and allows for a gradual, controlled migration to the new architecture.
4
7
 
8
+ - [Progress Tracker](#progress-tracker)
9
+ - [Facade Modules Completed](#facade-modules-completed)
10
+ - [Facade module naming convention](#facade-module-naming-convention)
11
+ - [Next Task](#next-task)
12
+ - [Phase 3 Public Interface Completion](#phase-3-public-interface-completion)
13
+ - [Workstream A — Fill facade coverage gaps](#workstream-a--fill-facade-coverage-gaps)
14
+ - [Workstream B — C0: Redirect `Git::Base` factory methods to `facade_repository`](#workstream-b--c0-redirect-gitbase-factory-methods-to-facade_repository)
15
+ - [Workstream C — C1: Prepare and flip top-level entry points to return `Git::Repository`](#workstream-c--c1-prepare-and-flip-top-level-entry-points-to-return-gitrepository)
16
+ - [Workstream D — C2+C3: Remove compatibility bridges](#workstream-d--c2c3-remove-compatibility-bridges)
17
+ - [Workstream E — Migrate or deprecate instance helper methods](#workstream-e--migrate-or-deprecate-instance-helper-methods)
18
+ - [Workstream F — `Git` module utility methods still using `Git::Lib` directly](#workstream-f--git-module-utility-methods-still-using-gitlib-directly)
19
+ - [Phase 3 dependency order](#phase-3-dependency-order)
20
+ - [Phase 3 steps and release compatibility](#phase-3-steps-and-release-compatibility)
21
+ - [Phase 3 completion criteria](#phase-3-completion-criteria)
22
+ - [Facade coverage checklist](#facade-coverage-checklist)
23
+ - [Quality gates (per step)](#quality-gates-per-step)
24
+ - [Reference Files](#reference-files)
5
25
  - [Phase 1: Foundation and Scaffolding](#phase-1-foundation-and-scaffolding)
6
26
  - [Phase 2: The Strangler Fig Pattern - Migrating Commands](#phase-2-the-strangler-fig-pattern---migrating-commands)
27
+ - [Key Architectural Insight: Git::Lib as the Adapter Layer](#key-architectural-insight-gitlib-as-the-adapter-layer)
28
+ - [Architectural Insights from Command Migrations](#architectural-insights-from-command-migrations)
29
+ - [Command Migration Checklist](#command-migration-checklist)
30
+ - [✅ Migrated Commands](#-migrated-commands)
31
+ - [⏳ Commands To Migrate](#-commands-to-migrate)
7
32
  - [Phase 3: Refactoring the Public Interface](#phase-3-refactoring-the-public-interface)
8
33
  - [Phase 4: Final Cleanup and Release Preparation](#phase-4-final-cleanup-and-release-preparation)
9
34
 
10
- ## Phase 1: Foundation and Scaffolding
35
+ ## Progress Tracker
36
+
37
+ | Phase | Status | Description | Estimated Effort | Percent Complete |
38
+ | ----- | ------ | ----------- | :--------------: | :--------------: |
39
+ | Phase 1 | ✅ Complete | Foundation and scaffolding | 5% | 100% |
40
+ | Phase 2 | ✅ Complete | Migrating commands (all checklist items done) | 40% | 100% |
41
+ | Phase 3 | ⏳ In Progress | Refactoring public interface — see [Facade Modules Completed](#facade-modules-completed) and [Facade coverage checklist](#facade-coverage-checklist) | 45% | 50% |
42
+ | Phase 4 | 🔲 Not Started | Final cleanup and release | 10% | 0% |
43
+ | **TOTAL** | -- | -- | **100%** | **68%** |
44
+
45
+ ### Facade Modules Completed
46
+
47
+ | Module | File | Included in `Git::Repository` | `Git::Base` delegates |
48
+ | ------ | ---- | ------------------------------ | --------------------- |
49
+ | `Git::Repository::Staging` | `lib/git/repository/staging.rb` | ✅ | `add`, `reset`, `rm`, `clean`, `ignored_files` |
50
+ | `Git::Repository::Committing` | `lib/git/repository/committing.rb` | ✅ | `commit`, `commit_all`, `write_tree`; `commit_tree` and `write_and_commit_tree` wrap the SHA result in `Git::Object::Commit.new(self, ...)` |
51
+ | `Git::Repository::Branching` | `lib/git/repository/branching.rb` | ✅ | `checkout`, `checkout_file`, `checkout_index`, `current_branch`, `local_branch?`, `remote_branch?`, `branch?`, `branch_delete`, `branch_new`, `branch_contains`, `branches_all`, `update_ref` |
52
+ | `Git::Repository::Merging` | `lib/git/repository/merging.rb` | ✅ | `merge`, `revert`, `each_conflict`; `merge_base` wraps the returned SHA strings in `Git::Object::Commit.new(self, ...)` instances |
53
+ | `Git::Repository::RemoteOperations` | `lib/git/repository/remote_operations.rb` | ✅ | `fetch`, `pull`, `push`, `add_remote`, `remove_remote`, `config_remote`, `remotes`, `set_remote_url`, `remote_set_branches` |
54
+ | `Git::Repository::Stashing` | `lib/git/repository/stashing.rb` | ✅ | `stash_save`, `stash_apply`, `stash_clear`, `stashes_all` |
55
+ | `Git::Repository::Diffing` | `lib/git/repository/diffing.rb` | ✅ | `diff_path_status`, `diff_name_status`, `diff_full` |
56
+ | `Git::Repository::Inspecting` | `lib/git/repository/inspecting.rb` | ✅ | `show`, `fsck` |
57
+ | `Git::Repository::ObjectOperations` | `lib/git/repository/object_operations.rb` | ✅ | `rev_parse`, `tree_depth`, `ls_tree`, `grep`, `archive`, `tags`, `add_tag`, `delete_tag` |
58
+ | `Git::Repository::Logging` | `lib/git/repository/logging.rb` | ✅ | `log`, `full_log_commits` |
59
+ | `Git::Repository::StatusOperations` | `lib/git/repository/status_operations.rb` | ✅ | `ls_files`, `no_commits?` (renamed from `Git::Lib#empty?`), `untracked_files`, `status`; `Git::Base#ls_files` delegates to facade |
60
+ | `Git::Repository::Configuring` | `lib/git/repository/configuring.rb` | ✅ | `config`; `Git::Base#config` delegates to facade |
61
+ | `Git::Repository::WorktreeOperations` | `lib/git/repository/worktree_operations.rb` | ✅ | `worktrees_all`, `worktree_add`, `worktree_remove`, `worktree_prune`, `worktree`, `worktrees` |
62
+
63
+ #### Facade module naming convention
64
+
65
+ New topic modules follow a **two-tier** convention:
66
+
67
+ - **Gerund** (verb-ing) when a single action word clearly names the whole module:
68
+ `Staging`, `Committing`, `Branching`, `Merging`, `Logging`, `Diffing`, `Stashing`, `Configuring`.
69
+ - **Noun + `Operations`** when the module is a mixed bag of methods grouped by git
70
+ concept rather than a single action: `RemoteOperations`, `ObjectOperations`,
71
+ `StatusOperations`, `WorktreeOperations`.
72
+
73
+ Do **not** use plain nouns that clash with existing domain-object class names
74
+ such as `Branch`, `Diff`, `Log`, `Object`, `Remote`, `Status`, `Worktree`, etc.
75
+
76
+ ### Next Task
77
+
78
+ #### Phase 3 Public Interface Completion
79
+
80
+ All 9 domain-object migrations are ✅ complete:
81
+
82
+ | Domain objects | PRs |
83
+ | -------------- | --- |
84
+ | `Git::Stash` + `Git::Stashes` | [PR #1306](https://github.com/ruby-git/ruby-git/pull/1306) |
85
+ | `Git::DiffPathStatus` | — |
86
+ | `Git::Object::*` | — |
87
+ | `Git::Log` | [PR #1327](https://github.com/ruby-git/ruby-git/pull/1327) |
88
+ | `Git::Diff` + `Git::DiffStats` | — |
89
+ | `Git::Status` | — |
90
+ | `Git::Branch` + `Git::Remote` | — |
91
+ | `Git::Branches` | [PR #1356](https://github.com/ruby-git/ruby-git/pull/1356), [PR #1357](https://github.com/ruby-git/ruby-git/pull/1357), [PR #1358](https://github.com/ruby-git/ruby-git/pull/1358), [PR #1359](https://github.com/ruby-git/ruby-git/pull/1359) |
92
+ | `Git::Worktree` + `Git::Worktrees` | — |
93
+
94
+ The remaining work splits into six workstreams:
95
+
96
+ - Workstream A adds missing `Git::Repository` facade methods
97
+ - B wires `Git::Base` factory methods to call the facade
98
+ - C moves construction/global state and flips the top-level entry points
99
+ - D removes compatibility bridges
100
+ - E migrates helper/path-context methods
101
+ - F removes the last `Git` module utility calls that still route through `Git::Lib`
102
+
103
+ The required `Git::Commands::*` classes already exist (`Clone`, `LsRemote`,
104
+ `ConfigOptionSyntax::*`, `Rm`, `Clean`, `Show`, `Fsck`, etc.). The remaining work is
105
+ facade/adapter wiring, parser reuse, and public-API parity decisions — not new
106
+ command-layer scaffolding. The full scope is defined in Workstreams A–F below.
107
+
108
+ **Sequencing** (see [Phase 3 dependency order](#phase-3-dependency-order) for the
109
+ reasoning behind each edge):
110
+
111
+ ```mermaid
112
+ graph LR
113
+ A1 --> C1c-2
114
+ A2 --> C1c-2
115
+ A3 --> B --> C1c-2
116
+ A3 --> C1c-2
117
+ A4 --> C1c-2
118
+ C1a-1 --> C1a-2
119
+ C1a-1 --> E --> C1c-2
120
+ C1a-1 --> C1c-2
121
+ C1a-2 --> C1c-2
122
+ C1b --> C1c-2
123
+ C1c-1 --> C1c-2
124
+ C1c-2 --> C1d
125
+ C1d --> D1 --> Phase4["Phase 4"]
126
+ D2 --> Phase4
127
+ F1 --> Phase4
128
+ F2 --> Phase4
129
+ ```
130
+
131
+ ---
132
+
133
+ #### Workstream A — Fill facade coverage gaps
134
+
135
+ `Git::Base` still calls `lib.*` directly for 11 high-priority methods that have no
136
+ `Git::Repository` counterpart yet. Each step below adds the missing facade methods and
137
+ updates `Git::Base` to delegate.
138
+
139
+ **Step A1 — Extend `Git::Repository::Staging`: `rm`, `clean`, `ignored_files`** ✅
140
+
141
+ | `Git::Base` method | Facade to add |
142
+ | --- | --- |
143
+ | `rm(path, opts)` | `Git::Repository::Staging#rm` → `Commands::Rm` |
144
+ | `clean(opts)` | `Git::Repository::Staging#clean` → `Commands::Clean`; the `migrate_clean_legacy_options` deprecation adapter (`:ff`/`:force_force`) moves into the facade |
145
+ | `ignored_files` | `Git::Repository::Staging#ignored_files` → `Commands::LsFiles` |
146
+
147
+ Files touched: `lib/git/repository/staging.rb`, `spec/unit/git/repository/staging_spec.rb`, `lib/git/base.rb`
148
+
149
+ **Step A2 — Extend `Git::Repository::RemoteOperations`: `remotes`, `set_remote_url`, `remote_set_branches`** ✅
150
+
151
+ | `Git::Base` method | Facade to add |
152
+ | --- | --- |
153
+ | `remotes` | `Git::Repository::RemoteOperations#remotes` → `Commands::Remote::List`; returns `Array<Git::Remote>` |
154
+ | `set_remote_url(name, url)` | `Git::Repository::RemoteOperations#set_remote_url` → `Commands::Remote::SetUrl`; coerce local-repo `Git::Base` url to string in facade pre-processing; return `Git::Remote` |
155
+ | `remote_set_branches(name, *branches, add:)` | `Git::Repository::RemoteOperations#remote_set_branches` → `Commands::Remote::SetBranches` |
156
+
157
+ Files touched: `lib/git/repository/remote_operations.rb`, `spec/unit/git/repository/remote_operations_spec.rb`, `lib/git/base.rb`
158
+
159
+ **Step A3 — Extend `Git::Repository::ObjectOperations`: `tags`, `add_tag`, `delete_tag`** ✅
160
+
161
+ | `Git::Base` method | Facade to add |
162
+ | --- | --- |
163
+ | `tags` | `Git::Repository::ObjectOperations#tags` → `Commands::Tag::List` + `Parsers::Tag`; returns `Array<Git::Object::Tag>` |
164
+ | `add_tag(name, *options)` | `Git::Repository::ObjectOperations#add_tag` → `Commands::Tag::Create`; `validate_tag_options!` validation logic moves into the facade |
165
+ | `delete_tag(name)` | `Git::Repository::ObjectOperations#delete_tag` → `Commands::Tag::Delete` |
166
+
167
+ Files touched: `lib/git/repository/object_operations.rb`, `spec/unit/git/repository/object_operations_spec.rb`, `lib/git/base.rb`
168
+
169
+ **Step A4 — New `Git::Repository::Inspecting` module: `show`, `fsck`** ✅
170
+
171
+ These are read-only repository inspection operations that don't fit an existing topic module.
172
+
173
+ | `Git::Base` method | Facade to add |
174
+ | --- | --- |
175
+ | `show(objectish, path)` | `Git::Repository::Inspecting#show` → `Commands::Show`; returns `String` |
176
+ | `fsck(*objects, **opts)` | `Git::Repository::Inspecting#fsck` → `Commands::Fsck` + `Parsers::Fsck`; returns `Git::FsckResult` |
177
+
178
+ Files touched: `lib/git/repository/inspecting.rb` (new), `lib/git/repository.rb` (add `include Git::Repository::Inspecting`), `spec/unit/git/repository/inspecting_spec.rb` (new), `lib/git/base.rb`
179
+
180
+ **Not covered by A1–A4:** lower-level public methods such as `describe`, `repack`,
181
+ `gc`, `apply`, `apply_mail`, `read_tree`, and `cat_file` are handled by the C1c-2
182
+ public-API parity audit before `Git.open` starts returning `Git::Repository`.
183
+
184
+ ---
185
+
186
+ #### Workstream B — C0: Redirect `Git::Base` factory methods to `facade_repository`
187
+
188
+ These `Git::Base` methods construct domain objects directly with `self` instead of
189
+ delegating. All corresponding facade methods already exist — this is pure delegation
190
+ wiring with no new facade code needed. Ship as one PR (`feat/c0-delegate-base-factories`).
191
+
192
+ ⚠️ Depends on A3 (`tags`/`add_tag`/`delete_tag`) before `tag` can be redirected.
193
+
194
+ **Step B — Redirect `Git::Base` domain-object factories to `facade_repository`** ✅
195
+
196
+ | `Git::Base` method | Current | Replace with |
197
+ | --- | --- | --- |
198
+ | `branch(branch_name)` [L936] | `Git::Branch.new(self, ...)` | `facade_repository.branch(branch_name)` |
199
+ | `branches` [L950] | `Git::Branches.new(self)` | `facade_repository.branches` |
200
+ | `gblob(objectish)` [L993] | `Git::Object.new(self, objectish, 'blob')` | `facade_repository.gblob(objectish)` |
201
+ | `gcommit(objectish)` [L998] | `Git::Object.new(self, objectish, 'commit')` | `facade_repository.gcommit(objectish)` |
202
+ | `gtree(objectish)` [L1003] | `Git::Object.new(self, objectish, 'tree')` | `facade_repository.gtree(objectish)` |
203
+ | `object(objectish)` [L1030] | `Git::Object.new(self, objectish)` | `facade_repository.object(objectish)` |
204
+ | `remote(remote_name)` [L1035] | `Git::Remote.new(self, remote_name)` | `facade_repository.remote(remote_name)` |
205
+ | `tag(tag_name)` [L1045] | `Git::Object::Tag.new(self, tag_name)` | `facade_repository.tag(tag_name)` |
206
+
207
+ ---
208
+
209
+ #### Workstream C — C1: Prepare and flip top-level entry points to return `Git::Repository`
210
+
211
+ ⚠️ C1d, the actual return-type flip, depends on all of Workstreams A, B, and E plus
212
+ C1a-1/C1a-2/C1b/C1c being complete.
213
+
214
+ This workstream has six sub-tasks. C1a-1, C1a-2, and C1b can land early; C1c-1 and C1c-2 must run after
215
+ facade/helper coverage; C1d is the final step.
216
+
217
+ **C1a — Add factory class methods to `Git::Repository`** (group: Step C1a-1 and Step C1a-2)
218
+
219
+ The construction logic currently in `Git::Base.open`, `.bare`, `.clone`, and
220
+ `Git.init` must move to equivalent factory class methods on `Git::Repository` so
221
+ that `Git.open` etc. can call `Git::Repository.open` instead of `Git::Base.open`.
222
+ The notable complexity is clone result parsing/path resolution: `Git::Base.clone`
223
+ currently delegates to `Git::Lib#clone`, which already wraps `Git::Commands::Clone`.
224
+ Move that adapter behavior into `Git::Repository.clone` without reintroducing a
225
+ `Git::Lib` dependency.
226
+
227
+ This workstream is intentionally split into two PRs because the path/accessor
228
+ state work is independent of the clone/init work, and combining them would make a
229
+ very large PR.
230
+
231
+ **Step C1a-1 — Path state, accessors, and `.open`/`.bare` factories**
232
+
233
+ | `Git::Base` class method | Target |
234
+ | --- | --- |
235
+ | `.open(working_dir, options)` | `Git::Repository.open` — path validation + `resolve_paths` + constructor |
236
+ | `.bare(git_dir, options)` | `Git::Repository.bare` — bare path resolution + constructor |
237
+
238
+ Also move `resolve_paths` and `root_of_worktree` private helpers from `Git::Base`
239
+ to `Git::Repository` (or a private `RepositoryPaths` helper module). `Git::Repository`
240
+ must also expose the path/accessor surface currently provided by `Git::Base`: `dir`,
241
+ `repo`, `index`, and `repo_size`.
242
+
243
+ Files touched: `lib/git/repository.rb`, `lib/git/base.rb`
244
+
245
+ **Step C1a-2 — `.clone` and `.init` factories**
246
+
247
+ | `Git::Base` / `Git` method | Target |
248
+ | --- | --- |
249
+ | `.clone(url, dir, options)` | `Git::Repository.clone` — delegates to `Commands::Clone`, resolves paths, constructs instance |
250
+ | `Git.init(dir, options)` | `Git::Repository.init` — delegates to `Commands::Init` using `Git::ExecutionContext::Global` (not `Git::Lib`), then calls `.open`/`.bare` |
251
+
252
+ Note: `Git.init` in `lib/git.rb` currently passes `Git::Lib.new` into
253
+ `Git::Commands::Init`. That `Git::Lib.new` call is removed here by routing
254
+ through `Git::Repository.init` instead.
255
+
256
+ Note: `.repository_default_branch` is **not** part of C1a. That class method
257
+ routes through `Git::Lib` and belongs with the `LsRemote` parser migration in
258
+ Workstream F.
259
+
260
+ Files touched: `lib/git/repository.rb`, `lib/git/base.rb`, `lib/git.rb`
261
+
262
+ **Step C1b — Move global config singleton ownership off `Git::Base`**
263
+
264
+ `Git.configure` and `Git.config` both delegate to `Base.config`, which returns the
265
+ `Git::Base`-owned `Git::Config` singleton. When `Git::Base` is deleted, these break.
266
+ The fix is to move `config` to `Git::Config` itself as a class-level singleton (or to
267
+ the `Git` module directly) and update `Git.configure`, `Git.config`,
268
+ `Git.git_version`, `Git.binary_version`, and the surviving `Git::ExecutionContext`
269
+ classes to reference it without going through `Git::Base`. While `Git::Base` exists,
270
+ `Git::Base.config` can remain as a delegator for compatibility.
271
+
272
+ Note: Both `Git.git_version` and the deprecated `Git.binary_version` in `lib/git.rb`
273
+ currently evaluate `Git::Base.config.binary_path` at call time (not definition time),
274
+ so both method bodies must be updated in this PR.
275
+
276
+ Files touched: `lib/git/config.rb`, `lib/git.rb`, `lib/git/base.rb`,
277
+ `lib/git/execution_context.rb`, `lib/git/execution_context/global.rb`,
278
+ `lib/git/execution_context/repository.rb`
279
+
280
+ **C1c — Public API parity/deprecation audit before the flip** (group: Step C1c-1 and Step C1c-2)
281
+
282
+ Before `Git.open`, `Git.clone`, `Git.init`, and `Git.bare` return
283
+ `Git::Repository`, every public `Git::Base` method that should survive in v5.0 must
284
+ exist on `Git::Repository`; every method that should not survive must be explicitly
285
+ documented as a v5 breaking change or already deprecated for removal. This audit is
286
+ the gate that prevents the entry-point flip from silently dropping public methods
287
+ just because `Git::Base` still exists in the tree.
288
+
289
+ **Step C1c-1 — Signature-compatibility guidance and process** ⬜
290
+
291
+ Update extraction and review skills to document the signature-compatibility
292
+ classification policy (legacy-contract vs 5.x-native), parity-check requirements,
293
+ and test-creation expectations so that all future extraction work follows consistent
294
+ rules before the remediation sweep begins.
295
+
296
+ | Artifact | Change |
297
+ | --- | --- |
298
+ | `extract-facade-from-base-lib/SKILL.md` | Add `## Signature compatibility policy` section with legacy-contract vs 5.x-native classification table and four rules (legacy-contract preserves exact 4.x signatures, including rare `**opts`; 5.x-native uses `opts = {}` for consistency) |
299
+ | `facade-implementation/SKILL.md` | Add policy classification check to review workflow (step 4) |
300
+ | `facade-test-conventions/SKILL.md` | Add `context 'signature compatibility'` grouping convention and review checks |
301
+
302
+ Files touched: `.github/skills/extract-facade-from-base-lib/SKILL.md`,
303
+ `.github/skills/facade-implementation/SKILL.md`,
304
+ `.github/skills/facade-test-conventions/SKILL.md`
305
+
306
+ Tracked as [Issue #1369](https://github.com/ruby-git/ruby-git/issues/1369).
307
+
308
+ **Step C1c-2 — End-of-Phase-3 public-API parity audit and remediation sweep** ⬜
309
+
310
+ Compare every public `Git::Base` method against `Git::Repository`; fix mismatches
311
+ or explicitly record each as a documented v5 breaking change. No unclassified
312
+ compatibility gap may remain before C1d.
313
+
314
+ Required audit buckets:
11
315
 
12
- ***Goal**: Set up the new file structure and class names without altering existing logic. The gem will be fully functional after this phase.*
316
+ | Surface | Required decision before C1d |
317
+ | --- | --- |
318
+ | Path/accessors | `dir`, `repo`, `index`, `repo_size` must exist on `Git::Repository` (C1a-1 owns this) |
319
+ | Compatibility aliases/wrappers | `remove`, `revparse`, `diff_name_status`, `reset_hard`, `is_local_branch?`, `is_remote_branch?`, `is_branch?`, `checkout` must be migrated or intentionally removed with upgrade notes (`checkout` is called by `Git.export` on the `Git.clone` result) |
320
+ | Low-level public methods | `describe`, `repack`, `gc`, `apply`, `apply_mail`, `read_tree`, `cat_file` must be migrated to topic modules or intentionally removed with upgrade notes |
321
+ | Factory/domain-object returns | Confirm B plus A2/A3 cover `branch`, `branches`, `remote`, `remotes`, `tag`, `tags`, object factories, and tag create/delete return shapes |
322
+ | Keyword-arg facades | For `legacy-contract` methods, preserve the exact 4.x call shape (including rare `**opts` signatures); for `5.x-native` methods, use `opts = {}` style for consistency |
13
323
 
14
- 1. **Create New Directory Structure**:
324
+ Files touched: `lib/git/repository/*.rb` (topic modules for migrated methods),
325
+ `lib/git/base.rb`, upgrade notes / CHANGELOG for documented removals
15
326
 
16
- - Create the new directories that will house the refactored components:
327
+ Tracked as [Issue #1370](https://github.com/ruby-git/ruby-git/issues/1370).
17
328
 
18
- - `lib/git/commands/`
329
+ **Step C1d — Update `lib/git.rb` entry points to return `Git::Repository`**
19
330
 
20
- - `lib/git/repository/` (for the facade modules)
331
+ With C1a, C1b, C1c, A, B, and E in place, update `Git.open`, `Git.clone`,
332
+ `Git.init`, and `Git.bare` in `lib/git.rb` to call `Git::Repository.*` and return
333
+ `Git::Repository` directly, bypassing `Git::Base` entirely.
21
334
 
22
- 2. **Rename Path Classes**:
335
+ ---
23
336
 
24
- - Perform a project-wide, safe rename of the existing path-related classes. This is a low-risk mechanical change.
337
+ #### Workstream D C2+C3: Remove compatibility bridges
25
338
 
26
- - `Git::WorkingDirectory` -> `Git::WorkingTreePath`
339
+ ⚠️ These are v5-only cleanup steps. They are not 4.x-compatible and must be kept out
340
+ of 4.x release candidates unless an explicit breaking-change decision has already
341
+ been recorded.
27
342
 
28
- - `Git::Index` -> `Git::IndexPath`
343
+ ##### Step D1 — Remove domain-object compatibility fallbacks
29
344
 
30
- - `Git::Repository` -> `Git::RepositoryPath`
345
+ ⚠️ Depends on C1d. This can be a releasable v5 cleanup PR after `Git.open` returns
346
+ `Git::Repository`, because normal construction paths no longer pass `Git::Base` into
347
+ domain objects. It is breaking for callers that directly construct domain objects
348
+ with a `Git::Base` provider, so that removal must be documented in the upgrade notes.
31
349
 
32
- - Run the test suite to ensure everything still works as expected.
350
+ Remove `is_a?(Git::Base)` guards. Current sites:
33
351
 
34
- 3. **Introduce New Core Classes (Empty Shells)**:
352
+ | File | Line |
353
+ | --- | --- |
354
+ | `lib/git/branch.rb` | L478 (`branch_repository` helper) |
355
+ | `lib/git/branches.rb` | L150 (`branches_repository` helper) |
356
+ | `lib/git/log.rb` | L170 (`log_repository` helper) |
357
+ | `lib/git/object.rb` | L137, L329, L405 |
358
+ | `lib/git/remote.rb` | L149 (`remote_repository` helper) |
359
+ | `lib/git/stash.rb` | L104 (`stash_repository` helper) |
360
+ | `lib/git/stashes.rb` | L170 (`stashes_repository` helper) |
361
+ | `lib/git/worktree.rb` | L151 (`worktree_repository` helper) |
362
+ | `lib/git/worktrees.rb` | L144 (`worktrees_repository` helper) |
363
+ | `lib/git/repository/remote_operations.rb` | L418 (`url.is_a?(Git::Base)` coercion — handled in A2 facade pre-processing instead) |
35
364
 
36
- - Create the new `Git::ExecutionContext` class in `lib/git/execution_context.rb`. For now, its implementation can be a simple shell or a thin wrapper around the existing `Git::Lib`.
365
+ Each guard simplifies to just the `Git::Repository` branch the `Git::Base` branch is deleted.
37
366
 
38
- - Create the new `Git::Repository` class in `lib/git/repository.rb`. This will initially be an empty class.
367
+ Verify no guards remain: `grep -r 'is_a?(Git::Base)' lib/`
39
368
 
40
- 4. **Set Up RSpec Environment**:
369
+ Also remove legacy `@base.lib` fallback paths that only exist to support
370
+ `Git::Base`/`Git::Lib`-backed domain objects:
41
371
 
42
- - Add rspec dependencies to the `Gemfile` as a development dependency.
372
+ | File | Fallback |
373
+ | --- | --- |
374
+ | `lib/git/diff.rb` | `@base.lib.diff_full` |
375
+ | `lib/git/diff_stats.rb` | `@base.lib.diff_stats` |
376
+ | `lib/git/diff_path_status.rb` | `@base.lib.diff_path_status` |
43
377
 
44
- - Configure the test setup to allow both TestUnit and RSpec tests to run concurrently.
378
+ After D1, domain objects should assume their provider is `Git::Repository` (or a
379
+ compatible object that implements the repository facade methods directly), not an
380
+ object with a `.lib` escape hatch.
381
+
382
+ ##### Step D2 — Remove the `base_object` bridge
383
+
384
+ ⚠️ Do **not** ship D2 as a standalone PR while `Git::Base` remains functional.
385
+ `Git::Base#facade_repository` currently depends on
386
+ `Git::ExecutionContext::Repository.from_base(self)`, so deleting the bridge before
387
+ deleting or retiring `Git::Base` would leave the tree unreleasable.
388
+
389
+ D2 belongs in the Phase 4 old-code deletion PR (or in the same v5 cleanup PR that
390
+ removes `Git::Base` as a usable public entry point):
391
+
392
+ - Delete `attr_reader :base_object`
393
+ - Remove `base_object:` from `#initialize` and body
394
+ - Remove or convert `from_base` factory
395
+
396
+ ---
397
+
398
+ #### Workstream E — Migrate or deprecate instance helper methods
399
+
400
+ ⚠️ Depends on C1a (factory/path state must exist so the helpers have a home). E must
401
+ complete before C1d, because `Git.open` returning `Git::Repository` without these
402
+ helpers would drop existing public `Git::Base` behavior.
403
+
404
+ `Git::Base` exposes several block-based helper methods that have no counterpart on
405
+ `Git::Repository`. They must either be migrated before `Git::Base` can be deleted,
406
+ or explicitly deprecated with removal in v6.0. The recommended path is migration.
407
+
408
+ **Step E — Migrate block-based helper/path-context methods to `Git::Repository`** ⬜
409
+
410
+ | `Git::Base` method | Proposed destination | Notes |
411
+ | --- | --- | --- |
412
+ | `#chdir(&block)` | `Git::Repository#chdir` | `Dir.chdir(dir.to_s) { yield dir }` — trivial; just needs the `dir` accessor on `Git::Repository` |
413
+ | `#with_index(new_index, &block)` | `Git::Repository#with_index` | Invalidates and restores `@index`; rebuilds the repository execution context |
414
+ | `#with_temp_index(&block)` | `Git::Repository#with_temp_index` | Creates a `Tempfile`-backed index, delegates to `with_index` |
415
+ | `#with_working(work_dir, &block)` | `Git::Repository#with_working` | Invalidates and restores `@working_directory`; rebuilds the repository execution context |
416
+ | `#with_temp_working(&block)` | `Git::Repository#with_temp_working` | Creates a `Dir.mktmpdir`-backed working dir, delegates to `with_working` |
417
+
418
+ `set_index` and `set_working` (the non-block mutators) must also be migrated or
419
+ removed at the same time, since `with_index`/`with_working` depend on the same
420
+ invalidation logic.
421
+
422
+ Files touched: `lib/git/repository.rb` (or a new `Git::Repository::ContextHelpers`
423
+ module), `lib/git/base.rb`, `spec/unit/git/repository/` (new or extended spec)
424
+
425
+ ---
426
+
427
+ #### Workstream F — `Git` module utility methods still using `Git::Lib` directly
428
+
429
+ ⚠️ These are **Phase 4 prerequisites** — they do not block A–E but must be done
430
+ before `Git::Lib` can be deleted.
431
+
432
+ Three `Git`-module-level methods bypass `Git::Repository` entirely and call
433
+ `Git::Lib` directly. The required command classes already exist; each method needs a
434
+ non-`Git::Lib` adapter path using `Git::ExecutionContext::Global` plus existing
435
+ parsing logic.
436
+
437
+ **Step F1 — Move `Git.ls_remote` and `Git.default_branch` off `Git::Lib`** ⬜
438
+
439
+ | `Git` module method | Current path | Required work |
440
+ | --- | --- | --- |
441
+ | `Git.default_branch(repo, options)` | `Base.repository_default_branch` → `Git::Lib.new.repository_default_branch` | Use `Git::Commands::LsRemote` with `symref: true` and migrate the default-branch parser out of `Git::Lib` |
442
+ | `Git.ls_remote(location, options)` | `Git::Lib.new.ls_remote` | Migrate to `Git::Commands::LsRemote` (shared with `default_branch`) |
443
+
444
+ Also migrate `Git::Base.repository_default_branch` to use `Git::Commands::LsRemote`
445
+ directly (sharing the `LsRemote` parser with `Git.ls_remote`). This is the call
446
+ chain behind `Git.default_branch` and can be migrated in the same F1 PR since both
447
+ use the same command class.
448
+
449
+ Files touched: `lib/git.rb`, `lib/git/base.rb`, and parser/helper code extracted
450
+ from `Git::Lib` as needed
451
+
452
+ **Step F2 — Move `Git.global_config`, `#config`, and `#global_config` off `Git::Lib`** ⬜
453
+
454
+ | `Git` module method | Current path | Required work |
455
+ | --- | --- | --- |
456
+ | `Git.global_config(name, value)` | `Git::Lib.new.global_config_{get,set,list}` | Use `Git::Commands::ConfigOptionSyntax::{Get,List,Set}` with `global: true` |
457
+
458
+ Also audit the `Git` module instance methods `#config` and `#global_config` for
459
+ callers that `include Git`; `#global_config` should continue delegating to the class
460
+ method, while `#config` must either be reimplemented without `Git::Lib.new` or
461
+ documented as removed.
462
+
463
+ Files touched: `lib/git.rb`, `lib/git/base.rb`, and parser/helper code extracted
464
+ from `Git::Lib` as needed
465
+
466
+ ---
467
+
468
+ #### Phase 3 dependency order
469
+
470
+ 1. **Parallel starters**: A1–A4, C1a, C1b, F1, and F2 can begin independently.
471
+ 2. **B after A3**: B can start once A3 supplies facade tag factories.
472
+ 3. **E after C1a**: helper/path-context methods need `Git::Repository` path state.
473
+ 4. **C1c-2 after A+B+E+C1c-1**: API parity remediation can only be actioned after facade and helper coverage exists and the guidance/policy (C1c-1) is in place.
474
+ 5. **C1d is the v5 boundary step**: the `Git.open`/`.clone`/`.init`/`.bare` return-type flip waits for A, B, C1a, C1b, C1c-1, C1c-2, and E, and is explicitly not a 4.x-compatible change.
475
+ 6. **D1 after C1d**: domain-object fallback removal waits until normal construction no longer passes `Git::Base` into domain objects.
476
+ 7. **Phase 4 after D1+F1+F2**: deleting `Git::Base`/`Git::Lib` waits for domain-object fallback removal and `Git` module utilities to stop using `Git::Lib`; D2 lands with that deletion, not before it.
477
+
478
+ ---
479
+
480
+ #### Phase 3 steps and release compatibility
481
+
482
+ Default rule: every step before C1d that produces code must be small, independently releasable on the
483
+ 4.x-compatible line, and must preserve public signatures, return values, deprecation
484
+ warnings, and top-level factory behavior. Any intentional break must be explicitly
485
+ classified as a v5-only PR with upgrade-note coverage before it lands.
486
+
487
+ **GitHub PR column:** ⬜ = not yet opened; replace with a PR link (e.g. `[#1234](…)`) when opened, then append ✅ when merged.
488
+
489
+ | Step | GitHub PR | Scope | Release lane | Backward-compatibility rule |
490
+ | --- | --- | --- | --- | --- |
491
+ | A1 | ✅ | Add `rm`, `clean`, `ignored_files` facade coverage | 4.x-compatible | `Git::Base` public methods keep the same signatures, return values, and deprecation behavior. |
492
+ | A2 | ✅ | Add `remotes`, `set_remote_url`, `remote_set_branches` facade coverage | 4.x-compatible | `Git::Base` remote methods keep the same return objects and validation behavior. |
493
+ | A3 | ✅ | Add `tags`, `add_tag`, `delete_tag` facade coverage | 4.x-compatible | Tag list/create/delete return contracts match 4.x behavior. |
494
+ | A4 | ✅ | Add `Inspecting#show` and `#fsck` | 4.x-compatible | `Git::Base#show` and `#fsck` remain behavior-compatible and delegate internally. |
495
+ | B | ✅ | Redirect `Git::Base` domain-object factories | 4.x-compatible | Method signatures and return types stay the same; only the internal provider changes to `Git::Repository`. Split into object/tag factories and branch/remote factories if the PR grows. |
496
+ | C1a-1 | ✅ | Add `Git::Repository.open`/`.bare`, path state, and `dir`/`repo`/`index`/`repo_size` | 4.x-compatible additive | `Git.open`/`.bare` still return `Git::Base`; new repository factories are additive until C1d. |
497
+ | C1a-2 | ⬜ | Add `Git::Repository.clone`/`.init` | 4.x-compatible additive | `Git.clone`/`.init` still return `Git::Base`; clone/init behavior is duplicated behind new factories without changing public entry points. |
498
+ | C1b | ⬜ | Move global config ownership | 4.x-compatible | `Git.config`, `Git.configure`, and `Git::Base.config` keep working; `Git::Base.config` remains as a delegator. |
499
+ | E | ⬜ | Add repository helper/path-context methods | 4.x-compatible additive | `Git::Base` helpers keep working; `Git::Repository` gains equivalent behavior before any top-level return-type change. Split index helpers and working-directory helpers if needed. |
500
+ | F1 | ⬜ | Move `Git.ls_remote` and `Git.default_branch` off `Git::Lib` | 4.x-compatible | Return formats and error behavior match current 4.x-compatible behavior. |
501
+ | F2 | ⬜ | Move `Git.global_config`, module `#config`, and module `#global_config` off `Git::Lib` | 4.x-compatible | Config methods keep the same return formats and write behavior. |
502
+ | C1c-1 | ⬜ | Guidance/process: signature-compatibility policy for extraction and review ([#1369](https://github.com/ruby-git/ruby-git/issues/1369)) | 4.x-compatible / docs-only | Guidance and review checklists define legacy-contract vs 5.x-native signatures, including test expectations. |
503
+ | C1c-2 | ⬜ | End-of-Phase-3 public-API parity audit and remediation sweep ([#1370](https://github.com/ruby-git/ruby-git/issues/1370)) | 4.x-compatible | All four parity audit buckets resolved (fix or documented removal) before C1d; no unclassified compatibility gap remains. |
504
+ | C1d | ⬜ | Flip `Git.open`/`.clone`/`.init`/`.bare` to return `Git::Repository` | v5 boundary | Explicit breaking change because class identity changes from `Git::Base` to `Git::Repository`; method-level parity must be complete first. |
505
+ | D1 | ⬜ | Remove domain-object `Git::Base` guards and `@base.lib` fallbacks | v5 cleanup | Explicitly drops direct `Git::Base` provider support in domain-object constructors; normal factory-created objects remain supported. |
506
+ | D2 / Phase 4 | ⬜ | Remove `base_object`/`from_base` with `Git::Base` deletion or retirement | v5 cleanup | Must land with the old-code deletion path; not releasable as a standalone PR while `Git::Base#facade_repository` depends on it. |
507
+
508
+ ---
509
+
510
+ #### Phase 3 completion criteria
511
+
512
+ Use this table to decide whether a checklist item can be marked complete. A step is
513
+ done only when its code, focused specs, and delegation/cleanup checks are all true.
514
+
515
+ | Step | Done when |
516
+ | --- | --- |
517
+ | A1: `Staging` — `rm`, `clean`, `ignored_files` | `Git::Repository::Staging` implements all three methods; `Git::Base#rm`, `#clean`, and `#ignored_files` delegate to `facade_repository`; legacy clean option deprecations still fire; focused staging specs cover success, option validation, and return values. |
518
+ | A2: `RemoteOperations` — `remotes`, `set_remote_url`, `remote_set_branches` | `Git::Repository::RemoteOperations` implements all three methods; returned remotes are `Git::Remote` objects backed by `Git::Repository`; local repository URL coercion no longer requires a `Git::Base` branch after D1; focused remote-operation specs cover branch validation, return values, and command arguments. |
519
+ | A3: `ObjectOperations` — `tags`, `add_tag`, `delete_tag` | `Git::Repository::ObjectOperations` implements all three methods; tag parsing/validation matches the legacy `Git::Lib` behavior; `tags`/`add_tag` return repository-backed tag objects; `delete_tag` preserves the legacy return contract; focused object-operation specs cover create/delete/list paths. |
520
+ | A4: `Inspecting` — `show`, `fsck` | `Git::Repository::Inspecting` exists, is required and included by `Git::Repository`, and implements both methods; `show` returns the expected string output; `fsck` returns `Git::FsckResult`; `Git::Base#show` and `#fsck` delegate to the facade; focused inspecting specs cover parser and command wiring. |
521
+ | B: `Git::Base` factory delegation wiring | Every listed `Git::Base` factory delegates to `facade_repository`; constructed domain objects receive a `Git::Repository` provider, not `self`; legacy method signatures and default arguments stay unchanged; focused specs prove each factory return type and provider. |
522
+ | C1a-1: `Git::Repository.open`/`.bare` + path state | `Git::Repository.open` and `.bare` exist; `resolve_paths` and `root_of_worktree` helpers are on `Git::Repository`; repository instances expose `dir`, `repo`, `index`, and `repo_size`; focused specs cover working and bare construction. |
523
+ | C1a-2: `Git::Repository.clone`/`.init` | `Git::Repository.clone` and `.init` exist and preserve legacy path resolution, `git_ssh:`, `binary_path:`, `log:`, `index:`, and `repository:` behavior; clone/init use `Git::ExecutionContext::Global`, not `Git::Lib`; `Git.init` in `lib/git.rb` no longer passes `Git::Lib.new` into `Commands::Init`; focused specs cover clone and init construction. |
524
+ | C1b: global config ownership | `Git.config`, `Git.configure`, `Git.git_version`, `Git.binary_version`, and `Git::ExecutionContext` resolve global config without referencing `Git::Base.config`; both the method body of `git_version` and the default-parameter expression of `binary_version` are updated; `Git::Base.config` remains only as a compatibility delegator while `Git::Base` exists; specs prove runtime changes to global `binary_path` and `git_ssh` are still honored. |
525
+ | C1c-1: signature-compatibility guidance | Skill updates merged (Issue #1369): `extract-facade-from-base-lib`, `facade-implementation`, and `facade-test-conventions` skills document the legacy-contract vs 5.x-native classification policy, parity-check requirements, and test-creation expectations; legacy-contract methods preserve exact 4.x call shapes while 5.x-native methods use `opts = {}`; keyword-arg remediation list for C1c-2 is established. |
526
+ | C1c-2: public-API parity audit and remediation | End-of-Phase-3 sweep complete (Issue #1370): a public-method inventory compares `Git::Base` and `Git::Repository`; every surviving public method has a repository implementation and focused coverage; every intentional removal has an upgrade-note/deprecation decision; no unclassified compatibility gap remains. |
527
+ | C1d: entry-point flip | `Git.open`, `Git.clone`, `Git.init`, and `Git.bare` return `Git::Repository`; common existing workflows still pass through those entry points; YARD return docs are updated; no top-level factory method calls `Git::Base.*`; full suite passes. |
528
+ | D1: domain-object fallback removal | No `is_a?(Git::Base)` guards remain; no `@base.lib` fallback remains in domain objects; direct `Git::Base` provider support is documented as a v5-only removal; full suite passes. |
529
+ | D2 / Phase 4: `base_object` bridge removal | `Git::ExecutionContext::Repository` no longer accepts or exposes `base_object`; `from_base` is removed or converted to a non-`Git::Base` path; this lands only with the PR that deletes or retires `Git::Base`, so no releasable state contains a broken `Git::Base#facade_repository`. |
530
+ | E: instance helper methods | `Git::Repository` implements or explicitly deprecates `chdir`, `with_index`, `with_temp_index`, `with_working`, `with_temp_working`, `set_index`, and `set_working`; context rebuilding after index/worktree changes is covered by specs; helpers yield the same values and restore state after block exit/errors. |
531
+ | F: `Git` module utilities off `Git::Lib` | `Git.default_branch`, `Git.global_config`, `Git.ls_remote`, module instance `#config`, and module instance `#global_config` no longer call `Git::Lib.new`; `Git::Base.repository_default_branch` migrated to use `Git::Commands::LsRemote` directly; existing `LsRemote` and `ConfigOptionSyntax` commands provide the behavior; parser/helper code needed from `Git::Lib` has moved; `grep -n 'Lib.new' lib/git.rb` returns no matches. |
532
+
533
+ ---
534
+
535
+ #### Facade coverage checklist
536
+
537
+ | Step | Status |
538
+ | --- | --- |
539
+ | A1: `Staging` — `rm`, `clean`, `ignored_files` | ✅ |
540
+ | A2: `RemoteOperations` — `remotes`, `set_remote_url`, `remote_set_branches` | ✅ |
541
+ | A3: `ObjectOperations` — `tags`, `add_tag`, `delete_tag` | ✅ |
542
+ | A4: new `Inspecting` — `show`, `fsck` | ✅ |
543
+ | B (C0): `Git::Base` factory delegation wiring | ✅ |
544
+ | C1a-1: `Git::Repository.open`/`.bare`, path state (`dir`, `repo`, `index`, `repo_size`) | ✅ |
545
+ | C1a-2: `Git::Repository.clone`/`.init` (no `Git::Lib` dependency) | ⬜ |
546
+ | C1b: Global config ownership (`Base.config` → `Git::Config`) | ⬜ |
547
+ | C1c-1: Guidance/process updates for signature compatibility (#1369) | ⬜ |
548
+ | C1c-2: End-of-Phase-3 public-API parity audit and remediation (#1370) | ⬜ |
549
+ | C1d: Entry-point flip (`Git.open` etc. → `Git::Repository`) | ⬜ |
550
+ | D1 (C3): Remove `is_a?(Git::Base)` guards + `@base.lib` fallbacks | ⬜ (v5 cleanup) |
551
+ | D2 (C2 / Phase 4): Remove `base_object` bridge with `Git::Base` deletion | ⬜ (v5 cleanup) |
552
+ | E: Instance helpers (`#chdir`, `#with_index`, `#with_temp_index`, `#with_working`, `#with_temp_working`) | ⬜ |
553
+ | F: `Git` module utilities (`default_branch`, `global_config`, `ls_remote`) off `Git::Lib` | ⬜ (Phase 4 prereq) |
554
+
555
+ #### Quality gates (per step)
556
+
557
+ 1. Run the focused spec for the touched module: `bundle exec rspec spec/unit/git/repository/<topic>_spec.rb`
558
+ 2. Run the full suite: `bundle exec rake default:parallel` — CI-equivalent aggregate task covering Test::Unit, RSpec, RuboCop, YARD, and build
559
+ 3. For every 4.x-compatible step before C1d: confirm no public `Git`, `Git::Base`, or `Git::Lib` method signature/return contract changes unless the step explicitly documents a compatible deprecation path
560
+ 4. After C1b: confirm `Git.configure`, `Git.config`, `Git.git_version`, and `Git::ExecutionContext` no longer depend on `Git::Base.config`
561
+ 5. After C1c-2: compare `Git::Base` public methods against `Git::Repository` and record every intentional removal in upgrade notes
562
+ 6. After C1d: confirm `Git.open(...)` returns a `Git::Repository` instance and common legacy call sites still work or fail with documented breaking-change coverage
563
+ 7. After D1: confirm no guards or legacy lib fallbacks remain: `grep -r 'is_a?(Git::Base)\|@base\.lib' lib/`
564
+ 8. After D2: confirm no `base_object` or `from_base` bridge remains and `Git::Base` is deleted or retired in the same releasable PR
565
+ 9. After F: confirm no `Git::Lib.new` calls remain in `lib/git.rb`: `grep -n 'Lib.new' lib/git.rb`
566
+
567
+ #### Reference Files
568
+
569
+ - Facade shell: `lib/git/repository.rb`
570
+ - Staging module (pattern reference): `lib/git/repository/staging.rb`
571
+ - Staging spec (pattern reference): `spec/unit/git/repository/staging_spec.rb`
572
+ - RemoteOperations (more complex example): `lib/git/repository/remote_operations.rb`
573
+ - Command classes: `lib/git/commands/` (especially `clone.rb`, `ls_remote.rb`, and `config_option_syntax/*`)
574
+
575
+ ## Phase 1: Foundation and Scaffolding
576
+
577
+ ***Goal**: Set up the new file structure and class names without altering existing
578
+ logic. The gem will be fully functional after this phase.*
579
+
580
+ 1. **Create New Directory Structure**
581
+
582
+ - `lib/git/commands/` ✅
583
+ - `lib/git/repository/` ✅ — populated with 12 facade modules in Phase 3 (see [Facade Modules Completed](#facade-modules-completed))
584
+
585
+ 2. **Eliminate Custom Path Classes**
586
+
587
+ Path wrapper classes removed and replaced with `Pathname` objects:
588
+
589
+ - `Git::Path` ✅
590
+ - `Git::WorkingDirectory` ✅
591
+ - `Git::Index` ✅
592
+ - `Git::Repository` (the path class) ✅
593
+
594
+ `Git::Base` now stores paths as `Pathname` objects directly via
595
+ `@working_directory`, `@repository`, and `@index` instance variables.
596
+
597
+ 3. **Introduce New Core Classes (Empty Shells)**
598
+
599
+ - `Git::ExecutionContext` in `lib/git/execution_context.rb` ✅
600
+ - Real base class with `command_capturing`, `command_streaming`, `git_version`
601
+ - `Git::ExecutionContext::Repository` subclass in `lib/git/execution_context/repository.rb` ✅
602
+ - `Git::ExecutionContext::Global` subclass in `lib/git/execution_context/global.rb` ✅
603
+
604
+ - `Git::Repository` in `lib/git/repository.rb` ✅
605
+ - Now includes 12 facade modules (see [Facade Modules Completed](#facade-modules-completed))
606
+
607
+ - `Git::Commands::Arguments` DSL in `lib/git/commands/arguments.rb` ✅
608
+ - Provides declarative argument definition for command classes
609
+
610
+ 4. **Set Up RSpec Environment**
611
+
612
+ RSpec configured and working alongside TestUnit. Specs live in `spec/` and can be
613
+ run with `bundle exec rspec`. ✅
45
614
 
46
615
  ## Phase 2: The Strangler Fig Pattern - Migrating Commands
47
616
 
48
- ***Goal**: Incrementally move the implementation of each git command from `Git::Lib` to a new `Command` class, strangling the old implementation one piece at a time using a Test-Driven Development workflow.*
617
+ ***Goal**: Incrementally move the implementation of each git command from `Git::Lib`
618
+ to a new `Command` class, strangling the old implementation one piece at a time using
619
+ a Test-Driven Development workflow.*
620
+
621
+ **Important Note**: During this phase, `Git::Lib` acts as a stand-in for the
622
+ `ExecutionContext` hierarchy:
623
+
624
+ - `Git::Lib.new(nil, logger)` effectively acts like `ExecutionContext::Global` (no repository
625
+ paths set)
626
+ - `Git::Lib.new(base, logger)` effectively acts like `ExecutionContext::Repository` (repository
627
+ paths set)
628
+
629
+ All new `Git::Commands::*` classes should accept any object that responds to
630
+ `command` (duck typing), not a specific context class. This allows them to work with
631
+ `Git::Lib` during migration and the proper context classes in Phase 3.
632
+
633
+ The `command` method provides important functionality including default options
634
+ (normalize, chomp, timeout), option validation, and a simplified interface that
635
+ returns just stdout. Commands should call `@execution_context.command('subcommand',
636
+ *args, **opts)` rather than working with `CommandLine` instances directly.
637
+
638
+ ### Key Architectural Insight: Git::Lib as the Adapter Layer
639
+
640
+ A fundamental principle of this migration is that `Git::Lib` methods serve as
641
+ **adapters** between the legacy public interface and the new `Git::Commands::*`
642
+ classes. This separation of concerns provides several benefits:
643
+
644
+ 1. **Legacy Interface Acceptance**: `Git::Lib` methods continue to accept the
645
+ historical interface—positional arguments, deprecated options, and quirky
646
+ parameter names that users have come to rely on.
647
+
648
+ 2. **Interface Translation**: The adapter converts legacy patterns to the clean
649
+ `Git::Commands::*` API. For example:
650
+ - Positional `message` argument → `:message` keyword
651
+ - `:no_gpg_sign => true` → `:gpg_sign => false`
652
+ - Options hash → keyword arguments via `**options`
653
+
654
+ 3. **Deprecation Handling**: Warnings about deprecated options are issued in the
655
+ adapter layer, *before* delegating to the command class. This ensures users are
656
+ informed even if they're making other errors.
657
+
658
+ 4. **Clean Command Classes**: `Git::Commands::*` classes remain free of legacy
659
+ baggage. They have a consistent, modern API that:
660
+ - Uses keyword arguments with sensible defaults
661
+ - Matches the underlying git command's interface closely
662
+ - Is easier to test in isolation
663
+ - Could potentially be used directly by advanced users
664
+
665
+ Example adapter pattern:
666
+
667
+ ```ruby
668
+ # Git::Lib#commit - the adapter layer
669
+ def commit(message, opts = {})
670
+ # Legacy: positional message → keyword argument
671
+ opts = opts.merge(message: message) if message
49
672
 
50
- - **1. Migrate the First Command (`config`)**:
673
+ # Legacy: :no_gpg_sign :gpg_sign => false (with deprecation warning)
674
+ if opts[:no_gpg_sign]
675
+ Git::Deprecation.warn(':no_gpg_sign option is deprecated...')
676
+ raise ArgumentError, '...' if opts.key?(:gpg_sign)
677
+ opts.delete(:no_gpg_sign)
678
+ opts[:gpg_sign] = false
679
+ end
51
680
 
52
- - **Write Unit Tests First**: Write comprehensive RSpec unit tests for the *proposed* `Git::Commands::Config` class. These tests will fail initially because the class doesn't exist yet. The tests should be fast and mock the `ExecutionContext`.
681
+ # Delegate to clean interface
682
+ Git::Commands::Commit.new(self).call(**opts)
683
+ end
684
+ ```
53
685
 
54
- - **Create Command Class**: Implement `Git::Commands::Config` to make the tests pass. This class will contain all the logic for building git config arguments and parsing its output. It will accept an `ExecutionContext` instance in its constructor.
686
+ This pattern makes future cleanup straightforward—once deprecation periods end, the
687
+ adapter logic can be simplified or removed entirely.
55
688
 
56
- - **Delegate from `Git::Lib`**: Modify the `config_*` methods within the existing `Git::Lib` class. Instead of containing the implementation, they will now instantiate and call the new `Git::Commands::Config` object.
689
+ **Parameter Design Principle**: Command class `#call` method parameters should
690
+ generally match the underlying git command's interface. This keeps the Commands layer
691
+ thin and transparent—directly mapping to git documentation. The public facade API
692
+ (Git.*, Git::Repository#*) can add convenience features like:
57
693
 
58
- - **Verify**: Run the full test suite (both TestUnit and RSpec). The existing tests for `g.config` should still pass, but they will now be executing the new, refactored code.
694
+ - Path expansion or normalization
695
+ - Ruby-idiomatic defaults
696
+ - Parameter validation specific to the Ruby context
697
+ - Combining multiple git operations into one public method
698
+
699
+ Keep Command parameters matching git closely for simplicity, maintainability, and
700
+ easier testing. Allow the public API to diverge when it adds real value, but without
701
+ obscuring what's actually happening underneath.
702
+
703
+ **Method Signature Convention**: The `#call` signature SHOULD, if possible, use
704
+ anonymous repeatable arguments for both positional and keyword arguments:
705
+
706
+ ```ruby
707
+ # ✅ Preferred: anonymous forwarding with ARGS.bind
708
+ # Note: defaults defined in the DSL (e.g., `positional :paths, default: ['.']`)
709
+ # are applied automatically by ARGS.bind
710
+ def call(*, **)
711
+ @execution_context.command('add', *ARGS.bind(*, **))
712
+ end
713
+
714
+ # ✅ Acceptable: assign bound_args when you need to access argument values
715
+ def call(*, **)
716
+ bound_args = ARGS.bind(*, **)
717
+ output = @execution_context.command('diff', *bound_args).stdout
718
+ Parsers::Diff.parse(output, include_dirstat: !bound_args.dirstat.nil?)
719
+ end
720
+
721
+ # ❌ Incorrect: options hash parameter
722
+ def call(paths = '.', options = {})
723
+ @execution_context.command('add', *ARGS.bind(*Array(paths), **options))
724
+ end
725
+ ```
726
+
727
+ This convention provides:
728
+
729
+ - **Better IDE support**: Editors can autocomplete and validate keyword arguments
730
+ - **Clearer method signatures**: The `#call` signature documents available options
731
+ - **Centralized validation**: `ARGS.bind` enforces allowed options and raises errors for unknown or invalid keywords
732
+ - **Consistency**: All command classes follow the same pattern
733
+
734
+ The facade layer (`Git::Lib`, `Git::Base`) may accept either keyword arguments or an
735
+ options hash for backward compatibility, but must use `**options` when delegating to
736
+ command classes.
737
+
738
+ ### Architectural Insights from Command Migrations
739
+
740
+ The following insights were discovered during command migrations and should guide
741
+ future work:
742
+
743
+ 1. **`Data.define` creates frozen objects—no memoization allowed**
744
+
745
+ Ruby's `Data.define` creates immutable, frozen objects. This means patterns like
746
+ `@cached ||= expensive_computation` will raise `FrozenError`. When using
747
+ `Data.define` for value objects, either:
748
+ - Accept repeated computation (preferred for simple operations)
749
+ - Move caching outside the value object
750
+ - Use a regular class with `freeze` called explicitly after initialization
751
+
752
+ 2. **Parsing logic duplication is unavoidable when one path needs repository context**
753
+
754
+ Value objects like `BranchInfo` cannot create domain objects like `Remote` because
755
+ they lack repository context. This leads to seemingly duplicate parsing:
756
+
757
+ ```ruby
758
+ # Value object (pure, no context)
759
+ BranchInfo#short_name # → returns String
760
+
761
+ # Domain object (has @base context)
762
+ Branch#parse_name # → returns [Remote, String]
763
+ ```
764
+
765
+ This is **intentional duplication**, not a code smell. Eliminating it would couple
766
+ the value object to the repository, defeating its purpose.
767
+
768
+ 3. **The command's return type shapes the entire downstream architecture**
769
+
770
+ When a command returns primitive types (`Array<Array>`), all consumers need magic
771
+ index knowledge. Changing to value objects (`Array<BranchInfo>`) ripples through
772
+ every consumer. Plan return types carefully—they define contracts across the
773
+ system.
774
+
775
+ 4. **Constructor polymorphism enables gradual deprecation**
776
+
777
+ When changing a constructor's expected argument type, accept both old and new
778
+ types with a deprecation warning for the legacy path:
779
+
780
+ ```ruby
781
+ def initialize(base, branch_info_or_name)
782
+ if branch_info_or_name.is_a?(Git::BranchInfo)
783
+ initialize_from_branch_info(branch_info_or_name)
784
+ else
785
+ Git::Deprecation.warn('...')
786
+ initialize_from_name(branch_info_or_name)
787
+ end
788
+ end
789
+ ```
790
+
791
+ This allows migrating internal code first while external users continue working.
792
+
793
+ 5. **The boundary between "pure data" and "contextualized operations" is the most
794
+ important architectural decision**
795
+
796
+ Commands should return pure value objects (no repository context needed).
797
+ Domain objects wrap those value objects and add operations requiring context.
798
+ This single decision determines where parsing lives, what types flow where, and
799
+ how the system layers together.
800
+
801
+ 6. **Use `flag_or_value_option ..., negatable: true` for options with positive, negative, and value forms**
802
+
803
+ When a git option supports `--flag`, `--no-flag`, AND `--flag=value` forms (like
804
+ `--track`/`--no-track`/`--track=inherit`), use `flag_or_value_option` with
805
+ `negatable: true` instead of defining separate options with conflict declarations.
806
+ Under the companion-key model this registers two entries (`:track` and
807
+ `:no_track`), each following standard boolean semantics, with an automatic
808
+ conflict between them:
809
+
810
+ ```ruby
811
+ # ✅ Preferred: single declaration registers the companion-key pair
812
+ flag_or_value_option :track, negatable: true, inline: true
813
+ # track: nil → (omitted)
814
+ # track: true → --track
815
+ # track: 'inherit' → --track=inherit
816
+ # no_track: true → --no-track
817
+ # track: false → (omitted; false is always absent)
818
+
819
+ # ❌ Avoid: separate definitions require manual conflict management
820
+ flag_option :track
821
+ flag_option :no_track
822
+ conflicts :track, :no_track
823
+ ```
824
+
825
+ **Validation delegation policy — constraint DSL declarations are not used in
826
+ command classes.** The Arguments DSL provides `conflicts`, `requires`,
827
+ `requires_one_of`, `requires_exactly_one_of`, `forbid_values`, and
828
+ `allowed_values` for declaring inter-option constraints. Command classes
829
+ generally do **not** use these declarations. Git is the single source of truth
830
+ for its own option semantics. Command classes use per-argument validation
831
+ parameters (`required:`, `type:`, `allow_nil:`, etc.) and operand format
832
+ validation (option-like operand rejection before `--`). The narrow exception is
833
+ arguments that git cannot observe — see the exception policy below.
834
+
835
+ **What command classes validate:**
836
+
837
+ | Validation | Mechanism | Rationale |
838
+ | --- | --- | --- |
839
+ | Unknown options | `validate_unsupported_options!` in Arguments DSL | Catches typos/misspellings before spawning a process. Git would also reject these, but the error message would be less clear about the Ruby-side fix needed. |
840
+ | Required options | `required: true` in Arguments DSL | Enforces the minimum contract for a command to be meaningful. Avoids spawning a process that will certainly fail. |
841
+ | Type checking | `type:` in Arguments DSL | Catches programming errors (e.g., passing an Integer where a String is expected) that would produce confusing git errors or silent coercion. |
842
+ | Option-like operand rejection | Automatic for operands before `--` | Security concern: prevents user-supplied strings like `'-s'` from being misinterpreted as git flags. |
843
+
844
+ **What command classes do NOT validate (semantic concerns — delegated to git):**
845
+
846
+ | Validation | Delegated to | Rationale |
847
+ | --- | --- | --- |
848
+ | Option conflicts (`--soft` vs `--hard`) | Git (stderr → `Git::FailedError`) | Git is the authority on which options conflict. Constraints drift as git evolves. |
849
+ | Option dependencies (`--all-match` requires `--grep`) | Git (stderr or silent behavior) | Same drift risk. Some dependencies are version-specific. |
850
+ | At-least-one-of groups | Git (stderr → `Git::FailedError`) | Git enforces its own required-argument semantics. |
851
+ | Value-set membership (`--chmod` only accepts `+x`/`-x`) | Git (stderr → `Git::FailedError`) | Git may expand accepted values in future versions. |
852
+ | Forbidden value combinations | Git (stderr → `Git::FailedError`) | Specific to git's internal semantics. |
853
+
854
+ **Design rationale:**
855
+
856
+ 1. **Git is the single source of truth.** Git validates its own option
857
+ interactions and reports clear errors via stderr, surfaced as
858
+ `Git::FailedError`. Ruby-side constraints duplicate this validation and risk
859
+ becoming stale — potentially blocking valid usage when git relaxes a
860
+ restriction in a newer version.
861
+
862
+ 2. **Partial coverage is worse than none.** Inconsistent constraint coverage
863
+ creates a false promise of safety: users can't know whether the absence of
864
+ an `ArgumentError` means "this combination is valid" or "this command
865
+ doesn't have constraints."
866
+
867
+ 3. **Constraint violations are programming errors.** When a developer passes
868
+ conflicting options, they must stop and fix their code regardless of whether
869
+ the error is `ArgumentError` or `Git::FailedError`. The cost difference is
870
+ negligible.
871
+
872
+ 4. **Uniform error semantics.** All invalid-option errors surface uniformly as
873
+ `Git::FailedError` with git's actual error message, rather than a mix of
874
+ `ArgumentError` (Ruby constraint) and `Git::FailedError` (git rejection).
875
+
876
+ 5. **The DSL infrastructure remains available.** The constraint methods in
877
+ `Git::Commands::Arguments` are kept intact. If a compelling case arises for
878
+ a specific constraint (e.g., preventing data loss that git silently allows),
879
+ it can be added on a case-by-case basis with documented justification.
880
+
881
+ **Exception policy — declare constraints only for arguments git cannot observe:**
882
+
883
+ The test: *does this argument appear in git's argv?*
884
+ - **Yes** (normal `flag_option`, `value_option`, etc.) → git can observe it and
885
+ report the error → do not declare a constraint.
886
+ - **No** (`skip_cli: true` arguments, or arguments transformed before reaching
887
+ argv) → git has no mechanism to detect incompatibilities → Ruby must enforce
888
+ them with a constraint declaration.
889
+
890
+ The canonical case is `skip_cli: true` operands routed via stdin. `cat-file
891
+ --batch` commands declare both `conflicts :objects, :batch_all_objects` and
892
+ `requires_one_of :objects, :batch_all_objects`. `:objects` is `skip_cli: true`
893
+ — git never sees it, only `:batch_all_objects` reaches argv. Git cannot detect
894
+ that you passed both (silent wrong result: dumps entire object database) or
895
+ neither (empty output with exit 0), so Ruby must enforce those constraints.
896
+
897
+ A secondary exception: if a combination of **git-visible** arguments causes
898
+ git to **silently discard data** (no error, wrong result), a `conflicts`
899
+ declaration MAY be added with: a code comment explaining why, a reference to
900
+ the git version(s) where the behavior was verified, and a test. As of this
901
+ writing, no such case has been identified.
902
+
903
+ 7. **Adapter methods should forward all positional arguments, not just options**
904
+
905
+ **BUT ONLY IF BACKWARD COMPATIBILITY IS MAINTAINED**
906
+
907
+ When `Git::Lib` methods delegate to command classes, ensure the method signature
908
+ supports ALL positional arguments the command class accepts:
909
+
910
+ ```ruby
911
+ # ❌ Wrong: loses start_point positional argument
912
+ def branch_new(branch, options = {})
913
+ Git::Commands::Branch::Create.new(self).call(branch, **options)
914
+ end
915
+
916
+ # ✅ Correct: forwards all positional arguments
917
+ def branch_new(branch, start_point = nil, options = {})
918
+ Git::Commands::Branch::Create.new(self).call(branch, start_point = nil, **options)
919
+ end
920
+ ```
921
+
922
+ Review the command class's `#call` signature when writing the adapter to ensure
923
+ no arguments are lost in translation.
924
+
925
+ 8. **Arguments are rendered in definition order**
926
+
927
+ The Arguments DSL outputs arguments in the exact order they are defined,
928
+ regardless of type. This allows precise control over argument positioning,
929
+ which is important for commands like `git checkout` where `--` must appear
930
+ between options and pathspecs only when pathspecs are present:
931
+
932
+ ```ruby
933
+ # Arguments render in definition order; end_of_options emits '--' only when
934
+ # at least one following operand produces output
935
+ ARGS = Arguments.define do
936
+ flag_option :force
937
+ operand :tree_ish
938
+ end_of_options
939
+ operand :paths, repeatable: true
940
+ end
941
+ # bind('HEAD', 'file.txt', force: true) => ['--force', 'HEAD', '--', 'file.txt']
942
+ # bind('HEAD', force: true) => ['--force', 'HEAD'] (no trailing --)
943
+
944
+ # Common pattern: static flags first for subcommands like branch --delete
945
+ ARGS = Arguments.define do
946
+ literal '--delete'
947
+ flag_option %i[force f], args: '--force'
948
+ operand :branch_names, repeatable: true, required: true
949
+ end
950
+ # build('feature', force: true) => ['--delete', '--force', 'feature']
951
+ ```
952
+
953
+ 9. **Use `%i[long short]` array syntax for flag aliases**
954
+
955
+ When defining flags with short aliases, use the `%i[]` symbol array syntax with
956
+ the long (canonical) name first. This provides a clean, consistent pattern:
957
+
958
+ ```ruby
959
+ flag_option %i[force f], args: '--force' # force: true OR f: true
960
+ flag_option %i[remotes r], args: '--remotes' # remotes: true OR r: true
961
+ flag_option %i[quiet q], args: '--quiet' # quiet: true OR q: true
962
+ ```
963
+
964
+ The first symbol becomes the primary name used in documentation and error
965
+ messages; subsequent symbols are aliases.
966
+
967
+ 10. **Consider repeatable support in adapter methods when command supports it**
968
+
969
+ When a command class supports repeatable positional arguments (e.g., deleting
970
+ multiple branches), consider whether the `Git::Lib` adapter should expose this
971
+ capability:
972
+
973
+ ```ruby
974
+ # Command class supports multiple branches
975
+ def call(*, **) # repeatable positional
976
+ @execution_context.command('branch', *ARGS.bind(*, **))
977
+ end
978
+
979
+ # ❌ Adapter only accepts single branch
980
+ def branch_delete(branch, options = {})
981
+ Git::Commands::Branch::Delete.new(self).call(branch, **options)
982
+ end
983
+
984
+ # ✅ Adapter exposes repeatable capability
985
+ def branch_delete(*branches, **options)
986
+ options = { force: true }.merge(options)
987
+ Git::Commands::Branch::Delete.new(self).call(*branches, **options)
988
+ end
989
+ ```
990
+
991
+ This allows callers to delete multiple branches efficiently in one git command.
992
+
993
+ 11. **Use `def call(*, **)` when Arguments DSL handles all validation**
994
+
995
+ When using the Arguments DSL with patterns where optional positionals precede
996
+ required ones (matching Ruby's parameter binding semantics), prefer the
997
+ catch-all signature `def call(*, **)` and let `ARGS.bind(*, **)` handle
998
+ all validation.
999
+
1000
+ Note: `ARGS.bind` validates per-argument parameters (unknown options,
1001
+ `required:`, `type:`, `allow_nil:`, and operand format) and also evaluates
1002
+ any declared cross-argument constraints (`conflicts`, `requires`,
1003
+ `requires_one_of`, `requires_exactly_one_of`, `forbid_values`, `allowed_values`).
1004
+ Command classes generally do not declare cross-argument constraints (see Insight
1005
+ 6 validation delegation policy) — inter-option constraint enforcement is
1006
+ delegated to git, with the exception of `skip_cli: true` arguments that never
1007
+ reach git's argv (see the `cat-file --batch` example above).
1008
+
1009
+ ```ruby
1010
+ # git branch -m [<old-branch>] <new-branch>
1011
+ ARGS = Arguments.define do
1012
+ literal '--move'
1013
+ flag_option :force
1014
+ operand :old_branch # optional (no required: true)
1015
+ operand :new_branch, required: true # required
1016
+ end.freeze
1017
+
1018
+ # ✅ Preferred: let ARGS.bind handle validation
1019
+ def call(*, **)
1020
+ @execution_context.command('branch', *ARGS.bind(*, **))
1021
+ end
1022
+
1023
+ # ❌ Avoid: explicit params trigger RuboCop Style/OptionalArguments
1024
+ def call(old_branch = nil, new_branch, **)
1025
+ # ...
1026
+ end
1027
+ ```
1028
+
1029
+ The Arguments DSL with Ruby-like positional allocation correctly fills
1030
+ required parameters before optional ones, so `move.call('new-name')` works
1031
+ as expected.
1032
+
1033
+ 12. **Arguments DSL supports Ruby-like positional parameter allocation**
1034
+
1035
+ The `PositionalAllocator` in the Arguments DSL follows Ruby's method parameter
1036
+ binding semantics. When optional positionals precede required ones, values are
1037
+ allocated to required parameters first:
1038
+
1039
+ ```ruby
1040
+ # Ruby method: def foo(a = 'default', b); end
1041
+ # foo('value') → a='default', b='value' (required b filled first)
1042
+
1043
+ # Arguments DSL equivalent:
1044
+ operand :old_branch # optional
1045
+ operand :new_branch, required: true # required
1046
+
1047
+ # Single value: ARGS.bind('new-name')
1048
+ # → old_branch=nil, new_branch='new-name'
1049
+
1050
+ # Two values: ARGS.bind('old-name', 'new-name')
1051
+ # → old_branch='old-name', new_branch='new-name'
1052
+ ```
1053
+
1054
+ This enables command interfaces that match git CLI patterns like
1055
+ `git branch -m [<old-branch>] <new-branch>` without awkward workarounds.
1056
+
1057
+ 13. **Commands layer maps option semantics, not argument ergonomics**
1058
+
1059
+ The Commands layer should strictly mirror git CLI semantics. When git uses
1060
+ `--option=value` syntax, the Commands class should use a keyword argument—even
1061
+ if a positional would feel more natural in Ruby:
1062
+
1063
+ ```ruby
1064
+ # Git CLI: git branch --set-upstream-to=<upstream> [<branch>]
1065
+ # ↑ <upstream> is the VALUE of --set-upstream-to option, not a positional
1066
+
1067
+ # ✅ Commands layer: strict CLI mapping
1068
+ class SetUpstream
1069
+ ARGS = Arguments.define do
1070
+ value_option :set_upstream_to, inline: true # keyword, not positional
1071
+ operand :branch_name
1072
+ end
1073
+
1074
+ def call(*, **)
1075
+ @execution_context.command('branch', *ARGS.bind(*, **))
1076
+ end
1077
+ end
1078
+
1079
+ # ✅ Higher-layer facade: ergonomic Ruby API (Phase 3)
1080
+ # This wrapper belongs in Git::Repository or Git::Branch, NOT in Git::Lib.
1081
+ # Git::Lib only adapts methods that existed in v4.3.0.
1082
+ def branch_set_upstream(upstream, branch_name = nil)
1083
+ SetUpstream.new(@execution_context).call(branch_name, set_upstream_to: upstream)
1084
+ end
1085
+ ```
1086
+
1087
+ This separation keeps Commands classes predictable (they mirror git 1:1) while
1088
+ allowing higher layers to provide intuitive Ruby interfaces. Ergonomic
1089
+ transformations—like reordering arguments or converting keywords to
1090
+ positionals—belong in higher layers (`Git::Repository`, `Git::Base`, `Git::Branch`),
1091
+ not in `Git::Lib` (which only adapts pre-existing methods for backward compatibility).
1092
+
1093
+ 14. **Use `allow_nil: true` for positional arguments that can be intentionally omitted**
1094
+
1095
+ Some git commands have positional arguments that are semantically present but
1096
+ should not appear in the command line. For example, `git checkout -- file.txt`
1097
+ restores from the index (no tree-ish), while `git checkout HEAD -- file.txt`
1098
+ restores from a commit.
1099
+
1100
+ Use `allow_nil: true` to mark a positional that can accept `nil` as a valid
1101
+ "present but empty" value:
1102
+
1103
+ ```ruby
1104
+ ARGS = Arguments.define do
1105
+ operand :tree_ish, required: true, allow_nil: true
1106
+ end_of_options
1107
+ operand :paths, repeatable: true
1108
+ end
1109
+
1110
+ # Restore from index (tree_ish intentionally nil)
1111
+ ARGS.bind(nil, 'file.txt')
1112
+ # → ['--', 'file.txt']
1113
+
1114
+ # Restore from commit
1115
+ ARGS.bind('HEAD', 'file.txt')
1116
+ # → ['HEAD', '--', 'file.txt']
1117
+ ```
1118
+
1119
+ Without `allow_nil: true`, passing `nil` would either skip the positional slot
1120
+ (causing argument misalignment) or raise a validation error for required
1121
+ arguments.
1122
+
1123
+ 15. **Namespace commands by mode, not just by operation**
1124
+
1125
+ When a git command has fundamentally different modes (not just different
1126
+ operations on the same concept), use nested namespaces that reflect the mode:
1127
+
1128
+ ```ruby
1129
+ # ✅ Different modes of git checkout → separate namespaces
1130
+ Git::Commands::Checkout::Branch # branch switching, creation
1131
+ Git::Commands::Checkout::Files # file restoration from tree-ish/index
1132
+
1133
+ # ✅ Different operations on same concept → flat namespace with operation suffix
1134
+ Git::Commands::Branch::Create
1135
+ Git::Commands::Branch::Delete
1136
+ Git::Commands::Branch::Move
1137
+ ```
1138
+
1139
+ The distinction: `Checkout::Branch` and `Checkout::Files` accept fundamentally
1140
+ different arguments and have different semantics. `Branch::Create` and
1141
+ `Branch::Delete` operate on the same conceptual entity (a branch) with the same
1142
+ core argument (branch name).
1143
+
1144
+ 16. **Subclass by operation, not output mode; `literal` is for operation selectors only**
1145
+
1146
+ Early command migrations created output-mode subclasses (`Diff::Patch`,
1147
+ `Diff::Numstat`, `Diff::Raw`, `Stash::ShowPatch`, etc.) that hardcoded format
1148
+ flags as `literal` entries. This was an anti-pattern: those subclasses were
1149
+ differentiated only by which output format they requested, not by which git
1150
+ operation they performed. The result was that every format change required a
1151
+ new class, the facade had to choose between them by type, and the parser
1152
+ contract was invisible.
1153
+
1154
+ **Correct subclass criterion:** Create a subclass (or a separate class in a
1155
+ namespace) only when the git operation itself differs — e.g.,
1156
+ `Branch::Create` vs `Branch::Delete` (different `--delete` flag makes them
1157
+ different operations). Do **not** create subclasses for the same operation
1158
+ with different `--format`, `--patch`, `--numstat`, `--raw`, etc. flags.
1159
+
1160
+ **Correct `literal` criterion:** A `literal` entry is justified only when it
1161
+ is an operation selector that defines what the class does — e.g.,
1162
+ `literal 'stash'` and `literal 'show'` in `Stash::Show`, or
1163
+ `literal '--delete'` in `Branch::Delete`. Output-mode flags (`--patch`,
1164
+ `--numstat`, `--raw`, `--no-color`, `--format=…`) are never operation
1165
+ selectors; they are options the facade passes to fulfill its own parsing or
1166
+ display requirements.
1167
+
1168
+ **Correct option placement:** Output-mode flags and parser-contract options
1169
+ belong at the facade call site, not inside the command class as `literal`
1170
+ entries. Declare them with `flag_option` or `value_option` in the DSL so the
1171
+ facade can pass them explicitly:
1172
+
1173
+ ```ruby
1174
+ # ❌ Anti-pattern: output mode hardcoded as literal
1175
+ class Diff::Patch < Git::Commands::Base
1176
+ arguments do
1177
+ literal 'diff'
1178
+ literal '--patch' # ← hides the parser contract; wrong layer
1179
+ ...
1180
+ end
1181
+ end
1182
+
1183
+ # ✅ Correct: single class; facade controls output mode
1184
+ class Diff < Git::Commands::Base
1185
+ arguments do
1186
+ literal 'diff'
1187
+ flag_option :patch # facade passes patch: true when it needs patch output
1188
+ flag_option :numstat # facade passes numstat: true when it needs numstat
1189
+ flag_option :raw # facade passes raw: true when it needs raw output
1190
+ ...
1191
+ end
1192
+ end
1193
+
1194
+ # lib/git/lib.rb — parser contract is now explicit and auditable:
1195
+ Git::Commands::Diff.new(self).call(patch: true, numstat: true, ...)
1196
+ ```
1197
+
1198
+ **Known anti-patterns (now fixed):** `Git::Commands::Diff::Patch`,
1199
+ `Git::Commands::Diff::Numstat`, `Git::Commands::Diff::Raw`,
1200
+ `Git::Commands::Stash::ShowPatch`, `Git::Commands::Stash::ShowNumstat`,
1201
+ `Git::Commands::Stash::ShowRaw` — all collapsed in this refactor.
1202
+ Additionally, `literal '--no-color'` in `Log` and `Grep`, and
1203
+ `literal "--format=…"` in `Branch::List` and `Tag::List` were moved to
1204
+ their respective facade call sites.
1205
+
1206
+ 17. **Command classes are neutral; the facade owns policy**
1207
+
1208
+ Command classes are faithful, neutral representations of the git CLI. They
1209
+ never hardcode `literal` entries for output-control, editor-suppression, or
1210
+ progress flags. The facade (`Git::Lib`) sets safe defaults at each call site
1211
+ (e.g. `edit: false`, `progress: false`); callers may override when needed.
1212
+ The execution layer (`GIT_EDITOR='true'`) is an unconditional safety net.
1213
+
1214
+ ```ruby
1215
+ # ❌ Anti-pattern: policy embedded in command class
1216
+ class Pull < Git::Commands::Base
1217
+ arguments do
1218
+ literal 'pull'
1219
+ literal '--no-edit' # ← wrong layer
1220
+ literal '--no-progress' # ← same problem
1221
+ end
1222
+ end
1223
+
1224
+ # ✅ Correct: command is neutral; facade passes policy options
1225
+ class Pull < Git::Commands::Base
1226
+ arguments do
1227
+ literal 'pull'
1228
+ flag_option :edit, negatable: true
1229
+ flag_option :progress, negatable: true
1230
+ end
1231
+ end
1232
+
1233
+ # lib/git/lib.rb — facade sets safe defaults:
1234
+ Git::Commands::Pull.new(self).call(edit: false, progress: false)
1235
+ ```
1236
+
1237
+ See "Command-layer neutrality" in CONTRIBUTING.md for the full policy.
1238
+
1239
+ - **1. Migrate the First Command (`add`)**:
1240
+
1241
+ - **Write Unit Tests First**: Write comprehensive RSpec unit tests for the
1242
+ *proposed* `Git::Commands::Add` class. These tests will fail initially because
1243
+ the class doesn't exist yet. The tests should be fast and mock an object with a
1244
+ `command` method that returns stdout strings.
1245
+
1246
+ - **Create Command Class**: Implement `Git::Commands::Add` to make the tests pass.
1247
+ This class will contain all the logic for building git add arguments and parsing
1248
+ its output. It will accept an execution context (any object responding to
1249
+ `command`) in its constructor and call `@execution_context.command('add', *args,
1250
+ **opts)` to execute commands.
1251
+
1252
+ - **Delegate from `Git::Lib`**: Modify the `add` method within the existing
1253
+ `Git::Lib` class. Instead of containing the implementation, it will now
1254
+ instantiate and call the new `Git::Commands::Add` object, passing `self` as the
1255
+ context.
1256
+
1257
+ - **Verify**: Run the full test suite (both TestUnit and RSpec). The existing tests
1258
+ for `g.add` should still pass, but they will now be executing the new, refactored
1259
+ code.
59
1260
 
60
1261
  - **2. Incrementally Migrate Remaining Commands:**
61
1262
 
62
- - Repeat the process from the previous step for all other commands, one by one or in logical groups (e.g., all `diff` related commands, then all `log` commands).
1263
+ - Repeat the process from the previous step for all other commands, one by one or
1264
+ in logical groups (e.g., all `diff` related commands, then all `log` commands).
63
1265
 
64
1266
  - For each command (`add`, `commit`, `log`, `diff`, `status`, etc.):
65
1267
 
@@ -71,53 +1273,246 @@ This document outlines a step-by-step plan to implement the proposed architectur
71
1273
 
72
1274
  4. Run the full test suite to ensure no regressions have been introduced.
73
1275
 
74
- ## Phase 3: Refactoring the Public Interface
1276
+ ### Command Migration Checklist
1277
+
1278
+ The following tracks the migration status of commands from `Git::Lib` to
1279
+ `Git::Commands::*` classes.
1280
+
1281
+ **Reference implementations** (use these as templates):
1282
+
1283
+ - Simple command: `lib/git/commands/add.rb` + `spec/unit/git/commands/add_spec.rb`
1284
+ - Command with output parsing: `lib/git/commands/fsck.rb` +
1285
+ `spec/unit/git/commands/fsck_spec.rb`
1286
+ - Command with complex options: `lib/git/commands/clone.rb` +
1287
+ `spec/unit/git/commands/clone_spec.rb`
1288
+
1289
+ #### ✅ Migrated Commands
75
1290
 
76
- ***Goal**: Switch the public-facing classes to use the new architecture directly, breaking the final ties to the old implementation.*
1291
+ | Git::Lib Method | Command Class | Spec | Git Command |
1292
+ | --------------- | ------------- | ---- | ----------- |
1293
+ | `add` | `Git::Commands::Add` | `spec/unit/git/commands/add_spec.rb` | `git add` |
1294
+ | `clone` | `Git::Commands::Clone` | `spec/unit/git/commands/clone_spec.rb` | `git clone` |
1295
+ | `commit` | `Git::Commands::Commit` | `spec/unit/git/commands/commit_spec.rb` | `git commit` |
1296
+ | `fsck` | `Git::Commands::Fsck` | `spec/unit/git/commands/fsck_spec.rb` | `git fsck` |
1297
+ | `init` | `Git::Commands::Init` | `spec/unit/git/commands/init_spec.rb` | `git init` |
1298
+ | `mv` | `Git::Commands::Mv` | `spec/unit/git/commands/mv_spec.rb` | `git mv` |
1299
+ | `reset` | `Git::Commands::Reset` | `spec/unit/git/commands/reset_spec.rb` | `git reset` |
1300
+ | `rm` | `Git::Commands::Rm` | `spec/unit/git/commands/rm_spec.rb` | `git rm` |
1301
+ | `clean` | `Git::Commands::Clean` | `spec/unit/git/commands/clean_spec.rb` | `git clean` |
1302
+ | `branches_all` | `Git::Commands::Branch::List` | `spec/unit/git/commands/branch/list_spec.rb` | `git branch --list` |
1303
+ | `branch_new` | `Git::Commands::Branch::Create` | `spec/unit/git/commands/branch/create_spec.rb` | `git branch <name>` |
1304
+ | `branch_delete` | `Git::Commands::Branch::Delete` | `spec/unit/git/commands/branch/delete_spec.rb` | `git branch --delete <branch>` |
1305
+ | N/A (new) | `Git::Commands::Branch::Move` | `spec/unit/git/commands/branch/move_spec.rb` | `git branch --move <old-name> <new-name>` |
1306
+ | `branch_current` | `Git::Commands::Branch::ShowCurrent` | `spec/unit/git/commands/branch/show_current_spec.rb` | `git branch --show-current` |
1307
+ | N/A (new) | `Git::Commands::Branch::Copy` | `spec/unit/git/commands/branch/copy_spec.rb` | `git branch --copy <old-name> <new-name>` |
1308
+ | N/A (new) | `Git::Commands::Branch::SetUpstream` | `spec/unit/git/commands/branch/set_upstream_spec.rb` | `git branch --set-upstream-to <upstream> [<branch>]` |
1309
+ | N/A (new) | `Git::Commands::Branch::UnsetUpstream` | `spec/unit/git/commands/branch/unset_upstream_spec.rb` | `git branch --unset-upstream [<branch>]` |
1310
+ | `diff_full` / `diff_stats` / `diff_path_status` / `diff_index` | `Git::Commands::Diff` | `spec/unit/git/commands/diff_spec.rb` | `git diff` |
1311
+ | `stashes_list` | `Git::Commands::Stash::List` | `spec/unit/git/commands/stash/list_spec.rb` | `git stash list` |
1312
+ | `stash_save` | `Git::Commands::Stash::Push` | `spec/unit/git/commands/stash/push_spec.rb` | `git stash push` |
1313
+ | `stash_pop` | `Git::Commands::Stash::Pop` | `spec/unit/git/commands/stash/pop_spec.rb` | `git stash pop` |
1314
+ | `stash_apply` | `Git::Commands::Stash::Apply` | `spec/unit/git/commands/stash/apply_spec.rb` | `git stash apply` |
1315
+ | `stash_drop` | `Git::Commands::Stash::Drop` | `spec/unit/git/commands/stash/drop_spec.rb` | `git stash drop` |
1316
+ | `stash_clear` | `Git::Commands::Stash::Clear` | `spec/unit/git/commands/stash/clear_spec.rb` | `git stash clear` |
1317
+ | `checkout` / `checkout_file` | `Git::Commands::Checkout::Branch` / `Git::Commands::Checkout::Files` | `spec/unit/git/commands/checkout/branch_spec.rb` / `spec/unit/git/commands/checkout/files_spec.rb` | `git checkout` (branch) / `git checkout` (files) |
1318
+ | `merge` | `Git::Commands::Merge::Start` | `spec/unit/git/commands/merge/start_spec.rb` | `git merge` |
1319
+ | `tag` | `Git::Commands::Tag::*` | `spec/unit/git/commands/tag/*_spec.rb` | `git tag` |
1320
+ | N/A (new) | `Git::Commands::Merge::Abort` | `spec/unit/git/commands/merge/abort_spec.rb` | `git merge --abort` |
1321
+ | N/A (new) | `Git::Commands::Merge::Continue` | `spec/unit/git/commands/merge/continue_spec.rb` | `git merge --continue` |
1322
+ | N/A (new) | `Git::Commands::Merge::Quit` | `spec/unit/git/commands/merge/quit_spec.rb` | `git merge --quit` |
1323
+ | `merge_base` | `Git::Commands::MergeBase` | `spec/unit/git/commands/merge_base_spec.rb` | `git merge-base <commit> <commit>...` |
1324
+ | N/A (new) | `Git::Commands::Stash::Create` | `spec/unit/git/commands/stash/create_spec.rb` | `git stash create` |
1325
+ | N/A (new) | `Git::Commands::Stash::Store` | `spec/unit/git/commands/stash/store_spec.rb` | `git stash store` |
1326
+ | N/A (new) | `Git::Commands::Stash::Branch` | `spec/unit/git/commands/stash/branch_spec.rb` | `git stash branch` |
1327
+ | N/A (new) | `Git::Commands::Stash::Show` | `spec/unit/git/commands/stash/show_spec.rb` | `git stash show` |
1328
+ | `cat_file_*` | `Git::Commands::CatFile::*` | `spec/unit/git/commands/cat_file/*_spec.rb` | `git cat-file` |
1329
+ | `checkout_index` | `Git::Commands::CheckoutIndex` | `spec/unit/git/commands/checkout_index_spec.rb` | `git checkout-index` |
1330
+ | `archive` | `Git::Commands::Archive` | `spec/unit/git/commands/archive_spec.rb` | `git archive` |
1331
+ | `grep` | `Git::Commands::Grep` | `spec/unit/git/commands/grep_spec.rb` | `git grep` |
1332
+ | `log_commits` / `full_log_commits` | `Git::Commands::Log` | `spec/unit/git/commands/log_spec.rb` | `git log` |
1333
+ | `show` | `Git::Commands::Show` | `spec/unit/git/commands/show_spec.rb` | `git show` |
1334
+ | `describe` | `Git::Commands::Describe` | `spec/unit/git/commands/describe_spec.rb` | `git describe` |
1335
+ | `ls_files` | `Git::Commands::LsFiles` | `spec/unit/git/commands/ls_files_spec.rb` | `git ls-files` |
1336
+ | `ls_tree` / `full_tree` / `tree_depth` | `Git::Commands::LsTree` | `spec/unit/git/commands/ls_tree_spec.rb` | `git ls-tree` |
1337
+ | `fetch` | `Git::Commands::Fetch` | `spec/unit/git/commands/fetch_spec.rb` | `git fetch` |
1338
+ | `pull` | `Git::Commands::Pull` | `spec/unit/git/commands/pull_spec.rb` | `git pull` |
1339
+ | `push` | `Git::Commands::Push` | `spec/unit/git/commands/push_spec.rb` | `git push` |
1340
+ | `ls_remote` / `repository_default_branch` | `Git::Commands::LsRemote` | `spec/unit/git/commands/ls_remote_spec.rb` | `git ls-remote` |
1341
+ | `unmerged` | `Git::Commands::Diff` (existing) | — | `git diff --cached` |
1342
+ | N/A (index refresh for `diff_files`/`diff_index`) | `Git::Commands::Status` | `spec/unit/git/commands/status_spec.rb` | `git status` |
1343
+ | N/A (ref listing namespace) | `Git::Commands::ShowRef::List` | `spec/unit/git/commands/show_ref/list_spec.rb` | `git show-ref` |
1344
+ | N/A (ref verification namespace) | `Git::Commands::ShowRef::Verify` | `spec/unit/git/commands/show_ref/verify_spec.rb` | `git show-ref --verify` |
1345
+ | N/A (stdin filter namespace) | `Git::Commands::ShowRef::ExcludeExisting` | `spec/unit/git/commands/show_ref/exclude_existing_spec.rb` | `git show-ref --exclude-existing` |
1346
+ | N/A (existence check namespace) | `Git::Commands::ShowRef::Exists` | `spec/unit/git/commands/show_ref/exists_spec.rb` | `git show-ref --exists` |
1347
+ | `tag_sha` | `Git::Commands::ShowRef::List` | `spec/unit/git/lib_command_spec.rb` | `git show-ref --tags --hash` |
1348
+ | `apply` | `Git::Commands::Apply` | `spec/unit/git/commands/apply_spec.rb` | `git apply` |
1349
+ | `apply_mail` | `Git::Commands::Am::Apply` | `spec/unit/git/commands/am/apply_spec.rb` | `git am` |
1350
+ | N/A (new) | `Git::Commands::Am::Abort` | `spec/unit/git/commands/am/abort_spec.rb` | `git am --abort` |
1351
+ | N/A (new) | `Git::Commands::Am::Continue` | `spec/unit/git/commands/am/continue_spec.rb` | `git am --continue` |
1352
+ | N/A (new) | `Git::Commands::Am::Skip` | `spec/unit/git/commands/am/skip_spec.rb` | `git am --skip` |
1353
+ | N/A (new) | `Git::Commands::Am::Quit` | `spec/unit/git/commands/am/quit_spec.rb` | `git am --quit` |
1354
+ | N/A (new) | `Git::Commands::Am::Retry` | `spec/unit/git/commands/am/retry_spec.rb` | `git am --retry` |
1355
+ | N/A (new) | `Git::Commands::Am::ShowCurrentPatch` | `spec/unit/git/commands/am/show_current_patch_spec.rb` | `git am --show-current-patch` |
1356
+ | `name_rev` | `Git::Commands::NameRev` | `spec/unit/git/commands/name_rev_spec.rb` | `git name-rev` |
1357
+ | `commit_tree` | `Git::Commands::CommitTree` | `spec/unit/git/commands/commit_tree_spec.rb` | `git commit-tree` |
1358
+ | `update_ref` | `Git::Commands::UpdateRef::Update` | `spec/unit/git/commands/update_ref/update_spec.rb` | `git update-ref` |
1359
+ | N/A (new) | `Git::Commands::UpdateRef::Delete` | `spec/unit/git/commands/update_ref/delete_spec.rb` | `git update-ref -d` |
1360
+ | N/A (new) | `Git::Commands::UpdateRef::Batch` | `spec/unit/git/commands/update_ref/batch_spec.rb` | `git update-ref --stdin` |
1361
+ | `gc` | `Git::Commands::Gc` | `spec/unit/git/commands/gc_spec.rb` | `git gc` |
1362
+ | `repack` | `Git::Commands::Repack` | `spec/unit/git/commands/repack_spec.rb` | `git repack` |
1363
+ | `config_get` / `global_config_get` | `Git::Commands::ConfigOptionSyntax::Get` | `spec/unit/git/commands/config_option_syntax/get_spec.rb` | `git config --get` |
1364
+ | `config_list` / `global_config_list` / `parse_config` | `Git::Commands::ConfigOptionSyntax::List` | `spec/unit/git/commands/config_option_syntax/list_spec.rb` | `git config --list` |
1365
+ | `config_set` / `global_config_set` | `Git::Commands::ConfigOptionSyntax::Set` | `spec/unit/git/commands/config_option_syntax/set_spec.rb` | `git config` (set value) |
1366
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::Add` | `spec/unit/git/commands/config_option_syntax/add_spec.rb` | `git config --add` |
1367
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::GetAll` | `spec/unit/git/commands/config_option_syntax/get_all_spec.rb` | `git config --get-all` |
1368
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::GetColor` | `spec/unit/git/commands/config_option_syntax/get_color_spec.rb` | `git config --get-color` |
1369
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::GetColorBool` | `spec/unit/git/commands/config_option_syntax/get_color_bool_spec.rb`, `spec/integration/git/commands/config_option_syntax/get_color_bool_spec.rb` | `git config --get-colorbool` |
1370
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::GetRegexp` | `spec/unit/git/commands/config_option_syntax/get_regexp_spec.rb` | `git config --get-regexp` |
1371
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::GetUrlmatch` | `spec/unit/git/commands/config_option_syntax/get_urlmatch_spec.rb` | `git config --get-urlmatch` |
1372
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::RemoveSection` | `spec/unit/git/commands/config_option_syntax/remove_section_spec.rb` | `git config --remove-section` |
1373
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::RenameSection` | `spec/unit/git/commands/config_option_syntax/rename_section_spec.rb` | `git config --rename-section` |
1374
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::ReplaceAll` | `spec/unit/git/commands/config_option_syntax/replace_all_spec.rb` | `git config --replace-all` |
1375
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::Unset` | `spec/unit/git/commands/config_option_syntax/unset_spec.rb` | `git config --unset` |
1376
+ | N/A (new) | `Git::Commands::ConfigOptionSyntax::UnsetAll` | `spec/unit/git/commands/config_option_syntax/unset_all_spec.rb` | `git config --unset-all` |
77
1377
 
78
- 1. **Refactor Factory Methods**:
1378
+ | `worktrees_all` / `worktree_add` / `worktree_remove` / `worktree_prune` | `Git::Commands::Worktree::List` / `Git::Commands::Worktree::Add` / `Git::Commands::Worktree::Remove` / `Git::Commands::Worktree::Prune` (+ `Lock`, `Unlock`, `Move`, `Repair`) | `spec/unit/git/commands/worktree/*_spec.rb` | `git worktree` |
1379
+ | `change_head_branch` | `Git::Commands::SymbolicRef::Update` (+ `Read`, `Delete`) | `spec/unit/git/commands/symbolic_ref/*_spec.rb` | `git symbolic-ref` |
1380
+ | `current_command_version` | `Git::Commands::Version` | `spec/unit/git/commands/version_spec.rb` | `git version` |
1381
+ | `diff_as_hash` (private) | `Git::Commands::DiffFiles` / `Git::Commands::DiffIndex` | `spec/unit/git/commands/diff_files_spec.rb` / `spec/unit/git/commands/diff_index_spec.rb` | `git diff-files` / `git diff-index` |
1382
+ | `remote_add` / `remote_remove` / `remote_set_url` / `remote_set_branches` | `Git::Commands::Remote::*` | `spec/unit/git/commands/remote/*_spec.rb` | `git remote` |
1383
+ | `revert` | `Git::Commands::Revert::*` | `spec/unit/git/commands/revert/*_spec.rb` | `git revert` |
1384
+ | `rev_parse` | `Git::Commands::RevParse` | `spec/unit/git/commands/rev_parse_spec.rb` | `git rev-parse` |
1385
+ | `read_tree` | `Git::Commands::ReadTree` | `spec/unit/git/commands/read_tree_spec.rb` | `git read-tree` |
1386
+ | `write_tree` | `Git::Commands::WriteTree` | `spec/unit/git/commands/write_tree_spec.rb` | `git write-tree` |
1387
+ | N/A (new) | `Git::Commands::Maintenance::*` | `spec/unit/git/commands/maintenance/*_spec.rb` ⚠️ missing — needs specs | `git maintenance` |
79
1388
 
80
- - Modify the factory methods in the top-level `Git` module (`.open`, `.clone`, etc.).
1389
+ #### Commands To Migrate
81
1390
 
82
- - These methods will now be responsible for creating an instance of `Git::ExecutionContext` and injecting it into the constructor of a `Git::Repository` object.
1391
+ Commands are listed in recommended migration order within each group. Migrate in
1392
+ order: Basic Snapshotting → Branching & Merging → etc.
83
1393
 
84
- The return value of these factories will now be a `Git::Repository` instance, not a `Git::Base` instance.
1394
+ **Basic Snapshotting**:
85
1395
 
86
- 2. **Implement the Facade**:
1396
+ - [x] `rm` → `Git::Commands::Rm` — `git rm`
1397
+ - [x] `mv` → `Git::Commands::Mv` — `git mv`
1398
+ - [x] `commit` → `Git::Commands::Commit` — `git commit`
1399
+ - [x] `reset` → `Git::Commands::Reset` — `git reset`
1400
+ - [x] `clean` → `Git::Commands::Clean` — `git clean`
87
1401
 
88
- - Populate the `Git::Repository` class with the simple, one-line facade methods that delegate to the `Command` objects. For example:
1402
+ **Branching & Merging:**
1403
+
1404
+ - [x] `branches_all` → `Git::Commands::Branch::List` — `git branch --list` (returns `BranchInfo` value objects)
1405
+ - [x] `branch_new` → `Git::Commands::Branch::Create` — `git branch <name> [start-point]`
1406
+ - [x] `branch_delete` → `Git::Commands::Branch::Delete` — `git branch --delete <branch>`
1407
+ - [x] N/A (new) → `Git::Commands::Branch::Move` — `git branch --move <old-name> <new-name>`
1408
+ - [x] `branch_current` → `Git::Commands::Branch::ShowCurrent` — `git branch --show-current`
1409
+ - [x] N/A (new) → `Git::Commands::Branch::Copy` — `git branch --copy <old-name> <new-name>`
1410
+ - [x] N/A (new) → `Git::Commands::Branch::SetUpstream` — `git branch --set-upstream-to <upstream> [<branch>]`
1411
+ - [x] N/A (new) → `Git::Commands::Branch::UnsetUpstream` — `git branch --unset-upstream [<branch>]`
1412
+ - [x] `merge_base` → `Git::Commands::MergeBase` — `git merge-base <commit> <commit>...`
1413
+ - [x] `checkout` / `checkout_file` → `Git::Commands::Checkout::Branch` / `Git::Commands::Checkout::Files` — `git checkout`
1414
+ - [x] `merge` → `Git::Commands::Merge::Start` — `git merge`
1415
+ - [x] N/A (new) → `Git::Commands::Merge::Abort` / `Git::Commands::Merge::Continue` / `Git::Commands::Merge::Quit` — `git merge --abort/--continue/--quit`
1416
+ - [x] `tag` → `Git::Commands::Tag::*` — `git tag` (implemented as `List`, `Create`, `Delete`, and `Verify`)
1417
+ - [x] `stash_*` → `Git::Commands::Stash::*` — `git stash` (List, Push, Pop, Apply, Drop, Clear, Create, Store, Branch, Show)
1418
+
1419
+ **Inspection & Comparison:**
1420
+
1421
+ - [x] `log_commits` / `full_log_commits` → `Git::Commands::Log` — `git log`
1422
+ - [x] `diff_full` / `diff_stats` / `diff_path_status` / `diff_index` →
1423
+ `Git::Commands::Diff` — `git diff`
1424
+ - [x] `unmerged` → (use existing `Git::Commands::Diff` class) — `git diff`
1425
+ (one unmigrated call site in `Git::Lib#unmerged`; command class already exists)
1426
+ - [x] `diff_as_hash` (private) → `Git::Commands::DiffFiles` / `Git::Commands::DiffIndex`
1427
+ — `git diff-files` / `git diff-index`
1428
+ - [x] `status` → `Git::Commands::Status` — `git status`
1429
+ - [x] `tag_sha` (uses `show-ref` internally) → `Git::Commands::ShowRef::List` — `git show-ref`
1430
+ - [x] `show` → `Git::Commands::Show` — `git show`
1431
+ - [x] `describe` → `Git::Commands::Describe` — `git describe`
1432
+ - [x] `grep` → `Git::Commands::Grep` — `git grep`
1433
+ - [x] `ls_files` → `Git::Commands::LsFiles` — `git ls-files`
1434
+ - [x] `ls_tree` / `full_tree` / `tree_depth` → `Git::Commands::LsTree` — `git ls-tree`
1435
+
1436
+ **Sharing & Updating:**
1437
+
1438
+ - [x] `fetch` → `Git::Commands::Fetch` — `git fetch`
1439
+ - [x] `pull` → `Git::Commands::Pull` — `git pull`
1440
+ - [x] `push` → `Git::Commands::Push` — `git push`
1441
+ - [x] `remote_add` / `remote_remove` / `remote_set_url` / `remote_set_branches` →
1442
+ `Git::Commands::Remote` — `git remote`
1443
+ - [x] `ls_remote` / `repository_default_branch` → `Git::Commands::LsRemote` — `git ls-remote`
1444
+
1445
+ **Patching:**
1446
+
1447
+ - [x] `apply` / `apply_mail` → `Git::Commands::Apply` / `Git::Commands::Am::Apply` — `git apply` / `git am`
1448
+ - [x] `revert` → `Git::Commands::Revert::*` — `git revert` (implemented as `Start`, `Continue`, `Skip`, `Abort`, and `Quit`)
1449
+
1450
+ **Plumbing:**
1451
+
1452
+ - [x] `rev_parse` → `Git::Commands::RevParse` — `git rev-parse`
1453
+ - [x] `name_rev` → `Git::Commands::NameRev` — `git name-rev`
1454
+ - [x] `cat_file_*` → `Git::Commands::CatFile::*` — `git cat-file` (implemented as `Full`, `Meta`, `Pretty`, and `Typed`, with `Git::Lib#cat_file_*` delegating through these classes)
1455
+ - [x] `read_tree` → `Git::Commands::ReadTree` — `git read-tree`
1456
+ - [x] `commit_tree` → `Git::Commands::CommitTree` — `git commit-tree`
1457
+ - [x] `update_ref` → `Git::Commands::UpdateRef::*` — `git update-ref` (implemented as `Update`, `Delete`, and `Batch`)
1458
+ - [x] `checkout_index` → `Git::Commands::CheckoutIndex` — `git checkout-index`
1459
+ - [x] `archive` → `Git::Commands::Archive` — `git archive`
1460
+ - [x] `write_tree` → `Git::Commands::WriteTree` — `git write-tree`
1461
+
1462
+ **Administration:**
1463
+
1464
+ - [x] `gc` → `Git::Commands::Gc` — `git gc`
1465
+ - [x] `repack` → `Git::Commands::Repack` — `git repack`
1466
+
1467
+ **Setup & Config:**
1468
+
1469
+ - [x] `config_get` / `config_set` / `global_config_*` / `config_list` →
1470
+ `Git::Commands::ConfigOptionSyntax::*` — `git config`
1471
+
1472
+ **Other:**
1473
+
1474
+ - [x] `worktree_add` / `worktree_remove` → `Git::Commands::Worktree` — `git worktree`
1475
+ - [x] `branch_contains` → (part of `Git::Commands::Branch`)
1476
+ - [x] `change_head_branch` → `Git::Commands::SymbolicRef` — `git symbolic-ref`
1477
+ - [x] `repository_default_branch` → (part of `Git::Commands::LsRemote`)
1478
+ - [x] `current_command_version` → `Git::Commands::Version` — `git version`
1479
+ - [x] N/A (new) → `Git::Commands::Maintenance::*` — `git maintenance` (Register, Run, Start, Stop, Unregister) ⚠️ missing specs
1480
+
1481
+ ## Phase 3: Refactoring the Public Interface
89
1482
 
90
- ```ruby
91
- def commit(msg)
92
- Git::Commands::Commit.new(@execution_context, msg).run
93
- end
94
- ```
1483
+ ***Goal**: Switch the public-facing classes to use the new architecture directly,
1484
+ breaking the final ties to the old implementation.*
95
1485
 
96
- - Organize these facade methods into modules as planned (`lib/git/repository/branching.rb`, etc.) and include them in the main `Git::Repository` class.
1486
+ > **Status**: Task 1 below is complete; Task 2 is in progress — see the [Next Task](#next-task) section above for the current plan.
97
1487
 
98
- 3. **Deprecate and Alias `Git::Base`**:
1488
+ 1. **Add `binary_path:` to `Git::ExecutionContext`** ✅
99
1489
 
100
- - To maintain a degree of backward compatibility through the transition, make `Git::Base` a deprecated constant that points to `Git::Repository`.
1490
+ `Git::ExecutionContext` gained `binary_path:` in its constructor; all subclasses
1491
+ forward it. `command_line_capturing`/`command_line_streaming` use `@binary_path`.
1492
+ `Git::ExecutionContext::Repository.from_base` forwards `binary_path:`. The `Open3`
1493
+ stopgap in `Git.run_git_version` was replaced with
1494
+ `Git::Commands::Version.new(Git::ExecutionContext::Global.new(...)).call`.
101
1495
 
102
- ```ruby
103
- Git::Base = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
104
- 'Git::Base',
105
- 'Git::Repository',
106
- Git::Deprecation
107
- )
108
- ```
1496
+ 2. **Implement the Facade** ⏳
109
1497
 
110
- - This ensures that any user code checking `is_a?(Git::Base)` will not immediately break.
1498
+ `Git::Repository` now includes 12 facade modules (see
1499
+ [Facade Modules Completed](#facade-modules-completed)). `Git::Base` wraps the
1500
+ corresponding methods via `facade_repository`. The domain-object migration
1501
+ iterations (iters 1–9) add further facade methods, but full `Git::Base`
1502
+ coverage requires additional facade PRs for methods not touched by those
1503
+ iterations (e.g. `clean`, `rm`, `tags`/`add_tag`, `fsck`, `show`, `remotes`,
1504
+ remote URL/branch helpers) before the cleanup PR can run.
111
1505
 
112
1506
  ## Phase 4: Final Cleanup and Release Preparation
113
1507
 
114
- ***Goal**: Remove all old code, finalize the test suite, and prepare for the v5.0.0 release.*
1508
+ ***Goal**: Remove all old code, finalize the test suite, and prepare for the v5.0.0
1509
+ release.*
115
1510
 
116
1511
  1. **Remove Old Code**:
117
1512
 
118
1513
  - Delete the `Git::Lib` class entirely.
119
1514
 
120
- - Delete the `Git::Base` class file and remove the deprecation proxy.
1515
+ - Delete the `Git::Base` class file.
121
1516
 
122
1517
  - Remove any other dead code that was part of the old implementation.
123
1518
 
@@ -133,6 +1528,8 @@ This document outlines a step-by-step plan to implement the proposed architectur
133
1528
 
134
1529
  - Thoroughly document the new public API (`Git`, `Git::Repository`, etc.).
135
1530
 
136
- - Mark all internal classes (`ExecutionContext`, `Commands`, `*Path`) with `@api private` in the YARD documentation.
1531
+ - Mark all internal classes (`ExecutionContext`, `Commands`, `*Path`) with `@api
1532
+ private` in the YARD documentation.
137
1533
 
138
- - Update the `README.md` and create a `UPGRADING.md` guide explaining the breaking changes for v5.0.0.
1534
+ - Update the `README.md` and create a `UPGRADING.md` guide explaining the breaking
1535
+ changes for v5.0.0.