git 5.0.0.beta.1 → 5.0.0.beta.2

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +6 -0
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  5. data/.github/skills/extract-facade-from-base-lib/SKILL.md +28 -14
  6. data/.github/skills/facade-implementation/SKILL.md +14 -0
  7. data/.github/skills/facade-test-conventions/SKILL.md +14 -0
  8. data/.rubocop.yml +5 -0
  9. data/README.md +51 -11
  10. data/UPGRADING.md +141 -0
  11. data/git.gemspec +5 -0
  12. data/lib/git/branch.rb +7 -18
  13. data/lib/git/branches.rb +2 -10
  14. data/lib/git/command_line/base.rb +10 -0
  15. data/lib/git/command_line/capturing.rb +5 -3
  16. data/lib/git/command_line/streaming.rb +5 -3
  17. data/lib/git/command_line.rb +3 -3
  18. data/lib/git/commands/base.rb +7 -6
  19. data/lib/git/commands/cat_file/batch.rb +6 -1
  20. data/lib/git/commands/cat_file/raw.rb +7 -1
  21. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +5 -0
  22. data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
  23. data/lib/git/commands/update_ref/batch.rb +1 -1
  24. data/lib/git/commands/version.rb +5 -0
  25. data/lib/git/commands.rb +5 -7
  26. data/lib/git/config.rb +17 -0
  27. data/lib/git/config_entry_info.rb +106 -0
  28. data/lib/git/configuring.rb +665 -0
  29. data/lib/git/deprecation.rb +9 -0
  30. data/lib/git/diff.rb +4 -8
  31. data/lib/git/diff_path_status.rb +2 -13
  32. data/lib/git/diff_stats.rb +1 -9
  33. data/lib/git/execution_context/global.rb +3 -28
  34. data/lib/git/execution_context/repository.rb +30 -41
  35. data/lib/git/execution_context.rb +43 -24
  36. data/lib/git/log.rb +3 -9
  37. data/lib/git/object.rb +14 -21
  38. data/lib/git/parsers/config_entry.rb +110 -0
  39. data/lib/git/parsers/ls_remote.rb +79 -0
  40. data/lib/git/remote.rb +7 -20
  41. data/lib/git/repository/branching.rb +183 -12
  42. data/lib/git/repository/committing.rb +64 -68
  43. data/lib/git/repository/configuring.rb +208 -13
  44. data/lib/git/repository/context_helpers.rb +264 -0
  45. data/lib/git/repository/factories.rb +682 -0
  46. data/lib/git/repository/inspecting.rb +99 -0
  47. data/lib/git/repository/maintenance.rb +65 -0
  48. data/lib/git/repository/merging.rb +63 -1
  49. data/lib/git/repository/object_operations.rb +133 -35
  50. data/lib/git/repository/path_resolver.rb +1 -1
  51. data/lib/git/repository/remote_operations.rb +166 -21
  52. data/lib/git/repository/staging.rb +187 -23
  53. data/lib/git/repository/stashing.rb +39 -3
  54. data/lib/git/repository/status_operations.rb +21 -0
  55. data/lib/git/repository.rb +68 -129
  56. data/lib/git/stash.rb +2 -9
  57. data/lib/git/stashes.rb +2 -7
  58. data/lib/git/status.rb +8 -17
  59. data/lib/git/version.rb +2 -2
  60. data/lib/git/worktree.rb +2 -15
  61. data/lib/git/worktrees.rb +2 -15
  62. data/lib/git.rb +180 -77
  63. data/redesign/3_architecture_implementation.md +148 -111
  64. data/redesign/Phase 4 - Step A.md +360 -0
  65. data/redesign/beta_release.md +107 -0
  66. data/redesign/c1c2_audit.md +566 -0
  67. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  68. data/redesign/config_design.rb +501 -0
  69. metadata +19 -5
  70. data/lib/git/base.rb +0 -1204
  71. data/lib/git/lib.rb +0 -2855
