git 5.7.0 → 6.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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -35
  3. data/CONTRIBUTING.md +44 -31
  4. data/README.md +138 -100
  5. data/UPGRADING.md +867 -262
  6. data/git.gemspec +6 -8
  7. data/lib/git/author_info.rb +1 -2
  8. data/lib/git/commands/base.rb +22 -2
  9. data/lib/git/commands/cat_file/raw.rb +0 -68
  10. data/lib/git/commands/maintenance/register.rb +0 -4
  11. data/lib/git/commands/maintenance/run.rb +0 -4
  12. data/lib/git/commands/maintenance/start.rb +0 -4
  13. data/lib/git/commands/maintenance/stop.rb +0 -4
  14. data/lib/git/commands/maintenance/unregister.rb +0 -4
  15. data/lib/git/commands/maintenance.rb +0 -2
  16. data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
  17. data/lib/git/commands/show_ref/exists.rb +0 -5
  18. data/lib/git/commands/show_ref/list.rb +1 -1
  19. data/lib/git/commands/show_ref/verify.rb +1 -1
  20. data/lib/git/commands/show_ref.rb +1 -1
  21. data/lib/git/commands/worktree/repair.rb +0 -5
  22. data/lib/git/diff_path_status.rb +1 -1
  23. data/lib/git/encoding_utils.rb +25 -0
  24. data/lib/git/errors.rb +24 -18
  25. data/lib/git/factories.rb +361 -125
  26. data/lib/git/log.rb +3 -75
  27. data/lib/git/object.rb +4 -185
  28. data/lib/git/path_resolver.rb +104 -36
  29. data/lib/git/repository/branching.rb +21 -189
  30. data/lib/git/repository/committing.rb +0 -25
  31. data/lib/git/repository/context_helpers.rb +249 -155
  32. data/lib/git/repository/diffing.rb +2 -72
  33. data/lib/git/repository/merging.rb +32 -54
  34. data/lib/git/repository/object_operations.rb +120 -497
  35. data/lib/git/repository/remote_operations.rb +7 -219
  36. data/lib/git/repository/staging.rb +6 -184
  37. data/lib/git/repository/stashing.rb +126 -130
  38. data/lib/git/repository/status_operations.rb +1 -54
  39. data/lib/git/repository/worktree_operations.rb +30 -125
  40. data/lib/git/repository.rb +20 -299
  41. data/lib/git/system_call_guard.rb +95 -0
  42. data/lib/git/version.rb +1 -1
  43. data/lib/git.rb +47 -364
  44. metadata +24 -18
  45. data/lib/git/author.rb +0 -46
  46. data/lib/git/branch.rb +0 -693
  47. data/lib/git/branches.rb +0 -194
  48. data/lib/git/remote.rb +0 -191
  49. data/lib/git/stash.rb +0 -124
  50. data/lib/git/stashes.rb +0 -198
  51. data/lib/git/status.rb +0 -384
  52. data/lib/git/worktree.rb +0 -180
  53. data/lib/git/worktrees.rb +0 -152
@@ -17,6 +17,7 @@ require 'git/parsers/grep'
17
17
  require 'git/parsers/ls_tree'
18
18
  require 'git/parsers/tag'
19
19
  require 'git/repository/shared_private'
20
+ require 'git/system_call_guard'
20
21
  require 'git/escaped_path'
21
22
  require 'tempfile'
22
23
  require 'zlib'
@@ -72,20 +73,25 @@ module Git
72
73
  #
73
74
  # @raise [Git::FailedError] if git exits with a non-zero exit status
74
75
  #
76
+ # @raise [Git::Error] if a block is given and the object content cannot be
77
+ # written to a temporary file
78
+ #
75
79
  # @see https://git-scm.com/docs/git-cat-file git-cat-file documentation
76
80
  #
77
- def cat_file_contents(object)
81
+ def cat_file_contents(object, &block)
78
82
  raise ArgumentError, "Invalid object: '#{object}'" if object&.start_with?('-')
79
83
 
80
84
  return Git::Commands::CatFile::Raw.new(@execution_context).call(object, p: true).stdout unless block_given?
81
85
 
82
86
  # Stream git output directly to a tempfile to avoid buffering large
83
87
  # object content in memory when a block is given.
84
- Tempfile.create do |file|
85
- file.binmode
86
- Git::Commands::CatFile::Raw.new(@execution_context).call(object, p: true, out: file)
87
- file.rewind
88
- yield file
88
+ Git::SystemCallGuard.call('Failed to write the object content to a temporary file') do |guard|
89
+ Tempfile.create do |file|
90
+ file.binmode
91
+ Git::Commands::CatFile::Raw.new(@execution_context).call(object, p: true, out: file)
92
+ file.rewind
93
+ guard.unguarded { block.call(file) }
94
+ end
89
95
  end
90
96
  end
91
97
 
@@ -95,52 +101,6 @@ module Git
95
101
  # @see #cat_file_contents
96
102
  alias cat_file cat_file_contents
97
103
 
