git 4.3.2 → 4.4.1

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: 317aadaf4169f420466db692337037d90a5a7085dcc84b2b167a9dd5247f369a
4
- data.tar.gz: 4308bcfb71982891d57cdec3c75d66c640c96788ac924f246df8c0d76a09fbf1
3
+ metadata.gz: a24c04e4ade577716464aaf17cdac3f1e1d7e860e60dcd9ed964eb8f0f503b4b
4
+ data.tar.gz: 7913679e60e68c2f51d9189c150dbf8310244afd71a38a0a7ff88845e58d2d96
5
5
  SHA512:
6
- metadata.gz: 6e0b4a0da59ed13a76d46c68619ce882cdfaf9512b5ed3b1c0da3c57f5a763804cf866e3cd6ec374191d1cf506ab05953e4ad62878d28c771cb746be11cf2d94
7
- data.tar.gz: a3db0a284e6211bfd9684422748b998df7cb8f21d8654f6a4e399879b7670287bc515bccfd7fdd885219fa39c167b738ab2b6ca694801523f5ffdb58d51d82ac
6
+ metadata.gz: 3e768e8f510d5919d7bacae8095da345e8446546e39e0567aaa2b242700fc4a2045bd6328bd8c5bfa0876375380b679f07aaa33992ebf4828b5298999e033201
7
+ data.tar.gz: a80276c4a8d613aae86a1adfdaa5f39aabfd3f85e9b4f3dc703612a4fea7afc452f37b9b5d4f3a6d632a2a1e8d12427a5f6f35697ed8daa16e8b2aaebfca5d29
@@ -53,3 +53,68 @@ jobs:
53
53
 
54
54
  - name: Test Gem
55
55
  run: bundle exec rake test:gem
56
+
57
+ # The branch ruleset cannot require the matrix job above directly. A required status
58
+ # check is matched by name, and `build`'s name is a template -- GitHub only expands it
59
+ # into "Ruby 3.2 on ubuntu-latest" and friends when it evaluates the matrix. The
60
+ # job-level `if:` short-circuits before that happens, so a release PR reports one check
61
+ # named literally "Ruby ${{ matrix.ruby }} on ${{ matrix.operating-system }}" and the
62
+ # six expanded names never report at all. Required checks that never report block a PR
63
+ # indefinitely rather than passing, which is what stranded the v5.0.5 release PR on
64
+ # main and the v4.4.1 release PR here.
65
+ #
66
+ # So the ruleset requires this job instead: one stable name, no matrix, no template to
67
+ # expand. It also decouples the required-check list from the matrix, so adding or
68
+ # dropping a Ruby version no longer needs a matching ruleset edit.
69
+ #
70
+ # Unlike main, this branch has no separate lint job: `rake default` runs RuboCop and
71
+ # YARD inside every matrix leg, so this one check covers everything CI does here.
72
+ #
73
+ # This job carries the same release-PR guard as the matrix above, so that a release PR
74
+ # reports it as skipped rather than as a success for specs that never ran. Skipping is
75
+ # safe here and was not safe for `build`: this job's name is a literal string, so the
76
+ # check run exists and reports a skipped conclusion whether or not the job runs, and
77
+ # rulesets treat skipped as passing. A matrix job's name does not exist at all until the
78
+ # matrix is evaluated, which is the whole reason this job exists.
79
+ #
80
+ # `always()` is load-bearing and must stay. Without a status check function a job-level
81
+ # `if:` implies `success()`, so a failing matrix would skip this job -- and a skipped
82
+ # required check counts as passing, meaning a red build would merge. `always()` forces a
83
+ # real verdict on every run that is not a release PR. `!cancelled()` is not a substitute:
84
+ # a cancelled run would then skip the gate, and a skipped conclusion is one rulesets treat
85
+ # as passing -- so a build that never finished would stop blocking the merge.
86
+ #
87
+ # The case below accepts `success` only. `build` and this job now carry the same release-PR
88
+ # guard, so this job runs exactly when `build` ran; a `skipped` result would mean the two
89
+ # guards have drifted apart, and failing closed on it turns that into a loud failure rather
90
+ # than a silent pass.
91
+ #
92
+ # Note that a matrix leg marked `experimental: Yes` carries continue-on-error, and a
93
+ # continue-on-error failure does not fail the job as a whole -- so it does not fail this
94
+ # gate either. That is what `experimental` is for; no leg currently sets it.
95
+ build-complete:
96
+ name: All Specs Passed
97
+
98
+ needs: [build]
99
+
100
+ # Report a verdict on every non-release PR regardless of the matrix result, and skip
101
+ # on release PRs, where the matrix was never meant to run.
102
+ if: >-
103
+ always() &&
104
+ (github.event_name == 'workflow_dispatch' ||
105
+ (github.event_name == 'pull_request' && !startsWith(github.event.pull_request.head.ref, 'release-please--')))
106
+
107
+ runs-on: ubuntu-latest
108
+
109
+ steps:
110
+ - name: Check the result of the build job
111
+ run: |
112
+ case "${{ needs.build.result }}" in
113
+ success)
114
+ echo "build: ${{ needs.build.result }}"
115
+ ;;
116
+ *)
117
+ echo "::error::build: ${{ needs.build.result }}"
118
+ exit 1
119
+ ;;
120
+ esac
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "4.3.2"
2
+ ".": "4.4.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@
5
5
 
