git 5.6.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 -16
  3. data/CONTRIBUTING.md +44 -31
  4. data/README.md +135 -67
  5. data/UPGRADING.md +915 -268
  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 +2 -2
  23. data/lib/git/encoding_utils.rb +25 -0
  24. data/lib/git/errors.rb +23 -5
  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 -44
  33. data/lib/git/repository/merging.rb +32 -54
  34. data/lib/git/repository/object_operations.rb +121 -338
  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 +31 -118
  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 +25 -19
  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,35 +73,34 @@ 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
 
92
- # Alias for {#cat_file_contents}; retained for backward compatibility
98
+ # Alias for {#cat_file_contents}; kept as a legacy name and not scheduled
99
+ # for removal
93
100
  #
94
101
  # @see #cat_file_contents
95
102
  alias cat_file cat_file_contents
96
103
 
97
- # Alias for {#cat_file_contents}
98
- #
99
- # @deprecated Use {#cat_file_contents} instead
100
- #
101
- # @see #cat_file_contents
102
- alias object_contents cat_file_contents
103
-
104
104
  # Returns the size of a git object in bytes
105
105
  #
106
106
  # @example Get the size of a commit object
@@ -125,13 +125,6 @@ module Git
125
125
  Git::Commands::CatFile::Raw.new(@execution_context).call(object, s: true).stdout.chomp.to_i
126
126
  end
127
127
 
128
- # Alias for {#cat_file_size}
129
- #
130
- # @deprecated Use {#cat_file_size} instead
131
- #
132
- # @see #cat_file_size
133
- alias object_size cat_file_size
134
-
135
128
  # Returns the type of a git object
136
129
  #
137
130
  # @example Get the type of a commit reference
@@ -157,13 +150,6 @@ module Git
157
150
  Git::Commands::CatFile::Raw.new(@execution_context).call(object, t: true).stdout.chomp
158
151
  end
159
152
 
160
- # Alias for {#cat_file_type}
161
- #
162
- # @deprecated Use {#cat_file_type} instead
163
- #
164
- # @see #cat_file_type
165
- alias object_type cat_file_type
166
-
167
153
  # Returns parsed commit data for the given git object
168
154
  #
169
155
  # @example Get commit data for HEAD
@@ -200,13 +186,6 @@ module Git
200
186
  Git::Parsers::CatFile.parse_commit(result.stdout.split("\n"), object)
201
187
  end
202
188
 
203
- # Alias for {#cat_file_commit}
204
- #
205
- # @deprecated Use {#cat_file_commit} instead
206
- #
207
- # @see #cat_file_commit
208
- alias commit_data cat_file_commit
209
-
210
189
  # Returns parsed tag data for the given annotated tag object
211
190
  #
212
191
  # Does not work with lightweight tags. To list all annotated tags in a
@@ -256,13 +235,6 @@ module Git
256
235
  Git::Parsers::CatFile.parse_tag(tdata, object)
257
236
  end
258
237
 
259
- # Alias for {#cat_file_tag}
260
- #
261
- # @deprecated Use {#cat_file_tag} instead
262
- #
263
- # @see #cat_file_tag
264
- alias tag_data cat_file_tag
265
-
266
238
  # Resolve a revision specifier to its full object ID
267
239
  #
268
240
  # Passes the given revision specifier to `git rev-parse` and returns the
@@ -292,12 +264,14 @@ module Git
292
264
  Git::Commands::RevParse.new(@execution_context).call(objectish, '--', revs_only: true).stdout
293
265
  end
294
266
 
295
- alias revparse rev_parse
296
-
297
267
  # Returns the SHA of a named tag
298
268
  #
299
269
  # Returns an empty string when the tag does not exist.
300
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
+ #
301
275
  # @example Get the SHA of an existing tag
302
276
  # repo.tag_sha('v1.0')
303
277
  # #=> "abc1234567890abcdef1234567890abcdef123456"
@@ -315,9 +289,7 @@ module Git
315
289
  def tag_sha(tag_name)
316
290
  tags_dir = File.expand_path(File.join(@execution_context.git_dir, 'refs', 'tags'))