98
- # Returns the raw contents of a git object (deprecated name for {#cat_file_contents})
99
- #
100
- # @overload object_contents(object)
101
- # Returns the object's raw content as a string
102
- #
103
- # @example Read a blob's contents
104
- # repo.object_contents('HEAD:README.md') #=> "# My Project\n"
105
- #
106
- # @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
107
- #
108
- # @return [String] the raw content of the object
109
- #
110
- # @overload object_contents(object, &block)
111
- # Streams the object's raw content to a temporary file and yields it
112
- #
113
- # @example Read a large blob without buffering it in memory
114
- # repo.object_contents('HEAD:large_file.bin') { |f| process(f) }
115
- #
116
- # @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
117
- #
118
- # @yield [file] the temporary file containing the streamed content,
119
- # positioned at the start
120
- #
121
- # @yieldparam file [File] readable `IO` object positioned at the beginning
122
- # of the content
123
- #
124
- # @yieldreturn [Object] the value to return from this method
125
- #
126
- # @return [Object] the value returned by the block
127
- #
128
- # @raise [ArgumentError] if `object` starts with a hyphen
129
- #
130
- # @raise [Git::FailedError] if git exits with a non-zero exit status
131
- #
132
- # @deprecated Use {#cat_file_contents} instead. Will be removed in v6.0.0.
133
- #
134
- # @see #cat_file_contents
135
- #
136
- def object_contents(object, &)
137
- Git::Deprecation.warn(
138
- 'Git::Repository#object_contents is deprecated and will be removed in v6.0.0. ' \
139
- 'Use Git::Repository#cat_file_contents instead.'
140
- )
141
- cat_file_contents(object, &)
142
- end
143
-
144
104
  # Returns the size of a git object in bytes
145
105
  #
146
106
  # @example Get the size of a commit object
@@ -165,31 +125,6 @@ module Git
165
125
  Git::Commands::CatFile::Raw.new(@execution_context).call(object, s: true).stdout.chomp.to_i
166
126
  end
167
127
 
168
- # Returns the size of a git object in bytes (deprecated name for {#cat_file_size})
169
- #
170
- # @example Get the size of a commit object
171
- # repo.object_size('HEAD') #=> 265
172
- #
173
- # @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
174
- #
175
- # @return [Integer] the size of the object in bytes
176
- #
177
- # @raise [ArgumentError] if `object` starts with a hyphen
178
- #
179
- # @raise [Git::FailedError] if git exits with a non-zero exit status
180
- #
181
- # @deprecated Use {#cat_file_size} instead. Will be removed in v6.0.0.
182
- #
183
- # @see #cat_file_size
184
- #
185
- def object_size(object)
186
- Git::Deprecation.warn(
187
- 'Git::Repository#object_size is deprecated and will be removed in v6.0.0. ' \
188
- 'Use Git::Repository#cat_file_size instead.'
189
- )
190
- cat_file_size(object)
191
- end
192
-
193
128
  # Returns the type of a git object
194
129
  #
195
130
  # @example Get the type of a commit reference
@@ -215,31 +150,6 @@ module Git
215
150
  Git::Commands::CatFile::Raw.new(@execution_context).call(object, t: true).stdout.chomp
216
151
  end
217
152
 
218
- # Returns the type of a git object (deprecated name for {#cat_file_type})
219
- #
220
- # @example Get the type of HEAD
221
- # repo.object_type('HEAD') #=> "commit"
222
- #
223
- # @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
224
- #
225
- # @return [String] the object type: `"blob"`, `"tree"`, `"commit"`, or `"tag"`
226
- #
227
- # @raise [ArgumentError] if `object` starts with a hyphen
228
- #
229
- # @raise [Git::FailedError] if git exits with a non-zero exit status
230
- #
231
- # @deprecated Use {#cat_file_type} instead. Will be removed in v6.0.0.
232
- #
233
- # @see #cat_file_type
234
- #
235
- def object_type(object)
236
- Git::Deprecation.warn(
237
- 'Git::Repository#object_type is deprecated and will be removed in v6.0.0. ' \
238
- 'Use Git::Repository#cat_file_type instead.'
239
- )
240
- cat_file_type(object)
241
- end
242
-
243
153
  # Returns parsed commit data for the given git object
244
154
  #
245
155
  # @example Get commit data for HEAD
@@ -276,29 +186,6 @@ module Git
276
186
  Git::Parsers::CatFile.parse_commit(result.stdout.split("\n"), object)
277
187
  end
278
188
 
279
- # Returns parsed commit data for a git object (deprecated name for {#cat_file_commit})
280
- #
281
- # @example Get commit data for HEAD
282
- # repo.commit_data('HEAD')
283
- #
284
- # @param object [String] the commit name (SHA, ref, `HEAD`, etc.)
285
- #
286
- # @return [Hash] the parsed commit data; see {#cat_file_commit} for the keys
287
- #
288
- # @raise [Git::FailedError] if git exits with a non-zero exit status
289
- #
290
- # @deprecated Use {#cat_file_commit} instead. Will be removed in v6.0.0.
291
- #
292
- # @see #cat_file_commit
293
- #
294
- def commit_data(object)
295
- Git::Deprecation.warn(
296
- 'Git::Repository#commit_data is deprecated and will be removed in v6.0.0. ' \
297
- 'Use Git::Repository#cat_file_commit instead.'
298
- )
299
- cat_file_commit(object)
300
- end
301
-
302
189
  # Returns parsed tag data for the given annotated tag object
