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.
@@ -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 is similar to Git::Branch::BRANCH_NAME_REGEXP but uses \A/\z anchors
17
- # instead of ^/$ for stricter matching. As part of the architectural redesign,
18
- # Git::Branch will eventually be refactored to use BranchInfo internally, at which
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 assumes remote names do not contain '/'. If a remote name
22
- # contains '/', parsing will be incorrect. For example, 'remotes/team/upstream/main'
23
- # would parse as remote_name='team' instead of 'team/upstream'. This is an inherent
24
- # ambiguity in git refnames that can only be resolved with knowledge of configured
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 # start of string
30
- (?:(?:refs/)?remotes/(?<remote_name>[^/]+)/)? # optional 'refs?/remotes/<remote_name>/'
31
- (?<branch_name>.+) # branch name (everything else)
32
- \z # end of string
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 Creating from git branch output
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
- # worktree: false,
51
+ # worktree_path: nil,
47
52
  # symref: nil,
48
- # upstream: nil
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
- # worktree: false,
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
- # @return [String] the branch refname (e.g., 'main', 'remotes/origin/main')
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 unavailable
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
- # @!attribute [r] worktree
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
- # Whether this branch is checked out in another linked worktree
114
+ # The absolute path of the *other* linked worktree this branch is checked
115
+ # out in, or nil.
113
116
  #
114
- # @return [Boolean] true if checked out in a different worktree
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 [Git::BranchInfo, nil] the upstream branch info, or nil if no upstream is configured
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 When upstream exists but the remote-tracking branch hasn't been fetched,
131
- # the upstream's target_oid may be nil
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, :worktree, :symref, :upstream) do
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 worktree? = worktree
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
@@ -30,7 +30,7 @@ module Git
30
30
 
31
31
  @base = base
32
32
 
33
- branch_repository.branches_all.each do |branch_info|
33
+ branch_repository.branch_list.each do |branch_info|
34
34
  branch = Git::Branch.new(base, branch_info)
35
35
 
36
36
  @branches[branch_info.refname] = branch
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. Call #execute and then #each on the result object.'
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. Call #execute and then #size on the result object.'
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. Call #execute and then #to_s on the result object.'
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. Call #execute and then #first on the result object.'
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. Call #execute and then #last on the result object.'
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. Call #execute and then #[] on the result object.'
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 a future version. ' \
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('Git::Object.new with is_tag argument is deprecated. Use Git::Object::Tag.new instead.')
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
 
@@ -38,18 +38,21 @@ module Git
38
38
  module Branch
39
39
  # Format string for git branch --format
40
40
  #
41
- # Fields (pipe-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
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 - target ref if symbolic reference, empty otherwise
47
- # 6. upstream - full upstream ref (e.g., refs/remotes/origin/main), empty if none
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
- FORMAT_STRING = '%(refname)|%(objectname)|%(HEAD)|%(worktreepath)|%(symref)|%(upstream)'
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("refs/heads/main|abc1234|*|||\nrefs/heads/feature|def5678||||\n")
70
- # # => [#<data Git::BranchInfo refname="main", ...>, #<data Git::BranchInfo refname="feature", ...>]
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 (pipe-delimited fields)
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: [refname, objectname, head, worktreepath, symref, upstream]
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: normalize_refname(raw_refname),
120
+ refname: raw_refname,
106
121
  target_oid: presence(objectname),
107
- current: current,
108
- worktree: in_other_worktree?(worktreepath, current),
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
- # Normalize a full refname to the expected format
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 [Git::BranchInfo, nil] upstream branch info or nil
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
- Git::BranchInfo.new(
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