317
291
  head = File.expand_path(File.join(tags_dir, tag_name))
318
- return File.read(head).chomp if head.start_with?("#{tags_dir}#{File::SEPARATOR}") && File.file?(head)
319
-
320
- 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)
321
293
  end
322
294
 
323
295
  # Returns all recursive entries for a given tree object
@@ -395,13 +367,6 @@ module Git
395
367
  Git::Commands::NameRev.new(@execution_context).call(commit_ish).stdout.split[1]
396
368
  end
397
369
 
398
- # Alias for {#name_rev}
399
- #
400
- # @deprecated Use {#name_rev} instead
401
- #
402
- # @see #name_rev
403
- alias namerev name_rev
404
-
405
370
  # Option keys accepted by {#ls_tree}
406
371
  LS_TREE_ALLOWED_OPTS = %i[recursive path].freeze
407
372
  private_constant :LS_TREE_ALLOWED_OPTS
@@ -621,20 +586,21 @@ module Git
621
586
  #
622
587
  # @raise [Git::FailedError] if git exits with a non-zero exit status
623
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
+ #
624
595
  # @see https://git-scm.com/docs/git-archive git-archive documentation
625
596
  #
626
597
  def archive(treeish, file = nil, opts = {})
627
598
  SharedPrivate.assert_valid_opts!(ARCHIVE_ALLOWED_OPTS, **opts)
628
599
  raise ArgumentError, "#{file.inspect} is a directory" if file && File.directory?(file)
629
600
 
630
- tmp = Private.write_archive_tmp(@execution_context, treeish, opts, dest_dir: Private.staging_dir_for(file))
631
- return tmp unless file
632
-
633
- Private.atomic_replace(tmp, file)
634
- file
635
- rescue StandardError
636
- FileUtils.rm_f(tmp) if tmp
637
- raise
601
+ Git::SystemCallGuard.call('Failed to write the archive') do
602
+ Private.write_archive(@execution_context, treeish, file, opts)
603
+ end
638
604
  end
639
605
 
640
606
  # Returns a blob object for the given object reference
@@ -701,50 +667,6 @@ module Git
701
667
  Git::Object.new(self, objectish, 'tree')
702
668
  end
703
669
 
704
- # Returns a tag object for the given tag name
705
- #
706
- # Returns a {Git::Object::Tag} for `tag_name`. The returned object is
707
- # either an annotated or a lightweight tag depending on the underlying
708
- # ref type.
709
- #
710
- # @example Get a tag object
711
- # repo.tag('v1.0')
712
- # #=> #<Git::Object::Tag name="v1.0" ...>
713
- #
714
- # @param tag_name [String] the name of the tag
715
- #
716
- # @return [Git::Object::Tag] the tag object
717
- #
718
- # @raise [Git::UnexpectedResultError] if `tag_name` does not name an
719
- # existing tag
720
- #
721
- # @raise [Git::FailedError] if the underlying `git show-ref` invocation
722
- # exits with an unexpected status (i.e., outside the allowed 0..1 range)
723
- #
724
- # @deprecated Use `tag_list(name).first` instead
725
- #
726
- # {#tag_list} returns immutable {Git::TagInfo} value objects rather
727
- # than {Git::Object::Tag}. `tag_list(name).first` is `nil` when the tag
728
- # does not exist, where this method raises
729
- # {Git::UnexpectedResultError}. Call the corresponding
730
- # {Git::Repository} method (e.g. {#archive}, {#log}, {#diff},
731
- # {#cat_file_contents}) with `info.oid || info.target_oid` for
732
- # operations on a tag; that is the object this method's return value
733
- # pins at construction, so a later move of the tag does not redirect
734
- # it, whereas the tag name would. The
735
- # {Git::Object::Tag} constructor is deprecated too; this method
736
- # silences it so one call emits one warning.
737
- #
738
- # @see #tag_list
739
- #
740
- def tag(tag_name)
741
- Git::Deprecation.warn(
742
- 'Git::Repository#tag is deprecated and will be removed in v6.0.0. ' \
743
- 'Use Git::Repository#tag_list(name).first instead.'
744
- )
745
- Git::Deprecation.silence { Git::Object::Tag.new(self, tag_name) }
746
- end
747
-
748
670
  # Returns the appropriate git object for the given object reference
