git 4.3.2 → 5.0.0.beta.1

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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +486 -410
  52. data/lib/git/branch.rb +380 -43
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +224 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +114 -18
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +349 -20
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,51 @@
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 `options` contains only keys 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 options [Hash] the options hash provided by the caller
36
+ #
37
+ # @return [void]
38
+ #
39
+ # @raise [ArgumentError] when `options` contains any key not in `allowed`
40
+ #
41
+ def assert_valid_opts!(allowed, **options)
42
+ unknown = options.keys - allowed
43
+ return if unknown.empty?
44
+
45
+ raise ArgumentError, "Unknown options: #{unknown.join(', ')}"
46
+ end
47
+ end
48
+
49
+ private_constant :SharedPrivate
50
+ end
51
+ end
@@ -0,0 +1,390 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/add'
4
+ require 'git/commands/clean'
5
+ require 'git/commands/ls_files'
6
+ require 'git/commands/reset'
7
+ require 'git/commands/rm'
8
+ require 'git/escaped_path'
9
+ require 'git/repository/shared_private'
10
+
11
+ module Git
12
+ class Repository
13
+ # Facade methods for staging-area operations: adding, resetting, removing, and
14
+ # cleaning files
15
+ #
16
+ # Included by {Git::Repository}.
17
+ #
18
+ # @api public
19
+ #
20
+ module Staging
21
+ # Option keys accepted by {#add}
22
+ ADD_ALLOWED_OPTS = %i[all force].freeze
23
+ private_constant :ADD_ALLOWED_OPTS
24
+
25
+ # Update the index with the current content found in the working tree
26
+ #
27
+ # @overload add(paths = '.', **options)
28
+ #
29
+ # @example Stage all changed files
30
+ # repo.add
31
+ #
32
+ # @example Stage a specific file
33
+ # repo.add('README.md')
34
+ #
35
+ # @example Stage all changes including deletions
36
+ # repo.add(all: true)
37
+ #
38
+ # @param paths [String, Array<String>] a file or files to add (relative to
39
+ # the worktree root); defaults to `'.'` (all files)
40
+ #
41
+ # @param options [Hash] options for the add command
42
+ #
43
+ # @option options [Boolean, nil] :all (nil) add, modify, and remove index
44
+ # entries to match the worktree
45
+ #
46
+ # @option options [Boolean, nil] :force (nil) allow adding otherwise ignored
47
+ # files
48
+ #
49
+ # @return [String] git's stdout from the add
50
+ #
51
+ # @raise [ArgumentError] when unsupported options are provided
52
+ #
53
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
54
+ #
55
+ def add(paths = '.', **)
56
+ SharedPrivate.assert_valid_opts!(ADD_ALLOWED_OPTS, **)
57
+ Git::Commands::Add.new(@execution_context).call(*Array(paths), **).stdout
58
+ end
59
+
60
+ # Option keys accepted by {#reset}
61
+ RESET_ALLOWED_OPTS = %i[hard].freeze
62
+ private_constant :RESET_ALLOWED_OPTS
63
+
64
+ # Reset the current HEAD to a specified state
65
+ #
66
+ # @overload reset(commitish = nil, **options)
67
+ #
68
+ # @example Reset the index and working tree to HEAD
69
+ # repo.reset
70
+ #
71
+ # @example Hard reset to a specific commit
72
+ # repo.reset('HEAD~1', hard: true)
73
+ #
74
+ # @param commitish [String, nil] the commit or tree-ish to reset to;
75
+ # defaults to HEAD when `nil`
76
+ #
77
+ # @param options [Hash] options for the reset command
78
+ #
79
+ # @option options [Boolean, nil] :hard (nil) reset the index and working
80
+ # tree; discards all tracked changes
81
+ #
82
+ # @return [String] git's stdout from the reset
83
+ #
84
+ # @raise [ArgumentError] when unsupported options are provided
85
+ #
86
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
87
+ #
88
+ def reset(commitish = nil, **)
89
+ SharedPrivate.assert_valid_opts!(RESET_ALLOWED_OPTS, **)
90
+ Git::Commands::Reset.new(@execution_context).call(commitish, **).stdout
91
+ end
92
+
93
+ # Option keys accepted by {#rm}
94
+ RM_ALLOWED_OPTS = %i[
95
+ force f dry_run n r cached ignore_unmatch sparse quiet q
96
+ pathspec_from_file pathspec_file_nul
97
+ ].freeze
98
+ private_constant :RM_ALLOWED_OPTS
99
+
100
+ # Remove file(s) from the working tree and the index
101
+ #
102
+ # @example Remove a single file
103
+ # repo.rm('obsolete.txt', force: true)
104
+ #
105
+ # @example Remove a directory recursively
106
+ # repo.rm('build', r: true)
107
+ #
108
+ # @example Remove from the index only, keeping the working tree copy
109
+ # repo.rm('keep_me.txt', cached: true)
110
+ #
111
+ # @param path [String, Array<String>] a file or files to remove (relative to
112
+ # the worktree root); defaults to `'.'` (all files)
113
+ #
114
+ # @param opts [Hash] options for the rm command
115
+ #
116
+ # @option opts [Boolean, nil] :force (nil) override the up-to-date check and
117
+ # remove files with local modifications (alias: `:f`)
118
+ #
119
+ # @option opts [Boolean, nil] :f (nil) alias for `:force`
120
+ #
121
+ # @option opts [Boolean, nil] :dry_run (nil) do not actually remove any files;
122
+ # only show what would be removed (alias: `:n`)
123
+ #
124
+ # @option opts [Boolean, nil] :n (nil) alias for `:dry_run`
125
+ #
126
+ # @option opts [Boolean, nil] :r (nil) allow recursive removal when a leading
127
+ # directory name is given
128
+ #
129
+ # @option opts [Boolean, nil] :cached (nil) only remove from the index, keeping
130
+ # the working tree files
131
+ #
132
+ # @option opts [Boolean, nil] :ignore_unmatch (nil) exit with a zero status even
133
+ # if no files matched
134
+ #
135
+ # @option opts [Boolean, nil] :sparse (nil) allow updating index entries outside
136
+ # of the sparse-checkout cone
137
+ #
138
+ # @option opts [Boolean, nil] :quiet (nil) suppress the one-line-per-file output
139
+ # (alias: `:q`)
140
+ #
141
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
142
+ #
143
+ # @option opts [String] :pathspec_from_file (nil) read pathspec from the given
144
+ # file, one pathspec element per line; pass `-` to read from standard input
145
+ #
146
+ # @option opts [Boolean, nil] :pathspec_file_nul (nil) when used with
147
+ # `:pathspec_from_file`, separate pathspec elements with NUL instead of newlines
148
+ #
149
+ # @return [String] git's stdout from the rm
150
+ #
151
+ # @raise [ArgumentError] when unsupported options are provided
152
+ #
153
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
154
+ #
155
+ def rm(path = '.', opts = {})
156
+ SharedPrivate.assert_valid_opts!(RM_ALLOWED_OPTS, **opts)
157
+ Git::Commands::Rm.new(@execution_context).call(*Array(path), **opts).stdout
158
+ end
159
+
160
+ # Option keys accepted by {#clean}
161
+ #
162
+ # The deprecated `:ff` and `:force_force` keys are handled by
163
+ # {Git::Repository::Staging::Private.migrate_clean_legacy_options} before this
164
+ # whitelist is enforced, so they are intentionally absent here.
165
+ CLEAN_ALLOWED_OPTS = %i[d force f dry_run n quiet q exclude e x X pathspec].freeze
166
+ private_constant :CLEAN_ALLOWED_OPTS
167
+
168
+ # Remove untracked files from the working tree
169
+ #
170
+ # @example Remove untracked files
171
+ # repo.clean(force: true)
172
+ #
173
+ # @example Remove untracked files and directories
174
+ # repo.clean(force: true, d: true)
175
+ #
176
+ # @example Remove untracked and ignored files
177
+ # repo.clean(force: true, x: true)
178
+ #
179
+ # @param opts [Hash] options for the clean command
180
+ #
181
+ # @option opts [Boolean, nil] :d (nil) recurse into untracked directories
182
+ #
183
+ # @option opts [Boolean, Integer, nil] :force (nil) force the removal of
184
+ # untracked files; pass `2` to also remove untracked nested git repositories
185
+ # (alias: `:f`)
186
+ #
187
+ # @option opts [Boolean, Integer, nil] :f (nil) alias for `:force`
188
+ #
189
+ # @option opts [Boolean, nil] :dry_run (nil) do not actually remove anything,
190
+ # just show what would be done (alias: `:n`)
191
+ #
192
+ # @option opts [Boolean, nil] :n (nil) alias for `:dry_run`
193
+ #
194
+ # @option opts [Boolean, nil] :quiet (nil) be quiet, only report errors
195
+ # (alias: `:q`)
196
+ #
197
+ # @option opts [Boolean, nil] :q (nil) alias for `:quiet`
198
+ #
199
+ # @option opts [String, Array<String>] :exclude (nil) use the given exclude
200
+ # pattern in addition to the standard ignore rules (alias: `:e`)
201
+ #
202
+ # @option opts [String, Array<String>] :e (nil) alias for `:exclude`
203
+ #
204
+ # @option opts [Boolean, nil] :x (nil) don't use the standard ignore rules
205
+ #
206
+ # @option opts [Boolean, nil] :X (nil) remove only files ignored by git
207
+ #
208
+ # @option opts [String, Array<String>] :pathspec (nil) limit cleaning to files
209
+ # matching the given pathspec(s)
210
+ #
211
+ # @return [String] git's stdout from the clean
212
+ #
213
+ # @raise [ArgumentError] when unsupported options are provided, or when a
214
+ # deprecated `:ff`/`:force_force` value is not `true`, `false`, or `nil`
215
+ #
216
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
217
+ #
218
+ def clean(opts = {})
219
+ opts = Private.migrate_clean_legacy_options(opts)
220
+ SharedPrivate.assert_valid_opts!(CLEAN_ALLOWED_OPTS, **opts)
221
+ Git::Commands::Clean.new(@execution_context).call(**opts).stdout
222
+ end
223
+
224
+ # List the files in the working tree that are ignored by git
225
+ #
226
+ # Runs `git ls-files --others --ignored --exclude-standard` and returns the
227
+ # ignored files as repository-relative paths.
228
+ #
229
+ # @example List ignored files
230
+ # repo.ignored_files #=> ["coverage/index.html", "tmp/cache.db"]
231
+ #
232
+ # @example No ignored files
233
+ # repo.ignored_files #=> []
234
+ #
235
+ # @return [Array<String>] repository-relative paths of ignored files; empty
236
+ # when there are none
237
+ #
238
+ # @raise [Git::FailedError] when git exits with a non-zero exit status
239
+ #
240
+ def ignored_files
241
+ Git::Commands::LsFiles.new(@execution_context).call(
242
+ others: true, ignored: true, exclude_standard: true
243
+ ).stdout.split("\n").map { |f| Private.unescape_quoted_path(f) }
244
+ end
245
+
246
+ # Private helpers local to {Git::Repository::Staging}
247
+ #
248
+ # @api private
249
+ #
250
+ module Private
251
+ module_function
252
+
253
+ # Translate deprecated `git clean` option keys into their modern form
254
+ #
255
+ # Maps the legacy `:ff` and `:force_force` boolean options onto the
256
+ # `:force` option, emitting a deprecation warning for each.
257
+ #
258
+ # @param opts [Hash] the caller-provided clean options
259
+ #
260
+ # @return [Hash] a new options hash with deprecated keys translated
261
+ #
262
+ # @raise [ArgumentError] when a deprecated value is not `true`, `false`, or `nil`
263
+ #
264
+ def migrate_clean_legacy_options(opts)
265
+ opts = deprecate_clean_option(opts, :ff, ':ff option is deprecated. Use force: 2 instead.')
266
+ deprecate_clean_option(opts, :force_force, ':force_force option is deprecated. Use force: 2 instead.')
267
+ end
268
+
269
+ # Translate a single deprecated clean option key onto `:force`
270
+ #
271
+ # @param opts [Hash] the clean options
272
+ #
273
+ # @param key [Symbol] the deprecated option key (`:ff` or `:force_force`)
274
+ #
275
+ # @param message [String] the deprecation message to emit
276
+ #
277
+ # @return [Hash] a new options hash with the deprecated key removed
278
+ #
279
+ # @raise [ArgumentError] when the deprecated value is not `true`, `false`,
280
+ # or `nil`
281
+ #
282
+ def deprecate_clean_option(opts, key, message)
283
+ return opts unless opts.key?(key)
284
+
285
+ opts = opts.dup
286
+ deprecated_value = opts.delete(key)
287
+ validate_deprecated_clean_option_value!(key, deprecated_value)
288
+
289
+ Git::Deprecation.warn(message)
290
+ return opts unless deprecated_value
291
+
292
+ opts[:force] = merge_clean_force_option(opts[:force], force_specified: force_option_specified?(opts))
293
+ opts
294
+ end
295
+
296
+ # Whether the caller explicitly set a non-nil `:force` value
297
+ #
298
+ # @param opts [Hash] the clean options
299
+ #
300
+ # @return [Boolean] true if `:force` was set to a non-nil value, false
301
+ # otherwise
302
+ #
303
+ def force_option_specified?(opts)
304
+ opts.key?(:force) && !opts[:force].nil?
305
+ end
306
+
307
+ # Validate the value passed to a deprecated clean option
308
+ #
309
+ # @param key [Symbol] the deprecated option key
310
+ #
311
+ # @param value [Object] the value provided for the deprecated key
312
+ #
313
+ # @return [void]
314
+ #
315
+ # @raise [ArgumentError] when `value` is not `true`, `false`, or `nil`
316
+ #
317
+ def validate_deprecated_clean_option_value!(key, value)
318
+ return if value.nil? || value == true || value == false
319
+
320
+ raise ArgumentError, "#{key} option only accepts true, false, or nil"
321
+ end
322
+
323
+ # Merge a deprecated force request into the existing `:force` value
324
+ #
325
+ # @param existing_force [Boolean, Integer, nil] the caller's `:force` value
326
+ #
327
+ # @param force_specified [Boolean] whether the caller explicitly set `:force`
328
+ #
329
+ # @return [Integer] the resolved `:force` value
330
+ #
331
+ def merge_clean_force_option(existing_force, force_specified: false)
332
+ return 2 unless force_specified
333
+
334
+ normalized_force = normalize_clean_force_option(existing_force)
335
+
336
+ case normalized_force
337
+ when Integer then merge_integer_clean_force_option(normalized_force)
338
+ when false then 2
339
+ else normalized_force
340
+ end
341
+ end
342
+
343
+ # Merge an integer `:force` value with the deprecated force request
344
+ #
345
+ # @param normalized_force [Integer] the caller's normalized `:force` value
346
+ #
347
+ # @return [Integer] the resolved `:force` value
348
+ #
349
+ def merge_integer_clean_force_option(normalized_force)
350
+ return normalized_force if normalized_force < 1
351
+
352
+ [normalized_force, 2].max
353
+ end
354
+
355
+ # Normalize a `:force` value, coercing `true` to the integer `1`
356
+ #
357
+ # @param value [Boolean, Integer, nil] the `:force` value
358
+ #
359
+ # @return [Integer, Boolean, nil] the normalized value
360
+ #
361
+ def normalize_clean_force_option(value)
362
+ case value
363
+ when true then 1
364
+ else value
365
+ end
366
+ end
367
+
368
+ # Unescape a git-quoted path
369
+ #
370
+ # Git wraps paths containing non-ASCII or special characters in
371
+ # double-quotes and octal-escapes each byte. This method strips the
372
+ # surrounding quotes and delegates unescaping to {Git::EscapedPath}.
373
+ #
374
+ # @param path [String] the path as it appears in git output
375
+ #
376
+ # @return [String] the unescaped path
377
+ #
378
+ def unescape_quoted_path(path)
379
+ if path.start_with?('"') && path.end_with?('"')
380
+ Git::EscapedPath.new(path[1..-2]).unescape
381
+ else
382
+ path
383
+ end
384
+ end
385
+ end
386
+
387
+ private_constant :Private
388
+ end
389
+ end
390
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/stash'
4
+ require 'git/parsers/stash'
5
+
6
+ module Git
7
+ class Repository
8
+ # Facade methods for stash operations
9
+ #
10
+ # Included by {Git::Repository}.
11
+ #
12
+ # @api public
13
+ #
14
+ module Stashing
15
+ # Returns all stash entries as an array of index and message pairs
16
+ #
17
+ # Lists all stash entries in the repository ordered from oldest to newest.
18
+ # The index is a sequential number starting from 0 for the oldest stash. The
19
+ # message is the stash description with the leading branch prefix (e.g.
20
+ # `"On main:"` or `"WIP on main:"`) stripped.
21
+ #
22
+ # @example List all stashes (oldest first)
23
+ # repo.stashes_all #=> [[0, "Fix bug"], [1, "Add feature"]]
24
+ #
25
+ # @return [Array<Array(Integer, String)>] array of `[index, message]` pairs
26
+ # where index is the sequential position (0 is oldest) and message is the
27
+ # stash description with the branch prefix stripped
28
+ #
29
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
30
+ #
31
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
32
+ #
33
+ def stashes_all
34
+ result = Git::Commands::Stash::List.new(@execution_context).call
35
+ stashes = Git::Parsers::Stash.parse_list(result.stdout)
36
+ 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
39
+ [i, message]
40
+ end
41
+ end
42
+
43
+ # Save the current working directory and index state to a new stash
44
+ #
45
+ # @param message [String] the stash message
46
+ #
47
+ # @return [Boolean] true if changes were stashed, false if there were no
48
+ # local changes to save
49
+ #
50
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
51
+ #
52
+ # @example Save current changes
53
+ # repo.stash_save('WIP: feature work')
54
+ #
55
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
56
+ #
57
+ def stash_save(message) # rubocop:disable Naming/PredicateMethod
58
+ result = Git::Commands::Stash::Push.new(@execution_context).call(message: message)
59
+ !result.stdout.include?('No local changes to save')
60
+ end
61
+
62
+ # Apply a stash to the working directory
63
+ #
64
+ # Applies the changes recorded in a stash entry to the working directory
65
+ # without removing the entry from the stash list. Unlike `git stash pop`,
66
+ # the stash entry is preserved after applying.
67
+ #
68
+ # @example Apply the most recent stash
69
+ # repo.stash_apply #=> "HEAD is now at abc1234 Initial commit"
70
+ #
71
+ # @example Apply a specific stash entry by reference
72
+ # repo.stash_apply('stash@{1}') #=> "HEAD is now at abc1234 Initial commit"
73
+ #
74
+ # @param id [String, Integer, nil] the stash identifier (e.g., `'stash@{0}'`,
75
+ # `0`) or `nil` to apply the most recent stash entry
76
+ #
77
+ # @return [String] the output from the git stash apply command
78
+ #
79
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
80
+ #
81
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
82
+ #
83
+ def stash_apply(id = nil)
84
+ Git::Commands::Stash::Apply.new(@execution_context).call(id).stdout
85
+ end
86
+
87
+ # Remove all stash entries
88
+ #
89
+ # Removes all entries from the stash list. Use with caution as this
90
+ # operation cannot be undone.
91
+ #
92
+ # @return [String] the output from the git stash clear command
93
+ # (typically empty)
94
+ #
95
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
96
+ #
97
+ # @example Clear all stashes
98
+ # repo.stash_clear #=> ""
99
+ #
100
+ # @see https://git-scm.com/docs/git-stash git-stash documentation
101
+ #
102
+ def stash_clear
103
+ Git::Commands::Stash::Clear.new(@execution_context).call.stdout
104
+ end
105
+ end
106
+ end
107
+ end