git 1.19.1 → 4.4.5

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.commitlintrc.yml +38 -0
  3. data/.github/copilot-instructions.md +2733 -0
  4. data/.github/pull_request_template.md +17 -0
  5. data/.github/workflows/continuous_integration.yml +92 -21
  6. data/.github/workflows/enforce_conventional_commits.yml +29 -0
  7. data/.github/workflows/experimental_continuous_integration.yml +59 -0
  8. data/.github/workflows/release.yml +53 -0
  9. data/.gitignore +5 -0
  10. data/.husky/commit-msg +1 -0
  11. data/.release-please-manifest.json +3 -0
  12. data/.rubocop.yml +55 -0
  13. data/.rubocop_todo.yml +12 -0
  14. data/.yardopts +4 -1
  15. data/AI_POLICY.md +24 -0
  16. data/CHANGELOG.md +501 -0
  17. data/CODE_OF_CONDUCT.md +25 -0
  18. data/CONTRIBUTING.md +323 -102
  19. data/GOVERNANCE.md +106 -0
  20. data/LICENSE +1 -1
  21. data/MAINTAINERS.md +17 -4
  22. data/README.md +575 -246
  23. data/Rakefile +13 -55
  24. data/git.gemspec +36 -30
  25. data/lib/git/args_builder.rb +111 -0
  26. data/lib/git/author.rb +9 -7
  27. data/lib/git/base.rb +602 -173
  28. data/lib/git/branch.rb +318 -38
  29. data/lib/git/branches.rb +21 -24
  30. data/lib/git/command_line.rb +330 -0
  31. data/lib/git/command_line_result.rb +9 -3
  32. data/lib/git/config.rb +10 -6
  33. data/lib/git/diff.rb +149 -81
  34. data/lib/git/diff_path_status.rb +46 -0
  35. data/lib/git/diff_stats.rb +59 -0
  36. data/lib/git/errors.rb +212 -0
  37. data/lib/git/escaped_path.rb +2 -2
  38. data/lib/git/fsck_object.rb +48 -0
  39. data/lib/git/fsck_result.rb +121 -0
  40. data/lib/git/index.rb +2 -1
  41. data/lib/git/lib.rb +1648 -643
  42. data/lib/git/log.rb +143 -106
  43. data/lib/git/object.rb +151 -125
  44. data/lib/git/path.rb +23 -16
  45. data/lib/git/remote.rb +5 -4
  46. data/lib/git/repository.rb +2 -2
  47. data/lib/git/stash.rb +11 -12
  48. data/lib/git/stashes.rb +16 -15
  49. data/lib/git/status.rb +104 -143
  50. data/lib/git/url.rb +3 -3
  51. data/lib/git/version.rb +3 -1
  52. data/lib/git/working_directory.rb +2 -0
  53. data/lib/git/worktree.rb +6 -5
  54. data/lib/git/worktrees.rb +6 -6
  55. data/lib/git.rb +131 -28
  56. data/package.json +10 -0
  57. data/redesign/1_architecture_existing.md +66 -0
  58. data/redesign/2_architecture_redesign.md +130 -0
  59. data/redesign/3_architecture_implementation.md +138 -0
  60. data/redesign/index.md +34 -0
  61. data/release-please-config.json +36 -0
  62. data/tasks/gem_tasks.rake +10 -0
  63. data/tasks/rubocop.rake +12 -0
  64. data/tasks/test.rake +13 -0
  65. data/tasks/test_gem.rake +12 -0
  66. data/tasks/yard.rake +23 -0
  67. metadata +114 -37
  68. data/.github/stale.yml +0 -25
  69. data/Dockerfile.changelog-rs +0 -12
  70. data/PULL_REQUEST_TEMPLATE.md +0 -9
  71. data/RELEASING.md +0 -70
  72. data/lib/git/base/factory.rb +0 -99
  73. data/lib/git/failed_error.rb +0 -53
  74. data/lib/git/git_execute_error.rb +0 -7
  75. data/lib/git/signaled_error.rb +0 -50
  76. /data/{ISSUE_TEMPLATE.md → .github/issue_template.md} +0 -0