749
671
  #
750
672
  # Runs `git cat-file -t` to determine the object type, then constructs
@@ -814,46 +736,7 @@ module Git
814
736
  Git::Parsers::Tag.parse_list(result.stdout)
815
737
  end
816
738
 
817
- # Returns all tags in the repository as tag objects
818
- #
819
- # Runs `git tag --list` with a machine-readable format, parses the output,
820
- # and returns a {Git::Object::Tag} for each tag name.
821
- #
822
- # @example List the names of all tags
823
- # repo.tags.map(&:name) #=> ["v1.0.0", "v2.0.0"]
824
- #
825
- # @example No tags exist
826
- # repo.tags #=> []
827
- #
828
- # @return [Array<Git::Object::Tag>] one tag object per tag in the
829
- # repository; empty when there are none
830
- #
831
- # @raise [Git::FailedError] if git exits with a non-zero exit status
832
- #
833
- # @deprecated Use {#tag_list} instead
834
- #
835
- # {#tag_list} returns `Array<Git::TagInfo>` (immutable value objects)
836
- # rather than `Array<Git::Object::Tag>`. Look a tag up by name with
837
- # `tag_list(name).first`, and call the corresponding {Git::Repository}
838
- # method (e.g. {#archive}, {#log}, {#diff}, {#cat_file_contents}) with
839
- # `info.oid || info.target_oid` for operations on a tag; that is the
840
- # object each returned {Git::Object::Tag} pins at construction, so a
841
- # later move of the tag does not redirect it, whereas the tag name
842
- # would. The {Git::Object::Tag}
843
- # constructor is deprecated too; this method silences it so one call
844
- # emits one warning.
845
- #
846
- # @see #tag_list
847
- #
848
- def tags
849
- Git::Deprecation.warn(
850
- 'Git::Repository#tags is deprecated and will be removed in v6.0.0. ' \
851
- 'Use Git::Repository#tag_list instead.'
852
- )
853
- Git::Deprecation.silence { tag_list.map { |info| Git::Object::Tag.new(self, info.name) } }
854
- end
855
-
856
- # Option keys accepted by {#tag_create} and {#tag_add}
739
+ # Option keys accepted by {#tag_create}
857
740
  TAG_CREATE_ALLOWED_OPTS = %i[
858
741
  annotate a sign s no_sign local_user u force f message m file F
859
742
  edit e no_edit trailer cleanup create_reflog
@@ -948,158 +831,28 @@ module Git
948
831
  # @return [Git::TagInfo] the newly created tag
949
832
  #
950
833
  # @raise [ArgumentError] if unsupported options are provided, including the
951
- # `:d` and `:delete` keys that {#tag_add} accepts; use {#tag_delete} to
952
- # delete a tag
834
+ # `:d` and `:delete` keys; use {#tag_delete} to delete a tag
953
835
  #
954
836
  # @raise [ArgumentError] if an annotated or signed tag is requested without
955
837
  # a message
956
838
  #
957
839
  # @raise [ArgumentError] if more than one positional argument follows the
958
- # name (before any options hash); {#tag_add} silently ignored the extra
959
- # arguments and tagged the first
840
+ # name (before any options hash)
960
841
  #
961
842
  # @raise [Git::FailedError] if git exits with a non-zero exit status
962
843
  #
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)
847
+ #
963
848
  # @see https://git-scm.com/docs/git-tag git-tag
964
849
  #
965
850
  def tag_create(name, *args)
