git 5.0.0.beta.1 → 5.0.0.beta.2

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +6 -0
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  5. data/.github/skills/extract-facade-from-base-lib/SKILL.md +28 -14
  6. data/.github/skills/facade-implementation/SKILL.md +14 -0
  7. data/.github/skills/facade-test-conventions/SKILL.md +14 -0
  8. data/.rubocop.yml +5 -0
  9. data/README.md +51 -11
  10. data/UPGRADING.md +141 -0
  11. data/git.gemspec +5 -0
  12. data/lib/git/branch.rb +7 -18
  13. data/lib/git/branches.rb +2 -10
  14. data/lib/git/command_line/base.rb +10 -0
  15. data/lib/git/command_line/capturing.rb +5 -3
  16. data/lib/git/command_line/streaming.rb +5 -3
  17. data/lib/git/command_line.rb +3 -3
  18. data/lib/git/commands/base.rb +7 -6
  19. data/lib/git/commands/cat_file/batch.rb +6 -1
  20. data/lib/git/commands/cat_file/raw.rb +7 -1
  21. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +5 -0
  22. data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
  23. data/lib/git/commands/update_ref/batch.rb +1 -1
  24. data/lib/git/commands/version.rb +5 -0
  25. data/lib/git/commands.rb +5 -7
  26. data/lib/git/config.rb +17 -0
  27. data/lib/git/config_entry_info.rb +106 -0
  28. data/lib/git/configuring.rb +665 -0
  29. data/lib/git/deprecation.rb +9 -0
  30. data/lib/git/diff.rb +4 -8
  31. data/lib/git/diff_path_status.rb +2 -13
  32. data/lib/git/diff_stats.rb +1 -9
  33. data/lib/git/execution_context/global.rb +3 -28
  34. data/lib/git/execution_context/repository.rb +30 -41
  35. data/lib/git/execution_context.rb +43 -24
  36. data/lib/git/log.rb +3 -9
  37. data/lib/git/object.rb +14 -21
  38. data/lib/git/parsers/config_entry.rb +110 -0
  39. data/lib/git/parsers/ls_remote.rb +79 -0
  40. data/lib/git/remote.rb +7 -20
  41. data/lib/git/repository/branching.rb +183 -12
  42. data/lib/git/repository/committing.rb +64 -68
  43. data/lib/git/repository/configuring.rb +208 -13
  44. data/lib/git/repository/context_helpers.rb +264 -0
  45. data/lib/git/repository/factories.rb +682 -0
  46. data/lib/git/repository/inspecting.rb +99 -0
  47. data/lib/git/repository/maintenance.rb +65 -0
  48. data/lib/git/repository/merging.rb +63 -1
  49. data/lib/git/repository/object_operations.rb +133 -35
  50. data/lib/git/repository/path_resolver.rb +1 -1
  51. data/lib/git/repository/remote_operations.rb +166 -21
  52. data/lib/git/repository/staging.rb +187 -23
  53. data/lib/git/repository/stashing.rb +39 -3
  54. data/lib/git/repository/status_operations.rb +21 -0
  55. data/lib/git/repository.rb +68 -129
  56. data/lib/git/stash.rb +2 -9
  57. data/lib/git/stashes.rb +2 -7
  58. data/lib/git/status.rb +8 -17
  59. data/lib/git/version.rb +2 -2
  60. data/lib/git/worktree.rb +2 -15
  61. data/lib/git/worktrees.rb +2 -15
  62. data/lib/git.rb +180 -77
  63. data/redesign/3_architecture_implementation.md +148 -111
  64. data/redesign/Phase 4 - Step A.md +360 -0
  65. data/redesign/beta_release.md +107 -0
  66. data/redesign/c1c2_audit.md +566 -0
  67. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  68. data/redesign/config_design.rb +501 -0
  69. metadata +19 -5
  70. data/lib/git/base.rb +0 -1204
  71. data/lib/git/lib.rb +0 -2855
@@ -2,9 +2,11 @@
2
2
 
3
3
  require 'git/commands/config_option_syntax'
4
4
  require 'git/commands/fetch'
5
+ require 'git/commands/ls_remote'
5
6
  require 'git/commands/pull'
6
7
  require 'git/commands/push'
