git 4.3.2 → 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 (297) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +73 -2705
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/pull_request_template.md +3 -1
  5. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  6. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  7. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  8. data/.github/skills/command-implementation/SKILL.md +229 -0
  9. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  10. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  11. data/.github/skills/dependency-management/SKILL.md +72 -0
  12. data/.github/skills/development-workflow/SKILL.md +506 -0
  13. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  14. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  15. data/.github/skills/extract-facade-from-base-lib/SKILL.md +600 -0
  16. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  17. data/.github/skills/facade-implementation/SKILL.md +274 -0
  18. data/.github/skills/facade-test-conventions/SKILL.md +394 -0
  19. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  20. data/.github/skills/make-skill-template/SKILL.md +176 -0
  21. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  22. data/.github/skills/project-context/SKILL.md +313 -0
  23. data/.github/skills/pull-request-review/SKILL.md +168 -0
  24. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  25. data/.github/skills/release-management/SKILL.md +125 -0
  26. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  27. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  28. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  29. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  30. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  31. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  32. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  33. data/.github/skills/test-debugging/SKILL.md +160 -0
  34. data/.github/skills/yard-documentation/SKILL.md +793 -0
  35. data/.github/workflows/continuous_integration.yml +3 -2
  36. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  37. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  38. data/.github/workflows/release.yml +3 -4
  39. data/.gitignore +8 -0
  40. data/.husky/pre-commit +13 -0
  41. data/.release-please-manifest.json +1 -1
  42. data/.rspec +3 -0
  43. data/.rubocop.yml +12 -3
  44. data/.rubocop_todo.yml +23 -5
  45. data/.yardopts +1 -0
  46. data/CHANGELOG.md +0 -40
  47. data/CONTRIBUTING.md +694 -53
  48. data/README.md +67 -15
  49. data/Rakefile +61 -9
  50. data/UPGRADING.md +141 -0
  51. data/commitlint.test +4 -0
  52. data/git.gemspec +18 -7
  53. data/lib/git/args_builder.rb +0 -8
  54. data/lib/git/branch.rb +369 -43
  55. data/lib/git/branch_delete_failure.rb +31 -0
  56. data/lib/git/branch_delete_result.rb +63 -0
  57. data/lib/git/branch_info.rb +178 -0
  58. data/lib/git/branches.rb +122 -24
  59. data/lib/git/command_line/base.rb +255 -0
  60. data/lib/git/command_line/capturing.rb +251 -0
  61. data/lib/git/command_line/result.rb +96 -0
  62. data/lib/git/command_line/streaming.rb +196 -0
  63. data/lib/git/command_line.rb +43 -322
  64. data/lib/git/command_line_result.rb +4 -88
  65. data/lib/git/commands/add.rb +131 -0
  66. data/lib/git/commands/am/abort.rb +43 -0
  67. data/lib/git/commands/am/apply.rb +252 -0
  68. data/lib/git/commands/am/continue.rb +43 -0
  69. data/lib/git/commands/am/quit.rb +43 -0
  70. data/lib/git/commands/am/retry.rb +47 -0
  71. data/lib/git/commands/am/show_current_patch.rb +64 -0
  72. data/lib/git/commands/am/skip.rb +42 -0
  73. data/lib/git/commands/am.rb +33 -0
  74. data/lib/git/commands/apply.rb +237 -0
  75. data/lib/git/commands/archive/list_formats.rb +46 -0
  76. data/lib/git/commands/archive.rb +140 -0
  77. data/lib/git/commands/arguments.rb +3510 -0
  78. data/lib/git/commands/base.rb +404 -0
  79. data/lib/git/commands/branch/copy.rb +94 -0
  80. data/lib/git/commands/branch/create.rb +173 -0
  81. data/lib/git/commands/branch/delete.rb +80 -0
  82. data/lib/git/commands/branch/list.rb +162 -0
  83. data/lib/git/commands/branch/move.rb +94 -0
  84. data/lib/git/commands/branch/set_upstream.rb +86 -0
  85. data/lib/git/commands/branch/show_current.rb +49 -0
  86. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  87. data/lib/git/commands/branch.rb +34 -0
  88. data/lib/git/commands/cat_file/batch.rb +369 -0
  89. data/lib/git/commands/cat_file/filtered.rb +105 -0
  90. data/lib/git/commands/cat_file/raw.rb +216 -0
  91. data/lib/git/commands/cat_file.rb +49 -0
  92. data/lib/git/commands/checkout/branch.rb +151 -0
  93. data/lib/git/commands/checkout/files.rb +115 -0
  94. data/lib/git/commands/checkout.rb +38 -0
  95. data/lib/git/commands/checkout_index.rb +105 -0
  96. data/lib/git/commands/clean.rb +100 -0
  97. data/lib/git/commands/clone.rb +240 -0
  98. data/lib/git/commands/commit.rb +272 -0
  99. data/lib/git/commands/commit_tree.rb +100 -0
  100. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  101. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  102. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  103. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  104. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  105. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  106. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  108. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  109. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  110. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  111. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  112. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  113. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  114. data/lib/git/commands/config_option_syntax.rb +56 -0
  115. data/lib/git/commands/describe.rb +155 -0
  116. data/lib/git/commands/diff.rb +656 -0
  117. data/lib/git/commands/diff_files.rb +518 -0
  118. data/lib/git/commands/diff_index.rb +496 -0
  119. data/lib/git/commands/fetch.rb +352 -0
  120. data/lib/git/commands/fsck.rb +136 -0
  121. data/lib/git/commands/gc.rb +132 -0
  122. data/lib/git/commands/grep.rb +338 -0
  123. data/lib/git/commands/init.rb +99 -0
  124. data/lib/git/commands/log.rb +632 -0
  125. data/lib/git/commands/ls_files.rb +191 -0
  126. data/lib/git/commands/ls_remote.rb +155 -0
  127. data/lib/git/commands/ls_tree.rb +131 -0
  128. data/lib/git/commands/maintenance/register.rb +75 -0
  129. data/lib/git/commands/maintenance/run.rb +104 -0
  130. data/lib/git/commands/maintenance/start.rb +66 -0
  131. data/lib/git/commands/maintenance/stop.rb +55 -0
  132. data/lib/git/commands/maintenance/unregister.rb +79 -0
  133. data/lib/git/commands/maintenance.rb +31 -0
  134. data/lib/git/commands/merge/abort.rb +44 -0
  135. data/lib/git/commands/merge/continue.rb +44 -0
  136. data/lib/git/commands/merge/quit.rb +46 -0
  137. data/lib/git/commands/merge/start.rb +245 -0
  138. data/lib/git/commands/merge.rb +28 -0
  139. data/lib/git/commands/merge_base.rb +86 -0
  140. data/lib/git/commands/mv.rb +77 -0
  141. data/lib/git/commands/name_rev.rb +114 -0
  142. data/lib/git/commands/pull.rb +377 -0
  143. data/lib/git/commands/push.rb +246 -0
  144. data/lib/git/commands/read_tree.rb +149 -0
  145. data/lib/git/commands/remote/add.rb +91 -0
  146. data/lib/git/commands/remote/get_url.rb +66 -0
  147. data/lib/git/commands/remote/list.rb +54 -0
  148. data/lib/git/commands/remote/prune.rb +61 -0
  149. data/lib/git/commands/remote/remove.rb +52 -0
  150. data/lib/git/commands/remote/rename.rb +69 -0
  151. data/lib/git/commands/remote/set_branches.rb +63 -0
  152. data/lib/git/commands/remote/set_head.rb +82 -0
  153. data/lib/git/commands/remote/set_url.rb +71 -0
  154. data/lib/git/commands/remote/set_url_add.rb +61 -0
  155. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  156. data/lib/git/commands/remote/show.rb +71 -0
  157. data/lib/git/commands/remote/update.rb +72 -0
  158. data/lib/git/commands/remote.rb +42 -0
  159. data/lib/git/commands/repack.rb +277 -0
  160. data/lib/git/commands/reset.rb +147 -0
  161. data/lib/git/commands/rev_parse.rb +297 -0
  162. data/lib/git/commands/revert/abort.rb +45 -0
  163. data/lib/git/commands/revert/continue.rb +57 -0
  164. data/lib/git/commands/revert/quit.rb +47 -0
  165. data/lib/git/commands/revert/skip.rb +44 -0
  166. data/lib/git/commands/revert/start.rb +153 -0
  167. data/lib/git/commands/revert.rb +29 -0
  168. data/lib/git/commands/rm.rb +114 -0
  169. data/lib/git/commands/show.rb +632 -0
  170. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  171. data/lib/git/commands/show_ref/exists.rb +78 -0
  172. data/lib/git/commands/show_ref/list.rb +145 -0
  173. data/lib/git/commands/show_ref/verify.rb +120 -0
  174. data/lib/git/commands/show_ref.rb +42 -0
  175. data/lib/git/commands/stash/apply.rb +75 -0
  176. data/lib/git/commands/stash/branch.rb +65 -0
  177. data/lib/git/commands/stash/clear.rb +41 -0
  178. data/lib/git/commands/stash/create.rb +58 -0
  179. data/lib/git/commands/stash/drop.rb +67 -0
  180. data/lib/git/commands/stash/list.rb +39 -0
  181. data/lib/git/commands/stash/pop.rb +78 -0
  182. data/lib/git/commands/stash/push.rb +103 -0
  183. data/lib/git/commands/stash/show.rb +149 -0
  184. data/lib/git/commands/stash/store.rb +63 -0
  185. data/lib/git/commands/stash.rb +38 -0
  186. data/lib/git/commands/status.rb +169 -0
  187. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  188. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  189. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  190. data/lib/git/commands/symbolic_ref.rb +38 -0
  191. data/lib/git/commands/tag/create.rb +139 -0
  192. data/lib/git/commands/tag/delete.rb +55 -0
  193. data/lib/git/commands/tag/list.rb +143 -0
  194. data/lib/git/commands/tag/verify.rb +71 -0
  195. data/lib/git/commands/tag.rb +26 -0
  196. data/lib/git/commands/update_ref/batch.rb +140 -0
  197. data/lib/git/commands/update_ref/delete.rb +92 -0
  198. data/lib/git/commands/update_ref/update.rb +106 -0
  199. data/lib/git/commands/update_ref.rb +42 -0
  200. data/lib/git/commands/version.rb +57 -0
  201. data/lib/git/commands/worktree/add.rb +140 -0
  202. data/lib/git/commands/worktree/list.rb +64 -0
  203. data/lib/git/commands/worktree/lock.rb +58 -0
  204. data/lib/git/commands/worktree/management_base.rb +51 -0
  205. data/lib/git/commands/worktree/move.rb +66 -0
  206. data/lib/git/commands/worktree/prune.rb +67 -0
  207. data/lib/git/commands/worktree/remove.rb +63 -0
  208. data/lib/git/commands/worktree/repair.rb +76 -0
  209. data/lib/git/commands/worktree/unlock.rb +47 -0
  210. data/lib/git/commands/worktree.rb +43 -0
  211. data/lib/git/commands/write_tree.rb +68 -0
  212. data/lib/git/commands.rb +87 -0
  213. data/lib/git/config.rb +17 -0
  214. data/lib/git/config_entry_info.rb +106 -0
  215. data/lib/git/configuring.rb +665 -0
  216. data/lib/git/deprecation.rb +9 -0
  217. data/lib/git/detached_head_info.rb +54 -0
  218. data/lib/git/diff.rb +293 -7
  219. data/lib/git/diff_file_numstat_info.rb +29 -0
  220. data/lib/git/diff_file_patch_info.rb +134 -0
  221. data/lib/git/diff_file_raw_info.rb +127 -0
  222. data/lib/git/diff_info.rb +169 -0
  223. data/lib/git/diff_path_status.rb +67 -19
  224. data/lib/git/diff_result.rb +32 -0
  225. data/lib/git/diff_stats.rb +51 -14
  226. data/lib/git/dirstat_info.rb +86 -0
  227. data/lib/git/errors.rb +65 -2
  228. data/lib/git/execution_context/global.rb +31 -0
  229. data/lib/git/execution_context/repository.rb +136 -0
  230. data/lib/git/execution_context.rb +501 -0
  231. data/lib/git/file_ref.rb +74 -0
  232. data/lib/git/fsck_object.rb +9 -9
  233. data/lib/git/fsck_result.rb +1 -1
  234. data/lib/git/log.rb +9 -2
  235. data/lib/git/object.rb +90 -27
  236. data/lib/git/parsers/branch.rb +224 -0
  237. data/lib/git/parsers/cat_file.rb +111 -0
  238. data/lib/git/parsers/config_entry.rb +110 -0
  239. data/lib/git/parsers/diff.rb +585 -0
  240. data/lib/git/parsers/fsck.rb +133 -0
  241. data/lib/git/parsers/grep.rb +42 -0
  242. data/lib/git/parsers/ls_remote.rb +79 -0
  243. data/lib/git/parsers/ls_tree.rb +58 -0
  244. data/lib/git/parsers/stash.rb +208 -0
  245. data/lib/git/parsers/tag.rb +257 -0
  246. data/lib/git/remote.rb +120 -9
  247. data/lib/git/repository/branching.rb +743 -0
  248. data/lib/git/repository/committing.rb +187 -0
  249. data/lib/git/repository/configuring.rb +351 -0
  250. data/lib/git/repository/context_helpers.rb +264 -0
  251. data/lib/git/repository/diffing.rb +775 -0
  252. data/lib/git/repository/factories.rb +682 -0
  253. data/lib/git/repository/inspecting.rb +252 -0
  254. data/lib/git/repository/logging.rb +247 -0
  255. data/lib/git/repository/maintenance.rb +65 -0
  256. data/lib/git/repository/merging.rb +357 -0
  257. data/lib/git/repository/object_operations.rb +1199 -0
  258. data/lib/git/repository/path_resolver.rb +207 -0
  259. data/lib/git/repository/remote_operations.rb +898 -0
  260. data/lib/git/repository/shared_private.rb +51 -0
  261. data/lib/git/repository/staging.rb +554 -0
  262. data/lib/git/repository/stashing.rb +143 -0
  263. data/lib/git/repository/status_operations.rb +201 -0
  264. data/lib/git/repository/worktree_operations.rb +159 -0
  265. data/lib/git/repository.rb +203 -1
  266. data/lib/git/stash.rb +78 -4
  267. data/lib/git/stash_info.rb +104 -0
  268. data/lib/git/stashes.rb +125 -13
  269. data/lib/git/status.rb +215 -18
  270. data/lib/git/tag_delete_failure.rb +31 -0
  271. data/lib/git/tag_delete_result.rb +63 -0
  272. data/lib/git/tag_info.rb +105 -0
  273. data/lib/git/version.rb +109 -2
  274. data/lib/git/version_constraint.rb +81 -0
  275. data/lib/git/worktree.rb +108 -6
  276. data/lib/git/worktrees.rb +94 -7
  277. data/lib/git.rb +260 -61
  278. data/redesign/1_architecture_existing.md +54 -18
  279. data/redesign/2_architecture_redesign.md +365 -46
  280. data/redesign/3_architecture_implementation.md +1497 -63
  281. data/redesign/Phase 4 - Step A.md +360 -0
  282. data/redesign/beta_release.md +107 -0
  283. data/redesign/c1c2_audit.md +566 -0
  284. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  285. data/redesign/config_design.rb +501 -0
  286. data/tasks/gem_tasks.rake +4 -0
  287. data/tasks/npm_tasks.rake +7 -0
  288. data/tasks/rspec.rake +48 -0
  289. data/tasks/test.rake +13 -1
  290. data/tasks/yard.rake +34 -7
  291. metadata +365 -22
  292. data/lib/git/base.rb +0 -1128
  293. data/lib/git/index.rb +0 -6
  294. data/lib/git/lib.rb +0 -2277
  295. data/lib/git/path.rb +0 -38
  296. data/lib/git/working_directory.rb +0 -6
  297. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,404 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/arguments'
