git 5.1.0 → 5.2.0

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