git 5.7.1 → 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 -42
  3. data/CONTRIBUTING.md +44 -31
  4. data/README.md +138 -100
  5. data/UPGRADING.md +864 -259
  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
data/lib/git/log.rb CHANGED
@@ -9,10 +9,10 @@ module Git
9
9
  # return every matching commit. Calling {#all} adds the `--all` flag to include
10
10
  # all refs in the search but does not change the number of commits returned.
11
11
  #
12
- # The query is lazily executed when results are requested either via the modern
13
- # `#execute` method or the deprecated Enumerable methods.
12
+ # The query runs when {#execute} is called and returns a {Git::Log::Result}
13
+ # holding the matching commits.
14
14
  #
15
- # @example Using the modern `execute` API
15
+ # @example Running a query
16
16
  # log = git.log.max_count(50).between('v1.0', 'v1.1').author('Scott')
17
17
  # results = log.execute
18
18
  # puts "Found #{results.size} commits."
@@ -21,8 +21,6 @@ module Git
21
21
  # @api public
22
22
  #
23
23
  class Log
24
- include Enumerable
25
-
26
24
  # An immutable, Enumerable collection of `Git::Object::Commit` objects.
27
25
  # Returned by `Git::Log#execute`.
28
26
  #
@@ -236,76 +234,6 @@ module Git
236
234
  Result.new(@commits)
237
235
  end
238
236
 
239
- # @!group Deprecated Enumerable Interface
240
-
241
- # @deprecated Use {#execute} and call `each` on the result.
242
- def each(&)
243
- Git::Deprecation.warn(
244
- 'Calling Git::Log#each is deprecated and will be removed in v6.0.0. ' \
245
- 'Call #execute and then #each on the result object.'
246
- )
247
- run_log_if_dirty
248
- @commits.each(&)
249
- end
250
-
251
- # @deprecated Use {#execute} and call `size` on the result.
252
- def size
253
- Git::Deprecation.warn(
254
- 'Calling Git::Log#size is deprecated and will be removed in v6.0.0. ' \
255
- 'Call #execute and then #size on the result object.'
256
- )
257
- run_log_if_dirty
258
- @commits.size
259
- end
260
-
261
- # @deprecated Use {#execute} and call `to_s` on the result.
262
- def to_s
263
- Git::Deprecation.warn(
264
- 'Calling Git::Log#to_s is deprecated and will be removed in v6.0.0. ' \
265
- 'Call #execute and then #to_s on the result object.'
266
- )
267
- run_log_if_dirty
268
- @commits.join("\n")
269
- end
270
-
271
- # @deprecated Use {#execute} and call the method on the result.
272
- def first
273
- Git::Deprecation.warn(
274
- 'Calling Git::Log#first is deprecated and will be removed in v6.0.0. ' \
275
- 'Call #execute and then #first on the result object.'
276
- )
277
- run_log_if_dirty
278
- @commits.first
279
- end
280
-
281
- # @deprecated Use {#execute} and call the method on the result.
282
- def last
283
- Git::Deprecation.warn(
284
- 'Calling Git::Log#last is deprecated and will be removed in v6.0.0. ' \
285
- 'Call #execute and then #last on the result object.'
286
- )
287
- run_log_if_dirty
288
- @commits.last
289
- end
290
-
291
- # @param index [Integer, Range] the commit index or range to retrieve
292
- #
293
- # @return [Git::Object::Commit, Array<Git::Object::Commit>, nil] the selected
294
- # commit or commits
295
- #
296
- # @deprecated Use {#execute} and call the method on the result.
297
- #
298
- def [](index)
299
- Git::Deprecation.warn(
300
- 'Calling Git::Log#[] is deprecated and will be removed in v6.0.0. ' \
301
- 'Call #execute and then #[] on the result object.'
302
- )
303
- run_log_if_dirty
304
- @commits[index]
305
- end
306
-
307
- # @!endgroup
308
-
309
237
  private
310
238
 
311
239
  # Sets a log query option and marks cached results dirty
