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
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
# Facade Implementation — Reference
|
|
2
|
+
|
|
3
|
+
Detailed reference for `Git::Repository::*` facade modules and methods. This file
|
|
4
|
+
is loaded by subagents during the [Facade Implementation](SKILL.md) workflow.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Contents](#contents)
|
|
9
|
+
- [Files to generate](#files-to-generate)
|
|
10
|
+
- [Topic module selection](#topic-module-selection)
|
|
11
|
+
- [Existing modules](#existing-modules)
|
|
12
|
+
- [Decision rules for adding a new module](#decision-rules-for-adding-a-new-module)
|
|
13
|
+
- [One-at-a-time extraction from `Git::Base` / `Git::Lib`](#one-at-a-time-extraction-from-gitbase--gitlib)
|
|
14
|
+
- [Naming a new topic module](#naming-a-new-topic-module)
|
|
15
|
+
- [Designing a facade method](#designing-a-facade-method)
|
|
16
|
+
- [Choosing the return type](#choosing-the-return-type)
|
|
17
|
+
- [Choosing the method signature](#choosing-the-method-signature)
|
|
18
|
+
- [One-line delegator](#one-line-delegator)
|
|
19
|
+
- [Orchestration sequence](#orchestration-sequence)
|
|
20
|
+
- [Sequencing multiple commands](#sequencing-multiple-commands)
|
|
21
|
+
- [Topic module skeleton](#topic-module-skeleton)
|
|
22
|
+
- [The five facade responsibilities checklist](#the-five-facade-responsibilities-checklist)
|
|
23
|
+
- [Argument pre-processing patterns](#argument-pre-processing-patterns)
|
|
24
|
+
- [Path normalization](#path-normalization)
|
|
25
|
+
- [Option whitelisting (preventing API expansion)](#option-whitelisting-preventing-api-expansion)
|
|
26
|
+
- [Deprecation handling](#deprecation-handling)
|
|
27
|
+
- [Defaults and policy options](#defaults-and-policy-options)
|
|
28
|
+
- [Internal helpers and encapsulation](#internal-helpers-and-encapsulation)
|
|
29
|
+
- [The rule](#the-rule)
|
|
30
|
+
- [The pattern](#the-pattern)
|
|
31
|
+
- [Why this works](#why-this-works)
|
|
32
|
+
- [Naming rules](#naming-rules)
|
|
33
|
+
- [Growth path](#growth-path)
|
|
34
|
+
- [Decision 1 — Where does the helper live?](#decision-1--where-does-the-helper-live)
|
|
35
|
+
- [Decision 2 — How is state passed to the helper?](#decision-2--how-is-state-passed-to-the-helper)
|
|
36
|
+
- [Why not `ActiveSupport::Concern`?](#why-not-activesupportconcern)
|
|
37
|
+
- [Parser vs. raw stdout](#parser-vs-raw-stdout)
|
|
38
|
+
- [Result-class factory methods](#result-class-factory-methods)
|
|
39
|
+
- [Common failures](#common-failures)
|
|
40
|
+
- [One-line delegation when orchestration is needed](#one-line-delegation-when-orchestration-is-needed)
|
|
41
|
+
- [Leaking command-class types into the public API](#leaking-command-class-types-into-the-public-api)
|
|
42
|
+
- [Exposing command-DSL-shaped argv in the facade signature](#exposing-command-dsl-shaped-argv-in-the-facade-signature)
|
|
43
|
+
- [Changing the legacy return type or signature on extraction](#changing-the-legacy-return-type-or-signature-on-extraction)
|
|
44
|
+
- [Bypassing `@execution_context`](#bypassing-execution_context)
|
|
45
|
+
- [Placing an overridable policy default after caller options](#placing-an-overridable-policy-default-after-caller-options)
|
|
46
|
+
- [Skipping option whitelisting on opaque opts hashes](#skipping-option-whitelisting-on-opaque-opts-hashes)
|
|
47
|
+
- [Mixing facade and command responsibilities](#mixing-facade-and-command-responsibilities)
|
|
48
|
+
- [Adding a topic module whose methods fit an existing one](#adding-a-topic-module-whose-methods-fit-an-existing-one)
|
|
49
|
+
|
|
50
|
+
## Files to generate
|
|
51
|
+
|
|
52
|
+
For a facade method on `Git::Repository::<Topic>`:
|
|
53
|
+
|
|
54
|
+
- `lib/git/repository/<topic>.rb` — the topic module (created on first method,
|
|
55
|
+
extended for subsequent methods)
|
|
56
|
+
- `spec/unit/git/repository/<topic>_spec.rb` — unit tests
|
|
57
|
+
- `spec/integration/git/repository/<topic>_spec.rb` — integration tests
|
|
58
|
+
(omit for true one-line delegators that add no orchestration)
|
|
59
|
+
|
|
60
|
+
When the topic module is new, also update:
|
|
61
|
+
|
|
62
|
+
- `lib/git/repository.rb` — add `require 'git/repository/<topic>'` and `include
|
|
63
|
+
Git::Repository::<Topic>` in alphabetical order with the existing entries.
|
|
64
|
+
|
|
65
|
+
## Topic module selection
|
|
66
|
+
|
|
67
|
+
### Existing modules
|
|
68
|
+
|
|
69
|
+
List `lib/git/repository/` to see all current topic modules. Add to one of those
|
|
70
|
+
modules whenever the new method fits the topic. Do not create a new module when
|
|
71
|
+
an existing one would do.
|
|
72
|
+
|
|
73
|
+
### Decision rules for adding a new module
|
|
74
|
+
|
|
75
|
+
Create a new topic module only when **both** of the following are true:
|
|
76
|
+
|
|
77
|
+
1. The topic is recognizable to a reader familiar with git — preferably matching
|
|
78
|
+
one of the categories at <https://git-scm.com/docs> (Working tree, Branching,
|
|
79
|
+
History, Sharing, Patching, Inspection, Configuration, Plumbing).
|
|
80
|
+
2. The methods would be awkward to place in any existing module without diluting
|
|
81
|
+
that module's topic.
|
|
82
|
+
|
|
83
|
+
There is no fixed method-count requirement. A module that starts with one or two
|
|
84
|
+
methods is fine when the topic is genuinely distinct; the question is always
|
|
85
|
+
*fit*, not count. Default to extending an existing module whenever the new method
|
|
86
|
+
plausibly fits there.
|
|
87
|
+
|
|
88
|
+
#### One-at-a-time extraction from `Git::Base` / `Git::Lib`
|
|
89
|
+
|
|
90
|
+
When migrating a single method without a planned batch:
|
|
91
|
+
|
|
92
|
+
1. Before deciding placement, scan `Git::Base` and `Git::Lib` for sibling methods
|
|
93
|
+
on the same git topic (e.g. when extracting `branches_all`, also look for
|
|
94
|
+
`branch`, `branch_current`, `branch_delete`, `current_branch_state`).
|
|
95
|
+
2. If those siblings form a coherent topic that does not fit any existing module,
|
|
96
|
+
create the new module on this first extraction so subsequent extractions have
|
|
97
|
+
a home.
|
|
98
|
+
3. Otherwise place the method in the closest existing module. Revisit module
|
|
99
|
+
organization later if a distinct topic emerges; promote the cluster to its own
|
|
100
|
+
module in a single `refactor(repository):` commit at that point.
|
|
101
|
+
|
|
102
|
+
For a one-off method that does not fit any existing module and does not justify a
|
|
103
|
+
new module yet, place it in the closest existing module and revisit the
|
|
104
|
+
organization when more methods join it.
|
|
105
|
+
|
|
106
|
+
### Naming a new topic module
|
|
107
|
+
|
|
108
|
+
Topic modules follow a **two-tier** convention (documented in
|
|
109
|
+
[redesign/3_architecture_implementation.md §Facade module naming convention](../../../redesign/3_architecture_implementation.md#facade-module-naming-convention)):
|
|
110
|
+
|
|
111
|
+
- **Gerund** (`verb-ing`) when a single action word clearly names the whole module:
|
|
112
|
+
`Staging`, `Committing`, `Branching`, `Merging`, `Logging`, `Diffing`, `Stashing`.
|
|
113
|
+
- **Noun + `Operations`** when the module groups a mixed bag of methods by git
|
|
114
|
+
concept rather than a single action: `RemoteOperations`, `ObjectOperations`,
|
|
115
|
+
`StatusOperations`, `WorktreeOperations`.
|
|
116
|
+
|
|
117
|
+
Additional rules:
|
|
118
|
+
|
|
119
|
+
- Use PascalCase; the file name is the snake_case equivalent (`Branching` →
|
|
120
|
+
`branching.rb`).
|
|
121
|
+
- Do **not** use plain nouns that clash with existing public or domain-object class names such
|
|
122
|
+
as `Branch`, `Diff`, `Log`, `Object`, `Remote`, `Status`, `Worktree`, etc. Those names
|
|
123
|
+
belong to existing domain/query/value objects, not topic modules.
|
|
124
|
+
- Do not use the `*Info` or `*Result` suffix — reserved for parsed result structs.
|
|
125
|
+
|
|
126
|
+
## Designing a facade method
|
|
127
|
+
|
|
128
|
+
Decide the return type and the signature first (together they are the public
|
|
129
|
+
contract), then choose the body shape — one-line delegator for the simplest
|
|
130
|
+
cases, orchestration sequence otherwise.
|
|
131
|
+
|
|
132
|
+
### Choosing the return type
|
|
133
|
+
|
|
134
|
+
Apply these rules in order:
|
|
135
|
+
|
|
136
|
+
1. **Extracting from `Git::Base` or `Git::Lib`?** The facade method **must**
|
|
137
|
+
return exactly what the legacy method returned (same type, same shape, same
|
|
138
|
+
nil/empty semantics). Backward compatibility for users who called
|
|
139
|
+
`g.foo` / `g.lib.foo` is the public contract being preserved during
|
|
140
|
+
migration; capture the legacy return in the Step 2 plan.
|
|
141
|
+
2. **No legacy method (greenfield facade method)?** Choose the return type from
|
|
142
|
+
the public-API perspective, in this order of preference:
|
|
143
|
+
1. A **domain object** (`Git::BranchInfo`, `Git::DiffResult`, …) when the
|
|
144
|
+
output has structure callers will inspect.
|
|
145
|
+
2. A **primitive** (`String` of chomped stdout, `Boolean`, `Integer`) when
|
|
146
|
+
the output is a single value.
|
|
147
|
+
3. `nil` or `self` when the method is called for its side effects.
|
|
148
|
+
4. **`Git::CommandLineResult`** only when the topic module explicitly
|
|
149
|
+
documents that as its contract (rare — reserved for low-level escape
|
|
150
|
+
hatches). Do not return `CommandLineResult` by default just because the
|
|
151
|
+
command returns it.
|
|
152
|
+
3. **Never return** a type from `Git::Commands::*` (e.g.
|
|
153
|
+
`Git::Commands::Foo::Bar::SomeResult`). Command-internal types are not part
|
|
154
|
+
of the facade's public API.
|
|
155
|
+
|
|
156
|
+
### Choosing the method signature
|
|
157
|
+
|
|
158
|
+
The Ruby signature is part of the public contract — just as binding as the
|
|
159
|
+
return type. Apply these rules in order:
|
|
160
|
+
|
|
161
|
+
1. **Extracting from `Git::Base` or `Git::Lib`?** The facade method **must**
|
|
162
|
+
preserve the legacy signature exactly: same positional arguments in the same
|
|
163
|
+
order, same defaults, same `opts = {}` vs. `**options` shape, same
|
|
164
|
+
nil/sentinel semantics. Capture the legacy signature in the Step 2 plan and
|
|
165
|
+
diff against it after implementation.
|
|
166
|
+
2. **No legacy method (greenfield facade method)?** Design the signature from
|
|
167
|
+
the public-API perspective:
|
|
168
|
+
1. **Positional arguments** for the natural domain identifiers the method
|
|
169
|
+
operates on (paths, refs, names, messages). At most two or three.
|
|
170
|
+
2. **Keyword arguments with `**options`** for option hashes. Prefer
|
|
171
|
+
`**options` over `opts = {}` in greenfield code — it surfaces unknown
|
|
172
|
+
keys at the call site and reads better with the [whitelist
|
|
173
|
+
pattern](#option-whitelisting-preventing-api-expansion). When the body
|
|
174
|
+
forwards the options unchanged (the common case), use the **anonymous**
|
|
175
|
+
keyword splat (`**`) so RuboCop's `Style/ArgumentsForwarding` cop is
|
|
176
|
+
satisfied; name the splat (`**options`) only when the body must inspect
|
|
177
|
+
or mutate the hash before forwarding (e.g. merging a positional
|
|
178
|
+
argument into it, or applying a deprecation rewrite).
|
|
179
|
+
3. **Named keyword arguments** (`force: false`, `all: true`) for a small,
|
|
180
|
+
fixed set of flags that are part of the documented API and unlikely to
|
|
181
|
+
grow. Switch to `**options` once the set exceeds ~3 keys.
|
|
182
|
+
3. **Validate cross-argument constraints in the facade**, before calling the
|
|
183
|
+
command. Raise `ArgumentError` with a message that names the offending
|
|
184
|
+
arguments — for example, `pull` raises when `branch` is given without
|
|
185
|
+
`remote`. The command class stays neutral about Ruby-level argument
|
|
186
|
+
relationships.
|
|
187
|
+
4. **Never expose** command-DSL-shaped arguments (`*argv`, raw `Hash` of CLI
|
|
188
|
+
flags) on the facade. The facade's job is to translate Ruby idioms into
|
|
189
|
+
command calls; passing through opaque argv defeats the layer.
|
|
190
|
+
|
|
191
|
+
Mechanical patterns for shaping the inputs (path coercion, option whitelisting,
|
|
192
|
+
deprecation handling) live in [Argument pre-processing
|
|
193
|
+
patterns](#argument-pre-processing-patterns).
|
|
194
|
+
|
|
195
|
+
### One-line delegator
|
|
196
|
+
|
|
197
|
+
When the facade method takes no options hash, does no pre-processing, and only
|
|
198
|
+
a trivial post-processing step (such as `.stdout.chomp`), it is a single-line
|
|
199
|
+
delegation. For example, a hypothetical `Git::Repository::Inspection#current_branch`
|
|
200
|
+
preserving `Git::Lib#current_branch`'s `String` contract:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# Return the name of the currently checked-out branch
|
|
204
|
+
#
|
|
205
|
+
# @example Get the current branch name
|
|
206
|
+
# repo.current_branch #=> "main"
|
|
207
|
+
#
|
|
208
|
+
# @return [String] the current branch name
|
|
209
|
+
#
|
|
210
|
+
# @raise [Git::FailedError] when `git rev-parse` exits with a non-zero status
|
|
211
|
+
#
|
|
212
|
+
def current_branch
|
|
213
|
+
Git::Commands::RevParse.new(@execution_context).call('--abbrev-ref', 'HEAD').stdout.chomp
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Use the one-line form only when **all** of the following hold:
|
|
218
|
+
|
|
219
|
+
- The Ruby signature exactly matches the command's `#call` signature (with at most
|
|
220
|
+
trivial coercion like `Array(paths)` or `*[remote, branch].compact`).
|
|
221
|
+
- The method takes no options hash. Any facade method that accepts options must
|
|
222
|
+
whitelist them — see [Option whitelisting](#option-whitelisting-preventing-api-expansion) —
|
|
223
|
+
which makes it at minimum a two-line orchestration.
|
|
224
|
+
- The post-processing is at most a single chained call (`.stdout`,
|
|
225
|
+
`.stdout.chomp`, etc.) that produces the documented return type (see
|
|
226
|
+
[Choosing the return type](#choosing-the-return-type) above).
|
|
227
|
+
|
|
228
|
+
If the documented return type requires parsing, multiple commands, validation,
|
|
229
|
+
deprecation, option whitelisting, or any conditional logic, the facade needs an
|
|
230
|
+
orchestration sequence — not a one-line delegator.
|
|
231
|
+
|
|
232
|
+
### Orchestration sequence
|
|
233
|
+
|
|
234
|
+
When the facade method needs pre-processing, multiple commands, parsing, or result
|
|
235
|
+
assembly, expand the body into explicit phases:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
def branches_all
|
|
239
|
+
result = Git::Commands::Branch::List.new(@execution_context).call(
|
|
240
|
+
all: true,
|
|
241
|
+
format: Git::Parsers::Branch::FORMAT_STRING
|
|
242
|
+
)
|
|
243
|
+
Git::Parsers::Branch.parse_list(result.stdout)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def commit(message, opts = {})
|
|
247
|
+
SharedPrivate.assert_valid_opts!(COMMIT_ALLOWED_OPTS, **opts)
|
|
248
|
+
opts = opts.merge(message: message) if message
|
|
249
|
+
opts = deprecate_commit_no_gpg_sign_option(opts)
|
|
250
|
+
Git::Commands::Commit.new(@execution_context).call(no_edit: true, **opts).stdout
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Three phases — keep them in this order:
|
|
255
|
+
|
|
256
|
+
1. **Pre-process** — validate, whitelist, normalize, deprecate, default.
|
|
257
|
+
2. **Call** — invoke one or more `Git::Commands::*` instances, each via
|
|
258
|
+
`@execution_context`.
|
|
259
|
+
3. **Assemble** — pass stdout/stderr/status through a parser or result-class
|
|
260
|
+
factory method, or return the raw `CommandLineResult` value the topic module
|
|
261
|
+
documents.
|
|
262
|
+
|
|
263
|
+
### Sequencing multiple commands
|
|
264
|
+
|
|
265
|
+
When a facade method orchestrates more than one command, sequence the calls
|
|
266
|
+
explicitly with intermediate results in local variables:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
def branch_status(name)
|
|
270
|
+
upstream_result = Git::Commands::RevParse.new(@execution_context).call("#{name}@{upstream}")
|
|
271
|
+
ahead_behind = Git::Commands::RevList.new(@execution_context).call(
|
|
272
|
+
"#{name}...#{upstream_result.stdout.chomp}",
|
|
273
|
+
left_right: true,
|
|
274
|
+
count: true
|
|
275
|
+
)
|
|
276
|
+
Git::BranchStatus.from_rev_list_output(name, ahead_behind.stdout)
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Do not build a generic dispatcher or a "run everything in parallel" abstraction.
|
|
281
|
+
Explicit sequential calls are the documented pattern.
|
|
282
|
+
|
|
283
|
+
## Topic module skeleton
|
|
284
|
+
|
|
285
|
+
The full file layout for a topic module under `lib/git/repository/`:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
# frozen_string_literal: true
|
|
289
|
+
|
|
290
|
+
require 'git/commands/<command_a>'
|
|
291
|
+
require 'git/commands/<command_b>'
|
|
292
|
+
# require 'git/parsers/<parser>' — when the module uses a parser
|
|
293
|
+
|
|
294
|
+
module Git
|
|
295
|
+
class Repository
|
|
296
|
+
# Short summary of the topic and the facade methods it provides
|
|
297
|
+
#
|
|
298
|
+
# Included by {Git::Repository}.
|
|
299
|
+
#
|
|
300
|
+
# @api public
|
|
301
|
+
#
|
|
302
|
+
module Topic
|
|
303
|
+
# YARD docs per facade-yard-documentation skill
|
|
304
|
+
def method_a(...)
|
|
305
|
+
# body
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# YARD docs per facade-yard-documentation skill
|
|
309
|
+
def method_b(...)
|
|
310
|
+
# body
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Then wire into `lib/git/repository.rb`:
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
require 'git/repository/topic'
|
|
321
|
+
# ...
|
|
322
|
+
|
|
323
|
+
class Repository
|
|
324
|
+
include Git::Repository::Topic
|
|
325
|
+
# ...
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## The five facade responsibilities checklist
|
|
330
|
+
|
|
331
|
+
From [redesign/2_architecture_redesign.md §2.1](../../../redesign/2_architecture_redesign.md).
|
|
332
|
+
For each facade method, confirm whether each responsibility applies and is handled:
|
|
333
|
+
|
|
334
|
+
- [ ] **Manage execution context** — calls `Git::Commands::*.new(@execution_context)`,
|
|
335
|
+
never builds CLI argv directly and never bypasses the execution context.
|
|
336
|
+
- [ ] **Pre-process arguments** — applies path expansion, Ruby-idiomatic defaults,
|
|
337
|
+
option whitelisting, deprecations.
|
|
338
|
+
- [ ] **Collect data** — gathers any additional information needed before or after
|
|
339
|
+
command execution to build the response (e.g., reading config, listing refs).
|
|
340
|
+
Most facade methods do not need this; flag explicitly when present.
|
|
341
|
+
- [ ] **Call commands** — invokes one or more `Git::Commands::*` classes; multiple
|
|
342
|
+
calls are sequenced explicitly with intermediate results held in local variables.
|
|
343
|
+
- [ ] **Build rich response objects** — passes stdout through a `Git::Parsers::*`
|
|
344
|
+
class or a result-class factory method to produce the documented return type.
|
|
345
|
+
Returning the raw `CommandLineResult` is acceptable only when that is the
|
|
346
|
+
documented public contract for the topic module.
|
|
347
|
+
|
|
348
|
+
## Argument pre-processing patterns
|
|
349
|
+
|
|
350
|
+
### Path normalization
|
|
351
|
+
|
|
352
|
+
Accept `String` or `Array<String>` for path arguments and splat into the command:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
def add(paths = '.', **)
|
|
356
|
+
SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
|
|
357
|
+
Git::Commands::Add.new(@execution_context).call(*Array(paths), **).stdout
|
|
358
|
+
end
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
For path arguments that must be absolute or relative to the worktree root, expand
|
|
362
|
+
with `File.expand_path` against `@execution_context.git_work_dir`.
|
|
363
|
+
|
|
364
|
+
### Option whitelisting (preventing API expansion)
|
|
365
|
+
|
|
366
|
+
When the facade method accepts an options hash (positional `opts = {}` *or*
|
|
367
|
+
keyword `**options`) and forwards it to a command, the underlying command class
|
|
368
|
+
typically exposes many more options than the public facade contract. Without
|
|
369
|
+
filtering, callers could pass options that happen to match command DSL names but
|
|
370
|
+
were never part of the facade's public API — silently expanding the contract.
|
|
371
|
+
|
|
372
|
+
Use a per-method whitelist constant + `SharedPrivate.assert_valid_opts!`:
|
|
373
|
+
|
|
374
|
+
```ruby
|
|
375
|
+
PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
|
|
376
|
+
private_constant :PULL_ALLOWED_OPTS
|
|
377
|
+
|
|
378
|
+
def pull(remote = nil, branch = nil, **)
|
|
379
|
+
raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
|
|
380
|
+
|
|
381
|
+
SharedPrivate.assert_valid_opts!(PULL_ALLOWED_OPTS, **)
|
|
382
|
+
positional_args = [remote, branch].compact
|
|
383
|
+
Git::Commands::Pull.new(@execution_context)
|
|
384
|
+
.call(*positional_args, no_edit: true, **)
|
|
385
|
+
.stdout
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The helper's signature is `assert_valid_opts!(allowed, **opts)` — the allowed
|
|
390
|
+
set comes first as a positional argument so callers can re-forward the
|
|
391
|
+
anonymous splat (`**`) into both the assertion and the command call. Name the
|
|
392
|
+
splat (`**options`) only when the body must inspect or mutate the options
|
|
393
|
+
hash before forwarding it (see the [`commit` example](#orchestration-sequence)
|
|
394
|
+
above for that case).
|
|
395
|
+
|
|
396
|
+
Rules:
|
|
397
|
+
|
|
398
|
+
- Name the constant `<METHOD>_ALLOWED_OPTS` and mark it `private_constant`. It
|
|
399
|
+
is implementation detail, not part of the public API.
|
|
400
|
+
- Place the constant immediately before the method definition.
|
|
401
|
+
- The whitelist must match the `@option` tags in the YARD doc exactly. Reviewers
|
|
402
|
+
should verify the two lists are equal in both directions.
|
|
403
|
+
- `SharedPrivate.assert_valid_opts!` raises
|
|
404
|
+
`ArgumentError: Unknown options: <key>` for any unrecognized key. Document this
|
|
405
|
+
with `@raise [ArgumentError]` on the facade method.
|
|
406
|
+
- Every facade method that accepts an options hash **must** have a unit test
|
|
407
|
+
that passes an unknown key and expects `ArgumentError`. That test — not a
|
|
408
|
+
defensive `slice` at the call site — is what guarantees the whitelist stays
|
|
409
|
+
load-bearing under future refactors. Forward `**options` directly after the
|
|
410
|
+
assertion; do not also `slice` it (the assertion already proves every key is
|
|
411
|
+
allowed, and a second mechanism invites cargo-culting and confusion about
|
|
412
|
+
which one enforces the contract).
|
|
413
|
+
|
|
414
|
+
Even when the facade uses `**options` keyword forwarding, whitelist explicitly.
|
|
415
|
+
Relying on the command's own `ArgumentError` couples the facade contract to the
|
|
416
|
+
command's argument DSL, which is exactly what this layer exists to prevent.
|
|
417
|
+
|
|
418
|
+
### Deprecation handling
|
|
419
|
+
|
|
420
|
+
Handle deprecated option keys explicitly in the facade — never let deprecation
|
|
421
|
+
shims leak into the command class. Pattern:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
def commit(message, opts = {})
|
|
425
|
+
opts = opts.merge(message: message) if message
|
|
426
|
+
opts = deprecate_commit_no_gpg_sign_option(opts)
|
|
427
|
+
opts = deprecate_commit_add_all_option(opts)
|
|
428
|
+
Git::Commands::Commit.new(@execution_context).call(no_edit: true, **opts).stdout
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
private
|
|
432
|
+
|
|
433
|
+
def deprecate_commit_no_gpg_sign_option(opts)
|
|
434
|
+
return opts unless opts.key?(:no_gpg_sign)
|
|
435
|
+
|
|
436
|
+
Git::Deprecation.warn(
|
|
437
|
+
"Git::Repository#commit's :no_gpg_sign option is deprecated. " \
|
|
438
|
+
'Use gpg_sign: false instead.'
|
|
439
|
+
)
|
|
440
|
+
opts.dup.tap do |o|
|
|
441
|
+
o[:gpg_sign] = false unless o.key?(:gpg_sign)
|
|
442
|
+
o.delete(:no_gpg_sign)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### Defaults and policy options
|
|
448
|
+
|
|
449
|
+
The facade is where **policy defaults** are applied — options that support
|
|
450
|
+
non-interactive execution, control output format for parsing, or set safe
|
|
451
|
+
command-level defaults. The command class stays neutral; the facade makes the
|
|
452
|
+
defaults explicit.
|
|
453
|
+
|
|
454
|
+
There are two categories of policy defaults:
|
|
455
|
+
|
|
456
|
+
**Fixed policy defaults** are set unconditionally and are NOT included in the
|
|
457
|
+
method's `ALLOWED_OPTS` constant. `assert_valid_opts!` rejects any
|
|
458
|
+
caller-supplied value for these keys before it reaches the command call,
|
|
459
|
+
enforcing the policy. They are not part of the facade's public API and must
|
|
460
|
+
not be documented as `@option` tags.
|
|
461
|
+
|
|
462
|
+
| Policy option | Why facade sets it |
|
|
463
|
+
| --- | --- |
|
|
464
|
+
| `no_edit: true` | Subprocesses cannot launch `$EDITOR` |
|
|
465
|
+
| `no_progress: true` | Progress output goes to stderr and pollutes parsing |
|
|
466
|
+
| `no_color: true` | ANSI escapes interfere with parsers |
|
|
467
|
+
| `format: Git::Parsers::Foo::FORMAT_STRING` | Facade wants a parseable format |
|
|
468
|
+
|
|
469
|
+
**Overridable policy defaults** are included in `ALLOWED_OPTS`. The facade
|
|
470
|
+
sets a sensible default but callers may override it. Place these **before**
|
|
471
|
+
the caller's `**opts` in the command call so the caller's value wins on key
|
|
472
|
+
collision, and document them as `@option` tags since they are part of the
|
|
473
|
+
public API:
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
# :verbose is in ALLOWED_OPTS — caller can pass verbose: true to override
|
|
477
|
+
Git::Commands::Log.new(@execution_context).call(*args, verbose: false, **opts)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
## Internal helpers and encapsulation
|
|
481
|
+
|
|
482
|
+
Topic modules under `lib/git/repository/` often share helper logic — option
|
|
483
|
+
validation, path normalization, deprecation warnings, error wrapping. These
|
|
484
|
+
helpers must be reachable from any topic module without leaking onto the public
|
|
485
|
+
`Git::Repository` API surface.
|
|
486
|
+
|
|
487
|
+
### The rule
|
|
488
|
+
|
|
489
|
+
**Do not put shared helpers as private methods on `Git::Repository`** (directly
|
|
490
|
+
or via `include`). `include` copies private instance methods onto the host class,
|
|
491
|
+
so any caller with a `Git::Repository` instance can `repo.send(:helper, ...)`.
|
|
492
|
+
This:
|
|
493
|
+
|
|
494
|
+
1. Re-creates the `Git::Lib` god-class problem the redesign is escaping.
|
|
495
|
+
2. Couples every topic module silently to ambient mixin state.
|
|
496
|
+
3. Is not actually private and not `@api`-marked, so YARD/tooling cannot enforce it.
|
|
497
|
+
|
|
498
|
+
### The pattern
|
|
499
|
+
|
|
500
|
+
Put shared helpers in a sibling **internal module** under `lib/git/repository/`
|
|
501
|
+
that is **not** `include`d into `Git::Repository`. Use `module_function` so
|
|
502
|
+
methods are called as singleton methods from the topic modules:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
# lib/git/repository/shared_private.rb
|
|
506
|
+
module Git
|
|
507
|
+
class Repository
|
|
508
|
+
# Namespace for internal helpers shared across facade topic modules
|
|
509
|
+
#
|
|
510
|
+
# @api private
|
|
511
|
+
#
|
|
512
|
+
module SharedPrivate
|
|
513
|
+
module_function
|
|
514
|
+
|
|
515
|
+
def assert_valid_opts!(allowed, **options)
|
|
516
|
+
unknown = options.keys - allowed
|
|
517
|
+
return if unknown.empty?
|
|
518
|
+
|
|
519
|
+
raise ArgumentError, "Unknown options: #{unknown.join(', ')}"
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
private_constant :SharedPrivate
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Call sites use the short unqualified form (since the constant is private,
|
|
529
|
+
fully-qualified external references are not possible):
|
|
530
|
+
|
|
531
|
+
```ruby
|
|
532
|
+
# lib/git/repository/staging.rb
|
|
533
|
+
def add(paths = '.', **)
|
|
534
|
+
SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
|
|
535
|
+
Git::Commands::Add.new(@execution_context)
|
|
536
|
+
.call(*Array(paths), **)
|
|
537
|
+
.stdout
|
|
538
|
+
end
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Why this works
|
|
542
|
+
|
|
543
|
+
- **No mixin pollution.** `Git::Repository` instances do not gain
|
|
544
|
+
`assert_valid_opts!` as a method. The helper is namespaced and private-by-API.
|
|
545
|
+
- **Explicit dependency.** A reader of `staging.rb` sees exactly where the helper
|
|
546
|
+
comes from. No magic mixin chain.
|
|
547
|
+
- **Stateless by contract.** Without `include`, helpers cannot access
|
|
548
|
+
`@execution_context` or other instance state — they must take everything as
|
|
549
|
+
arguments. This keeps them pure and trivially unit-testable.
|
|
550
|
+
- **Truly private constant.** `private_constant :SharedPrivate` causes
|
|
551
|
+
fully-qualified external references (`Git::Repository::SharedPrivate`) to
|
|
552
|
+
raise a `NameError` at runtime. Callers inside the `Git::Repository` class
|
|
553
|
+
body (i.e. the topic modules) use the short `SharedPrivate.foo(...)` form
|
|
554
|
+
and are unaffected.
|
|
555
|
+
|
|
556
|
+
### Naming rules
|
|
557
|
+
|
|
558
|
+
**Topic modules** (those `include`d in `Git::Repository`) follow the two-tier
|
|
559
|
+
naming convention in [Naming a new topic module](#naming-a-new-topic-module):
|
|
560
|
+
gerund for single-action modules, `Noun + Operations` for mixed-bag modules.
|
|
561
|
+
|
|
562
|
+
**Internal helper modules** (those **not** `include`d) use descriptive nouns,
|
|
563
|
+
never generic role-suffixes like `*Helpers`, `*Utils`, or `*Support`. The
|
|
564
|
+
`*Operations` suffix is a topic-module convention — it is not a generic role
|
|
565
|
+
suffix and is not prohibited here:
|
|
566
|
+
|
|
567
|
+
| Module | Distinguished by |
|
|
568
|
+
| -------------------------------------------------- | -------------------------------------------------- |
|
|
569
|
+
| `Git::Repository::Staging` | `include`d, `@api public` (gerund topic module) |
|
|
570
|
+
| `Git::Repository::Branching` | `include`d, `@api public` (gerund topic module) |
|
|
571
|
+
| `Git::Repository::RemoteOperations` | `include`d, `@api public` (`*Operations` topic module) |
|
|
572
|
+
| `Git::Repository::SharedPrivate` | not `include`d, `@api private`, `private_constant` |
|
|
573
|
+
| `Git::Repository::SharedPrivate::OptionValidation` | nested under `SharedPrivate`, `@api private` |
|
|
574
|
+
|
|
575
|
+
Reasons:
|
|
576
|
+
|
|
577
|
+
- The location (`lib/git/repository/`) plus the `@api` tag and absence of an
|
|
578
|
+
`include` line in `lib/git/repository.rb` already convey API status. A
|
|
579
|
+
`*Helpers` suffix is redundant signage.
|
|
580
|
+
- Symmetry with topic modules keeps the directory listing readable.
|
|
581
|
+
- "Helpers" invites junk-drawer dumping; responsibility-named modules invite
|
|
582
|
+
cohesion. Ruby stdlib follows the same convention (`URI::DEFAULT_PARSER`,
|
|
583
|
+
`ActiveSupport::Inflector`, not `*Helpers`).
|
|
584
|
+
|
|
585
|
+
### Growth path
|
|
586
|
+
|
|
587
|
+
**Every time a new method is added to `SharedPrivate`**, count the total methods
|
|
588
|
+
and look for sub-themes. If **either** trigger fires, extract before committing
|
|
589
|
+
the new method:
|
|
590
|
+
|
|
591
|
+
1. `SharedPrivate` would exceed ~5 methods after the addition.
|
|
592
|
+
2. Clear sub-themes are visible (validation vs. normalization vs. error wrapping).
|
|
593
|
+
|
|
594
|
+
Then extract responsibility-named submodules **nested under `SharedPrivate`**:
|
|
595
|
+
|
|
596
|
+
```text
|
|
597
|
+
Git::Repository::SharedPrivate # catch-all (initial)
|
|
598
|
+
↓ grows / develops sub-themes
|
|
599
|
+
Git::Repository::SharedPrivate::OptionValidation # extracted by responsibility
|
|
600
|
+
Git::Repository::SharedPrivate::PathNormalization
|
|
601
|
+
Git::Repository::SharedPrivate # remaining miscellany (or deleted)
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Submodules live in `lib/git/repository/shared_private/option_validation.rb`.
|
|
605
|
+
They do not need their own `private_constant` since the parent is already
|
|
606
|
+
private. The extraction is mechanical — call sites change from
|
|
607
|
+
`SharedPrivate.foo(...)` to `SharedPrivate::OptionValidation.foo(...)`.
|
|
608
|
+
|
|
609
|
+
### Decision 1 — Where does the helper live?
|
|
610
|
+
|
|
611
|
+
Choose placement by working through these questions in order:
|
|
612
|
+
|
|
613
|
+
1. **Is it used by only one topic module?** → `module Private` nested inside
|
|
614
|
+
that topic module.
|
|
615
|
+
2. **Is it used by two or more modules, and does an existing `SharedPrivate::*`
|
|
616
|
+
submodule cover this concern?** → Add the method to that submodule.
|
|
617
|
+
3. **Is it used by two or more modules, and no fitting submodule exists?** →
|
|
618
|
+
Add it directly to `SharedPrivate`, then apply the growth-path check (see
|
|
619
|
+
[Growth path](#growth-path)) to decide whether a new submodule is now
|
|
620
|
+
warranted.
|
|
621
|
+
|
|
622
|
+
| Condition | Placement |
|
|
623
|
+
| --- | --- |
|
|
624
|
+
| Used by one topic module only | `module Private` nested inside that topic module |
|
|
625
|
+
| Shared; fitting `SharedPrivate::*` submodule exists | That submodule (e.g. `SharedPrivate::OptionValidation`) |
|
|
626
|
+
| Shared; no fitting submodule exists | `SharedPrivate` directly |
|
|
627
|
+
|
|
628
|
+
**Nested `module Private`** — for helpers local to one topic module.
|
|
629
|
+
Nest it inside the topic module, mark it `@api private`, and call its methods
|
|
630
|
+
as `Private.foo(...)`:
|
|
631
|
+
|
|
632
|
+
```ruby
|
|
633
|
+
module Git
|
|
634
|
+
class Repository
|
|
635
|
+
module Branching
|
|
636
|
+
# ... public facade methods ...
|
|
637
|
+
|
|
638
|
+
# Helpers private to the `Branching` topic module
|
|
639
|
+
#
|
|
640
|
+
# @api private
|
|
641
|
+
module Private
|
|
642
|
+
module_function
|
|
643
|
+
|
|
644
|
+
# Translates checkout options into git command arguments
|
|
645
|
+
#
|
|
646
|
+
# @param branch [String, nil] the target branch name
|
|
647
|
+
#
|
|
648
|
+
# @param options [Hash] caller-supplied checkout options
|
|
649
|
+
#
|
|
650
|
+
# @return [Array] a two-element tuple of translated arguments
|
|
651
|
+
#
|
|
652
|
+
# @api private
|
|
653
|
+
def translate_checkout_opts(branch, options)
|
|
654
|
+
# ...
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
private_constant :Private
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**Existing `SharedPrivate::*` submodule** — check
|
|
664
|
+
`lib/git/repository/shared_private/` for a file whose name matches the
|
|
665
|
+
concern (e.g. `option_validation.rb`). If one exists, add the method there.
|
|
666
|
+
|
|
667
|
+
**`SharedPrivate` directly** — when no fitting submodule exists yet.
|
|
668
|
+
See [The pattern](#the-pattern) for the full skeleton. Call sites use
|
|
669
|
+
`SharedPrivate.foo(...)`. After adding, re-run the growth-path check.
|
|
670
|
+
|
|
671
|
+
### Decision 2 — How is state passed to the helper?
|
|
672
|
+
|
|
673
|
+
`module_function` helpers (whether in `module Private` or `SharedPrivate`) are
|
|
674
|
+
stateless by design — they cannot access `@execution_context` or any other
|
|
675
|
+
instance state. This is intentional: stateless helpers are trivially
|
|
676
|
+
unit-testable and have no hidden dependencies.
|
|
677
|
+
|
|
678
|
+
When a helper needs state, pass it explicitly rather than making the helper
|
|
679
|
+
stateful:
|
|
680
|
+
|
|
681
|
+
1. **Pass state as an argument** — the preferred approach. Add the execution
|
|
682
|
+
context (or whatever state is needed) as a positional argument:
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
module Private
|
|
686
|
+
module_function
|
|
687
|
+
|
|
688
|
+
def build_result(execution_context, name)
|
|
689
|
+
Git::Commands::Foo.new(execution_context).call(name)
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
2. **Extract a PORO** — when the helper has enough state and behavior to
|
|
695
|
+
justify its own object. Place it under `lib/git/repository/`, mark it
|
|
696
|
+
`@api private`, and do not `include` it. In `lib/git/repository/commit_operation.rb`:
|
|
697
|
+
|
|
698
|
+
```ruby
|
|
699
|
+
# Callable helper for executing a git commit
|
|
700
|
+
#
|
|
701
|
+
# @api private
|
|
702
|
+
class Git::Repository::CommitOperation
|
|
703
|
+
def initialize(execution_context)
|
|
704
|
+
@execution_context = execution_context
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def call(...)
|
|
708
|
+
# ...
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Avoid: inline private instance methods directly on the topic module (i.e.,
|
|
714
|
+
`def` after `private` in the module body without a `Private` namespace). This
|
|
715
|
+
pollutes the `Git::Repository` instance namespace with methods reachable via
|
|
716
|
+
`repo.send(:helper, ...)`, which is the exact problem these patterns exist to
|
|
717
|
+
prevent.
|
|
718
|
+
|
|
719
|
+
### Why not `ActiveSupport::Concern`?
|
|
720
|
+
|
|
721
|
+
`Concern` does not solve the include-time leak: a `Concern` `include`d into a
|
|
722
|
+
class still copies its private instance methods onto that class. Rails tolerates
|
|
723
|
+
the leak via underscore prefixes and `:nodoc:`, or extracts service objects.
|
|
724
|
+
This project takes the stricter approach (sibling module + `module_function`)
|
|
725
|
+
without depending on `activesupport`.
|
|
726
|
+
|
|
727
|
+
## Parser vs. raw stdout
|
|
728
|
+
|
|
729
|
+
| Situation | Use |
|
|
730
|
+
| --- | --- |
|
|
731
|
+
| The facade returns a `String` of git's stdout (chomped or as-is) | `.stdout` |
|
|
732
|
+
| The facade returns a structured object built from line-by-line parsing | A `Git::Parsers::*` class |
|
|
733
|
+
| The facade returns a single bool/int derived from output | Inline transformation in the facade |
|
|
734
|
+
| The facade returns a `Git::CommandLineResult` | Return the raw result |
|
|
735
|
+
|
|
736
|
+
If the parsing logic exceeds ~5 lines, extract it into a `Git::Parsers::*` class
|
|
737
|
+
and call it from the facade. The facade method remains an orchestration sequence,
|
|
738
|
+
not a parser.
|
|
739
|
+
|
|
740
|
+
## Result-class factory methods
|
|
741
|
+
|
|
742
|
+
When the facade returns a domain object (e.g. `BranchInfo`, `BranchDeleteResult`,
|
|
743
|
+
`DiffResult`), use a factory method on the result class rather than constructing
|
|
744
|
+
it inline:
|
|
745
|
+
|
|
746
|
+
```ruby
|
|
747
|
+
def branch_delete(name, force: false)
|
|
748
|
+
result = Git::Commands::Branch::Delete.new(@execution_context).call(name, force: force)
|
|
749
|
+
Git::BranchDeleteResult.from(name: name, command_result: result)
|
|
750
|
+
end
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
This keeps result-object construction in one place per type and makes parsers
|
|
754
|
+
reusable across facade methods.
|
|
755
|
+
|
|
756
|
+
## Common failures
|
|
757
|
+
|
|
758
|
+
### One-line delegation when orchestration is needed
|
|
759
|
+
|
|
760
|
+
If the facade method discards information the caller documented as part of the
|
|
761
|
+
return type (e.g. returns `result.stdout` when the caller expects a parsed Hash),
|
|
762
|
+
the one-line form is wrong. Expand to the orchestration sequence and call the
|
|
763
|
+
appropriate parser.
|
|
764
|
+
|
|
765
|
+
### Leaking command-class types into the public API
|
|
766
|
+
|
|
767
|
+
The public return type should never be `Git::Commands::Foo::Bar::SomeResult` or
|
|
768
|
+
any other type from `Git::Commands::*`. Returning `Git::CommandLineResult` is
|
|
769
|
+
acceptable when the topic module documents that as its contract, but is not the
|
|
770
|
+
default — see [Choosing the return type](#choosing-the-return-type). Returning
|
|
771
|
+
domain objects (`Git::BranchInfo`, `Git::DiffResult`, etc.) is preferred for
|
|
772
|
+
methods that produce structured data.
|
|
773
|
+
|
|
774
|
+
### Exposing command-DSL-shaped argv in the facade signature
|
|
775
|
+
|
|
776
|
+
The facade signature is a Ruby API, not a transcription of the git CLI. Accepting
|
|
777
|
+
a free-form `*args` that is forwarded straight to the command, or naming keyword
|
|
778
|
+
arguments after CLI flags (e.g. `no_ff:`, `set_upstream_to:`) instead of
|
|
779
|
+
Ruby-idiomatic names, leaks the command DSL into the public contract. Define the
|
|
780
|
+
signature from the caller's perspective; translate to command DSL inside the
|
|
781
|
+
body.
|
|
782
|
+
|
|
783
|
+
### Changing the legacy return type or signature on extraction
|
|
784
|
+
|
|
785
|
+
When a facade method is extracted from `Git::Base` / `Git::Lib`, returning a
|
|
786
|
+
different type, accepting a different positional/keyword shape, or changing
|
|
787
|
+
nil-handling silently breaks every caller. Capture the legacy contract in the
|
|
788
|
+
Step 2 plan and diff before/after — see
|
|
789
|
+
[Choosing the return type](#choosing-the-return-type) rule 1 and
|
|
790
|
+
[Choosing the method signature](#choosing-the-method-signature) rule 1.
|
|
791
|
+
|
|
792
|
+
### Bypassing `@execution_context`
|
|
793
|
+
|
|
794
|
+
Constructing a command with anything other than `@execution_context` (e.g.
|
|
795
|
+
`Git::Commands::Add.new(self)` from inside a facade method) is wrong. The facade
|
|
796
|
+
holds a `Git::ExecutionContext::Repository`; commands must always be constructed
|
|
797
|
+
with it.
|
|
798
|
+
|
|
799
|
+
### Placing an overridable policy default after caller options
|
|
800
|
+
|
|
801
|
+
This anti-pattern applies to **overridable** policy defaults (those in `ALLOWED_OPTS`).
|
|
802
|
+
Placing the default after `**opts` silently overwrites the caller's explicit value:
|
|
803
|
+
|
|
804
|
+
```ruby
|
|
805
|
+
# ❌ Wrong — caller's :verbose is silently discarded
|
|
806
|
+
Git::Commands::Log.new(@execution_context).call(*args, **opts, verbose: false)
|
|
807
|
+
|
|
808
|
+
# ✅ Correct — caller's :verbose wins because opts is splatted last
|
|
809
|
+
Git::Commands::Log.new(@execution_context).call(*args, verbose: false, **opts)
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Place overridable policy defaults before the caller's `**opts` so the caller's
|
|
813
|
+
value wins on key collision. This does not apply to fixed policy options (not in
|
|
814
|
+
`ALLOWED_OPTS`): `assert_valid_opts!` prevents those keys from reaching the
|
|
815
|
+
command call at all.
|
|
816
|
+
|
|
817
|
+
### Skipping option whitelisting on opaque opts hashes
|
|
818
|
+
|
|
819
|
+
If the facade accepts an options hash (positional `opts = {}` *or* keyword
|
|
820
|
+
`**options`), it must call `SharedPrivate.assert_valid_opts!` against a
|
|
821
|
+
`private_constant`-marked `<METHOD>_ALLOWED_OPTS` constant. Without it,
|
|
822
|
+
callers can silently pass any key the command DSL happens to accept, which is
|
|
823
|
+
API expansion that the facade did not commit to.
|
|
824
|
+
|
|
825
|
+
### Mixing facade and command responsibilities
|
|
826
|
+
|
|
827
|
+
The facade does not build CLI argv. The command does not pre-process Ruby
|
|
828
|
+
arguments or parse output. If a facade method calls `command(...)` directly
|
|
829
|
+
(rather than `Git::Commands::*.new(...).call(...)`) it is bypassing the command
|
|
830
|
+
layer; refactor by introducing or extending the appropriate command class first.
|
|
831
|
+
|
|
832
|
+
### Adding a topic module whose methods fit an existing one
|
|
833
|
+
|
|
834
|
+
A topic module is not justified when its method(s) would fit naturally in an
|
|
835
|
+
existing module. Before creating a new module, scan existing modules for a
|
|
836
|
+
plausible home. The absence of a method-count threshold is not an invitation
|
|
837
|
+
to fragment the API — place the method in the closest existing module unless
|
|
838
|
+
the topic is genuinely distinct and the method would be awkward there. See
|
|
839
|
+
[Decision rules for adding a new module](#decision-rules-for-adding-a-new-module)
|
|
840
|
+
for the full two-criteria test.
|