7
8
  require 'git/commands/remote'
9
+ require 'git/parsers/ls_remote'
8
10
  require 'git/remote'
9
11
 
10
12
  require 'git/repository/shared_private'
@@ -17,7 +19,7 @@ module Git
17
19
  #
18
20
  # @api public
19
21
  #
20
- module RemoteOperations
22
+ module RemoteOperations # rubocop:disable Metrics/ModuleLength
21
23
  # Key normalizations for {#fetch} options
22
24
  #
23
25
  # Maps dash-style option keys (which the 4.x `Git::Lib#fetch` accepted)
@@ -370,11 +372,11 @@ module Git
370
372
  Private.push_tags(@execution_context, remote, opts).stdout
371
373
  end
372
374
 
373
- # Option keys accepted by {#add_remote}
375
+ # Option keys accepted by {#remote_add}
374
376
  #
375
377
  # Derived from the 4.x `REMOTE_ADD_OPTION_MAP` in `Git::Lib`.
376
- ADD_REMOTE_ALLOWED_OPTS = %i[fetch track].freeze
377
- private_constant :ADD_REMOTE_ALLOWED_OPTS
378
+ REMOTE_ADD_ALLOWED_OPTS = %i[fetch track].freeze
379
+ private_constant :REMOTE_ADD_ALLOWED_OPTS
378
380
 
379
381
  # Register a new remote in the local repository
380
382
  #
@@ -382,19 +384,19 @@ module Git
382
384
  # configures which branches are tracked.
383
385
  #
384
386
  # @example Add a remote
385
- # repo.add_remote('upstream', 'https://github.com/user/repo.git')
387
+ # repo.remote_add('upstream', 'https://github.com/user/repo.git')
386
388
  #
387
389
  # @example Add a remote and fetch immediately
388
- # repo.add_remote('upstream', 'https://github.com/user/repo.git', fetch: true)
390
+ # repo.remote_add('upstream', 'https://github.com/user/repo.git', fetch: true)
389
391
  #
390
392
  # @example Add a remote tracking a specific branch
391
- # repo.add_remote('upstream', 'https://github.com/user/repo.git', track: 'main')
393
+ # repo.remote_add('upstream', 'https://github.com/user/repo.git', track: 'main')
392
394
  #
393
395
  # @param name [String] the name for the new remote
394
396
  #
395
- # @param url [String, Git::Base] the URL of the remote repository
397
+ # @param url [String, Git::Repository] the URL of the remote repository
396
398
  #
397
- # A {Git::Base} instance is accepted for local references and converted
399
+ # A {Git::Repository} instance is accepted for local references and converted
398
400
  # to `url.repo.to_s`.
399
401
  #
400
402
  # @param opts [Hash] options for adding the remote
@@ -414,22 +416,56 @@ module Git
414
416
  #
415
417
  # @raise [Git::FailedError] when git exits with a non-zero status
416
418
  #
417
- def add_remote(name, url, opts = {})
418
- url = url.repo.to_s if url.is_a?(Git::Base)
419
+ def remote_add(name, url, opts = {})
420
+ url = url.repo.to_s if url.is_a?(Git::Repository)
419
421
  opts = Private.normalize_add_remote_keys(opts)
420
- SharedPrivate.assert_valid_opts!(ADD_REMOTE_ALLOWED_OPTS, **opts)
422
+ SharedPrivate.assert_valid_opts!(REMOTE_ADD_ALLOWED_OPTS, **opts)
421
423
  Git::Commands::Remote::Add.new(@execution_context).call(name, url, **opts)
422
424
 
423
425
  Git::Remote.new(self, name)
424
426
  end
425
427
 