data/lib/git/object.rb CHANGED
@@ -223,6 +223,9 @@ module Git
223
223
  #
224
224
  # @raise [Git::FailedError] if `git archive` fails
225
225
  #
226
+ # @note A failure leaves nothing behind and does not replace an existing
227
+ # `file`; see {Git::Repository::ObjectOperations#archive}.
228
+ #
226
229
  # @api public
227
230
  #
228
231
  def archive(file = nil, opts = {})
@@ -481,22 +484,6 @@ module Git
481
484
  diff(parent)
482
485
  end
483
486
 
484
- # Sets parsed commit data on this commit object
485
- #
486
- # @param data [Hash] parsed commit data
487
- #
488
- # @return [void]
489
- #
490
- # @deprecated use {#from_data} instead
491
- #
492
- def set_commit(data) # rubocop:disable Naming/AccessorMethodName
493
- Git::Deprecation.warn(
494
- 'Git::Object::Commit#set_commit is deprecated and will be removed in v6.0.0. ' \
495
- 'Use #from_data instead.'
496
- )
497
- from_data(data)
498
- end
499
-
500
487
  # Loads parsed commit data into this commit object
501
488
  #
502
489
  # @param data [Hash] parsed commit data from `git cat-file commit`
@@ -531,146 +518,6 @@ module Git
531
518
  end
532
519
  end
533
520
 
534
- # A Git tag object
535
- #
536
- # This class represents a tag in Git, which can be either annotated or lightweight.
537
- #
538
- # Annotated tags contain additional metadata such as the tagger's name, email, and
539
- # the date when the tag was created, along with a message.
540
- #
541
- # @deprecated Use {Git::Repository::ObjectOperations#tag_list} and
542
- # {Git::TagInfo} instead
543
- #
544
- # {Git::TagInfo} is an immutable value object carrying the tag's `name`,
545
- # `oid`, `target_oid`, `annotated?`, `message`, and `tagger`. Call the
546
- # corresponding {Git::Repository} method (e.g. `archive`, `log`, `diff`,
547
- # `cat_file_contents`) with `info.oid || info.target_oid` for operations
548
- # on a tag; that is the object this class resolves and pins at
549
- # construction, so a later move of the tag does not redirect an existing
550
- # object, whereas the tag name would. Constructing a `Git::Object::Tag`
551
- # emits a deprecation warning.
552
- #
553
- class Tag < AbstractObject
554
- # @return [String] the tag name
555
- #
556
- attr_accessor :name
557
-
558
- # @overload initialize(base, name)
559
- #
560
- # @param base [Git::Repository] the git repository
561
- #
562
- # @param name [String] the name of the tag
563
- #
564
- # @overload initialize(base, sha, name)
565
- #
566
- # `sha` is kept as the object that the inherited operations (`size`,
567
- # `contents`, `grep`, `diff`, `log`, `archive`) run against; `annotated?`,
568
- # `message`, and `tagger` read the ref `name`. {Git::TagInfo} describes a
569
- # ref, so there is no OID-based replacement for this form: pass `sha` to
570
- # the {Git::Repository} operation directly, or read the tag object with
571
- # {Git::Repository::ObjectOperations#cat_file_tag}.
572
- #
573
- # @param base [Git::Repository] the git repository
574
- #
575
- # @param sha [String] the SHA of the tag object
576
- #
577
- # @param name [String] the name of the tag
578
- #
579
- def initialize(base, sha, name = nil)
580
- Git::Deprecation.warn(
581
- 'Git::Object::Tag is deprecated and will be removed in v6.0.0. ' \
582
- 'Use Git::Repository#tag_list and Git::TagInfo instead.'
583
- )
584
- sha, name = resolve_sha_and_name(base, sha, name)
585
- super(base, sha)
586
-
587
- @name = name
588
- @annotated = nil
589
- @loaded = false
590
- end
591
-
592
- # Returns whether this tag is annotated
593
- #
594
- # @return [Boolean] `true` when the tag has an annotated tag object
595
- #
596
- def annotated?
597
- @annotated = @annotated.nil? ? (object_repository.cat_file_type(name) == 'tag') : @annotated
598
- end
599
-
600
- # Returns the tag message
601
- #
602
- # @return [String, nil] the annotated tag message, or `nil` for a
603
- # lightweight tag
604
- #
605
- def message
606
- check_tag
607
- @message
608
- end
609
-
610
- # Returns whether this object is a tag
611
- #
612
- # @return [Boolean] `true`
613
- #
614
- def tag?
615
- true
616
- end
617
-
618
- # Returns the tagger identity
619
- #
620
- # @return [Git::AuthorInfo, nil] the tagger for an annotated tag, or `nil`
621
- # for a lightweight tag
622
- #
623
- def tagger
624
- check_tag
625
- @tagger
626
- end
627
-
628
- private
629
-
630
- # Resolves the two-argument constructor form to a SHA and a tag name
631
- #
632
- # In the two-argument form `sha` carries the tag name and the SHA is
633
- # looked up from the repository.
634
- #
635
- # @param base [Git::Repository] the git repository
636
- #
637
- # @param sha [String] the SHA of the tag object, or the tag name in the
638
- # two-argument form
639
- #
640
- # @param name [String, nil] the tag name, or `nil` in the two-argument form
641
- #
642
- # @return [Array(String, String)] the resolved `[sha, name]` pair
643
- #
644
- # @raise [Git::UnexpectedResultError] if the tag does not exist
645
- #
646
- def resolve_sha_and_name(base, sha, name)
647
- return [sha, name] unless name.nil?
648
-
649
- resolved = base.tag_sha(sha)
650
- raise Git::UnexpectedResultError, "Tag '#{sha}' does not exist." if resolved == ''
651
-
652
- [resolved, sha]
653
- end
654
-
655
- # Loads annotated tag data when available
656
- #
657
- # @return [void]
658
- #
659
- def check_tag
660
- return if @loaded
661
-
662
- if annotated?
663
- tdata = object_repository.cat_file_tag(@name)
664
- @message = tdata['message'].chomp
665
- @tagger = Git::AuthorInfo.parse(tdata['tagger'])
666
- else
667
- @message = @tagger = nil
668
- end
669
-
670
- @loaded = true
671
- end
672
- end
673
-
674
521
  # if we're calling this, we don't know what type it is yet