966
- target, options = Private.tag_target_and_options(args, strict: true)
967
- SharedPrivate.assert_valid_opts!(TAG_CREATE_ALLOWED_OPTS, **options)
968
- Private.create_tag(@execution_context, name, target, options)
969
- tag_list(name).first
970
- end
971
-
972
- # Create a new tag
973
- #
974
- # @overload tag_add(name, options = {})
975
- #
976
- # @example Create a lightweight tag on HEAD
977
- # repo.tag_add('v1.0.0')
978
- #
979
- # @example Create an annotated tag on HEAD
980
- # repo.tag_add('v1.0.0', annotate: true, message: 'Release 1.0.0')
981
- #
982
- # @example Replace an existing tag on HEAD
983
- # repo.tag_add('v1.0.0', force: true)
984
- #
985
- # @param name [String] the name of the tag to create
986
- #
987
- # @param options [Hash] options for creating the tag (same keys as
988
- # {#tag_create})
989
- #
990
- # @return [Git::Object::Tag] the newly created tag
991
- #
992
- # @overload tag_add(name, target, options = {})
993
- #
994
- # @example Create a lightweight tag on a specific commit
995
- # repo.tag_add('v1.0.0', 'abc123')
996
- #
997
- # @example Create an annotated tag on a specific commit
998
- # repo.tag_add('v1.0.0', 'abc123', annotate: true, message: 'Release 1.0.0')
999
- #
1000
- # @param name [String] the name of the tag to create
1001
- #
1002
- # @param target [String] the object to tag (commit SHA, branch name, etc.)
1003
- #
1004
- # @param options [Hash] options for creating the tag (same keys as
1005
- # {#tag_create})
1006
- #
1007
- # @return [Git::Object::Tag] the newly created tag
1008
- #
1009
- # @overload tag_add(name, delete_options)
1010
- #
1011
- # @deprecated Use {#tag_delete} instead.
1012
- #
1013
- # @example Delete a tag (deprecated)
1014
- # repo.tag_add('v1.0.0', d: true)
1015
- #
1016
- # @param name [String] the name of the tag to delete
1017
- #
1018
- # @param delete_options [Hash{Symbol => Boolean}] deletion options;
1019
- # only `:d` or `:delete` (set to `true`) is accepted — no other keys
1020
- # and no `target` argument may be combined with this form
1021
- #
1022
- # @return [String] git's stdout from the delete
1023
- #
1024
- # @raise [ArgumentError] if a target is also provided
1025
- #
1026
- # @raise [ArgumentError] if options other than `:d`/`:delete` are also
1027
- # provided
1028
- #
1029
- # @raise [ArgumentError] if unsupported options are provided
1030
- #
1031
- # @raise [ArgumentError] if an annotated or signed tag is requested without
1032
- # a message
1033
- #
1034
- # @raise [Git::FailedError] if git exits with a non-zero exit status
1035
- #
1036
- # @deprecated Use {#tag_create} instead
1037
- #
1038
- # {#tag_create} accepts the same `name`, `target`, and options and
1039
- # returns a {Git::TagInfo} (an immutable value object) rather than a
1040
- # {Git::Object::Tag}. It does not accept the `:d`/`:delete` form; use
1041
- # {#tag_delete} for that. The {Git::Object::Tag} constructor is
1042
- # deprecated too; this method silences it so one call emits one
1043
- # warning, except that the `:d`/`:delete` form emits a second warning
1044
- # of its own.
1045
- #
1046
- # @see #tag_create
1047
- #
1048
- def tag_add(name, *args)
1049
- Git::Deprecation.warn(
1050
- 'Git::Repository#tag_add is deprecated and will be removed in v6.0.0. ' \
1051
- 'Use Git::Repository#tag_create instead.'
1052
- )
1053
851
  target, options = Private.tag_target_and_options(args)
1054
-
1055
- return Private.tag_add_delete_deprecated(self, name, target, options) if options[:d] || options[:delete]
1056
-
1057
- options = options.except(:d, :delete)
1058
852
  SharedPrivate.assert_valid_opts!(TAG_CREATE_ALLOWED_OPTS, **options)
1059
853
  Private.create_tag(@execution_context, name, target, options)