data/lib/git/branch.rb CHANGED
@@ -1,9 +1,84 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'git/path'
2
4
 
3
5
  module Git
4
- class Branch < Path
5
- attr_accessor :full, :remote, :name
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
+ #
22
+ class Branch
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
6
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
+ #
7
82
  def initialize(base, name)
8
83
  @full = name
9
84
  @base = base
@@ -12,29 +87,102 @@ module Git
12
87
  @remote, @name = parse_name(name)
13
88
  end
14
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
+ #
15
99
  def gcommit
16
100
  @gcommit ||= @base.gcommit(@full)
17
101
  @gcommit
18
102
  end
19
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
+ #
20
113
  def stashes
21
114
  @stashes ||= Git::Stashes.new(@base)
22
115
  end
23
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
+ #
24
134
  def checkout
25
135
  check_if_create
26
136
  @base.checkout(@full)
27
137
  end
28
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
+ #
29
155
  def archive(file, opts = {})
30
156
  @base.lib.archive(@full, file, opts)
31
157
  end
32
158
 
33
- # g.branch('new_branch').in_branch do
34
- # # create new file
35
- # # do other stuff
36
- # return true # auto commits and switches back
37
- # 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
+ #
38
186
  def in_branch(message = 'in branch work')
39
187
  old_current = @base.lib.branch_current
40
188
  checkout
@@ -46,22 +194,112 @@ module Git
46
194
  @base.checkout(old_current)
47
195
  end
48
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
+ #
49
208
  def create
50
209
  check_if_create
51
210
  end
52
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
+ #
53
228
  def delete
54
229
  @base.lib.branch_delete(@name)
55
230
  end
56
231
 
57
- def current
58
- determine_current
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
+ #
247
+ def current # rubocop:disable Naming/PredicateMethod
248
+ @base.lib.branch_current == @name
59
249
  end
60
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
+ #
61
267
  def contains?(commit)
62
- !@base.lib.branch_contains(commit, self.name).empty?
268
+ !@base.lib.branch_contains(commit, name).empty?
63
269
  end
64
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
+ #
65
303
  def merge(branch = nil, message = nil)
66
304
  if branch
67
305
  in_branch do
@@ -75,6 +313,26 @@ module Git
75
313
  end
76
314
  end
77
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
+ #
78
336
  def update_ref(commit)
79
337
  if @remote
80
338
  @base.lib.update_ref("refs/remotes/#{@remote.name}/#{@name}", commit)
@@ -83,27 +341,38 @@ module Git
83
341
  end
84
342
  end
85
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
+ #
86
351
  def to_a
87
352
  [@full]
88
353
  end
89
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
+ #
90
362
  def to_s
91
363
  @full
92
364
  end
93
365
 