675
522
  # so this is our little factory method
676
523
  #
@@ -680,15 +527,10 @@ module Git
680
527
  #
681
528
  # @param type [String, nil] object type hint: `blob`, `commit`, or `tree`
682
529
  #
683
- # @param is_tag [Boolean] whether to construct a tag object
684
- #
685
530
  # @return [Git::Object::AbstractObject] the concrete object wrapper
686
531
  #
687
- def self.new(base, objectish, type = nil, is_tag = false) # rubocop:disable Style/OptionalBooleanParameter
688
- return new_tag(base, objectish) if is_tag
689
-
532
+ def self.new(base, objectish, type = nil)
690
533
  type ||= object_repository_for(base).cat_file_type(objectish)
691
- # TODO: why not handle tag case here too?
692
534
  klass =
693
535
  case type
694
536
  when /blob/ then Blob
@@ -698,29 +540,6 @@ module Git
698
540
  klass.new(base, objectish)
699
541
  end
700
542
 
701
- # Creates a tag object through the deprecated factory path
702
- #
703
- # @param base [Git::Repository] the repository used to query object data
704
- #
705
- # @param objectish [String] the tag name or SHA
706
- #
707
- # @return [Git::Object::Tag] the tag object wrapper
708
- #
709
- # @deprecated Use {Git::Repository::ObjectOperations#tag_list} instead
710
- #
711
- # The warning names `Git::Object::Tag.new`, the replacement this path
712
- # shipped with, and the {Git::Object::Tag} constructor is deprecated as
713
- # well; this method silences it so one call emits one warning. Go
714
- # straight to `Git::Repository#tag_list(name).first`.
715
- #
716
- private_class_method def self.new_tag(base, objectish)
717
- Git::Deprecation.warn(
718
- 'Git::Object.new with is_tag argument is deprecated and will be removed in v6.0.0. ' \
719
- 'Use Git::Object::Tag.new instead.'
720
- )
721
- Git::Deprecation.silence { Git::Object::Tag.new(base, objectish) }
722
- end
723
-
724
543
  # Returns the repository used for object lookup