303
190
  #
304
191
  # Does not work with lightweight tags. To list all annotated tags in a
@@ -348,31 +235,6 @@ module Git
348
235
  Git::Parsers::CatFile.parse_tag(tdata, object)
349
236
  end
350
237
 
351
- # Returns parsed tag data for an annotated tag (deprecated name for {#cat_file_tag})
352
- #
353
- # @example Get tag data for an annotated tag
354
- # repo.tag_data('v1.0.0')
355
- #
356
- # @param object [String] the tag name or SHA of the annotated tag object
357
- #
358
- # @return [Hash] the parsed tag data; see {#cat_file_tag} for the keys
359
- #
360
- # @raise [ArgumentError] if `object` starts with a hyphen
361
- #
362
- # @raise [Git::FailedError] if git exits with a non-zero exit status
363
- #
364
- # @deprecated Use {#cat_file_tag} instead. Will be removed in v6.0.0.
365
- #
366
- # @see #cat_file_tag
367
- #
368
- def tag_data(object)
369
- Git::Deprecation.warn(
370
- 'Git::Repository#tag_data is deprecated and will be removed in v6.0.0. ' \
371
- 'Use Git::Repository#cat_file_tag instead.'
372
- )
373
- cat_file_tag(object)
374
- end
375
-
376
238
  # Resolve a revision specifier to its full object ID
377
239
  #
378
240
  # Passes the given revision specifier to `git rev-parse` and returns the
@@ -402,34 +264,14 @@ module Git
402
264
  Git::Commands::RevParse.new(@execution_context).call(objectish, '--', revs_only: true).stdout
403
265
  end
404
266
 
405
- # Resolves a revision specifier to its full object ID (deprecated name for {#rev_parse})
406
- #
407
- # @example Resolve HEAD to its SHA
408
- # repo.revparse('HEAD') #=> "9b9b31e704c0b85ffdd8d2af2ded85170a5af87d"
409
- #
410
- # @param objectish [String] the revision specifier to resolve (branch name,
411
- # tag, abbreviated SHA, refspec, etc.)
412
- #
413
- # @return [String] the full object ID of the resolved object
414
- #
415
- # @raise [Git::FailedError] if git exits with a non-zero exit status
416
- #
417
- # @deprecated Use {#rev_parse} instead. Will be removed in v6.0.0.
418
- #
419
- # @see #rev_parse
420
- #
421
- def revparse(objectish)
422
- Git::Deprecation.warn(
423
- 'Git::Repository#revparse is deprecated and will be removed in v6.0.0. ' \
424
- 'Use Git::Repository#rev_parse instead.'
425
- )
426
- rev_parse(objectish)
427
- end
428
-
429
267
  # Returns the SHA of a named tag
430
268
  #
431
269
  # Returns an empty string when the tag does not exist.
432
270
  #
271
+ # Reads the loose ref file under `refs/tags` directly when it exists and
272
+ # falls back to `git show-ref` otherwise, including when the file cannot
273
+ # be read.
274
+ #
433
275
  # @example Get the SHA of an existing tag
434
276
  # repo.tag_sha('v1.0')
435
277
  # #=> "abc1234567890abcdef1234567890abcdef123456"
@@ -447,9 +289,7 @@ module Git
447
289
  def tag_sha(tag_name)
448
290
  tags_dir = File.expand_path(File.join(@execution_context.git_dir, 'refs', 'tags'))
449
291
  head = File.expand_path(File.join(tags_dir, tag_name))
450
- return File.read(head).chomp if head.start_with?("#{tags_dir}#{File::SEPARATOR}") && File.file?(head)
451
-
452
- Private.show_ref_tag_sha(@execution_context, tag_name)
292
+ Private.loose_tag_sha(tags_dir, head) || Private.show_ref_tag_sha(@execution_context, tag_name)
453
293
  end
454
294
 
455
295
  # Returns all recursive entries for a given tree object
@@ -527,31 +367,6 @@ module Git
527
367
  Git::Commands::NameRev.new(@execution_context).call(commit_ish).stdout.split[1]
528
368
  end
529
369
 
