git 5.0.0.beta.5 → 5.0.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/.gitignore +2 -0
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +520 -0
- data/README.md +53 -475
- data/UPGRADING.md +26 -0
- data/lib/git/branch.rb +5 -1
- data/lib/git/branch_info.rb +135 -76
- data/lib/git/branches.rb +1 -1
- data/lib/git/log.rb +12 -6
- data/lib/git/object.rb +5 -2
- data/lib/git/parsers/branch.rb +62 -64
- data/lib/git/parsers/remote.rb +162 -0
- data/lib/git/remote.rb +1 -22
- data/lib/git/remote_info.rb +146 -0
- data/lib/git/repository/branching.rb +57 -22
- data/lib/git/repository/committing.rb +25 -6
- data/lib/git/repository/context_helpers.rb +1 -1
- data/lib/git/repository/diffing.rb +2 -1
- data/lib/git/repository/factories.rb +10 -3
- data/lib/git/repository/merging.rb +5 -5
- data/lib/git/repository/remote_operations.rb +64 -8
- data/lib/git/repository/staging.rb +13 -4
- data/lib/git/repository/stashing.rb +1 -1
- data/lib/git/repository/status_operations.rb +1 -1
- data/lib/git/version.rb +1 -1
- data/lib/git.rb +8 -5
- data/redesign/branch_parse_refactor_plan.md +163 -0
- data/redesign/info_object_migration_plan.md +126 -0
- data/redesign/integration_test_analysis.md +521 -0
- data/redesign/remote_refactor_plan.md +164 -0
- metadata +10 -4
data/lib/git/branch_info.rb
CHANGED
|
@@ -9,54 +9,60 @@ module Git
|
|
|
9
9
|
#
|
|
10
10
|
# @example Parse branch refnames
|
|
11
11
|
# 'main' => { remote_name: nil, branch_name: 'main' }
|
|
12
|
+
# 'refs/heads/main' => { remote_name: nil, branch_name: 'main' }
|
|
12
13
|
# 'remotes/origin/main' => { remote_name: 'origin', branch_name: 'main' }
|
|
14
|
+
# 'refs/remotes/origin/main' => { remote_name: 'origin', branch_name: 'main' }
|
|
13
15
|
# 'feature/foo' => { remote_name: nil, branch_name: 'feature/foo' }
|
|
14
16
|
# 'remotes/origin/feature/bar' => { remote_name: 'origin', branch_name: 'feature/bar' }
|
|
15
17
|
#
|
|
16
|
-
# @note This regex
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# point this will become the single source of truth for branch name parsing.
|
|
18
|
+
# @note This regex handles both raw full refs (e.g., `refs/heads/main`) as stored in
|
|
19
|
+
# {Git::BranchInfo#refname} and normalized short-form refs (e.g., `main`,
|
|
20
|
+
# `remotes/origin/main`) used elsewhere.
|
|
20
21
|
#
|
|
21
|
-
# @note This regex
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# remotes. See: https://github.com/ruby-git/ruby-git/issues/919
|
|
22
|
+
# @note This regex is a fallback for branch refnames parsed without configured
|
|
23
|
+
# remote context. Remote names containing '/' can only be resolved reliably
|
|
24
|
+
# when the parser is given the configured remote names. See:
|
|
25
|
+
# https://github.com/ruby-git/ruby-git/issues/919
|
|
26
26
|
#
|
|
27
27
|
# @api private
|
|
28
28
|
BRANCH_REFNAME_REGEXP = %r{
|
|
29
|
-
\A
|
|
30
|
-
(?:
|
|
31
|
-
(?<
|
|
32
|
-
|
|
29
|
+
\A # start of string
|
|
30
|
+
(?:refs/heads/)? # optional refs/heads/ prefix (stripped)
|
|
31
|
+
(?:(?:refs/)?remotes/(?<remote_name>[^/]+)/)? # optional refs?/remotes/<remote_name>/
|
|
32
|
+
(?<branch_name>.+) # branch name (everything else)
|
|
33
|
+
\z # end of string
|
|
33
34
|
}x
|
|
34
35
|
|
|
36
|
+
# Sentinel for distinguishing omitted BranchInfo remote_name from explicit nil
|
|
37
|
+
REMOTE_NAME_NOT_GIVEN = Object.new.freeze
|
|
38
|
+
private_constant :REMOTE_NAME_NOT_GIVEN
|
|
39
|
+
|
|
35
40
|
# Value object representing branch metadata from git branch output
|
|
36
41
|
#
|
|
37
42
|
# This is a lightweight, immutable data structure returned by branch listing
|
|
38
43
|
# commands. It contains only the data parsed from git output without any
|
|
39
44
|
# repository context or operations.
|
|
40
45
|
#
|
|
41
|
-
# @example
|
|
46
|
+
# @example Local branch with upstream tracking
|
|
42
47
|
# info = Git::BranchInfo.new(
|
|
43
|
-
# refname: 'main',
|
|
48
|
+
# refname: 'refs/heads/main',
|
|
44
49
|
# target_oid: 'abc123def456789012345678901234567890abcd',
|
|
45
50
|
# current: true,
|
|
46
|
-
#
|
|
51
|
+
# worktree_path: nil,
|
|
47
52
|
# symref: nil,
|
|
48
|
-
# upstream:
|
|
53
|
+
# upstream: 'refs/remotes/origin/main'
|
|
49
54
|
# )
|
|
50
55
|
# info.current? #=> true
|
|
51
56
|
# info.remote? #=> false
|
|
52
57
|
# info.short_name #=> 'main'
|
|
58
|
+
# info.upstream #=> 'refs/remotes/origin/main'
|
|
53
59
|
#
|
|
54
|
-
# @example Remote branch
|
|
60
|
+
# @example Remote-tracking branch
|
|
55
61
|
# info = Git::BranchInfo.new(
|
|
56
|
-
# refname: 'remotes/origin/main',
|
|
62
|
+
# refname: 'refs/remotes/origin/main',
|
|
57
63
|
# target_oid: 'abc123def456789012345678901234567890abcd',
|
|
58
64
|
# current: false,
|
|
59
|
-
#
|
|
65
|
+
# worktree_path: nil,
|
|
60
66
|
# symref: nil,
|
|
61
67
|
# upstream: nil
|
|
62
68
|
# )
|
|
@@ -64,25 +70,6 @@ module Git
|
|
|
64
70
|
# info.remote_name #=> 'origin'
|
|
65
71
|
# info.short_name #=> 'main'
|
|
66
72
|
#
|
|
67
|
-
# @example Local branch with upstream tracking
|
|
68
|
-
# upstream_info = Git::BranchInfo.new(
|
|
69
|
-
# refname: 'remotes/origin/main',
|
|
70
|
-
# target_oid: 'abc123def456789012345678901234567890abcd',
|
|
71
|
-
# current: false,
|
|
72
|
-
# worktree: false,
|
|
73
|
-
# symref: nil,
|
|
74
|
-
# upstream: nil
|
|
75
|
-
# )
|
|
76
|
-
# info = Git::BranchInfo.new(
|
|
77
|
-
# refname: 'main',
|
|
78
|
-
# target_oid: 'abc123def456789012345678901234567890abcd',
|
|
79
|
-
# current: true,
|
|
80
|
-
# worktree: false,
|
|
81
|
-
# symref: nil,
|
|
82
|
-
# upstream: upstream_info
|
|
83
|
-
# )
|
|
84
|
-
# info.upstream.remote_name #=> 'origin'
|
|
85
|
-
#
|
|
86
73
|
# @see Git::Branch for the full-featured branch object with operations
|
|
87
74
|
#
|
|
88
75
|
# @see Git::Commands::Branch::List for the command that produces these
|
|
@@ -93,13 +80,24 @@ module Git
|
|
|
93
80
|
#
|
|
94
81
|
# The full reference name of the branch
|
|
95
82
|
#
|
|
96
|
-
#
|
|
83
|
+
# Must be the full refname as returned by git (e.g., 'refs/heads/main',
|
|
84
|
+
# 'refs/remotes/origin/main') because the short name alone is not guaranteed to
|
|
85
|
+
# be unique (e.g., 'main' could exist as both a local and remote branch).
|
|
86
|
+
#
|
|
87
|
+
# @return [String] the branch refname (e.g., 'refs/heads/main',
|
|
88
|
+
# 'refs/remotes/origin/main')
|
|
89
|
+
#
|
|
90
|
+
# @!attribute [r] remote_name
|
|
91
|
+
#
|
|
92
|
+
# @return [String, nil] the resolved or fallback-derived remote name, or nil
|
|
93
|
+
# for local branches
|
|
97
94
|
#
|
|
98
95
|
# @!attribute [r] target_oid
|
|
99
96
|
#
|
|
100
|
-
# The commit object ID (SHA) that this branch points to
|
|
97
|
+
# The commit object ID (SHA) that this branch points to (aka HEAD)
|
|
101
98
|
#
|
|
102
|
-
# @return [String, nil] the full 40-character object ID, or nil if
|
|
99
|
+
# @return [String, nil] the full 40-character object ID, or nil if branch is
|
|
100
|
+
# unborn (no commits yet)
|
|
103
101
|
#
|
|
104
102
|
# @!attribute [r] current
|
|
105
103
|
#
|
|
@@ -107,11 +105,28 @@ module Git
|
|
|
107
105
|
#
|
|
108
106
|
# @return [Boolean] true if this is the current branch
|
|
109
107
|
#
|
|
110
|
-
#
|
|
108
|
+
# @note A branch can be current ({#current?} true) or in another worktree
|
|
109
|
+
# ({#other_worktree?} true), but never both. A branch not checked out
|
|
110
|
+
# anywhere has both false.
|
|
111
|
+
#
|
|
112
|
+
# @!attribute [r] worktree_path
|
|
111
113
|
#
|
|
112
|
-
#
|
|
114
|
+
# The absolute path of the *other* linked worktree this branch is checked
|
|
115
|
+
# out in, or nil.
|
|
113
116
|
#
|
|
114
|
-
#
|
|
117
|
+
# This is nil in two distinct cases:
|
|
118
|
+
# - The branch is the current branch in this worktree (use {#current?} to
|
|
119
|
+
# distinguish that case)
|
|
120
|
+
# - The branch is not checked out in any worktree
|
|
121
|
+
#
|
|
122
|
+
# This path is suppressed for the current branch even though git reports it
|
|
123
|
+
# via `%(worktreepath)`, because the current worktree's path is already
|
|
124
|
+
# known from the repository object and storing it here would make
|
|
125
|
+
# {#other_worktree?} incorrect.
|
|
126
|
+
#
|
|
127
|
+
# @return [String, nil] the absolute path of the linked worktree root
|
|
128
|
+
# directory (e.g., `'/home/user/projects/my-repo-hotfix'`), or nil if
|
|
129
|
+
# the branch is not checked out in a different linked worktree
|
|
115
130
|
#
|
|
116
131
|
# @!attribute [r] symref
|
|
117
132
|
#
|
|
@@ -121,27 +136,94 @@ module Git
|
|
|
121
136
|
#
|
|
122
137
|
# @!attribute [r] upstream
|
|
123
138
|
#
|
|
124
|
-
# The configured upstream/tracking branch
|
|
139
|
+
# The configured upstream/tracking branch refname as reported by git
|
|
125
140
|
#
|
|
126
|
-
# @return [
|
|
141
|
+
# @return [String, nil] the raw upstream refname from `%(upstream)`
|
|
142
|
+
# (e.g., `'refs/remotes/origin/main'`), or nil if no upstream is configured
|
|
127
143
|
#
|
|
128
|
-
# @note Remote-tracking branches (e.g., 'origin/main') have upstream: nil
|
|
144
|
+
# @note Remote-tracking branches (e.g., `'refs/remotes/origin/main'`) have upstream: nil
|
|
129
145
|
#
|
|
130
|
-
# @note
|
|
131
|
-
#
|
|
146
|
+
# @note This is the raw refname snapshot from when the branch list was read.
|
|
147
|
+
# It does not reflect live git state after the snapshot was taken.
|
|
132
148
|
#
|
|
133
|
-
BranchInfo = Data.define(:refname, :target_oid, :current, :
|
|
149
|
+
BranchInfo = Data.define(:refname, :remote_name, :target_oid, :current, :worktree_path, :symref, :upstream) do
|
|
150
|
+
# @param refname [String] the full branch refname
|
|
151
|
+
#
|
|
152
|
+
# @param remote_name [String, nil] resolved remote name, nil for local branches,
|
|
153
|
+
# or omitted to derive from `refname`
|
|
154
|
+
#
|
|
155
|
+
# @param target_oid [String, nil] the commit object ID, or nil for unborn branches
|
|
156
|
+
#
|
|
157
|
+
# @param current [Boolean] whether this branch is currently checked out
|
|
158
|
+
#
|
|
159
|
+
# @param worktree_path [String, nil] path to another linked worktree, or nil
|
|
160
|
+
#
|
|
161
|
+
# @param symref [String, nil] symbolic reference target, or nil
|
|
162
|
+
#
|
|
163
|
+
# @param upstream [String, nil] upstream refname, or nil
|
|
164
|
+
#
|
|
165
|
+
def initialize(refname:, target_oid:, current:, worktree_path:, symref:, upstream:, # rubocop:disable Metrics/ParameterLists
|
|
166
|
+
remote_name: REMOTE_NAME_NOT_GIVEN)
|
|
167
|
+
remote_name = self.class.fallback_remote_name(refname) if remote_name.equal?(REMOTE_NAME_NOT_GIVEN)
|
|
168
|
+
self.class.validate_remote_name!(refname, remote_name)
|
|
169
|
+
|
|
170
|
+
super
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# @param refname [String] the branch refname to validate
|
|
174
|
+
#
|
|
175
|
+
# @param remote_name [String, nil] the remote name to validate
|
|
176
|
+
#
|
|
177
|
+
# @return [void]
|
|
178
|
+
#
|
|
179
|
+
# @raise [ArgumentError] if the remote name contradicts the refname type
|
|
180
|
+
def self.validate_remote_name!(refname, remote_name)
|
|
181
|
+
if remote_tracking_refname?(refname)
|
|
182
|
+
unless remote_name.is_a?(String) && !remote_name.empty?
|
|
183
|
+
raise ArgumentError, 'remote_name must be a non-empty String for remote-tracking refname'
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
remote_ref_prefix = %r{\A(?:refs/)?remotes/#{Regexp.escape(remote_name)}/}
|
|
187
|
+
raise ArgumentError, 'remote_name must match remote-tracking refname' unless refname.match?(remote_ref_prefix)
|
|
188
|
+
elsif !remote_name.nil?
|
|
189
|
+
raise ArgumentError, 'remote_name must be nil for local branch refname'
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @param refname [String] the branch refname to parse
|
|
194
|
+
#
|
|
195
|
+
# @return [String, nil] the regex-derived remote name, or nil for local branches
|
|
196
|
+
def self.fallback_remote_name(refname)
|
|
197
|
+
refname.match(Git::BRANCH_REFNAME_REGEXP)[:remote_name]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# @param refname [String] the branch refname to inspect
|
|
201
|
+
#
|
|
202
|
+
# @return [Boolean] true if the refname is a remote-tracking refname
|
|
203
|
+
def self.remote_tracking_refname?(refname)
|
|
204
|
+
refname.match?(%r{\A(?:refs/)?remotes/[^/]+/.+})
|
|
205
|
+
end
|
|
206
|
+
|
|
134
207
|
# @return [Boolean] always false for BranchInfo (see DetachedHeadInfo for detached state)
|
|
135
208
|
def detached? = false
|
|
136
209
|
|
|
137
210
|
# @return [Boolean] true if this is an unborn branch (no commits yet)
|
|
138
211
|
def unborn? = target_oid.nil?
|
|
139
212
|
|
|
213
|
+
# @return [String] the short branch name without any remote or heads prefix
|
|
214
|
+
# (e.g., 'main' or 'feature/foo')
|
|
215
|
+
def short_name
|
|
216
|
+
return refname.delete_prefix('refs/heads/') if remote_name.nil?
|
|
217
|
+
|
|
218
|
+
remote_ref_prefix = %r{\A(?:refs/)?remotes/#{Regexp.escape(remote_name)}/}
|
|
219
|
+
refname.sub(remote_ref_prefix, '')
|
|
220
|
+
end
|
|
221
|
+
|
|
140
222
|
# @return [Boolean] true if this is the currently checked out branch
|
|
141
223
|
def current? = current
|
|
142
224
|
|
|
143
|
-
# @return [Boolean] true if this branch is checked out in another worktree
|
|
144
|
-
def
|
|
225
|
+
# @return [Boolean] true if this branch is checked out in another linked worktree
|
|
226
|
+
def other_worktree? = !worktree_path.nil?
|
|
145
227
|
|
|
146
228
|
# @return [Boolean] true if this is a symbolic reference
|
|
147
229
|
def symref? = !symref.nil?
|
|
@@ -149,30 +231,7 @@ module Git
|
|
|
149
231
|
# @return [Boolean] true if this is a remote-tracking branch
|
|
150
232
|
def remote? = !remote_name.nil?
|
|
151
233
|
|
|
152
|
-
# @return [String, nil] the name of the remote (e.g., 'origin'), or nil for local branches
|
|
153
|
-
def remote_name
|
|
154
|
-
parse_refname[:remote_name]
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# @return [String] the branch name without remote prefix (e.g., 'main' or 'feature/foo')
|
|
158
|
-
def short_name
|
|
159
|
-
parse_refname[:branch_name]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
234
|
# @return [String] string representation (the full refname)
|
|
163
235
|
def to_s = refname
|
|
164
|
-
|
|
165
|
-
private
|
|
166
|
-
|
|
167
|
-
# Parse the refname and return match data
|
|
168
|
-
#
|
|
169
|
-
# The regex is guaranteed to match any non-empty string due to the `.+` pattern,
|
|
170
|
-
# so we don't need nil checking. If refname is empty/nil, this would fail at
|
|
171
|
-
# object creation time since refname is a required attribute.
|
|
172
|
-
#
|
|
173
|
-
# @return [MatchData] the match result
|
|
174
|
-
def parse_refname
|
|
175
|
-
refname.match(Git::BRANCH_REFNAME_REGEXP)
|
|
176
|
-
end
|
|
177
236
|
end
|
|
178
237
|
end
|
data/lib/git/branches.rb
CHANGED
data/lib/git/log.rb
CHANGED
|
@@ -222,7 +222,8 @@ module Git
|
|
|
222
222
|
# @deprecated Use {#execute} and call `each` on the result.
|
|
223
223
|
def each(&)
|
|
224
224
|
Git::Deprecation.warn(
|
|
225
|
-
'Calling Git::Log#each is deprecated
|
|
225
|
+
'Calling Git::Log#each is deprecated and will be removed in v6.0.0. ' \
|
|
226
|
+
'Call #execute and then #each on the result object.'
|
|
226
227
|
)
|
|
227
228
|
run_log_if_dirty
|
|
228
229
|
@commits.each(&)
|
|
@@ -231,7 +232,8 @@ module Git
|
|
|
231
232
|
# @deprecated Use {#execute} and call `size` on the result.
|
|
232
233
|
def size
|
|
233
234
|
Git::Deprecation.warn(
|
|
234
|
-
'Calling Git::Log#size is deprecated
|
|
235
|
+
'Calling Git::Log#size is deprecated and will be removed in v6.0.0. ' \
|
|
236
|
+
'Call #execute and then #size on the result object.'
|
|
235
237
|
)
|
|
236
238
|
run_log_if_dirty
|
|
237
239
|
@commits.size
|
|
@@ -240,7 +242,8 @@ module Git
|
|
|
240
242
|
# @deprecated Use {#execute} and call `to_s` on the result.
|
|
241
243
|
def to_s
|
|
242
244
|
Git::Deprecation.warn(
|
|
243
|
-
'Calling Git::Log#to_s is deprecated
|
|
245
|
+
'Calling Git::Log#to_s is deprecated and will be removed in v6.0.0. ' \
|
|
246
|
+
'Call #execute and then #to_s on the result object.'
|
|
244
247
|
)
|
|
245
248
|
run_log_if_dirty
|
|
246
249
|
@commits.join("\n")
|
|
@@ -249,7 +252,8 @@ module Git
|
|
|
249
252
|
# @deprecated Use {#execute} and call the method on the result.
|
|
250
253
|
def first
|
|
251
254
|
Git::Deprecation.warn(
|
|
252
|
-
'Calling Git::Log#first is deprecated
|
|
255
|
+
'Calling Git::Log#first is deprecated and will be removed in v6.0.0. ' \
|
|
256
|
+
'Call #execute and then #first on the result object.'
|
|
253
257
|
)
|
|
254
258
|
run_log_if_dirty
|
|
255
259
|
@commits.first
|
|
@@ -258,7 +262,8 @@ module Git
|
|
|
258
262
|
# @deprecated Use {#execute} and call the method on the result.
|
|
259
263
|
def last
|
|
260
264
|
Git::Deprecation.warn(
|
|
261
|
-
'Calling Git::Log#last is deprecated
|
|
265
|
+
'Calling Git::Log#last is deprecated and will be removed in v6.0.0. ' \
|
|
266
|
+
'Call #execute and then #last on the result object.'
|
|
262
267
|
)
|
|
263
268
|
run_log_if_dirty
|
|
264
269
|
@commits.last
|
|
@@ -273,7 +278,8 @@ module Git
|
|
|
273
278
|
#
|
|
274
279
|
def [](index)
|
|
275
280
|
Git::Deprecation.warn(
|
|
276
|
-
'Calling Git::Log#[] is deprecated
|
|
281
|
+
'Calling Git::Log#[] is deprecated and will be removed in v6.0.0. ' \
|
|
282
|
+
'Call #execute and then #[] on the result object.'
|
|
277
283
|
)
|
|
278
284
|
run_log_if_dirty
|
|
279
285
|
@commits[index]
|
data/lib/git/object.rb
CHANGED
|
@@ -485,7 +485,7 @@ module Git
|
|
|
485
485
|
#
|
|
486
486
|
def set_commit(data) # rubocop:disable Naming/AccessorMethodName
|
|
487
487
|
Git::Deprecation.warn(
|
|
488
|
-
'Git::Object::Commit#set_commit is deprecated and will be removed in
|
|
488
|
+
'Git::Object::Commit#set_commit is deprecated and will be removed in v6.0.0. ' \
|
|
489
489
|
'Use #from_data instead.'
|
|
490
490
|
)
|
|
491
491
|
from_data(data)
|
|
@@ -660,7 +660,10 @@ module Git
|
|
|
660
660
|
# @deprecated use `Git::Object::Tag.new` instead
|
|
661
661
|
#
|
|
662
662
|
private_class_method def self.new_tag(base, objectish)
|
|
663
|
-
Git::Deprecation.warn(
|
|
663
|
+
Git::Deprecation.warn(
|
|
664
|
+
'Git::Object.new with is_tag argument is deprecated and will be removed in v6.0.0. ' \
|
|
665
|
+
'Use Git::Object::Tag.new instead.'
|
|
666
|
+
)
|
|
664
667
|
Git::Object::Tag.new(base, objectish)
|
|
665
668
|
end
|
|
666
669
|
|
data/lib/git/parsers/branch.rb
CHANGED
|
@@ -38,18 +38,21 @@ module Git
|
|
|
38
38
|
module Branch
|
|
39
39
|
# Format string for git branch --format
|
|
40
40
|
#
|
|
41
|
-
# Fields (
|
|
42
|
-
# 1. refname
|
|
43
|
-
# 2. objectname
|
|
44
|
-
# 3. HEAD
|
|
41
|
+
# Fields (null-delimited):
|
|
42
|
+
# 1. refname - full ref name (e.g., refs/heads/main, refs/remotes/origin/main)
|
|
43
|
+
# 2. objectname - full SHA of the commit the branch points to
|
|
44
|
+
# 3. HEAD - '*' if current branch, empty otherwise
|
|
45
45
|
# 4. worktreepath - path if checked out in another worktree, empty otherwise
|
|
46
|
-
# 5. symref
|
|
47
|
-
# 6. upstream
|
|
46
|
+
# 5. symref - target ref if symbolic reference, empty otherwise
|
|
47
|
+
# 6. upstream - full upstream ref (e.g., refs/remotes/origin/main), empty if none
|
|
48
48
|
#
|
|
49
|
-
|
|
49
|
+
# Null bytes (%00) are used as field delimiters so that worktree paths
|
|
50
|
+
# containing special characters (including '|') parse correctly.
|
|
51
|
+
#
|
|
52
|
+
FORMAT_STRING = '%(refname)%00%(objectname)%00%(HEAD)%00%(worktreepath)%00%(symref)%00%(upstream)'
|
|
50
53
|
|
|
51
54
|
# Delimiter used in format output
|
|
52
|
-
FIELD_DELIMITER =
|
|
55
|
+
FIELD_DELIMITER = "\0"
|
|
53
56
|
|
|
54
57
|
# Regex to parse successful deletion lines from stdout
|
|
55
58
|
# Matches: Deleted branch branchname (was abc123).
|
|
@@ -65,52 +68,84 @@ module Git
|
|
|
65
68
|
|
|
66
69
|
# Parse git branch --list output into BranchInfo objects
|
|
67
70
|
#
|
|
68
|
-
# @example
|
|
69
|
-
# Git::Parsers::Branch.parse_list(
|
|
70
|
-
#
|
|
71
|
+
# @example Parse NUL-delimited branch list output
|
|
72
|
+
# Git::Parsers::Branch.parse_list(
|
|
73
|
+
# "refs/heads/main\0abc1234\0*\0\0\0\n" \
|
|
74
|
+
# "refs/heads/feature\0def5678\0\0\0\0\n"
|
|
75
|
+
# )
|
|
76
|
+
# # => [#<data Git::BranchInfo refname="refs/heads/main", ...>,
|
|
77
|
+
# # #<data Git::BranchInfo refname="refs/heads/feature", ...>]
|
|
71
78
|
#
|
|
72
79
|
# @param stdout [String] output from git branch --list --format=...
|
|
73
80
|
#
|
|
81
|
+
# @param remote_names [Array<String>] configured remote names used to resolve
|
|
82
|
+
# remote-tracking refs with slash-containing remote names
|
|
83
|
+
#
|
|
74
84
|
# @return [Array<Git::BranchInfo>] parsed branch information
|
|
75
85
|
#
|
|
76
|
-
def parse_list(stdout)
|
|
77
|
-
stdout.split("\n").filter_map { |line| parse_branch_line(line) }
|
|
86
|
+
def parse_list(stdout, remote_names: [])
|
|
87
|
+
stdout.split("\n").filter_map { |line| parse_branch_line(line, remote_names: remote_names) }
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
# Parse a single formatted branch line
|
|
81
91
|
#
|
|
82
|
-
# @param line [String] the line to parse (
|
|
92
|
+
# @param line [String] the line to parse (NUL-delimited fields)
|
|
93
|
+
#
|
|
94
|
+
# @param remote_names [Array<String>] configured remote names used to resolve
|
|
95
|
+
# remote-tracking refs with slash-containing remote names
|
|
83
96
|
#
|
|
84
97
|
# @return [Git::BranchInfo, nil] branch info object, or nil if line should be skipped
|
|
85
98
|
#
|
|
86
|
-
def parse_branch_line(line)
|
|
99
|
+
def parse_branch_line(line, remote_names: [])
|
|
87
100
|
fields = line.split(FIELD_DELIMITER, 6)
|
|
88
101
|
|
|
89
102
|
return nil if non_branch_entry?(fields[0])
|
|
90
103
|
|
|
91
|
-
build_branch_info(fields)
|
|
104
|
+
build_branch_info(fields, remote_names: remote_names)
|
|
92
105
|
end
|
|
93
106
|
|
|
94
107
|
# Build a BranchInfo from parsed fields
|
|
95
108
|
#
|
|
96
|
-
# @param fields [Array<String>] the parsed fields:
|
|
109
|
+
# @param fields [Array<String>] the parsed fields:
|
|
110
|
+
# [refname, objectname, head, worktreepath, symref, upstream]
|
|
111
|
+
#
|
|
112
|
+
# @param remote_names [Array<String>] configured remote names used to resolve
|
|
113
|
+
# remote-tracking refs with slash-containing remote names
|
|
97
114
|
#
|
|
98
115
|
# @return [Git::BranchInfo] the branch info object
|
|
99
116
|
#
|
|
100
|
-
def build_branch_info(fields)
|
|
117
|
+
def build_branch_info(fields, remote_names: [])
|
|
101
118
|
raw_refname, objectname, head, worktreepath, symref, upstream = fields
|
|
102
|
-
current = head == '*'
|
|
103
|
-
|
|
104
119
|
Git::BranchInfo.new(
|
|
105
|
-
refname:
|
|
120
|
+
refname: raw_refname,
|
|
106
121
|
target_oid: presence(objectname),
|
|
107
|
-
current:
|
|
108
|
-
|
|
122
|
+
current: head == '*',
|
|
123
|
+
worktree_path: head == '*' ? nil : presence(worktreepath),
|
|
109
124
|
symref: presence(symref),
|
|
110
|
-
upstream: build_upstream_info(upstream)
|
|
125
|
+
upstream: build_upstream_info(upstream),
|
|
126
|
+
remote_name: resolve_remote_name(raw_refname, remote_names)
|
|
111
127
|
)
|
|
112
128
|
end
|
|
113
129
|
|
|
130
|
+
# Resolve a remote-tracking refname to a configured remote name
|
|
131
|
+
#
|
|
132
|
+
# @param refname [String] the branch refname to inspect
|
|
133
|
+
#
|
|
134
|
+
# @param remote_names [Array<String>] configured remote names
|
|
135
|
+
#
|
|
136
|
+
# @return [String, nil] the resolved remote name, or nil for local branches
|
|
137
|
+
#
|
|
138
|
+
def resolve_remote_name(refname, remote_names)
|
|
139
|
+
remote_path = refname[%r{\A(?:refs/)?remotes/(.+)\z}, 1]
|
|
140
|
+
return nil if remote_path.nil?
|
|
141
|
+
|
|
142
|
+
configured_remote_name = remote_names
|
|
143
|
+
.select { |remote_name| remote_path.start_with?("#{remote_name}/") }
|
|
144
|
+
.max_by(&:length)
|
|
145
|
+
|
|
146
|
+
configured_remote_name || Git::BranchInfo.fallback_remote_name(refname)
|
|
147
|
+
end
|
|
148
|
+
|
|
114
149
|
# Check if the refname represents a detached HEAD state or non-branch entry
|
|
115
150
|
#
|
|
116
151
|
# Git outputs special entries for detached HEAD and non-branch states:
|
|
@@ -125,53 +160,16 @@ module Git
|
|
|
125
160
|
refname.match?(/^\(HEAD detached/) || refname.match?(/^\(not a branch\)/)
|
|
126
161
|
end
|
|
127
162
|
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
# Converts:
|
|
131
|
-
# - refs/heads/main -> main
|
|
132
|
-
# - refs/remotes/origin/main -> remotes/origin/main
|
|
133
|
-
#
|
|
134
|
-
# @param refname [String] the full refname from git
|
|
135
|
-
#
|
|
136
|
-
# @return [String] normalized refname
|
|
137
|
-
#
|
|
138
|
-
def normalize_refname(refname)
|
|
139
|
-
refname.sub(%r{^refs/heads/}, '').sub(%r{^refs/}, '')
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Check if the branch is checked out in another worktree
|
|
143
|
-
#
|
|
144
|
-
# worktree is true when the branch is checked out in ANOTHER worktree
|
|
145
|
-
# (worktreepath is non-empty AND it's not the current branch)
|
|
146
|
-
#
|
|
147
|
-
# @param worktreepath [String, nil] the worktree path from git output
|
|
148
|
-
#
|
|
149
|
-
# @param current [Boolean] whether this is the current branch
|
|
150
|
-
#
|
|
151
|
-
# @return [Boolean] true if checked out in another worktree
|
|
152
|
-
#
|
|
153
|
-
def in_other_worktree?(worktreepath, current)
|
|
154
|
-
has_worktree = !worktreepath.nil? && !worktreepath.empty?
|
|
155
|
-
has_worktree && !current
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# Build upstream BranchInfo from upstream refname
|
|
163
|
+
# Return the raw upstream refname string, or nil if empty
|
|
159
164
|
#
|
|
160
165
|
# @param upstream_ref [String, nil] the upstream ref (e.g., 'refs/remotes/origin/main')
|
|
161
166
|
#
|
|
162
|
-
# @return [
|
|
167
|
+
# @return [String, nil] the raw upstream refname, or nil
|
|
163
168
|
#
|
|
164
169
|
def build_upstream_info(upstream_ref)
|
|
165
170
|
return nil if upstream_ref.nil? || upstream_ref.empty?
|
|
166
171
|
|
|
167
|
-
|
|
168
|
-
refname: normalize_refname(upstream_ref),
|
|
169
|
-
target_oid: nil, # We don't have upstream's OID from this format
|
|
170
|
-
current: false,
|
|
171
|
-
worktree: false,
|
|
172
|
-
symref: nil,
|
|
173
|
-
upstream: nil # Upstream branches don't have their own upstream in this context
|
|
174
|
-
)
|
|
172
|
+
upstream_ref
|
|
175
173
|
end
|
|
176
174
|
|
|
177
175
|
# Return value if non-empty, nil otherwise
|