1060
- Git::Deprecation.silence { Git::Object::Tag.new(self, name) }
1061
- end
1062
-
1063
- # @overload add_tag(name, options = {})
1064
- #
1065
- # @param name [String] the name of the tag to create
1066
- #
1067
- # @param options [Hash] options for creating the tag
1068
- #
1069
- # @return [Git::Object::Tag] the newly created tag
1070
- #
1071
- # @overload add_tag(name, target, options = {})
1072
- #
1073
- # @param name [String] the name of the tag to create
1074
- #
1075
- # @param target [String] the object to tag (commit SHA, branch name, etc.)
1076
- #
1077
- # @param options [Hash] options for creating the tag
1078
- #
1079
- # @return [Git::Object::Tag] the newly created tag
1080
- #
1081
- # @raise [ArgumentError] if unsupported options are provided
1082
- #
1083
- # @raise [ArgumentError] if an annotated or signed tag is requested without
1084
- # a message
1085
- #
1086
- # @raise [Git::FailedError] if git exits with a non-zero exit status
1087
- #
1088
- # @deprecated Use {#tag_create} instead
1089
- #
1090
- # The warning names {#tag_add}, the replacement this method shipped
1091
- # with, and {#tag_add} is deprecated as well, so a creation call emits
1092
- # two warnings: one for this method and one for {#tag_add}. The delete
1093
- # form `add_tag(name, d: true)` emits a third, for the deprecated `:d`
1094
- # and `:delete` options on {#tag_add}; use {#tag_delete} for that. Go
1095
- # straight to {#tag_create} for creation.
1096
- #
1097
- def add_tag(name, *)
1098
- Git::Deprecation.warn(
1099
- 'Git::Repository#add_tag is deprecated and will be removed in v6.0.0. ' \
1100
- 'Use Git::Repository#tag_add instead.'
1101
- )
1102
- tag_add(name, *)
854
+ tag_list(name).first ||
855
+ raise(Git::UnexpectedResultError, "tag was created but not found in the tag list: #{name}")
1103
856
  end
1104
857
 
1105
858
  # Delete a tag
@@ -1120,54 +873,34 @@ module Git
1120
873
  result.stdout
1121
874
  end
1122
875
 
1123
- # @param name [String] the name of the tag to delete
1124
- #
1125
- # @return [String] git's stdout from the delete
1126
- #
1127
- # @raise [Git::FailedError] if git exits with a non-zero exit status
1128
- #
1129
- # @deprecated Use {#tag_delete} instead
1130
- #
1131
- def delete_tag(name)
1132
- Git::Deprecation.warn(
1133
- 'Git::Repository#delete_tag is deprecated and will be removed in v6.0.0. ' \
1134
- 'Use Git::Repository#tag_delete instead.'
1135
- )
1136
- tag_delete(name)
1137
- end
1138
-
1139
876
  # Private helpers
1140
877
  #
1141
878
  # @api private
1142
879
  #
1143
- module Private
880
+ module Private # rubocop:disable Metrics/ModuleLength
1144
881
  module_function
1145
882
 
1146
- # Splits the variadic `*args` of {ObjectOperations#tag_create} and
1147
- # {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
1148
885
  #
1149
- # Both methods accept `(name, opts = {})` and `(name, target, opts = {})`,
886
+ # `tag_create` accepts `(name, opts = {})` and `(name, target, opts = {})`,
1150
887
  # so a trailing `Hash` is the options and anything before it is the
1151
888
  # target.
1152
889
  #
1153
890
  # @param args [Array] the arguments after the tag name
1154
891
  #
1155
- # @param strict [Boolean] when `true`, raise instead of silently ignoring
1156
- # a second positional argument before the options; `tag_create` is
1157
- # strict, while the deprecated `tag_add` keeps its lenient behavior
1158
- #
1159
892
  # @return [Array((String, nil), Hash)] the two-element tuple
1160
893
  # `[target, options]`; `target` is `nil` when only options were given
1161
894
  #
1162
- # @raise [ArgumentError] if `strict` is `true` and more than one
1163
- # positional argument precedes the options hash
895
+ # @raise [ArgumentError] if more than one positional argument precedes
896
+ # the options hash
1164
897
  #
1165
898
  # @api private
1166
899
  #
1167
- def tag_target_and_options(args, strict: false)
900
+ def tag_target_and_options(args)
1168
901
  args = args.dup