530
- # Finds a symbolic name for a commit (deprecated name for {#name_rev})
531
- #
532
- # @example Name the current commit
533
- # repo.namerev('HEAD') #=> "main"
534
- #
535
- # @param commit_ish [String] the commit to name (SHA, ref, `HEAD`, etc.)
536
- #
537
- # @return [String, nil] the symbolic name, or `nil` if git finds none
538
- #
539
- # @raise [ArgumentError] if commit_ish starts with a hyphen
540
- #
541
- # @raise [Git::FailedError] if git exits with a non-zero exit status
542
- #
543
- # @deprecated Use {#name_rev} instead. Will be removed in v6.0.0.
544
- #
545
- # @see #name_rev
546
- #
547
- def namerev(commit_ish)
548
- Git::Deprecation.warn(
549
- 'Git::Repository#namerev is deprecated and will be removed in v6.0.0. ' \
550
- 'Use Git::Repository#name_rev instead.'
551
- )
552
- name_rev(commit_ish)
553
- end
554
-
555
370
  # Option keys accepted by {#ls_tree}
556
371
  LS_TREE_ALLOWED_OPTS = %i[recursive path].freeze
557
372
  private_constant :LS_TREE_ALLOWED_OPTS
@@ -771,20 +586,21 @@ module Git
771
586
  #
772
587
  # @raise [Git::FailedError] if git exits with a non-zero exit status
773
588
  #
589
+ # @raise [Git::Error] if the archive file cannot be written or gzip
590
+ # compression fails
591
+ #
592
+ # @note A failure leaves nothing behind: the staging and temporary files
593
+ # are removed and an existing `file` is not replaced.
594
+ #
774
595
  # @see https://git-scm.com/docs/git-archive git-archive documentation
775
596
  #
776
597
  def archive(treeish, file = nil, opts = {})
777
598
  SharedPrivate.assert_valid_opts!(ARCHIVE_ALLOWED_OPTS, **opts)
778
599
  raise ArgumentError, "#{file.inspect} is a directory" if file && File.directory?(file)
779
600
 
780
- tmp = Private.write_archive_tmp(@execution_context, treeish, opts, dest_dir: Private.staging_dir_for(file))
781
- return tmp unless file
782
-
783
- Private.atomic_replace(tmp, file)
784
- file
785
- rescue StandardError
786
- FileUtils.rm_f(tmp) if tmp
787
- raise
601
+ Git::SystemCallGuard.call('Failed to write the archive') do
602
+ Private.write_archive(@execution_context, treeish, file, opts)
603
+ end
788
604
  end
789
605
 
790
606
  # Returns a blob object for the given object reference
@@ -851,50 +667,6 @@ module Git
851
667
  Git::Object.new(self, objectish, 'tree')
852
668
  end
853
669
 
854
- # Returns a tag object for the given tag name
855
- #
856
- # Returns a {Git::Object::Tag} for `tag_name`. The returned object is
857
- # either an annotated or a lightweight tag depending on the underlying
858
- # ref type.
859
- #
860
- # @example Get a tag object
861
- # repo.tag('v1.0')
862
- # #=> #<Git::Object::Tag name="v1.0" ...>
863
- #
864
- # @param tag_name [String] the name of the tag
865
- #
866
- # @return [Git::Object::Tag] the tag object
867
- #
868
- # @raise [Git::UnexpectedResultError] if `tag_name` does not name an
869
- # existing tag
870
- #
871
- # @raise [Git::FailedError] if the underlying `git show-ref` invocation
872
- # exits with an unexpected status (i.e., outside the allowed 0..1 range)
873
- #
874
- # @deprecated Use `tag_list(name).first` instead
875
- #
876
- # {#tag_list} returns immutable {Git::TagInfo} value objects rather
877
- # than {Git::Object::Tag}. `tag_list(name).first` is `nil` when the tag
878
- # does not exist, where this method raises
879
- # {Git::UnexpectedResultError}. Call the corresponding
880
- # {Git::Repository} method (e.g. {#archive}, {#log}, {#diff},
881
- # {#cat_file_contents}) with `info.oid || info.target_oid` for
882
- # operations on a tag; that is the object this method's return value
883
- # pins at construction, so a later move of the tag does not redirect
884
- # it, whereas the tag name would. The
885
- # {Git::Object::Tag} constructor is deprecated too; this method
886
- # silences it so one call emits one warning.
887
- #
888
- # @see #tag_list
889
- #
890
- def tag(tag_name)
891
- Git::Deprecation.warn(
892
- 'Git::Repository#tag is deprecated and will be removed in v6.0.0. ' \
893
- 'Use Git::Repository#tag_list(name).first instead.'
894
- )
895
- Git::Deprecation.silence { Git::Object::Tag.new(self, tag_name) }
896
- end
897
-
898
670
  # Returns the appropriate git object for the given object reference
899
671
  #
900
672
  # Runs `git cat-file -t` to determine the object type, then constructs
@@ -964,46 +736,7 @@ module Git
964
736
  Git::Parsers::Tag.parse_list(result.stdout)
965
737
  end
966
738
 