428
+ # @deprecated Use {#remote_add} instead.
429
+ #
430
+ # @param name [String] the name for the new remote
431
+ #
432
+ # @param url [String, Git::Repository] the URL of the remote repository
433
+ #
434
+ # A {Git::Repository} instance is accepted for local references and converted
435
+ # to `url.repo.to_s`.
436
+ #
437
+ # @param opts [Hash] options for adding the remote
438
+ #
439
+ # @option opts [Boolean, nil] :fetch (nil) fetch from the remote immediately
440
+ # after adding it (`-f`)
441
+ #
442
+ # The deprecated alias `:with_fetch` is accepted and normalized
443
+ # automatically.
444
+ #
445
+ # @option opts [String, nil] :track (nil) track only the given branch during
446
+ # fetch (`-t`)
447
+ #
448
+ # @return [Git::Remote] the newly added remote
449
+ #
450
+ # @raise [ArgumentError] when unsupported option keys are provided
451
+ #
452
+ # @raise [Git::FailedError] when git exits with a non-zero status
453
+ #
454
+ def add_remote(name, url, opts = {})
455
+ Git::Deprecation.warn(
456
+ 'Git::Repository#add_remote is deprecated and will be removed in v6.0.0. ' \
457
+ 'Use Git::Repository#remote_add instead.'
458
+ )
459
+ remote_add(name, url, opts)
460
+ end
461
+
426
462
  # Removes a remote from this repository
427
463
  #
428
464
  # Deletes the remote named `name` along with its associated configuration,
429
465
  # tracking references, and remote-tracking branches.
430
466
  #
431
467
  # @example Remove a remote named 'upstream'
432
- # repo.remove_remote('upstream')
468
+ # repo.remote_remove('upstream')
433
469
  #
434
470
  # @param name [String] the name of the remote to remove
435
471
  #
@@ -437,39 +473,76 @@ module Git
437
473
  #
438
474
  # @raise [Git::FailedError] when git exits with a non-zero status
439
475
  #
440
- def remove_remote(name)
476
+ def remote_remove(name)
441
477
  Git::Commands::Remote::Remove.new(@execution_context).call(name)
442
478
  end
443
479
 
480
+ # @deprecated Use {#remote_remove} instead.
481
+ #
482
+ # @param name [String] the name of the remote to remove
483
+ #
484
+ # @return [Git::CommandLineResult] the result of calling `git remote remove`
485
+ #
486
+ # @raise [Git::FailedError] when git exits with a non-zero status
487
+ #
488
+ def remove_remote(name)
489
+ Git::Deprecation.warn(
490
+ 'Git::Repository#remove_remote is deprecated and will be removed in v6.0.0. ' \
491
+ 'Use Git::Repository#remote_remove instead.'
492
+ )
493
+ remote_remove(name)
494
+ end
495
+
444
496
  # Sets the URL for an existing remote
445
497
  #
446
498
  # Replaces the fetch URL configured for the remote named `name`.
447
499
  #
448
500
  # @example Set the URL for a remote
449
- # repo.set_remote_url('origin', 'https://github.com/user/repo.git')
501
+ # repo.remote_set_url('origin', 'https://github.com/user/repo.git')
450
502
  #
451
503
  # @example Set the URL from a local repository reference
452
504
  # source = Git.open('/path/to/source')
453
- # repo.set_remote_url('origin', source)
505
+ # repo.remote_set_url('origin', source)
454
506
  #
455
507
  # @param name [String] the name of the remote to update
456
508
  #
457
- # @param url [String, Git::Base] the new URL for the remote
509
+ # @param url [String, Git::Repository] the new URL for the remote
458
510
  #
459
- # A {Git::Base} instance is accepted for local references and converted
511
+ # A {Git::Repository} instance is accepted for local references and converted
460
512
  # to `url.repo.to_s`.
461
513
  #
462
514
  # @return [Git::Remote] the updated remote
463
515
  #
464
516
  # @raise [Git::FailedError] when git exits with a non-zero status
465
517
  #
466
- def set_remote_url(name, url)
467
- url = url.repo.to_s if url.is_a?(Git::Base)
518
+ def remote_set_url(name, url)
519
+ url = url.repo.to_s if url.is_a?(Git::Repository)
468
520
  Git::Commands::Remote::SetUrl.new(@execution_context).call(name, url)
469
521
 
470
522
  Git::Remote.new(self, name)
471
523
  end
472
524
 
