git 5.2.0 → 5.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0046556c3068d7b41423d2f0ce69631d5f5818ce279e410797c44c46c6df0311
4
- data.tar.gz: 59c5cc50b9a8ad522feac1360610f87932ee8624ed4119e1780a39eab29c9e43
3
+ metadata.gz: aedbda16896f72f7adc9320f2e1ade684cd1f4a49a392929ce3eb64d0320ca31
4
+ data.tar.gz: 8a77684970cde47e1ce927b2c4dab8c1e3c7b9356d876bbcb66a79c5d39769f4
5
5
  SHA512:
6
- metadata.gz: 5e2fd4a85f9bfac64f8255aab6442b03384f69fc8376e6cd280dd2f4f93cbaac1cd15fce9169bb3d441a6ddcf12b901cb4e2d9fafa8efea06906f0bd55181d76
7
- data.tar.gz: d87911a2437fd057da4caf47ee893df61a4e872eaa43d1fc470ea15ce8a3e546e4f083922df1eb38b0e44cd18d501cc1ba30e8333ba550eb8583cfb7ee13c218
6
+ metadata.gz: 37f051a10d8fad9eb51e3bebd2cefd84d523ca5961bb1fabc619f01a2db28f9c271a2a97701e8a575d2ab2e3244c5c1ee36b97bf9b94f45caa29f2f4a46340c1
7
+ data.tar.gz: 8c9cb46f8a7a74eff2488ef24e3641a3f6dccadee4735009290450630f3aab6bc90402e333c45c5061d8cb276767971eb316cba00196a1b7c3f3a6d7ad4fda85
data/CHANGELOG.md CHANGED
@@ -5,6 +5,39 @@
5
5
 
6
6
  # Change Log
7
7
 