725
544
  #
726
545
  # @param base [Git::Repository] the repository to return
@@ -4,20 +4,37 @@ require 'git/commands/rev_parse'
4
4
  require 'git/errors'
5
5
  require 'git/execution_context'
6
6
  require 'git/execution_context/global'
7
+ require 'git/system_call_guard'
7
8
 
8
9
  module Git
9
10
  # Resolves and normalizes the filesystem paths that locate a Git repository
10
11
  #
11
12
  # `PathResolver` is the single home for the path-resolution logic used by the
12
- # `Git::Repository` factory class methods ({Git::Repository.open} and
13
- # {Git::Repository.bare}). It computes the absolute working-directory,
14
- # repository (`.git`), and index paths from the caller-supplied values,
15
- # following the same rules Git itself uses (including gitdir-pointer files for
16
- # submodules and linked worktrees).
13
+ # `Git` factory methods ({Git.open}, {Git.bare}, {Git.init}, and {Git.clone}).
14
+ # It computes the absolute working-directory, repository (`.git`), and index
15
+ # paths from the caller-supplied values, following the same rules Git itself
16
+ # uses (including gitdir-pointer files for submodules and linked worktrees).
17
+ #
18
+ # A relative path given by the caller is expanded against the process working
19
+ # directory, as git does for a path on its command line. Only the defaults are
20
+ # derived from the layout: an omitted repository is `<working_directory>/.git`
21
+ # and an omitted index is `<repository>/index`.
17
22
  #
18
23
  # @api private
19
24
  #
20
25
  module PathResolver
26
+ # The {Git::Error} message when a working directory path cannot be expanded
27
+ WORKING_DIRECTORY_MESSAGE = 'Failed to resolve the working directory'
28
+
29
+ # The {Git::Error} message when a repository directory path cannot be expanded
30
+ REPOSITORY_MESSAGE = 'Failed to resolve the repository directory'
31
+
32
+ # The {Git::Error} message when an index file path cannot be expanded
33
+ INDEX_MESSAGE = 'Failed to resolve the index file'
34
+
35
+ # The {Git::Error} message when the `:chdir` option of `Git.clone` cannot be expanded
36
+ CHDIR_MESSAGE = 'Failed to resolve the chdir directory'
37
+
21
38
  module_function
22
39
 
23
40
  # Resolve and normalize the paths that locate a Git repository
@@ -44,10 +61,12 @@ module Git
44
61
  # @return [Hash{Symbol => (String, nil)}] a hash with `:working_directory`,
45
62
  # `:repository`, and `:index` keys
46
63
  #
64
+ # @raise [Git::Error] if the repository path is a gitdir pointer file that
65
+ # cannot be read
66
+ #
47
67
  def resolve_paths(working_directory: nil, repository: nil, index: nil, bare: false)
48
68
  working_dir = resolve_working_directory(working_directory, bare: bare)
49
- # For bare repos, use working_directory as the default repository location
50
- repo_path = resolve_repository(repository, working_dir, bare: bare, bare_default: working_directory)
69
+ repo_path = resolve_repository(repository, working_dir)
51
70
  index_path = resolve_index(index, repo_path)
52
71
 