6
6
  # Change Log
7
7
 
8
+ ## [4.4.1](https://github.com/ruby-git/ruby-git/compare/v4.4.0...v4.4.1) (2026-08-09)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Pin LC_ALL to a locale that exists on the host platform ([690360e](https://github.com/ruby-git/ruby-git/commit/690360e386bac1dafb7b5bd0adbf10072e834f69)), closes [#1669](https://github.com/ruby-git/ruby-git/issues/1669)
14
+
15
+
16
+ ### Other Changes
17
+
18
+ * Add a matrix-free All Specs Passed gate to 4.x ([5352d6d](https://github.com/ruby-git/ruby-git/commit/5352d6d4edfcb52c544139e265fb3636bc1937b7)), closes [#1675](https://github.com/ruby-git/ruby-git/issues/1675)
19
+ * Skip the All Specs Passed gate on release PRs ([52473b2](https://github.com/ruby-git/ruby-git/commit/52473b21a733499eb274a7b03d53f1a0efbf5c86)), closes [#1675](https://github.com/ruby-git/ruby-git/issues/1675)
20
+
21
+ ## [4.4.0](https://github.com/ruby-git/ruby-git/compare/v4.3.2...v4.4.0) (2026-07-11)
22
+
23
+
24
+ ### Features
25
+
26
+ * Allow deprecation behavior env var ([fd622be](https://github.com/ruby-git/ruby-git/commit/fd622bea6513d78f2dbed8dc19c110be80d07ec8))
27
+ * Warn on first direct access to Git::Lib constant ([52059c6](https://github.com/ruby-git/ruby-git/commit/52059c63d5c23b4d80025b95332e946aba942f0c))
28
+
29
+
30
+ ### Other Changes
31
+
32
+ * Improve YARD documentation for Git::Branch ([#1277](https://github.com/ruby-git/ruby-git/issues/1277)) ([0769bf9](https://github.com/ruby-git/ruby-git/commit/0769bf91a21ecb24d48a31439de073b689bc62b6))
33
+
8
34
  ## [4.3.2](https://github.com/ruby-git/ruby-git/compare/v4.3.1...v4.3.2) (2026-03-31)
9
35
 
10
36
 
data/README.md CHANGED
@@ -589,6 +589,18 @@ You can silence deprecation warnings by adding this line to your source code:
589
589
  Git::Deprecation.behavior = :silence
590
590
  ```
591
591
 
592
+ Or by setting this environment variable before loading the gem:
593
+
594
+ ```sh
595
+ GIT_DEPRECATION_BEHAVIOR=silence
596
+ ```
597
+
598
+ Accepted environment variable values are the behavior names supported by your
599
+ installed ActiveSupport version.
600
+
601
+ If `GIT_DEPRECATION_BEHAVIOR` is set to an unsupported value, loading the gem
602
+ raises `ArgumentError` with the accepted behavior names.
603
+
592
604
  See [the Active Support Deprecation
593
605
  documentation](https://api.rubyonrails.org/classes/ActiveSupport/Deprecation.html)
594
606
  for more details.
data/lib/git/base.rb CHANGED
@@ -23,14 +23,14 @@ module Git
23
23
  def self.clone(repository_url, directory, options = {})
24
24
  lib_options = {}
25
25
  lib_options[:git_ssh] = options[:git_ssh] if options.key?(:git_ssh)
26
- new_options = Git::Lib.new(lib_options, options[:log]).clone(repository_url, directory, options)
26
+ new_options = LibImpl.new(lib_options, options[:log]).clone(repository_url, directory, options)
27
27
  normalize_paths(new_options, bare: options[:bare] || options[:mirror])
28
28
  new(new_options)
29
29
  end
30
30
 
31
31
  # (see Git.default_branch)
32
32
  def self.repository_default_branch(repository, options = {})
33
- Git::Lib.new(nil, options[:log]).repository_default_branch(repository)
33
+ LibImpl.new(nil, options[:log]).repository_default_branch(repository)
34
34
  end
35
35
 
36
36
  # Returns (and initialize if needed) a Git::Config instance
@@ -90,7 +90,7 @@ module Git
90
90
  # repository you have a Git::Base instance for. This would not
91
91
  # change the existing interface (other than adding to it).
92
92
  #
93
- Git::Lib.new(options).init(init_options)
93
+ LibImpl.new(options).init(init_options)
94
94
 
95
95
  new(options)
96
96
  end
@@ -335,7 +335,7 @@ module Git
335
335
  # actual 'git' forked system calls. At some point I hope to replace the Git::Lib
336
336
  # class with one that uses native methods or libgit C bindings
337
337
  def lib
338
- @lib ||= Git::Lib.new(self, @logger)
338
+ @lib ||= LibImpl.new(self, @logger)
339
339
  end
340
340
 
341
341
  # Returns the per-instance git_ssh configuration value.
data/lib/git/branch.rb CHANGED
@@ -4,9 +4,81 @@ require 'git/path'
4
4
 
5
5
  module Git
6
6
  # Represents a Git branch
7
+ #
8
+ # Branch objects provide access to branch metadata and operations like checkout,
9
+ # delete, and merge. They should be obtained via {Git::Base#branch} or
10
+ # {Git::Base#branches}, not constructed directly.
11
+ #
12
+ # @example Getting a branch
13
+ # git = Git.open('.')
14
+ # branch = git.branch('main')
15
+ # branch.checkout
16
+ #
17
+ # @example Listing branches
18
+ # git.branches.each { |b| puts b.name }
19
+ #
20
+ # @api public
21
+ #
7
22
  class Branch
8
- attr_accessor :full, :remote, :name
23
+ # The full refname of this branch
24
+ #
25
+ # For local branches this is the short name (e.g. `'main'`). For
26
+ # remote-tracking branches obtained via {Git::Base#branches} this includes
27
+ # the `remotes/` prefix (e.g. `'remotes/origin/main'`). Branches constructed
28
+ # by {Git::Remote#branch} use the `<remote>/<branch>` form (e.g.
29
+ # `'origin/main'`) which does **not** populate {#remote}.
30
+ #
31
+ # @example
32
+ # git.branch('main').full #=> 'main'
33
+ # git.branch('remotes/origin/main').full #=> 'remotes/origin/main'
34
+ #
35
+ # @return [String] the full refname
36
+ #
37
+ attr_accessor :full
38
+
39
+ # The remote for this branch, or `nil` for local or bare-name remote-tracking branches
40
+ #
41
+ # Set to a {Git::Remote} object only when this branch was initialized with a
42
+ # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix. `nil` for local
43
+ # branches and for remote-tracking branches in `<remote>/<branch>` form
44
+ # (such as those returned by {Git::Remote#branch}).
45
+ #
46
+ # @example
47
+ # git.branch('main').remote #=> nil
48
+ # git.branch('remotes/origin/main').remote #=> #<Git::Remote 'origin'>
49
+ # git.remote('origin').branch('main').remote #=> nil # uses 'origin/main' form
50
+ #
51
+ # @return [Git::Remote, nil] the remote object, or `nil`
52
+ #
53
+ attr_accessor :remote
54
+
55
+ # The short branch name without the remote prefix
56
+ #
57
+ # For branches initialized with a `remotes/` or `refs/remotes/` prefix, the
58
+ # prefix is stripped and this returns the bare branch name (e.g. `'main'`
59
+ # rather than `'remotes/origin/main'`). For branches in the
60
+ # `<remote>/<branch>` form (such as those created by {Git::Remote#branch}),
61
+ # no stripping occurs and `name` returns the full form (e.g. `'origin/main'`).
62
+ #
63
+ # @example
64
+ # git.branch('main').name #=> 'main'
65
+ # git.branch('remotes/origin/main').name #=> 'main'
66
+ # git.remote('origin').branch('main').name #=> 'origin/main'
67
+ #
68
+ # @return [String] the branch name
69
+ #
70
+ attr_accessor :name
9
71
 
72
+ # Initialize a new Branch object
73
+ #
74
+ # @api private
75
+ #
76
+ # @note Use {Git::Base#branch} or {Git::Base#branches} instead of constructing directly
77
+ #
78
+ # @param base [Git::Base] the git repository
79
+ #
80
+ # @param name [String] the full or short branch name
81
+ #
10
82
  def initialize(base, name)
11
83
  @full = name
12
84
  @base = base
@@ -15,29 +87,102 @@ module Git
15
87
  @remote, @name = parse_name(name)
16
88
  end
17
89
 
90
+ # Returns the commit at the tip of this branch
91
+ #
92
+ # The result is memoized after the first call.
93
+ #
94
+ # @example Get the tip commit
95
+ # git.branch('main').gcommit #=> #<Git::Object ...>
96
+ #
97
+ # @return [Git::Object] the commit at the tip of this branch
98
+ #
18
99
  def gcommit
19
100
  @gcommit ||= @base.gcommit(@full)
20
101
  @gcommit
21
102
  end
22
103
 
104
+ # Returns the stash list for this repository
105
+ #
106
+ # The result is memoized after the first call.
107
+ #
108
+ # @example Iterate over stash entries
109
+ # git.branch('main').stashes.each { |s| puts s }
110
+ #
111
+ # @return [Git::Stashes] the stash list
112
+ #
23
113
  def stashes
24
114
  @stashes ||= Git::Stashes.new(@base)
25
115
  end
26
116
 
117
+ # Checks out this branch, attempting to create it first if it does not already exist
118
+ #
119
+ # Branch creation is attempted via {#check_if_create}; any error from that
120
+ # step is silently ignored and the checkout proceeds regardless.
121
+ #
122
+ # **Note:** for remote-tracking branches (where {#remote} is not `nil`),
123
+ # {#full} is a ref such as `'remotes/origin/main'`. Checking out a
124
+ # remote-tracking ref places the repository in a **detached HEAD** state.
125
+ #
126
+ # @example Check out a branch
127
+ # git = Git.open('.')
128
+ # git.branch('main').checkout
129
+ #
130
+ # @return [String] git's stdout from the checkout
131
+ #
132
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
133
+ #
27
134
  def checkout
28
135
  check_if_create
29
136
  @base.checkout(@full)
30
137
  end
31
138
 
139
+ # Archives this branch and writes the result to a file
140
+ #
141
+ # @example Archive to a tar file
142
+ # git.branch('main').archive('/tmp/main.tar')
143
+ #
144
+ # @example Archive to a zip file
145
+ # git.branch('main').archive('/tmp/main.zip', format: 'zip')
146
+ #
147
+ # @param file [String] path to the destination archive file
148
+ #
149
+ # @param opts [Hash] archive options (see {Git::Base#archive})
150
+ #
151
+ # @return [String] the path to the written archive file
152
+ #
153
+ # @raise [Git::FailedError] if `git archive` fails
154
+ #
32
155
  def archive(file, opts = {})
33
156
  @base.lib.archive(@full, file, opts)
34
157
  end
35
158
 
36
- # g.branch('new_branch').in_branch do
37
- # # create new file
38
- # # do other stuff
39
- # return true # auto commits and switches back
40
- # end
159
+ # Checks out this branch for the duration of a block, then restores the original branch
160
+ #
161
+ # If the block returns a truthy value, all pending changes are committed with the
162
+ # given message before switching back to the original branch. If the block returns
163
+ # a falsy value, a hard reset is performed before switching back.
164
+ #
165
+ # **Note:** the restore checkout is not wrapped in `ensure`. If the block,
166
+ # the commit, or the reset raises an exception, the repository will be left
167
+ # checked out on this branch rather than restored to the original.
168
+ #
169
+ # @example Commit a new file on a feature branch
170
+ # git.branch('feature').in_branch('Add README') do
171
+ # File.write('README.md', '# Hello')
172
+ # git.add('README.md')
173
+ # true # commit and return to original branch
174
+ # end
175
+ #
176
+ # @param message [String] commit message used when the block returns truthy
177
+ #
178
+ # @yield Executes the block with this branch checked out
179
+ #
180
+ # @yieldreturn [Object] return a truthy value to commit all changes, a falsy value to hard-reset
181
+ #
182
+ # @return [String] git's stdout from the final checkout back to the original branch
183
+ #
184
+ # @raise [Git::FailedError] if any of the underlying git operations (checkout, commit, reset) fail
185
+ #
41
186
  def in_branch(message = 'in branch work')
42
187
  old_current = @base.lib.branch_current
43
188
  checkout
@@ -49,22 +194,112 @@ module Git
49
194
  @base.checkout(old_current)
50
195
  end
51
196
 
197
+ # Creates this branch if it does not already exist
198
+ #
199
+ # Silently ignores any error raised during branch creation (including the case
200
+ # where the branch already exists).
201
+ #
202
+ # @example Create a new branch
203
+ # git.branch('feature').create
204
+ #
205
+ # @return [String, nil] git's stdout from branch creation (typically empty),
206
+ # or `nil` if an error was rescued
207
+ #
52
208
  def create
53
209
  check_if_create
54
210
  end
55
211
 
212
+ # Deletes this branch
213
+ #
214
+ # **Note:** this method only works correctly for local branches. Calling it on
215
+ # a remote-tracking branch (one where {#remote} is not `nil`) will attempt to
216
+ # delete a *local* branch with the same short name rather than the
217
+ # remote-tracking ref, which is almost certainly not what you want.
218
+ # See [ruby-git#1280](https://github.com/ruby-git/ruby-git/issues/1280) for
219
+ # the planned fix.
220
+ #
221
+ # @example Delete a local branch
222
+ # git.branch('old-feature').delete
223
+ #
224
+ # @return [String] git's deletion output
225
+ #
226
+ # @raise [Git::FailedError] if the branch cannot be deleted
227
+ #
56
228
  def delete
57
229
  @base.lib.branch_delete(@name)
58
230
  end
59
231
 
232
+ # Returns true if this is the currently checked-out branch
233
+ #
234
+ # **Note:** this compares the current branch's short name against {#name}.
235
+ # For a remote-tracking branch (where {#remote} is not `nil`), {#name} is
236
+ # still the bare short name (e.g. `'main'`), so this will return `true`
237
+ # whenever the *local* branch with that name is checked out — not the
238
+ # remote-tracking ref itself.
239
+ #
240
+ # @example Check whether currently on main
241
+ # git.branch('main').current #=> true
242
+ #
243
+ # @return [Boolean] whether this branch is currently checked out
244
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
245
+ #
246
+ #
60
247
  def current # rubocop:disable Naming/PredicateMethod
61
248
  @base.lib.branch_current == @name
62
249
  end
63
250
 
251
+ # Returns true if this branch contains the given commit
252
+ #
253
+ # **Note:** this queries local branches by short name. For a remote-tracking
254
+ # branch (where {#remote} is not `nil`), it checks the *local* branch with
255
+ # the same {#name} rather than the remote-tracking ref, which may give an
256
+ # inaccurate result.
257
+ #
258
+ # @example Check if a commit is reachable from this branch
259
+ # git.branch('main').contains?('abc1234') #=> true
260
+ #
261
+ # @param commit [String] the commit SHA or ref to check
262
+ #
263
+ # @return [Boolean] whether this branch contains the given commit
264
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
265
+ #
266
+ #
64
267
  def contains?(commit)
65
268
  !@base.lib.branch_contains(commit, name).empty?
66
269
  end
67
270
 
271
+ # Merges a branch into this branch, or merges this branch into the current branch
272
+ #
273
+ # @overload merge(branch, message = nil)
274
+ #
275
+ # Temporarily checks out this branch, merges the given branch into it,
276
+ # then restores the original branch.
277
+ #
278
+ # **Note:** if `self` is a remote-tracking branch (where {#remote} is not
279
+ # `nil`), this delegates to {#checkout} which has the detached-HEAD
280
+ # side-effect described there. The remote-tracking ref will not be updated.
281
+ #
282
+ # @example Merge a feature branch into main
283
+ # git.branch('main').merge('feature')
284
+ #
285
+ # @param branch [String] the name of the branch to merge into this one
286
+ #
287
+ # @param message [String, nil] commit message for the merge commit
288
+ #
289
+ # @return [String] git's stdout from the final checkout back to the original branch
290
+ #
291
+ # @overload merge()
292
+ #
293
+ # Merges this branch into the currently checked-out branch.
294
+ #
295
+ # @example Merge main into the current branch
296
+ # git.branch('main').merge
297
+ #
298
+ # @return [String] git's stdout from the merge command
299
+ #
300
+ # @raise [Git::FailedError] if git exits with a non-zero exit status during
301
+ # the merge, checkout, commit, or reset operations
302
+ #
68
303
  def merge(branch = nil, message = nil)
69
304
  if branch
70
305
  in_branch do
@@ -78,6 +313,26 @@ module Git
78
313
  end
79
314
  end
80
315
 
316
+ # Updates the git ref for this branch to point to the given commit
317
+ #
318
+ # The target ref depends on whether {#remote} is set:
319
+ # - When {#remote} is not `nil` (i.e. the branch was initialised with a
320
+ # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix), updates
321
+ # `refs/remotes/<remote>/<name>`.
322
+ # - Otherwise updates `refs/heads/<name>`. Note that branches in the
323
+ # `<remote>/<branch>` form (e.g. those returned by {Git::Remote#branch})
324
+ # have `remote == nil` and therefore update `refs/heads/<remote>/<name>`,
325
+ # **not** `refs/remotes/...`.
326
+ #
327
+ # @example Advance a local branch to a new commit
328
+ # git.branch('feature').update_ref('abc1234def5678')
329
+ #
330
+ # @param commit [String] the commit SHA to point this branch at
331
+ #
332
+ # @return [String] the stdout output from `git update-ref`
333
+ #
334
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
335
+ #
81
336
  def update_ref(commit)
82
337
  if @remote
83
338
  @base.lib.update_ref("refs/remotes/#{@remote.name}/#{@name}", commit)
@@ -86,17 +341,38 @@ module Git
86
341
  end
87
342
  end
88
343
 
344
+ # Returns this branch as a single-element array containing its full refname
345
+ #
346
+ # @example Get branch as array
347
+ # git.branch('main').to_a #=> ['main']
348
+ #
349
+ # @return [Array<String>] a single-element array containing the full refname
350
+ #
89
351
  def to_a
90
352
  [@full]
91
353
  end
92
354
 
355
+ # Returns the full refname of this branch as a string
356
+ #
357
+ # @example Get branch as string
358
+ # git.branch('main').to_s #=> 'main'
359
+ #
360
+ # @return [String] the full refname
361
+ #
93
362
  def to_s
94
363
  @full
95
364
  end
96
365
 
366
+ # Regular expression for parsing branch refnames
367
+ #
368
+ # Matches full and short refnames, capturing an optional remote name and the
369
+ # branch name. Used internally to identify remote-tracking branches.
370
+ #
371
+ # @api private
372
+ #
97
373
  BRANCH_NAME_REGEXP = %r{
98
374
  ^
99
- # Optional 'refs/remotes/' at the beggining to specify a remote tracking branch
375
+ # Optional 'remotes/' or 'refs/remotes/' at the beginning to specify a remote tracking branch
100
376
  # with a <remote_name>. <remote_name> is nil if not present.
101
377
  (?:
102
378
  (?:(?:refs/)?remotes/)(?<remote_name>[^/]+)/
@@ -107,26 +383,25 @@ module Git
107
383
 
108
384
  private
109
385
 
110
- # Given a full branch name return an Array containing the remote and branch names.
386
+ # Parses a full branch name into remote and short branch name components
387
+ #
388
+ # Strips an optional `remotes/` or `refs/remotes/` prefix. Only inputs that begin
389
+ # with one of those prefixes yield a remote object; all other inputs (including
390
+ # `'origin/master'`) are treated as local branch names with a `nil` remote.
111
391
  #
112
- # Removes 'remotes' from the beggining of the name (if present).
113
- # Takes the second part (splittign by '/') as the remote name.
114
- # Takes the rest as the repo name (can also hold one or more '/').
392
+ # @example Local branches
393
+ # parse_name('master') #=> [nil, 'master']
394
+ # parse_name('origin/master') #=> [nil, 'origin/master']
115
395
  #
116
- # Example:
117
- # # local branches
118
- # parse_name('master') #=> [nil, 'master']
119
- # parse_name('origin/master') #=> [nil, 'origin/master']
120
- # parse_name('origin/master/v2') #=> [nil, 'origin/master']
396
+ # @example Remote-tracking branches
397
+ # parse_name('remotes/origin/master') #=> [#<Git::Remote 'origin'>, 'master']
398
+ # parse_name('refs/remotes/origin/master') #=> [#<Git::Remote 'origin'>, 'master']
121
399
  #
122
- # # remote branches
123
- # parse_name('remotes/origin/master') #=> ['origin', 'master']
124
- # parse_name('remotes/origin/master/v2') #=> ['origin', 'master/v2']
125
- # parse_name('refs/remotes/origin/master') #=> ['origin', 'master']
126
- # parse_name('refs/remotes/origin/master/v2') #=> ['origin', 'master/v2']
400
+ # @param name [String] the full branch name to parse
401
+ #
402
+ # @return [Array(Git::Remote, String)] a two-element array with the remote object
403
+ # (or `nil`) and the short branch name
127
404
  #
128
- # param [String] name branch full name.
129
- # return [<Git::Remote,NilClass,String>] an Array containing the remote and branch names.
130
405
  def parse_name(name)
131
406
  # Expect this will always match
132
407
  match = name.match(BRANCH_NAME_REGEXP)
@@ -135,6 +410,10 @@ module Git
135
410
  [remote, branch_name]
136
411
  end
137
412
 
413
+ # Creates the branch if it does not already exist, ignoring errors
414
+ #
415
+ # @return [String, nil] stdout from branch creation, or `nil` if an error was rescued
416
+ #
138
417
  def check_if_create
139
418
  @base.lib.branch_new(@name)
140
419
  rescue StandardError
data/lib/git/lib.rb CHANGED
@@ -2075,7 +2075,7 @@ module Git
2075
2075
  # @example Unsetting an environment variable (used by worktree_command_line)
2076
2076
  # env_overrides('GIT_INDEX_FILE' => nil)
2077
2077
  # # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree',
2078
- # # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' => 'en_US.UTF-8' }
2078
+ # # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' => <pinned_locale> }
2079
2079
  # # When passed to Process.spawn, GIT_INDEX_FILE will be unset in the environment
2080
2080
  #
2081
2081
  # @see https://ruby-doc.org/core/Process.html#method-c-spawn Process.spawn
@@ -2088,10 +2088,67 @@ module Git
2088
2088
  'GIT_WORK_TREE' => @git_work_dir,
2089
2089
  'GIT_INDEX_FILE' => @git_index_file,
2090
2090
  'GIT_SSH' => resolved_git_ssh,
2091
- 'LC_ALL' => 'en_US.UTF-8'
2091
+ # Pin the locale so git's behavior does not depend on the user's environment.
2092
+ # Added for issue #753, where a German user's `git branch` output
2093
+ # ("* (HEAD losgelöst bei origin/25.1)") broke branch parsing.
2094
+ #
2095
+ # The pin has two halves, and both are load-bearing here:
2096
+ #
2097
+ # - Messages: keeps git's human-readable text in English. `BRANCH_LINE_REGEXP`
2098
+ # matches the literal English strings "(HEAD detached at ...)" and
2099
+ # "(not a branch)" in `git branch -a` output, which is the exact issue #753
2100
+ # failure mode.
2101
+ # - Ctype: makes git's regex engine match *characters* rather than *bytes*.
2102
+ # Under a C ctype, `grep` and `log --grep` silently return wrong answers —
2103
+ # with exit status zero — for any pattern whose metacharacters span
2104
+ # non-ASCII text.
2105
+ #
2106
+ # So do not "simplify" this to `C`: that yields English messages and a broken
2107
+ # ctype, which is the worst of both. A pinned locale the host does not have
2108
+ # degrades to that same C ctype, which is why each branch below has to name a
2109
+ # locale that actually exists on the platform it applies to:
2110
+ #
2111
+ # - Non-Darwin: `C.UTF-8`. Stock Debian, Ubuntu, and RHEL images generate no
2112
+ # `en_US.UTF-8`, so pinning it there produced exactly the silent breakage
2113
+ # above. (musl ignores the locale name for ctype, so Alpine is UTF-8 either
2114
+ # way.)
2115
+ # - Darwin: `en_US.UTF-8`, which every macOS release ships. `C.UTF-8` did not
2116
+ # arrive until macOS 15, so macOS 11–14 — including Intel Macs that are
2117
+ # hardware-capped below 15 — would break under it. Delete this branch once
2118
+ # macOS 14 and earlier are out of support.
2119
+ #
2120
+ # Windows takes the non-Darwin branch, where the value makes no difference:
2121
+ # Git for Windows folds case and runs PCRE in UTF mode under every value, and
2122
+ # matches bytes for `.` and POSIX classes under every value — measured on git
2123
+ # 2.55.0 against `en_US.UTF-8`, `C.UTF-8`, `C`, and no pin at all.
2124
+ #
2125
+ # RHEL 7 (glibc 2.17) has neither locale and gets a C ctype whatever is pinned.
2126
+ # It is EOL, and there is deliberately no public override for this value.
2127
+ 'LC_ALL' => darwin_platform? ? 'en_US.UTF-8' : 'C.UTF-8'
2092
2128
  }.merge(additional_overrides)
2093
2129
  end
2094
2130
 
2131
+ # Whether this process is running on macOS
2132
+ #
2133
+ # Checks `RUBY_DESCRIPTION` as well as `RUBY_PLATFORM` because JRuby reports
2134
+ # `RUBY_PLATFORM` as `"java"` on every host and records the real platform only in
2135
+ # `RUBY_DESCRIPTION` (as, for example, `"... [arm64-darwin]"`). Testing
2136
+ # `RUBY_PLATFORM` alone would put JRuby on macOS onto the non-Darwin branch, which is
2137
+ # the one platform pairing that branch must not be given. This is the same detection
2138
+ # the test suite uses for Windows, and for the same reason.
2139
+ #
2140
+ # Test the platform plainly rather than sniffing the Darwin version: `RUBY_PLATFORM`
2141
+ # records the version Ruby was *built* against, so a Ruby built on macOS 14 still
2142
+ # reports `darwin23` when run on macOS 15.
2143
+ #
2144
+ # @return [Boolean] true if this process is running on macOS
2145
+ #
2146
+ # @api private
2147
+ #
2148
+ def darwin_platform?
2149
+ RUBY_PLATFORM.include?('darwin') || RUBY_DESCRIPTION.include?('darwin')
2150
+ end
2151
+
2095
2152
  # Resolve the git_ssh value to use for this instance
2096
2153
  #
2097
2154
  # @return [String, nil] the resolved git_ssh value
data/lib/git/status.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # These would be required by the main `git.rb` file
4
-
5
3
  module Git
6
4
  # The Status class gets the status of a git repository. It identifies which
7
5
  # files have been modified, added, or deleted, including untracked files.
data/lib/git/version.rb CHANGED
@@ -3,5 +3,5 @@
3
3
  module Git
4
4
  # The current gem version
5
5
  # @return [String] the current gem version.
6
- VERSION = '4.3.2'
6
+ VERSION = '4.4.1'
7
7
  end
data/lib/git.rb CHANGED
@@ -1,10 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support'
4
3
  require 'active_support/deprecation'
5
4
 
5
+ # Define Git::Deprecation before requiring the rest of the library to ensure that
6
+ # any deprecation warnings emitted during the loading of the library are properly
7
+ # configured according to the GIT_DEPRECATION_BEHAVIOR environment variable.
8
+ #
6
9
  module Git
10
+ # The deprecation instance used to emit deprecation warnings for the Git gem
11
+ #
12
+ # @api public
7
13
  Deprecation = ActiveSupport::Deprecation.new('5.0.0', 'Git')
14
+
15
+ if (behavior = ENV.fetch('GIT_DEPRECATION_BEHAVIOR', nil))
16
+ behavior = behavior.strip
17
+ allowed_behaviors = ActiveSupport::Deprecation::DEFAULT_BEHAVIORS.keys.map(&:to_s)
18
+
19
+ unless allowed_behaviors.include?(behavior)
20
+ raise ArgumentError,
21
+ "Invalid GIT_DEPRECATION_BEHAVIOR=#{behavior.inspect}; " \
22
+ "expected one of: #{allowed_behaviors.join(', ')}"
23
+ end
24
+
25
+ Deprecation.behavior = behavior.to_sym
26
+ end
8
27
  end
9
28
 
10
29
  require 'git/author'
@@ -44,12 +63,28 @@ require 'git/worktrees'
44
63
  # @author Scott Chacon (mailto:schacon@gmail.com)
45
64
  #
46
65
  module Git # rubocop:disable Style/OneClassPerFile
66
+ # Internal alias for Git::Lib, used by the gem itself after the public constant
67
+ # is deprecated. Code outside the gem should not reference this constant.
68
+ # @api private
69
+ LibImpl = remove_const(:Lib)
70
+
71
+ # @api private
72
+ def self.const_missing(name)
73
+ return super unless name == :Lib
74
+
75
+ Git::Deprecation.warn(
76
+ 'Git::Lib is deprecated and will be removed in version 5.x. ' \
77
+ 'Use the #lib accessor on the object returned by Git.init, Git.open, or Git.clone instead.'
78
+ )
79
+ const_set(:Lib, LibImpl)
80
+ end
81
+
47
82
  # g.config('user.name', 'Scott Chacon') # sets value
48
83
  # g.config('user.email', 'email@email.com') # sets value
49
84
  # g.config('user.name') # returns 'Scott Chacon'
50
85
  # g.config # returns whole config hash
51
86
  def config(name = nil, value = nil)
52
- lib = Git::Lib.new
87
+ lib = LibImpl.new
53
88
  if name && value
54
89
  # set value
55
90
  lib.config_set(name, value)
@@ -287,7 +322,7 @@ module Git # rubocop:disable Style/OneClassPerFile
287
322
  # g.config('user.name') # returns 'Scott Chacon'
288
323
  # g.config # returns whole config hash
289
324
  def self.global_config(name = nil, value = nil)
290
- lib = Git::Lib.new(nil, nil)
325
+ lib = LibImpl.new(nil, nil)
291
326
  if name && value
292
327
  # set value
293
328
  lib.global_config_set(name, value)
@@ -369,7 +404,7 @@ module Git # rubocop:disable Style/OneClassPerFile
369
404
  # @param [String|NilClass] location the target repository location or nil for '.'
370
405
  # @return [{String=>Hash}] the available references of the target repo.
371
406
  def self.ls_remote(location = nil, options = {})
372
- Git::Lib.new.ls_remote(location, options)
407
+ LibImpl.new.ls_remote(location, options)
373
408
  end
374
409
 
375
410
  # Open a an existing Git working directory
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.2
4
+ version: 4.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Chacon and others
@@ -310,8 +310,8 @@ licenses:
310
310
  metadata:
311
311
  homepage_uri: http://github.com/ruby-git/ruby-git
312
312
  source_code_uri: http://github.com/ruby-git/ruby-git
313
- changelog_uri: https://rubydoc.info/gems/git/4.3.2/file/CHANGELOG.md
314
- documentation_uri: https://rubydoc.info/gems/git/4.3.2
313
+ changelog_uri: https://rubydoc.info/gems/git/4.4.1/file/CHANGELOG.md
314
+ documentation_uri: https://rubydoc.info/gems/git/4.4.1
315
315
  rubygems_mfa_required: 'true'
316
316
  rdoc_options: []
317
317
  require_paths:
@@ -328,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
328
328
  version: '0'
329
329
  requirements:
330
330
  - git 2.28.0 or greater
331
- rubygems_version: 4.0.6
331
+ rubygems_version: 4.0.16
332
332
  specification_version: 4
333
333
  summary: An API to create, read, and manipulate Git repositories
334
334
  test_files: []