967
- # Returns all tags in the repository as tag objects
968
- #
969
- # Runs `git tag --list` with a machine-readable format, parses the output,
970
- # and returns a {Git::Object::Tag} for each tag name.
971
- #
972
- # @example List the names of all tags
973
- # repo.tags.map(&:name) #=> ["v1.0.0", "v2.0.0"]
974
- #
975
- # @example No tags exist
976
- # repo.tags #=> []
977
- #
978
- # @return [Array<Git::Object::Tag>] one tag object per tag in the
979
- # repository; empty when there are none
980
- #
981
- # @raise [Git::FailedError] if git exits with a non-zero exit status
982
- #
983
- # @deprecated Use {#tag_list} instead
984
- #
985
- # {#tag_list} returns `Array<Git::TagInfo>` (immutable value objects)
986
- # rather than `Array<Git::Object::Tag>`. Look a tag up by name with
987
- # `tag_list(name).first`, and call the corresponding {Git::Repository}
988
- # method (e.g. {#archive}, {#log}, {#diff}, {#cat_file_contents}) with
989
- # `info.oid || info.target_oid` for operations on a tag; that is the
990
- # object each returned {Git::Object::Tag} pins at construction, so a
991
- # later move of the tag does not redirect it, whereas the tag name
992
- # would. The {Git::Object::Tag}
993
- # constructor is deprecated too; this method silences it so one call
994
- # emits one warning.
995
- #
996
- # @see #tag_list
997
- #
998
- def tags
999
- Git::Deprecation.warn(
1000
- 'Git::Repository#tags is deprecated and will be removed in v6.0.0. ' \
1001
- 'Use Git::Repository#tag_list instead.'
1002
- )
1003
- Git::Deprecation.silence { tag_list.map { |info| Git::Object::Tag.new(self, info.name) } }
1004
- end
1005
-
1006
- # Option keys accepted by {#tag_create} and {#tag_add}
739
+ # Option keys accepted by {#tag_create}
1007
740
  TAG_CREATE_ALLOWED_OPTS = %i[
1008
741
  annotate a sign s no_sign local_user u force f message m file F
1009
742
  edit e no_edit trailer cleanup create_reflog
@@ -1078,9 +811,7 @@ module Git
1078
811
  # @option options [Boolean, nil] :create_reflog (nil) create a reflog for
1079
812
  # the tag
1080
813
  #
1081
- # @return [Git::TagInfo, nil] the newly created tag, or `nil` when the tag
1082
- # is missing from the tag list after the create (for example, another
1083
- # process deleted it before the lookup)
814
+ # @return [Git::TagInfo] the newly created tag
1084
815
  #
1085
816
  # @overload tag_create(name, target, options = {})
1086
817
  #
@@ -1097,169 +828,31 @@ module Git
1097
828
  # @param options [Hash] options for creating the tag (same keys as the
1098
829
  # first overload)
1099
830
  #
1100
- # @return [Git::TagInfo, nil] the newly created tag, or `nil` when the tag
1101
- # is missing from the tag list after the create (for example, another
1102
- # process deleted it before the lookup)
831
+ # @return [Git::TagInfo] the newly created tag
1103
832
  #
1104
833
  # @raise [ArgumentError] if unsupported options are provided, including the
1105
- # `:d` and `:delete` keys that {#tag_add} accepts; use {#tag_delete} to
1106
- # delete a tag
834
+ # `:d` and `:delete` keys; use {#tag_delete} to delete a tag
1107
835
  #
1108
836
  # @raise [ArgumentError] if an annotated or signed tag is requested without
1109
837
  # a message
1110
838
  #
1111
839
  # @raise [ArgumentError] if more than one positional argument follows the
1112
- # name (before any options hash); {#tag_add} silently ignored the extra
1113
- # arguments and tagged the first
840
+ # name (before any options hash)
1114
841
  #
1115
842
  # @raise [Git::FailedError] if git exits with a non-zero exit status
1116
843
  #
1117
- # @note In v6.0.0 this method raises {Git::UnexpectedResultError} instead of
1118
- # returning `nil` when the tag it has just created is missing from the tag
1119
- # list, so its return value is always a {Git::TagInfo}. Code that checks
1120
- # the result for `nil` today can be left as is; the check becomes
1121
- # unreachable in v6.0.0.
844
+ # @raise [Git::UnexpectedResultError] if the tag listing cannot be parsed
845
+ # or does not contain the new tag (for example, it was deleted by another
846
+ # process before the lookup)
1122
847
  #
1123
848
  # @see https://git-scm.com/docs/git-tag git-tag
1124
849
  #
1125
850
  def tag_create(name, *args)
1126
- target, options = Private.tag_target_and_options(args, strict: true)
1127
- SharedPrivate.assert_valid_opts!(TAG_CREATE_ALLOWED_OPTS, **options)
1128
- Private.create_tag(@execution_context, name, target, options)
1129
- tag_list(name).first
1130
- end
1131
-
1132
- # Create a new tag
1133
- #
1134
- # @overload tag_add(name, options = {})
1135
- #
1136
- # @example Create a lightweight tag on HEAD
1137
- # repo.tag_add('v1.0.0')
1138
- #
1139
- # @example Create an annotated tag on HEAD
1140
- # repo.tag_add('v1.0.0', annotate: true, message: 'Release 1.0.0')
1141
- #
1142
- # @example Replace an existing tag on HEAD
1143
- # repo.tag_add('v1.0.0', force: true)
1144
- #
1145
- # @param name [String] the name of the tag to create
1146
- #
1147
- # @param options [Hash] options for creating the tag (same keys as
1148
- # {#tag_create})
1149
- #
1150
- # @return [Git::Object::Tag] the newly created tag
1151
- #
1152
- # @overload tag_add(name, target, options = {})
1153
- #
1154
- # @example Create a lightweight tag on a specific commit
1155
- # repo.tag_add('v1.0.0', 'abc123')
1156
- #
1157
- # @example Create an annotated tag on a specific commit
1158
- # repo.tag_add('v1.0.0', 'abc123', annotate: true, message: 'Release 1.0.0')
1159
- #
1160
- # @param name [String] the name of the tag to create
1161
- #
1162
- # @param target [String] the object to tag (commit SHA, branch name, etc.)
1163
- #
1164
- # @param options [Hash] options for creating the tag (same keys as
1165
- # {#tag_create})
1166
- #
1167
- # @return [Git::Object::Tag] the newly created tag
1168
- #
1169
- # @overload tag_add(name, delete_options)
1170
- #
1171
- # @deprecated Use {#tag_delete} instead.
1172
- #
1173
- # @example Delete a tag (deprecated)
1174
- # repo.tag_add('v1.0.0', d: true)
1175
- #
1176
- # @param name [String] the name of the tag to delete
1177
- #
1178
- # @param delete_options [Hash{Symbol => Boolean}] deletion options;
1179
- # only `:d` or `:delete` (set to `true`) is accepted — no other keys
1180
- # and no `target` argument may be combined with this form
1181
- #
1182
- # @return [String] git's stdout from the delete
1183
- #
1184
- # @raise [ArgumentError] if a target is also provided
1185
- #
1186
- # @raise [ArgumentError] if options other than `:d`/`:delete` are also
1187
- # provided
1188
- #
1189
- # @raise [ArgumentError] if unsupported options are provided
1190
- #
1191
- # @raise [ArgumentError] if an annotated or signed tag is requested without
1192
- # a message
1193
- #
1194
- # @raise [Git::FailedError] if git exits with a non-zero exit status
1195
- #
1196
- # @deprecated Use {#tag_create} instead
1197
- #
1198
- # {#tag_create} accepts the same `name`, `target`, and options and
1199
- # returns a {Git::TagInfo} (an immutable value object) rather than a
1200
- # {Git::Object::Tag}. It does not accept the `:d`/`:delete` form; use
1201
- # {#tag_delete} for that. The {Git::Object::Tag} constructor is
1202
- # deprecated too; this method silences it so one call emits one
1203
- # warning, except that the `:d`/`:delete` form emits a second warning
1204
- # of its own.
1205
- #
1206
- # @see #tag_create
1207
- #
1208
- def tag_add(name, *args)
1209
- Git::Deprecation.warn(
1210
- 'Git::Repository#tag_add is deprecated and will be removed in v6.0.0. ' \
1211
- 'Use Git::Repository#tag_create instead.'
1212
- )
1213
851
  target, options = Private.tag_target_and_options(args)
1214
-
1215
- return Private.tag_add_delete_deprecated(self, name, target, options) if options[:d] || options[:delete]
1216
-
1217
- options = options.except(:d, :delete)
1218
852
  SharedPrivate.assert_valid_opts!(TAG_CREATE_ALLOWED_OPTS, **options)
1219
853
  Private.create_tag(@execution_context, name, target, options)
1220
- Git::Deprecation.silence { Git::Object::Tag.new(self, name) }
1221
- end
1222
-
1223
- # @overload add_tag(name, options = {})
1224
- #
1225
- # @param name [String] the name of the tag to create
1226
- #
1227
- # @param options [Hash] options for creating the tag
1228
- #
1229
- # @return [Git::Object::Tag] the newly created tag
1230
- #
1231
- # @overload add_tag(name, target, options = {})
1232
- #
1233
- # @param name [String] the name of the tag to create
1234
- #
1235
- # @param target [String] the object to tag (commit SHA, branch name, etc.)
1236
- #
1237
- # @param options [Hash] options for creating the tag
1238
- #
1239
- # @return [Git::Object::Tag] the newly created tag
1240
- #
1241
- # @raise [ArgumentError] if unsupported options are provided
1242
- #
1243
- # @raise [ArgumentError] if an annotated or signed tag is requested without
1244
- # a message
1245
- #
1246
- # @raise [Git::FailedError] if git exits with a non-zero exit status
1247
- #
1248
- # @deprecated Use {#tag_create} instead
1249
- #
1250
- # The warning names {#tag_add}, the replacement this method shipped
1251
- # with, and {#tag_add} is deprecated as well, so a creation call emits
1252
- # two warnings: one for this method and one for {#tag_add}. The delete
1253
- # form `add_tag(name, d: true)` emits a third, for the deprecated `:d`
1254
- # and `:delete` options on {#tag_add}; use {#tag_delete} for that. Go
1255
- # straight to {#tag_create} for creation.
1256
- #
1257
- def add_tag(name, *)
1258
- Git::Deprecation.warn(
1259
- 'Git::Repository#add_tag is deprecated and will be removed in v6.0.0. ' \
1260
- 'Use Git::Repository#tag_add instead.'
1261
- )
1262
- tag_add(name, *)
854
+ tag_list(name).first ||
855
+ raise(Git::UnexpectedResultError, "tag was created but not found in the tag list: #{name}")
1263
856
  end
1264
857
 
1265
858
  # Delete a tag
@@ -1280,54 +873,34 @@ module Git
1280
873
  result.stdout
1281
874
  end
1282
875
 
1283
- # @param name [String] the name of the tag to delete
1284
- #
1285
- # @return [String] git's stdout from the delete
1286
- #
1287
- # @raise [Git::FailedError] if git exits with a non-zero exit status
1288
- #
1289
- # @deprecated Use {#tag_delete} instead
1290
- #
1291
- def delete_tag(name)
1292
- Git::Deprecation.warn(
1293
- 'Git::Repository#delete_tag is deprecated and will be removed in v6.0.0. ' \
1294
- 'Use Git::Repository#tag_delete instead.'
1295
- )
1296
- tag_delete(name)
1297
- end
1298
-
1299
876
  # Private helpers
1300
877
  #
1301
878
  # @api private
1302
879
  #
1303
- module Private
880
+ module Private # rubocop:disable Metrics/ModuleLength
1304
881
  module_function
1305
882
 
1306
- # Splits the variadic `*args` of {ObjectOperations#tag_create} and
1307
- # {ObjectOperations#tag_add} into the target and the options hash
883
+ # Splits the variadic `*args` of {ObjectOperations#tag_create} into the
884
+ # target and the options hash
1308
885
  #
1309
- # Both methods accept `(name, opts = {})` and `(name, target, opts = {})`,
886
+ # `tag_create` accepts `(name, opts = {})` and `(name, target, opts = {})`,
1310
887
  # so a trailing `Hash` is the options and anything before it is the
1311
888
  # target.
1312
889
  #
1313
890
  # @param args [Array] the arguments after the tag name
1314
891
  #
1315
- # @param strict [Boolean] when `true`, raise instead of silently ignoring
1316
- # a second positional argument before the options; `tag_create` is
1317
- # strict, while the deprecated `tag_add` keeps its lenient behavior
1318
- #
1319
892
  # @return [Array((String, nil), Hash)] the two-element tuple
1320
893
  # `[target, options]`; `target` is `nil` when only options were given
1321
894
  #
1322
- # @raise [ArgumentError] if `strict` is `true` and more than one
1323
- # positional argument precedes the options hash
895
+ # @raise [ArgumentError] if more than one positional argument precedes
896
+ # the options hash
1324
897
  #
1325
898
  # @api private
1326
899
  #
1327
- def tag_target_and_options(args, strict: false)
900
+ def tag_target_and_options(args)
1328
901
  args = args.dup
1329
902
  options = args.last.is_a?(Hash) ? args.pop : {}
1330
- if strict && args.size > 1
903
+ if args.size > 1
1331
904
  raise ArgumentError,
1332
905
  "Expected at most one target before the options, got #{args.size}: #{args.inspect}"
1333
906
  end
@@ -1418,39 +991,25 @@ module Git
1418
991
  raise ArgumentError, 'Cannot create an annotated or signed tag without a message.'
1419
992
  end
1420
993
 
1421
- # Handle the deprecated :d/:delete option on tag_add
1422
- #
1423
- # Issues a deprecation warning and delegates to tag_delete. Raises
1424
- # ArgumentError if a target or incompatible options are also supplied.
1425
- #
1426
- # @param facade [ObjectOperations] the calling facade instance
994
+ # Read the SHA from a loose tag ref file without forking git
1427
995
  #
1428
- # @param name [String] tag name
996
+ # Returns `nil` when the ref is not a loose file under `tags_dir` or when
997
+ # the file cannot be read, so the caller falls back to `git show-ref`.
1429
998
  #
1430
- # @param target [String, nil] target argument (must be nil)
999
+ # @param tags_dir [String] absolute path of the `refs/tags` directory
1431
1000
  #
1432
- # @param opts [Hash] options hash (must contain only :d/:delete)
1001
+ # @param head [String] absolute path of the candidate loose ref file
1433
1002
  #
1434
- # @option opts [Boolean] :d (true) request deletion in the deprecated
1435
- # `tag_add` form
1436
- #
1437
- # @option opts [Boolean] :delete (true) alias for `:d`
1438
- #
1439
- # @return [String] stdout from tag_delete
1003
+ # @return [String, nil] the SHA in the file, or `nil` to fall back to git
1440
1004
  #
1441
1005
  # @api private
1442
1006
  #
1443
- def tag_add_delete_deprecated(facade, name, target, opts)
1444
- Git::Deprecation.warn(
1445
- 'Passing :d or :delete to tag_add is deprecated and will be removed in v6.0.0. ' \
1446
- 'Use tag_delete instead.'
1447
- )
1448
- raise ArgumentError, 'Cannot pass a target when using the :d/:delete option.' if target
1007
+ def loose_tag_sha(tags_dir, head)
1008
+ return nil unless head.start_with?("#{tags_dir}#{File::SEPARATOR}") && File.file?(head)
1449
1009
 
1450
- extra = opts.keys - %i[d delete]
1451
- raise ArgumentError, "Cannot combine :d/:delete with other options: #{extra.join(', ')}" unless extra.empty?
1452
-
1453
- facade.tag_delete(name)
1010
+ File.read(head).chomp
1011
+ rescue SystemCallError
1012
+ nil
1454
1013
  end
1455
1014
 
1456
1015
  # Returns the direct SHA for a tag reference
@@ -1511,6 +1070,45 @@ module Git
1511
1070
  File.dirname(File.expand_path(file))
1512
1071
  end
1513
1072
 
1073
+ # Write a git archive, staging it in a temporary file first
1074
+ #
1075
+ # @param execution_context [Git::ExecutionContext] the execution context
1076
+ #
1077
+ # @param treeish [String] tree-ish to archive
1078
+ #
1079
+ # @param file [String, nil] destination path, or `nil` to keep the
1080
+ # temporary file
1081
+ #
1082
+ # @param opts [Hash] caller-supplied options (read-only)
1083
+ #
1084
+ # @option opts [String] :format ('zip') archive format (`'tar'`, `'zip'`,
1085
+ # or `'tgz'`)
1086
+ #
1087
+ # @option opts [Boolean, nil] :add_gzip (nil) apply gzip post-processing
1088
+ # to the generated archive
1089
+ #
1090
+ # @option opts [String] :prefix (nil) prefix for entries in the archive
1091
+ #
1092
+ # @option opts [String] :path (nil) path within `treeish` to archive
1093
+ #
1094
+ # @option opts [String] :remote (nil) remote repository from which to
1095
+ # retrieve the archive
1096
+ #
1097
+ # @return [String] path to the written archive file
1098
+ #
1099
+ # @api private
1100
+ #
1101
+ def write_archive(execution_context, treeish, file, opts)
1102
+ tmp = write_archive_tmp(execution_context, treeish, opts, dest_dir: staging_dir_for(file))
1103
+ return tmp unless file
1104
+
1105
+ atomic_replace(tmp, file)
1106
+ file
1107
+ rescue StandardError
1108
+ FileUtils.rm_f(tmp) if tmp
1109
+ raise
1110
+ end
1111
+
1514
1112
  # Write a git archive to a fresh temporary file and return its path
1515
1113
  #
1516
1114
  # Always writes to a new temporary file so that on error the caller's
@@ -1693,17 +1291,42 @@ module Git
1693
1291
  #
1694
1292
  # @return [void]
1695
1293
  #
1294
+ # @raise [Git::Error] if zlib fails during compression, with the
1295
+ # `Zlib::Error` as `cause`
1296
+ #
1696
1297
  # @api private
1697
1298
  #
1698
1299
  def apply_gzip(file)
1699
1300
  gz_tmp = Tempfile.create('archive_gz', File.dirname(file)).tap(&:close).path
1700
- Zlib::GzipWriter.open(gz_tmp) { |gz| File.open(file, 'rb') { |f| IO.copy_stream(f, gz) } }
1301
+ gzip_file(file, gz_tmp)
1701
1302
  FileUtils.rm_f(file)
1702
1303
  File.rename(gz_tmp, file)
1703
1304
  rescue StandardError
1704
1305
  FileUtils.rm_f(gz_tmp) if gz_tmp
1705
1306
  raise
1706
1307
  end
1308
+
1309
+ # Stream `src` through a {Zlib::GzipWriter} into `dest`
1310
+ #
1311
+ # A `Zlib::Error` is not a `SystemCallError`, so {Git::SystemCallGuard}
1312
+ # does not convert it; this method does, per ADR-0008.
1313
+ #
1314
+ # @param src [String] path to the file to compress
1315
+ #
1316
+ # @param dest [String] path to write the gzip stream to
1317
+ #
1318
+ # @return [void]
1319
+ #
1320
+ # @raise [Git::Error] if zlib fails during compression, with the
1321
+ # `Zlib::Error` as `cause`
1322
+ #
1323
+ # @api private
1324
+ #
1325
+ def gzip_file(src, dest)
1326
+ Zlib::GzipWriter.open(dest) { |gz| File.open(src, 'rb') { |f| IO.copy_stream(f, gz) } }
1327
+ rescue Zlib::Error => e
1328
+ raise Git::Error, "Failed to gzip the archive: #{e.message}"
1329
+ end
1707
1330
  end
1708
1331
  private_constant :Private
1709
1332
  end