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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +67 -2705
- data/.github/pull_request_template.md +3 -1
- data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
- data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
- data/.github/skills/command-implementation/REFERENCE.md +993 -0
- data/.github/skills/command-implementation/SKILL.md +229 -0
- data/.github/skills/command-test-conventions/SKILL.md +660 -0
- data/.github/skills/command-yard-documentation/SKILL.md +426 -0
- data/.github/skills/dependency-management/SKILL.md +72 -0
- data/.github/skills/development-workflow/SKILL.md +506 -0
- data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
- data/.github/skills/facade-implementation/REFERENCE.md +840 -0
- data/.github/skills/facade-implementation/SKILL.md +260 -0
- data/.github/skills/facade-test-conventions/SKILL.md +380 -0
- data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
- data/.github/skills/make-skill-template/SKILL.md +176 -0
- data/.github/skills/pr-readiness-review/SKILL.md +185 -0
- data/.github/skills/project-context/SKILL.md +313 -0
- data/.github/skills/pull-request-review/SKILL.md +168 -0
- data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
- data/.github/skills/release-management/SKILL.md +125 -0
- data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
- data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
- data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
- data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
- data/.github/skills/reviewing-skills/SKILL.md +189 -0
- data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
- data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
- data/.github/skills/test-debugging/SKILL.md +160 -0
- data/.github/skills/yard-documentation/SKILL.md +793 -0
- data/.github/workflows/continuous_integration.yml +3 -2
- data/.github/workflows/enforce_conventional_commits.yml +1 -1
- data/.github/workflows/experimental_continuous_integration.yml +2 -2
- data/.github/workflows/release.yml +3 -4
- data/.gitignore +8 -0
- data/.husky/pre-commit +13 -0
- data/.release-please-manifest.json +1 -1
- data/.rspec +3 -0
- data/.rubocop.yml +7 -3
- data/.rubocop_todo.yml +23 -5
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -40
- data/CONTRIBUTING.md +694 -53
- data/README.md +17 -5
- data/Rakefile +61 -9
- data/commitlint.test +4 -0
- data/git.gemspec +14 -8
- data/lib/git/args_builder.rb +0 -8
- data/lib/git/base.rb +486 -410
- data/lib/git/branch.rb +380 -43
- data/lib/git/branch_delete_failure.rb +31 -0
- data/lib/git/branch_delete_result.rb +63 -0
- data/lib/git/branch_info.rb +178 -0
- data/lib/git/branches.rb +130 -24
- data/lib/git/command_line/base.rb +245 -0
- data/lib/git/command_line/capturing.rb +249 -0
- data/lib/git/command_line/result.rb +96 -0
- data/lib/git/command_line/streaming.rb +194 -0
- data/lib/git/command_line.rb +43 -322
- data/lib/git/command_line_result.rb +4 -88
- data/lib/git/commands/add.rb +131 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +252 -0
- data/lib/git/commands/am/continue.rb +43 -0
- data/lib/git/commands/am/quit.rb +43 -0
- data/lib/git/commands/am/retry.rb +47 -0
- data/lib/git/commands/am/show_current_patch.rb +64 -0
- data/lib/git/commands/am/skip.rb +42 -0
- data/lib/git/commands/am.rb +33 -0
- data/lib/git/commands/apply.rb +237 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +140 -0
- data/lib/git/commands/arguments.rb +3510 -0
- data/lib/git/commands/base.rb +403 -0
- data/lib/git/commands/branch/copy.rb +94 -0
- data/lib/git/commands/branch/create.rb +173 -0
- data/lib/git/commands/branch/delete.rb +80 -0
- data/lib/git/commands/branch/list.rb +162 -0
- data/lib/git/commands/branch/move.rb +94 -0
- data/lib/git/commands/branch/set_upstream.rb +86 -0
- data/lib/git/commands/branch/show_current.rb +49 -0
- data/lib/git/commands/branch/unset_upstream.rb +57 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +364 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +210 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +151 -0
- data/lib/git/commands/checkout/files.rb +115 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +105 -0
- data/lib/git/commands/clean.rb +100 -0
- data/lib/git/commands/clone.rb +240 -0
- data/lib/git/commands/commit.rb +272 -0
- data/lib/git/commands/commit_tree.rb +100 -0
- data/lib/git/commands/config_option_syntax/add.rb +83 -0
- data/lib/git/commands/config_option_syntax/get.rb +117 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
- data/lib/git/commands/config_option_syntax/list.rb +107 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
- data/lib/git/commands/config_option_syntax/set.rb +114 -0
- data/lib/git/commands/config_option_syntax/unset.rb +89 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +155 -0
- data/lib/git/commands/diff.rb +656 -0
- data/lib/git/commands/diff_files.rb +518 -0
- data/lib/git/commands/diff_index.rb +496 -0
- data/lib/git/commands/fetch.rb +352 -0
- data/lib/git/commands/fsck.rb +136 -0
- data/lib/git/commands/gc.rb +132 -0
- data/lib/git/commands/grep.rb +338 -0
- data/lib/git/commands/init.rb +99 -0
- data/lib/git/commands/log.rb +632 -0
- data/lib/git/commands/ls_files.rb +191 -0
- data/lib/git/commands/ls_remote.rb +155 -0
- data/lib/git/commands/ls_tree.rb +131 -0
- data/lib/git/commands/maintenance/register.rb +75 -0
- data/lib/git/commands/maintenance/run.rb +104 -0
- data/lib/git/commands/maintenance/start.rb +66 -0
- data/lib/git/commands/maintenance/stop.rb +55 -0
- data/lib/git/commands/maintenance/unregister.rb +79 -0
- data/lib/git/commands/maintenance.rb +31 -0
- data/lib/git/commands/merge/abort.rb +44 -0
- data/lib/git/commands/merge/continue.rb +44 -0
- data/lib/git/commands/merge/quit.rb +46 -0
- data/lib/git/commands/merge/start.rb +245 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +86 -0
- data/lib/git/commands/mv.rb +77 -0
- data/lib/git/commands/name_rev.rb +114 -0
- data/lib/git/commands/pull.rb +377 -0
- data/lib/git/commands/push.rb +246 -0
- data/lib/git/commands/read_tree.rb +149 -0
- data/lib/git/commands/remote/add.rb +91 -0
- data/lib/git/commands/remote/get_url.rb +66 -0
- data/lib/git/commands/remote/list.rb +54 -0
- data/lib/git/commands/remote/prune.rb +61 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +69 -0
- data/lib/git/commands/remote/set_branches.rb +63 -0
- data/lib/git/commands/remote/set_head.rb +82 -0
- data/lib/git/commands/remote/set_url.rb +71 -0
- data/lib/git/commands/remote/set_url_add.rb +61 -0
- data/lib/git/commands/remote/set_url_delete.rb +64 -0
- data/lib/git/commands/remote/show.rb +71 -0
- data/lib/git/commands/remote/update.rb +72 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +277 -0
- data/lib/git/commands/reset.rb +147 -0
- data/lib/git/commands/rev_parse.rb +297 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +57 -0
- data/lib/git/commands/revert/quit.rb +47 -0
- data/lib/git/commands/revert/skip.rb +44 -0
- data/lib/git/commands/revert/start.rb +153 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +114 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
- data/lib/git/commands/show_ref/exists.rb +78 -0
- data/lib/git/commands/show_ref/list.rb +145 -0
- data/lib/git/commands/show_ref/verify.rb +120 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +75 -0
- data/lib/git/commands/stash/branch.rb +65 -0
- data/lib/git/commands/stash/clear.rb +41 -0
- data/lib/git/commands/stash/create.rb +58 -0
- data/lib/git/commands/stash/drop.rb +67 -0
- data/lib/git/commands/stash/list.rb +39 -0
- data/lib/git/commands/stash/pop.rb +78 -0
- data/lib/git/commands/stash/push.rb +103 -0
- data/lib/git/commands/stash/show.rb +149 -0
- data/lib/git/commands/stash/store.rb +63 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +169 -0
- data/lib/git/commands/symbolic_ref/delete.rb +68 -0
- data/lib/git/commands/symbolic_ref/read.rb +95 -0
- data/lib/git/commands/symbolic_ref/update.rb +76 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +139 -0
- data/lib/git/commands/tag/delete.rb +55 -0
- data/lib/git/commands/tag/list.rb +143 -0
- data/lib/git/commands/tag/verify.rb +71 -0
- data/lib/git/commands/tag.rb +26 -0
- data/lib/git/commands/update_ref/batch.rb +140 -0
- data/lib/git/commands/update_ref/delete.rb +92 -0
- data/lib/git/commands/update_ref/update.rb +106 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +52 -0
- data/lib/git/commands/worktree/add.rb +140 -0
- data/lib/git/commands/worktree/list.rb +64 -0
- data/lib/git/commands/worktree/lock.rb +58 -0
- data/lib/git/commands/worktree/management_base.rb +51 -0
- data/lib/git/commands/worktree/move.rb +66 -0
- data/lib/git/commands/worktree/prune.rb +67 -0
- data/lib/git/commands/worktree/remove.rb +63 -0
- data/lib/git/commands/worktree/repair.rb +76 -0
- data/lib/git/commands/worktree/unlock.rb +47 -0
- data/lib/git/commands/worktree.rb +43 -0
- data/lib/git/commands/write_tree.rb +68 -0
- data/lib/git/commands.rb +89 -0
- data/lib/git/detached_head_info.rb +54 -0
- data/lib/git/diff.rb +297 -7
- data/lib/git/diff_file_numstat_info.rb +29 -0
- data/lib/git/diff_file_patch_info.rb +134 -0
- data/lib/git/diff_file_raw_info.rb +127 -0
- data/lib/git/diff_info.rb +169 -0
- data/lib/git/diff_path_status.rb +78 -19
- data/lib/git/diff_result.rb +32 -0
- data/lib/git/diff_stats.rb +59 -14
- data/lib/git/dirstat_info.rb +86 -0
- data/lib/git/errors.rb +65 -2
- data/lib/git/execution_context/global.rb +56 -0
- data/lib/git/execution_context/repository.rb +147 -0
- data/lib/git/execution_context.rb +482 -0
- data/lib/git/file_ref.rb +74 -0
- data/lib/git/fsck_object.rb +9 -9
- data/lib/git/fsck_result.rb +1 -1
- data/lib/git/lib.rb +1606 -1028
- data/lib/git/log.rb +15 -2
- data/lib/git/object.rb +92 -22
- data/lib/git/parsers/branch.rb +224 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/diff.rb +585 -0
- data/lib/git/parsers/fsck.rb +133 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/stash.rb +208 -0
- data/lib/git/parsers/tag.rb +257 -0
- data/lib/git/remote.rb +133 -9
- data/lib/git/repository/branching.rb +572 -0
- data/lib/git/repository/committing.rb +191 -0
- data/lib/git/repository/configuring.rb +156 -0
- data/lib/git/repository/diffing.rb +775 -0
- data/lib/git/repository/inspecting.rb +153 -0
- data/lib/git/repository/logging.rb +247 -0
- data/lib/git/repository/merging.rb +295 -0
- data/lib/git/repository/object_operations.rb +1101 -0
- data/lib/git/repository/path_resolver.rb +207 -0
- data/lib/git/repository/remote_operations.rb +753 -0
- data/lib/git/repository/shared_private.rb +51 -0
- data/lib/git/repository/staging.rb +390 -0
- data/lib/git/repository/stashing.rb +107 -0
- data/lib/git/repository/status_operations.rb +180 -0
- data/lib/git/repository/worktree_operations.rb +159 -0
- data/lib/git/repository.rb +264 -1
- data/lib/git/stash.rb +85 -4
- data/lib/git/stash_info.rb +104 -0
- data/lib/git/stashes.rb +130 -13
- data/lib/git/status.rb +224 -18
- data/lib/git/tag_delete_failure.rb +31 -0
- data/lib/git/tag_delete_result.rb +63 -0
- data/lib/git/tag_info.rb +105 -0
- data/lib/git/version.rb +109 -2
- data/lib/git/version_constraint.rb +81 -0
- data/lib/git/worktree.rb +120 -5
- data/lib/git/worktrees.rb +107 -7
- data/lib/git.rb +114 -18
- data/redesign/1_architecture_existing.md +54 -18
- data/redesign/2_architecture_redesign.md +365 -46
- data/redesign/3_architecture_implementation.md +1451 -54
- data/tasks/gem_tasks.rake +4 -0
- data/tasks/npm_tasks.rake +7 -0
- data/tasks/rspec.rake +48 -0
- data/tasks/test.rake +13 -1
- data/tasks/yard.rake +34 -7
- metadata +349 -20
- data/lib/git/index.rb +0 -6
- data/lib/git/path.rb +0 -38
- data/lib/git/working_directory.rb +0 -6
- /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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
130
|
-
before submitting changes. You are responsible for
|
|
131
|
-
AI-assisted work included in PRs and ensuring it
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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.
|
|
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
|
|
217
|
-
|
|
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::
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
#
|
|
281
|
-
$
|
|
922
|
+
# Run all tests (TestUnit + RSpec):
|
|
923
|
+
$ bundle exec rake test_all
|
|
282
924
|
|
|
283
|
-
#
|
|
284
|
-
$
|
|
925
|
+
# Run only TestUnit integration tests:
|
|
926
|
+
$ bundle exec rake test
|
|
285
927
|
|
|
286
|
-
#
|
|
287
|
-
$
|
|
928
|
+
# Run only RSpec tests (unit + integration):
|
|
929
|
+
$ bundle exec rake spec
|
|
288
930
|
|
|
289
|
-
#
|
|
290
|
-
$
|
|
291
|
-
```
|
|
931
|
+
# Run only RSpec unit tests:
|
|
932
|
+
$ bundle exec rake spec:unit
|
|
292
933
|
|
|
293
|
-
|
|
934
|
+
# Run only RSpec integration tests:
|
|
935
|
+
$ bundle exec rake spec:integration
|
|
294
936
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
pull request will be merged.
|
|
937
|
+
# Run a single TestUnit file (from tests/units):
|
|
938
|
+
$ bin/test test_object
|
|
298
939
|
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
943
|
+
# Run a specific RSpec file:
|
|
944
|
+
$ bundle exec rspec spec/unit/git/commands/add_spec.rb
|
|
305
945
|
|
|
306
|
-
|
|
307
|
-
|
|
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(
|
|
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:
|