4
+ require 'git/version_constraint'
5
+
6
+ module Git
7
+ module Commands
8
+ # @api private
9
+ #
10
+ # Base class for git command implementations.
11
+ #
12
+ # Provides default {#initialize} and {#call} methods so that simple commands
13
+ # only need to declare their arguments:
14
+ #
15
+ # class Add < Git::Commands::Base
16
+ # arguments do
17
+ # literal 'add'
18
+ # flag_option :all
19
+ # flag_option :force
20
+ # end_of_options
21
+ # operand :paths, repeatable: true
22
+ # end
23
+ #
24
+ # # Execute the git add command
25
+ # # ...YARD docs...
26
+ # def call(...) = super
27
+ # end
28
+ #
29
+ # Commands whose git process may exit with a non-zero status that is
30
+ # *not* an error can declare the acceptable range of exit codes:
31
+ #
32
+ # class Delete < Git::Commands::Base
33
+ # arguments do
34
+ # literal 'branch'
35
+ # literal '--delete'
36
+ # operand :branch_names, repeatable: true, required: true
37
+ # end
38
+ #
39
+ # allow_exit_status 0..1
40
+ #
41
+ # # Execute the git branch --delete command
42
+ # # ...YARD docs...
43
+ # def call(...) = super
44
+ # end
45
+ #
46
+ # Commands with execution options (e.g., timeout) work with the default
47
+ # `call` — execution options are extracted and forwarded automatically.
48
+ class Base # rubocop:disable Metrics/ClassLength
49
+ class << self
50
+ # @return [Git::Commands::Arguments, nil] the frozen argument definition for this command
51
+ attr_reader :args_definition
52
+
53
+ # Define the command's arguments using the {Arguments} DSL.
54
+ #
55
+ # @yield the block passed to {Arguments.define}
56
+ #
57
+ # @raise [ArgumentError] if called more than once on the same class
58
+ #
59
+ # @return [void]
60
+ def arguments(&)
61
+ raise ArgumentError, "arguments already defined for #{name}" if @args_definition
62
+
63
+ @args_definition = Arguments.define(&).freeze
64
+ end
65
+
66
+ # @return [Range, nil] range of exit status values accepted by this command
67
+ attr_reader :allowed_exit_status_range
68
+
69
+ # Declare the acceptable range of exit status values for this command.
70
+ #
71
+ # @example git-diff exits 1 when a diff is found (not an error)
72
+ # allow_exit_status 0..1
73
+ #
74
+ # @example git-fsck uses exit codes 0-7 as bit flags
75
+ # allow_exit_status 0..7
76
+ #
77
+ # @param range [Range] range of accepted exit status values
78
+ #
79
+ # @raise [ArgumentError] if range is invalid
80
+ #
81
+ # @return [void]
82
+ def allow_exit_status(range)
83
+ raise ArgumentError, 'allow_exit_status expects a Range' unless range.is_a?(Range)
84
+ unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
85
+ raise ArgumentError, 'allow_exit_status bounds must be Integers'
86
+ end
87
+
88
+ raise ArgumentError, 'allow_exit_status range must not be empty' if range.begin > range.end
89
+
90
+ @allowed_exit_status_range = range
91
+ end
92
+
93
+ # @return [Git::VersionConstraint, nil] version constraint for this command
94
+ #
95
+ # Returns +nil+ if the command is available in all supported git versions.
96
+ attr_reader :git_version_constraint
97
+
98
+ # @!attribute [r] skip_version_validation?
99
+ # @return [Boolean] whether this command skips version validation
100
+ def skip_version_validation? = !!@skip_version_validation
101
+
102
+ # Declare that this command should skip version validation.
103
+ #
104
+ # This is intended for internal use only — specifically for the
105
+ # `git version` command, which cannot validate versions without
106
+ # causing infinite recursion.
107
+ #
108
+ # @api private
109
+ #
110
+ # @return [void]
111
+ def skip_version_validation
112
+ @skip_version_validation = true
113
+ end
114
+
115
+ # Declare the git version requirements for this command.
116
+ #
117
+ # Use this when the command (or the specific sub-action this class wraps) was
118
+ # introduced in a git version later than +Git::MINIMUM_GIT_VERSION+, or when
119
+ # the command was removed in a later version. When not declared, the command
120
+ # is assumed to be available in all supported git versions.
121
+ #
122
+ # @example git-am --retry requires git 2.46.0 or later
123
+ # requires_git_version '2.46.0'
124
+ #
125
+ # @example Feature with version range
126
+ # requires_git_version '2.29.0', before: '2.50.0'
127
+ #
128
+ # @example Feature available before git 2.50.0
129
+ # requires_git_version before: '2.50.0'
130
+ #
131
+ # @param min [String, nil] minimum version
132
+ # @param before [String, nil] upper bound version (exclusive)
133
+ #
134
+ # @raise [ArgumentError] if version format is invalid or called twice
135
+ #
136
+ # @return [void]
137
+ def requires_git_version(min = nil, before: nil)
138
+ raise ArgumentError, 'requires_git_version already declared for this class' if @git_version_constraint
139
+
140
+ @git_version_constraint = normalize_version_constraint(min, before)
141
+ end
142
+
143
+ private
144
+
145
+ # Normalize a version constraint to a VersionConstraint
146
+ #
147
+ # @param min [String, nil] minimum version
148
+ # @param before_version [String, nil] upper bound version
149
+ #
150
+ # @return [Git::VersionConstraint] the normalized constraint
151
+ #
152
+ # @raise [ArgumentError] if the constraint is invalid
153
+ #
154
+ def normalize_version_constraint(min, before_version)
155
+ raise ArgumentError, 'requires_git_version requires min or before:' unless min || before_version
156
+
157
+ min_version = min ? parse_version(min, 'min') : nil
158
+ before_parsed = before_version ? parse_version(before_version, 'before') : nil
159
+
160
+ Git::VersionConstraint.new(min: min_version, before: before_parsed)
161
+ end
162
+
163
+ def parse_version(version, key_name)
164
+ validate_version_format!(version, key_name)
165
+ Git::Version.parse(version)
166
+ end
167
+
168
+ def validate_version_format!(version, context = nil)
169
+ return if version.is_a?(String) && version.match?(/\A\d+\.\d+\.\d+\z/)
170
+
171
+ ctx = context ? " for #{context}" : ''
172
+ raise ArgumentError,
173
+ "requires_git_version expects a 'major.minor.patch' string#{ctx}, got: #{version.inspect}"
174
+ end
175
+ end
176
+
177
+ # @param execution_context [Git::ExecutionContext] context that provides
178
+ # {Git::ExecutionContext#command_capturing} and {Git::ExecutionContext#command_streaming}
179
+ def initialize(execution_context)
180
+ @execution_context = execution_context
181
+ end
182
+
183
+ # Execute the git command.
184
+ #
185
+ # @overload call(*args, **kwargs)
186
+ # Bind arguments and execute the command.
187
+ #
188
+ # Execution options (declared via `execution_option` in the Arguments
189
+ # DSL) are extracted from the bound arguments via
190
+ # {Git::Commands::Arguments::Bound#execution_options} and forwarded as
191
+ # keyword arguments to the execution context via {#execute_command}.
192
+ #
193
+ # When the `:out` execution option is present, stdout is streamed using
194
+ # `@execution_context.command_streaming`. Otherwise, stdout is captured
195
+ # using `@execution_context.command_capturing`.
196
+ #
197
+ # @example
198
+ # # In a command subclass:
199
+ # # result = command.call('HEAD', timeout: 10)
200
+ #
201
+ # @param args [Array] positional arguments forwarded to {Arguments#bind}
202
+ #
203
+ # @param kwargs [Hash] keyword arguments forwarded to {Arguments#bind}
204
+ #
205
+ # @return [Git::CommandLineResult] the result of calling `git`
206
+ #
207
+ # @raise [ArgumentError] if no arguments definition is declared on the command class
208
+ #
209
+ # @raise [Git::FailedError] if git returns an exit code outside the allowed range
210
+ #
211
+ # @raise [Git::VersionError] if the installed git version doesn't meet requirements
212
+ def call(*, **)
213
+ bound = args_definition.bind(*, **)
214
+ validate_version!(bound.execution_options)
215
+ result = execute_command(bound)
216
+ validate_exit_status!(result)
217
+ result
218
+ end
219
+
220
+ private
221
+
222
+ def args_definition
223
+ self.class.args_definition || raise(ArgumentError, "arguments not defined for #{self.class.name}")
224
+ end
225
+
226
+ def execute_command(bound)
227
+ exec_opts = execution_opts(bound)
228
+
229
+ if exec_opts.key?(:out)
230
+ @execution_context.command_streaming(*bound, **exec_opts, raise_on_failure: false)
231
+ else
232
+ @execution_context.command_capturing(*bound, **capturing_opts(exec_opts), raise_on_failure: false)
233
+ end
234
+ end
235
+
236
+ def execution_opts(bound)
237
+ caller_env = bound.execution_options.fetch(:env, {}) || {}
238
+ merged_env = caller_env.merge(env)
239
+ opts = bound.execution_options.except(:env)
240
+ merged_env.empty? ? opts : opts.merge(env: merged_env)
241
+ end
242
+
243
+ def capturing_opts(exec_opts)
244
+ opts = exec_opts
245
+ opts = opts.merge(normalize: false) unless normalize_captured_stdout?
246
+ opts = opts.merge(chomp: false) unless chomp_captured_stdout?
247
+ opts
248
+ end
249
+
250
+ # Whether {#execute_command} should apply Ruby string normalization to
251
+ # `result.stdout` on the capturing path.
252
+ #
253
+ # When `true` (the default), `command_capturing` normalizes the encoding of
254
+ # captured stdout. Override to return `false` in subclasses whose output is
255
+ # intrinsically binary (e.g. `git archive`), so that stdout bytes in
256
+ # `result.stdout` are returned unchanged.
257
+ #
258
+ # This hook only affects the **capturing** path — when an `out:` execution
259
+ # option is present, stdout is streamed directly to the caller-supplied IO
260
+ # object and is never normalized or chomped regardless of this setting.
261
+ #
262
+ # @return [Boolean]
263
+ def normalize_captured_stdout?
264
+ true
265
+ end
266
+
267
+ # Whether {#execute_command} should chomp trailing newlines from
268
+ # `result.stdout` on the capturing path.
269
+ #
270
+ # When `true` (the default), `command_capturing` strips the trailing
271
+ # newline from captured stdout. Override to return `false` in subclasses
272
+ # whose output must preserve trailing whitespace (e.g. `git show`, which
273
+ # can return blob content with significant trailing newlines).
274
+ #
275
+ # This hook only affects the **capturing** path — when an `out:` execution
276
+ # option is present, stdout is streamed directly to the caller-supplied IO
277
+ # object and is never chomped regardless of this setting.
278
+ #
279
+ # @return [Boolean]
280
+ def chomp_captured_stdout?
281
+ true
282
+ end
283
+
284
+ # Environment variable overrides to pass to the subprocess.
285
+ #
286
+ # Returns an empty hash by default. Subclasses may override this method
287
+ # to inject process-level environment changes required for correctness
288
+ # (e.g. unsetting `GIT_INDEX_FILE` for worktree management commands).
289
+ #
290
+ # The returned hash is merged with any environment overrides the caller
291
+ # supplies via the `env:` execution option — this hook's values win on
292
+ # conflict so that safety invariants cannot be accidentally overridden.
293
+ #
294
+ # @return [Hash] environment variable overrides (key: variable name, value: new value or nil to unset)
295
+ #
296
+ def env
297
+ {}
298
+ end
299
+
300
+ def allowed_exit_status_range
301
+ self.class.allowed_exit_status_range || (0..0)
302
+ end
303
+
304
+ def validate_exit_status!(result)
305
+ raise Git::FailedError, result unless allowed_exit_status_range.include?(result.status.exitstatus)
306
+ end
307
+
308
+ # Validate that the installed git version meets requirements
309
+ #
310
+ # Raises Git::VersionError if:
311
+ # 1. The installed version is below Git::MINIMUM_GIT_VERSION (floor check)
312
+ # 2. The command has a class-level constraint that isn't satisfied
313
+ #
314
+ # Floor check always runs first and fails fast.
315
+ #
316
+ def validate_version!(exec_opts = {})
317
+ return if self.class.skip_version_validation?
318
+
319
+ actual_version = @execution_context.git_version(timeout: exec_opts[:timeout])
320
+
321
+ # Floor check: fail-fast if git is too old for the gem itself
322
+ validate_floor_version!(actual_version)
323
+
324
+ # Class-level constraint check
325
+ validate_class_version_constraint!(actual_version)
326
+ end
327
+
328
+ def validate_floor_version!(actual_version)
329
+ return if actual_version >= Git::MINIMUM_GIT_VERSION
330
+
331
+ raise Git::VersionError.new(
332
+ subject: 'The git gem',
333
+ constraint: Git::VersionConstraint.new(min: Git::MINIMUM_GIT_VERSION, before: nil),
334
+ actual_version: actual_version
335
+ )
336
+ end
337
+
338
+ def validate_class_version_constraint!(actual_version)
339
+ constraint = self.class.git_version_constraint
340
+ return unless constraint
341
+ return if constraint.satisfied_by?(actual_version)
342
+
343
+ raise Git::VersionError.new(
344
+ subject: self.class,
345
+ constraint: constraint,
346
+ actual_version: actual_version
347
+ )
348
+ end
349
+
350
+ # Opens an in-memory IO pipe, spawns a background thread to write
351
+ # `content` to the write end (then close it), and immediately yields
352
+ # the read end. The write and close happen concurrently with the block.
353
+ #
354
+ # The read end can be passed as the `in:` keyword to
355
+ # {Git::ExecutionContext#command_capturing} / {Git::CommandLine#run_with_capture}, connecting it directly to
356
+ # the spawned git process's stdin without an intermediate file or shell
357
+ # heredoc. This is required because `Process.spawn` only accepts real IO
358
+ # objects with a file descriptor — `StringIO` does not work.
359
+ #
360
+ # The threaded write prevents deadlocks when `content` exceeds the OS
361
+ # pipe buffer: the subprocess can drain the pipe concurrently while the
362
+ # writer thread continues writing.
363
+ #
364
+ # Pass an empty string when the process should receive no input (e.g.
365
+ # when `--batch-all-objects` is used and git enumerates objects itself).
366
+ #
367
+ # @example Feed bound object names to a git batch command
368
+ # bound = args_definition.bind(*, **)
369
+ # stdin_content = Array(bound.objects).map { |object| "#{object}\n" }.join
370
+ # with_stdin(stdin_content) do |reader|
371
+ # @execution_context.command_capturing('cat-file', '--batch-check', in: reader, raise_on_failure: false)
372
+ # end
373
+ #
374
+ # @param content [String] text to write to the process's stdin
375
+ #
376
+ # @yield [reader [IO]] the read end of the pipe; valid only for the
377
+ # duration of the block
378
+ #
379
+ # @return [Object] the value returned by the block
380
+ #
381
+ def with_stdin(content)
382
+ reader, writer = IO.pipe
383
+ writer_thread = start_stdin_writer(content, writer)
384
+ yield reader
385
+ ensure
386
+ reader.close unless reader.closed?
387
+ writer_thread&.join
388
+ end
389
+
390
+ # Spawns a thread that writes content to writer then closes it.
391
+ # Rescues EPIPE/IOError so the thread exits cleanly when the subprocess
392
+ # closes its stdin early (e.g. on error exit before reading all input).
393
+ def start_stdin_writer(content, writer)
394
+ Thread.new do
395
+ writer.write(content) unless content.empty?
396
+ rescue Errno::EPIPE, IOError
397
+ nil # subprocess closed stdin early
398
+ ensure
399
+ writer.close unless writer.closed?
400
+ end
401
+ end
402
+ end
403
+ end
404
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/base'
4
+
5
+ module Git
6
+ module Commands
7
+ module Branch
8
+ # Implements the `git branch --copy` command for copying branches
9
+ #
10
+ # This command copies a branch, together with its config and reflog.
11
+ # If the old branch name is omitted, copies the current branch.
12
+ #
13
+ # @example Copy the current branch
14
+ # copy = Git::Commands::Branch::Copy.new(execution_context)
15
+ # copy.call('new-branch-name')
16
+ #
17
+ # @example Copy a specific branch
18
+ # copy = Git::Commands::Branch::Copy.new(execution_context)
19
+ # copy.call('old-branch-name', 'new-branch-name')
20
+ #
21
+ # @example Force copy (overwrite existing branch)
22
+ # copy = Git::Commands::Branch::Copy.new(execution_context)
23
+ # copy.call('old-branch', 'existing-branch', force: true)
24
+ #
25
+ # @note `arguments` block audited against https://git-scm.com/docs/git-branch/2.53.0
26
+ #
27
+ # @see Git::Commands::Branch
28
+ #
29
+ # @see https://git-scm.com/docs/git-branch git-branch
30
+ #
31
+ # @api private
32
+ #
33
+ class Copy < Git::Commands::Base
34
+ # NOTE: The positional arguments follow Ruby semantics:
35
+ # - When one positional is provided, it fills new_branch (required)
36
+ # - When two positionals are provided, they fill old_branch and new_branch
37
+ #
38
+ # This matches the git CLI: `git branch -c [<old-branch>] <new-branch>`
39
+ arguments do
40
+ literal 'branch'
41
+ literal '--copy'
42
+ flag_option %i[force f]
43
+
44
+ end_of_options
45
+
46
+ operand :old_branch
47
+ operand :new_branch, required: true
48
+ end
49
+
50
+ # @!method call(*, **)
51
+ #
52
+ # Execute the git branch --copy command to copy a branch
53
+ #
54
+ # @overload call(new_branch, **options)
55
+ #
56
+ # Copies the current branch to the new_branch
57
+ #
58
+ # @param new_branch [String] the new name for the copied branch
59
+ #
60
+ # @param options [Hash] command options
61
+ #
62
+ # @option options [Boolean, nil] :force (nil) allow copying even if new_branch already exists
63
+ #
64
+ # Alias: :f
65
+ #
66
+ # @return [Git::CommandLineResult] the result of calling `git branch --copy`
67
+ #
68
+ # @raise [ArgumentError] if unsupported options are provided
69
+ #
70
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
71
+ #
72
+ # @overload call(old_branch, new_branch, **options)
73
+ #
74
+ # Copies old_branch to new_branch
75
+ #
76
+ # @param old_branch [String] branch to copy from
77
+ #
78
+ # @param new_branch [String] the new name for the copied branch
79
+ #
80
+ # @param options [Hash] command options
81
+ #
82
+ # @option options [Boolean, nil] :force (nil) allow copying even if new_branch already exists
83
+ #
84
+ # Alias: :f
85
+ #
86
+ # @return [Git::CommandLineResult] the result of calling `git branch --copy`
87
+ #
88
+ # @raise [ArgumentError] if unsupported options are provided
89
+ #
90
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/base'
4
+
5
+ module Git
6
+ module Commands
7
+ module Branch
8
+ # `git branch` command for creating new branches
9
+ #
10
+ # This command creates a new branch head pointing to the current HEAD
11
+ # or a specified start point.
12
+ #
13
+ # @example Basic branch creation
14
+ # create = Git::Commands::Branch::Create.new(execution_context)
15
+ # create.call('feature-branch')
16
+ #
17
+ # @example Create branch from a specific start point
18
+ # create = Git::Commands::Branch::Create.new(execution_context)
19
+ # create.call('feature-branch', 'main')
20
+ #
21
+ # @example Force create (reset existing branch)
22
+ # create = Git::Commands::Branch::Create.new(execution_context)
23
+ # create.call('feature-branch', 'main', force: true)
24
+ #
25
+ # @example Create with upstream tracking
26
+ # create = Git::Commands::Branch::Create.new(execution_context)
27
+ # create.call('feature-branch', 'origin/main', track: true)
28
+ #
29
+ # @example Create without upstream tracking
30
+ # create = Git::Commands::Branch::Create.new(execution_context)
31
+ # create.call('feature-branch', 'origin/main', no_track: true)
32
+ #
33
+ # @example Create with inherited tracking configuration
34
+ # create = Git::Commands::Branch::Create.new(execution_context)
35
+ # create.call('feature-branch', 'origin/main', track: 'inherit')
36
+ #
37
+ # @note `arguments` block audited against https://git-scm.com/docs/git-branch/2.53.0
38
+ #
39
+ # @see Git::Commands::Branch
40
+ #
41
+ # @see https://git-scm.com/docs/git-branch git-branch
42
+ #
43
+ # @api private
44
+ #
45
+ class Create < Git::Commands::Base
46
+ arguments do
47
+ literal 'branch'
48
+ flag_or_value_option %i[track t], negatable: true, inline: true
49
+ flag_option %i[force f]
50
+ flag_option :recurse_submodules
51
+ flag_option %i[quiet q]
52
+ flag_option :create_reflog, negatable: true
53
+ end_of_options
54
+ operand :branch_name, required: true
55
+ operand :start_point
56
+ end
57
+
58
+ # @!method call(*, **)
59
+ #
60
+ # @overload call(branch_name, **options)
61
+ #
62
+ # Create a new branch from the current HEAD
63
+ #
64
+ # @param branch_name [String] the name of the branch to create
65
+ #
66
+ # @param options [Hash] command options
67
+ #
68
+ # @option options [Boolean, String, nil] :track (nil)
69
+ # configure upstream tracking for the new branch
70
+ #
71
+ # - `true`: Set up tracking using the start-point branch itself (`--track`)
72
+ # - `'direct'`: Same as `true`, explicitly use start-point as upstream (`--track=direct`)
73
+ # - `'inherit'`: Copy upstream configuration from start-point branch (`--track=inherit`)
74
+ #
75
+ # Alias: :t
76
+ #
77
+ # @option options [Boolean, nil] :no_track (nil)
78
+ # do not set up tracking even if `branch.autoSetupMerge` is set (`--no-track`)
79
+ #
80
+ # @option options [Boolean, nil] :force (nil)
81
+ # reset the branch to start point even if it already exists
82
+ #
83
+ # Without this, git branch refuses to change an existing branch.
84
+ #
85
+ # Alias: :f
86
+ #
87
+ # @option options [Boolean, nil] :recurse_submodules (nil)
88
+ # create the branch in the superproject and all submodules
89
+ #
90
+ # This is an experimental feature.
91
+ #
92
+ # @option options [Boolean, nil] :quiet (nil)
93
+ # suppress informational messages
94
+ #
95
+ # Alias: :q
96
+ #
97
+ # @option options [Boolean, nil] :create_reflog (nil)
98
+ # create the branch's reflog (`--create-reflog`)
99
+ #
100
+ # Enables date-based sha1 expressions such as `branch@{yesterday}`.
101
+ # In non-bare repositories, reflogs are usually enabled by default
102
+ # via `core.logAllRefUpdates`.
103
+ #
104
+ # @option options [Boolean, nil] :no_create_reflog (nil)
105
+ # forcibly disable the branch's reflog (`--no-create-reflog`)
106
+ #
107
+ # @return [Git::CommandLineResult] the result of calling `git branch`
108
+ #
109
+ # @raise [ArgumentError] if unsupported options are provided
110
+ #
111
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
112
+ #
113
+ # @overload call(branch_name, start_point, **options)
114
+ #
115
+ # Create a new branch from the specified start point
116
+ #
117
+ # @param branch_name [String] the name of the branch to create
118
+ #
119
+ # @param start_point [String, nil] the commit, branch, or tag to
120
+ # start the new branch from
121
+ #
122
+ # Can also use `<rev-A>...<rev-B>` syntax for merge base.
123
+ #
124
+ # @param options [Hash] command options
125
+ #
126
+ # @option options [Boolean, String, nil] :track (nil)
127
+ # configure upstream tracking for the new branch
128
+ #
129
+ # - `true`: Set up tracking using the start-point branch itself (`--track`)
130
+ # - `'direct'`: Same as `true`, explicitly use start-point as upstream (`--track=direct`)
131
+ # - `'inherit'`: Copy upstream configuration from start-point branch (`--track=inherit`)
132
+ #
133
+ # Alias: :t
134
+ #
135
+ # @option options [Boolean, nil] :no_track (nil)
136
+ # do not set up tracking even if `branch.autoSetupMerge` is set (`--no-track`)
137
+ #
138
+ # @option options [Boolean, nil] :force (nil)
139
+ # reset the branch to start point even if it already exists
140
+ #
141
+ # Without this, git branch refuses to change an existing branch.
142
+ #
143
+ # Alias: :f
144
+ #
145
+ # @option options [Boolean, nil] :recurse_submodules (nil)
146
+ # create the branch in the superproject and all submodules
147
+ #
148
+ # This is an experimental feature.
149
+ #
150
+ # @option options [Boolean, nil] :quiet (nil)
151
+ # suppress informational messages
152
+ #
153
+ # Alias: :q
154
+ #
155
+ # @option options [Boolean, nil] :create_reflog (nil)
156
+ # create the branch's reflog (`--create-reflog`)
157
+ #
158
+ # Enables date-based sha1 expressions such as `branch@{yesterday}`.
159
+ # In non-bare repositories, reflogs are usually enabled by default
160
+ # via `core.logAllRefUpdates`.
161
+ #
162
+ # @option options [Boolean, nil] :no_create_reflog (nil)
163
+ # forcibly disable the branch's reflog (`--no-create-reflog`)
164
+ #
165
+ # @return [Git::CommandLineResult] the result of calling `git branch`
166
+ #
167
+ # @raise [ArgumentError] if unsupported options are provided
168
+ #
169
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
170
+ end
171
+ end
172
+ end
173
+ end