94
- private
95
-
96
- def check_if_create
97
- @base.lib.branch_new(@name) rescue nil
98
- end
99
-
100
- def determine_current
101
- @base.lib.branch_current == @name
102
- end
103
-
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
+ #
104
373
  BRANCH_NAME_REGEXP = %r{
105
374
  ^
106
- # 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
107
376
  # with a <remote_name>. <remote_name> is nil if not present.
108
377
  (?:
109
378
  (?:(?:refs/)?remotes/)(?<remote_name>[^/]+)/
@@ -112,32 +381,43 @@ module Git
112
381
  $
113
382
  }x
114
383
 
115
- # Given a full branch name return an Array containing the remote and branch names.
384
+ private
385
+
386
+ # Parses a full branch name into remote and short branch name components
116
387
  #
117
- # Removes 'remotes' from the beggining of the name (if present).
118
- # Takes the second part (splittign by '/') as the remote name.
119
- # Takes the rest as the repo name (can also hold one or more '/').
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.
120
391
  #
121
- # Example:
122
- # # local branches
123
- # parse_name('master') #=> [nil, 'master']
124
- # parse_name('origin/master') #=> [nil, 'origin/master']
125
- # parse_name('origin/master/v2') #=> [nil, 'origin/master']
392
+ # @example Local branches
393
+ # parse_name('master') #=> [nil, 'master']
394
+ # parse_name('origin/master') #=> [nil, 'origin/master']
126
395
  #
127
- # # remote branches
128
- # parse_name('remotes/origin/master') #=> ['origin', 'master']
129
- # parse_name('remotes/origin/master/v2') #=> ['origin', 'master/v2']
130
- # parse_name('refs/remotes/origin/master') #=> ['origin', 'master']
131
- # parse_name('refs/remotes/origin/master/v2') #=> ['origin', 'master/v2']
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']
399
+ #
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
132
404
  #
133
- # param [String] name branch full name.
134
- # return [<Git::Remote,NilClass,String>] an Array containing the remote and branch names.
135
405
  def parse_name(name)
136
406
  # Expect this will always match
137
407
  match = name.match(BRANCH_NAME_REGEXP)
138
408
  remote = match[:remote_name] ? Git::Remote.new(@base, match[:remote_name]) : nil
139
409
  branch_name = match[:branch_name]
140
- [ remote, branch_name ]
410
+ [remote, branch_name]
411
+ end
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
+ #
417
+ def check_if_create
418
+ @base.lib.branch_new(@name)
419
+ rescue StandardError
420
+ nil
141
421
  end
142
422
  end
143
423
  end
data/lib/git/branches.rb CHANGED
@@ -1,38 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Git
2
-
3
4
  # object that holds all the available branches
4
5
  class Branches
5
-
6
6
  include Enumerable
7
-
7
+
8
8
  def initialize(base)
9
9
  @branches = {}
10
-
10
+
11
11
  @base = base
12
-
12
+
13
13
  @base.lib.branches_all.each do |b|
14
14
  @branches[b[0]] = Git::Branch.new(@base, b[0])
15
15
  end
16
16
  end
17
17
 
18
18
  def local
19
- self.select { |b| !b.remote }
19
+ reject(&:remote)
20
20
  end
21
-
21
+
22
22
  def remote
23
- self.select { |b| b.remote }
23
+ self.select(&:remote)
24
24
  end
25
-
25
+
26
26
  # array like methods
27
27
 
28
28
  def size
29
29
  @branches.size
30
- end
31
-
32
- def each(&block)
33
- @branches.values.each(&block)
34
30
  end
35
-
31
+
32
+ def each(&)
33
+ @branches.values.each(&)
34
+ end
35
+
36
36
  # Returns the target branch
37
37
  #
38
38
  # Example:
@@ -47,25 +47,22 @@ module Git
47
47
  # @param [#to_s] branch_name the target branch name.
48
48
  # @return [Git::Branch] the target branch.
49
49
  def [](branch_name)
50
- @branches.values.inject(@branches) do |branches, branch|
50
+ @branches.values.each_with_object(@branches) do |branch, branches|
51
51
  branches[branch.full] ||= branch
52
52
 
53
- # This is how Git (version 1.7.9.5) works.
54
- # Lets you ignore the 'remotes' if its at the beginning of the branch full name (even if is not a real remote branch).
55
- branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ /^remotes\/.+/
56
-
57
- branches
53
+ # This is how Git (version 1.7.9.5) works.
54
+ # Lets you ignore the 'remotes' if its at the beginning of the branch full
55
+ # name (even if is not a real remote branch).
56
+ branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ %r{^remotes/.+}
58
57
  end[branch_name.to_s]
59
58
  end
60
-
59
+
61
60
  def to_s
62
61
  out = ''
63
- @branches.each do |k, b|
62
+ @branches.each_value do |b|
64
63
  out << (b.current ? '* ' : ' ') << b.to_s << "\n"
65
64
  end
66
65
  out
67
66
  end
68
-
69
67
  end
70
-
71
68
  end