git 1.19.1 → 5.5.0

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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -1
  3. data/AI_POLICY.md +24 -0
  4. data/CHANGELOG.md +689 -0
  5. data/CODE_OF_CONDUCT.md +25 -0
  6. data/CONTRIBUTING.md +1175 -97
  7. data/GOVERNANCE.md +106 -0
  8. data/LICENSE +1 -1
  9. data/MAINTAINERS.md +17 -4
  10. data/README.md +476 -320
  11. data/UPGRADING.md +1138 -0
  12. data/git.gemspec +124 -36
  13. data/lib/git/author.rb +39 -7
  14. data/lib/git/author_info.rb +66 -0
  15. data/lib/git/branch.rb +615 -65
  16. data/lib/git/branch_delete_failure.rb +34 -0
  17. data/lib/git/branch_delete_result.rb +66 -0
  18. data/lib/git/branch_info.rb +237 -0
  19. data/lib/git/branches.rb +167 -44
  20. data/lib/git/command_line/base.rb +247 -0
  21. data/lib/git/command_line/capturing.rb +308 -0
  22. data/lib/git/command_line/result.rb +88 -0
  23. data/lib/git/command_line/streaming.rb +236 -0
  24. data/lib/git/command_line.rb +52 -0
  25. data/lib/git/commands/add.rb +139 -0
  26. data/lib/git/commands/am/abort.rb +43 -0
  27. data/lib/git/commands/am/apply.rb +263 -0
  28. data/lib/git/commands/am/continue.rb +43 -0
  29. data/lib/git/commands/am/quit.rb +43 -0
  30. data/lib/git/commands/am/retry.rb +49 -0
  31. data/lib/git/commands/am/show_current_patch.rb +64 -0
  32. data/lib/git/commands/am/skip.rb +42 -0
  33. data/lib/git/commands/am.rb +33 -0
  34. data/lib/git/commands/apply.rb +242 -0
  35. data/lib/git/commands/archive/list_formats.rb +46 -0
  36. data/lib/git/commands/archive.rb +145 -0
  37. data/lib/git/commands/arguments.rb +4521 -0
  38. data/lib/git/commands/base.rb +502 -0
  39. data/lib/git/commands/branch/copy.rb +102 -0
  40. data/lib/git/commands/branch/create.rb +177 -0
  41. data/lib/git/commands/branch/delete.rb +88 -0
  42. data/lib/git/commands/branch/list.rb +178 -0
  43. data/lib/git/commands/branch/move.rb +102 -0
  44. data/lib/git/commands/branch/set_upstream.rb +86 -0
  45. data/lib/git/commands/branch/show_current.rb +49 -0
  46. data/lib/git/commands/branch/unset_upstream.rb +53 -0
  47. data/lib/git/commands/branch.rb +34 -0
  48. data/lib/git/commands/cat_file/batch.rb +385 -0
  49. data/lib/git/commands/cat_file/filtered.rb +105 -0
  50. data/lib/git/commands/cat_file/raw.rb +271 -0
  51. data/lib/git/commands/cat_file.rb +49 -0
  52. data/lib/git/commands/checkout/branch.rb +153 -0
  53. data/lib/git/commands/checkout/files.rb +116 -0
  54. data/lib/git/commands/checkout.rb +38 -0
  55. data/lib/git/commands/checkout_index.rb +106 -0
  56. data/lib/git/commands/clean.rb +102 -0
  57. data/lib/git/commands/clone.rb +241 -0
  58. data/lib/git/commands/commit.rb +273 -0
  59. data/lib/git/commands/commit_tree.rb +101 -0
  60. data/lib/git/commands/config_option_syntax/add.rb +86 -0
  61. data/lib/git/commands/config_option_syntax/get.rb +121 -0
  62. data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
  63. data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
  64. data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
  65. data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
  66. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
  67. data/lib/git/commands/config_option_syntax/list.rb +111 -0
  68. data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
  69. data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
  70. data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
  71. data/lib/git/commands/config_option_syntax/set.rb +119 -0
  72. data/lib/git/commands/config_option_syntax/unset.rb +92 -0
  73. data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
  74. data/lib/git/commands/config_option_syntax.rb +56 -0
  75. data/lib/git/commands/describe.rb +156 -0
  76. data/lib/git/commands/diff.rb +657 -0
  77. data/lib/git/commands/diff_files.rb +519 -0
  78. data/lib/git/commands/diff_index.rb +499 -0
  79. data/lib/git/commands/fetch.rb +354 -0
  80. data/lib/git/commands/fsck.rb +138 -0
  81. data/lib/git/commands/gc.rb +134 -0
  82. data/lib/git/commands/grep.rb +339 -0
  83. data/lib/git/commands/init.rb +101 -0
  84. data/lib/git/commands/log.rb +634 -0
  85. data/lib/git/commands/ls_files.rb +195 -0
  86. data/lib/git/commands/ls_remote.rb +161 -0
  87. data/lib/git/commands/ls_tree.rb +135 -0
  88. data/lib/git/commands/maintenance/register.rb +77 -0
  89. data/lib/git/commands/maintenance/run.rb +109 -0
  90. data/lib/git/commands/maintenance/start.rb +71 -0
  91. data/lib/git/commands/maintenance/stop.rb +60 -0
  92. data/lib/git/commands/maintenance/unregister.rb +84 -0
  93. data/lib/git/commands/maintenance.rb +31 -0
  94. data/lib/git/commands/merge/abort.rb +44 -0
  95. data/lib/git/commands/merge/continue.rb +44 -0
  96. data/lib/git/commands/merge/quit.rb +46 -0
  97. data/lib/git/commands/merge/start.rb +250 -0
  98. data/lib/git/commands/merge.rb +28 -0
  99. data/lib/git/commands/merge_base.rb +91 -0
  100. data/lib/git/commands/mv.rb +82 -0
  101. data/lib/git/commands/name_rev.rb +119 -0
  102. data/lib/git/commands/pull.rb +382 -0
  103. data/lib/git/commands/push.rb +251 -0
  104. data/lib/git/commands/read_tree.rb +154 -0
  105. data/lib/git/commands/remote/add.rb +96 -0
  106. data/lib/git/commands/remote/get_url.rb +68 -0
  107. data/lib/git/commands/remote/list.rb +56 -0
  108. data/lib/git/commands/remote/prune.rb +63 -0
  109. data/lib/git/commands/remote/remove.rb +52 -0
  110. data/lib/git/commands/remote/rename.rb +76 -0
  111. data/lib/git/commands/remote/set_branches.rb +70 -0
  112. data/lib/git/commands/remote/set_head.rb +89 -0
  113. data/lib/git/commands/remote/set_url.rb +78 -0
  114. data/lib/git/commands/remote/set_url_add.rb +70 -0
  115. data/lib/git/commands/remote/set_url_delete.rb +71 -0
  116. data/lib/git/commands/remote/show.rb +77 -0
  117. data/lib/git/commands/remote/update.rb +79 -0
  118. data/lib/git/commands/remote.rb +42 -0
  119. data/lib/git/commands/repack.rb +281 -0
  120. data/lib/git/commands/reset.rb +154 -0
  121. data/lib/git/commands/rev_parse.rb +304 -0
  122. data/lib/git/commands/revert/abort.rb +45 -0
  123. data/lib/git/commands/revert/continue.rb +62 -0
  124. data/lib/git/commands/revert/quit.rb +47 -0
  125. data/lib/git/commands/revert/skip.rb +44 -0
  126. data/lib/git/commands/revert/start.rb +158 -0
  127. data/lib/git/commands/revert.rb +29 -0
  128. data/lib/git/commands/rm.rb +113 -0
  129. data/lib/git/commands/show.rb +632 -0
  130. data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
  131. data/lib/git/commands/show_ref/exists.rb +80 -0
  132. data/lib/git/commands/show_ref/list.rb +149 -0
  133. data/lib/git/commands/show_ref/verify.rb +122 -0
  134. data/lib/git/commands/show_ref.rb +42 -0
  135. data/lib/git/commands/stash/apply.rb +81 -0
  136. data/lib/git/commands/stash/branch.rb +67 -0
  137. data/lib/git/commands/stash/clear.rb +43 -0
  138. data/lib/git/commands/stash/create.rb +60 -0
  139. data/lib/git/commands/stash/drop.rb +73 -0
  140. data/lib/git/commands/stash/list.rb +43 -0
  141. data/lib/git/commands/stash/pop.rb +87 -0
  142. data/lib/git/commands/stash/push.rb +112 -0
  143. data/lib/git/commands/stash/show.rb +158 -0
  144. data/lib/git/commands/stash/store.rb +72 -0
  145. data/lib/git/commands/stash.rb +38 -0
  146. data/lib/git/commands/status.rb +174 -0
  147. data/lib/git/commands/symbolic_ref/delete.rb +72 -0
  148. data/lib/git/commands/symbolic_ref/read.rb +99 -0
  149. data/lib/git/commands/symbolic_ref/update.rb +79 -0
  150. data/lib/git/commands/symbolic_ref.rb +38 -0
  151. data/lib/git/commands/tag/create.rb +142 -0
  152. data/lib/git/commands/tag/delete.rb +57 -0
  153. data/lib/git/commands/tag/list.rb +146 -0
  154. data/lib/git/commands/tag/verify.rb +71 -0
  155. data/lib/git/commands/tag.rb +26 -0
  156. data/lib/git/commands/update_ref/batch.rb +145 -0
  157. data/lib/git/commands/update_ref/delete.rb +90 -0
  158. data/lib/git/commands/update_ref/update.rb +103 -0
  159. data/lib/git/commands/update_ref.rb +42 -0
  160. data/lib/git/commands/version.rb +60 -0
  161. data/lib/git/commands/worktree/add.rb +139 -0
  162. data/lib/git/commands/worktree/list.rb +64 -0
  163. data/lib/git/commands/worktree/lock.rb +58 -0
  164. data/lib/git/commands/worktree/management_base.rb +51 -0
  165. data/lib/git/commands/worktree/move.rb +66 -0
  166. data/lib/git/commands/worktree/prune.rb +67 -0
  167. data/lib/git/commands/worktree/remove.rb +63 -0
  168. data/lib/git/commands/worktree/repair.rb +76 -0
  169. data/lib/git/commands/worktree/unlock.rb +47 -0
  170. data/lib/git/commands/worktree.rb +43 -0
  171. data/lib/git/commands/write_tree.rb +68 -0
  172. data/lib/git/commands.rb +88 -0
  173. data/lib/git/config.rb +72 -5
  174. data/lib/git/config_entry_info.rb +106 -0
  175. data/lib/git/configuring.rb +795 -0
  176. data/lib/git/detached_head_info.rb +57 -0
  177. data/lib/git/diff.rb +437 -86
  178. data/lib/git/diff_file_numstat_info.rb +31 -0
  179. data/lib/git/diff_file_patch_info.rb +136 -0
  180. data/lib/git/diff_file_raw_info.rb +129 -0
  181. data/lib/git/diff_info.rb +162 -0
  182. data/lib/git/diff_path_status.rb +107 -0
  183. data/lib/git/diff_result.rb +34 -0
  184. data/lib/git/diff_stats.rb +111 -0
  185. data/lib/git/dirstat_info.rb +102 -0
  186. data/lib/git/encoding_utils.rb +32 -1
  187. data/lib/git/errors.rb +285 -0
  188. data/lib/git/escaped_path.rb +57 -5
  189. data/lib/git/execution_context/global.rb +31 -0
  190. data/lib/git/execution_context/repository.rb +151 -0
  191. data/lib/git/execution_context.rb +559 -0
  192. data/lib/git/factories.rb +813 -0
  193. data/lib/git/file_ref.rb +77 -0
  194. data/lib/git/fsck_object.rb +56 -0
  195. data/lib/git/fsck_result.rb +132 -0
  196. data/lib/git/log.rb +306 -90
  197. data/lib/git/object.rb +563 -141
  198. data/lib/git/parsers/branch.rb +240 -0
  199. data/lib/git/parsers/cat_file.rb +111 -0
  200. data/lib/git/parsers/config_entry.rb +110 -0
  201. data/lib/git/parsers/diff.rb +792 -0
  202. data/lib/git/parsers/fsck.rb +144 -0
  203. data/lib/git/parsers/grep.rb +42 -0
  204. data/lib/git/parsers/ls_remote.rb +79 -0
  205. data/lib/git/parsers/ls_tree.rb +58 -0
  206. data/lib/git/parsers/remote.rb +162 -0
  207. data/lib/git/parsers/stash.rb +292 -0
  208. data/lib/git/parsers/status.rb +251 -0
  209. data/lib/git/parsers/tag.rb +341 -0
  210. data/lib/git/parsers/worktree.rb +185 -0
  211. data/lib/git/path_resolver.rb +206 -0
  212. data/lib/git/remote.rb +165 -12
  213. data/lib/git/remote_info.rb +203 -0
  214. data/lib/git/repository/branching.rb +964 -0
  215. data/lib/git/repository/committing.rb +246 -0
  216. data/lib/git/repository/context_helpers.rb +293 -0
  217. data/lib/git/repository/diffing.rb +785 -0
  218. data/lib/git/repository/inspecting.rb +252 -0
  219. data/lib/git/repository/logging.rb +410 -0
  220. data/lib/git/repository/maintenance.rb +65 -0
  221. data/lib/git/repository/merging.rb +451 -0
  222. data/lib/git/repository/object_operations.rb +1551 -0
  223. data/lib/git/repository/remote_operations.rb +984 -0
  224. data/lib/git/repository/shared_private.rb +120 -0
  225. data/lib/git/repository/staging.rb +587 -0
  226. data/lib/git/repository/stashing.rb +623 -0
  227. data/lib/git/repository/status_operations.rb +249 -0
  228. data/lib/git/repository/worktree_operations.rb +339 -0
  229. data/lib/git/repository.rb +484 -2
  230. data/lib/git/stash.rb +109 -12
  231. data/lib/git/stash_info.rb +102 -0
  232. data/lib/git/stashes.rb +169 -26
  233. data/lib/git/status.rb +308 -122
  234. data/lib/git/status_file_info.rb +258 -0
  235. data/lib/git/status_info.rb +189 -0
  236. data/lib/git/tag_delete_failure.rb +34 -0
  237. data/lib/git/tag_delete_result.rb +66 -0
  238. data/lib/git/tag_info.rb +99 -0
  239. data/lib/git/url.rb +15 -8
  240. data/lib/git/version.rb +113 -2
  241. data/lib/git/version_constraint.rb +85 -0
  242. data/lib/git/worktree.rb +150 -8
  243. data/lib/git/worktree_info.rb +128 -0
  244. data/lib/git/worktrees.rb +118 -13
  245. data/lib/git.rb +632 -234
  246. metadata +369 -54
  247. data/.github/stale.yml +0 -25
  248. data/.github/workflows/continuous_integration.yml +0 -49
  249. data/.gitignore +0 -10
  250. data/Dockerfile.changelog-rs +0 -12
  251. data/Gemfile +0 -5
  252. data/ISSUE_TEMPLATE.md +0 -15
  253. data/PULL_REQUEST_TEMPLATE.md +0 -9
  254. data/RELEASING.md +0 -70
  255. data/Rakefile +0 -60
  256. data/lib/git/base/factory.rb +0 -99
  257. data/lib/git/base.rb +0 -711
  258. data/lib/git/command_line_result.rb +0 -86
  259. data/lib/git/failed_error.rb +0 -53
  260. data/lib/git/git_execute_error.rb +0 -7
  261. data/lib/git/index.rb +0 -5
  262. data/lib/git/lib.rb +0 -1328
  263. data/lib/git/path.rb +0 -31
  264. data/lib/git/signaled_error.rb +0 -50
  265. data/lib/git/working_directory.rb +0 -4