53
72
  {
@@ -81,17 +100,20 @@ module Git
81
100
  # @raise [ArgumentError] if `working_dir` does not exist, is not a
82
101
  # directory, or is not inside a git working tree
83
102
  #
84
- # Also raised if the git binary cannot be found.
103
+ # @raise [Git::Error] if the git binary cannot be found or fails to launch, or
104
+ # if `working_dir` cannot be expanded to an absolute path
85
105
  #
86
106
  def root_of_worktree(working_dir, binary_path: :use_global_config, git_ssh: :use_global_config)
87
- raise ArgumentError, "'#{working_dir}' does not exist or is not a directory" unless Dir.exist?(working_dir)
107
+ expanded_dir = expand_path(working_dir, 'Failed to expand the working directory path')
108
+ raise ArgumentError, "'#{working_dir}' does not exist or is not a directory" unless Dir.exist?(expanded_dir)
88
109
 
89
- execute_rev_parse_toplevel(working_dir, binary_path: binary_path, git_ssh: git_ssh)
110
+ execute_rev_parse_toplevel(expanded_dir, binary_path: binary_path, git_ssh: git_ssh)
90
111
  end
91
112
 
92
113
  # Run `git rev-parse --show-toplevel` from `working_dir` and return stdout
93
114
  #
94
- # @param working_dir [String] a path inside the working tree
115
+ # @param working_dir [String] the absolute path of a directory inside the
116
+ # working tree
95
117
  #
96
118
  # @param binary_path [String, :use_global_config] path to the git binary
97
119
  #
@@ -99,18 +121,16 @@ module Git
99
121
  #
100
122
  # @return [String] the top-level directory reported by git
101
123
  #
102
- # @raise [ArgumentError] if the git binary is not found or `working_dir` is
103
- # not inside a git working tree
124
+ # @raise [ArgumentError] if `working_dir` is not inside a git working tree
125
+ #
126
+ # @raise [Git::Error] if the git binary cannot be found or fails to launch
104
127
  #
105
128
  # @api private
106
129
  #
107
130
  def execute_rev_parse_toplevel(working_dir, binary_path: :use_global_config, git_ssh: :use_global_config)
108
131
  execution_context = Git::ExecutionContext::Global.new(binary_path: binary_path, git_ssh: git_ssh)
109
- Git::Commands::RevParse.new(execution_context).call(
110
- show_toplevel: true, chdir: File.expand_path(working_dir)
111
- ).stdout
112
- rescue Errno::ENOENT
113
- raise ArgumentError, 'Failed to find the root of the worktree: git binary not found'
132
+
133
+ Git::Commands::RevParse.new(execution_context).call(show_toplevel: true, chdir: working_dir).stdout
114
134
  rescue Git::FailedError
115
135
  raise ArgumentError, "'#{working_dir}' is not in a git working tree"
116
136
  end
@@ -124,12 +144,15 @@ module Git
124
144
  #
125
145
  # @return [String, nil] the absolute path, or `nil` for bare repos
126
146
  #
147
+ # @raise [Git::Error] if the path cannot be expanded, which happens when the
148
+ # process working directory has been removed
149
+ #
127
150
  # @api private
128
151
  #
129
152
  def resolve_working_directory(path, bare:)
130
153
  return nil if bare
131
154
 
132
- File.expand_path(path || Dir.pwd)
155
+ expand_path(path, WORKING_DIRECTORY_MESSAGE)
133
156
  end
134
157
  private_class_method :resolve_working_directory
135
158
 
@@ -139,24 +162,24 @@ module Git
139
162
  #
140
163
  # @param path [String, nil] the repository path or `nil`
141
164
  #
142
- # @param working_dir [String, nil] the working directory used for relative
143
- # path resolution
144
- #
145
- # @param bare [Boolean] whether this is a bare repository
146
- #
147
- # @param bare_default [String, nil] for bare repos, used as the default when
148
- # `path` is `nil`
165
+ # @param working_dir [String, nil] the working directory whose `.git` is the
166
+ # default when `path` is `nil`, or `nil` for a bare repository, whose
167
+ # default is the process working directory
149
168
  #
150
169
  # @return [String] the absolute path to the repository
151
170
  #
171
+ # @raise [Git::Error] if the path cannot be expanded, which happens when the
172
+ # process working directory has been removed
173
+ #
152
174
  # @api private
153
175
  #
154
- def resolve_repository(path, working_dir, bare:, bare_default: nil)
155
- initial_path = if bare
156
- File.expand_path(path || bare_default || Dir.pwd)
157
- else
158
- File.expand_path(path || '.git', working_dir)
159
- end
176
+ def resolve_repository(path, working_dir)
177
+ initial_path =
178
+ if working_dir && path.nil?
179
+ File.join(working_dir, '.git')
180
+ else
181
+ expand_path(path, REPOSITORY_MESSAGE)
182
+ end
160
183
 
161
184
  resolve_gitdir_pointer(initial_path)
162
185
  end
@@ -171,6 +194,8 @@ module Git
171
194
  #
172
195
  # @return [String] the resolved absolute path
173
196
  #
197
+ # @raise [Git::Error] if `path` is a file that cannot be read
198
+ #
174
199
  # Relative pointer targets are resolved from the directory containing the
175
200
  # pointer file itself, matching git's pointer-file semantics.
176
201
  #
@@ -179,7 +204,7 @@ module Git
179
204
  def resolve_gitdir_pointer(path)
180
205
  return path unless File.file?(path)
181
206
 
182
- gitdir_content = File.read(path).strip
207
+ gitdir_content = Git::SystemCallGuard.call('Failed to read the gitdir pointer file') { File.read(path) }.strip
183
208
  return path unless gitdir_content.start_with?('gitdir: ')
184
209
 
185
210
  gitdir_path = gitdir_content.sub(/\Agitdir: /, '')
@@ -191,16 +216,59 @@ module Git
191
216
  #
192
217
  # @param path [String, nil] the index path or `nil`
193
218
  #
194
- # @param repository [String] the repository directory used for relative
195
- # path resolution
219
+ # @param repository [String] the repository directory whose `index` is the
220
+ # default when `path` is `nil`
196
221
  #
197
222
  # @return [String] the absolute path to the index file
198
223
  #
224
+ # @raise [Git::Error] if the path cannot be expanded, which happens when the
225
+ # process working directory has been removed
226
+ #
199
227
  # @api private
200
228
  #
201
229
  def resolve_index(path, repository)
202
- File.expand_path(path || 'index', repository)
230
+ return File.join(repository, 'index') unless path
231
+
232
+ expand_path(path, INDEX_MESSAGE)
203
233
  end
204
234
  private_class_method :resolve_index
235
+
236
+ # Expand a path the way a shell prompt would before git runs
237
+ #
238
+ # Follows `File.expand_path`: `~` is the home directory, a relative path is
239
+ # joined onto `base` (or onto the process working directory when `base` is
240
+ # `nil`), `.` and `..` are normalized, and an absolute path is unchanged.
241
+ #
242
+ # @example Expand a relative path against the process working directory
243
+ # Git::PathResolver.expand_path('repo', 'Failed to resolve the working directory')
244
+ # #=> '/current/dir/repo'
245
+ #
246
+ # @example Expand a relative path against another directory
247
+ # Git::PathResolver.expand_path('repo', 'Failed to resolve the working directory', base: '/output')
248
+ # #=> '/output/repo'
249
+ #
250
+ # @param path [String, Pathname, nil] the path to expand, or `nil` for
251
+ # `base` itself
252
+ #
253
+ # @param message [String] the message of the {Git::Error} raised when the
254
+ # expansion fails
255
+ #
256
+ # @param base [String, Pathname, nil] the directory a relative `path` is
257
+ # expanded against, or `nil` for the process working directory
258
+ #
259
+ # @return [String] the absolute path
260
+ #
261
+ # @raise [ArgumentError] if `~` cannot be expanded because no home directory
262
+ # can be determined, or if the path begins with `~user` for a user that
263
+ # does not exist
264
+ #
265
+ # @raise [Git::Error] if the path cannot be expanded, which happens when the
266
+ # process working directory has been removed
267
+ #
268
+ # @api private
269
+ #
270
+ def expand_path(path, message, base: nil)
271
+ Git::SystemCallGuard.call(message) { File.expand_path(path || '.', base) }
272
+ end
205
273
  end
206
274
  end