525
+ # @deprecated Use {#remote_set_url} instead.
526
+ #
527
+ # @param name [String] the name of the remote to update
528
+ #
529
+ # @param url [String, Git::Repository] the new URL for the remote
530
+ #
531
+ # A {Git::Repository} instance is accepted for local references and converted
532
+ # to `url.repo.to_s`.
533
+ #
534
+ # @return [Git::Remote] the updated remote
535
+ #
536
+ # @raise [Git::FailedError] when git exits with a non-zero status
537
+ #
538
+ def set_remote_url(name, url)
539
+ Git::Deprecation.warn(
540
+ 'Git::Repository#set_remote_url is deprecated and will be removed in v6.0.0. ' \
541
+ 'Use Git::Repository#remote_set_url instead.'
542
+ )
543
+ remote_set_url(name, url)
544
+ end
545
+
473
546
  # Configures which branches are fetched for a remote
474
547
  #
475
548
  # Uses `git remote set-branches` to set or append fetch refspecs. When the
@@ -570,6 +643,78 @@ module Git
570
643
  result.stdout.split("\n").map { |name| Git::Remote.new(self, name) }
571
644
  end
572
645
 
646
+ # Option keys accepted by {#ls_remote}
647
+ #
648
+ # @return [Array<Symbol>]
649
+ #
650
+ # @api private
651
+ #
652
+ LS_REMOTE_ALLOWED_OPTS = %i[
653
+ branches b heads h tags t refs upload_pack quiet q exit_code sort server_option o timeout
654
+ ].freeze
655
+ private_constant :LS_REMOTE_ALLOWED_OPTS
656
+
657
+ # List references available in a remote repository
658
+ #
659
+ # Queries a remote for its available refs and returns a structured Hash
660
+ # mapping ref types to name/sha pairs. The remote is contacted but no local
661
+ # objects are created or updated.
662
+ #
663
+ # @example List all refs from the local repository
664
+ # repo.ls_remote
665
+ # # => {"head"=>{ref: "HEAD", sha: "abc123"},
666
+ # # "branches"=>{"main"=>{ref: "refs", sha: "abc123"}}}
667
+ #
668
+ # @note The `:ref` value in each pair is only the **first path segment** of the
669
+ # full git ref (e.g. `"refs"` for `refs/heads/main`), not the complete ref
670
+ # path. This matches the behavior of 4.x. See
671
+ # [issue 1416](https://github.com/ruby-git/ruby-git/issues/1416) for the
672
+ # planned fix.
673
+ #
674
+ # @example List all refs from a named remote
675
+ # repo.ls_remote('origin')
676
+ # # => {"head"=>..., "branches"=>..., "tags"=>...}
677
+ #
678
+ # @example List only tags from a named remote
679
+ # repo.ls_remote('origin', tags: true)
680
+ # # => {"tags"=>{"v1.0"=>{ref: "refs", sha: "def456"}}}
681
+ #
682
+ # @param location [String, nil] the remote name or URL to query; defaults to
683
+ # `'.'` (the local repository) when nil
684
+ #
685
+ # @param opts [Hash] options for the ls-remote command
686
+ #
687
+ # @option opts [Boolean, nil] :branches (nil) limit output to refs under
688
+ # `refs/heads/`; alias: `:b`
689
+ #
690
+ # @option opts [Boolean, nil] :heads (nil) limit output to refs under
691
+ # `refs/heads/`; kept for backward compatibility; alias: `:h`
692
+ #
693
+ # @option opts [Boolean, nil] :tags (nil) limit output to refs under
694
+ # `refs/tags/`; alias: `:t`
695
+ #
696
+ # @option opts [Boolean, nil] :refs (nil) exclude peeled tags and pseudorefs
697
+ # like `HEAD` from the output
698
+ #
699
+ # @option opts [Numeric] :timeout (nil) execution timeout in seconds
700
+ #
701
+ # @return [Hash{String => Hash}] a Hash keyed by ref type (e.g. `"head"`,
702
+ # `"branches"`, `"tags"`; other git namespace segments may appear for
703
+ # non-standard refs); for named refs the value is a Hash keyed by ref name
704
+ # mapping to `{ ref: String, sha: String }`; for the `"head"` entry the value
705
+ # is `{ ref: String, sha: String }` directly
706
+ #
707
+ # @raise [ArgumentError] if unsupported options are provided
708
+ #
709
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 2)
710
+ #
711
+ def ls_remote(location = nil, opts = {})
712
+ SharedPrivate.assert_valid_opts!(LS_REMOTE_ALLOWED_OPTS, **opts)
713
+ repository = location || '.'
714
+ output_lines = Git::Commands::LsRemote.new(@execution_context).call(repository, **opts).stdout.split("\n")
715
+ Git::Parsers::LsRemote.parse_output(output_lines)
716
+ end
717
+
573
718
  # Helpers private to the `RemoteOperations` topic module
574
719
  #
575
720
  # @api private
@@ -707,7 +852,7 @@ module Git
707
852
  Git::Commands::Push.new(execution_context).call(*[remote].compact, **opts)
708
853
  end
709
854
 
710
- # Normalize deprecated {#add_remote} option keys to their canonical equivalents
855
+ # Normalize deprecated option keys for {#remote_add} to their canonical equivalents
711
856
  #
712
857
  # Renames the deprecated `:with_fetch` key to `:fetch`, removing it from
713
858
  # the copy. When both keys are present, `:with_fetch` takes precedence.
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'git/commands/add'
4
+ require 'git/commands/am/apply'
5
+ require 'git/commands/apply'
4
6
  require 'git/commands/clean'
5
7
  require 'git/commands/ls_files'
8
+ require 'git/commands/mv'
9
+ require 'git/commands/read_tree'
6
10
  require 'git/commands/reset'
7
11
  require 'git/commands/rm'
8
12
  require 'git/escaped_path'
@@ -10,8 +14,8 @@ require 'git/repository/shared_private'
10
14
 
11
15
  module Git
12
16
  class Repository
13
- # Facade methods for staging-area operations: adding, resetting, removing, and
14
- # cleaning files
17
+ # Facade methods for staging-area operations: adding, resetting, moving,
18
+ # removing, and cleaning files
15
19
  #
16
20
  # Included by {Git::Repository}.
17
21
  #
@@ -63,31 +67,140 @@ module Git
63
67
 
64
68
  # Reset the current HEAD to a specified state
65
69
  #
66
- # @overload reset(commitish = nil, **options)
70
+ # @example Reset the index and working tree to HEAD
71
+ # repo.reset
67
72
  #
68
- # @example Reset the index and working tree to HEAD
69
- # repo.reset
73
+ # @example Hard reset to a specific commit
74
+ # repo.reset('HEAD~1', hard: true)
75
+ #
76
+ # @param commitish [String, nil] the commit or tree-ish to reset to;
77
+ # defaults to HEAD when `nil`
78
+ #
79
+ # @param opts [Hash] options for the reset command
80
+ #
81
+ # @option opts [Boolean, nil] :hard (nil) reset the index and working
82
+ # tree; discards all tracked changes
83
+ #
84
+ # @return [String] git's stdout from the reset
85
+ #
86
+ # @raise [ArgumentError] when unsupported options are provided
87
+ #
88
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
89
+ #
90
+ def reset(commitish = nil, opts = {})
91
+ SharedPrivate.assert_valid_opts!(RESET_ALLOWED_OPTS, **opts)
92
+ Git::Commands::Reset.new(@execution_context).call(commitish, **opts).stdout
93
+ end
94
+
95
+ # Reset the current HEAD to a specified state with `--hard`
96
+ #
97
+ # @deprecated Use {#reset} with `hard: true` instead.
98
+ #
99
+ # @example Hard reset to HEAD
100
+ # repo.reset_hard
70
101
  #
71
102
  # @example Hard reset to a specific commit
72
- # repo.reset('HEAD~1', hard: true)
103
+ # repo.reset_hard('HEAD~1')
73
104
  #
74
- # @param commitish [String, nil] the commit or tree-ish to reset to;
75
- # defaults to HEAD when `nil`
105
+ # @param commitish [String, nil] the commit or tree-ish to reset to;
106
+ # defaults to HEAD when `nil`
76
107
  #
77
- # @param options [Hash] options for the reset command
108
+ # @param opts [Hash] options passed through to {#reset}
78
109
  #
79
- # @option options [Boolean, nil] :hard (nil) reset the index and working
80
- # tree; discards all tracked changes
110
+ # @return [String] git's stdout from the reset
81
111
  #
82
- # @return [String] git's stdout from the reset
112
+ # @raise [ArgumentError] when unsupported options are provided
83
113
  #
84
- # @raise [ArgumentError] when unsupported options are provided
114
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
85
115
  #
86
- # @raise [Git::FailedError] when git exits with a non-zero exit status
116
+ def reset_hard(commitish = nil, opts = {})
117
+ Git::Deprecation.warn(
118
+ 'Git::Repository::Staging#reset_hard is deprecated and will be removed in a future version. ' \
119
+ 'Use #reset(commitish, hard: true) instead.'
120
+ )
121
+ reset(commitish, **opts, hard: true)
122
+ end
123
+
124
+ # Apply a patch file to the working tree
87
125
  #
88
- def reset(commitish = nil, **)
89
- SharedPrivate.assert_valid_opts!(RESET_ALLOWED_OPTS, **)
90
- Git::Commands::Reset.new(@execution_context).call(commitish, **).stdout
126
+ # Reads the unified diff in `file` and applies it to the working tree via
127
+ # `git apply`. If `file` does not exist, the method returns `nil` without
128
+ # calling git — preserving the 4.x `Git::Base#apply` no-op contract.
129
+ #
130
+ # @example Apply a patch to the working tree
131
+ # repo.apply('fix.patch')
132
+ #
133
+ # @param file [String] path to the patch file to apply
134
+ #
135
+ # @return [String] git's stdout (usually empty on success)
136
+ #
137
+ # @return [nil] when `file` does not exist
138
+ #
139
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
140
+ #
141
+ def apply(file)
142
+ return unless File.exist?(file)
143
+
144
+ Git::Commands::Apply.new(@execution_context).call(file, chdir: @execution_context.git_work_dir).stdout
145
+ end
146
+
147
+ # Apply a series of patches from a mailbox file to the current branch
148
+ #
149
+ # Reads the mbox-format file in `file` and applies the patches via
150
+ # `git am`. If `file` does not exist, the method returns `nil` without
151
+ # calling git — preserving the 4.x `Git::Base#apply_mail` no-op contract.
152
+ #
153
+ # @example Apply patches from a mailbox
154
+ # repo.apply_mail('patches.mbox')
155
+ #
156
+ # @param file [String] path to the mbox patch file to apply
157
+ #
158
+ # @return [String] git's stdout (usually empty on success)
159
+ #
160
+ # @return [nil] when `file` does not exist
161
+ #
162
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
163
+ #
164
+ def apply_mail(file)
165
+ return unless File.exist?(file)
166
+
167
+ Git::Commands::Am::Apply.new(@execution_context).call(file, chdir: @execution_context.git_work_dir).stdout
168
+ end
169
+
170
+ # Option keys accepted by {#read_tree}
171
+ READ_TREE_ALLOWED_OPTS = %i[prefix].freeze
172
+ private_constant :READ_TREE_ALLOWED_OPTS
173
+
174
+ # Read tree information into the index
175
+ #
176
+ # Reads the named tree object into the index. This is a low-level plumbing
177
+ # operation used to stage the contents of a tree without updating the
178
+ # working tree. Typically called before {#checkout_index} or as part of
179
+ # custom merge flows.
180
+ #
181
+ # @example Read HEAD into the index
182
+ # repo.read_tree('HEAD')
183
+ #
184
+ # @example Read a tree under a prefix directory
185
+ # repo.read_tree('HEAD', { prefix: 'subdir/' })
186
+ #
187
+ # @param treeish [String] the tree-ish to read into the index
188
+ #
189
+ # @param opts [Hash] options for the read-tree command
190
+ #
191
+ # @option opts [String] :prefix (nil) keep the current index contents and
192
+ # read the named tree-ish under the directory at the given prefix
193
+ # (`--prefix=<prefix>`)
194
+ #
195
+ # @return [String] git's stdout (usually empty on success)
196
+ #
197
+ # @raise [ArgumentError] when unsupported options are provided
198
+ #
199
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
200
+ #
201
+ def read_tree(treeish, opts = {})
202
+ SharedPrivate.assert_valid_opts!(READ_TREE_ALLOWED_OPTS, **opts)
203
+ Git::Commands::ReadTree.new(@execution_context).call(treeish, **opts).stdout
91
204
  end
92
205
 
93
206
  # Option keys accepted by {#rm}
@@ -100,13 +213,13 @@ module Git
100
213
  # Remove file(s) from the working tree and the index
101
214
  #
102
215
  # @example Remove a single file
103
- # repo.rm('obsolete.txt', force: true)
216
+ # repo.rm('obsolete.txt', { force: true })
104
217
  #
105
218
  # @example Remove a directory recursively
106
- # repo.rm('build', r: true)
219
+ # repo.rm('build', { r: true })
107
220
  #
108
221
  # @example Remove from the index only, keeping the working tree copy
109
- # repo.rm('keep_me.txt', cached: true)
222
+ # repo.rm('keep_me.txt', { cached: true })
110
223
  #
111
224
  # @param path [String, Array<String>] a file or files to remove (relative to
112
225
  # the worktree root); defaults to `'.'` (all files)
@@ -157,6 +270,57 @@ module Git
157
270
  Git::Commands::Rm.new(@execution_context).call(*Array(path), **opts).stdout
158
271
  end
159
272
 
273
+ alias remove rm
274
+
275
+ # Option keys accepted by {#mv}
276
+ MV_ALLOWED_OPTS = %i[force f dry_run n k].freeze
277
+ private_constant :MV_ALLOWED_OPTS
278
+
279
+ # Move or rename a file, directory, or symlink in the working tree
280
+ #
281
+ # Updates the index after successful completion, but the change must still
282
+ # be committed.
283
+ #
284
+ # @example Move a single file
285
+ # repo.mv('old.rb', 'new.rb')
286
+ #
287
+ # @example Move multiple files to a directory
288
+ # repo.mv(['file1.rb', 'file2.rb'], 'destination_dir/')
289
+ #
290
+ # @example Force overwrite if destination exists
291
+ # repo.mv('source.rb', 'dest.rb', force: true)
292
+ #
293
+ # @param source [String, Array<String>] one or more source file(s),
294
+ # directory(ies), or symlink(s) to move (relative to the worktree root)
295
+ #
296
+ # @param destination [String] the destination file or directory
297
+ #
298
+ # @param options [Hash] options for the mv command
299
+ #
300
+ # @option options [Boolean, nil] :force (nil) force renaming or moving even
301
+ # if the destination exists (alias: `:f`)
302
+ #
303
+ # @option options [Boolean, nil] :f (nil) alias for `:force`
304
+ #
305
+ # @option options [Boolean, nil] :dry_run (nil) do not actually move any
306
+ # files; only show what would happen (alias: `:n`)
307
+ #
308
+ # @option options [Boolean, nil] :n (nil) alias for `:dry_run`
309
+ #
310
+ # @option options [Boolean, nil] :k (nil) skip move or rename actions which
311
+ # would lead to an error
312
+ #
313
+ # @return [String] git's stdout from the mv command
314
+ #
315
+ # @raise [ArgumentError] when unsupported options are provided
316
+ #
317
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
318
+ #
319
+ def mv(source, destination, options = {})
320
+ SharedPrivate.assert_valid_opts!(MV_ALLOWED_OPTS, **options)
321
+ Git::Commands::Mv.new(@execution_context).call(*Array(source), destination, verbose: true, **options).stdout
322
+ end
323
+
160
324
  # Option keys accepted by {#clean}
161
325
  #
162
326
  # The deprecated `:ff` and `:force_force` keys are handled by
@@ -168,13 +332,13 @@ module Git
168
332
  # Remove untracked files from the working tree
169
333
  #
170
334
  # @example Remove untracked files
171
- # repo.clean(force: true)
335
+ # repo.clean({ force: true })
172
336
  #
173
337
  # @example Remove untracked files and directories
174
- # repo.clean(force: true, d: true)
338
+ # repo.clean({ force: true, d: true })
175
339
  #
176
340
  # @example Remove untracked and ignored files
177
- # repo.clean(force: true, x: true)
341
+ # repo.clean({ force: true, x: true })
178
342
  #
179
343
  # @param opts [Hash] options for the clean command
180
344
  #
@@ -19,6 +19,13 @@ module Git
19
19
  # message is the stash description with the leading branch prefix (e.g.
20
20
  # `"On main:"` or `"WIP on main:"`) stripped.
21
21
  #
22
+ # @note The sequential index returned here is **not** the same as git's
23
+ # `stash@{N}` reference used by {#stash_apply}. In git, `stash@{0}` is the
24
+ # **most recent** stash, while index `0` here is the **oldest**. To apply a
25
+ # specific stash from this list, convert the entry's position to a git
26
+ # reference: `'stash@{%d}' % (total - 1 - index)`, or pass the string
27
+ # reference directly to {#stash_apply}.
28
+ #
22
29
  # @example List all stashes (oldest first)
23
30
  # repo.stashes_all #=> [[0, "Fix bug"], [1, "Add feature"]]
24
31
  #
@@ -34,12 +41,38 @@ module Git
34
41
  result = Git::Commands::Stash::List.new(@execution_context).call
35
42
  stashes = Git::Parsers::Stash.parse_list(result.stdout)
36
43
  stashes.reverse.each_with_index.map do |info, i|
37
- match_data = info.message.match(/^[^:]+:(.*)$/)
38
- message = match_data ? match_data[1].strip : info.message
44
+ message = info.message.sub(/^(?:WIP on|On)\s+[^:]+:\s*/, '')
39
45
  [i, message]
40
46
  end
41
47
  end
42
48
 
49
+ # Returns stash entries as a formatted string matching `git stash list` output
50
+ #
51
+ # @deprecated Use {#stashes_all} instead.
52
+ #
53
+ # @example List stashes as a formatted string
54
+ # repo.stash_list #=> "stash@{0}: On main: WIP\nstash@{1}: On feature: Fix bug"
55
+ #
56
+ # @return [String] newline-joined `"stash@{n}: <full message>"` entries, or an
57
+ # empty string when there are no stashes; the format matches `git stash list`
58
+ # output
59
+ #
60
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
61
+ #
62
+ # @see #stashes_all
63
+ #
64
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
65
+ #
66
+ def stash_list
67
+ Git::Deprecation.warn(
68
+ 'Git::Repository#stash_list is deprecated and will be removed in a future version. ' \
69
+ 'Use Git::Repository#stashes_all instead.'
70
+ )
71
+ result = Git::Commands::Stash::List.new(@execution_context).call
72
+ stashes = Git::Parsers::Stash.parse_list(result.stdout)
73
+ stashes.map { |info| "#{info.name}: #{info.message}" }.join("\n")
74
+ end
75
+
43
76
  # Save the current working directory and index state to a new stash
44
77
  #
45
78
  # @param message [String] the stash message
@@ -72,7 +105,10 @@ module Git
72
105
  # repo.stash_apply('stash@{1}') #=> "HEAD is now at abc1234 Initial commit"
73
106
  #
74
107
  # @param id [String, Integer, nil] the stash identifier (e.g., `'stash@{0}'`,
75
- # `0`) or `nil` to apply the most recent stash entry
108
+ # `0`) or `nil` to apply the most recent stash entry. When an Integer is
109
+ # given it is passed directly to git as `stash@{N}`, where `0` is the
110
+ # **most recent** stash — the opposite order from {#stashes_all}'s
111
+ # sequential indices, where `0` is the **oldest** stash.
76
112
  #
77
113
  # @return [String] the output from the git stash apply command
78
114
  #
@@ -43,6 +43,27 @@ module Git
43
43
  true
44
44
  end
45
45
 
46
+ # Returns `true` if the repository has no commits yet
47
+ #
48
+ # @example Check whether a repository is empty
49
+ # repo.empty? #=> true # freshly initialized, no commits yet
50
+ # repo.empty? #=> false # at least one commit exists
51
+ #
52
+ # @return [Boolean] `true` when the repository has no commits, `false` otherwise
53
+ #
54
+ # @raise [Git::FailedError] if git exits with a non-zero exit status other
55
+ # than when the repository has no commits
56
+ #
57
+ # @deprecated Use {#no_commits?} instead
58
+ #
59
+ def empty?
60
+ Git::Deprecation.warn(
61
+ 'Git::Repository#empty? is deprecated and will be removed in a future version. ' \
62
+ 'Use Git::Repository#no_commits? instead.'
63
+ )
64
+ no_commits?
65
+ end
66
+
46
67
  # List all files in the working tree that are not tracked by git
47
68
  #
48
69
  # Runs `git ls-files --others --exclude-standard` from the working tree