data/CONTRIBUTING.md CHANGED
@@ -3,161 +3,1239 @@
3
3
  # @title How To Contribute
4
4
  -->
5
5
 
6
- # Contributing to ruby-git
6
+ # Contributing to the git gem
7
7
 
8
- Thank you for your interest in contributing to the ruby-git project.
8
+ - [Summary](#summary)
9
+ - [How to contribute](#how-to-contribute)
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
+ - [A note for Windows contributors](#a-note-for-windows-contributors)
14
+ - [Bootstrap the project](#bootstrap-the-project)
15
+ - [Verify the toolchain](#verify-the-toolchain)
16
+ - [Contributor validation policy](#contributor-validation-policy)
17
+ - [How to submit a code or documentation change](#how-to-submit-a-code-or-documentation-change)
18
+ - [Commit your changes to a fork of `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git)
19
+ - [Create a pull request](#create-a-pull-request)
20
+ - [Get your pull request reviewed](#get-your-pull-request-reviewed)
21
+ - [Before requesting review](#before-requesting-review)
22
+ - [Branch strategy](#branch-strategy)
23
+ - [AI-assisted contributions](#ai-assisted-contributions)
24
+ - [Agent configuration](#agent-configuration)
25
+ - [Agent skills](#agent-skills)
26
+ - [Design philosophy](#design-philosophy)
27
+ - [Layered architecture](#layered-architecture)
28
+ - [Implementing a git command](#implementing-a-git-command)
29
+ - [API design](#api-design)
30
+ - [Method placement](#method-placement)
31
+ - [Method naming](#method-naming)
32
+ - [Result class naming](#result-class-naming)
33
+ - [Parameter naming](#parameter-naming)
34
+ - [Parameter values](#parameter-values)
35
+ - [Output processing](#output-processing)
36
+ - [Implementation](#implementation)
37
+ - [Example implementations](#example-implementations)
38
+ - [Coding standards](#coding-standards)
39
+ - [Commit message guidelines](#commit-message-guidelines)
40
+ - [What does this mean for contributors?](#what-does-this-mean-for-contributors)
41
+ - [What to know about Conventional Commits](#what-to-know-about-conventional-commits)
42
+ - [Issue and PR references](#issue-and-pr-references)
43
+ - [Testing guidelines](#testing-guidelines)
44
+ - [Test coverage policy](#test-coverage-policy)
45
+ - [Unit tests vs integration tests](#unit-tests-vs-integration-tests)
46
+ - [What ships in the gem](#what-ships-in-the-gem)
47
+ - [Building a specific version of the Git command-line](#building-a-specific-version-of-the-git-command-line)
48
+ - [Install prerequisites](#install-prerequisites)
49
+ - [Obtain Git source code](#obtain-git-source-code)
50
+ - [Build git](#build-git)
51
+ - [Use the new Git version](#use-the-new-git-version)
9
52
 
10
- This document gives the guidelines for contributing to the ruby-git project.
11
- These guidelines may not fit every situation. When contributing use your best
12
- judgement.
53
+ ## Summary
13
54
 
14
- Propose changes to these guidelines with a pull request.
55
+ Thank you for your interest in contributing to the `ruby-git` project.
56
+
57
+ This document provides guidelines for contributing to the `ruby-git` project. They
58
+ may not cover every situation, so use your best judgment.
59
+
60
+ If you have suggestions for improving these guidelines, please propose changes via a
61
+ pull request.
62
+
63
+ Please also review and adhere to our [Code of Conduct](CODE_OF_CONDUCT.md) when
64
+ participating in the project. Governance and maintainer expectations are described in
65
+ [GOVERNANCE.md](GOVERNANCE.md).
15
66
 
16
67
  ## How to contribute
17
68
 
18
- You can contribute in two ways:
69
+ You can contribute in the following ways:
19
70
 
20
- 1. [Report an issue or make a feature request](#how-to-report-an-issue-or-make-a-feature-request)
21
- 2. [Submit a code or documentation change](#how-to-submit-a-code-or-documentation-change)
71
+ 1. [Report an issue or request a
72
+ feature](#how-to-report-an-issue-or-request-a-feature)
73
+ 2. [Submit a code or documentation
74
+ change](#how-to-submit-a-code-or-documentation-change)
22
75
 
23
- ## How to report an issue or make a feature request
76
+ ## How to report an issue or request a feature
24
77
 
25
- ruby-git utilizes [GitHub Issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues)
78
+ `ruby-git` uses [GitHub
79
+ Issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues)
26
80
  for issue tracking and feature requests.
27
81
 
28
- Report an issue or feature request by [creating a ruby-git Github issue](https://github.com/ruby-git/ruby-git/issues/new).
29
- Fill in the template to describe the issue or feature request the best you can.
82
+ To report an issue or request a feature, please [create a `ruby-git` GitHub
83
+ issue](https://github.com/ruby-git/ruby-git/issues/new). Fill in the template as
84
+ thoroughly as possible to describe the issue or feature request.
85
+
86
+ ## Local development setup
87
+
88
+ Before submitting a change, set up a working local development environment.
89
+ `bin/setup` automates the bootstrap and fails fast with a clear message when a
90
+ prerequisite is missing.
91
+
92
+ ### Prerequisites
93
+
94
+ | Tool | Required version | Notes |
95
+ | --- | --- | --- |
96
+ | 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. |
97
+ | Bundler | Any 2.x or 4.x | Install with `gem install bundler`. |
98
+ | git | `>= 2.28.0` (matches `git.gemspec` `requirements`) | Older git versions are not supported and the test suite will not pass against them. |
99
+ | 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. |
100
+ | [lychee](https://lychee.cli.rs) | `>= 0.24.0` | Runs the markdown link check (`rake markdown:links`), which is part of the default task. The floor comes from [`.lychee.toml`](.lychee.toml): older releases cannot parse the enum form of `include_fragments`. Install with `brew install lychee` (macOS), `snap install lychee` (Ubuntu), `pacman -S lychee` (Arch), `winget install --id lycheeverse.lychee` (Windows), or see the [install docs](https://github.com/lycheeverse/lychee#installation). |
101
+
102
+ #### A note for Windows contributors
103
+
104
+ A few unit specs create real symlinks, which on Windows requires
105
+ `SeCreateSymbolicLinkPrivilege`. A non-elevated process only holds that privilege
106
+ when Developer Mode is enabled (Settings → System → For developers). Without it
107
+ those specs skip rather than fail, so `bundle exec rake` still passes, but the
108
+ behavior they cover goes unverified locally.
109
+
110
+ The same privilege decides whether Git for Windows materializes the committed
111
+ `.claude/skills` symlink, so enabling Developer Mode fixes both at once. See
112
+ [Agent configuration](#agent-configuration).
113
+
114
+ ### Bootstrap the project
115
+
116
+ From the project root, run:
117
+
118
+ ```shell
119
+ bin/setup
120
+ ```
121
+
122
+ `bin/setup` will:
123
+
124
+ 1. Verify the prerequisites above and exit with a non-zero status if any are
125
+ missing, or if Ruby, git, or lychee is out of date. (Bundler is only
126
+ checked for presence, not version.)
127
+ 2. Run `bundle install` to install Ruby gem dependencies.
128
+ 3. Run `npm install` (when npm is available) to install the Conventional Commit
129
+ `commit-msg` hook used by this project (Husky + commitlint). A separate
130
+ `pre-commit` hook is also installed that blocks direct commits to the
131
+ protected branches (`main`, `5.x`, `4.x`).
132
+ 4. Verify the toolchain by running `bundle exec rake --tasks`.
133
+
134
+ `bin/setup` checks for [lychee](https://lychee.cli.rs) alongside Ruby, git, and
135
+ Bundler, and exits non-zero when it is missing or too old. lychee is a Rust binary
136
+ rather than a gem, so `bundle install` cannot supply it and `bin/setup` cannot
137
+ install it for you. Every platform this project supports has a packaged build,
138
+ and the error message names the command for yours.
139
+
140
+ ### Verify the toolchain
141
+
142
+ Once `bin/setup` succeeds, confirm the full test and lint suite passes locally:
143
+
144
+ ```shell
145
+ bundle exec rake
146
+ ```
147
+
148
+ This runs everything CI checks: specs, RuboCop, the markdown link check, YARD,
149
+ and the gem build. It is the canonical way to validate a change before
150
+ requesting review.
151
+
152
+ One caveat on the `links` task: passing locally does not guarantee the CI job
153
+ passes, and the gap is the environment rather than the tool. A link whose
154
+ capitalization is wrong resolves on a case-insensitive filesystem such as macOS and
155
+ 404s on the Linux runner, so `](docs/README.MD)` against a file named `README.md`
156
+ looks fine locally and fails in CI. [`tasks/markdown.rake`](tasks/markdown.rake) lists
157
+ this and the other differences. CI remains the authoritative link check.
158
+
159
+ ### Contributor validation policy
160
+
161
+ Contributors are expected to run `bundle exec rake` locally and confirm it
162
+ passes before requesting review on a pull request. Trivial documentation-only
163
+ fixes (e.g., typo corrections in markdown files) are excepted. "CI passed" is
164
+ not a substitute for local validation; it is a backstop. This applies equally to
165
+ human-authored and AI-assisted contributions. See
166
+ [AI-assisted contributions](#ai-assisted-contributions).
30
167
 
31
168
  ## How to submit a code or documentation change
32
169
 
33
- There is three step process for code or documentation changes:
170
+ Submitting a code or documentation change has three steps:
34
171
 
35
- 1. [Commit your changes to a fork of ruby-git](#commit-changes-to-a-fork-of-ruby-git)
172
+ 1. [Commit your changes to a fork of
173
+ `ruby-git`](#commit-your-changes-to-a-fork-of-ruby-git) using [Conventional
174
+ Commits](#commit-message-guidelines)
36
175
  2. [Create a pull request](#create-a-pull-request)
37
176
  3. [Get your pull request reviewed](#get-your-pull-request-reviewed)
38
177
 
39
- ### Commit changes to a fork of ruby-git
40
-
41
- Make your changes in a fork of the ruby-git repository.
178
+ ### Commit your changes to a fork of `ruby-git`
42
179
 
43
- Each commit must include a [DCO sign-off](#developer-certificate-of-origin-dco)
44
- by adding the line `Signed-off-by: Name <email>` to the end of the commit
45
- message.
180
+ Make your changes in a fork of the `ruby-git` repository.
46
181
 
47
182
  ### Create a pull request
48
183
 
49
- See [this article](https://help.github.com/articles/about-pull-requests/) if you
50
- are not familiar with GitHub Pull Requests.
184
+ If you are not familiar with GitHub Pull Requests, please refer to [this
185
+ article](https://help.github.com/articles/about-pull-requests/).
51
186
 
52
187
  Follow the instructions in the pull request template.
53
188
 
54
189
  ### Get your pull request reviewed
55
190
 
56
- Code review takes place in a GitHub pull request using the [the Github pull request review feature](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews).
191
+ Code review takes place in a GitHub pull request using the [GitHub pull request
192
+ review
193
+ feature](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews).
57
194
 
58
195
  Once your pull request is ready for review, request a review from at least one
59
- [maintainer](MAINTAINERS.md) and any number of other contributors.
196
+ [maintainer](MAINTAINERS.md) and any other contributors you deem necessary.
197
+
198
+ During the review process, you may need to make additional commits; squash them.
199
+ You will also need to rebase your branch onto the latest version of the target
200
+ branch (e.g., `main`, `5.x`, or `4.x`) before merging.
201
+
202
+ At least one approval from a project maintainer is required before your pull request
203
+ can be merged. The maintainer is responsible for ensuring that the pull request meets
204
+ [the project's coding standards](#coding-standards).
205
+
206
+ ### Before requesting review
207
+
208
+ Before moving a pull request out of draft or requesting a review, confirm:
209
+
210
+ - [ ] `bundle exec rake` passes locally on your branch (see
211
+ [Local development setup](#local-development-setup)).
212
+ - [ ] New or changed code has accompanying tests under `spec/`
213
+ (see [Unit tests vs integration tests](#unit-tests-vs-integration-tests)).
214
+ - [ ] Every commit message follows [Conventional Commits](#commit-message-guidelines).
215
+ - [ ] User-facing changes are documented in `README.md` and/or YARD as appropriate.
216
+
217
+ These checks mirror what reviewers and CI will look for; running them locally
218
+ first keeps the review cycle short.
219
+
220
+ ## Branch strategy
221
+
222
+ This project maintains `main` plus one maintenance branch for each supported previous
223
+ major series:
224
+
225
+ - **`main`**: All development. It releases the next version of the gem, including
226
+ the next major version. Its next release is v6.0.0; every further v5.x release is
227
+ cut from `5.x`.
228
+ - **`5.x`** and **`4.x`**: The maintenance branches for the v5.x and v4.x series. Each
229
+ receives bug fixes and security fixes, and backward-compatible features at the
230
+ maintainers' discretion.
231
+
232
+ The README's [Release support policy](README.md#release-support-policy) says how long
233
+ each major series is supported.
234
+
235
+ When submitting a pull request:
236
+
237
+ - **New features and breaking changes**: Target the `main` branch
238
+ - **Bug fixes**: Target `main`, and maintainers will backport to the maintenance
239
+ branches if applicable
240
+ - **Security fixes**: Target `main` and every affected maintenance branch, or only a
241
+ maintenance branch if the issue affects that series alone
242
+
243
+ Removing a deprecated API follows the
244
+ [deprecation policy](.github/skills/breaking-change-analysis/SKILL.md#step-4-deprecation-policy):
245
+
246
+ A removal PR merges to main only when its deprecation warning and `UPGRADING.md` entry
247
+ are contained in a previous normal release. Once any removal has merged to main, main
248
+ becomes the release line for the next major version. If another release of the
249
+ previous major is needed, it is cut from a branch created for that major (e.g. `4.x`
250
+ or `5.x`).
251
+
252
+ ## AI-assisted contributions
253
+
254
+ AI-assisted contributions are welcome. Please review and apply our [AI
255
+ Policy](AI_POLICY.md) before submitting changes. You are responsible for
256
+ understanding and verifying any AI-assisted work included in PRs and ensuring it
257
+ meets our standards for quality, security, and licensing.
258
+
259
+ The human submitter, not the AI agent, is responsible for ensuring that
260
+ `bundle exec rake` passes locally before requesting review. This is true even
261
+ when the change was authored end-to-end by an agent. "The agent ran the tests"
262
+ and "CI is green" are not substitutes for the submitter running
263
+ [the local validation step](#contributor-validation-policy) themselves; CI is a
264
+ backstop, not a primary validation surface.
265
+
266
+ ### Agent configuration
267
+
268
+ Agent configuration is shared: each piece of guidance is stored once and surfaced to
269
+ every supported agent.
270
+
271
+ | Content | Canonical location | Also read by |
272
+ | --- | --- | --- |
273
+ | Project instructions | [`.github/copilot-instructions.md`](.github/copilot-instructions.md) | Claude Code, via an import in [`CLAUDE.md`](CLAUDE.md) |
274
+ | Skills | [`.github/skills/`](.github/skills/) | Claude Code, via the `.claude/skills` symlink |
275
+ | Setup hook | [`.github/hooks/run-bin-setup-once.sh`](.github/hooks/run-bin-setup-once.sh) | Claude Code, via `.claude/settings.json` |
276
+
277
+ Always edit the canonical file. The Claude Code side is a pointer in every case, so
278
+ changes reach both agents without a sync step.
279
+
280
+ One caveat: `.claude/skills` is a committed symlink. Git for Windows only
281
+ materializes symlinks when `core.symlinks` is enabled (which requires Developer Mode
282
+ or an elevated shell). Without it, Windows contributors get a plain text file there
283
+ and Claude Code silently loads no skills; either enable symlinks or point your agent
284
+ at [`.github/skills/`](.github/skills/) directly. Copilot is unaffected.
285
+
286
+ The symlink stays out of the published gem, so it never reaches users. See
287
+ [What ships in the gem](#what-ships-in-the-gem).
288
+
289
+ ### Agent skills
290
+
291
+ If you use an AI coding agent that understands repository skills, the
292
+ [`.github/skills/`](.github/skills/) directory contains optional, project-specific
293
+ guidance that mirrors maintainer expectations:
294
+
295
+ - [`project-context`](.github/skills/project-context/SKILL.md): architecture, coding
296
+ standards, design philosophy, and compatibility requirements
297
+ - [`development-workflow`](.github/skills/development-workflow/SKILL.md): TDD workflow
298
+ for bug fixes, features, refactoring, and maintenance tasks
299
+ - [`command-implementation`](.github/skills/command-implementation/SKILL.md) and
300
+ [`facade-implementation`](.github/skills/facade-implementation/SKILL.md): guidance for
301
+ adding or updating command classes and `Git::Repository` facade methods
302
+ - [`review-arguments-dsl`](.github/skills/review-arguments-dsl/SKILL.md): audits
303
+ `arguments do ... end` blocks against the git CLI
304
+ - [`rspec-unit-testing-standards`](.github/skills/rspec-unit-testing-standards/SKILL.md),
305
+ [`command-test-conventions`](.github/skills/command-test-conventions/SKILL.md), and
306
+ [`facade-test-conventions`](.github/skills/facade-test-conventions/SKILL.md): testing
307
+ conventions for new and updated code
308
+ - [`yard-documentation`](.github/skills/yard-documentation/SKILL.md),
309
+ [`command-yard-documentation`](.github/skills/command-yard-documentation/SKILL.md), and
310
+ [`facade-yard-documentation`](.github/skills/facade-yard-documentation/SKILL.md):
311
+ documentation standards
312
+ - [`test-debugging`](.github/skills/test-debugging/SKILL.md) and
313
+ [`ci-cd-troubleshooting`](.github/skills/ci-cd-troubleshooting/SKILL.md): help for
314
+ failing or flaky tests and CI failures
315
+ - [`breaking-change-analysis`](.github/skills/breaking-change-analysis/SKILL.md): impact
316
+ analysis before removing methods, changing interfaces, or planning deprecations
317
+ - [`pr-readiness-review`](.github/skills/pr-readiness-review/SKILL.md): final checks
318
+ before requesting review
319
+
320
+ ## Design philosophy
321
+
322
+ The `git` gem lets users apply what they already know about Git while working in
323
+ idiomatic Ruby.
324
+
325
+ Its public API is a lightweight wrapper around the `git` command-line tool that
326
+ gives Ruby developers a direct way to run Git programmatically.
327
+
328
+ This gem follows the principle of least surprise: it does not add unnecessary
329
+ abstraction layers or modify Git's core functionality. It stays close to the existing `git`
330
+ command-line interface and avoids extensions or alterations that could lead to
331
+ unexpected behavior.
332
+
333
+ `git` commands generally translate to `Git::Repository` methods of the same name.
334
+ Positional arguments map to the `git` CLI operands (such as paths and SHAs) in the
335
+ same order. Keyword arguments map to `git` CLI options by long OR short name.
336
+
337
+ Some examples:
338
+
339
+ - To execute `git clone <url> --depth=1`, call `Git.clone(url, depth: 1)`
340
+ - To execute `git add <path> --force`, call `Git::Repository#add(path, force: true)`
341
+
342
+ ## Layered architecture
343
+
344
+ The `git` gem is organized into three architectural layers:
345
+
346
+ | Layer | Responsibility | Mechanism |
347
+ | --- | --- | --- |
348
+ | **Facade** (`Git::Repository` and `Git`) | Public API | Normalizes Ruby arguments, sets safe defaults, calls one or more `Git::Commands::*` classes, and may parse output into public Ruby objects |
349
+ | **Command** (`Git::Commands::*`) | Neutral git CLI interface | Declares CLI arguments via the [Arguments DSL](lib/git/commands/arguments.rb), builds the git argv and executes git via `#call`, and returns `Git::CommandLine::Result` |
350
+ | **Execution** (`Git::ExecutionContext::*`) | Execution context and subprocess defaults | Carries execution settings such as working directory, environment, timeout, binary path, and logging; runs the git CLI with default global options (such as `-c color.ui=false`) and subprocess environment variables (such as a platform-conditional `LC_ALL`, which is `en_US.UTF-8` on macOS and `C.UTF-8` elsewhere) |
351
+
352
+ Command classes (`Git::Commands::*`) are **faithful, neutral representations of the
353
+ git CLI**. Each command class does the following:
354
+
355
+ - Declares acceptable CLI arguments and options via the
356
+ [Arguments DSL](lib/git/commands/arguments.rb)
357
+ - Defines a `#call` method which:
358
+ - Maps its parameters to the git argv using the declared arguments
359
+ - Executes a git CLI command via `Git::ExecutionContext`
360
+ - Returns the unprocessed git CLI result as a `Git::CommandLine::Result` object
361
+
362
+ Command classes should not embed choices such as output format flags, editor
363
+ suppression, progress output, or verbose mode. These decisions belong to the facade
364
+ layer which sets them as needed. The facade layer may give callers the choice to
365
+ override those decisions when appropriate (e.g., running in a TTY-attached
366
+ environment where an editor is desired).
367
+
368
+ For example:
369
+
370
+ - **Anti-pattern:** declaring non-overridable and non-default options in the Arguments
371
+ DSL to control output such as `literal '--no-edit'`, `literal '--verbose'`, or
372
+ `literal '--no-progress'` inside a command class. This embeds policy in the wrong
373
+ layer.
374
+ - **Correct pattern:** declaring options which allow the user of the command (often a
375
+ facade method) to set desired values such as: `flag_option :edit, negatable: true`.
376
+ This allows the facade to either accept the default or to hard code `edit: false`
377
+ if it is needed.
378
+
379
+ This separation keeps command classes reusable across facade methods with different
380
+ policy needs. For example, a facade method that parses command output may pass
381
+ options such as `no_color: true`, `z: true`, or a fixed `format:` value so git emits
382
+ a stable, parseable output shape. Those parser-contract options belong at the facade
383
+ call site, not as hard-coded literals in the command class. Other facade methods can
384
+ reuse the same command class with different options.
385
+
386
+ ## Implementing a git command
387
+
388
+ Start with the official git documentation page for the command (e.g., `man git-add`
389
+ or the [git-scm.com](https://git-scm.com/docs) reference page). Its SYNOPSIS line
390
+ identifies the positional operands, and its OPTIONS section identifies the flags and
391
+ value options the Ruby method must expose.
392
+
393
+ Implementing the command has two major tasks: [API design](#api-design) and
394
+ [Implementation](#implementation).
395
+
396
+ ### API design
397
+
398
+ This section covers where git command methods belong, how to name them, and how to
399
+ handle parameters and output. These describe the public interface that gem
400
+ users will see.
401
+
402
+ #### Method placement
403
+
404
+ The public API is `Git::Repository` (and the `Git` module). These facade methods must
405
+ be exposed there, even when their implementation lives in private mixin modules or
406
+ `Git::Commands::*` classes.
407
+
408
+ **Repository factory commands** are exposed via `Git` as module methods and
409
+ are usually implemented in the `Git::Factories` mixin. These methods return a
410
+ `Git::Repository` object for subsequent operations:
411
+
412
+ ```ruby
413
+ repo = Git.clone('https://github.com/user/repo.git', 'local_path')
414
+ repo = Git.init('new_repo', initial_branch: 'main')
415
+ repo = Git.open('.')
416
+ ```
417
+
418
+ **Repository-scoped commands** require a repository context. These methods are
419
+ exposed via `Git::Repository` instance methods and are usually implemented in a
420
+ `Git::Repository::*` mixin.
421
+
422
+ ```ruby
423
+ repo.add('file.txt')
424
+ repo.commit('Add file')
425
+ repo.log
426
+ ```
427
+
428
+ **Global commands** do not require a repository context. Expose these
429
+ as methods on the `Git` module:
430
+
431
+ ```ruby
432
+ Git.config_get('user.name', global: true)
433
+ Git.config_set('user.email', 'user@example.com', global: true)
434
+ ```
435
+
436
+ Some commands, like `git config` commands, can be called either in a global or
437
+ repository scope. Here is how that was solved for the config commands:
438
+
439
+ - When called via the `Git` module, a scope parameter such as `global: true`,
440
+ `system: true`, or `file: <filename>` MUST be given. `local` and `worktree`
441
+ scopes are not allowed.
442
+
443
+ - When called via a `Git::Repository` instance, `local: true` and `worktree: true`
444
+ scope parameters may be given, with `local` being the default if no scope is given.
445
+ `global`, `system`, and `file` scopes are also allowed.
446
+
447
+ #### Method naming
448
+
449
+ Each method corresponds directly to a `git` command. For example, the `git add`
450
+ command is implemented as `Git::Repository#add`, and the `git ls-files` command is
451
+ implemented as `Git::Repository#ls_files`.
452
+
453
+ When a single Git command serves multiple distinct purposes, method names should use
454
+ the git command name as a prefix, followed by a descriptive suffix indicating the
455
+ specific function. The suffix should correspond to the git option that distinguishes
456
+ the behavior.
457
+
458
+ For example, `git config` supports `--get`, `--set`, `--list`, `--unset`, and other
459
+ options. These are implemented as separate methods:
460
+
461
+ ```ruby
462
+ repo.config_get('user.name') # git config --get user.name
463
+ repo.config_set('user.name', 'Scott') # git config user.name Scott
464
+ repo.config_list # git config --list
465
+ repo.config_unset('user.name') # git config --unset user.name
466
+ repo.config_get_all('remote.origin.url') # git config --get-all remote.origin.url
467
+ ```
468
+
469
+ Aliases may be added to provide friendlier method names where appropriate.
470
+
471
+ See also [Output processing](#output-processing) for when different output formats
472
+ require separate methods.
473
+
474
+ #### Result class naming
475
+
476
+ Parsed result objects returned from facade methods follow a reserved suffix
477
+ convention:
478
+
479
+ - **`*Info`**: a parsed metadata struct returned from a query (e.g., `BranchInfo`,
480
+ `TagInfo`, `StashInfo`, `DiffInfo`). Always lives in the top-level `Git::`
481
+ namespace.
482
+ - **`*Result`**: the outcome of a mutating or destructive operation (e.g.,
483
+ `BranchDeleteResult`, `TagDeleteResult`). Also lives in `Git::`.
484
+
485
+ Do not use these suffixes on `Git::Commands::*` command classes. Those are
486
+ subprocess runners, not data objects. A reader seeing `Commands::Foo::BarInfo`
487
+ expects a parsed struct, not a class that shells out to git.
488
+
489
+ #### Parameter naming
490
+
491
+ Parameters within the `git` gem methods are named after their corresponding long
492
+ command-line options, so developers already accustomed to Git will recognize them.
493
+
494
+ For example, `git config --global` becomes `global: true`, and `git config --file`
495
+ becomes `file: '/path/to/config'`.
496
+
497
+ As a lightweight wrapper, the gem passes options directly to the git command-line.
498
+ This means git itself will validate option combinations and report errors. This
499
+ approach is preferred as long as the error messages returned by git are actionable
500
+ and understandable for users of the gem.
501
+
502
+ #### Parameter values
503
+
504
+ This section defines how git command-line options and positional arguments map to
505
+ Ruby method parameters. Contributors must follow these conventions:
506
+
507
+ ##### Options
508
+
509
+ Git command-line options are passed as keyword arguments in the Ruby API. Methods
510
+ accept these via an options splat parameter (e.g., `def replace(object, replacement,
511
+ **options)`). Each option is mapped to a keyword argument as described below.
512
+
513
+ - **Boolean flags**: Git options like `--global` or `--bare` are mapped to `global:
514
+ true` or `bare: true`. Omit the key or use `false` to leave the flag unset.
515
+ - `git config --global` → `global: true`
516
+
517
+ - **Negated boolean flags**: Options like `--no-reflogs` are mapped to `no_reflogs:
518
+ true`.
519
+ - `git branch --no-reflogs` → `no_reflogs: true`
520
+
521
+ - **Value options**: Options that take a value, such as `--file <path>` or `--author
522
+ <name>`, are mapped as `file: '/path'`, `author: 'Name'`.
523
+ - `git config --file /tmp/config` → `file: '/tmp/config'`
524
+
525
+ - **Options with optional values**: If a git option can be used as a flag or with a
526
+ value (e.g., `--color` or `--color=always`), use `color: true` for the flag form,
527
+ or `color: 'always'` for the value form.
528
+ - `git log --color` → `color: true`
529
+ - `git log --color=always` → `color: 'always'`
530
+
531
+ - **List/array options**: Options that can be repeated or take multiple values (e.g.,
532
+ `--exclude <pattern>`, `--pathspec-from-file <file>`) are mapped to arrays:
533
+ `exclude: ['foo', 'bar']`.
534
+ - `git ls-files --exclude=foo --exclude=bar` → `exclude: ['foo', 'bar']`
535
+
536
+ - **Key-value pair options**: Options like `-c key=value` are mapped as `c: { 'key'
537
+ => 'value' }` or as an array of pairs if multiple are allowed.
538
+ - `git -c user.name=Scott` → `c: { 'user.name' => 'Scott' }`
539
+
540
+ ##### Positional arguments
541
+
542
+ Arguments that are not options (e.g., file names, branch names) are passed as method
543
+ arguments, not as keyword arguments.
544
+
545
+ - **Only single-valued positional arguments**: If a command has one or more
546
+ single-valued positional arguments (e.g., `<arg1>` or `<arg1> <arg2>`), pass each
547
+ as a separate method argument, in the order they appear in the official git
548
+ documentation and CLI usage. Optional arguments (indicated by `[<arg>]`) should
549
+ default to `nil`.
550
+ - `git cmd <object>` → `def cmd(object)` (fictitious command)
551
+ - `git replace <object> <replacement>` → `def replace(object, replacement)`
552
+ - `git clone <repository> [<directory>]` → `def clone(repository, directory = nil)`
553
+
554
+ - **Single multi-valued positional argument**: If a command has a single multi-valued
555
+ positional argument (e.g., `<pathspec>...` or `[<pathspec>...]`), use a splat
556
+ parameter to accept zero or more values (optional) or one or more values
557
+ (required).
558
+ - `git add [<pathspec>...]` → `def add(*paths)`
559
+
560
+ - **Mixed single-valued and multi-valued positional arguments, `--` separated
561
+ (independently reachable groups)**: When a git command separates two optional
562
+ groups with `--` (e.g., `[<tree-ish>] [-- <pathspec>...]`), callers may want
563
+ to supply the post-`--` group *without* supplying the first group. Use the
564
+ single-valued argument as a regular optional parameter and the multi-valued
565
+ group as a keyword argument with an empty array default. The keyword argument
566
+ should accept a single value or an array; wrap a single value in an array
567
+ internally.
568
+ - `git checkout [<branch>] [-- <pathspec>...]` → `def checkout(branch = nil,
569
+ pathspecs: [])`
570
+ - `git diff [<tree-ish>] [-- <pathspec>...]` → `def diff(tree_ish = nil,
571
+ pathspec: [])`
572
+ - Callers can then do `checkout(pathspecs: ['file.rb'])` (no branch) or
573
+ `diff('HEAD~3', pathspec: ['file.rb'])` (both), with no ambiguity.
574
+
575
+ - **Multiple optional single-valued positional arguments, pure nesting
576
+ (second only meaningful with first)**: When the git SYNOPSIS shows nested
577
+ optional brackets and the inner operand is only useful in the presence of the
578
+ outer one, both arguments may be regular optional parameters in left-to-right
579
+ order. A caller would never supply the second without the first.
580
+ - `git diff [<commit1> [<commit2>]]` → `def diff(commit1 = nil, commit2 = nil)`
581
+ - Callers can do `diff` (no args), `diff('HEAD~3')`, or `diff('HEAD~3', 'HEAD')`.
582
+ There is no case where someone would pass `commit2` without `commit1`.
583
+
584
+ ##### Cross-argument constraints
585
+
586
+ Constraints that span arguments — mutually exclusive options, required groups,
587
+ forbidden value combinations — are not validated in Ruby. Command classes pass the
588
+ arguments through and leave the judgment to git. When git rejects a combination,
589
+ the rejection surfaces as a `Git::FailedError` carrying git's own message; a
590
+ combination git accepts, even one it silently ignores, raises nothing. The decision and its rationale are
591
+ recorded in
592
+ [ADR-0003](docs/adr/0003-validation-of-git-semantics-is-delegated-to-git.md).
593
+
594
+ The arguments DSL does provide constraint declarations (`conflicts`, `requires`,
595
+ `requires_one_of`, `requires_exactly_one_of`, `forbid_values`, `allowed_values`),
596
+ which raise `ArgumentError` at bind time. Declare one only when git cannot report
597
+ the error itself, under the two exception criteria defined in
598
+ [Validation Boundaries](.github/skills/project-context/SKILL.md#validation-boundaries):
599
+
600
+ - **The argv-invisible exception**: the argument never appears in git's argv
601
+ (`skip_cli: true` operands, `execution_option` entries), so git has no token to
602
+ object to. The two current uses are `Git::Commands::CatFile::Batch` (`conflicts`
603
+ and `requires_one_of` on its stdin-fed `:object` operand) and
604
+ `Git::Commands::Archive` (`conflicts :output, :out`, where `:out` is a Ruby IO
605
+ object).
606
+ - **The silent-wrong-result exception**: git accepts the combination but silently
607
+ discards data or produces a wrong answer. A declaration under this exception
608
+ needs a code comment explaining why, the git version(s) where the behavior was
609
+ verified, and a test.
610
+
611
+ These conventions ensure the API is predictable and closely aligned with the git CLI.
612
+ If a new option type is encountered, extend this section to document the mapping.
60
613
 
61
- During the review process, you may need to make additional commits which would
62
- need to be squashed. It may also be necessary to rebase to master again if other
63
- changes are merged before your PR.
614
+ #### Output processing
64
615
 
65
- At least one approval is required from a project maintainer before your pull
66
- request can be merged. The maintainer is responsible for ensuring that the pull
67
- request meets [the project's coding standards](#coding-standards).
616
+ The `git` gem translates the output of many Git commands into Ruby objects that are
617
+ easier to work with programmatically.
618
+
619
+ These Ruby objects often include methods for further Git operations where useful,
620
+ while staying close to the underlying Git behavior.
621
+
622
+ When a single git command can produce distinctly different output types based on its
623
+ options, implement separate methods for each output type. Follow the same naming
624
+ convention used for commands with multiple purposes: use the git command name as a
625
+ prefix, followed by a suffix that describes the specific output type or
626
+ functionality.
627
+
628
+ For example, `git diff` can produce full diffs, statistical summaries, or path status
629
+ information depending on the options used. These are implemented as separate methods:
630
+
631
+ ```ruby
632
+ repo.diff_full('HEAD~1', 'HEAD') # Full diff output (git diff -p)
633
+ repo.diff_stats('HEAD~1', 'HEAD') # Statistical summary (git diff --numstat)
634
+ repo.diff_path_status('HEAD~1', 'HEAD') # File paths and status (git diff --name-status)
635
+ ```
636
+
637
+ This gives each method a clear, predictable return type and parsing logic targeted
638
+ to its output format.
639
+
640
+ ### Implementation
641
+
642
+ The gem uses the three-layer architecture described in
643
+ [Layered architecture](#layered-architecture). When wrapping a git command, keep the
644
+ layer responsibilities separate:
645
+
646
+ 1. **Design the public API** using the guidelines in this section (placement, naming,
647
+ parameters, output)
648
+
649
+ 2. **Create a command class** in `lib/git/commands/` that:
650
+ - Accepts a `Git::ExecutionContext` at initialization
651
+ - Defines arguments using the [Arguments DSL](lib/git/commands/arguments.rb)
652
+ - Returns a raw `Git::CommandLine::Result`
653
+
654
+ 3. **Add the facade method** to `Git::Repository` (or the `Git` module) that applies
655
+ facade policy, calls the command class, and parses the raw result when returning
656
+ structured Ruby objects.
657
+
658
+ Steps 2 and 3 correspond to the Command and Facade layers, respectively. The
659
+ Execution layer (`Git::ExecutionContext::*`) already exists. A command class only
660
+ consumes it via `@execution_context`; it is not authored per command.
661
+
662
+ Example structure for `git add`:
663
+
664
+ ```ruby
665
+ # lib/git/commands/add.rb (internal)
666
+ require 'git/commands/base'
667
+
668
+ module Git
669
+ module Commands
670
+ class Add < Git::Commands::Base
671
+ arguments do
672
+ literal 'add'
673
+ flag_option %i[verbose v]
674
+ flag_option %i[force f]
675
+ # ...additional flag and value options elided for brevity...
676
+ end_of_options
677
+ operand :pathspec, repeatable: true
678
+ end
679
+
680
+ # @overload call(*pathspec, **options)
681
+ #
682
+ # Execute the `git add` command
683
+ #
684
+ # @param pathspec [Array<String>] files to be added to the repository
685
+ # (relative to the worktree root)
686
+ #
687
+ # @param options [Hash] command options
688
+ #
689
+ # @option options [Boolean, nil] :verbose (nil) be verbose
690
+ #
691
+ # Alias: :v
692
+ #
693
+ # @option options [Boolean, nil] :force (nil) allow adding otherwise ignored
694
+ # files
695
+ #
696
+ # Alias: :f
697
+ #
698
+ # @return [Git::CommandLine::Result] the result of calling `git add`
699
+ #
700
+ # @raise [ArgumentError] if unsupported options are provided
701
+ #
702
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
703
+ #
704
+ # @api public
705
+ #
706
+ def call(*, **)
707
+ super
708
+ end
709
+ end
710
+ end
711
+ end
712
+ ```
713
+
714
+ Here is the corresponding facade method that calls it:
715
+
716
+ ```ruby
717
+ # lib/git/repository/staging.rb (facade, a topic module included into Git::Repository)
718
+ module Git
719
+ class Repository
720
+ module Staging
721
+ ADD_ALLOWED_OPTS = %i[all force].freeze
722
+ private_constant :ADD_ALLOWED_OPTS
723
+
724
+ def add(paths = '.', **)
725
+ SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
726
+ Git::Commands::Add.new(@execution_context).call(*Array(paths), **).stdout
727
+ end
728
+ end
729
+ end
730
+ end
731
+ ```
732
+
733
+ **How `Git::Commands::Base` works**: `Base` provides a default `#initialize` (accepts
734
+ an `execution_context`) and `#call(*, **)` (binds arguments via the DSL, dispatches to
735
+ `@execution_context.command_capturing` or `@execution_context.command_streaming`
736
+ depending on whether an `out:` execution option is present, and validates the exit
737
+ status). Simple commands need only declare `arguments do … end` and inherit
738
+ `Base#call(*, **)` unchanged. To attach command-specific YARD documentation to the
739
+ inherited `call`, use either a `# @!method call(*, **)` directive (when there is no
740
+ `def call` in the class) or place the YARD tags directly above an explicit
741
+ `def call(*, **); super; end`. Both patterns produce identical runtime behavior.
742
+ Only add real logic to `def call` when the command needs custom behavior beyond what
743
+ `Base` provides.
744
+
745
+ Override `call` explicitly in three situations:
746
+
747
+ 1. **Input validation**: guard `ArgumentError` for invalid option combinations that
748
+ the DSL cannot express (e.g., empty operands without a compensating flag).
749
+ 2. **Stdin via IO pipe**: commands using the `--batch` / `--batch-check` protocol
750
+ must feed object names to the subprocess's stdin. Use the inherited
751
+ `Base#with_stdin(content)`, which opens an `IO.pipe`, writes the string content,
752
+ and yields the read end as `in:`. Do not open a pipe manually. `StringIO` is
753
+ not accepted by `Process.spawn` because it has no file descriptor.
754
+ 3. **Non-trivial option routing**: when multiple call shapes require different
755
+ argument sets built separately before dispatching.
756
+
757
+ When overriding, work with `args_definition.bind(...)` directly and delegate
758
+ exit-status handling to the inherited `validate_exit_status!`. Extract bulk logic
759
+ into private helpers to satisfy RuboCop `Metrics` thresholds:
760
+
761
+ ```ruby
762
+ def call(*objects, **options)
763
+ raise ArgumentError, '...' if objects.empty? && !options[:batch_all_objects]
764
+
765
+ bound = args_definition.bind(*objects, **options)
766
+ with_stdin(objects.map { |o| "#{o}\n" }.join) { |reader| run_batch(bound, reader) }
767
+ end
768
+
769
+ private
770
+
771
+ def run_batch(bound, reader)
772
+ result = @execution_context.command_capturing(*bound, in: reader, **bound.execution_options, raise_on_failure: false)
773
+ validate_exit_status!(result)
774
+ result
775
+ end
776
+ ```
777
+
778
+ Option validation happens at two layers: the `Git::Repository` facade method calls
779
+ `SharedPrivate.assert_valid_opts!` against its own documented public option list first,
780
+ then the [`Arguments` DSL](lib/git/commands/arguments.rb) raises `ArgumentError` for
781
+ any keyword the command class does not recognize during argument binding. The facade
782
+ also handles translation from single values or arrays to the splat format.
783
+
784
+ > **YARD documentation note:** Because `call` uses anonymous argument forwarding
785
+ > (`*, **`), YARD cannot infer its signature. Document it with an `@overload` that names
786
+ > the operands and an `**options` hash (e.g., `@overload call(*pathspec, **options)`),
787
+ > add a `@param options [Hash]` tag, and document each supported keyword with its own
788
+ > `@option` tag. When the class defines no `def call`, place these tags under a
789
+ > `# @!method call(*, **)` directive (as shown above); when it defines an explicit
790
+ > `def call` override, place them directly above that method instead.
791
+ >
792
+ > **Testing requirement:** When defining arguments with the DSL, you must write RSpec
793
+ > tests that verify each argument handles valid values correctly (booleans, strings,
794
+ > arrays) and handles invalid values appropriately. Use a separate `context` block for
795
+ > testing each option to ensure clarity and isolation. See
796
+ > `spec/unit/git/commands/add_spec.rb` for examples of comprehensive argument testing.
797
+
798
+ For factory methods and module-level commands, the pattern is the same but
799
+ `Git::ExecutionContext::Global` is used instead of the repository's
800
+ `@execution_context`:
801
+
802
+ ```ruby
803
+ # Factory method (Git.clone): creates a global context, runs the command, returns a repository
804
+ module Git
805
+ def self.clone(repository_url, directory = nil, options = {})
806
+ context = Git::ExecutionContext::Global.new
807
+ Git::Commands::Clone.new(context).call(repository_url, directory, **options)
808
+ # ... then build and return a Git::Repository for the cloned working tree
809
+ end
810
+ end
811
+ ```
812
+
813
+ > **Note:** `Git::Repository` facade methods pass `@execution_context` (a
814
+ > `Git::ExecutionContext::Repository`) to each command class they invoke.
815
+ > Module-level methods such as `Git.clone` construct a
816
+ > `Git::ExecutionContext::Global` instead.
817
+
818
+ ### Example implementations
819
+
820
+ The following command classes demonstrate implementation patterns.
821
+ See `lib/git/commands/` and `spec/unit/git/commands/` for the full implementations:
822
+
823
+ - **Simple command**: `Git::Commands::Add`, straightforward argument building with
824
+ the [Arguments DSL](lib/git/commands/arguments.rb)
825
+ - **Command with parser-backed facade result**: `Git::Commands::Fsck` with
826
+ `Git::Parsers::Fsck`, which returns raw command output that the facade parses into
827
+ structured Ruby objects
828
+ - **Factory command**: `Git::Commands::Clone`, used by `Git.clone`; returns a
829
+ `Git::CommandLine::Result` like all command classes (the factory method then builds
830
+ and returns a `Git::Repository` from the cloned working tree)
831
+ - **Multiple output modes**: `Git::Commands::Diff`, which declares output-mode options that
832
+ facade methods choose from when building different Ruby-facing results
833
+ - **Multi-context command family**: `Git::Commands::ConfigOptionSyntax::*`, command
834
+ classes shared by module-level and repository-scoped config methods
68
835
 
69
836
  ## Coding standards
70
837
 
71
- In order to ensure high quality, all pull requests must meet these requirements:
838
+ All pull requests must meet the following requirements:
839
+
840
+ ### Commit message guidelines
841
+
842
+ The `ruby-git` project has adopted the [Conventional Commits
843
+ standard](https://www.conventionalcommits.org/en/v1.0.0/) for all commit messages.
844
+
845
+ Structured commit messages let tools determine the semantic version bump (patch,
846
+ minor, major) from the commits merged and generate an accurate `CHANGELOG.md`
847
+ automatically. A standardized format also makes the history easier to read at a
848
+ glance.
849
+
850
+ #### What does this mean for contributors?
851
+
852
+ All commits to this repository must follow the [Conventional Commits
853
+ standard](https://www.conventionalcommits.org/en/v1.0.0/). Commits that do not
854
+ follow it will fail the CI build, and PRs that include them will not be merged.
855
+
856
+ A git `commit-msg` hook (Husky + commitlint) that validates your Conventional
857
+ Commit messages locally is installed automatically as part of the project
858
+ bootstrap. See [Local development setup](#local-development-setup). The hook
859
+ depends on Node.js and npm; if those are not installed, `bin/setup` will warn
860
+ and skip the hook, and commit-message validation will only run in CI.
861
+
862
+ #### What to know about Conventional Commits
863
+
864
+ The simplest conventional commit is in the form `type: description` where `type`
865
+ indicates the type of change and `description` is your usual commit message (with
866
+ some limitations).
867
+
868
+ - Types include: `feat`, `fix`, `docs`, `test`, `refactor`, and `chore`. See the full
869
+ list of types supported in [.commitlintrc.yml](.commitlintrc.yml).
870
+ - The description must (1) not start with an upper case letter, (2) be no more than
871
+ 100 characters, and (3) not end with punctuation.
872
+
873
+ Examples of valid commits:
874
+
875
+ - `feat: add the --merges option to Git::Repository#log`
876
+ - `fix: exception thrown by Git::Repository#log when repo has no commits`
877
+ - `docs: add conventional commit announcement to README.md`
878
+
879
+ Commits that include breaking changes must include an exclamation mark before the
880
+ colon:
881
+
882
+ - `feat!: removed Git::Repository#commit_force`
883
+
884
+ The commit messages drive how the version is incremented for each release:
885
+
886
+ - a release containing a breaking change gets a major version increment
887
+ - a release containing a new feature gets a minor increment
888
+ - a release containing neither gets a patch increment
889
+
890
+ The full conventional commit format is:
891
+
892
+ ```text
893
+ <type>[optional scope][!]: <description>
894
+
895
+ [optional body]
896
+
897
+ [optional footer(s)]
898
+ ```
899
+
900
+ - `optional body` may include multiple lines of descriptive text limited to 100 chars
901
+ each
902
+ - `optional footers` only uses `BREAKING CHANGE: <description>` where description
903
+ should describe the nature of the backward incompatibility.
904
+
905
+ The `BREAKING CHANGE:` footer flags a backward incompatible change even if the
906
+ type is not marked with an exclamation mark. Other footers are allowed
907
+ but not acted upon.
908
+
909
+ See [the Conventional Commits
910
+ specification](https://www.conventionalcommits.org/en/v1.0.0/) for more details.
72
911
 
73
- ### 1 PR = 1 Commit
74
- * All commits for a PR must be squashed into one commit
75
- * To avoid an extra merge commit, the PR must be able to be merged as [a fast forward merge](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging)
76
- * The easiest way to ensure a fast forward merge is to rebase your local branch
77
- to the ruby-git master branch
912
+ #### Issue and PR references
78
913
 
79
- ### Unit tests
80
- * All changes must be accompanied by new or modified unit tests
81
- * The entire test suite must pass when `bundle exec rake default` is run from the
82
- project's local working copy.
914
+ Due to a parser limitation in commitlint, using `#<number>` anywhere in the commit
915
+ **body** causes everything from that line onward to be treated as a footer, which
916
+ triggers a `footer-leading-blank` error.
83
917
 
84
- While working on specific features you can run individual test files or
85
- a group of tests using `bin/test`:
918
+ To avoid this:
86
919
 
87
- # run a single file (from tests/units):
88
- $ bin/test test_object
920
+ - **In the body**, omit the `#` when mentioning an issue or PR: write `issue 1000`,
921
+ not `issue #1000`.
922
+ - **In the footer**, always include `#` for closing references:
923
+ `Closes #1000`, `Fixes #1000`, or `Resolves #1000`.
924
+ - If you only want to mention an issue for context (not close it), omit the `#` in
925
+ the body. No footer line is needed.
89
926
 
90
- # run multiple files:
91
- $ bin/test test_object test_archive
927
+ To validate a commit message before committing:
92
928
 
93
- # run all unit tests:
94
- $ bin/test
929
+ ```bash
930
+ npx commitlint --format @commitlint/format < commit_msg.txt
931
+ ```
932
+
933
+ To see how commitlint has parsed a commit message:
934
+
935
+ ```bash
936
+ cat commit_msg.txt | node -e "
937
+ const parse = require('@commitlint/parse');
938
+ let msg = '';
939
+ process.stdin.on('data', d => msg += d);
940
+ process.stdin.on('end', () =>
941
+ parse.default(msg.trim()).then(r => console.log(JSON.stringify(r, null, 2)))
942
+ );
943
+ " | jq
944
+ ```
945
+
946
+ ### Testing guidelines
947
+
948
+ - All changes must be accompanied by new or modified unit and integration tests as
949
+ appropriate.
950
+ - The entire test suite must pass when `bundle exec rake` is run from the project's
951
+ local working copy.
952
+ - Test runs are covered by SimpleCov by default. Set `COVERAGE=false` (or `0`/`no`/
953
+ `off`) to skip coverage, e.g. `COVERAGE=false bundle exec rake spec`.
954
+ `rake spec:integration` always disables coverage, regardless of `COVERAGE`:
955
+ integration tests aren't meant to be exhaustive, so tracking their coverage would
956
+ misleadingly suggest that low integration coverage is a problem to fix.
957
+ - `rake spec:integration` runs in parallel (via `parallel_tests`) on MRI. Set
958
+ `PARALLEL_TESTS=false` (or `0`/`no`/`off`) to force serial execution, e.g.
959
+ `PARALLEL_TESTS=false bundle exec rake spec:integration`. A run narrowed by `SPEC`
960
+ to a single spec file always runs serially. There is nothing to divide across
961
+ workers, and serial execution gives per-example (documentation) output.
962
+ - Set `SPEC=<glob>` to run specific files instead of a task's whole directory, e.g.
963
+ `SPEC=spec/unit/git/version_spec.rb bundle exec rake spec:unit`.
964
+ - Each task runs only the matches that live under its own directory, so one glob
965
+ spanning `spec/unit/` and `spec/integration/` can drive `bundle exec rake spec` and
966
+ exercise both layers of an area in a single command:
967
+
968
+ ```bash
969
+ # Unit and integration specs for the add command
970
+ SPEC=spec/**/git/commands/add_spec.rb bundle exec rake spec
971
+
972
+ # Unit and integration specs for every command class
973
+ SPEC=spec/**/git/commands bundle exec rake spec
974
+ ```
975
+
976
+ The glob is expanded by Rake, not the shell, so `**` works the same in any shell.
977
+ A task whose directory contains none of the matches is skipped with a message:
978
+ `SPEC=spec/unit/...` on `rake spec` runs the unit specs and skips
979
+ `spec:integration`. A glob matching nothing anywhere fails the task outright.
980
+
981
+ This project uses RSpec (`spec/`) as its sole test framework. Structure,
982
+ naming, setup, stubbing, and coverage rules for unit specs are defined in the
983
+ [`rspec-unit-testing-standards`](.github/skills/rspec-unit-testing-standards/SKILL.md)
984
+ skill. Follow it when writing or reviewing specs under `spec/unit/`.
985
+
986
+ #### Test coverage policy
987
+
988
+ **Every pull request to `main` or `5.x` must keep `bundle exec rake spec:unit` at
989
+ 100% line coverage and 100% branch coverage of `lib/`.** CI fails the build when it
990
+ drops below either threshold.
991
+
992
+ This is enforceable without being onerous because unit coverage in this project is
993
+ deterministic: `lib/` has no Ruby-version, Ruby-engine, or platform conditionals, and
994
+ the handful of unit specs that are conditionally skipped are redundant for coverage:
995
+ every `lib/` line and branch they reach is also reached by a spec that always runs.
996
+ Every supported MRI runtime therefore measures exactly the same lines and branches, so
997
+ a coverage failure is always something the pull request introduced.
998
+
999
+ A new conditional skip in `spec/unit/` must preserve that property. Verify it on a
1000
+ host where the guard actually skips: run the full unit suite there and confirm it
1001
+ still reports 100% line and branch coverage. A conditionally skipped unit spec that
1002
+ is the only thing covering a line would turn this gate into a platform-dependent
1003
+ failure, which is exactly what the policy exists to prevent.
1004
+
1005
+ Write the guard the same way the rest of the suite does: a reusable predicate in
1006
+ `spec/spec_helper.rb` (`unless_git`, `unless_command`, `unless_pcre`,
1007
+ `unless_ci_build`) used as `skip:` metadata, or, for a one-off capability that the
1008
+ `before` block is already exercising, a `rescue` in that block that calls `skip`.
1009
+
1010
+ What the policy does and does not cover:
1011
+
1012
+ - **Scope is the unit suite on MRI.** Integration specs are deliberately not
1013
+ exhaustive and are not measured (`rake spec:integration` always disables coverage).
1014
+ JRuby and TruffleRuby do not produce reliable coverage data and are not measured.
1015
+ - **Enforcement applies to full-suite runs.** A focused run
1016
+ (`SPEC=<glob> bundle exec rake spec:unit`, or `bundle exec rspec <file>`) still
1017
+ reports coverage but will not fail on it, so the usual edit-test loop is unaffected.
1018
+ Set `FAIL_ON_LOW_COVERAGE=true` to force enforcement on for a focused run.
1019
+ - **A focused run lists gaps only in the code it is about.** The reported percentage is
1020
+ always for the whole of `lib/`, but the list of uncovered lines and branches is scoped
1021
+ to the files the run tests: the classes it describes, plus the `lib/` file each spec
1022
+ file mirrors. So a focused run answers "is what I just changed fully covered?" without
1023
+ waiting for CI:
1024
+
1025
+ ```text
1026
+ Reporting uncovered lines and branches for 1 of 225 files.
1027
+
1028
+ No uncovered lines and branches in this file.
1029
+ ```
1030
+
1031
+ Ignore the percentage on a focused run; it is low because `spec_helper` loads all of
1032
+ `lib/`, not because anything is wrong. Set `LIST_UNCOVERED_FILES=all` to list gaps for
1033
+ every file instead.
1034
+ - **The thresholds do not move.** Do not lower `minimum_coverage` and do not add a
1035
+ SimpleCov filter to exclude a file. The only sanctioned escape hatch is a
1036
+ `# simplecov:disable` directive.
1037
+ - **Every file under `lib/` is measured.** SimpleCov only tracks files loaded after it
1038
+ starts, so a file loaded earlier is silently absent from the report rather than
1039
+ counted as uncovered. This is why `git.gemspec` reads the version string out of
1040
+ `lib/git/version.rb` instead of `require`ing it: the `Gemfile` uses `gemspec`, so
1041
+ requiring it there would load it on every `bundle exec`, before SimpleCov starts.
1042
+ Keep new code out of the load path that runs ahead of `spec_helper`.
1043
+
1044
+ When a branch is hard to cover, apply these in order:
1045
+
1046
+ 1. **Reach it through the public interface.** If a branch is reachable, test it.
1047
+ 2. **Delete it.** A branch that cannot be reached through the public interface is
1048
+ usually dead code, and removing it is preferable to excluding it. See commit
1049
+ `74e919a4` ("fix: remove unreachable nil check in `Git::Parsers::Grep.parse`") for
1050
+ the pattern.
1051
+ 3. **Exclude it with `# simplecov:disable`.** Reserved for defensive guards that could
1052
+ only be reached by breaking an OS-level invariant. Cover the smallest possible span,
1053
+ state why the code is unreachable, and expect a reviewer to question it. `lib/`
1054
+ currently contains no coverage directives.
1055
+
1056
+ Use the inline form wherever the exclusion is a single line. It applies only to the
1057
+ line it sits on and needs no matching `enable`, which makes it impossible to leave a
1058
+ region accidentally open:
1059
+
1060
+ ```ruby
1061
+ raise 'unreachable' # simplecov:disable defensive guard; only reachable on OOM
1062
+ ```
1063
+
1064
+ The block form covers a span and stays in effect until the matching
1065
+ `# simplecov:enable` (or end of file, if you forget it):
1066
+
1067
+ ```ruby
1068
+ # simplecov:disable branch platform-specific fallback; not reachable on MRI
1069
+ ...
1070
+ # simplecov:enable branch
1071
+ ```
1072
+
1073
+ Name the narrowest criterion that solves the problem and spell it exactly:
1074
+ `line`, `branch`, `method`, or a comma-separated combination. A word SimpleCov
1075
+ does not recognize is treated as free-form reason text, which silently widens the
1076
+ directive to all three criteria instead of failing. Write the reason after the
1077
+ criteria, so the required justification lives in the directive itself.
1078
+
1079
+ **Coverage is a floor on evidence, not a proof of correctness.** A test written only
1080
+ to execute a line, without asserting a meaningful outcome, violates Rule 24 of the
1081
+ [`rspec-unit-testing-standards`](.github/skills/rspec-unit-testing-standards/SKILL.md)
1082
+ skill and will be rejected in review even though it turns the report green. The
1083
+ threshold exists to surface untested behavior, not to be satisfied.
1084
+
1085
+ To see exactly what is uncovered:
1086
+
1087
+ ```bash
1088
+ # Names every uncovered line and branch (printed automatically when a full run fails)
1089
+ $ LIST_UNCOVERED_DETAIL=true bundle exec rake spec:unit
1090
+
1091
+ # Browsable HTML report, also uploaded as a CI artifact when a build fails
1092
+ $ open coverage/index.html
1093
+ ```
1094
+
1095
+ This policy applies to `main` and `5.x`. The `4.x` maintenance branch predates it and
1096
+ is not held to these thresholds.
1097
+
1098
+ #### Unit tests vs integration tests
1099
+
1100
+ This project uses two types of RSpec tests, organized by directory:
95
1101
 
96
- ### Continuous integration
97
- * All tests must pass in the project's [GitHub Continuous Integration build](https://github.com/ruby-git/ruby-git/actions?query=workflow%3ACI)
98
- before the pull request will be merged.
99
- * The [Continuous Integration workflow](https://github.com/ruby-git/ruby-git/blob/master/.github/workflows/continuous_integration.yml)
100
- runs both `bundle exec rake default` and `bundle exec rake test:gem` from the project's [Rakefile](https://github.com/ruby-git/ruby-git/blob/master/Rakefile).
1102
+ - **Unit tests** (`spec/unit/`): test individual classes and methods with mocked
1103
+ execution context. These verify that the gem builds correct git command arguments
1104
+ and properly handles git output. Unit tests should mock `@execution_context` to
1105
+ avoid calling real git commands.
101
1106
 
102
- ### Documentation
103
- * New and updated public methods must have [YARD](https://yardoc.org/)
104
- documentation added to them
105
- * New and updated public facing features should be documented in the project's
106
- [README.md](README.md)
1107
+ - **Integration tests** (`spec/integration/`): test the gem's behavior against real
1108
+ git repositories. These verify that mocked assumptions in unit tests match actual
1109
+ git behavior. Integration tests create temporary repositories using `Dir.mktmpdir`
1110
+ and run real git commands through the gem's public API.
107
1111
 
108
- ### Licensing sign-off
109
- * Each commit must contain [the DCO sign-off](#developer-certificate-of-origin-dco)
110
- in the form: `Signed-off-by: Name <email>`
1112
+ Integration tests validate that the gem correctly interacts with git, not that git
1113
+ itself works correctly. They should verify:
111
1114
 
112
- ## Licensing
1115
+ - That the gem's mocked command expectations match real git output format
1116
+ - That the gem correctly handles real git behavior (e.g., unicode in branch names)
1117
+ - That command options produce expected git behavior
1118
+ - Edge cases that are difficult to mock reliably
113
1119
 
114
- ruby-git uses [the MIT license](https://choosealicense.com/licenses/mit/) as
115
- declared in the [LICENSE](LICENSE) file.
1120
+ **Integration test guidelines**:
116
1121
 
117
- Licensing is very important to open source projects. It helps ensure the
118
- software continues to be available under the terms that the author desired.
1122
+ - Keep tests minimal and purposeful; only create what's needed for the test
1123
+ - Focus on key behaviors that unit tests can't verify
1124
+ - Don't test git's functionality; test the gem's interaction with git
1125
+ - Use the shared context `'in an empty repository'` for temporary repo setup
1126
+ - Use `Git::IntegrationTestHelpers` methods for file operations
1127
+ - Each test should verify one specific git interaction pattern
119
1128
 
120
- ### Developer Certificate of Origin (DCO)
1129
+ **Example**: An integration test for branch listing should verify that the gem
1130
+ correctly parses git's branch list format, not that git can create branches.
121
1131
 
122
- This project requires that authors have permission to submit their contributions
123
- under the MIT license. To make a good faith effort to ensure this, ruby-git
124
- requires the [Developer Certificate of Origin (DCO)](https://elinux.org/Developer_Certificate_Of_Origin)
125
- process be followed.
1132
+ While working on specific features, you can run tests using:
126
1133
 
127
- This process requires that each commit include a `Signed-off-by` line that
128
- indicates the author accepts the DCO. Here is an example DCO sign-off line:
1134
+ ```bash
1135
+ # Run all RSpec tests (unit + integration):
1136
+ $ bundle exec rake spec
129
1137
 
1138
+ # Run only RSpec unit tests:
1139
+ $ bundle exec rake spec:unit
1140
+
1141
+ # Run only RSpec integration tests:
1142
+ $ bundle exec rake spec:integration
1143
+
1144
+ # Run a specific RSpec file:
1145
+ $ bundle exec rspec spec/unit/git/commands/add_spec.rb
1146
+
1147
+ # Run tests with a different version of the git command line:
1148
+ $ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bundle exec rake spec
130
1149
  ```
131
- Signed-off-by: John Doe <john.doe@hisdomain.com>
1150
+
1151
+ ### What ships in the gem
1152
+
1153
+ `spec.files` in [`git.gemspec`](git.gemspec) is an **allowlist**: the released gem
1154
+ contains `lib/`, the documents [`.yardopts`](.yardopts) names as extra files, plus
1155
+ `UPGRADING.md` and the gemspec itself. Nothing else in the repository is published.
1156
+
1157
+ It used to be a denylist, which meant every new path was published by default. That
1158
+ shipped `.github/`, `tasks/`, the Husky hooks, and the `.claude/skills` symlink to
1159
+ users. The symlink is what forced the change: extracting a symlink requires a
1160
+ privilege Windows grants only under Developer Mode or an elevated shell, so
1161
+ `gem install git` either failed there or, on RubyGems new enough to fall back to a
1162
+ copy, quietly duplicated the whole skills tree into the installed gem.
1163
+
1164
+ What this means when you add a file:
1165
+
1166
+ - **Under `lib/`**: nothing to do; it ships automatically.
1167
+ - **A new top-level document**: add it to `doc_files` in the gemspec if users should
1168
+ get it, and to `.yardopts` if rubydoc.info should render it. The two lists are
1169
+ checked against each other, so a file in `.yardopts` but not the gem fails the
1170
+ suite rather than becoming a broken documentation link.
1171
+ - **Anything else**: it stays out of the gem, which is almost always what you want.
1172
+
1173
+ [`spec/unit/gemspec_spec.rb`](spec/unit/gemspec_spec.rb) enforces all of this: every
1174
+ tracked file under `lib/` is present, no symlink is, and nothing outside `lib/` and
1175
+ the project root is.
1176
+
1177
+ ## Building a specific version of the Git command-line
1178
+
1179
+ To test with a specific version of the Git command-line, you may need to build that
1180
+ version from source code. The following instructions are adapted from Atlassian's
1181
+ [How to install Git](https://www.atlassian.com/git/tutorials/install-git) page for
1182
+ building Git on macOS.
1183
+
1184
+ ### Install prerequisites
1185
+
1186
+ Install prerequisites only if they are not already present.
1187
+
1188
+ From your terminal, install Xcode's Command Line Tools:
1189
+
1190
+ ```shell
1191
+ xcode-select --install
132
1192
  ```
133
1193
 
134
- The full text of the DCO version 1.1 is below or at <http://developercertificate.org/>.
1194
+ Install [Homebrew](http://brew.sh/) by following the instructions on the Homebrew
1195
+ page.
1196
+
1197
+ Using Homebrew, install OpenSSL:
135
1198
 
1199
+ ```shell
1200
+ brew install openssl
136
1201
  ```
137
- Developer's Certificate of Origin 1.1
138
1202
 
139
- By making a contribution to this project, I certify that:
1203
+ ### Obtain Git source code
140
1204
 
141
- (a) The contribution was created in whole or in part by me and I
142
- have the right to submit it under the open source license
143
- indicated in the file; or
1205
+ Download and extract the source tarball for the desired Git version from [this source
1206
+ code mirror](https://mirrors.edge.kernel.org/pub/software/scm/git/).
144
1207
 
145
- (b) The contribution is based upon previous work that, to the
146
- best of my knowledge, is covered under an appropriate open
147
- source license and I have the right under that license to
148
- submit that work with modifications, whether created in whole
149
- or in part by me, under the same open source license (unless
150
- I am permitted to submit under a different license), as
151
- Indicated in the file; or
1208
+ ### Build git
152
1209
 
153
- (c) The contribution was provided directly to me by some other
154
- person who certified (a), (b) or (c) and I have not modified
155
- it.
1210
+ From your terminal, change to the root directory of the extracted source code and run
1211
+ the build with the following command:
156
1212
 
157
- (d) I understand and agree that this project and the contribution
158
- are public and that a record of the contribution (including
159
- all personal information I submit with it, including my
160
- sign-off) is maintained indefinitely and may be redistributed
161
- consistent with this project or the open source license(s)
162
- involved.
1213
+ ```shell
1214
+ NO_GETTEXT=1 make CFLAGS="-I/usr/local/opt/openssl/include" LDFLAGS="-L/usr/local/opt/openssl/lib"
163
1215
  ```
1216
+
1217
+ The build script will place the newly compiled Git executables in the `bin-wrappers`
1218
+ directory (e.g., `bin-wrappers/git`).
1219
+
1220
+ ### Use the new Git version
1221
+
1222
+ To configure programs that use the Git gem to run the newly built version, do the
1223
+ following:
1224
+
1225
+ ```ruby
1226
+ require 'git'
1227
+
1228
+ # Set the binary path
1229
+ Git.configure { |c| c.binary_path = '/Users/james/Downloads/git-2.30.2/bin-wrappers/git' }
1230
+
1231
+ # Validate the version (if desired)
1232
+ assert_equal(Git::Version.new(2, 30, 2), Git.git_version)
1233
+ ```
1234
+
1235
+ Tests can be run using the newly built Git version as follows:
1236
+
1237
+ ```shell
1238
+ GIT_PATH=/Users/james/Downloads/git-2.30.2/bin-wrappers bundle exec rake spec
1239
+ ```
1240
+
1241
+ Note: `GIT_PATH` refers to the directory containing the `git` executable.