git 5.1.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 +4 -4
- data/CHANGELOG.md +78 -0
- data/CONTRIBUTING.md +202 -169
- data/LICENSE +1 -1
- data/README.md +192 -151
- data/UPGRADING.md +287 -1
- data/git.gemspec +35 -3
- data/lib/git/author.rb +11 -0
- data/lib/git/author_info.rb +66 -0
- data/lib/git/branch.rb +210 -15
- data/lib/git/branch_info.rb +1 -1
- data/lib/git/branches.rb +35 -7
- data/lib/git/command_line/base.rb +1 -2
- data/lib/git/commands/base.rb +1 -1
- data/lib/git/commands/cat_file/raw.rb +62 -7
- data/lib/git/object.rb +13 -7
- data/lib/git/parsers/stash.rb +50 -17
- data/lib/git/parsers/tag.rb +54 -8
- data/lib/git/remote.rb +37 -7
- data/lib/git/remote_info.rb +67 -10
- data/lib/git/repository/branching.rb +190 -6
- data/lib/git/repository/merging.rb +96 -2
- data/lib/git/repository/remote_operations.rb +57 -0
- data/lib/git/repository/shared_private.rb +67 -0
- data/lib/git/stash_info.rb +32 -34
- data/lib/git/tag_info.rb +21 -29
- data/lib/git/version.rb +1 -1
- data/lib/git.rb +1 -0
- metadata +6 -94
- data/.claude/commands/address-copilot-reviews.md +0 -14
- data/.claude/settings.json +0 -16
- data/.claude/skills +0 -1
- data/.commitlintrc.yml +0 -38
- data/.dockerignore +0 -27
- data/.github/copilot-instructions.md +0 -30
- data/.github/hooks/bin-setup-on-worktree.json +0 -11
- data/.github/hooks/run-bin-setup-once.sh +0 -20
- data/.github/issue_template.md +0 -15
- data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +0 -188
- data/.github/pull_request_template.md +0 -21
- data/.github/skills/breaking-change-analysis/SKILL.md +0 -99
- data/.github/skills/ci-cd-troubleshooting/SKILL.md +0 -264
- data/.github/skills/command-implementation/REFERENCE.md +0 -994
- data/.github/skills/command-implementation/SKILL.md +0 -230
- data/.github/skills/command-test-conventions/SKILL.md +0 -664
- data/.github/skills/command-yard-documentation/SKILL.md +0 -434
- data/.github/skills/dependency-management/SKILL.md +0 -72
- data/.github/skills/development-workflow/SKILL.md +0 -512
- data/.github/skills/facade-implementation/REFERENCE.md +0 -837
- data/.github/skills/facade-implementation/SKILL.md +0 -269
- data/.github/skills/facade-test-conventions/SKILL.md +0 -391
- data/.github/skills/facade-yard-documentation/SKILL.md +0 -435
- data/.github/skills/make-skill-template/SKILL.md +0 -226
- data/.github/skills/pr-readiness-review/SKILL.md +0 -205
- data/.github/skills/project-context/SKILL.md +0 -306
- data/.github/skills/pull-request-review/SKILL.md +0 -168
- data/.github/skills/rebase/SKILL.md +0 -148
- data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +0 -131
- data/.github/skills/release-management/SKILL.md +0 -125
- data/.github/skills/resolve-feedback/SKILL.md +0 -288
- data/.github/skills/review-arguments-dsl/CHECKLIST.md +0 -788
- data/.github/skills/review-arguments-dsl/SKILL.md +0 -214
- data/.github/skills/review-cross-command-consistency/SKILL.md +0 -139
- data/.github/skills/reviewing-skills/SKILL.md +0 -214
- data/.github/skills/rspec-unit-testing-standards/SKILL.md +0 -685
- data/.github/skills/tdd-refactor-step/SKILL.md +0 -236
- data/.github/skills/test-debugging/SKILL.md +0 -161
- data/.github/skills/yard-documentation/SKILL.md +0 -981
- data/.github/skills/yard-documentation/element-rules.md +0 -162
- data/.github/skills-deprecated/README.md +0 -21
- data/.github/skills-deprecated/extract-command-from-lib/SKILL.md +0 -487
- data/.github/skills-deprecated/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +0 -22
- data/.github/skills-deprecated/extract-facade-from-base-lib/SKILL.md +0 -600
- data/.github/skills-deprecated/review-backward-compatibility/SKILL.md +0 -275
- data/.github/workflows/continuous_integration.yml +0 -358
- data/.github/workflows/enforce_conventional_commits.yml +0 -35
- data/.github/workflows/experimental_continuous_integration.yml +0 -59
- data/.github/workflows/release.yml +0 -52
- data/.github/workflows/warm_bundler_caches.yml +0 -82
- data/.gitignore +0 -30
- data/.husky/commit-msg +0 -1
- data/.husky/pre-commit +0 -13
- data/.release-please-config.json +0 -36
- data/.release-please-manifest.json +0 -3
- data/.rspec +0 -2
- data/.rubocop.yml +0 -44
- data/.rubocop_todo.yml +0 -30
- data/.yard-lint.yml +0 -75
- data/CLAUDE.md +0 -11
- data/Gemfile +0 -22
- data/Rakefile +0 -41
- data/docker/test/Dockerfile +0 -32
- data/docker/test/docker-compose.yml +0 -0
- data/package.json +0 -10
- data/redesign/1_architecture_existing.md +0 -102
- data/redesign/2_architecture_redesign.md +0 -449
- data/redesign/3_architecture_implementation.md +0 -1623
- data/redesign/Phase 4 - Step A.md +0 -366
- data/redesign/Phase 4 - Step B.md +0 -921
- data/redesign/Phase 4 - Step C.md +0 -833
- data/redesign/beta_release.md +0 -107
- data/redesign/branch_parse_refactor_plan.md +0 -163
- data/redesign/c1a-public-api-scope.tsv +0 -256
- data/redesign/c1c2_audit.md +0 -566
- data/redesign/c1c2_bucket6_lib_orphans.md +0 -626
- data/redesign/config_design.rb +0 -501
- data/redesign/index.md +0 -34
- data/redesign/info_object_migration_plan.md +0 -126
- data/redesign/integration_test_analysis.md +0 -521
- data/redesign/phase-4-step-b-test-audit.tsv +0 -485
- data/redesign/remote_refactor_plan.md +0 -164
- data/redesign/reverse_dependencies.sql +0 -44
- data/tasks/gem_tasks.rake +0 -14
- data/tasks/npm_tasks.rake +0 -7
- data/tasks/rspec.rake +0 -111
- data/tasks/rubocop.rake +0 -5
- data/tasks/test_gem.rake +0 -12
- data/tasks/yard.rake +0 -57
data/lib/git/parsers/stash.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
require 'git/author_info'
|
|
3
6
|
require 'git/stash_info'
|
|
4
7
|
|
|
5
8
|
module Git
|
|
@@ -45,10 +48,10 @@ module Git
|
|
|
45
48
|
# %gs = reflog subject (the stash message)
|
|
46
49
|
# %an = author name
|
|
47
50
|
# %ae = author email
|
|
48
|
-
# %aI = author date (ISO 8601 format)
|
|
51
|
+
# %aI = author date (ISO 8601 format, parsed into a Time)
|
|
49
52
|
# %cn = committer name
|
|
50
53
|
# %ce = committer email
|
|
51
|
-
# %cI = committer date (ISO 8601 format)
|
|
54
|
+
# %cI = committer date (ISO 8601 format, parsed into a Time)
|
|
52
55
|
STASH_FORMAT = [
|
|
53
56
|
'%H', # 0: full SHA
|
|
54
57
|
'%h', # 1: short SHA
|
|
@@ -163,7 +166,7 @@ module Git
|
|
|
163
166
|
# @return [Hash] attributes for StashInfo.new
|
|
164
167
|
#
|
|
165
168
|
def stash_info_attrs(parts, index)
|
|
166
|
-
core_attrs(parts, index).merge(
|
|
169
|
+
core_attrs(parts, index).merge(author: author_info(parts), committer: committer_info(parts))
|
|
167
170
|
end
|
|
168
171
|
|
|
169
172
|
# Build core StashInfo attributes from parsed fields
|
|
@@ -182,30 +185,60 @@ module Git
|
|
|
182
185
|
}
|
|
183
186
|
end
|
|
184
187
|
|
|
185
|
-
# Build author
|
|
188
|
+
# Build the author identity from the parsed fields
|
|
186
189
|
#
|
|
187
190
|
# @param parts [Array<String>] the parsed format fields
|
|
188
191
|
#
|
|
189
|
-
# @return [
|
|
192
|
+
# @return [Git::AuthorInfo] the stash author; its `date` is a `Time`
|
|
190
193
|
#
|
|
191
|
-
def
|
|
192
|
-
|
|
193
|
-
author_name: parts[Fields::AUTHOR_NAME], author_email: parts[Fields::AUTHOR_EMAIL],
|
|
194
|
-
author_date: parts[Fields::AUTHOR_DATE]
|
|
195
|
-
}
|
|
194
|
+
def author_info(parts)
|
|
195
|
+
build_author_info(parts[Fields::AUTHOR_NAME], parts[Fields::AUTHOR_EMAIL], parts[Fields::AUTHOR_DATE])
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
# Build committer
|
|
198
|
+
# Build the committer identity from the parsed fields
|
|
199
199
|
#
|
|
200
200
|
# @param parts [Array<String>] the parsed format fields
|
|
201
201
|
#
|
|
202
|
-
# @return [
|
|
202
|
+
# @return [Git::AuthorInfo] the stash committer; its `date` is a `Time`
|
|
203
203
|
#
|
|
204
|
-
def
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
def committer_info(parts)
|
|
205
|
+
build_author_info(
|
|
206
|
+
parts[Fields::COMMITTER_NAME], parts[Fields::COMMITTER_EMAIL], parts[Fields::COMMITTER_DATE]
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Build a Git::AuthorInfo from identity fields
|
|
211
|
+
#
|
|
212
|
+
# The date is parsed with `Time.iso8601`, so the UTC offset git emits for
|
|
213
|
+
# `%aI` and `%cI` is preserved in the resulting `Time`.
|
|
214
|
+
#
|
|
215
|
+
# @param name [String] the `%an` or `%cn` field
|
|
216
|
+
#
|
|
217
|
+
# @param email [String] the `%ae` or `%ce` field
|
|
218
|
+
#
|
|
219
|
+
# @param date [String] the `%aI` or `%cI` field in ISO 8601 format
|
|
220
|
+
#
|
|
221
|
+
# @return [Git::AuthorInfo] the identity with `date` as a `Time`
|
|
222
|
+
#
|
|
223
|
+
# @raise [Git::UnexpectedResultError] if the date is not a valid ISO 8601 date
|
|
224
|
+
#
|
|
225
|
+
def build_author_info(name, email, date)
|
|
226
|
+
Git::AuthorInfo.new(name: name, email: email, date: parse_date(date))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Parse a `%aI` or `%cI` field into a Time
|
|
230
|
+
#
|
|
231
|
+
# @param date [String] the date field in ISO 8601 format
|
|
232
|
+
#
|
|
233
|
+
# @return [Time] the parsed time, preserving the UTC offset
|
|
234
|
+
#
|
|
235
|
+
# @raise [Git::UnexpectedResultError] if the field is not a valid ISO 8601 date
|
|
236
|
+
#
|
|
237
|
+
def parse_date(date)
|
|
238
|
+
Time.iso8601(date)
|
|
239
|
+
rescue ArgumentError => e
|
|
240
|
+
raise Git::UnexpectedResultError,
|
|
241
|
+
"Unexpected date #{date.inspect} in output from `git stash list`: #{e.message}"
|
|
209
242
|
end
|
|
210
243
|
|
|
211
244
|
# Extract the stash index from a reflog selector
|
data/lib/git/parsers/tag.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
require 'git/author_info'
|
|
3
6
|
require 'git/tag_info'
|
|
4
7
|
require 'git/tag_delete_result'
|
|
5
8
|
require 'git/tag_delete_failure'
|
|
@@ -114,7 +117,7 @@ module Git
|
|
|
114
117
|
# where <FS> is the unit separator character ("\x1f").
|
|
115
118
|
#
|
|
116
119
|
# For lightweight tags, Git emits empty strings for the tagger fields and message;
|
|
117
|
-
# these are converted to nil by {#
|
|
120
|
+
# these are converted to nil by {#parse_tagger} and {#parse_message}.
|
|
118
121
|
#
|
|
119
122
|
# @param record [String] a single tag record from git tag --format output
|
|
120
123
|
#
|
|
@@ -183,19 +186,62 @@ module Git
|
|
|
183
186
|
def build_tag_info_object(parts, oid, target_oid)
|
|
184
187
|
Git::TagInfo.new(
|
|
185
188
|
name: parts[0], oid: oid, target_oid: target_oid, objecttype: parts[3],
|
|
186
|
-
|
|
187
|
-
tagger_date: parse_optional_field(parts[6]), message: parse_message(parts[3], parts[7])
|
|
189
|
+
tagger: parse_tagger(parts[4], parts[5], parts[6]), message: parse_message(parts[3], parts[7])
|
|
188
190
|
)
|
|
189
191
|
end
|
|
190
192
|
|
|
191
|
-
#
|
|
193
|
+
# Build the tagger identity from the tagger name, email, and date fields
|
|
194
|
+
#
|
|
195
|
+
# Git emits empty strings for all three fields when there is no tag object
|
|
196
|
+
# (lightweight tags) or the tag object has no tagger header, in which case
|
|
197
|
+
# the tagger is nil. Otherwise the angle brackets git wraps around
|
|
198
|
+
# `%(taggeremail)` are stripped and the strict ISO 8601
|
|
199
|
+
# `%(taggerdate:iso8601-strict)` value is parsed into a `Time` that
|
|
200
|
+
# preserves the UTC offset. A partially populated identity (for example an
|
|
201
|
+
# empty name with an email and date) is kept as emitted rather than dropped,
|
|
202
|
+
# and an empty date becomes `nil`.
|
|
203
|
+
#
|
|
204
|
+
# @example An annotated tag's tagger
|
|
205
|
+
# parse_tagger('John Doe', '<john@example.com>', '2024-01-15T10:30:00-08:00')
|
|
206
|
+
# #=> #<data Git::AuthorInfo name="John Doe", email="john@example.com", ...>
|
|
207
|
+
#
|
|
208
|
+
# @example A lightweight tag has no tagger
|
|
209
|
+
# parse_tagger('', '', '') #=> nil
|
|
210
|
+
#
|
|
211
|
+
# @param name [String] the `%(taggername)` field
|
|
212
|
+
#
|
|
213
|
+
# @param email [String] the `%(taggeremail)` field, including angle brackets
|
|
214
|
+
#
|
|
215
|
+
# @param date [String] the `%(taggerdate:iso8601-strict)` field
|
|
216
|
+
#
|
|
217
|
+
# @return [Git::AuthorInfo, nil] the tagger, or nil when all three fields are empty
|
|
218
|
+
#
|
|
219
|
+
# @raise [Git::UnexpectedResultError] if a non-empty date is not a valid ISO 8601
|
|
220
|
+
# date
|
|
221
|
+
#
|
|
222
|
+
def parse_tagger(name, email, date)
|
|
223
|
+
return nil if [name, email, date].all?(&:empty?)
|
|
224
|
+
|
|
225
|
+
Git::AuthorInfo.new(
|
|
226
|
+
name: name,
|
|
227
|
+
email: email.delete_prefix('<').delete_suffix('>'),
|
|
228
|
+
date: date.empty? ? nil : parse_date(date)
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Parse a `%(taggerdate:iso8601-strict)` field into a Time
|
|
233
|
+
#
|
|
234
|
+
# @param date [String] the date field in strict ISO 8601 format
|
|
192
235
|
#
|
|
193
|
-
# @
|
|
236
|
+
# @return [Time] the parsed time, preserving the UTC offset
|
|
194
237
|
#
|
|
195
|
-
# @
|
|
238
|
+
# @raise [Git::UnexpectedResultError] if the field is not a valid ISO 8601 date
|
|
196
239
|
#
|
|
197
|
-
def
|
|
198
|
-
|
|
240
|
+
def parse_date(date)
|
|
241
|
+
Time.iso8601(date)
|
|
242
|
+
rescue ArgumentError => e
|
|
243
|
+
raise Git::UnexpectedResultError,
|
|
244
|
+
"Unexpected tagger date #{date.inspect} in output from `git tag --list`: #{e.message}"
|
|
199
245
|
end
|
|
200
246
|
|
|
201
247
|
# Parse message field, returning nil for lightweight tags or empty messages
|
data/lib/git/remote.rb
CHANGED
|
@@ -7,13 +7,26 @@ module Git
|
|
|
7
7
|
# A remote in a Git repository
|
|
8
8
|
#
|
|
9
9
|
# Remote objects provide access to remote metadata and operations like fetch,
|
|
10
|
-
# merge, and remove.
|
|
11
|
-
#
|
|
10
|
+
# merge, and remove. This class and `Git::Repository#remote`, which returns
|
|
11
|
+
# it, are both deprecated: read remote configuration through
|
|
12
|
+
# {Git::Repository::RemoteOperations#remote_list} and call the
|
|
13
|
+
# repository-level operations with the remote name instead.
|
|
12
14
|
#
|
|
13
|
-
# @example
|
|
15
|
+
# @example Reading a remote and fetching from it without Git::Remote
|
|
14
16
|
# git = Git.open('.')
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
+
# origin = git.remote_list.find { |r| r.name == 'origin' } #=> Git::RemoteInfo
|
|
18
|
+
# origin.url.first
|
|
19
|
+
# git.fetch(origin.name)
|
|
20
|
+
#
|
|
21
|
+
# @deprecated Use {Git::Repository::RemoteOperations#remote_list} and the
|
|
22
|
+
# repository-level remote operations instead
|
|
23
|
+
#
|
|
24
|
+
# {Git::Repository::RemoteOperations#remote_list} returns immutable
|
|
25
|
+
# {Git::RemoteInfo} value objects. Operations that lived on this class are
|
|
26
|
+
# called on the repository with the remote name instead (for example
|
|
27
|
+
# {Git::Repository::RemoteOperations#fetch} and
|
|
28
|
+
# {Git::Repository::RemoteOperations#remote_remove}). Constructing a
|
|
29
|
+
# `Git::Remote` emits a deprecation warning.
|
|
17
30
|
#
|
|
18
31
|
# @api public
|
|
19
32
|
#
|
|
@@ -42,13 +55,20 @@ module Git
|
|
|
42
55
|
#
|
|
43
56
|
# @param name [String] the remote name (e.g. `'origin'`)
|
|
44
57
|
#
|
|
45
|
-
# @note
|
|
58
|
+
# @note Do not construct directly. `Git::Repository#remote` is deprecated as
|
|
59
|
+
# well; use {Git::Repository::RemoteOperations#remote_list} and the
|
|
60
|
+
# repository-level remote operations instead.
|
|
46
61
|
#
|
|
47
62
|
# @api private
|
|
48
63
|
#
|
|
49
64
|
def initialize(base, name)
|
|
65
|
+
Git::Deprecation.warn(
|
|
66
|
+
'Git::Remote is deprecated and will be removed in v6.0.0. ' \
|
|
67
|
+
'Use Git::Repository#remote_list and the repository-level remote operations instead.'
|
|
68
|
+
)
|
|
50
69
|
@base = base
|
|
51
|
-
|
|
70
|
+
# config_remote is deprecated too; silence it so one Git::Remote.new emits one warning
|
|
71
|
+
config = Git::Deprecation.silence { remote_repository.config_remote(name) }
|
|
52
72
|
@name = name
|
|
53
73
|
@url = config['url']
|
|
54
74
|
@fetch_opts = config['fetch']
|
|
@@ -119,6 +139,16 @@ module Git
|
|
|
119
139
|
#
|
|
120
140
|
# @return [Git::Branch] a branch object representing `<remote>/<branch>`
|
|
121
141
|
#
|
|
142
|
+
# @deprecated Use
|
|
143
|
+
# `Git::Repository#branch_list("#{name}/#{branch || current_branch}").first`
|
|
144
|
+
# instead
|
|
145
|
+
#
|
|
146
|
+
# With no argument this method falls back to the current branch, so the
|
|
147
|
+
# replacement has to supply `Git::Repository#current_branch` itself. The
|
|
148
|
+
# replacement returns a {Git::BranchInfo} value object rather than a
|
|
149
|
+
# {Git::Branch}, and returns `nil` when the remote-tracking branch does
|
|
150
|
+
# not exist.
|
|
151
|
+
#
|
|
122
152
|
def branch(branch = nil)
|
|
123
153
|
branch ||= remote_repository.current_branch
|
|
124
154
|
Git::Branch.new(@base, "#{@name}/#{branch}")
|
data/lib/git/remote_info.rb
CHANGED
|
@@ -6,7 +6,11 @@ module Git
|
|
|
6
6
|
# Each instance holds the parsed configuration for a single remote as read
|
|
7
7
|
# from the repository's git config. Multi-value fields (`:url`, `:push_url`,
|
|
8
8
|
# `:fetch`, `:push`) are always `Array<String>` (never `nil`; may be empty).
|
|
9
|
-
#
|
|
9
|
+
# Those arrays are frozen copies of the values given, so the set of URLs and
|
|
10
|
+
# refspecs cannot change after construction; use `with` to derive a modified
|
|
11
|
+
# copy. The immutability is shallow, as with any `Data` member: the strings
|
|
12
|
+
# inside those arrays and the scalar members are the objects the caller
|
|
13
|
+
# passed in, not copies. All other fields are nilable except `:name`.
|
|
10
14
|
#
|
|
11
15
|
# @example Minimal remote (fetch-only, one URL)
|
|
12
16
|
# info = Git::RemoteInfo.new(
|
|
@@ -88,13 +92,13 @@ module Git
|
|
|
88
92
|
#
|
|
89
93
|
# @param name [String] the name of the remote (required)
|
|
90
94
|
#
|
|
91
|
-
# @param url [Array<String>] fetch URLs (default `[]`)
|
|
95
|
+
# @param url [Array<String>] fetch URLs (default `[]`); stored as a frozen copy
|
|
92
96
|
#
|
|
93
|
-
# @param push_url [Array<String>] push URLs (default `[]`)
|
|
97
|
+
# @param push_url [Array<String>] push URLs (default `[]`); stored as a frozen copy
|
|
94
98
|
#
|
|
95
|
-
# @param fetch [Array<String>] fetch refspecs (default `[]`)
|
|
99
|
+
# @param fetch [Array<String>] fetch refspecs (default `[]`); stored as a frozen copy
|
|
96
100
|
#
|
|
97
|
-
# @param push [Array<String>] push refspecs (default `[]`)
|
|
101
|
+
# @param push [Array<String>] push refspecs (default `[]`); stored as a frozen copy
|
|
98
102
|
#
|
|
99
103
|
# @param mirror [Boolean, nil] mirror flag (default `nil`)
|
|
100
104
|
#
|
|
@@ -118,7 +122,7 @@ module Git
|
|
|
118
122
|
#
|
|
119
123
|
# @return [Git::RemoteInfo]
|
|
120
124
|
#
|
|
121
|
-
def initialize(
|
|
125
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
122
126
|
name:,
|
|
123
127
|
url: [],
|
|
124
128
|
push_url: [],
|
|
@@ -136,11 +140,64 @@ module Git
|
|
|
136
140
|
vcs: nil
|
|
137
141
|
)
|
|
138
142
|
super(
|
|
139
|
-
name:, url: Array(url), push_url: Array(push_url),
|
|
140
|
-
push: Array(push), mirror:,
|
|
141
|
-
|
|
142
|
-
vcs:
|
|
143
|
+
name:, url: Array(url).dup.freeze, push_url: Array(push_url).dup.freeze,
|
|
144
|
+
fetch: Array(fetch).dup.freeze, push: Array(push).dup.freeze, mirror:,
|
|
145
|
+
skip_default_update:, tag_opt:, prune:, prune_tags:, receivepack:, uploadpack:,
|
|
146
|
+
promisor:, partial_clone_filter:, vcs:
|
|
143
147
|
)
|
|
144
148
|
end
|
|
149
|
+
|
|
150
|
+
# Return a copy of this RemoteInfo with the given fields replaced
|
|
151
|
+
#
|
|
152
|
+
# Routes through {#initialize} so the multi-value fields of the copy are
|
|
153
|
+
# frozen copies, the same as on construction. `Data#with` bypasses
|
|
154
|
+
# `initialize` on Ruby 3.2, which would leave those arrays mutable.
|
|
155
|
+
#
|
|
156
|
+
# @example Replace the fetch URL
|
|
157
|
+
# info.with(url: ['https://example.com/other.git']).url
|
|
158
|
+
# # => ["https://example.com/other.git"]
|
|
159
|
+
#
|
|
160
|
+
# @param fields [Hash{Symbol => Object}] the fields to replace, keyed by
|
|
161
|
+
# member name
|
|
162
|
+
#
|
|
163
|
+
# @option fields [String] :name the name of the remote
|
|
164
|
+
#
|
|
165
|
+
# @option fields [Array<String>] :url fetch URLs
|
|
166
|
+
#
|
|
167
|
+
# @option fields [Array<String>] :push_url push URLs
|
|
168
|
+
#
|
|
169
|
+
# @option fields [Array<String>] :fetch fetch refspecs
|
|
170
|
+
#
|
|
171
|
+
# @option fields [Array<String>] :push push refspecs
|
|
172
|
+
#
|
|
173
|
+
# @option fields [Boolean, nil] :mirror mirror flag
|
|
174
|
+
#
|
|
175
|
+
# @option fields [Boolean, nil] :skip_default_update skip-default-update flag
|
|
176
|
+
#
|
|
177
|
+
# @option fields [String, nil] :tag_opt tag-fetching option
|
|
178
|
+
#
|
|
179
|
+
# @option fields [Boolean, nil] :prune prune flag
|
|
180
|
+
#
|
|
181
|
+
# @option fields [Boolean, nil] :prune_tags prune-tags flag
|
|
182
|
+
#
|
|
183
|
+
# @option fields [String, nil] :receivepack receive-pack path
|
|
184
|
+
#
|
|
185
|
+
# @option fields [String, nil] :uploadpack upload-pack path
|
|
186
|
+
#
|
|
187
|
+
# @option fields [Boolean, nil] :promisor promisor flag
|
|
188
|
+
#
|
|
189
|
+
# @option fields [String, nil] :partial_clone_filter partial-clone filter
|
|
190
|
+
#
|
|
191
|
+
# @option fields [String, nil] :vcs VCS type
|
|
192
|
+
#
|
|
193
|
+
# @return [Git::RemoteInfo] a new instance; `self` when no fields are given
|
|
194
|
+
#
|
|
195
|
+
# @raise [ArgumentError] if a key is not a member of this Data class
|
|
196
|
+
#
|
|
197
|
+
def with(**fields)
|
|
198
|
+
return self if fields.empty?
|
|
199
|
+
|
|
200
|
+
self.class.new(**to_h, **fields)
|
|
201
|
+
end
|
|
145
202
|
end
|
|
146
203
|
end
|
|
@@ -41,7 +41,7 @@ module Git
|
|
|
41
41
|
|
|
42
42
|
# Option keys accepted by {#checkout}
|
|
43
43
|
#
|
|
44
|
-
CHECKOUT_ALLOWED_OPTS = %i[force f new_branch b start_point].freeze
|
|
44
|
+
CHECKOUT_ALLOWED_OPTS = %i[force f new_branch b start_point orphan].freeze
|
|
45
45
|
private_constant :CHECKOUT_ALLOWED_OPTS
|
|
46
46
|
|
|
47
47
|
# Option keys accepted by {#checkout_index}
|
|
@@ -130,6 +130,9 @@ module Git
|
|
|
130
130
|
# @example Create a new branch with a name different from the start point
|
|
131
131
|
# repo.checkout('main', new_branch: 'new-feature')
|
|
132
132
|
#
|
|
133
|
+
# @example Create and check out an unborn branch with no history
|
|
134
|
+
# repo.checkout('gh-pages', orphan: true)
|
|
135
|
+
#
|
|
133
136
|
# @example Force checkout discarding local changes
|
|
134
137
|
# repo.checkout('main', force: true)
|
|
135
138
|
#
|
|
@@ -151,13 +154,25 @@ module Git
|
|
|
151
154
|
#
|
|
152
155
|
# @option opts [Boolean, nil] :f (nil) alias for `:force`
|
|
153
156
|
#
|
|
157
|
+
# @option opts [Boolean, String, nil] :orphan (nil) when `true`, creates a
|
|
158
|
+
# new unborn branch named `branch` whose first commit has no parents
|
|
159
|
+
#
|
|
160
|
+
# When a `String`, creates an unborn branch with that name, using
|
|
161
|
+
# `branch` as the start point for the working tree and index.
|
|
162
|
+
#
|
|
163
|
+
# `false` and `nil` are both treated as unset. A blank branch name is
|
|
164
|
+
# rejected rather than ignored.
|
|
165
|
+
#
|
|
154
166
|
# @option opts [String, nil] :start_point (nil) the commit or branch to
|
|
155
|
-
# start the new branch from; used together with `new_branch: true`
|
|
167
|
+
# start the new branch from; used together with `new_branch: true` or
|
|
168
|
+
# `orphan: true`
|
|
156
169
|
#
|
|
157
170
|
# @return [String] git's stdout from the checkout
|
|
158
171
|
#
|
|
159
172
|
# @raise [ArgumentError] if unsupported options are provided
|
|
160
173
|
#
|
|
174
|
+
# @raise [ArgumentError] if `:orphan` is given a blank or missing branch name
|
|
175
|
+
#
|
|
161
176
|
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
162
177
|
#
|
|
163
178
|
def checkout(branch = nil, opts = {})
|
|
@@ -172,6 +187,73 @@ module Git
|
|
|
172
187
|
Git::Commands::Checkout::Branch.new(@execution_context).call(target, **translated_opts).stdout
|
|
173
188
|
end
|
|
174
189
|
|
|
190
|
+
# Run a block with the given branch checked out, then restore the original branch
|
|
191
|
+
#
|
|
192
|
+
# Records the current branch (or the current commit when HEAD is detached),
|
|
193
|
+
# checks out `branch`, and yields to the block. If the block returns a truthy
|
|
194
|
+
# value, all pending changes are committed with `message` (see
|
|
195
|
+
# {#commit_all}); if it returns a falsy value, the index and working tree are
|
|
196
|
+
# hard-reset instead (see {#reset}). The original branch or commit is then
|
|
197
|
+
# checked out again. The hard reset discards changes to tracked files only;
|
|
198
|
+
# untracked files created by the block are left in place.
|
|
199
|
+
#
|
|
200
|
+
# Unlike `Git::Branch#in_branch`, this method does not create `branch`. The
|
|
201
|
+
# branch must be an existing local branch. Unlike {#checkout}, a commit SHA,
|
|
202
|
+
# tag, or remote-tracking branch is rejected before any checkout happens:
|
|
203
|
+
# those detach HEAD, and a commit made there would be left dangling once the
|
|
204
|
+
# original branch is restored. HEAD must
|
|
205
|
+
# be on a branch with at least one commit, or detached: an unborn branch (no
|
|
206
|
+
# commits yet) cannot be checked out again by name, so it is rejected before
|
|
207
|
+
# any checkout happens.
|
|
208
|
+
#
|
|
209
|
+
# **Note:** the restore checkout is not wrapped in `ensure`. If the block,
|
|
210
|
+
# the commit, or the reset raises an exception, the repository is left
|
|
211
|
+
# checked out on `branch` rather than restored to the original branch.
|
|
212
|
+
#
|
|
213
|
+
# @example Commit a new file on a feature branch
|
|
214
|
+
# repo.in_branch('feature', 'Add README') do
|
|
215
|
+
# File.write('README.md', '# Hello')
|
|
216
|
+
# repo.add('README.md')
|
|
217
|
+
# true # commit and return to the original branch
|
|
218
|
+
# end
|
|
219
|
+
#
|
|
220
|
+
# @example Discard experimental changes to a tracked file
|
|
221
|
+
# repo.in_branch('scratch') do
|
|
222
|
+
# File.write('README.md', '# Try something')
|
|
223
|
+
# false # hard-reset and return to the original branch
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
226
|
+
# @param branch [String] the name of an existing local branch to check out
|
|
227
|
+
#
|
|
228
|
+
# @param message [String] the commit message used when the block returns a
|
|
229
|
+
# truthy value
|
|
230
|
+
#
|
|
231
|
+
# @return [String] git's stdout from the final checkout back to the original
|
|
232
|
+
# branch or commit
|
|
233
|
+
#
|
|
234
|
+
# @raise [ArgumentError] if `branch` is not an existing local branch
|
|
235
|
+
#
|
|
236
|
+
# @raise [Git::Error] if HEAD is on an unborn branch
|
|
237
|
+
#
|
|
238
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
239
|
+
#
|
|
240
|
+
# @yield executes the block with `branch` checked out
|
|
241
|
+
#
|
|
242
|
+
# @yieldreturn [Object] a truthy value to commit all changes, a falsy value to
|
|
243
|
+
# hard-reset
|
|
244
|
+
#
|
|
245
|
+
def in_branch(branch, message = 'in branch work')
|
|
246
|
+
SharedPrivate.assert_local_branch!(self, branch)
|
|
247
|
+
restore_point = SharedPrivate.head_restore_point(self)
|
|
248
|
+
checkout(branch)
|
|
249
|
+
if yield
|
|
250
|
+
commit_all(message)
|
|
251
|
+
else
|
|
252
|
+
reset(nil, hard: true)
|
|
253
|
+
end
|
|
254
|
+
checkout(restore_point)
|
|
255
|
+
end
|
|
256
|
+
|
|
175
257
|
# Populate the working tree from the index
|
|
176
258
|
#
|
|
177
259
|
# @example Check out all files from the index
|
|
@@ -615,7 +697,31 @@ module Git
|
|
|
615
697
|
#
|
|
616
698
|
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
617
699
|
#
|
|
700
|
+
# @deprecated Use `branch_list(name).first` and the name-based branch
|
|
701
|
+
# operations instead
|
|
702
|
+
#
|
|
703
|
+
# {#branch_list} returns immutable {Git::BranchInfo} value objects
|
|
704
|
+
# rather than {Git::Branch}. It takes `git branch --list` patterns, so
|
|
705
|
+
# pass the short name of a local branch or `"#{remote}/#{name}"` for a
|
|
706
|
+
# remote-tracking branch; the `remotes/` and `refs/` prefixes this
|
|
707
|
+
# method accepts match nothing. A `"#{remote}/#{name}"` pattern also
|
|
708
|
+
# matches a local branch of that name, so take `find(&:remote?)` rather
|
|
709
|
+
# than `first` for a remote-tracking branch. With no argument this
|
|
710
|
+
# method wraps {#current_branch}, which is `'HEAD'` when HEAD is
|
|
711
|
+
# detached; {#branch_list} has no entry for a detached or unborn HEAD,
|
|
712
|
+
# so use {#current_branch_state} in those states. Call the
|
|
713
|
+
# corresponding {Git::Repository} method (e.g. {#checkout},
|
|
714
|
+
# {#branch_new}, {#branch_delete}) for operations on a branch.
|
|
715
|
+
#
|
|
716
|
+
# @see #branch_list
|
|
717
|
+
#
|
|
618
718
|
def branch(branch_name = current_branch)
|
|
719
|
+
Git::Deprecation.warn(
|
|
720
|
+
'Git::Repository#branch is deprecated and will be removed in v6.0.0. ' \
|
|
721
|
+
'Use Git::Repository#branch_list(name).first for a local branch, ' \
|
|
722
|
+
'Git::Repository#branch_list("remote/name").find(&:remote?) for a remote-tracking branch, ' \
|
|
723
|
+
'and the name-based branch operations instead.'
|
|
724
|
+
)
|
|
619
725
|
Git::Branch.new(self, branch_name)
|
|
620
726
|
end
|
|
621
727
|
|
|
@@ -642,7 +748,21 @@ module Git
|
|
|
642
748
|
#
|
|
643
749
|
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
644
750
|
#
|
|
751
|
+
# @deprecated Use {#branch_list} instead
|
|
752
|
+
#
|
|
753
|
+
# {#branch_list} returns `Array<Git::BranchInfo>` (immutable value
|
|
754
|
+
# objects) rather than a {Git::Branches} collection. Filter it with
|
|
755
|
+
# `select(&:remote?)` or `reject(&:remote?)` in place of
|
|
756
|
+
# `branches.remote` and `branches.local`, and look a branch up by name
|
|
757
|
+
# with `branch_list(name).first` in place of `branches[name]`.
|
|
758
|
+
#
|
|
759
|
+
# @see #branch_list
|
|
760
|
+
#
|
|
645
761
|
def branches
|
|
762
|
+
Git::Deprecation.warn(
|
|
763
|
+
'Git::Repository#branches is deprecated and will be removed in v6.0.0. ' \
|
|
764
|
+
'Use Git::Repository#branch_list instead.'
|
|
765
|
+
)
|
|
646
766
|
Git::Branches.new(self)
|
|
647
767
|
end
|
|
648
768
|
|
|
@@ -662,6 +782,11 @@ module Git
|
|
|
662
782
|
# @param execution_context [Git::ExecutionContext::Repository] the
|
|
663
783
|
# execution context for git commands
|
|
664
784
|
#
|
|
785
|
+
# The full `refs/heads/<name>` ref is verified rather than the bare name.
|
|
786
|
+
# A bare name follows the gitrevisions search order, in which
|
|
787
|
+
# `refs/tags/<name>` is tried before `refs/heads/<name>`, so an unborn
|
|
788
|
+
# branch that shares its name with a tag would be reported as `:active`.
|
|
789
|
+
#
|
|
665
790
|
# @param branch_name [String] the branch name to verify
|
|
666
791
|
#
|
|
667
792
|
# @return [:active, :unborn] the branch ref state
|
|
@@ -672,7 +797,7 @@ module Git
|
|
|
672
797
|
# @api private
|
|
673
798
|
#
|
|
674
799
|
def get_branch_state(execution_context, branch_name)
|
|
675
|
-
Git::Commands::RevParse.new(execution_context).call(branch_name, verify: true, quiet: true)
|
|
800
|
+
Git::Commands::RevParse.new(execution_context).call("refs/heads/#{branch_name}", verify: true, quiet: true)
|
|
676
801
|
:active
|
|
677
802
|
rescue Git::FailedError => e
|
|
678
803
|
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
|
|
@@ -680,19 +805,24 @@ module Git
|
|
|
680
805
|
:unborn
|
|
681
806
|
end
|
|
682
807
|
|
|
683
|
-
# Translates
|
|
808
|
+
# Translates {#checkout} options to the new command interface
|
|
684
809
|
#
|
|
685
810
|
# Legacy callers passed combinations like:
|
|
686
811
|
# checkout('branch', new_branch: true, start_point: 'main')
|
|
687
812
|
# which should map to:
|
|
688
813
|
# checkout('main', b: 'branch')
|
|
689
814
|
#
|
|
815
|
+
# `orphan: true` follows the same shape, naming the unborn branch:
|
|
816
|
+
# checkout('branch', orphan: true, start_point: 'main')
|
|
817
|
+
# maps to:
|
|
818
|
+
# checkout('main', orphan: 'branch')
|
|
819
|
+
#
|
|
690
820
|
# @param branch [String, nil] the branch argument passed to {#checkout}
|
|
691
821
|
#
|
|
692
822
|
# @param checkout_options [Hash] the raw options passed to {#checkout}
|
|
693
823
|
#
|
|
694
|
-
# @return [Array] a two-element tuple
|
|
695
|
-
# translated checkout arguments
|
|
824
|
+
# @return [Array((String, nil), Hash)] a two-element tuple
|
|
825
|
+
# `[target, options]` containing the translated checkout arguments
|
|
696
826
|
#
|
|
697
827
|
# `target` (`String` or `nil`) is the branch or commit to check out.
|
|
698
828
|
# `options` is a `Hash` of keyword arguments for
|
|
@@ -701,15 +831,69 @@ module Git
|
|
|
701
831
|
# @api private
|
|
702
832
|
#
|
|
703
833
|
def translate_checkout_opts(branch, checkout_options)
|
|
834
|
+
checkout_options = normalize_orphan_option(checkout_options)
|
|
835
|
+
|
|
704
836
|
if checkout_options[:new_branch] == true || checkout_options[:b] == true
|
|
705
837
|
[checkout_options[:start_point], checkout_options.except(:new_branch, :b, :start_point).merge(b: branch)]
|
|
706
838
|
elsif checkout_options[:new_branch].is_a?(String)
|
|
707
839
|
[branch, checkout_options.except(:new_branch).merge(b: checkout_options[:new_branch])]
|
|
840
|
+
elsif checkout_options[:orphan] == true
|
|
841
|
+
translate_orphan_opts(branch, checkout_options)
|
|
708
842
|
else
|
|
709
843
|
[branch, checkout_options]
|
|
710
844
|
end
|
|
711
845
|
end
|
|
712
846
|
|
|
847
|
+
# Normalizes the `:orphan` option, rejecting names that git would never see
|
|
848
|
+
#
|
|
849
|
+
# `:orphan` is a value option on the underlying command, so a literal
|
|
850
|
+
# `false` would be emitted as `--orphan false` and create a branch named
|
|
851
|
+
# "false". Flag options such as `:force` already ignore `false`; this
|
|
852
|
+
# gives `:orphan` the same behavior.
|
|
853
|
+
#
|
|
854
|
+
# A blank name is rejected rather than dropped: the argument DSL omits
|
|
855
|
+
# empty values, so `orphan: ''` would otherwise degrade silently into a
|
|
856
|
+
# plain checkout.
|
|
857
|
+
#
|
|
858
|
+
# @param checkout_options [Hash] the raw options passed to {#checkout}
|
|
859
|
+
#
|
|
860
|
+
# @return [Hash] the options with a `false` `:orphan` key removed
|
|
861
|
+
#
|
|
862
|
+
# @raise [ArgumentError] if `:orphan` is given a blank branch name
|
|
863
|
+
#
|
|
864
|
+
# @api private
|
|
865
|
+
#
|
|
866
|
+
def normalize_orphan_option(checkout_options)
|
|
867
|
+
orphan = checkout_options[:orphan]
|
|
868
|
+
return checkout_options.except(:orphan) if orphan == false
|
|
869
|
+
raise ArgumentError, 'orphan requires a non-empty branch name' if orphan.is_a?(String) && orphan.strip.empty?
|
|
870
|
+
|
|
871
|
+
checkout_options
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Translates `orphan: true` into the command's `:orphan` value option
|
|
875
|
+
#
|
|
876
|
+
# `orphan: true` names the unborn branch from the positional argument and
|
|
877
|
+
# takes its start point from `:start_point`, mirroring `new_branch: true`.
|
|
878
|
+
#
|
|
879
|
+
# @param branch [String, nil] the branch argument passed to {#checkout}
|
|
880
|
+
#
|
|
881
|
+
# @param checkout_options [Hash] the raw options passed to {#checkout}
|
|
882
|
+
#
|
|
883
|
+
# @return [Array((String, nil), Hash)] a two-element tuple
|
|
884
|
+
# `[target, options]` containing the translated checkout arguments
|
|
885
|
+
#
|
|
886
|
+
# @raise [ArgumentError] if `branch` is blank (`nil`, empty, or whitespace
|
|
887
|
+
# only), since the unborn branch would otherwise have no name
|
|
888
|
+
#
|
|
889
|
+
# @api private
|
|
890
|
+
#
|
|
891
|
+
def translate_orphan_opts(branch, checkout_options)
|
|
892
|
+
raise ArgumentError, 'orphan: true requires a branch name' if branch.to_s.strip.empty?
|
|
893
|
+
|
|
894
|
+
[checkout_options[:start_point], checkout_options.except(:start_point).merge(orphan: branch)]
|
|
895
|
+
end
|
|
896
|
+
|
|
713
897
|
# Normalizes path specifications for Git commands
|
|
714
898
|
#
|
|
715
899
|
# @param pathspecs [String, Pathname, Array<String, Pathname>, nil]
|