1169
902
  options = args.last.is_a?(Hash) ? args.pop : {}
1170
- if strict && args.size > 1
903
+ if args.size > 1
1171
904
  raise ArgumentError,
1172
905
  "Expected at most one target before the options, got #{args.size}: #{args.inspect}"
1173
906
  end
@@ -1258,39 +991,25 @@ module Git
1258
991
  raise ArgumentError, 'Cannot create an annotated or signed tag without a message.'
1259
992
  end
1260
993
 
1261
- # Handle the deprecated :d/:delete option on tag_add
1262
- #
1263
- # Issues a deprecation warning and delegates to tag_delete. Raises
1264
- # ArgumentError if a target or incompatible options are also supplied.
1265
- #
1266
- # @param facade [ObjectOperations] the calling facade instance
994
+ # Read the SHA from a loose tag ref file without forking git
1267
995
  #
1268
- # @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`.
1269
998
  #
1270
- # @param target [String, nil] target argument (must be nil)
999
+ # @param tags_dir [String] absolute path of the `refs/tags` directory
1271
1000
  #
1272
- # @param opts [Hash] options hash (must contain only :d/:delete)
1001
+ # @param head [String] absolute path of the candidate loose ref file
1273
1002
  #
1274
- # @option opts [Boolean] :d (true) request deletion in the deprecated
1275
- # `tag_add` form
1276
- #
1277
- # @option opts [Boolean] :delete (true) alias for `:d`
1278
- #
1279
- # @return [String] stdout from tag_delete
1003
+ # @return [String, nil] the SHA in the file, or `nil` to fall back to git
1280
1004
  #
1281
1005
  # @api private
1282
1006
  #
1283
- def tag_add_delete_deprecated(facade, name, target, opts)
1284
- Git::Deprecation.warn(
1285
- 'Passing :d or :delete to tag_add is deprecated and will be removed in v6.0.0. ' \
1286
- 'Use tag_delete instead.'
1287
- )
1288
- raise ArgumentError, 'Cannot pass a target when using the :d/:delete option.' if target
1289
-
1290
- extra = opts.keys - %i[d delete]
1291
- raise ArgumentError, "Cannot combine :d/:delete with other options: #{extra.join(', ')}" unless extra.empty?
1007
+ def loose_tag_sha(tags_dir, head)
1008
+ return nil unless head.start_with?("#{tags_dir}#{File::SEPARATOR}") && File.file?(head)
1292
1009
 
1293
- facade.tag_delete(name)
1010
+ File.read(head).chomp
1011
+ rescue SystemCallError
1012
+ nil
1294
1013
  end
1295
1014
 
1296
1015
  # Returns the direct SHA for a tag reference
@@ -1351,6 +1070,45 @@ module Git
1351
1070
  File.dirname(File.expand_path(file))
1352
1071
  end
1353
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
+
1354
1112
  # Write a git archive to a fresh temporary file and return its path
1355
1113
  #
1356
1114
  # Always writes to a new temporary file so that on error the caller's
@@ -1533,17 +1291,42 @@ module Git
1533
1291
  #
1534
1292
  # @return [void]
1535
1293
  #
1294
+ # @raise [Git::Error] if zlib fails during compression, with the
1295
+ # `Zlib::Error` as `cause`
1296
+ #
1536
1297
  # @api private
1537
1298
  #
1538
1299
  def apply_gzip(file)
1539
1300
  gz_tmp = Tempfile.create('archive_gz', File.dirname(file)).tap(&:close).path
1540
- Zlib::GzipWriter.open(gz_tmp) { |gz| File.open(file, 'rb') { |f| IO.copy_stream(f, gz) } }
1301
+ gzip_file(file, gz_tmp)
1541
1302
  FileUtils.rm_f(file)
1542
1303
  File.rename(gz_tmp, file)
1543
1304
  rescue StandardError
1544
1305
  FileUtils.rm_f(gz_tmp) if gz_tmp
1545
1306
  raise
1546
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
1547
1330
  end
1548
1331
  private_constant :Private
1549
1332
  end