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
data/CONTRIBUTING.md CHANGED
@@ -8,23 +8,39 @@
8
8
  - [Summary](#summary)
9
9
  - [How to contribute](#how-to-contribute)
10
10
  - [How to report an issue or request a feature](#how-to-report-an-issue-or-request-a-feature)
11
+ - [Local development setup](#local-development-setup)
12
+ - [Prerequisites](#prerequisites)
13
+ - [Bootstrap the project](#bootstrap-the-project)
14
+ - [Verify the toolchain](#verify-the-toolchain)
15
+ - [Contributor validation policy](#contributor-validation-policy)
11
16
  - [How to submit a code or documentation change](#how-to-submit-a-code-or-documentation-change)
12
17
  - [Commit your changes to a fork of `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git)
13
18
  - [Create a pull request](#create-a-pull-request)
14
19
  - [Get your pull request reviewed](#get-your-pull-request-reviewed)
20
+ - [Before requesting review](#before-requesting-review)
15
21
  - [Branch strategy](#branch-strategy)
16
22
  - [AI-assisted contributions](#ai-assisted-contributions)
17
23
  - [Design philosophy](#design-philosophy)
18
- - [Direct mapping to git commands](#direct-mapping-to-git-commands)
24
+ - [Command-layer neutrality](#command-layer-neutrality)
25
+ - [Wrapping a git command](#wrapping-a-git-command)
26
+ - [Method placement](#method-placement)
27
+ - [Method naming](#method-naming)
28
+ - [Result class naming](#result-class-naming)
19
29
  - [Parameter naming](#parameter-naming)
30
+ - [Parameter values](#parameter-values)
31
+ - [Options](#options)
32
+ - [Positional arguments](#positional-arguments)
20
33
  - [Output processing](#output-processing)
34
+ - [From design to implementation](#from-design-to-implementation)
35
+ - [Example implementations](#example-implementations)
21
36
  - [Coding standards](#coding-standards)
22
37
  - [Commit message guidelines](#commit-message-guidelines)
23
38
  - [What does this mean for contributors?](#what-does-this-mean-for-contributors)
24
39
  - [What to know about Conventional Commits](#what-to-know-about-conventional-commits)
40
+ - [Issue and PR references](#issue-and-pr-references)
25
41
  - [Unit tests](#unit-tests)
26
- - [Continuous integration](#continuous-integration)
27
- - [Documentation](#documentation)
42
+ - [RSpec best practices](#rspec-best-practices)
43
+ - [Unit tests vs Integration tests](#unit-tests-vs-integration-tests)
28
44
  - [Building a specific version of the Git command-line](#building-a-specific-version-of-the-git-command-line)
29
45
  - [Install pre-requisites](#install-pre-requisites)
30
46
  - [Obtain Git source code](#obtain-git-source-code)
@@ -44,8 +60,7 @@ If you have suggestions for improving these guidelines, please propose changes v
44
60
  pull request.
45
61
 
46
62
  Please also review and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) when
47
- participating in the project.
48
- Governance and maintainer expectations are described in
63
+ participating in the project. Governance and maintainer expectations are described in
49
64
  [GOVERNANCE.md](GOVERNANCE.md).
50
65
 
51
66
  ## How to contribute
@@ -67,6 +82,60 @@ To report an issue or request a feature, please [create a `ruby-git` GitHub
67
82
  issue](https://github.com/ruby-git/ruby-git/issues/new). Fill in the template as
68
83
  thoroughly as possible to describe the issue or feature request.
69
84
 
85
+ ## Local development setup
86
+
87
+ Before submitting a change, set up a working local development environment.
88
+ `bin/setup` automates the bootstrap and fails fast with a clear message when a
89
+ prerequisite is missing.
90
+
91
+ ### Prerequisites
92
+
93
+ | Tool | Required version | Notes |
94
+ | --- | --- | --- |
95
+ | Ruby | `>= 3.2.0` (matches `required_ruby_version` in [`git.gemspec`](git.gemspec)) | A version manager such as [rbenv](https://github.com/rbenv/rbenv), [asdf](https://asdf-vm.com/), [chruby](https://github.com/postmodern/chruby), or [rvm](https://rvm.io/) is recommended so you can match the project's CI matrix. |
96
+ | Bundler | Any 2.x or 4.x | Install with `gem install bundler`. |
97
+ | git | `>= 2.28.0` (matches `git.gemspec` `requirements`) | Older git versions are not supported and the test suite will not pass against them. |
98
+ | Node.js / npm | Optional | Required only to install the local Conventional Commit `commit-msg` hook (Husky + commitlint). If npm is missing, `bin/setup` will warn and continue — CI will still validate commit messages. |
99
+
100
+ ### Bootstrap the project
101
+
102
+ From the project root, run:
103
+
104
+ ```shell
105
+ bin/setup
106
+ ```
107
+
108
+ `bin/setup` will:
109
+
110
+ 1. Verify the prerequisites above and exit with a non-zero status if any are
111
+ missing or out of date.
112
+ 2. Run `bundle install` to install Ruby gem dependencies.
113
+ 3. Run `npm install` (when npm is available) to install the Conventional Commit
114
+ `commit-msg` hook used by this project (Husky + commitlint). A separate
115
+ `pre-commit` hook is also installed that blocks direct commits to the
116
+ protected branches (`main`, `4.x`).
117
+ 4. Verify the toolchain by running `bundle exec rake --tasks`.
118
+
119
+ ### Verify the toolchain
120
+
121
+ Once `bin/setup` succeeds, confirm the full test and lint suite passes locally:
122
+
123
+ ```shell
124
+ bundle exec rake
125
+ ```
126
+
127
+ This is the same default task that runs in CI and is the canonical way to
128
+ validate a change before requesting review.
129
+
130
+ ### Contributor validation policy
131
+
132
+ Contributors are expected to run `bundle exec rake` locally and confirm it
133
+ passes before requesting review on a pull request — trivial documentation-only
134
+ fixes (e.g., typo corrections in markdown files) are excepted. "CI passed" is
135
+ not a substitute for local validation; it is a backstop. This applies equally to
136
+ human-authored and AI-assisted contributions — see
137
+ [AI-assisted contributions](#ai-assisted-contributions).
138
+
70
139
  ## How to submit a code or documentation change
71
140
 
72
141
  There is a three-step process for submitting code or documentation changes:
@@ -105,14 +174,28 @@ At least one approval from a project maintainer is required before your pull req
105
174
  can be merged. The maintainer is responsible for ensuring that the pull request meets
106
175
  [the project's coding standards](#coding-standards).
107
176
 
177
+ ### Before requesting review
178
+
179
+ Before moving a pull request out of draft or requesting a review, confirm:
180
+
181
+ - [ ] `bundle exec rake` passes locally on your branch (see
182
+ [Local development setup](#local-development-setup)).
183
+ - [ ] New or changed code has accompanying tests under `spec/`
184
+ (see [Unit tests](#unit-tests)).
185
+ - [ ] Every commit message follows [Conventional Commits](#commit-message-guidelines).
186
+ - [ ] User-facing changes are documented in `README.md` and/or YARD as appropriate.
187
+
188
+ These checks mirror what reviewers and CI will look for; running them locally
189
+ first keeps the review cycle short.
190
+
108
191
  ## Branch strategy
109
192
 
110
193
  This project maintains two active branches:
111
194
 
112
195
  - **`main`**: Active development for the next major version (v5.0.0+). This branch
113
196
  may contain breaking changes.
114
- - **`4.x`**: Maintenance branch for the v4.x release series. This branch receives
115
- bug fixes and backward-compatible improvements only.
197
+ - **`4.x`**: Maintenance branch for the v4.x release series. This branch receives bug
198
+ fixes and backward-compatible improvements only.
116
199
 
117
200
  **Important:** Never commit directly to `main` or `4.x`. All changes must be
118
201
  submitted via pull requests from feature branches. This ensures proper code review,
@@ -126,15 +209,19 @@ When submitting a pull request:
126
209
 
127
210
  ## AI-assisted contributions
128
211
 
129
- AI-assisted contributions are welcome. Please review and apply our [AI Policy](AI_POLICY.md)
130
- before submitting changes. You are responsible for understanding and verifying any
131
- AI-assisted work included in PRs and ensuring it meets our standards for quality,
132
- security, and licensing.
212
+ AI-assisted contributions are welcome. Please review and apply our [AI
213
+ Policy](AI_POLICY.md) before submitting changes. You are responsible for
214
+ understanding and verifying any AI-assisted work included in PRs and ensuring it
215
+ meets our standards for quality, security, and licensing.
133
216
 
134
- ## Design philosophy
217
+ The **human submitter** — not the AI agent — is responsible for ensuring that
218
+ `bundle exec rake` passes locally before requesting review. This is true even
219
+ when the change was authored end-to-end by an agent. "The agent ran the tests"
220
+ and "CI is green" are not substitutes for the submitter running
221
+ [the local validation step](#contributor-validation-policy) themselves; CI is a
222
+ backstop, not a primary validation surface.
135
223
 
136
- *Note: As of v2.x of the `git` gem, this design philosophy is aspirational. Future
137
- versions may include interface changes to fully align with these principles.*
224
+ ## Design philosophy
138
225
 
139
226
  The `git` gem is designed as a lightweight wrapper around the `git` command-line
140
227
  tool, providing Ruby developers with a simple and intuitive interface for
@@ -149,31 +236,280 @@ By following this philosophy, the `git` gem allows users to leverage their exist
149
236
  knowledge of Git while benefiting from the expressiveness and power of Ruby's syntax
150
237
  and paradigms.
151
238
 
152
- ### Direct mapping to git commands
239
+ ## Command-layer neutrality
240
+
241
+ Command classes (`Git::Commands::*`) are **faithful, neutral representations of the
242
+ git CLI**. They declare every option via the DSL but never embed policy choices —
243
+ output-control flags, editor suppression, progress output, verbose mode, etc.
244
+ Policy decisions belong to the facade (`Git::Lib`), which sets safe defaults at
245
+ each call site. Callers may override those defaults when they have a legitimate
246
+ reason (e.g., running in a TTY-attached environment where an editor is desired).
247
+
248
+ This principle serves two purposes: it keeps the command layer a reusable, unbiased
249
+ interface to git, and it supports this gem's **non-interactive execution model** (git
250
+ is never allowed to prompt for input, open an editor, or wait for TTY interaction
251
+ by default). The execution layer provides an unconditional safety net regardless of
252
+ what the caller passes.
253
+
254
+ The three architectural layers each play a distinct role:
255
+
256
+ | Layer | Responsibility | Mechanism |
257
+ | --- | --- | --- |
258
+ | **Command** (`Git::Commands::*`) | Neutral git CLI interface | Declares options via DSL (e.g. `flag_option :edit, negatable: true`) — no policy |
259
+ | **Facade** (`Git::Lib`) | Safe defaults | Sets policy options at each call site (e.g. `edit: false`); callers may override |
260
+ | **Execution** (`Git::CommandLine`) | Unconditional safety net | `GIT_EDITOR='true'` in every subprocess environment |
261
+
262
+ > **Anti-pattern:** `literal '--no-edit'`, `literal '--verbose'`,
263
+ > `literal '--no-progress'` inside a command class — embeds policy in the wrong layer.
264
+ >
265
+ > **Correct pattern:** `flag_option :edit, negatable: true` in the command;
266
+ > `edit: false` passed from the facade call site.
267
+
268
+ ## Wrapping a git command
269
+
270
+ > **Note:** This documentation reflects **Phase 2 (Strangler Fig)** of the architectural
271
+ > redesign. It will be updated in **Phase 3** when `Git::Repository` becomes the primary
272
+ > public API and `Git::Lib` is bypassed. Currently, `Git::Base` remains the public API
273
+ > and `Git::Lib` acts as the delegation layer.
274
+
275
+ This section guides you through wrapping a git command. The first subsections focus
276
+ on **API design**: where methods belong, how to name them, and how to handle
277
+ parameters and output. These describe the public interface that gem users will see.
153
278
 
154
- Git commands are implemented within the `Git::Base` class, with each method directly
155
- corresponding to a `git` command. When a `Git::Base` object is instantiated via
156
- `Git.open`, `Git.clone`, or `Git.init`, the user can invoke these methods to interact
157
- with the underlying Git repository.
279
+ [From design to implementation](#from-design-to-implementation) then shows how to
280
+ structure your code using the gem's three-layer architecture. Note that while we are
281
+ transitioning to `Git::Repository`, the current public API is `Git::Base`, which
282
+ delegates to `Git::Lib`, which in turn delegates to internal command classes.
158
283
 
159
- For example, the `git add` command is implemented as `Git::Base#add`, and the `git
160
- ls-files` command is implemented as `Git::Base#ls_files`.
284
+ > **Note:** When adding new git command wrappers, **always use the new architecture**
285
+ > described in "From design to implementation" with `Git::Commands::*` classes and
286
+ > the Arguments DSL. The gem is being incrementally migrated from `Git::Lib` to this
287
+ > pattern. Do not add new methods directly to `Git::Lib`.
161
288
 
162
- When a single Git command serves multiple distinct purposes, method names within the
163
- `Git::Base` class should use the `git` command name as a prefix, followed by a
164
- descriptive suffix to indicate the specific function.
289
+ ### Method placement
165
290
 
166
- For instance, `#ls_files_untracked` and `#ls_files_staged` could be used to execute
167
- the `git ls-files` command and return untracked and staged files, respectively.
291
+ When implementing a git command, first determine what type of command it is. This
292
+ determines where to implement it in the Ruby API:
293
+
294
+ > **Note:** These placement guidelines define the **public API**. Always add public
295
+ > methods to `Git` module or `Git::Base` (which acts as the current facade for
296
+ > `Git::Repository`), even though the implementation will be in a `Git::Commands::*` class.
297
+
298
+ **Repository factory methods** are implemented on the `Git` module. Use these to
299
+ obtain a repository object for subsequent operations:
300
+
301
+ ```ruby
302
+ repo = Git.clone('https://github.com/user/repo.git', 'local_path')
303
+ repo = Git.init('new_repo')
304
+ repo = Git.open('.')
305
+ ```
306
+
307
+ **Repository-scoped commands** operate within a repository context. Implement these
308
+ `Git::Base` instance methods:
309
+
310
+ ```ruby
311
+ repo.add('file.txt')
312
+ repo.commit('Add file')
313
+ repo.log
314
+ ```
315
+
316
+ **Non-repository commands** do not require a repository context. Implement these as
317
+ methods on the `Git` module:
318
+
319
+ ```ruby
320
+ Git.config_get('user.name', global: true)
321
+ Git.config_set('user.email', 'user@example.com', global: true)
322
+ ```
323
+
324
+ Some commands, like `git config`, can operate in multiple contexts:
325
+
326
+ - **On the `Git` module**: A scope parameter (`global: true`, `system: true`) or
327
+ `file:` parameter is required. The `local:` and `worktree:` options are not allowed
328
+ since they require a repository.
329
+ - **On a `Git::Base` instance**: The command defaults to the repository's local
330
+ scope. The `worktree: true` option is also available.
331
+
332
+ ### Method naming
333
+
334
+ Each method corresponds directly to a `git` command. For example, the `git add`
335
+ command is implemented as `Git::Base#add`, and the `git ls-files` command is
336
+ implemented as `Git::Base#ls_files`.
337
+
338
+ When a single Git command serves multiple distinct purposes, method names should use
339
+ the git command name as a prefix, followed by a descriptive suffix indicating the
340
+ specific function. The suffix should correspond to the git option that distinguishes
341
+ the behavior.
342
+
343
+ For example, `git config` supports `--get`, `--set`, `--list`, `--unset`, and other
344
+ options. These are implemented as separate methods:
345
+
346
+ ```ruby
347
+ repo.config_get('user.name') # git config --get user.name
348
+ repo.config_set('user.name', 'Scott') # git config user.name Scott
349
+ repo.config_list # git config --list
350
+ repo.config_unset('user.name') # git config --unset user.name
351
+ repo.config_get_all('remote.origin.url') # git config --get-all remote.origin.url
352
+ ```
168
353
 
169
354
  To enhance usability, aliases may be introduced to provide more user-friendly method
170
355
  names where appropriate.
171
356
 
357
+ See also [Output processing](#output-processing) for when different output formats
358
+ require separate methods.
359
+
360
+ ### Result class naming
361
+
362
+ Parsed result objects returned from facade methods follow a reserved suffix convention:
363
+
364
+ - **`*Info`** — a parsed metadata struct returned from a query (e.g., `BranchInfo`,
365
+ `TagInfo`, `StashInfo`, `DiffInfo`). Always lives in the top-level `Git::` namespace.
366
+ - **`*Result`** — the outcome of a mutating or destructive operation (e.g.,
367
+ `BranchDeleteResult`, `TagDeleteResult`). Also lives in `Git::`.
368
+
369
+ Do **not** use these suffixes on `Git::Commands::*` command classes — those are
370
+ subprocess runners, not data objects. A reader seeing `Commands::Foo::BarInfo`
371
+ expects a parsed struct, not a class that shells out to git.
372
+
172
373
  ### Parameter naming
173
374
 
174
375
  Parameters within the `git` gem methods are named after their corresponding long
175
376
  command-line options, ensuring familiarity and ease of use for developers already
176
- accustomed to Git. Note that not all Git command options are supported.
377
+ accustomed to Git.
378
+
379
+ For example, `git config --global` becomes `global: true`, and `git config --file`
380
+ becomes `file: '/path/to/config'`.
381
+
382
+ As a lightweight wrapper, the gem passes options directly to the git command-line.
383
+ This means git itself will validate option combinations and report errors. This
384
+ approach is preferred as long as the error messages returned by git are actionable
385
+ and understandable for users of the gem.
386
+
387
+ When multiple options are mutually exclusive (like `--global`, `--local`,
388
+ `--system`), only one may be specified. Providing more than one will raise an
389
+ `ArgumentError`.
390
+
391
+ Note that not all Git command options are supported.
392
+
393
+ ### Parameter values
394
+
395
+ This section defines how git command-line options and positional arguments map to
396
+ Ruby method parameters. Contributors must follow these conventions:
397
+
398
+ #### Options
399
+
400
+ Git command-line options are passed as keyword arguments in the Ruby API. Methods
401
+ accept these via an options splat parameter (e.g., `def replace(object, replacement,
402
+ **options)`). Each option is mapped to a keyword argument as described below.
403
+
404
+ - **Boolean flags**: Git options like `--global` or `--bare` are mapped to `global:
405
+ true` or `bare: true`. Omit the key or use `false` to leave the flag unset.
406
+ - `git config --global` → `global: true`
407
+
408
+ - **Negated boolean flags**: Options like `--no-reflogs` are mapped to `no_reflogs:
409
+ true`.
410
+ - `git branch --no-reflogs` → `no_reflogs: true`
411
+
412
+ - **Value options**: Options that take a value, such as `--file <path>` or `--author
413
+ <name>`, are mapped as `file: '/path'`, `author: 'Name'`.
414
+ - `git config --file /tmp/config` → `file: '/tmp/config'`
415
+
416
+ - **Options with optional values**: If a git option can be used as a flag or with a
417
+ value (e.g., `--color` or `--color=always`), use `color: true` for the flag form,
418
+ or `color: 'always'` for the value form.
419
+ - `git log --color` → `color: true`
420
+ - `git log --color=always` → `color: 'always'`
421
+
422
+ - **List/array options**: Options that can be repeated or take multiple values (e.g.,
423
+ `--exclude <pattern>`, `--pathspec-from-file <file>`) are mapped to arrays:
424
+ `exclude: ['foo', 'bar']`.
425
+ - `git ls-files --exclude=foo --exclude=bar` → `exclude: ['foo', 'bar']`
426
+
427
+ - **Key-value pair options**: Options like `-c key=value` are mapped as `c: { 'key'
428
+ => 'value' }` or as an array of pairs if multiple are allowed.
429
+ - `git -c user.name=Scott` → `c: { 'user.name' => 'Scott' }`
430
+
431
+ - **Mutually exclusive options**: If options are mutually exclusive (e.g.,
432
+ `--global`, `--local`, `--system`), only one may be used at a time. Setting more
433
+ than one raises `ArgumentError`. The DSL enforces this via `conflicts`
434
+ declarations at bind time. For **negatable flag options** (`negatable: true`),
435
+ passing `false` (which emits `--no-flag`) also counts as using that option in the
436
+ conflict check; non-negatable `false` is treated as absent.
437
+
438
+ - **Forbidden value combinations (negatable flags)**: When two negatable flags may
439
+ both be present but only certain value pairings are contradictory, use
440
+ `forbid_values` declarations instead of (or in addition to) `conflicts`.
441
+ `conflicts` is presence-based and blocks all co-presence; `forbid_values` blocks
442
+ only the exact `name: value` tuples listed, leaving semantically equivalent pairs
443
+ valid. For example, `--all --no-ignore-removal` and `--no-all --ignore-removal`
444
+ are equivalent and should remain allowed, while `--all --ignore-removal` and
445
+ `--no-all --no-ignore-removal` are contradictory and should be rejected:
446
+
447
+ ```ruby
448
+ forbid_values all: true, ignore_removal: true # contradictory
449
+ forbid_values all: false, ignore_removal: false # contradictory
450
+ ```
451
+
452
+ Unknown names raise `ArgumentError` at definition time. Alias names are
453
+ canonicalized automatically.
454
+
455
+ - **Exactly-one required from a mutually exclusive group**: When exactly one of a
456
+ group of arguments must be provided (e.g., a command that accepts exactly one of
457
+ `--mode-a`, `--mode-b`, or `--mode-c`), omitting all of them or supplying more
458
+ than one raises `ArgumentError`. The DSL enforces this via
459
+ `requires_exactly_one_of` declarations, which combine `requires_one_of`
460
+ (at-least-one) and `conflicts` (at-most-one) in a single declaration.
461
+
462
+ - **At-least-one required**: When at least one of a group of arguments (options or
463
+ positional) must be provided, but the group is not mutually exclusive, omitting
464
+ all of them raises `ArgumentError`. The DSL enforces this via `requires_one_of`
465
+ declarations at bind time.
466
+
467
+ #### Positional arguments
468
+
469
+ Arguments that are not options (e.g., file names, branch names) are passed as method
470
+ arguments, not as keyword arguments.
471
+
472
+ - **Only single-valued positional arguments**: If a command has one or more
473
+ single-valued positional arguments (e.g., `<arg1>` or `<arg1> <arg2>`), pass each
474
+ as a separate method argument, in the order they appear in the official git
475
+ documentation and CLI usage. Optional arguments (indicated by `[<arg>]`) should
476
+ default to `nil`.
477
+ - `git cmd <object>` → `def cmd(object)` (fictitious command)
478
+ - `git replace <object> <replacement>` → `def replace(object, replacement)`
479
+ - `git clone <repository> [<directory>]` → `def clone(repository, directory = nil)`
480
+
481
+ - **Single multi-valued positional argument**: If a command has a single multi-valued
482
+ positional argument (e.g., `<pathspec>...` or `[<pathspec>...]`), use a splat
483
+ parameter to accept zero or more values (optional) or one or more values
484
+ (required).
485
+ - `git add [<pathspec>...]` → `def add(*paths)`
486
+
487
+ - **Mixed single-valued and multi-valued positional arguments — `--` separated
488
+ (independently reachable groups)**: When a git command separates two optional
489
+ groups with `--` (e.g., `[<tree-ish>] [-- <pathspec>...]`), callers may want
490
+ to supply the post-`--` group *without* supplying the first group. Use the
491
+ single-valued argument as a regular optional parameter and the multi-valued
492
+ group as a keyword argument with an empty array default. The keyword argument
493
+ should accept a single value or an array; wrap a single value in an array
494
+ internally.
495
+ - `git checkout [<branch>] [-- <pathspec>...]` → `def checkout(branch = nil,
496
+ pathspecs: [])`
497
+ - `git diff [<tree-ish>] [-- <pathspec>...]` → `def diff(tree_ish = nil,
498
+ pathspec: [])`
499
+ - Callers can then do `checkout(pathspecs: ['file.rb'])` (no branch) or
500
+ `diff('HEAD~3', pathspec: ['file.rb'])` (both), with no ambiguity.
501
+
502
+ - **Multiple optional single-valued positional arguments — pure nesting
503
+ (second only meaningful with first)**: When the git SYNOPSIS shows nested
504
+ optional brackets and the inner operand is only useful in the presence of the
505
+ outer one, both arguments may be regular optional parameters in left-to-right
506
+ order. A caller would never supply the second without the first.
507
+ - `git diff [<commit1> [<commit2>]]` → `def diff(commit1 = nil, commit2 = nil)`
508
+ - Callers can do `diff` (no args), `diff('HEAD~3')`, or `diff('HEAD~3', 'HEAD')`.
509
+ There is no case where someone would pass `commit2` without `commit1`.
510
+
511
+ These conventions ensure the API is predictable and closely aligned with the git CLI.
512
+ If a new option type is encountered, extend this section to document the mapping.
177
513
 
178
514
  ### Output processing
179
515
 
@@ -184,6 +520,222 @@ These Ruby objects often include methods that allow for further Git operations w
184
520
  useful, providing additional functionality while staying true to the underlying Git
185
521
  behavior.
186
522
 
523
+ When a single git command can produce distinctly different output types based on its
524
+ options, implement separate methods for each output type. Follow the same naming
525
+ convention used for commands with multiple purposes: use the git command name as a
526
+ prefix, followed by a suffix that describes the specific output type or
527
+ functionality.
528
+
529
+ For example, `git diff` can produce full diffs, statistical summaries, or path status
530
+ information depending on the options used. These are implemented as separate methods:
531
+
532
+ ```ruby
533
+ repo.diff_full('HEAD~1', 'HEAD') # Full diff output (git diff -p)
534
+ repo.diff_stats('HEAD~1', 'HEAD') # Statistical summary (git diff --numstat)
535
+ repo.diff_path_status('HEAD~1', 'HEAD') # File paths and status (git diff --name-status)
536
+ ```
537
+
538
+ This approach ensures each method has a clear, predictable return type and allows for
539
+ targeted parsing logic appropriate to each output format.
540
+
541
+ ### From design to implementation
542
+
543
+ > **Note:** **Use this architecture for all new commands.** The gem is being
544
+ > incrementally migrated using the "Strangler Fig" pattern:
545
+ >
546
+ > 1. **Phase 1 (completed)**: Foundation work to introduce the new command architecture
547
+ > and prepare the codebase for incremental migration.
548
+ > 2. **Phase 2 (current)**: New `Git::Commands::*` classes are created, and `Git::Lib`
549
+ > methods delegate to them. `Git::Lib` remains but becomes a thin wrapper.
550
+ > 3. **Phase 3 (planned)**: Public API (`Git::Base`) will be refactored to use
551
+ > `Git::Commands::*` directly, bypassing `Git::Lib`.
552
+ > 4. **Phase 4 (planned)**: `Git::Lib` will be removed entirely.
553
+ >
554
+ > When adding new commands, create the `Git::Commands::*` class and have the
555
+ > corresponding `Git::Lib` method delegate to it (see `Git::Lib#add` for an example).
556
+ > When you encounter existing commands, you may optionally refactor them to this
557
+ > pattern following the TDD workflow.
558
+
559
+ The gem uses a three-layer architecture that separates the public API from internal
560
+ implementation:
561
+
562
+ 1. **Facade layer (`Git::Base` and `Git` module)** — The current public interface.
563
+ Methods here are thin wrappers that delegate to `Git::Lib`, which in turn
564
+ delegates to internal command classes.
565
+
566
+ 2. **Command layer (`Git::Commands::*`)** — Internal classes that implement git
567
+ commands. Each command class handles argument building and output parsing.
568
+
569
+ 3. **Execution layer (`Git::ExecutionContext`)** — Runs raw git commands. Command
570
+ classes use this to execute git and receive output.
571
+
572
+ When wrapping a new git command:
573
+
574
+ 1. **Design the public API** using the guidelines in this section (placement, naming,
575
+ parameters, output)
576
+
577
+ 2. **Create a command class** in `lib/git/commands/` that:
578
+ - Accepts an `ExecutionContext` and any required arguments
579
+ - Defines arguments using the Arguments DSL
580
+ - Parses the output into Ruby objects
581
+
582
+ 3. **Add the facade method** to `Git::Base` (or `Git` module) that delegates to
583
+ `Git::Lib`.
584
+
585
+ Example structure for `git add`:
586
+
587
+ ```ruby
588
+ # lib/git/commands/add.rb (internal)
589
+ require 'git/commands/base'
590
+
591
+ module Git
592
+ module Commands
593
+ class Add < Base
594
+ arguments do
595
+ literal 'add'
596
+ flag_option :all
597
+ flag_option :force
598
+ operand :paths, repeatable: true, default: [], separator: '--'
599
+ end
600
+
601
+ # Execute the git add command
602
+ #
603
+ # @overload call(*paths, all: nil, force: nil)
604
+ #
605
+ # @param paths [Array<String>] files to be added
606
+ #
607
+ # @param all [Boolean] Add, modify, and remove index entries to match the worktree
608
+ #
609
+ # @param force [Boolean] Allow adding otherwise ignored files
610
+ #
611
+ # @return [Git::CommandLineResult] the result of the command
612
+ #
613
+ def call(...) = super # rubocop:disable Lint/UselessMethodDefinition
614
+ end
615
+ end
616
+ end
617
+ ```
618
+
619
+ **How `Base` works**: `Base` provides default `#initialize` (accepts an
620
+ `execution_context`) and `#call` (binds arguments via the DSL, calls
621
+ `execution_context.command`, validates exit status). Simple commands only need to
622
+ declare `arguments do … end` and write `def call(...) = super` (which also serves as
623
+ the YARD documentation anchor for per-command documentation). That forwarding method
624
+ looks "useless" to RuboCop, so we disable `Lint/UselessMethodDefinition` on it; the
625
+ method body must exist for YARD to attach command-specific docs, even though all work
626
+ is delegated to `Base#call`.
627
+
628
+ **Method Signature Convention**: Most commands use `def call(...) = super`, which
629
+ forwards all arguments to `Base#call`. `Base#call` binds them via the Arguments DSL,
630
+ calls `@execution_context.command(*args, **args.execution_options,
631
+ raise_on_failure: false)`, and validates the exit status.
632
+
633
+ Override `call` explicitly in three situations:
634
+
635
+ 1. **Input validation** — guard `ArgumentError` for invalid option combinations that
636
+ the DSL cannot express (e.g., empty operands without a compensating flag).
637
+ 2. **Stdin via IO pipe** — commands using the `--batch` / `--batch-check` protocol
638
+ must feed object names to the subprocess's stdin. Use the inherited
639
+ `Base#with_stdin(content)`, which opens an `IO.pipe`, writes the string content,
640
+ and yields the read end as `in:`. Do not open a pipe manually — `StringIO` is
641
+ not accepted by `Process.spawn` (it has no file descriptor).
642
+ 3. **Non-trivial option routing** — when multiple call shapes require different
643
+ argument sets built separately before dispatching.
644
+
645
+ When overriding, work with `args_definition.bind(...)` directly and delegate
646
+ exit-status handling to the inherited `validate_exit_status!`. Extract bulk logic
647
+ into private helpers to satisfy Rubocop `Metrics` thresholds:
648
+
649
+ ```ruby
650
+ def call(*objects, **options)
651
+ raise ArgumentError, '...' if objects.empty? && !options[:batch_all_objects]
652
+
653
+ bound = args_definition.bind(**options)
654
+ with_stdin(objects.map { |o| "#{o}\n" }.join) { |reader| run_batch(bound, reader) }
655
+ end
656
+
657
+ private
658
+
659
+ def run_batch(bound, reader)
660
+ result = @execution_context.command(*bound, in: reader, **bound.execution_options, raise_on_failure: false)
661
+ validate_exit_status!(result)
662
+ result
663
+ end
664
+ ```
665
+
666
+ Validation of supported options is handled by the `Arguments` DSL, which raises
667
+ `ArgumentError` for unsupported keywords. The public API in `Git::Lib` handles the
668
+ translation from single values or arrays to the splat format.
669
+
670
+ > **YARD Documentation Note:** When using anonymous keyword forwarding (`**`), YARD
671
+ > cannot infer the method signature. Use the `@overload` directive with **explicit
672
+ > keyword parameters** (e.g., `@overload call(paths, all: nil, force: nil)`) and
673
+ > document each keyword with its own `@param` tag. Do not use `@option` with
674
+ > `@overload`. See the example above for the pattern.
675
+ >
676
+ > **Testing Requirement:** When defining arguments with the DSL, you must write RSpec
677
+ > tests that verify each argument handles valid values correctly (booleans, strings,
678
+ > arrays) and handles invalid values appropriately. Use a separate `context` block for
679
+ > testing each option to ensure clarity and isolation. See
680
+ > `spec/unit/git/commands/add_spec.rb` for examples of comprehensive argument testing.
681
+
682
+ ```ruby
683
+ # lib/git/lib.rb (delegation)
684
+ class Git::Lib
685
+ # Git::Lib may accept an options hash for backward compatibility
686
+ def add(paths = '.', options = {})
687
+ # Convert to splat + keyword arguments when calling the command class
688
+ Git::Commands::Add.new(self).call(*Array(paths), **options)
689
+ end
690
+ end
691
+
692
+ # lib/git/base.rb (public facade)
693
+ class Git::Base
694
+ def add(paths = '.', **options)
695
+ lib.add(paths, options)
696
+ end
697
+ end
698
+ ```
699
+
700
+ For factory methods and non-repository commands, the pattern is similar but differs
701
+ in how the `ExecutionContext` is obtained:
702
+
703
+ ```ruby
704
+ # Factory method (Git.clone) — creates context, runs command, returns repository
705
+ module Git
706
+ def self.clone(url, path = nil, **options)
707
+ # logic to call Git::Commands::Clone via Git::Lib
708
+ end
709
+ end
710
+
711
+ # Non-repository command (Git.global_config) — standalone context
712
+ module Git
713
+ def self.global_config(name, value = nil)
714
+ Git::Lib.new.global_config(name, value)
715
+ end
716
+ end
717
+ ```
718
+
719
+ > **Note:** The `Git::Lib` class currently acts as the execution context. In the new
720
+ > architecture, `Git::Lib` methods delegate to `Git::Commands::*` classes, passing `self`
721
+ > (the `Git::Lib` instance) as the execution context.
722
+
723
+ ### Example implementations
724
+
725
+ The following command classes demonstrate patterns for implementing new commands.
726
+ See `lib/git/commands/` and `spec/unit/git/commands/` for the full implementations:
727
+
728
+ - **Simple command**: `Git::Commands::Add` — straightforward argument building with
729
+ the Arguments DSL
730
+ - **Command with output parsing**: `Git::Commands::Fsck` — parses git output into
731
+ structured Ruby objects
732
+ - **Factory command**: `Git::Commands::Clone` — returns data for creating a
733
+ repository object
734
+ - **Multiple outputs**: `Git::Commands::Diff::*` — subclasses for different output
735
+ formats (planned)
736
+ - **Multi-context**: `Git::Commands::Config` — handles both module and instance
737
+ variants (planned)
738
+
187
739
  ## Coding standards
188
740
 
189
741
  To ensure high-quality contributions, all pull requests must meet the following
@@ -213,8 +765,11 @@ Commits standard](https://www.conventionalcommits.org/en/v1.0.0/). Commits not
213
765
  adhering to this standard will cause the CI build to fail. PRs will not be merged if
214
766
  they include non-conventional commits.
215
767
 
216
- A git pre-commit hook may be installed to validate your conventional commit messages
217
- before pushing them to GitHub by running `bin/setup` in the project root.
768
+ A git `commit-msg` hook (Husky + commitlint) that validates your Conventional
769
+ Commit messages locally is installed automatically as part of the project
770
+ bootstrap — see [Local development setup](#local-development-setup). The hook
771
+ depends on Node.js and npm; if those are not installed, `bin/setup` will warn
772
+ and skip the hook, and commit-message validation will only run in CI.
218
773
 
219
774
  #### What to know about Conventional Commits
220
775
 
@@ -236,7 +791,7 @@ Examples of valid commits:
236
791
  Commits that include breaking changes must include an exclaimation mark before the
237
792
  colon:
238
793
 
239
- - `feat!: removed Git::Base.commit_force`
794
+ - `feat!: removed Git::Repository#commit_force`
240
795
 
241
796
  The commit messages will drive how the version is incremented for each release:
242
797
 
@@ -267,44 +822,130 @@ by not acted upon.
267
822
  See [the Conventional Commits
268
823
  specification](https://www.conventionalcommits.org/en/v1.0.0/) for more details.
269
824
 
825
+ #### Issue and PR references
826
+
827
+ Due to a parser limitation in commitlint, using `#<number>` anywhere in the commit
828
+ **body** causes everything from that line onward to be treated as a footer, which
829
+ triggers a `footer-leading-blank` error.
830
+
831
+ To avoid this:
832
+
833
+ - **In the body**, omit the `#` when mentioning an issue or PR — write `issue 1000`
834
+ not `issue #1000`.
835
+ - **In the footer**, always include `#` for closing references:
836
+ `Closes #1000`, `Fixes #1000`, or `Resolves #1000`.
837
+ - If you only want to mention an issue for context (not close it), omit the `#` in
838
+ the body — no footer line is needed.
839
+
840
+ To validate a commit message before committing:
841
+
842
+ ```bash
843
+ npx commitlint --format @commitlint/format < commit_msg.txt
844
+ ```
845
+
846
+ To see how commitlint has parsed a commit message:
847
+
848
+ ```bash
849
+ cat commit_msg.txt | node -e "
850
+ const parse = require('@commitlint/parse');
851
+ let msg = '';
852
+ process.stdin.on('data', d => msg += d);
853
+ process.stdin.on('end', () =>
854
+ parse.default(msg.trim()).then(r => console.log(JSON.stringify(r, null, 2)))
855
+ );
856
+ " | jq
857
+ ```
858
+
270
859
  ### Unit tests
271
860
 
272
861
  - All changes must be accompanied by new or modified unit tests.
273
862
  - The entire test suite must pass when `bundle exec rake default` is run from the
274
863
  project's local working copy.
275
864
 
276
- While working on specific features, you can run individual test files or a group of
277
- tests using `bin/test`:
865
+ This project uses two test frameworks:
866
+
867
+ - **RSpec** (`spec/`) - **Primary framework for all new tests.**
868
+ - **Test::Unit** (`tests/units/`) - **Legacy test suite.** Maintained for existing
869
+ coverage but should not be extended for new features unless absolutely necessary.
870
+
871
+ #### RSpec best practices
872
+
873
+ - **Public methods**: Use a separate `describe '#method_name'` block for each public
874
+ method.
875
+ - **Contexts**: Use separate `context` blocks for different scenarios.
876
+ - **Options**: For methods accepting options (like commands), use a separate
877
+ `context` for each option to ensure isolation and comprehensiveness.
878
+ - **One assertion per test**: Each test should verify one specific aspect of
879
+ behavior. Exceptions include: (a) testing that an object has expected attributes
880
+ after creation (e.g., verifying multiple fields of a returned object), (b)
881
+ verifying expected side effects of a single operation (e.g., a method that both
882
+ returns a value and modifies state), (c) testing that multiple related
883
+ assertions hold for the same setup (e.g., boundary conditions).
884
+
885
+ #### Unit tests vs Integration tests
886
+
887
+ This project uses two types of RSpec tests, organized by directory:
888
+
889
+ - **Unit tests** (`spec/unit/`) - Test individual classes and methods with mocked
890
+ execution context. These verify that the gem builds correct git command arguments
891
+ and properly handles git output. Unit tests should mock `@execution_context` to
892
+ avoid calling real git commands.
893
+
894
+ - **Integration tests** (`spec/integration/`) - Test the gem's behavior against real
895
+ git repositories. These verify that mocked assumptions in unit tests match actual
896
+ git behavior. Integration tests create temporary repositories using `Dir.mktmpdir`
897
+ and run real git commands through the gem's public API.
898
+
899
+ **Purpose of integration tests**: Integration tests validate that the gem correctly
900
+ interacts with git, not that git itself works correctly. They should verify:
901
+
902
+ - That the gem's mocked command expectations match real git output format
903
+ - That the gem correctly handles real git behavior (e.g., unicode in branch names)
904
+ - That command options produce expected git behavior
905
+ - Edge cases that are difficult to mock reliably
906
+
907
+ **Integration test guidelines**:
908
+
909
+ - Keep tests **minimal and purposeful** - only create what's needed for the test
910
+ - Focus on **key behaviors** that unit tests can't verify
911
+ - Don't test git's functionality - test the gem's interaction with git
912
+ - Use the shared context `'in an empty repository'` for temporary repo setup
913
+ - Use `Git::IntegrationTestHelpers` methods for file operations
914
+ - Each test should verify one specific git interaction pattern
915
+
916
+ **Example**: An integration test for branch listing should verify that the gem
917
+ correctly parses git's branch list format, not that git can create branches.
918
+
919
+ While working on specific features, you can run tests using:
278
920
 
279
921
  ```bash
280
- # run a single file (from tests/units):
281
- $ bin/test test_object
922
+ # Run all tests (TestUnit + RSpec):
923
+ $ bundle exec rake test_all
282
924
 
283
- # run multiple files:
284
- $ bin/test test_object test_archive
925
+ # Run only TestUnit integration tests:
926
+ $ bundle exec rake test
285
927
 
286
- # run all unit tests:
287
- $ bin/test
928
+ # Run only RSpec tests (unit + integration):
929
+ $ bundle exec rake spec
288
930
 
289
- # run unit tests with a different version of the git command line:
290
- $ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test
291
- ```
931
+ # Run only RSpec unit tests:
932
+ $ bundle exec rake spec:unit
292
933
 
293
- ### Continuous integration
934
+ # Run only RSpec integration tests:
935
+ $ bundle exec rake spec:integration
294
936
 
295
- All tests must pass in the project's [GitHub Continuous Integration
296
- build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI) before the
297
- pull request will be merged.
937
+ # Run a single TestUnit file (from tests/units):
938
+ $ bin/test test_object
298
939
 
299
- The [Continuous Integration
300
- workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml)
301
- runs both `bundle exec rake default` and `bundle exec rake test:gem` from the
302
- project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile).
940
+ # Run multiple TestUnit files:
941
+ $ bin/test test_object test_archive
303
942
 
304
- ### Documentation
943
+ # Run a specific RSpec file:
944
+ $ bundle exec rspec spec/unit/git/commands/add_spec.rb
305
945
 
306
- New and updated public methods must include [YARD](https://yardoc.org/)
307
- documentation.
946
+ # Run TestUnit tests with a different version of the git command line:
947
+ $ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bin/test
948
+ ```
308
949
 
309
950
  New and updated public-facing features should be documented in the project's
310
951
  [README.md](README.md).
@@ -364,7 +1005,7 @@ require 'git'
364
1005
  Git.configure { |c| c.binary_path = '/Users/james/Downloads/git-2.30.2/bin-wrappers/git' }
365
1006
 
366
1007
  # Validate the version (if desired)
367
- assert_equal([2, 30, 2], Git.binary_version)
1008
+ assert_equal(Git::Version.new(2, 30, 2), Git.git_version)
368
1009
  ```
369
1010
 
370
1011
  Tests can be run using the newly built Git version as follows: