git 1.19.1 → 5.5.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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -1
  3. data/AI_POLICY.md +24 -0
  4. data/CHANGELOG.md +689 -0
  5. data/CODE_OF_CONDUCT.md +25 -0
  6. data/CONTRIBUTING.md +1175 -97
  7. data/GOVERNANCE.md +106 -0
  8. data/LICENSE +1 -1
  9. data/MAINTAINERS.md +17 -4
  10. data/README.md +476 -320
  11. data/UPGRADING.md +1138 -0
  12. data/git.gemspec +124 -36
  13. data/lib/git/author.rb +39 -7
  14. data/lib/git/author_info.rb +66 -0
  15. data/lib/git/branch.rb +615 -65
  16. data/lib/git/branch_delete_failure.rb +34 -0
  17. data/lib/git/branch_delete_result.rb +66 -0
  18. data/lib/git/branch_info.rb +237 -0
  19. data/lib/git/branches.rb +167 -44
  20. data/lib/git/command_line/base.rb +247 -0
  21. data/lib/git/command_line/capturing.rb +308 -0
  22. data/lib/git/command_line/result.rb +88 -0
  23. data/lib/git/command_line/streaming.rb +236 -0
  24. data/lib/git/command_line.rb +52 -0
  25. data/lib/git/commands/add.rb +139 -0
  26. data/lib/git/commands/am/abort.rb +43 -0
  27. data/lib/git/commands/am/apply.rb +263 -0
  28. data/lib/git/commands/am/continue.rb +43 -0
  29. data/lib/git/commands/am/quit.rb +43 -0
  30. data/lib/git/commands/am/retry.rb +49 -0
  31. data/lib/git/commands/am/show_current_patch.rb +64 -0
  32. data/lib/git/commands/am/skip.rb +42 -0
  33. data/lib/git/commands/am.rb +33 -0
  34. data/lib/git/commands/apply.rb +242 -0
  35. data/lib/git/commands/archive/list_formats.rb +46 -0
  36. data/lib/git/commands/archive.rb +145 -0
  37. data/lib/git/commands/arguments.rb +4521 -0
  38. data/lib/git/commands/base.rb +502 -0
  39. data/lib/git/commands/branch/copy.rb +102 -0
  40. data/lib/git/commands/branch/create.rb +177 -0
  41. data/lib/git/commands/branch/delete.rb +88 -0
  42. data/lib/git/commands/branch/list.rb +178 -0
  43. data/lib/git/commands/branch/move.rb +102 -0
  44. data/lib/git/commands/branch/set_upstream.rb +86 -0
  45. data/lib/git/commands/branch/show_current.rb +49 -0
  46. data/lib/git/commands/branch/unset_upstream.rb +53 -0
  47. data/lib/git/commands/branch.rb +34 -0
  48. data/lib/git/commands/cat_file/batch.rb +385 -0
  49. data/lib/git/commands/cat_file/filtered.rb +105 -0
  50. data/lib/git/commands/cat_file/raw.rb +271 -0
  51. data/lib/git/commands/cat_file.rb +49 -0
  52. data/lib/git/commands/checkout/branch.rb +153 -0
  53. data/lib/git/commands/checkout/files.rb +116 -0
  54. data/lib/git/commands/checkout.rb +38 -0
  55. data/lib/git/commands/checkout_index.rb +106 -0
  56. data/lib/git/commands/clean.rb +102 -0
  57. data/lib/git/commands/clone.rb +241 -0
  58. data/lib/git/commands/commit.rb +273 -0
  59. data/lib/git/commands/commit_tree.rb +101 -0
  60. data/lib/git/commands/config_option_syntax/add.rb +86 -0
  61. data/lib/git/commands/config_option_syntax/get.rb +121 -0
  62. data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
  63. data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
  64. data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
  65. data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
  66. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
  67. data/lib/git/commands/config_option_syntax/list.rb +111 -0
  68. data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
  69. data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
  70. data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
  71. data/lib/git/commands/config_option_syntax/set.rb +119 -0
  72. data/lib/git/commands/config_option_syntax/unset.rb +92 -0
  73. data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
  74. data/lib/git/commands/config_option_syntax.rb +56 -0
  75. data/lib/git/commands/describe.rb +156 -0
  76. data/lib/git/commands/diff.rb +657 -0
  77. data/lib/git/commands/diff_files.rb +519 -0
  78. data/lib/git/commands/diff_index.rb +499 -0
  79. data/lib/git/commands/fetch.rb +354 -0
  80. data/lib/git/commands/fsck.rb +138 -0
  81. data/lib/git/commands/gc.rb +134 -0
  82. data/lib/git/commands/grep.rb +339 -0
  83. data/lib/git/commands/init.rb +101 -0
  84. data/lib/git/commands/log.rb +634 -0
  85. data/lib/git/commands/ls_files.rb +195 -0
  86. data/lib/git/commands/ls_remote.rb +161 -0
  87. data/lib/git/commands/ls_tree.rb +135 -0
  88. data/lib/git/commands/maintenance/register.rb +77 -0
  89. data/lib/git/commands/maintenance/run.rb +109 -0
  90. data/lib/git/commands/maintenance/start.rb +71 -0
  91. data/lib/git/commands/maintenance/stop.rb +60 -0
  92. data/lib/git/commands/maintenance/unregister.rb +84 -0
  93. data/lib/git/commands/maintenance.rb +31 -0
  94. data/lib/git/commands/merge/abort.rb +44 -0
  95. data/lib/git/commands/merge/continue.rb +44 -0
  96. data/lib/git/commands/merge/quit.rb +46 -0
  97. data/lib/git/commands/merge/start.rb +250 -0
  98. data/lib/git/commands/merge.rb +28 -0
  99. data/lib/git/commands/merge_base.rb +91 -0
  100. data/lib/git/commands/mv.rb +82 -0
  101. data/lib/git/commands/name_rev.rb +119 -0
  102. data/lib/git/commands/pull.rb +382 -0
  103. data/lib/git/commands/push.rb +251 -0
  104. data/lib/git/commands/read_tree.rb +154 -0
  105. data/lib/git/commands/remote/add.rb +96 -0
  106. data/lib/git/commands/remote/get_url.rb +68 -0
  107. data/lib/git/commands/remote/list.rb +56 -0
  108. data/lib/git/commands/remote/prune.rb +63 -0
  109. data/lib/git/commands/remote/remove.rb +52 -0
  110. data/lib/git/commands/remote/rename.rb +76 -0
  111. data/lib/git/commands/remote/set_branches.rb +70 -0
  112. data/lib/git/commands/remote/set_head.rb +89 -0
  113. data/lib/git/commands/remote/set_url.rb +78 -0
  114. data/lib/git/commands/remote/set_url_add.rb +70 -0
  115. data/lib/git/commands/remote/set_url_delete.rb +71 -0
  116. data/lib/git/commands/remote/show.rb +77 -0
  117. data/lib/git/commands/remote/update.rb +79 -0
  118. data/lib/git/commands/remote.rb +42 -0
  119. data/lib/git/commands/repack.rb +281 -0
  120. data/lib/git/commands/reset.rb +154 -0
  121. data/lib/git/commands/rev_parse.rb +304 -0
  122. data/lib/git/commands/revert/abort.rb +45 -0
  123. data/lib/git/commands/revert/continue.rb +62 -0
  124. data/lib/git/commands/revert/quit.rb +47 -0
  125. data/lib/git/commands/revert/skip.rb +44 -0
  126. data/lib/git/commands/revert/start.rb +158 -0
  127. data/lib/git/commands/revert.rb +29 -0
  128. data/lib/git/commands/rm.rb +113 -0
  129. data/lib/git/commands/show.rb +632 -0
  130. data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
  131. data/lib/git/commands/show_ref/exists.rb +80 -0
  132. data/lib/git/commands/show_ref/list.rb +149 -0
  133. data/lib/git/commands/show_ref/verify.rb +122 -0
  134. data/lib/git/commands/show_ref.rb +42 -0
  135. data/lib/git/commands/stash/apply.rb +81 -0
  136. data/lib/git/commands/stash/branch.rb +67 -0
  137. data/lib/git/commands/stash/clear.rb +43 -0
  138. data/lib/git/commands/stash/create.rb +60 -0
  139. data/lib/git/commands/stash/drop.rb +73 -0
  140. data/lib/git/commands/stash/list.rb +43 -0
  141. data/lib/git/commands/stash/pop.rb +87 -0
  142. data/lib/git/commands/stash/push.rb +112 -0
  143. data/lib/git/commands/stash/show.rb +158 -0
  144. data/lib/git/commands/stash/store.rb +72 -0
  145. data/lib/git/commands/stash.rb +38 -0
  146. data/lib/git/commands/status.rb +174 -0
  147. data/lib/git/commands/symbolic_ref/delete.rb +72 -0
  148. data/lib/git/commands/symbolic_ref/read.rb +99 -0
  149. data/lib/git/commands/symbolic_ref/update.rb +79 -0
  150. data/lib/git/commands/symbolic_ref.rb +38 -0
  151. data/lib/git/commands/tag/create.rb +142 -0
  152. data/lib/git/commands/tag/delete.rb +57 -0
  153. data/lib/git/commands/tag/list.rb +146 -0
  154. data/lib/git/commands/tag/verify.rb +71 -0
  155. data/lib/git/commands/tag.rb +26 -0
  156. data/lib/git/commands/update_ref/batch.rb +145 -0
  157. data/lib/git/commands/update_ref/delete.rb +90 -0
  158. data/lib/git/commands/update_ref/update.rb +103 -0
  159. data/lib/git/commands/update_ref.rb +42 -0
  160. data/lib/git/commands/version.rb +60 -0
  161. data/lib/git/commands/worktree/add.rb +139 -0
  162. data/lib/git/commands/worktree/list.rb +64 -0
  163. data/lib/git/commands/worktree/lock.rb +58 -0
  164. data/lib/git/commands/worktree/management_base.rb +51 -0
  165. data/lib/git/commands/worktree/move.rb +66 -0
  166. data/lib/git/commands/worktree/prune.rb +67 -0
  167. data/lib/git/commands/worktree/remove.rb +63 -0
  168. data/lib/git/commands/worktree/repair.rb +76 -0
  169. data/lib/git/commands/worktree/unlock.rb +47 -0
  170. data/lib/git/commands/worktree.rb +43 -0
  171. data/lib/git/commands/write_tree.rb +68 -0
  172. data/lib/git/commands.rb +88 -0
  173. data/lib/git/config.rb +72 -5
  174. data/lib/git/config_entry_info.rb +106 -0
  175. data/lib/git/configuring.rb +795 -0
  176. data/lib/git/detached_head_info.rb +57 -0
  177. data/lib/git/diff.rb +437 -86
  178. data/lib/git/diff_file_numstat_info.rb +31 -0
  179. data/lib/git/diff_file_patch_info.rb +136 -0
  180. data/lib/git/diff_file_raw_info.rb +129 -0
  181. data/lib/git/diff_info.rb +162 -0
  182. data/lib/git/diff_path_status.rb +107 -0
  183. data/lib/git/diff_result.rb +34 -0
  184. data/lib/git/diff_stats.rb +111 -0
  185. data/lib/git/dirstat_info.rb +102 -0
  186. data/lib/git/encoding_utils.rb +32 -1
  187. data/lib/git/errors.rb +285 -0
  188. data/lib/git/escaped_path.rb +57 -5
  189. data/lib/git/execution_context/global.rb +31 -0
  190. data/lib/git/execution_context/repository.rb +151 -0
  191. data/lib/git/execution_context.rb +559 -0
  192. data/lib/git/factories.rb +813 -0
  193. data/lib/git/file_ref.rb +77 -0
  194. data/lib/git/fsck_object.rb +56 -0
  195. data/lib/git/fsck_result.rb +132 -0
  196. data/lib/git/log.rb +306 -90
  197. data/lib/git/object.rb +563 -141
  198. data/lib/git/parsers/branch.rb +240 -0
  199. data/lib/git/parsers/cat_file.rb +111 -0
  200. data/lib/git/parsers/config_entry.rb +110 -0
  201. data/lib/git/parsers/diff.rb +792 -0
  202. data/lib/git/parsers/fsck.rb +144 -0
  203. data/lib/git/parsers/grep.rb +42 -0
  204. data/lib/git/parsers/ls_remote.rb +79 -0
  205. data/lib/git/parsers/ls_tree.rb +58 -0
  206. data/lib/git/parsers/remote.rb +162 -0
  207. data/lib/git/parsers/stash.rb +292 -0
  208. data/lib/git/parsers/status.rb +251 -0
  209. data/lib/git/parsers/tag.rb +341 -0
  210. data/lib/git/parsers/worktree.rb +185 -0
  211. data/lib/git/path_resolver.rb +206 -0
  212. data/lib/git/remote.rb +165 -12
  213. data/lib/git/remote_info.rb +203 -0
  214. data/lib/git/repository/branching.rb +964 -0
  215. data/lib/git/repository/committing.rb +246 -0
  216. data/lib/git/repository/context_helpers.rb +293 -0
  217. data/lib/git/repository/diffing.rb +785 -0
  218. data/lib/git/repository/inspecting.rb +252 -0
  219. data/lib/git/repository/logging.rb +410 -0
  220. data/lib/git/repository/maintenance.rb +65 -0
  221. data/lib/git/repository/merging.rb +451 -0
  222. data/lib/git/repository/object_operations.rb +1551 -0
  223. data/lib/git/repository/remote_operations.rb +984 -0
  224. data/lib/git/repository/shared_private.rb +120 -0
  225. data/lib/git/repository/staging.rb +587 -0
  226. data/lib/git/repository/stashing.rb +623 -0
  227. data/lib/git/repository/status_operations.rb +249 -0
  228. data/lib/git/repository/worktree_operations.rb +339 -0
  229. data/lib/git/repository.rb +484 -2
  230. data/lib/git/stash.rb +109 -12
  231. data/lib/git/stash_info.rb +102 -0
  232. data/lib/git/stashes.rb +169 -26
  233. data/lib/git/status.rb +308 -122
  234. data/lib/git/status_file_info.rb +258 -0
  235. data/lib/git/status_info.rb +189 -0
  236. data/lib/git/tag_delete_failure.rb +34 -0
  237. data/lib/git/tag_delete_result.rb +66 -0
  238. data/lib/git/tag_info.rb +99 -0
  239. data/lib/git/url.rb +15 -8
  240. data/lib/git/version.rb +113 -2
  241. data/lib/git/version_constraint.rb +85 -0
  242. data/lib/git/worktree.rb +150 -8
  243. data/lib/git/worktree_info.rb +128 -0
  244. data/lib/git/worktrees.rb +118 -13
  245. data/lib/git.rb +632 -234
  246. metadata +369 -54
  247. data/.github/stale.yml +0 -25
  248. data/.github/workflows/continuous_integration.yml +0 -49
  249. data/.gitignore +0 -10
  250. data/Dockerfile.changelog-rs +0 -12
  251. data/Gemfile +0 -5
  252. data/ISSUE_TEMPLATE.md +0 -15
  253. data/PULL_REQUEST_TEMPLATE.md +0 -9
  254. data/RELEASING.md +0 -70
  255. data/Rakefile +0 -60
  256. data/lib/git/base/factory.rb +0 -99
  257. data/lib/git/base.rb +0 -711
  258. data/lib/git/command_line_result.rb +0 -86
  259. data/lib/git/failed_error.rb +0 -53
  260. data/lib/git/git_execute_error.rb +0 -7
  261. data/lib/git/index.rb +0 -5
  262. data/lib/git/lib.rb +0 -1328
  263. data/lib/git/path.rb +0 -31
  264. data/lib/git/signaled_error.rb +0 -50
  265. data/lib/git/working_directory.rb +0 -4
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ class Repository
5
+ # Internal helpers shared by `Git::Repository::*` topic modules
6
+ #
7
+ # Methods defined here use `module_function` so they are callable as
8
+ # `SharedPrivate.foo(...)` from any topic module within `Git::Repository`
9
+ # without being added to `Git::Repository`'s instance namespace via `include`.
10
+ #
11
+ # The constant is declared `private_constant` so it is inaccessible from
12
+ # outside the `Git::Repository` class body; callers inside topic modules use
13
+ # the short unqualified form `SharedPrivate.foo(...)`.
14
+ #
15
+ # @api private
16
+ #
17
+ module SharedPrivate
18
+ module_function
19
+
20
+ # Validate that candidate option keys are listed in `allowed`
21
+ #
22
+ # Used by facade methods to enforce that only documented options (those
23
+ # named in `@option` tags) are accepted, even when the underlying command
24
+ # class would accept more keys. This prevents silent expansion of the
25
+ # facade's public contract.
26
+ #
27
+ # @example Reject an undocumented option
28
+ # ADD_ALLOWED_OPTS = %i[all force].freeze
29
+ #
30
+ # SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, bogus: true)
31
+ # #=> raises ArgumentError: Unknown options: bogus
32
+ #
33
+ # @param allowed [Array<Symbol>] the keys permitted by the facade method
34
+ #
35
+ # @param candidate_keywords [Hash<Symbol, Object>] the keywords to validate
36
+ #
37
+ # @option candidate_keywords [Object] key_name a candidate keyword value
38
+ #
39
+ # @return [void]
40
+ #
41
+ # @raise [ArgumentError] when any candidate key is not in `allowed`
42
+ #
43
+ def assert_valid_opts!(allowed, **candidate_keywords)
44
+ unknown = candidate_keywords.keys - allowed
45
+ return if unknown.empty?
46
+
47
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}"
48
+ end
49
+
50
+ # Raise unless `branch` names an existing local branch
51
+ #
52
+ # Used by facade methods that check out a branch, do work on it, and switch
53
+ # back. {Git::Repository#checkout} also accepts commit SHAs, tags, and
54
+ # remote-tracking branches, all of which detach HEAD; work committed there
55
+ # would be left dangling once the original branch is restored, and git has
56
+ # no way to report that.
57
+ #
58
+ # @example With an existing local branch
59
+ # SharedPrivate.assert_local_branch!(repo, 'feature') #=> nil
60
+ #
61
+ # @example With a tag
62
+ # SharedPrivate.assert_local_branch!(repo, 'v1.0.0')
63
+ # #=> raises ArgumentError: 'v1.0.0' is not an existing local branch
64
+ #
65
+ # @param repository [Git::Repository] the repository to check
66
+ #
67
+ # @param branch [String] the branch name to verify
68
+ #
69
+ # @return [void]
70
+ #
71
+ # @raise [ArgumentError] when `branch` is not an existing local branch
72
+ #
73
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
74
+ #
75
+ def assert_local_branch!(repository, branch)
76
+ return if repository.local_branch?(branch)
77
+
78
+ raise ArgumentError, "'#{branch}' is not an existing local branch"
79
+ end
80
+
81
+ # Returns a revision that restores the current HEAD after switching branches
82
+ #
83
+ # Used by facade methods that temporarily check out another branch and
84
+ # then switch back. On a branch, the branch name is enough. When HEAD is
85
+ # detached, {Git::Repository#current_branch} reports `'HEAD'`, which after
86
+ # a checkout resolves to the new branch rather than the original commit,
87
+ # so the commit SHA is captured instead. An unborn branch (one with no
88
+ # commits yet) has no ref to check out by name, so it is rejected here,
89
+ # before the caller switches away from it.
90
+ #
91
+ # @example On a branch
92
+ # SharedPrivate.head_restore_point(repo) #=> "main"
93
+ #
94
+ # @example With a detached HEAD
95
+ # SharedPrivate.head_restore_point(repo) #=> "9b9b31e704c0b85ffdd8d2af2ded85170a5af87d"
96
+ #
97
+ # @param repository [Git::Repository] the repository whose HEAD to record
98
+ #
99
+ # @return [String] the current branch name, or the full HEAD commit SHA
100
+ # when HEAD is detached
101
+ #
102
+ # @raise [Git::Error] when HEAD is on an unborn branch
103
+ #
104
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
105
+ #
106
+ def head_restore_point(repository)
107
+ head = repository.current_branch_state
108
+ case head.state
109
+ when :detached then repository.rev_parse('HEAD').strip
110
+ when :unborn
111
+ raise Git::Error, "HEAD is on the unborn branch '#{head.name}', which cannot be restored " \
112
+ 'after switching branches; make a commit on it first'
113
+ else head.name
114
+ end
115
+ end
116
+ end
117
+
118
+ private_constant :SharedPrivate
119
+ end
120
+ end
@@ -0,0 +1,587 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/add'
4
+ require 'git/commands/am/apply'
5
+ require 'git/commands/apply'
6
+ require 'git/commands/clean'
7
+ require 'git/commands/ls_files'
8
+ require 'git/commands/mv'
9
+ require 'git/commands/read_tree'
10
+ require 'git/commands/reset'
11
+ require 'git/commands/rm'
12
+ require 'git/escaped_path'
13
+ require 'git/repository/shared_private'
14
+
15
+ module Git
16
+ class Repository
17
+ # Facade methods for staging-area operations: adding, resetting, moving,
18
+ # removing, and cleaning files
19
+ #
20
+ # Included by {Git::Repository}.
21
+ #
22
+ # @api private
23
+ #
24
+ module Staging
25
+ # Option keys accepted by {#add}
26
+ ADD_ALLOWED_OPTS = %i[all force].freeze
27
+ private_constant :ADD_ALLOWED_OPTS
28
+
29
+ # Update the index with the current content found in the working tree
30
+ #
31
+ # @overload add(paths = '.', **options)
32
+ #
33
+ # @example Stage all changed files
34
+ # repo.add
35
+ #
36
+ # @example Stage a specific file
37
+ # repo.add('README.md')
38
+ #
39
+ # @example Stage all changes including deletions
40
+ # repo.add(all: true)
41
+ #
42
+ # @param paths [String, Array<String>] a file or files to add (relative to
43
+ # the worktree root); defaults to `'.'` (all files)
44
+ #
45
+ # @param options [Hash] options for the add command
46
+ #
47
+ # @option options [Boolean, nil] :all (nil) add, modify, and remove index
48
+ # entries to match the worktree
49
+ #
50
+ # @option options [Boolean, nil] :force (nil) allow adding otherwise ignored
51
+ # files
52
+ #
53
+ # @return [String] git's stdout from the add
54
+ #
55
+ # @raise [ArgumentError] when unsupported options are provided
56
+ #
57
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
58
+ #
59
+ def add(paths = '.', **)
60
+ SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
61
+ Git::Commands::Add.new(@execution_context).call(*Array(paths), **).stdout
62
+ end
63
+
64
+ # Option keys accepted by {#reset}
65
+ RESET_ALLOWED_OPTS = %i[hard].freeze
66
+ private_constant :RESET_ALLOWED_OPTS
67
+
68
+ # Reset the current HEAD to a specified state
69
+ #
70
+ # @example Reset the index and working tree to HEAD
71
+ # repo.reset
72
+ #
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
+ # @example Hard reset to HEAD
98
+ # repo.reset_hard
99
+ #
100
+ # @example Hard reset to a specific commit
101
+ # repo.reset_hard('HEAD~1')
102
+ #
103
+ # @param commitish [String, nil] the commit or tree-ish to reset to;
104
+ # defaults to HEAD when `nil`
105
+ #
106
+ # @param opts [Hash] options passed through to {#reset}
107
+ #
108
+ # @option opts [Boolean, nil] :hard (nil) ignored; this method always forces
109
+ # `hard: true`
110
+ #
111
+ # @return [String] git's stdout from the reset
112
+ #
113
+ # @raise [ArgumentError] when unsupported options are provided
114
+ #
115
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
116
+ #
117
+ # @deprecated Use {#reset} with `hard: true` instead
118
+ #
119
+ def reset_hard(commitish = nil, opts = {})
120
+ Git::Deprecation.warn(
121
+ 'Git::Repository#reset_hard is deprecated and will be removed in v6.0.0. ' \
122
+ 'Use Git::Repository#reset(commitish, hard: true) instead.'
123
+ )
124
+ reset(commitish, **opts, hard: true)
125
+ end
126
+
127
+ # Apply a patch file to the working tree
128
+ #
129
+ # Reads the unified diff in `file` and applies it to the working tree via
130
+ # `git apply`. If `file` does not exist, the method returns `nil` without
131
+ # calling git — preserving the 4.x `Git::Base#apply` no-op contract.
132
+ #
133
+ # @example Apply a patch to the working tree
134
+ # repo.apply('fix.patch')
135
+ #
136
+ # @param file [String] path to the patch file to apply
137
+ #
138
+ # @return [String] git's stdout (usually empty on success)
139
+ #
140
+ # @return [nil] when `file` does not exist
141
+ #
142
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
143
+ #
144
+ def apply(file)
145
+ return unless File.exist?(file)
146
+
147
+ Git::Commands::Apply.new(@execution_context).call(file, chdir: @execution_context.git_work_dir).stdout
148
+ end
149
+
150
+ # Apply a series of patches from a mailbox file to the current branch
151
+ #
152
+ # Reads the mbox-format file in `file` and applies the patches via
153
+ # `git am`. If `file` does not exist, the method returns `nil` without
154
+ # calling git — preserving the 4.x `Git::Base#apply_mail` no-op contract.
155
+ #
156
+ # @example Apply patches from a mailbox
157
+ # repo.apply_mail('patches.mbox')
158
+ #
159
+ # @param file [String] path to the mbox patch file to apply
160
+ #
161
+ # @return [String] git's stdout (usually empty on success)
162
+ #
163
+ # @return [nil] when `file` does not exist
164
+ #
165
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
166
+ #
167
+ def apply_mail(file)
168
+ return unless File.exist?(file)
169
+
170
+ Git::Commands::Am::Apply.new(@execution_context).call(file, chdir: @execution_context.git_work_dir).stdout
171
+ end
172
+
173
+ # Option keys accepted by {#read_tree}
174
+ READ_TREE_ALLOWED_OPTS = %i[prefix].freeze
175
+ private_constant :READ_TREE_ALLOWED_OPTS
176
+
177
+ # Read tree information into the index
178
+ #
179
+ # Reads the named tree object into the index. This is a low-level plumbing
180
+ # operation used to stage the contents of a tree without updating the
181
+ # working tree. Typically called before {#checkout_index} or as part of
182
+ # custom merge flows.
183
+ #
184
+ # @example Read HEAD into the index
185
+ # repo.read_tree('HEAD')
186
+ #
187
+ # @example Read a tree under a prefix directory
188
+ # repo.read_tree('HEAD', { prefix: 'subdir/' })
189
+ #
190
+ # @param treeish [String] the tree-ish to read into the index
191
+ #
192
+ # @param opts [Hash] options for the read-tree command
193
+ #
194
+ # @option opts [String] :prefix (nil) keep the current index contents and
195
+ # read the named tree-ish under the directory at the given prefix
196
+ # (`--prefix=<prefix>`)
197
+ #
198
+ # @return [String] git's stdout (usually empty on success)
199
+ #
200
+ # @raise [ArgumentError] when unsupported options are provided
201
+ #
202
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
203
+ #
204
+ def read_tree(treeish, opts = {})
205
+ SharedPrivate.assert_valid_opts!(READ_TREE_ALLOWED_OPTS, **opts)
206
+ Git::Commands::ReadTree.new(@execution_context).call(treeish, **opts).stdout
207
+ end
208
+
209
+ # Option keys accepted by {#rm}
210
+ RM_ALLOWED_OPTS = %i[
211
+ force f dry_run n r cached ignore_unmatch sparse quiet q
212
+ pathspec_from_file pathspec_file_nul
213
+ ].freeze
214
+ private_constant :RM_ALLOWED_OPTS
215
+
216
+ # Remove file(s) from the working tree and the index
217
+ #
218
+ # @example Remove a single file
219
+ # repo.rm('obsolete.txt', { force: true })
220
+ #
221
+ # @example Remove a directory recursively
222
+ # repo.rm('build', { r: true })
223
+ #
224
+ # @example Remove from the index only, keeping the working tree copy
225
+ # repo.rm('keep_me.txt', { cached: true })
226
+ #
227
+ # @param path [String, Array<String>] a file or files to remove (relative to
228
+ # the worktree root); defaults to `'.'` (all files)
229
+ #
230
+ # @param opts [Hash] options for the rm command
231
+ #
232
+ # @option opts [Boolean, nil] :force (nil) override the up-to-date check and
233
+ # remove files with local modifications (alias: `:f`)
234
+ #
235
+ # @option opts [Boolean, nil] :f (nil) alias for `:force`
236
+ #
237
+ # @option opts [Boolean, nil] :dry_run (nil) do not actually remove any files;
238
+ # only show what would be removed (alias: `:n`)
239
+ #
240
+ # @option opts [Boolean, nil] :n (nil) alias for `:dry_run`
241
+ #
242
+ # @option opts [Boolean, nil] :r (nil) allow recursive removal when a leading
243
+ # directory name is given
244
+ #
245
+ # @option opts [Boolean, nil] :cached (nil) only remove from the index, keeping
246
+ # the working tree files
247
+ #
248
+ # @option opts [Boolean, nil] :ignore_unmatch (nil) exit with a zero status even
249
+ # if no files matched
250
+ #
251
+ # @option opts [Boolean, nil] :sparse (nil) allow updating index entries outside
252
+ # of the sparse-checkout cone
253
+ #
254
+ # @option opts [Boolean, nil] :quiet (nil) suppress the one-line-per-file output
255
+ # (alias: `:q`)
256
+ #
257
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
258
+ #
259
+ # @option opts [String] :pathspec_from_file (nil) read pathspec from the given
260
+ # file, one pathspec element per line; pass `-` to read from standard input
261
+ #
262
+ # @option opts [Boolean, nil] :pathspec_file_nul (nil) when used with
263
+ # `:pathspec_from_file`, separate pathspec elements with NUL instead of newlines
264
+ #
265
+ # @return [String] git's stdout from the rm
266
+ #
267
+ # @raise [ArgumentError] when unsupported options are provided
268
+ #
269
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
270
+ #
271
+ def rm(path = '.', opts = {})
272
+ SharedPrivate.assert_valid_opts!(RM_ALLOWED_OPTS, **opts)
273
+ Git::Commands::Rm.new(@execution_context).call(*Array(path), **opts).stdout
274
+ end
275
+
276
+ alias remove rm
277
+
278
+ # Option keys accepted by {#mv}
279
+ MV_ALLOWED_OPTS = %i[force f dry_run n k].freeze
280
+ private_constant :MV_ALLOWED_OPTS
281
+
282
+ # Move or rename a file, directory, or symlink in the working tree
283
+ #
284
+ # Updates the index after successful completion, but the change must still
285
+ # be committed.
286
+ #
287
+ # @example Move a single file
288
+ # repo.mv('old.rb', 'new.rb')
289
+ #
290
+ # @example Move multiple files to a directory
291
+ # repo.mv(['file1.rb', 'file2.rb'], 'destination_dir/')
292
+ #
293
+ # @example Force overwrite if destination exists
294
+ # repo.mv('source.rb', 'dest.rb', force: true)
295
+ #
296
+ # @param source [String, Array<String>] one or more source file(s),
297
+ # directory(ies), or symlink(s) to move (relative to the worktree root)
298
+ #
299
+ # @param destination [String] the destination file or directory
300
+ #
301
+ # @param options [Hash] options for the mv command
302
+ #
303
+ # @option options [Boolean, nil] :force (nil) force renaming or moving even
304
+ # if the destination exists (alias: `:f`)
305
+ #
306
+ # @option options [Boolean, nil] :f (nil) alias for `:force`
307
+ #
308
+ # @option options [Boolean, nil] :dry_run (nil) do not actually move any
309
+ # files; only show what would happen (alias: `:n`)
310
+ #
311
+ # @option options [Boolean, nil] :n (nil) alias for `:dry_run`
312
+ #
313
+ # @option options [Boolean, nil] :k (nil) skip move or rename actions which
314
+ # would lead to an error
315
+ #
316
+ # @return [String] git's stdout from the mv command
317
+ #
318
+ # @raise [ArgumentError] when unsupported options are provided
319
+ #
320
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
321
+ #
322
+ def mv(source, destination, options = {})
323
+ SharedPrivate.assert_valid_opts!(MV_ALLOWED_OPTS, **options)
324
+ Git::Commands::Mv.new(@execution_context).call(*Array(source), destination, verbose: true, **options).stdout
325
+ end
326
+
327
+ # Option keys accepted by {#clean}
328
+ #
329
+ # The deprecated `:ff` and `:force_force` keys are handled by
330
+ # {Git::Repository::Staging::Private.migrate_clean_legacy_options} before this
331
+ # whitelist is enforced, so they are intentionally absent here.
332
+ CLEAN_ALLOWED_OPTS = %i[d force f dry_run n quiet q exclude e x X pathspec].freeze
333
+ private_constant :CLEAN_ALLOWED_OPTS
334
+
335
+ # Remove untracked files from the working tree
336
+ #
337
+ # @example Remove untracked files
338
+ # repo.clean({ force: true })
339
+ #
340
+ # @example Remove untracked files and directories
341
+ # repo.clean({ force: true, d: true })
342
+ #
343
+ # @example Remove untracked and ignored files
344
+ # repo.clean({ force: true, x: true })
345
+ #
346
+ # @param opts [Hash] options for the clean command
347
+ #
348
+ # @option opts [Boolean, nil] :d (nil) recurse into untracked directories
349
+ #
350
+ # @option opts [Boolean, Integer, nil] :force (nil) force the removal of
351
+ # untracked files; pass `2` to also remove untracked nested git repositories
352
+ # (alias: `:f`)
353
+ #
354
+ # @option opts [Boolean, Integer, nil] :f (nil) alias for `:force`
355
+ #
356
+ # @option opts [Boolean, nil] :dry_run (nil) do not actually remove anything,
357
+ # just show what would be done (alias: `:n`)
358
+ #
359
+ # @option opts [Boolean, nil] :n (nil) alias for `:dry_run`
360
+ #
361
+ # @option opts [Boolean, nil] :quiet (nil) be quiet, only report errors
362
+ # (alias: `:q`)
363
+ #
364
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
365
+ #
366
+ # @option opts [String, Array<String>] :exclude (nil) use the given exclude
367
+ # pattern in addition to the standard ignore rules (alias: `:e`)
368
+ #
369
+ # @option opts [String, Array<String>] :e (nil) alias for `:exclude`
370
+ #
371
+ # @option opts [Boolean, nil] :x (nil) don't use the standard ignore rules
372
+ #
373
+ # @option opts [Boolean, nil] :X (nil) remove only files ignored by git
374
+ #
375
+ # @option opts [String, Array<String>] :pathspec (nil) limit cleaning to files
376
+ # matching the given pathspec(s)
377
+ #
378
+ # @return [String] git's stdout from the clean
379
+ #
380
+ # @raise [ArgumentError] when unsupported options are provided, or when a
381
+ # deprecated `:ff`/`:force_force` value is not `true`, `false`, or `nil`
382
+ #
383
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
384
+ #
385
+ def clean(opts = {})
386
+ opts = Private.migrate_clean_legacy_options(opts)
387
+ SharedPrivate.assert_valid_opts!(CLEAN_ALLOWED_OPTS, **opts)
388
+ Git::Commands::Clean.new(@execution_context).call(**opts).stdout
389
+ end
390
+
391
+ # List the files in the working tree that are ignored by git
392
+ #
393
+ # Runs `git ls-files --others --ignored --exclude-standard` and returns the
394
+ # ignored files as repository-relative paths.
395
+ #
396
+ # @example List ignored files
397
+ # repo.ignored_files #=> ["coverage/index.html", "tmp/cache.db"]
398
+ #
399
+ # @example No ignored files
400
+ # repo.ignored_files #=> []
401
+ #
402
+ # @return [Array<String>] repository-relative paths of ignored files; empty
403
+ # when there are none
404
+ #
405
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
406
+ #
407
+ def ignored_files
408
+ Git::Commands::LsFiles.new(@execution_context).call(
409
+ others: true, ignored: true, exclude_standard: true
410
+ ).stdout.split("\n").map { |f| Private.unescape_quoted_path(f) }
411
+ end
412
+
413
+ # Private helpers local to {Git::Repository::Staging}
414
+ #
415
+ # @api private
416
+ #
417
+ module Private
418
+ module_function
419
+
420
+ # Translate deprecated `git clean` option keys into their modern form
421
+ #
422
+ # Maps the legacy `:ff` and `:force_force` boolean options onto the
423
+ # `:force` option, emitting a deprecation warning for each.
424
+ #
425
+ # @param opts [Hash] the caller-provided clean options
426
+ #
427
+ # @option opts [Boolean, nil] :ff (nil) deprecated alias for requesting a
428
+ # double-force clean
429
+ #
430
+ # @option opts [Boolean, nil] :force_force (nil) deprecated alias for
431
+ # requesting a double-force clean
432
+ #
433
+ # @option opts [Boolean, Integer, nil] :force (nil) existing force value
434
+ # merged with deprecated options when present
435
+ #
436
+ # @return [Hash] a new options hash with deprecated keys translated
437
+ #
438
+ # @raise [ArgumentError] when a deprecated value is not `true`, `false`, or `nil`
439
+ #
440
+ def migrate_clean_legacy_options(opts)
441
+ opts = deprecate_clean_option(
442
+ :ff,
443
+ ':ff option is deprecated and will be removed in v6.0.0. Use force: 2 instead.',
444
+ opts
445
+ )
446
+
447
+ deprecate_clean_option(
448
+ :force_force,
449
+ ':force_force option is deprecated and will be removed in v6.0.0. Use force: 2 instead.',
450
+ opts
451
+ )
452
+ end
453
+
454
+ # Translate a single deprecated clean option key onto `:force`
455
+ #
456
+ # @param key [Symbol] the deprecated option key (`:ff` or `:force_force`)
457
+ #
458
+ # @param message [String] the deprecation message to emit
459
+ #
460
+ # @param opts [Hash] the clean options
461
+ #
462
+ # @option opts [Boolean, nil] :ff (nil) deprecated alias for requesting a
463
+ # double-force clean
464
+ #
465
+ # @option opts [Boolean, nil] :force_force (nil) deprecated alias for
466
+ # requesting a double-force clean
467
+ #
468
+ # @option opts [Boolean, Integer, nil] :force (nil) existing force value
469
+ # updated when the deprecated key is true
470
+ #
471
+ # @return [Hash] a new options hash with the deprecated key removed
472
+ #
473
+ # @raise [ArgumentError] when the deprecated value is not `true`, `false`,
474
+ # or `nil`
475
+ #
476
+ def deprecate_clean_option(key, message, opts)
477
+ return opts unless opts.key?(key)
478
+
479
+ opts = opts.dup
480
+ deprecated_value = opts.delete(key)
481
+ validate_deprecated_clean_option_value!(key, deprecated_value)
482
+
483
+ Git::Deprecation.warn(message)
484
+ return opts unless deprecated_value
485
+
486
+ opts[:force] = merge_clean_force_option(opts[:force], force_specified: force_option_specified?(opts))
487
+ opts
488
+ end
489
+
490
+ # Whether the caller explicitly set a non-nil `:force` value
491
+ #
492
+ # @param opts [Hash] the clean options
493
+ #
494
+ # @option opts [Boolean, Integer, nil] :force (nil) the clean force value
495
+ # to inspect
496
+ #
497
+ # @return [Boolean] true if `:force` was set to a non-nil value, false
498
+ # otherwise
499
+ #
500
+ def force_option_specified?(opts)
501
+ opts.key?(:force) && !opts[:force].nil?
502
+ end
503
+
504
+ # Validate the value passed to a deprecated clean option
505
+ #
506
+ # @param key [Symbol] the deprecated option key
507
+ #
508
+ # @param value [Object] the value provided for the deprecated key
509
+ #
510
+ # @return [void]
511
+ #
512
+ # @raise [ArgumentError] when `value` is not `true`, `false`, or `nil`
513
+ #
514
+ def validate_deprecated_clean_option_value!(key, value)
515
+ return if value.nil? || value == true || value == false
516
+
517
+ raise ArgumentError, "#{key} option only accepts true, false, or nil"
518
+ end
519
+
520
+ # Merge a deprecated force request into the existing `:force` value
521
+ #
522
+ # @param existing_force [Boolean, Integer, nil] the caller's `:force` value
523
+ #
524
+ # @param force_specified [Boolean] whether the caller explicitly set `:force`
525
+ #
526
+ # @return [Integer] the resolved `:force` value
527
+ #
528
+ def merge_clean_force_option(existing_force, force_specified: false)
529
+ return 2 unless force_specified
530
+
531
+ normalized_force = normalize_clean_force_option(existing_force)
532
+
533
+ case normalized_force
534
+ when Integer then merge_integer_clean_force_option(normalized_force)
535
+ when false then 2
536
+ else normalized_force
537
+ end
538
+ end
539
+
540
+ # Merge an integer `:force` value with the deprecated force request
541
+ #
542
+ # @param normalized_force [Integer] the caller's normalized `:force` value
543
+ #
544
+ # @return [Integer] the resolved `:force` value
545
+ #
546
+ def merge_integer_clean_force_option(normalized_force)
547
+ return normalized_force if normalized_force < 1
548
+
549
+ [normalized_force, 2].max
550
+ end
551
+
552
+ # Normalize a `:force` value, coercing `true` to the integer `1`
553
+ #
554
+ # @param value [Boolean, Integer, nil] the `:force` value
555
+ #
556
+ # @return [Integer, Boolean, nil] the normalized value
557
+ #
558
+ def normalize_clean_force_option(value)
559
+ case value
560
+ when true then 1
561
+ else value
562
+ end
563
+ end
564
+
565
+ # Unescape a git-quoted path
566
+ #
567
+ # Git wraps paths containing non-ASCII or special characters in
568
+ # double-quotes and octal-escapes each byte. This method strips the
569
+ # surrounding quotes and delegates unescaping to {Git::EscapedPath}.
570
+ #
571
+ # @param path [String] the path as it appears in git output
572
+ #
573
+ # @return [String] the unescaped path
574
+ #
575
+ def unescape_quoted_path(path)
576
+ if path.start_with?('"') && path.end_with?('"')
577
+ Git::EscapedPath.new(path[1..-2]).unescape
578
+ else
579
+ path
580
+ end
581
+ end
582
+ end
583
+
584
+ private_constant :Private
585
+ end
586
+ end
587
+ end