8
+ ## [5.3.0](https://github.com/ruby-git/ruby-git/compare/v5.2.0...v5.3.0) (2026-09-03)
9
+
10
+
11
+ ### Features
12
+
13
+ * **author:** Add immutable Git::AuthorInfo and deprecate Git::Author ([c08b04b](https://github.com/ruby-git/ruby-git/commit/c08b04bf25142140c626fd16354355aae26b4fba)), closes [#1721](https://github.com/ruby-git/ruby-git/issues/1721)
14
+ * **branch:** Deprecate Git::Branch and Git::Branches in favor of Git::Repository#branch_list ([1581aa0](https://github.com/ruby-git/ruby-git/commit/1581aa00b3fc7984a2024b290b83f6f6f5e97838)), closes [#1639](https://github.com/ruby-git/ruby-git/issues/1639)
15
+ * **branch:** Deprecate Git::Branch#stashes in favor of Git::Repository#stashes_all ([dcee889](https://github.com/ruby-git/ruby-git/commit/dcee889a63bfd38cc3073577b55e2112721c5e33)), closes [#1637](https://github.com/ruby-git/ruby-git/issues/1637)
16
+ * **cat-file:** Deprecate the allow_unknown_type option of Git::Commands::CatFile::Raw ([95769ea](https://github.com/ruby-git/ruby-git/commit/95769ea683349c445a7a7ea7b839fc990d2d8c4b)), closes [#1709](https://github.com/ruby-git/ruby-git/issues/1709)
17
+ * **info:** Nest Git::AuthorInfo in Git::TagInfo and Git::StashInfo ([2f44f96](https://github.com/ruby-git/ruby-git/commit/2f44f9617122c3719625cf3a56f4b263b0de4c3c)), closes [#1745](https://github.com/ruby-git/ruby-git/issues/1745)
18
+ * **remote:** Deprecate Git::Remote and Git::Repository#remote in favor of #remote_list ([1abb585](https://github.com/ruby-git/ruby-git/commit/1abb585339c0d003622719aa5930b5bde2262502)), closes [#1643](https://github.com/ruby-git/ruby-git/issues/1643)
19
+ * **remote:** Freeze Git::RemoteInfo array-valued members ([4d44a3d](https://github.com/ruby-git/ruby-git/commit/4d44a3dc8fed77b39c7146675eaa9d46eb12e34d))
20
+ * **repository:** Add in_branch and merge_into facade methods ([59b58d0](https://github.com/ruby-git/ruby-git/commit/59b58d0389656051e8cd61bd856dc116139c2507)), closes [#1641](https://github.com/ruby-git/ruby-git/issues/1641)
21
+ * **repository:** Deprecate Git::Repository#remotes in favor of #remote_list ([410095c](https://github.com/ruby-git/ruby-git/commit/410095c4d2a3065c222f9cc750a05143230801d7)), closes [#1640](https://github.com/ruby-git/ruby-git/issues/1640)
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **branching:** Verify refs/heads when classifying the HEAD state ([bfc4d8a](https://github.com/ruby-git/ruby-git/commit/bfc4d8a2b82000906d1a398c69125fcfdc22c276)), closes [#1641](https://github.com/ruby-git/ruby-git/issues/1641)
27
+
28
+
29
+ ### Other Changes
30
+
31
+ * **adr:** Supersede ADR-0006 with a release-based deprecation period ([b3b32e3](https://github.com/ruby-git/ruby-git/commit/b3b32e339f3804d022ea83cec03977039ecdcace))
32
+ * **author:** Add failing tests for Git::AuthorInfo and Git::Author deprecation ([e0d3123](https://github.com/ruby-git/ruby-git/commit/e0d3123fadd836e301160f492e0a1303f43267b8)), closes [#1721](https://github.com/ruby-git/ruby-git/issues/1721)
33
+ * **branch:** Add failing test for the Git::Branch#stashes deprecation ([a806808](https://github.com/ruby-git/ruby-git/commit/a806808f3e5a5a38a92646c1ab64c8906b3dc525))
34
+ * **contributing:** Describe the release line and maintenance branch rules ([fd78c9c](https://github.com/ruby-git/ruby-git/commit/fd78c9ccd262470c9e33166ba63dc095b2060a5d))
35
+ * **info:** Add failing tests for nested Git::AuthorInfo on TagInfo and StashInfo ([a7f5e69](https://github.com/ruby-git/ruby-git/commit/a7f5e6988beaa85bce68e6295ad23a2754338d5b)), closes [#1745](https://github.com/ruby-git/ruby-git/issues/1745)
36
+ * **readme:** Add the deprecation and release support policies ([50ac2e2](https://github.com/ruby-git/ruby-git/commit/50ac2e28bdd6ddb9349b8980e457441a0d39f6c9))
37
+ * **remote:** Add failing tests for the Git::Remote and Git::Repository#remote deprecations ([36af404](https://github.com/ruby-git/ruby-git/commit/36af40419603f9c0a03f424e3839a1d7337b517b))
38
+ * **skills:** State the deprecation policy and the major release checklists ([a246cf8](https://github.com/ruby-git/ruby-git/commit/a246cf8fee93a96132f2bd1b0796899204b830a1))
39
+ * **upgrading:** Add the v6.0.0 upgrade procedure ([4d7c58c](https://github.com/ruby-git/ruby-git/commit/4d7c58c7a0e963da6146608093e9373dade1efe4))
40
+
8
41
  ## [5.2.0](https://github.com/ruby-git/ruby-git/compare/v5.1.0...v5.2.0) (2026-08-30)
9
42
 
10
43
 
data/CONTRIBUTING.md CHANGED
@@ -221,10 +221,14 @@ first keeps the review cycle short.
221
221
 
222
222
  This project maintains two active branches:
223
223
 
224
- - **`main`**: Active development for the next major version (v5.0.0+). This branch
225
- may contain breaking changes.
226
- - **`4.x`**: Maintenance branch for the v4.x release series. This branch receives bug
227
- fixes and backward-compatible improvements only.
224
+ - **`main`**: All development. It releases the next version of the gem, including
225
+ the next major version.
226
+ - **`4.x`**: The maintenance branch for the most recent previous major series. It
227
+ receives bug fixes and security fixes, and backward-compatible features at the
228
+ maintainers' discretion.
229
+
230
+ The README's [Release support policy](README.md#release-support-policy) says how long
231
+ each major series is supported.
228
232
 
229
233
  When submitting a pull request:
230
234
 
@@ -232,6 +236,15 @@ When submitting a pull request:
232
236
  - **Bug fixes**: Target `main`, and maintainers will backport to `4.x` if applicable
233
237
  - **Security fixes**: Target both branches or `4.x` if the issue only affects v4.x
234
238
 
239
+ Removing a deprecated API follows the
240
+ [deprecation policy](.github/skills/breaking-change-analysis/SKILL.md#step-4-deprecation-policy):
241
+
242
+ A removal PR merges to main only when its deprecation warning and `UPGRADING.md` entry
243
+ are contained in a previous normal release. Once any removal has merged to main, main
244
+ becomes the release line for the next major version. If another release of the
245
+ previous major is needed, it is cut from a branch created for that major (e.g. `4.x`
246
+ or `5.x`).
247
+
235
248
  ## AI-assisted contributions
236
249
 
237
250
  AI-assisted contributions are welcome. Please review and apply our [AI
data/README.md CHANGED
@@ -35,6 +35,8 @@ Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?log
35
35
  - [Project policies](#project-policies)
36
36
  - [Ruby version support policy](#ruby-version-support-policy)
37
37
  - [Git version support policy](#git-version-support-policy)
38
+ - [Deprecation policy](#deprecation-policy)
39
+ - [Release support policy](#release-support-policy)
38
40
  - [Project announcements](#project-announcements)
39
41
  - [2026-08-23: v5.x deprecations and the v6.0.0 roadmap](#2026-08-23-v5x-deprecations-and-the-v600-roadmap)
40
42
  - [2026-07-28: v5.0.0 released](#2026-07-28-v500-released)
@@ -296,9 +298,9 @@ See [the Active Support Deprecation
296
298
  documentation](https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html)
297
299
  for more details.
298
300
 
299
- If you silence deprecation warnings, reenable them before upgrading the git gem to
300
- the next major version. This makes it easier to identify changes needed for the
301
- upgrade.
301
+ Before upgrading the git gem to the next major version, follow the upgrade procedure
302
+ in [UPGRADING.md](UPGRADING.md#upgrading-to-v600). It turns the warnings into errors so
303
+ that you cannot miss one.
302
304
 
303
305
  For the full list of deprecated methods and their replacements, see
304
306
  [UPGRADING.md](UPGRADING.md).
@@ -382,6 +384,8 @@ opening issues or pull requests.
382
384
  | [AI_POLICY](AI_POLICY.md) | AI-assisted contributions are welcome. Contributors are expected to read and apply the AI Policy, and ensure any AI-assisted work meets our quality, security, and licensing standards. |
383
385
  | [Ruby version support policy](#ruby-version-support-policy) | Supported Ruby runtimes and platforms; bump decisions and CI coverage expectations. |
384
386
  | [Git version support policy](#git-version-support-policy) | Minimum supported git version and how version bumps are communicated and enforced. |
387
+ | [Deprecation policy](#deprecation-policy) | When an API may be removed, what a deprecation warning says, and how to upgrade across a major version. |
388
+ | [Release support policy](#release-support-policy) | Which branch releases what, and how long each major series is supported. |
385
389
  | [GOVERNANCE](GOVERNANCE.md) | Principles-first governance defining maintainer/project lead roles, least-privilege access, consensus/majority decisions, and nomination/emeritus steps. |
386
390
  | [MAINTAINERS](MAINTAINERS.md) | Lists active maintainers (Project Lead noted) and emeritus alumni with links; see governance for role scope. |
387
391
  | [LICENSE](LICENSE) | MIT License terms for using, modifying, and redistributing this project. |
@@ -418,6 +422,42 @@ The supported git version may be increased in future major or minor releases of
418
422
  gem as new git features are adopted or as maintaining backward compatibility becomes
419
423
  impractical. Such changes will be documented in the CHANGELOG and release notes.
420
424
 
425
+ ### Deprecation policy
426
+
427
+ This gem removes an API only in a major release, and only after a normal release
428
+ deprecated it with a runtime warning and documented its replacement in
429
+ [UPGRADING.md](UPGRADING.md). A normal release is one that is not a pre-release. The
430
+ warning names the major release that removes the API once that is decided. Until then
431
+ it says the API will be removed in a future major release.
432
+
433
+ The recommended way to upgrade across a major version:
434
+
435
+ 1. Upgrade to the latest release of the major series you are on.
436
+ 2. Set `GIT_DEPRECATION_BEHAVIOR=raise` (or `Git::Deprecation.behavior = :raise`) in
437
+ your test suite and, if possible, staging.
438
+ 3. Fix each deprecation using the entries in [UPGRADING.md](UPGRADING.md) until the
439
+ suite is clean.
440
+ 4. Upgrade to the next major release.
441
+
442
+ Because every deprecation warning is present in the last release of a major series,
443
+ this procedure finds every change the next major requires. The version-specific steps
444
+ are in [UPGRADING.md](UPGRADING.md#upgrading-to-v600). See
445
+ [Deprecations](#deprecations) for how to configure the warnings.
446
+
447
+ ### Release support policy
448
+
449
+ All development happens on `main`, which releases the next version of the gem,
450
+ including the next major version.
451
+
452
+ The most recent previous major series is maintained on a branch named for that
453
+ series, currently `4.x`. It receives bug fixes and security fixes, and
454
+ backward-compatible features at the maintainers' discretion. Fixes land on `main`
455
+ first and are backported, except a fix for a problem that exists only in the
456
+ maintenance branch, which targets that branch directly.
457
+
458
+ Support for a major series ends when the second major after it is released. v4.x is
459
+ supported until v6.0.0 ships, and v5.x until v7.0.0.
460
+
421
461
  ## Project announcements
422
462
 
423
463
  ### 2026-08-23: v5.x deprecations and the v6.0.0 roadmap
@@ -426,10 +466,11 @@ The road to v6.0.0 is now planned and public. The remaining ActiveRecord-style
426
466
  classes (`Git::Branch`, `Git::Remote`, `Git::Stash`, `Git::Worktree`,
427
467
  `Git::Object::Tag`, `Git::Status`, `Git::Author`, and their collections) will be
428
468
  deprecated during the v5.x series in favor of the immutable `*Info` value-object
429
- APIs. v6.0.0 will remove each deprecated class that passes the project's removal
430
- gate: a mandated deprecation soak period plus a proven-safe check. Any class that
431
- does not pass carries forward, still deprecated. v6.0.0 also raises the version
432
- floors: git ≥ 2.42.0, Ruby ≥ 3.4.
469
+ APIs. v6.0.0 will remove each deprecated class once a normal v5.x release has carried
470
+ its deprecation warning and UPGRADING.md entry, per the
471
+ [Deprecation policy](#deprecation-policy). v6.0.0 will not ship until every planned
472
+ deprecation has shipped that way. v6.0.0 also raises the version floors: git ≥ 2.42.0,
473
+ Ruby ≥ 3.4.
433
474
 
434
475
  [Issue #1717](https://github.com/ruby-git/ruby-git/issues/1717) is the living
435
476
  roadmap, tracking scope, sequencing, and status. If your code uses the classes
data/UPGRADING.md CHANGED
@@ -4,6 +4,7 @@ This document covers breaking changes and migration steps when upgrading the
4
4
  `git` gem to a new major version. Each section describes what changed and how
5
5
  to update your code when upgrading from the preceding major version.
6
6
 
7
+ - [Upgrading to v6.0.0](#upgrading-to-v600)
7
8
  - [Upgrading to v5.x](#upgrading-to-v5x)
8
9
  - [Overview](#overview)
9
10
  - [Breaking changes](#breaking-changes)
@@ -17,6 +18,31 @@ to update your code when upgrading from the preceding major version.
17
18
  - [Facade method renames](#facade-method-renames)
18
19
  - [v4.x-style configuration methods](#v4x-style-configuration-methods)
19
20
  - [`Git` module mixin deprecations](#git-module-mixin-deprecations)
21
+ - [`Git::Author` deprecated](#gitauthor-deprecated)
22
+ - [`Git::Branch#stashes` deprecated](#gitbranchstashes-deprecated)
23
+ - [`Git::Repository#remotes` deprecated](#gitrepositoryremotes-deprecated)
24
+ - [`Git::Remote` deprecated](#gitremote-deprecated)
25
+ - [`Git::Commands::CatFile::Raw` `allow_unknown_type` option deprecated](#gitcommandscatfileraw-allow_unknown_type-option-deprecated)
26
+ - [`Git::Branch` and `Git::Branches` deprecated](#gitbranch-and-gitbranches-deprecated)
27
+
28
+ ## Upgrading to v6.0.0
29
+
30
+ v6.0.0 is not yet released. This section will be completed when it ships.
31
+
32
+ v6.0.0 removes the APIs deprecated during v5.x under the project's
33
+ [deprecation policy](README.md#deprecation-policy).
34
+ [Issue 1717](https://github.com/ruby-git/ruby-git/issues/1717) tracks its scope.
35
+
36
+ To prepare:
37
+
38
+ 1. Upgrade to the latest v5.x release.
39
+ 2. Set `GIT_DEPRECATION_BEHAVIOR=raise` (or `Git::Deprecation.behavior = :raise`) in
40
+ your test suite and, if possible, staging.
41
+ 3. Fix each deprecation using the entries under
42
+ [Deprecated methods](#deprecated-methods) until the suite is clean.
43
+ 4. Upgrade to v6.0.0.
44
+
45
+ ---
20
46
 
21
47
  ## Upgrading to v5.x
22
48
 
@@ -240,7 +266,7 @@ shim cannot forward them). Update call sites directly:
240
266
 
241
267
  | v4.x call | Notes |
242
268
  |-----------|-------|
243
- | `g.lib.list_files(ref_dir)` | Walked `.git/refs/` directly. Use `g.branches`, `g.tags`, or `g.remotes` instead. |
269
+ | `g.lib.list_files(ref_dir)` | Walked `.git/refs/` directly. Use `g.branch_list`, `g.tags`, or `g.remote_list` instead. |
244
270
 
245
271
  ##### Internal plumbing methods (no replacement)
246
272
 
@@ -362,4 +388,264 @@ as bare methods is deprecated:
362
388
  | `include Git; global_config(name, value)` | `Git.config_set(name, value, global: true)` |
363
389
  | `include Git; global_config` | `Git.config_list(global: true)` |
364
390
 
391
+ #### `Git::Author` deprecated
392
+
393
+ Starting in v5.3.0, methods that return author, committer, or tagger data —
394
+ `Git::Object::Commit#author`, `Git::Object::Commit#committer`,
395
+ `Git::Object::Tag#tagger`, and `Git::TagInfo#tagger` — return an immutable
396
+ `Git::AuthorInfo` value object instead of the mutable `Git::Author`.
397
+
398
+ `Git::AuthorInfo` exposes the same `name`, `email`, and `date` readers, so code
399
+ that only reads these attributes needs no changes. Code that mutated a
400
+ `Git::Author` (via `name=`, `email=`, or `date=`) must be updated:
401
+ `Git::AuthorInfo` is frozen, and `#with` returns a modified copy rather than
402
+ updating in place (e.g. `info = info.with(name: 'New Name')`).
403
+
404
+ Constructing `Git::Author` directly emits a deprecation warning naming
405
+ `Git::AuthorInfo` as the replacement. The class is removed in v6.0.0.
406
+
407
+ | Deprecated usage | Replacement |
408
+ |-----------------|-------------|
409
+ | `Git::Author.new('Name <email> 1627849923 +0200')` | `Git::AuthorInfo.parse('Name <email> 1627849923 +0200')` |
410
+ | `author.name = 'New Name'` | `author = author.with(name: 'New Name')` (returns a new object) |
411
+
412
+ #### `Git::Branch#stashes` deprecated
413
+
414
+ `Git::Branch#stashes` ignores the branch it is called on and returns every stash
415
+ in the repository, so `g.branch('feature').stashes` and `g.branch('main').stashes`
416
+ return the same entries. Call `Git::Repository#stashes_all` instead; it is the
417
+ query `Git::Branch#stashes` was already running.
418
+
419
+ > **Return type change:** `Git::Branch#stashes` returns a `Git::Stashes`
420
+ > collection of `Git::Stash` objects, newest first. `g.stashes_all` returns an
421
+ > array of `[index, message]` pairs, oldest first. Code that read `stash.message`
422
+ > from each entry should read the second element of each pair instead. Code that
423
+ > iterated or indexed the collection must reverse the order first, because
424
+ > `Git::Stashes` yields and indexes newest first while `g.stashes_all` is oldest
425
+ > first.
426
+
427
+ `Git::Stashes` also exposes `save`, `apply`, and `clear`. Those map to the
428
+ repository's `stash_save`, `stash_apply`, and `stash_clear`, which are not
429
+ deprecated. `Git::Stashes#apply(i)` already passed `i` to git as `stash@{i}`
430
+ (`0` = newest), and `g.stash_apply(i)` does the same, so that index needs no
431
+ conversion.
432
+
433
+ | Deprecated call (works in v5.x, removed in v6.0.0) | Replacement |
434
+ |-----------------------------------------------------|-------------|
435
+ | `g.branch(name).stashes` | `g.stashes_all` — returns `[[index, message], ...]` |
436
+ | `g.branch(name).stashes.each { \|s\| puts s.message }` | `g.stashes_all.reverse_each { \|_index, message\| puts message }` |
437
+ | `g.branch(name).stashes.all` | `g.stashes_all` |
438
+ | `g.branch(name).stashes.size` | `g.stashes_all.size` |
439
+ | `g.branch(name).stashes[i].message` (`0` = newest, `i` coerced with `to_i`) | `g.stashes_all.reverse[i.to_i][1]` |
440
+ | `g.branch(name).stashes.save(message)` | `g.stash_save(message)` |
441
+ | `g.branch(name).stashes.apply` | `g.stash_apply` |
442
+ | `g.branch(name).stashes.apply(i)` (`0` = newest) | `g.stash_apply(i)` |
443
+ | `g.branch(name).stashes.clear` | `g.stash_clear` |
444
+ #### `Git::Repository#remotes` deprecated
445
+
446
+ `Git::Repository#remotes` is deprecated in favor of `Git::Repository#remote_list`
447
+ and is removed in v6.0.0. Its return value is unchanged. Calling `remotes` emits
448
+ one deprecation warning for itself plus one `Git::Remote` constructor warning for
449
+ each remote it returns (see the `Git::Remote` deprecation below), so a repository
450
+ with N remotes produces N + 1 warnings per call.
451
+
452
+ > **Return type change:** `remotes` returns `Array<Git::Remote>` — mutable
453
+ > objects with `name`, `url`, and `fetch_opts` accessors and `fetch`, `merge`,
454
+ > `branch`, and `remove` operations. `remote_list` returns
455
+ > `Array<Git::RemoteInfo>` — immutable value objects read from the repository's
456
+ > git config, with fields such as `name`, `url`, `push_url`, `fetch`, and `push`.
457
+ > Because a remote may carry more than one URL or refspec, `url`, `push_url`,
458
+ > `fetch`, and `push` are always frozen `Array<String>`. When a remote has more
459
+ > than one URL, git fetches from the first; the legacy `Git::Remote#url` returned
460
+ > the last one configured, so use `r.url.last` to reproduce that exact value.
461
+ > Likewise, `Git::Remote#fetch_opts` returned only the last configured fetch
462
+ > refspec, while `fetch` holds all of them. Operations that lived on
463
+ > `Git::Remote` are called on the repository with the remote name instead.
464
+ >
465
+ > **Order change:** `remotes` lists remotes in the order `git remote` prints
466
+ > them, while `remote_list` keeps the order in which remotes first appear in the
467
+ > config. When the legacy order matters, iterate `g.remote_names` (the same
468
+ > `git remote` order) or sort `g.remote_list` explicitly.
469
+
470
+ | Deprecated call (works in v5.x, removed in v6.0.0) | Replacement |
471
+ |-----------------------------------------------------|-------------|
472
+ | `g.remotes` | `g.remote_list` — returns `Array<Git::RemoteInfo>` |
473
+ | `g.remotes.map(&:name)` | `g.remote_list.map(&:name)` or `g.remote_names` |
474
+ | `g.remotes.map(&:to_s)` | `g.remote_list.map(&:name)` — `Git::RemoteInfo#to_s` is not the name |
475
+ | `g.remotes.map(&:url)` | `g.remote_list.map { \|r\| r.url.first }` — `url` is an `Array<String>` |
476
+ | `g.remotes.map(&:fetch_opts)` | `g.remote_list.map { \|r\| r.fetch.last }` — `fetch` holds every refspec |
477
+ | `g.remotes.each(&:fetch)` | `g.remote_names.each { \|name\| g.fetch(name) }` — same order as `remotes` |
478
+ | `remote.fetch` | `g.fetch(remote.name)` |
479
+ | `remote.fetch(opts)` | `g.fetch(remote.name, opts)` — same options hash |
480
+ | `remote.merge` | `g.merge("#{remote.name}/#{g.current_branch}")` |
481
+ | `remote.merge(branch)` | `g.merge("#{remote.name}/#{branch}")` |
482
+ | `remote.branch` | `g.branch_list("#{remote.name}/#{g.current_branch}").first` — returns a `Git::BranchInfo` |
483
+ | `remote.branch(name)` | `g.branch_list("#{remote.name}/#{name}").first` — returns a `Git::BranchInfo` |
484
+ | `remote.remove` | `g.remote_remove(remote.name)` |
485
+
486
+ #### `Git::Remote` deprecated
487
+
488
+ `Git::Remote`, `Git::Repository#remote`, and `Git::Repository#config_remote` are
489
+ deprecated and are removed in v6.0.0. Read a remote's configuration through
490
+ `Git::Repository#remote_list`, which returns one `Git::RemoteInfo` value object per
491
+ remote, and call the repository-level operations (`fetch`, `merge`, `branch_list`,
492
+ `remote_remove`) with the remote name. Return values are unchanged. Constructing a
493
+ `Git::Remote` directly emits one deprecation warning, and so does calling
494
+ `g.config_remote`. Calling `g.remote` emits two: one for `Git::Repository#remote`
495
+ and one for the `Git::Remote` it constructs. Likewise `g.remotes` emits one warning
496
+ for itself plus one per `Git::Remote` it returns (N + 1 for N remotes). The extra
497
+ warnings from `g.remote` and `g.remotes` are expected, not a bug.
498
+
499
+ > **Return type changes:** `Git::RemoteInfo#url` and `Git::RemoteInfo#fetch` are
500
+ > frozen `Array<String>` because a remote may carry more than one URL or fetch
501
+ > refspec. The legacy `Git::Remote#url` and `Git::Remote#fetch_opts` returned only
502
+ > the last configured value, so `r.url.last` and `r.fetch.last` reproduce them
503
+ > exactly; `r.url.first` is the URL git actually fetches from.
504
+ > `config_remote` returned a flat `Hash{String => String}` in which a repeated
505
+ > `url` or `fetch` key overwrote the earlier value, so it could not report every
506
+ > configured URL or refspec; `remote_list` keeps all of them. In the other
507
+ > direction, `Git::RemoteInfo` models only the remote variables git defines and
508
+ > drops any other `remote.<name>.*` key, while `config_remote` returned every key.
509
+ > Code that reads custom keys should filter `g.config_list` instead (see the
510
+ > table); that yields the same `Hash{String => String}` as `config_remote`.
511
+ > `Git::Remote#branch` returned a `Git::Branch`. Its replacement,
512
+ > `g.branch_list("#{name}/#{branch}").first`, returns a `Git::BranchInfo` value
513
+ > object, or `nil` when the remote-tracking branch does not exist.
514
+
515
+ In the table, `name` is the remote name (`g.remote` defaults it to `'origin'`).
516
+
517
+ | Deprecated call (works in v5.x, removed in v6.0.0) | Replacement |
518
+ |-----------------------------------------------------|-------------|
519
+ | `g.remote` | `g.remote_list.find { \|r\| r.name == 'origin' }` — returns a `Git::RemoteInfo`; the deprecated call emits two warnings |
520
+ | `g.remote(name)` | `g.remote_list.find { \|r\| r.name == name }` — returns a `Git::RemoteInfo`; the deprecated call emits two warnings |
521
+ | `g.config_remote(name)` for `url`, `fetch`, and the other modeled fields | `g.remote_list.find { \|r\| r.name == name }` — a `Git::RemoteInfo`, not a `Hash` |
522
+ | `g.config_remote(name)` for every key, including custom ones | `g.config_list.select { \|e\| e.key.start_with?("remote.#{name}.") }.to_h { \|e\| [e.key.delete_prefix("remote.#{name}."), e.value] }` — the same `Hash{String => String}` |
523
+ | `remote.name`, `remote.to_s` | `g.remote_list.find { \|r\| r.name == name }.name` or `g.remote_names` |
524
+ | `remote.url` | `g.remote_list.find { \|r\| r.name == name }.url` — `Array<String>`; `.first` for the single-URL case |
525
+ | `remote.fetch_opts` | `g.remote_list.find { \|r\| r.name == name }.fetch` — `Array<String>` of refspecs |
526
+ | `remote.fetch` | `g.fetch(name)` |
527
+ | `remote.fetch(opts)` | `g.fetch(name, opts)` — same option keys |
528
+ | `remote.merge` | `g.merge("#{name}/#{g.current_branch}")` |
529
+ | `remote.merge(branch)` | `g.merge("#{name}/#{branch}")` |
530
+ | `remote.branch` | `g.branch_list("#{name}/#{g.current_branch}").first` — returns a `Git::BranchInfo` |
531
+ | `remote.branch(branch)` | `g.branch_list("#{name}/#{branch}").first` — returns a `Git::BranchInfo` |
532
+ | `remote.remove` | `g.remote_remove(name)` |
533
+
534
+ #### `Git::Commands::CatFile::Raw` `allow_unknown_type` option deprecated
535
+
536
+ The `allow_unknown_type:` option of `Git::Commands::CatFile::Raw` is deprecated
537
+ and is removed in v6.0.0. Passing it emits a deprecation warning; the
538
+ `--allow-unknown-type` flag still reaches git unchanged until the option is
539
+ removed.
540
+
541
+ There is no replacement. Git 2.50 removed the unknown-type feature, so on git
542
+ 2.50 and later `--allow-unknown-type` is an accepted no-op and the option has no
543
+ effect. On git 2.28 through 2.49 the flag still lets `t: true` and `s: true`
544
+ report the type and size of an object whose type git does not recognize, but
545
+ that behavior is dropped together with the option. The class is internal
546
+ (`@api private`) and no `Git::Repository` method passes the option, so only code
547
+ that constructs the command class directly is affected.
548
+
549
+ | Deprecated call (works in v5.x, removed in v6.0.0) | Replacement |
550
+ |-----------------------------------------------------|-------------|
551
+ | `Git::Commands::CatFile::Raw.new(ctx).call(sha, t: true, allow_unknown_type: true)` | `Git::Commands::CatFile::Raw.new(ctx).call(sha, t: true)` |
552
+ | `Git::Commands::CatFile::Raw.new(ctx).call(sha, s: true, allow_unknown_type: true)` | `Git::Commands::CatFile::Raw.new(ctx).call(sha, s: true)` |
553
+
554
+ #### `Git::Branch` and `Git::Branches` deprecated
555
+
556
+ `Git::Branch`, `Git::Branches`, `Git::Repository#branch`, and
557
+ `Git::Repository#branches` are deprecated and are removed in v6.0.0. Read branch
558
+ data through `Git::Repository#branch_list`, which returns one `Git::BranchInfo`
559
+ value object per local and remote-tracking branch, and call the repository-level
560
+ operations (`checkout`, `branch_new`, `branch_delete`, `merge`, `merge_into`,
561
+ `in_branch`, and so on) with the branch name. Calling `g.branch` or `g.branches`,
562
+ constructing a `Git::Branches`, and calling any operation on a `Git::Branch` each
563
+ emit a deprecation warning; their return values are unchanged. The `full`,
564
+ `name`, `remote`, `to_s`, and `to_a` readers on `Git::Branch` do not warn.
565
+
566
+ > **Return shape change:** `Git::Branch` exposes `full` (`main` or
567
+ > `remotes/origin/main`), `name`, and `remote` (a `Git::Remote`, or `nil`).
568
+ > `Git::BranchInfo` exposes `refname` (always the full ref: `refs/heads/main` or
569
+ > `refs/remotes/origin/main`), `short_name` (`main` for both), `remote_name` (a
570
+ > `String`, or `nil`), `remote?`, `current?`, `target_oid`, `upstream`,
571
+ > `worktree_path`, and `symref`. `Git::BranchInfo#to_s` is the full ref, not the
572
+ > `remotes/origin/main` form `Git::Branch#to_s` returned. `branch_list` takes
573
+ > `git branch --list` patterns: `'main'` matches the local branch and
574
+ > `'origin/main'` matches the remote-tracking branch. The `remotes/origin/main`
575
+ > and `refs/...` forms that `g.branches[...]` accepted match nothing.
576
+ >
577
+ > **`checkout` no longer creates the branch:** `g.branch('x').checkout` created
578
+ > `x` when it did not exist, ignoring any error from that attempt, and then
579
+ > checked it out. `g.checkout('x')` does not create a missing local branch,
580
+ > with one exception that is git's own: when exactly one remote has a branch
581
+ > named `x`, git creates a local tracking branch from it (its default guess
582
+ > behavior). Otherwise the checkout fails. To reproduce create-or-checkout,
583
+ > call `g.branch_new('x') unless g.local_branch?('x')` and then
584
+ > `g.checkout('x')`. Use `g.checkout('x', new_branch: true)` only when `x` is
585
+ > known not to exist; like `g.branch_new('x')`, it fails when `x` already
586
+ > exists. Likewise `g.branch('x').create` ignored every error, while
587
+ > `g.branch_new('x')` raises `Git::FailedError` when `x` already exists.
588
+ >
589
+ > **`in_branch` and `merge_into` differences:**
590
+ >
591
+ > 1. **Branch creation.** `g.branch('x').in_branch { ... }` created `x` if it did
592
+ > not exist. `g.in_branch('x') { ... }` raises `ArgumentError` unless `x` is an
593
+ > existing local branch, so call `g.branch_new('x')` first. A commit SHA, tag,
594
+ > or remote-tracking name is also rejected before any checkout.
595
+ > 2. **Detached HEAD.** `Git::Branch#in_branch` recorded the literal `HEAD` and
596
+ > could not restore a detached HEAD to its original commit. `g.in_branch` and
597
+ > `g.merge_into` record the SHA and restore it.
598
+ > 3. **Unborn HEAD.** Both new methods raise `Git::Error` before checking anything
599
+ > out when HEAD is on a branch with no commits. The old methods failed later,
600
+ > mid-flow.
601
+ > 4. **Merge overload.** `g.branch('main').merge('feature')` returned stdout from
602
+ > the final restore checkout and ran a hard reset after the merge.
603
+ > `g.merge_into('main', 'feature')` returns the merge's stdout and does no
604
+ > reset. It also rejects the `:no_commit` option; callers who need
605
+ > `--no-commit` use `checkout` and `merge` directly.
606
+ > 5. **Remote-tracking receivers.** Called on a remote-tracking `Git::Branch`,
607
+ > `in_branch` and `merge(branch)` checked out the remote-tracking ref,
608
+ > detaching HEAD, and any commit made there was left dangling. `g.in_branch`
609
+ > and `g.merge_into` take an existing local branch only. Create one from the
610
+ > remote-tracking ref first, with
611
+ > `g.branch_new(name, "remotes/#{remote}/#{name}")`, and pass that branch.
612
+
613
+ In the table, `name` is the branch name (`g.branch` defaults it to the current
614
+ branch), `remote` is the remote name of a remote-tracking branch, `b` is a
615
+ `Git::Branch`, and `info` is the `Git::BranchInfo` that replaces it. Where a
616
+ row says to pass `info.refname` for a remote-tracking branch, `b.full` (the
617
+ `remotes/<remote>/<name>` form) works too; the shorter `"#{remote}/#{name}"`
618
+ can resolve a local branch of that name and is only used where git expects it
619
+ (`branch_delete` with `remotes: true`).
620
+
621
+ | Deprecated call (works in v5.x, removed in v6.0.0) | Replacement |
622
+ |-----------------------------------------------------|-------------|
623
+ | `g.branch(name)` | `g.branch_list(name).first` for a local branch, or `g.branch_list("#{remote}/#{name}").find(&:remote?)` for a remote-tracking one — a `Git::BranchInfo`, or `nil` when the branch does not exist; the `remotes/` and `refs/` forms match nothing |
624
+ | `g.branch` | `g.branch_list(g.current_branch).first` — `nil` when HEAD is detached or unborn; use `g.current_branch_state` there |
625
+ | `g.branches` | `g.branch_list` — returns `Array<Git::BranchInfo>` |
626
+ | `g.branches[name]` | `g.branch_list(name).first`, or `g.branch_list("#{remote}/#{name}").find(&:remote?)` for a remote-tracking branch |
627
+ | `g.branches.local` | `g.branch_list.reject(&:remote?)` |
628
+ | `g.branches.remote` | `g.branch_list.select(&:remote?)` |
629
+ | `g.branches.size` | `g.branch_list.size` |
630
+ | `g.branches.each { \|b\| ... }` | `g.branch_list.each { \|info\| ... }` |
631
+ | `g.branches.to_s` | `g.branch_list.map { \|i\| "#{i.current? ? '* ' : ' '}#{i.refname}\n" }.join` — full refs, not `remotes/...` |
632
+ | `b.full`, `b.to_s` | `info.refname` — `refs/remotes/origin/main` rather than `remotes/origin/main` |
633
+ | `b.to_a` | `[info.refname]` |
634
+ | `b.name` | `info.short_name` |
635
+ | `b.remote` | `info.remote_name` — a `String`, or `nil` for a local branch |
636
+ | `b.gcommit` | `g.gcommit(name)` — pass `info.refname` for a remote-tracking branch |
637
+ | `b.checkout` | `g.checkout(name)` — does not create the branch (see above); pass `info.refname` for a remote-tracking branch |
638
+ | `b.create` | `g.branch_new(name)` — raises when the branch already exists |
639
+ | `b.delete` (local) | `g.branch_delete(name)` |
640
+ | `b.delete` (remote-tracking) | `g.branch_delete("#{remote}/#{name}", remotes: true)` |
641
+ | `b.current` | `g.current_branch == name` |
642
+ | `b.contains?(commit)` | `!g.branch_contains(commit, name).empty?` |
643
+ | `b.merge` | `g.merge(name)` |
644
+ | `b.merge(branch, message)` | `g.merge_into(name, branch, message)` — local `b` only; see the differences above |
645
+ | `b.update_ref(commit)` (local) | `g.update_ref(name, commit)` |
646
+ | `b.update_ref(commit)` (remote-tracking) | `g.update_ref("remotes/#{remote}/#{name}", commit)` |
647
+ | `b.archive(file, opts)` | `g.archive(name, file, opts)` — pass `info.refname` for a remote-tracking branch |
648
+ | `b.in_branch(message) { ... }` | `g.in_branch(name, message) { ... }` — local `b` only; see the differences above |
649
+ | `b.stashes` | `g.stashes_all` — see [`Git::Branch#stashes` deprecated](#gitbranchstashes-deprecated) |
650
+
365
651
  ---
data/lib/git/author.rb CHANGED
@@ -3,6 +3,9 @@
3
3
  module Git
4
4
  # An author in a Git commit
5
5
  #
6
+ # @deprecated Use {Git::AuthorInfo} instead; this mutable class will be
7
+ # removed in v6.0.0
8
+ #
6
9
  # @api public
7
10
  #
8
11
  class Author
@@ -24,7 +27,15 @@ module Git
24
27
  #
25
28
  # @return [void]
26
29
  #
30
+ # @deprecated Use {Git::AuthorInfo.parse} instead
31
+ #
27
32
  def initialize(author_string)
33
+ if defined?(Git::Deprecation)
34
+ Git::Deprecation.warn(
35
+ 'Git::Author is deprecated and will be removed in v6.0.0. Use Git::AuthorInfo instead.'
36
+ )
37
+ end
38
+
28
39
  return unless (m = /(.*?) <(.*?)> (\d+) (.*)/.match(author_string))
29
40
 
30
41
  @name = m[1]
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Immutable value object representing an author or committer identity
5
+ #
6
+ # This is a lightweight, immutable data structure holding the identity data
7
+ # git records for commit authors, committers, and taggers. It replaces the
8
+ # mutable {Git::Author}, which is deprecated.
9
+ #
10
+ # @example Construct from individual values
11
+ # info = Git::AuthorInfo.new(
12
+ # name: 'John Doe',
13
+ # email: 'john.doe@example.com',
14
+ # date: Time.at(1627849923)
15
+ # )
16
+ # info.name #=> 'John Doe'
17
+ #
18
+ # @example Parse from a raw git author string
19
+ # info = Git::AuthorInfo.parse('John Doe <john.doe@example.com> 1627849923 +0200')
20
+ # info.email #=> 'john.doe@example.com'
21
+ # info.date.to_i #=> 1627849923
22
+ #
23
+ # @see Git::Object::Commit#author
24
+ #
25
+ # @see Git::Object::Commit#committer
26
+ #
27
+ # @api public
28
+ #
29
+ # @!attribute [r] name
30
+ # @return [String, nil] the person's name, or `nil` if not available
31
+ #
32
+ # @!attribute [r] email
33
+ # @return [String, nil] the person's email address, or `nil` if not available
34
+ #
35
+ # @!attribute [r] date
36
+ # @return [Time, nil] the timestamp of the change, or `nil` if not available
37
+ #
38
+ AuthorInfo = Data.define(:name, :email, :date) do
39
+ # Parses a raw git identity string into a Git::AuthorInfo
40
+ #
41
+ # The expected format is `"Name <email> timestamp offset"` as emitted by
42
+ # `git cat-file` for the `author`, `committer`, and `tagger` headers. The
43
+ # timestamp is interpreted as seconds since the Unix epoch; the timezone
44
+ # offset is not preserved in the resulting `date`.
45
+ #
46
+ # @example Parse a well-formed identity string
47
+ # Git::AuthorInfo.parse('John Doe <john.doe@example.com> 1627849923 +0200')
48
+ # #=> #<data Git::AuthorInfo name="John Doe", email="john.doe@example.com", ...>
49
+ #
50
+ # @example A string that does not match the expected format
51
+ # Git::AuthorInfo.parse('garbage')
52
+ # #=> #<data Git::AuthorInfo name=nil, email=nil, date=nil>
53
+ #
54
+ # @param author_string [String] the raw identity string to parse
55
+ #
56
+ # @return [Git::AuthorInfo] the parsed identity; all attributes are `nil`
57
+ # when the string does not match the expected format
58
+ #
59
+ def self.parse(author_string)
60
+ match = /(.*?) <(.*?)> (\d+) (.*)/.match(author_string)
61
+ return new(name: nil, email: nil, date: nil) unless match
62
+
63
+ new(name: match[1], email: match[2], date: Time.at(match[3].to_i))
64
+ end
65
+ end
66
+ end