@@ -0,0 +1,626 @@
1
+ # Bucket 6 Decision Brief — `Git::Lib` Orphaned Public Methods
2
+
3
+ **Date:** 2026-06-09
4
+
5
+ **Branch:** `agents/redesignc1c2bucket6decisionbrief`
6
+
7
+ **Companion to:** `redesign/c1c2_audit.md` §7.3
8
+
9
+ - [1. Purpose](#1-purpose)
10
+ - [2. The ❌ Confirmed Removal](#2-the--confirmed-removal)
11
+ - [`list_files(ref_dir)`](#list_filesref_dir)
12
+ - [3. Decision Items](#3-decision-items)
13
+ - [3.1 `change_head_branch(branch_name)`](#31-change_head_branchbranch_name)
14
+ - [3.2 `config_get(name)`, `config_list`, and `config_set(name, value, options = {})`](#32-config_getname-config_list-and-config_setname-value-options--)
15
+ - [`config_get(name)`](#config_getname)
16
+ - [`config_list`](#config_list)
17
+ - [`config_set(name, value, options = {})`](#config_setname-value-options--)
18
+ - [3.3 `global_config_get(name)`, `global_config_list`, and `global_config_set(name, value)`](#33-global_config_getname-global_config_list-and-global_config_setname-value)
19
+ - [`global_config_get(name)`](#global_config_getname)
20
+ - [`global_config_list`](#global_config_list)
21
+ - [`global_config_set(name, value)`](#global_config_setname-value)
22
+ - [3.4 `parse_config(file)`](#34-parse_configfile)
23
+ - [3.5 `stash_list`](#35-stash_list)
24
+ - [3.6 `unmerged`](#36-unmerged)
25
+ - [4. Pre-resolved questions](#4-pre-resolved-questions)
26
+ - [4.1 Global config API surface breadth (§3.3) — **Resolved**](#41-global-config-api-surface-breadth-33--resolved)
27
+ - [4.2 `change_head_branch` guard semantics (§3.1) — **Resolved**](#42-change_head_branch-guard-semantics-31--resolved)
28
+ - [4.3 `config()` overload complexity with `:file` for reads (§3.4) — **Resolved**](#43-config-overload-complexity-with-file-for-reads-34--resolved)
29
+ - [4.4 Semver classification of `stash_list` non-promotion (§3.5) — **Resolved**](#44-semver-classification-of-stash_list-non-promotion-35--resolved)
30
+ - [5. Next Steps](#5-next-steps)
31
+
32
+ ---
33
+
34
+ ## 1. Purpose
35
+
36
+ This document is the companion decision brief for the 10 🔍 human-decision
37
+ items and 1 ❌ confirmed removal identified in §7.3 of
38
+ `redesign/c1c2_audit.md`. Each of those items is a public method on
39
+ `Git::Lib` that has no existing `Git::Repository` facade and therefore has no
40
+ safe landing place when `Git::Lib` is deleted in Phase 4. Before the final
41
+ Bucket 6 remediation work (PR 5h+) can begin, a human reviewer must examine
42
+ each item, evaluate the concrete options provided below, and record a binding
43
+ decision in the **Decision** field of the relevant section. Once all 11 items
44
+ are resolved, this document serves as the authoritative specification for
45
+ whoever opens PR 5h.
46
+
47
+ ---
48
+
49
+ ## 2. The ❌ Confirmed Removal
50
+
51
+ ### `list_files(ref_dir)`
52
+
53
+ **Current implementation:** Constructs the path
54
+ `File.join(@git_dir, 'refs', ref_dir)` and returns
55
+ `Dir.glob('**/*', base: dir).select { |f| File.file?(...) }` — a raw
56
+ filesystem walk over the `.git/refs/` directory tree. No git command is
57
+ invoked; this is pure internal plumbing that bypasses the git object model
58
+ entirely.
59
+
60
+ **Why it must go:** There is no plausible clean public use for directly
61
+ walking `.git/refs/`. Callers who need to enumerate refs should use the
62
+ `git for-each-ref` family of commands, which is already surfaced via
63
+ `Git::Repository::Branching`, `Git::Repository::ObjectOperations`, and the
64
+ various ref-inspection methods on `Git::Repository`. Exposing filesystem
65
+ access to the packed-refs store would be misleading and would silently break
66
+ on repositories that use the packed-refs file instead of loose ref files.
67
+
68
+ **Upgrade note for callers:**
69
+
70
+ > Direct `repo.lib.list_files` calls are unsupported. Use
71
+ > `Git::Repository` ref-inspection methods (e.g., `branches`, `tags`,
72
+ > `remotes`) instead.
73
+
74
+ **Action required:** Add this note to `UPGRADING.md`. No migration
75
+ path is provided.
76
+
77
+ ---
78
+
79
+ ## 3. Decision Items
80
+
81
+ ### 3.1 `change_head_branch(branch_name)`
82
+
83
+ **Signature:** `change_head_branch(branch_name)`
84
+
85
+ **Description:** Calls
86
+ `Git::Commands::SymbolicRef::Update.new(self).call('HEAD', "refs/heads/#{branch_name}")`,
87
+ which is a direct write of the `HEAD` symbolic reference to
88
+ `refs/heads/<branch_name>`. This is equivalent to
89
+ `git symbolic-ref HEAD refs/heads/<name>` and is how git implements branch
90
+ renaming and orphan-branch checkout under the hood. `Git::Lib` uses it
91
+ internally in at least two workflows. There is no `Git::Base` delegator.
92
+
93
+ **Context:** No existing `Git::Repository` facade covers this operation.
94
+ The `Git::Commands::SymbolicRef::Update` command class already exists and is
95
+ wired correctly. The operation is low-level and requires caller awareness: pointing HEAD at a
96
+ branch that does not yet exist places the repository in unborn-branch state,
97
+ which is valid and intentional for initialization workflows but unexpected if
98
+ done by mistake. It is exactly what external tooling (e.g., repository
99
+ initialization scripts, branch-rename utilities) needs when git's own
100
+ higher-level commands are not available. No test-suite usage was found for
101
+ direct `lib.change_head_branch` calls, which suggests no external callers
102
+ have relied on it via `g.lib`.
103
+
104
+ **Options:**
105
+
106
+ - **A — Promote to `Git::Repository::Branching`:** Add a public facade
107
+ method `change_head_branch(branch_name)` with appropriate YARD docs and a
108
+ `Git::Base` delegator. Surface the footgun risk in the documentation.
109
+ - **B — Mark as internal:** Leave the method behind `private` in `Git::Lib`
110
+ and document that it has no public equivalent. Callers requiring this
111
+ low-level operation must use `git symbolic-ref` directly.
112
+ - **C — Promote with a guard:** Promote as Option A but add a guard that
113
+ raises `ArgumentError` when the target branch does not exist locally.
114
+
115
+ **Recommendation:** **Option A.** The operation has genuine tooling utility
116
+ and the command class is already in place, making the implementation cost
117
+ low. No guard should be added: the unborn-branch workflow — pointing HEAD
118
+ at a branch that does not yet exist before any commits land — is a
119
+ first-class git pattern used by repository initialization tooling. A guard
120
+ that rejects non-existent branches would silently break that workflow (see
121
+ §4.2). Promote to `Git::Repository::Branching` as a clean pass-through with
122
+ clear documentation explaining unborn-branch semantics.
123
+
124
+ **Decision:** Accepted. Promote `change_head_branch(branch_name)` to `Git::Repository::Branching` with a `Git::Base` delegator. No guard. Document unborn-branch semantics in YARD.
125
+
126
+ **Status:** ✅ Implemented — facade in `Git::Repository::Branching` + `Git::Base` delegator added (PR 5h-1).
127
+
128
+ ---
129
+
130
+ ### 3.2 `config_get(name)`, `config_list`, and `config_set(name, value, options = {})`
131
+
132
+ These three are treated together because they are the low-level primitives
133
+ that the existing `Git::Repository::Configuring#config()` facade already
134
+ dispatches to internally. The key question is whether to expose them as
135
+ additional public methods or to clarify that `config()` already covers all
136
+ three cases.
137
+
138
+ #### `config_get(name)`
139
+
140
+ **Signature:** `config_get(name)`
141
+
142
+ **Description:** Calls `Git::Commands::ConfigOptionSyntax::Get`, raises
143
+ `Git::FailedError` if the exit status is non-zero, and returns the raw
144
+ stdout string (the config value, newline-stripped).
145
+
146
+ **Context:** `Git::Repository::Configuring#config(name)` (one-argument form)
147
+ already calls the identical private helper `Private.config_get` with
148
+ identical behavior. The two implementations are functionally the same.
149
+ There is no `Git::Base` delegator.
150
+
151
+ #### `config_list`
152
+
153
+ **Signature:** `config_list`
154
+
155
+ **Description:** Calls `Git::Commands::ConfigOptionSyntax::List` and parses
156
+ the output into a `Hash{String => String}` via `parse_config_list`.
157
+
158
+ **Context:** `Git::Repository::Configuring#config()` (zero-argument form)
159
+ already calls the identical private helper `Private.config_list` and returns
160
+ the same hash. Functionally identical.
161
+
162
+ #### `config_set(name, value, options = {})`
163
+
164
+ **Signature:** `config_set(name, value, options = {})`
165
+
166
+ **Description:** Validates options against `CONFIG_SET_ALLOWED_OPTS =
167
+ %i[file]` and calls `Git::Commands::ConfigOptionSyntax::Set`. The `:file`
168
+ option allows writing to an arbitrary config file rather than the default
169
+ `.git/config`. The facade `config(name, value, options = {})` also accepts
170
+ a `:file` option and calls the same `Set` command.
171
+
172
+ **Context:** `Git::Repository::Configuring#config(name, value)` (two-argument
173
+ form) already calls the identical private helper `Private.config_set` with
174
+ the same `:file` passthrough. Functionally identical.
175
+
176
+ **Options for all three:**
177
+
178
+ - **A — Mark as covered; no new public methods:** Document in the upgrade
179
+ guide that `config_get`, `config_list`, and `config_set` are now accessed
180
+ via the unified `config()` facade, and that callers should migrate to
181
+ `repo.config`, `repo.config(name)`, and `repo.config(name, value)`
182
+ respectively.
183
+ - **B — Promote all three as public aliases:** Add `config_get`, `config_list`,
184
+ and `config_set` as public methods on `Git::Repository::Configuring` that
185
+ simply delegate to the private helpers, giving callers a named-method API.
186
+ - **C — Partial promotion:** Promote only `config_set` (since it takes extra
187
+ options that users may wish to call explicitly) while documenting that
188
+ `config_get` and `config_list` are covered by the overloaded `config()`.
189
+
190
+ **Recommendation:** **Deprecated forwarding aliases over `config()`, with Option A as the canonical v5 API.**
191
+ The `config()` facade is already a clean, well-documented, three-overload API
192
+ that covers every case. Adding parallel methods with the same behavior would
193
+ create API surface duplication without benefit in the long term. However, to
194
+ preserve backward compatibility, also add `config_get`, `config_list`, and
195
+ `config_set` as deprecated forwarding methods on `Git::Repository::Configuring`
196
+ (and corresponding `Git::Base` delegators), each emitting a runtime warning
197
+ via `Git::Deprecation.warn`:
198
+
199
+ ```ruby
200
+ # @deprecated Use {#config} instead.
201
+ def config_get(name)
202
+ Git::Deprecation.warn(
203
+ 'config_get is deprecated and will be removed in a future version. ' \
204
+ 'Use config(name) instead.'
205
+ )
206
+ config(name)
207
+ end
208
+
209
+ # @deprecated Use {#config} instead.
210
+ def config_list
211
+ Git::Deprecation.warn(
212
+ 'config_list is deprecated and will be removed in a future version. ' \
213
+ 'Use config instead.'
214
+ )
215
+ config
216
+ end
217
+
218
+ # @deprecated Use {#config} instead.
219
+ def config_set(name, value, opts = {})
220
+ Git::Deprecation.warn(
221
+ 'config_set is deprecated and will be removed in a future version. ' \
222
+ 'Use config(name, value) instead.'
223
+ )
224
+ config(name, value, opts)
225
+ end
226
+ ```
227
+
228
+ Callers drop the `.lib.` prefix and get a runtime deprecation warning; no
229
+ method-name change is required until v6. Remove all three aliases in v6.
230
+
231
+ **Decision:** Accepted. Add deprecated forwarding methods `config_get`, `config_list`, and `config_set` to `Git::Repository::Configuring` with `Git::Deprecation.warn` calls pointing to `config()`. Add `Git::Base` delegators. Remove in v6.
232
+
233
+ **Status:** ✅ Implemented — deprecated forwarding wrappers in `Git::Repository::Configuring` + `Git::Base` delegators added (PR 5h-2).
234
+
235
+ ---
236
+
237
+ ### 3.3 `global_config_get(name)`, `global_config_list`, and `global_config_set(name, value)`
238
+
239
+ These three are treated together as the global-config variants of the local
240
+ config primitives above.
241
+
242
+ #### `global_config_get(name)`
243
+
244
+ **Signature:** `global_config_get(name)`
245
+
246
+ **Description:** Identical to `config_get` except it passes `global: true`
247
+ to `Git::Commands::ConfigOptionSyntax::Get`, targeting `~/.gitconfig` (or
248
+ `$XDG_CONFIG_HOME/git/config`) rather than the repository's `.git/config`.
249
+
250
+ #### `global_config_list`
251
+
252
+ **Signature:** `global_config_list`
253
+
254
+ **Description:** Identical to `config_list` except it passes `global: true`
255
+ to `Git::Commands::ConfigOptionSyntax::List`.
256
+
257
+ #### `global_config_set(name, value)`
258
+
259
+ **Signature:** `global_config_set(name, value)`
260
+
261
+ **Description:** Calls `Git::Commands::ConfigOptionSyntax::Set` with
262
+ `global: true`, writing to the user's global git config. Takes no `:file`
263
+ option (global mode is mutually exclusive with `--file`).
264
+
265
+ **Context:** This is a **genuine gap** in the current `Git::Repository`
266
+ API. Unlike the local config methods above, `Git::Repository::Configuring#config()`
267
+ has no `:global` option and provides no way to read or write global config
268
+ at all. Any caller currently using `g.lib.global_config_get(name)` would
269
+ lose that capability entirely under v5 with no migration path unless this
270
+ gap is addressed.
271
+
272
+ **Options:**
273
+
274
+ - **A — Add a `:global` option to `config()`:** Extend the existing facade
275
+ with `config(name = nil, value = nil, global: false, **options)`. All
276
+ three overloads (get, list, set) would gain a global variant by passing
277
+ `global: true`. Keeps the API surface minimal.
278
+ - **B — Add dedicated `global_config_get/list/set` methods:** Mirror the
279
+ `Git::Lib` names exactly as public methods on `Git::Repository::Configuring`.
280
+ More discoverable by name; no overloading.
281
+ - **C — Add a separate `global_config(name = nil, value = nil)` facade:**
282
+ A parallel unified method that follows the same dispatch logic as `config()`
283
+ but targets global config. Clean API symmetry, no keyword arg overloading.
284
+
285
+ **Recommendation:** **Option C, with backward-compatible deprecated aliases.**
286
+ A dedicated `global_config()` method with identical dispatch logic to
287
+ `config()` (list/get/set based on argument count) gives the cleanest API
288
+ symmetry. Option A risks a confusing mix of positional and keyword arguments
289
+ on an already-overloaded method. Option B re-introduces three separate names
290
+ when the project has already committed to the unified `config()` pattern.
291
+ `global_config()` is obvious, memorable, and mirrors the existing `config()`
292
+ contract exactly.
293
+
294
+ To preserve backward compatibility, also add the three `Git::Lib` names as
295
+ deprecated forwarding methods on `Git::Repository::Configuring` (and
296
+ corresponding `Git::Base` delegators):
297
+
298
+ ```ruby
299
+ # @deprecated Use {#global_config} instead.
300
+ def global_config_get(name)
301
+ Git::Deprecation.warn(
302
+ 'global_config_get is deprecated and will be removed in a future version. ' \
303
+ 'Use global_config(name) instead.'
304
+ )
305
+ global_config(name)
306
+ end
307
+
308
+ # @deprecated Use {#global_config} instead.
309
+ def global_config_list
310
+ Git::Deprecation.warn(
311
+ 'global_config_list is deprecated and will be removed in a future version. ' \
312
+ 'Use global_config instead.'
313
+ )
314
+ global_config
315
+ end
316
+
317
+ # @deprecated Use {#global_config} instead.
318
+ def global_config_set(name, value)
319
+ Git::Deprecation.warn(
320
+ 'global_config_set is deprecated and will be removed in a future version. ' \
321
+ 'Use global_config(name, value) instead.'
322
+ )
323
+ global_config(name, value)
324
+ end
325
+ ```
326
+
327
+ These can be removed in v6 after a full deprecation cycle.
328
+
329
+ **Decision:** Accepted. Add `global_config(name = nil, value = nil)` to `Git::Repository::Configuring` with three-way dispatch (list/get/set). Add deprecated forwarding methods `global_config_get`, `global_config_list`, `global_config_set` with `Git::Deprecation.warn` calls. Add `Git::Base` delegators for all four. Remove deprecated aliases in v6.
330
+
331
+ **Implemented in PR 5h-3.** `global_config` facade added to `Git::Repository::Configuring` with three-way dispatch; private helpers `global_config_get/list/set` in the `Private` module; deprecated forwarding methods on the public API; `Git::Base` delegators for all four methods.
332
+
333
+ ---
334
+
335
+ ### 3.4 `parse_config(file)`
336
+
337
+ **Signature:** `parse_config(file)`
338
+
339
+ **Description:** Calls
340
+ `Git::Commands::ConfigOptionSyntax::List.new(self).call(file: file)` and
341
+ parses the output into a `Hash{String => String}` via `parse_config_list`.
342
+ The `file` argument is an arbitrary filesystem path to any
343
+ `.gitconfig`-format file — not necessarily the repository's own config.
344
+ This is the mechanism `Git::Lib` uses internally for reading `~/.gitconfig`
345
+ or any other git-style config file by path.
346
+
347
+ **Context:** `Git::Repository::Configuring#config()` already accepts a
348
+ `:file` option in its **set** overload (to write to a custom file), but has
349
+ no `:file` support in the **get** or **list** overloads, and there is no
350
+ way to read an arbitrary config file via the current facade. This is a
351
+ partial gap: write-to-file is covered, read-from-file is not.
352
+
353
+ **Options:**
354
+
355
+ - **A — Extend `config()` with a `:file` option for read operations:**
356
+ Allow `repo.config(file: '/path')` (list-from-file) and
357
+ `repo.config('key', file: '/path')` (get-from-file). Consistent with the
358
+ existing `config()` contract and the `:file` option already used for set.
359
+ - **B — Promote as a standalone `parse_config(file)` method:** Add
360
+ `parse_config(file)` directly to `Git::Repository::Configuring` (or a
361
+ suitable module) with the same signature. Preserves backward compatibility
362
+ for any callers using `g.lib.parse_config(path)`.
363
+ - **C — Mark as internal; no public equivalent:** The use case of reading
364
+ an arbitrary git config file is niche. Callers who need it can invoke
365
+ `git config --list --file <path>` via `Git::Commands` directly.
366
+
367
+ **Recommendation:** **Option A, with a backward-compatible deprecated alias.**
368
+ The `:file` option is already part of the `config()` facade for writes;
369
+ extending it to reads creates a consistent, symmetric API. The implementation
370
+ is trivial (pass `file:` to the `Get` and `List` command calls). To preserve
371
+ backward compatibility, also add `parse_config` as a deprecated forwarding
372
+ method on `Git::Repository::Configuring` (and a corresponding `Git::Base`
373
+ delegator):
374
+
375
+ ```ruby
376
+ # @deprecated Use {#config} with the :file option instead.
377
+ def parse_config(file)
378
+ Git::Deprecation.warn(
379
+ 'parse_config is deprecated and will be removed in a future version. ' \
380
+ 'Use config(file: <path>) instead.'
381
+ )
382
+ config(file: file)
383
+ end
384
+ ```
385
+
386
+ Callers migrating from `lib.parse_config(path)` can drop to
387
+ `repo.parse_config(path)` immediately (no method-name change needed), then
388
+ migrate to `repo.config(file: path)` at their own pace. Remove in v6.
389
+
390
+ **Decision:** Accepted. Extend `Git::Repository::Configuring#config()` to accept `:file` on the get and list overloads. Add deprecated `parse_config(file)` forwarding method with `Git::Deprecation.warn` call pointing to `config(file: <path>)`. Add `Git::Base` delegator. Remove deprecated alias in v6.
391
+
392
+ **Implemented in PR 5h-4.** `config()` extended to accept `:file` on the get and list overloads; deprecated `parse_config(file)` forwarding wrapper added to `Git::Repository::Configuring` with `Git::Deprecation.warn`; `Git::Base#parse_config` delegator added.
393
+
394
+ ---
395
+
396
+ ### 3.5 `stash_list`
397
+
398
+ **Signature:** `stash_list`
399
+
400
+ **Description:** Calls `Git::Commands::Stash::List` and parses the result
401
+ via `Git::Parsers::Stash.parse_list`, then formats the structured data back
402
+ into a plain `String` of the form `"stash@{0}: On main: WIP\nstash@{1}: On
403
+ feature: test"`. This replicates the raw output of `git stash list` and
404
+ exists specifically to preserve the v4 backward-compatible return type (a
405
+ formatted string rather than structured objects).
406
+
407
+ **Context:** `Git::Repository::Stashing#stashes_all` (already promoted in
408
+ §7.2) returns `Array<[Integer, String]>` — pairs of `[index, message_string]`
409
+ where the message has the branch prefix stripped (e.g., `[0, "Fix bug"]`
410
+ rather than `"stash@{0}: WIP on main: Fix bug"`). The two methods serve
411
+ different callers: `stashes_all` is the clean v5 API returning structured
412
+ data; `stash_list` returns the full formatted-string representation that v4
413
+ callers depending on `"stash@{n}: ..."` format may depend on. There is no
414
+ `Git::Base` delegator for `stash_list`.
415
+
416
+ **Options:**
417
+
418
+ - **A — Promote `stash_list` as a deprecated method:** Add `stash_list` to
419
+ `Git::Repository::Stashing` with a `@deprecated` YARD tag pointing callers
420
+ to `stashes_all`. Keep the formatted-string behavior to avoid a breaking
421
+ change for v4 callers.
422
+ - **B — Deprecate without promotion; add upgrade note only:** Do not expose
423
+ `stash_list` on `Git::Repository`. Document in `UPGRADING.md` that
424
+ callers should migrate to `stashes_all`. This is a breaking change for
425
+ anyone calling `g.lib.stash_list` and expecting a string.
426
+ - **C — Mark as covered; no action:** Treat `stash_list` as already
427
+ superseded by `stashes_all` and silently remove it. Same breaking-change
428
+ risk as Option B but without an upgrade note.
429
+
430
+ **Recommendation:** **Option A.** The formatted-string return type is the
431
+ kind of low-effort backward-compatible shim that prevents v4 callers from
432
+ hitting a hard error during migration. The method must emit a runtime
433
+ deprecation warning via `Git::Deprecation.warn`, consistent with every other
434
+ deprecated method in the project. Because `stashes_all` returns
435
+ `[index, message_string]` pairs with a different format, `stash_list` must
436
+ call the stash command directly to preserve the `"stash@{n}: <full message>"`
437
+ format that v4 callers expect. The implementation is:
438
+
439
+ ```ruby
440
+ # @deprecated Use {#stashes_all} instead.
441
+ def stash_list
442
+ Git::Deprecation.warn(
443
+ 'stash_list is deprecated and will be removed in a future version. ' \
444
+ 'Use stashes_all instead.'
445
+ )
446
+ result = Git::Commands::Stash::List.new(@execution_context).call
447
+ stashes = Git::Parsers::Stash.parse_list(result.stdout)
448
+ stashes.map { |info| "#{info.name}: #{info.message}" }.join("\n")
449
+ end
450
+ ```
451
+
452
+ Remove the method in v6 after a full deprecation cycle.
453
+
454
+ **Decision:** Accepted. Add `stash_list` to `Git::Repository::Stashing` as a deprecated method with `Git::Deprecation.warn` pointing to `stashes_all`. Add `Git::Base` delegator. Remove in v6.
455
+
456
+ **Status: ✅ Implemented** — `Git::Repository::Stashing#stash_list` + `Git::Base#stash_list` delegator added (PR 5h-5).
457
+
458
+ ---
459
+
460
+ ### 3.6 `unmerged`
461
+
462
+ **Signature:** `unmerged`
463
+
464
+ **Description:** Calls `Git::Commands::Diff.new(self).call(cached: true)`
465
+ and scans the output for lines matching `* Unmerged path (.*)`, returning
466
+ an `Array<String>` of repository-relative file paths that have unresolved
467
+ merge conflicts. It is the same logic as
468
+ `Git::Repository::Merging::Private.unmerged_paths` — the private helper
469
+ already extracted into the `Merging` module for use by `each_conflict`.
470
+
471
+ **Context:** `Git::Repository::Merging#each_conflict` already uses
472
+ `Private.unmerged_paths` internally and yields those same paths (alongside
473
+ temporary files of staged content) to its block. If a caller only needs the
474
+ list of conflicting file paths — not the staged content — they currently have
475
+ no public API for that; they must call `each_conflict` with a block that
476
+ discards the tempfile arguments. The logic for extracting unmerged paths is
477
+ already implemented and tested inside the `Merging` module's private layer.
478
+
479
+ **Options:**
480
+
481
+ - **A — Promote `unmerged` as a public method on `Git::Repository::Merging`:**
482
+ Surface `Private.unmerged_paths` directly as `unmerged` (or
483
+ `unmerged_paths`). Add a `Git::Base` delegator. One-line implementation;
484
+ no new commands needed.
485
+ - **B — Keep `each_conflict` as the only public API:** Document that callers
486
+ who only need the path list should call `each_conflict` and collect the
487
+ first argument in the block. No new methods added.
488
+ - **C — Rename the promotion to `conflict_files` (or similar):** Promote
489
+ the functionality under a clearer name that signals it returns a list of
490
+ conflicting file paths rather than "unmerged index entries".
491
+
492
+ **Recommendation:** **Option A.** Promoting `unmerged` is low-cost (the
493
+ private helper already exists) and fills a real usability gap: forcing
494
+ callers to use `each_conflict` when they only want a path list is
495
+ unnecessarily heavyweight. The name `unmerged` has v4 precedent and is
496
+ consistent with git's own terminology (`git diff --cached` unmerged paths).
497
+ Use `unmerged` as the promoted name to preserve backward compatibility.
498
+
499
+ **Decision:** Accepted. Promote `unmerged` as a public method on `Git::Repository::Merging` by surfacing `Private.unmerged_paths`. Add `Git::Base` delegator.
500
+
501
+ **Status:** ✅ Implemented — `Git::Repository::Merging#unmerged` added; `Git::Base#unmerged` delegator added.
502
+
503
+ ## 4. Pre-resolved questions
504
+
505
+ The following questions were raised during initial drafting and are now
506
+ resolved. The answers are binding on PR 5h unless explicitly overridden.
507
+
508
+ ### 4.1 Global config API surface breadth (§3.3) — **Resolved**
509
+
510
+ **Question:** Should `global_config()` also accept a `:system` flag for
511
+ system-level config, or a `:file` option for arbitrary-path reads?
512
+
513
+ **Answer: Narrow implementation only — global list/get/set, no `:system`,
514
+ no `:file`.**
515
+
516
+ System-level config (`/etc/gitconfig`) requires elevated filesystem
517
+ permissions and belongs to git administration, not application-level
518
+ tooling. There is no credible use case for a Ruby gem managing system config.
519
+ The `:file` option for arbitrary-path reads is already covered by
520
+ `config(file: path)` per §3.4's resolution — `global_config` does not
521
+ need to duplicate it. Keeping `global_config()` narrow also avoids questions
522
+ about whether `global: true` and `file:` can be combined (they cannot; git
523
+ rejects `--global --file` together). The initial signature is:
524
+
525
+ ```ruby
526
+ global_config(name = nil, value = nil)
527
+ ```
528
+
529
+ with the same three-way dispatch as `config()`. A `:system` variant can be
530
+ added in a later minor release if demand materializes.
531
+
532
+ ---
533
+
534
+ ### 4.2 `change_head_branch` guard semantics (§3.1) — **Resolved**
535
+
536
+ **Question:** Should `change_head_branch` guard against non-existent branch
537
+ names, raising `ArgumentError` if the branch does not yet exist?
538
+
539
+ **Answer: No guard.**
540
+
541
+ The unborn-branch workflow — pointing HEAD at a ref that does not yet exist,
542
+ before the first commit is made — is a first-class git pattern. It is how
543
+ `git init --initial-branch=main` works, and it is how repository
544
+ initialization scripts change the default branch before any commits land.
545
+ A guard that rejects non-existent branches would silently break this
546
+ workflow. `git symbolic-ref` itself imposes no such constraint, and neither
547
+ should the facade. The method should be promoted as a clean pass-through
548
+ with clear documentation explaining that the target branch need not exist,
549
+ that pointing HEAD at a non-existent ref places the repository in unborn
550
+ state, and that this is intentional for initialization workflows. Callers
551
+ who want an existence check can call `repo.branches.local.any? { |b| b.name
552
+ == branch_name }` before calling `change_head_branch`.
553
+
554
+ ---
555
+
556
+ ### 4.3 `config()` overload complexity with `:file` for reads (§3.4) — **Resolved**
557
+
558
+ **Question:** Does extending `config()` to accept `:file` on the get and
559
+ list overloads create unacceptable dispatch complexity?
560
+
561
+ **Answer: Acceptable. Confirm Option A for §3.4.**
562
+
563
+ The dispatch rule for `config()` is purely positional-argument-count-based
564
+ (0 args → list, 1 arg → get, 2 args → set). Adding `:file` as a keyword
565
+ modifier does not change that rule; it is a pure option, not a new dispatch
566
+ dimension. The resulting overload table is:
567
+
568
+ | Positional args | `:file` present | Mode |
569
+ |-----------------|-----------------|------|
570
+ | 0 | no | list from repo |
571
+ | 0 | yes | list from file |
572
+ | 1 | no | get from repo |
573
+ | 1 | yes | get from file |
574
+ | 2 | no/yes | set (`:file` already works) |
575
+
576
+ Every cell is unambiguous. The `:file` option is already documented and
577
+ validated for the set overload; extending its meaning to reads is symmetric
578
+ and matches caller expectations. There is no new dispatch rule to learn.
579
+
580
+ ---
581
+
582
+ ### 4.4 Semver classification of `stash_list` non-promotion (§3.5) — **Resolved**
583
+
584
+ **Question:** If `stash_list` is not promoted (Options B or C in §3.5), is
585
+ a `BREAKING CHANGE:` CHANGELOG entry mandatory?
586
+
587
+ **Answer: Yes, mandatory — but moot if the recommended Option A is followed.**
588
+
589
+ `Git::Base#lib` is a public accessor. The audit's own framing (§7.1)
590
+ recognizes that every public method on `Git::Lib` is reachable from external
591
+ call sites as `g.lib.method_name`. Any method that disappears from that
592
+ surface without a deprecation cycle is a semver major breaking change,
593
+ regardless of `Git::Lib`'s `@api private` annotation. If the reviewer
594
+ overrides §3.5's recommendation and chooses Option B or C, the implementing
595
+ commit **must** carry a `BREAKING CHANGE:` footer and `UPGRADING.md`
596
+ must document the removal explicitly. Choosing Option A (promote as
597
+ `@deprecated`) avoids this entirely: the method remains callable in v5 with
598
+ a deprecation warning, and is removed in v6 after a full deprecation cycle.
599
+ Option A is therefore the only path that does not require a BREAKING CHANGE
600
+ entry for this specific method.
601
+
602
+ ---
603
+
604
+ ## 5. Next Steps
605
+
606
+ Complete items 1–4 below, then open PR 5h to implement the decisions.
607
+
608
+ - [x] Record a decision for each of the 10 items above (§3.1–§3.6) in the
609
+ **Decision** fields. (The four questions in §4 are pre-resolved and
610
+ do not require separate reviewer input.)
611
+ - [x] For items decided as **"promote"** (§3.1, §3.5, §3.6): raise a
612
+ follow-up issue or include in the PR 5h scope, specifying the target
613
+ `Git::Repository` module. *(Issues: #1399 `unmerged`, #1400 `stash_list`, #1401 `change_head_branch`)*
614
+ - [x] For items decided as **"deprecated forwarding alias over existing facade"**
615
+ (§3.2, §3.3, §3.4): include both the facade extension *and* the
616
+ deprecated forwarding methods (with `Git::Deprecation.warn` calls) in
617
+ PR 5h scope. *(Issues: #1402 `config_get/list/set`, #1403 `global_config`, #1404 `parse_config`)*
618
+ - [x] For items decided as **"mark internal / no public equivalent"**: confirm
619
+ the upgrade note wording and ensure it appears in `UPGRADING.md`
620
+ before PR 5h merges. *(No items landed in this bucket — all decisions
621
+ were "promote" or "deprecated forwarding alias". `UPGRADING.md` created
622
+ with all entries pre-populated.)*
623
+ - [x] Once all decisions are recorded, open PR 5h targeting `main` to
624
+ implement the promotions, add the deprecated aliases with runtime
625
+ warnings, add the upgrade notes to `UPGRADING.md`, and close this document's action items.
626
+ *(All PR 5h-1 through 5h-6 implementations merged; `UPGRADING.md` populated.)*