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,502 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/commands/arguments'
4
+ require 'git/version'
5
+ require 'git/version_constraint'
6
+
7
+ module Git
8
+ module Commands
9
+ # @api private
10
+ #
11
+ # Base class for git command implementations.
12
+ #
13
+ # Provides default {#initialize} and {#call} methods so that simple commands
14
+ # only need to declare their arguments:
15
+ #
16
+ # class Add < Git::Commands::Base
17
+ # arguments do
18
+ # literal 'add'
19
+ # flag_option :all
20
+ # flag_option :force
21
+ # end_of_options
22
+ # operand :paths, repeatable: true
23
+ # end
24
+ #
25
+ # # Execute the git add command
26
+ # # ...YARD docs...
27
+ # def call(...) = super
28
+ # end
29
+ #
30
+ # Commands whose git process may exit with a non-zero status that is
31
+ # *not* an error can declare the acceptable range of exit codes:
32
+ #
33
+ # class Delete < Git::Commands::Base
34
+ # arguments do
35
+ # literal 'branch'
36
+ # literal '--delete'
37
+ # operand :branch_names, repeatable: true, required: true
38
+ # end
39
+ #
40
+ # allow_exit_status 0..1
41
+ #
42
+ # # Execute the git branch --delete command
43
+ # # ...YARD docs...
44
+ # def call(...) = super
45
+ # end
46
+ #
47
+ # Commands with execution options (e.g., timeout) work with the default
48
+ # `call` — execution options are extracted and forwarded automatically.
49
+ class Base # rubocop:disable Metrics/ClassLength
50
+ class << self
51
+ # @return [Git::Commands::Arguments, nil] the frozen argument definition for this command
52
+ attr_reader :args_definition
53
+
54
+ # Define the command's arguments using the {Arguments} DSL.
55
+ #
56
+ # @return [void]
57
+ #
58
+ # @raise [ArgumentError] if called more than once on the same class
59
+ #
60
+ # @yield the block passed to {Arguments.define}
61
+ #
62
+ def arguments(&)
63
+ raise ArgumentError, "arguments already defined for #{name}" if @args_definition
64
+
65
+ @args_definition = Arguments.define(&).freeze
66
+ end
67
+
68
+ # @return [Range, nil] range of exit status values accepted by this command
69
+ attr_reader :allowed_exit_status_range
70
+
71
+ # Declare the acceptable range of exit status values for this command.
72
+ #
73
+ # @example git-diff exits 1 when a diff is found (not an error)
74
+ # allow_exit_status 0..1
75
+ #
76
+ # @example git-fsck uses exit codes 0-7 as bit flags
77
+ # allow_exit_status 0..7
78
+ #
79
+ # @param range [Range] range of accepted exit status values
80
+ #
81
+ # @return [void]
82
+ #
83
+ # @raise [ArgumentError] if range is invalid
84
+ #
85
+ def allow_exit_status(range)
86
+ raise ArgumentError, 'allow_exit_status expects a Range' unless range.is_a?(Range)
87
+ unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
88
+ raise ArgumentError, 'allow_exit_status bounds must be Integers'
89
+ end
90
+
91
+ raise ArgumentError, 'allow_exit_status range must not be empty' if range.begin > range.end
92
+
93
+ @allowed_exit_status_range = range
94
+ end
95
+
96
+ # @return [Git::VersionConstraint, nil] version constraint for this command
97
+ #
98
+ # Returns +nil+ if the command is available in all supported git versions.
99
+ attr_reader :git_version_constraint
100
+
101
+ # @!attribute [r] skip_version_validation?
102
+ #
103
+ # @return [Boolean] whether this command skips version validation
104
+ def skip_version_validation? = !!@skip_version_validation
105
+
106
+ # Declare that this command should skip version validation.
107
+ #
108
+ # This is intended for internal use only — specifically for the
109
+ # `git version` command, which cannot validate versions without
110
+ # causing infinite recursion.
111
+ #
112
+ # @return [void]
113
+ #
114
+ # @api private
115
+ #
116
+ def skip_version_validation
117
+ @skip_version_validation = true
118
+ end
119
+
120
+ # Declare the git version requirements for this command.
121
+ #
122
+ # Use this when the command (or the specific sub-action this class wraps) was
123
+ # introduced in a git version later than +Git::MINIMUM_GIT_VERSION+, or when
124
+ # the command was removed in a later version. When not declared, the command
125
+ # is assumed to be available in all supported git versions.
126
+ #
127
+ # @example git-am --retry requires git 2.46.0 or later
128
+ # requires_git_version '2.46.0'
129
+ #
130
+ # @example Feature with version range
131
+ # requires_git_version '2.29.0', before: '2.50.0'
132
+ #
133
+ # @example Feature available before git 2.50.0
134
+ # requires_git_version before: '2.50.0'
135
+ #
136
+ # @param min [String, nil] minimum version
137
+ #
138
+ # @param before [String, nil] upper bound version (exclusive)
139
+ #
140
+ # @return [void]
141
+ #
142
+ # @raise [ArgumentError] if version format is invalid or called twice
143
+ #
144
+ def requires_git_version(min = nil, before: nil)
145
+ raise ArgumentError, 'requires_git_version already declared for this class' if @git_version_constraint
146
+
147
+ @git_version_constraint = normalize_version_constraint(min, before)
148
+ end
149
+
150
+ private
151
+
152
+ # Normalize a version constraint to a VersionConstraint
153
+ #
154
+ # @param min [String, nil] minimum version
155
+ #
156
+ # @param before_version [String, nil] upper bound version
157
+ #
158
+ # @return [Git::VersionConstraint] the normalized constraint
159
+ #
160
+ # @raise [ArgumentError] if the constraint is invalid
161
+ #
162
+ def normalize_version_constraint(min, before_version)
163
+ raise ArgumentError, 'requires_git_version requires min or before:' unless min || before_version
164
+
165
+ min_version = min ? parse_version(min, 'min') : nil
166
+ before_parsed = before_version ? parse_version(before_version, 'before:') : nil
167
+
168
+ Git::VersionConstraint.new(min: min_version, before: before_parsed)
169
+ end
170
+
171
+ # Parse a version string into a Git version object
172
+ #
173
+ # @param version [String] version string to parse
174
+ #
175
+ # @param key_name [String] argument name used in validation errors
176
+ #
177
+ # @return [Git::Version] the parsed version
178
+ #
179
+ # @raise [ArgumentError] if the version format is invalid
180
+ #
181
+ def parse_version(version, key_name)
182
+ validate_version_format!(version, key_name)
183
+ Git::Version.parse(version)
184
+ end
185
+
186
+ # Validate a version string uses the required format
187
+ #
188
+ # @param version [Object] value to validate
189
+ #
190
+ # @param context [String, nil] argument name included in validation errors
191
+ #
192
+ # @return [void]
193
+ #
194
+ # @raise [ArgumentError] if the value is not a `major.minor.patch` version string
195
+ #
196
+ def validate_version_format!(version, context = nil)
197
+ return if version.is_a?(String) && version.match?(/\A\d+\.\d+\.\d+\z/)
198
+
199
+ subject = context || 'a version'
200
+ raise ArgumentError,
201
+ "requires_git_version expects #{subject} to be a 'major.minor.patch' version string, " \
202
+ "got: #{version.inspect}"
203
+ end
204
+ end
205
+
206
+ # @param execution_context [Git::ExecutionContext] context that provides
207
+ # {Git::ExecutionContext#command_capturing} and {Git::ExecutionContext#command_streaming}
208
+ def initialize(execution_context)
209
+ @execution_context = execution_context
210
+ end
211
+
212
+ # Execute the git command.
213
+ #
214
+ # @overload call(*args, **kwargs)
215
+ # Bind arguments and execute the command.
216
+ #
217
+ # Execution options (declared via `execution_option` in the Arguments
218
+ # DSL) are extracted from the bound arguments via
219
+ # {Git::Commands::Arguments::Bound#execution_options} and forwarded as
220
+ # keyword arguments to the execution context via {#execute_command}.
221
+ #
222
+ # When the `:out` execution option is present, stdout is streamed using
223
+ # `@execution_context.command_streaming`. Otherwise, stdout is captured
224
+ # using `@execution_context.command_capturing`.
225
+ #
226
+ # @example
227
+ # # In a command subclass:
228
+ # # result = command.call('HEAD', timeout: 10)
229
+ #
230
+ # @param args [Array] positional arguments forwarded to {Arguments#bind}
231
+ #
232
+ # @param kwargs [Hash] keyword arguments forwarded to {Arguments#bind}
233
+ #
234
+ # @return [Git::CommandLine::Result] the result of calling `git`
235
+ #
236
+ # @raise [ArgumentError] if no arguments definition is declared on the command class
237
+ #
238
+ # @raise [Git::FailedError] if git returns an exit code outside the allowed range
239
+ #
240
+ # @raise [Git::VersionError] if the installed git version doesn't meet requirements
241
+ def call(*, **)
242
+ bound = args_definition.bind(*, **)
243
+ validate_version!(bound.execution_options)
244
+ result = execute_command(bound)
245
+ validate_exit_status!(result)
246
+ result
247
+ end
248
+
249
+ private
250
+
251
+ # Return the argument definition declared by this command class
252
+ #
253
+ # @return [Git::Commands::Arguments] the argument definition
254
+ #
255
+ # @raise [ArgumentError] if no argument definition is declared
256
+ #
257
+ def args_definition
258
+ self.class.args_definition || raise(ArgumentError, "arguments not defined for #{self.class.name}")
259
+ end
260
+
261
+ # Execute a bound git command through the execution context
262
+ #
263
+ # @param bound [Git::Commands::Arguments::Bound] bound command arguments
264
+ #
265
+ # @return [Git::CommandLine::Result] the command result
266
+ #
267
+ def execute_command(bound)
268
+ exec_opts = execution_opts(bound)
269
+
270
+ if exec_opts.key?(:out)
271
+ @execution_context.command_streaming(*bound, **exec_opts, raise_on_failure: false)
272
+ else
273
+ @execution_context.command_capturing(*bound, **capturing_opts(exec_opts), raise_on_failure: false)
274
+ end
275
+ end
276
+
277
+ # Build execution options for a bound command
278
+ #
279
+ # @param bound [Git::Commands::Arguments::Bound] bound command arguments
280
+ #
281
+ # @return [Hash] execution options for the command line
282
+ #
283
+ def execution_opts(bound)
284
+ caller_env = bound.execution_options.fetch(:env, {}) || {}
285
+ merged_env = caller_env.merge(env)
286
+ opts = bound.execution_options.except(:env)
287
+ merged_env.empty? ? opts : opts.merge(env: merged_env)
288
+ end
289
+
290
+ # Build options for captured command output
291
+ #
292
+ # @param exec_opts [Hash] execution options for the command line
293
+ #
294
+ # @return [Hash] options for captured command output
295
+ #
296
+ def capturing_opts(exec_opts)
297
+ opts = exec_opts
298
+ opts = opts.merge(normalize: false) unless normalize_captured_stdout?
299
+ opts = opts.merge(chomp: false) unless chomp_captured_stdout?
300
+ opts
301
+ end
302
+
303
+ # Whether {#execute_command} should apply Ruby string normalization to
304
+ # `result.stdout` on the capturing path.
305
+ #
306
+ # When `true` (the default), `command_capturing` normalizes the encoding of
307
+ # captured stdout. Override to return `false` in subclasses whose output is
308
+ # intrinsically binary (e.g. `git archive`), so that stdout bytes in
309
+ # `result.stdout` are returned unchanged.
310
+ #
311
+ # This hook only affects the **capturing** path — when an `out:` execution
312
+ # option is present, stdout is streamed directly to the caller-supplied IO
313
+ # object and is never normalized or chomped regardless of this setting.
314
+ #
315
+ # @return [Boolean]
316
+ def normalize_captured_stdout?
317
+ true
318
+ end
319
+
320
+ # Whether {#execute_command} should chomp trailing newlines from
321
+ # `result.stdout` on the capturing path.
322
+ #
323
+ # When `true` (the default), `command_capturing` strips the trailing
324
+ # newline from captured stdout. Override to return `false` in subclasses
325
+ # whose output must preserve trailing whitespace (e.g. `git show`, which
326
+ # can return blob content with significant trailing newlines).
327
+ #
328
+ # This hook only affects the **capturing** path — when an `out:` execution
329
+ # option is present, stdout is streamed directly to the caller-supplied IO
330
+ # object and is never chomped regardless of this setting.
331
+ #
332
+ # @return [Boolean]
333
+ def chomp_captured_stdout?
334
+ true
335
+ end
336
+
337
+ # Environment variable overrides to pass to the subprocess.
338
+ #
339
+ # Returns an empty hash by default. Subclasses may override this method
340
+ # to inject process-level environment changes required for correctness
341
+ # (e.g. unsetting `GIT_INDEX_FILE` for worktree management commands).
342
+ #
343
+ # The returned hash is merged with any environment overrides the caller
344
+ # supplies via the `env:` execution option — this hook's values win on
345
+ # conflict so that safety invariants cannot be accidentally overridden.
346
+ #
347
+ # @return [Hash] environment variable overrides (key: variable name, value: new value or nil to unset)
348
+ #
349
+ def env
350
+ {}
351
+ end
352
+
353
+ # Return the allowed exit status range for this command
354
+ #
355
+ # @return [Range] range of accepted exit status values
356
+ #
357
+ def allowed_exit_status_range
358
+ self.class.allowed_exit_status_range || (0..0)
359
+ end
360
+
361
+ # Validate that a command result has an accepted exit status
362
+ #
363
+ # @param result [Git::CommandLine::Result] command result to validate
364
+ #
365
+ # @return [void]
366
+ #
367
+ # @raise [Git::FailedError] if the result exit status is not accepted
368
+ #
369
+ def validate_exit_status!(result)
370
+ raise Git::FailedError, result unless allowed_exit_status_range.include?(result.status.exitstatus)
371
+ end
372
+
373
+ # Validate that the installed git version meets requirements
374
+ #
375
+ # Raises Git::VersionError if:
376
+ # 1. The installed version is below Git::MINIMUM_GIT_VERSION (floor check)
377
+ # 2. The command has a class-level constraint that isn't satisfied
378
+ #
379
+ # Floor check always runs first and fails fast.
380
+ #
381
+ # @param exec_opts [Hash] execution options used to query git version
382
+ #
383
+ # @return [void]
384
+ #
385
+ # @raise [Git::VersionError] if the installed git version is unsupported
386
+ #
387
+ def validate_version!(exec_opts = {})
388
+ return if self.class.skip_version_validation?
389
+
390
+ actual_version = @execution_context.git_version(timeout: exec_opts[:timeout])
391
+
392
+ # Floor check: fail-fast if git is too old for the gem itself
393
+ validate_floor_version!(actual_version)
394
+
395
+ # Class-level constraint check
396
+ validate_class_version_constraint!(actual_version)
397
+ end
398
+
399
+ # Validate that git satisfies the gem-wide minimum version
400
+ #
401
+ # @param actual_version [Git::Version] installed git version
402
+ #
403
+ # @return [void]
404
+ #
405
+ # @raise [Git::VersionError] if the installed git version is too old
406
+ #
407
+ def validate_floor_version!(actual_version)
408
+ return if actual_version >= Git::MINIMUM_GIT_VERSION
409
+
410
+ raise Git::VersionError.new(
411
+ subject: 'The git gem',
412
+ constraint: Git::VersionConstraint.new(min: Git::MINIMUM_GIT_VERSION, before: nil),
413
+ actual_version: actual_version
414
+ )
415
+ end
416
+
417
+ # Validate that git satisfies this command class's version constraint
418
+ #
419
+ # @param actual_version [Git::Version] installed git version
420
+ #
421
+ # @return [void]
422
+ #
423
+ # @raise [Git::VersionError] if the installed git version is unsupported
424
+ #
425
+ def validate_class_version_constraint!(actual_version)
426
+ constraint = self.class.git_version_constraint
427
+ return unless constraint
428
+ return if constraint.satisfied_by?(actual_version)
429
+
430
+ raise Git::VersionError.new(
431
+ subject: self.class,
432
+ constraint: constraint,
433
+ actual_version: actual_version
434
+ )
435
+ end
436
+
437
+ # Opens an in-memory IO pipe, spawns a background thread to write
438
+ # `content` to the write end (then close it), and immediately yields
439
+ # the read end. The write and close happen concurrently with the block.
440
+ #
441
+ # The read end can be passed as the `in:` keyword to
442
+ # {Git::ExecutionContext#command_capturing} / {Git::CommandLine#run_with_capture}, connecting it directly to
443
+ # the spawned git process's stdin without an intermediate file or shell
444
+ # heredoc. This is required because `Process.spawn` only accepts real IO
445
+ # objects with a file descriptor — `StringIO` does not work.
446
+ #
447
+ # The threaded write prevents deadlocks when `content` exceeds the OS
448
+ # pipe buffer: the subprocess can drain the pipe concurrently while the
449
+ # writer thread continues writing.
450
+ #
451
+ # Pass an empty string when the process should receive no input (e.g.
452
+ # when `--batch-all-objects` is used and git enumerates objects itself).
453
+ #
454
+ # @example Feed bound object names to a git batch command
455
+ # bound = args_definition.bind(*args, **kwargs)
456
+ # stdin_content = Array(bound.object).map { |object| "#{object}\n" }.join
457
+ # with_stdin(stdin_content) do |reader|
458
+ # @execution_context.command_capturing('cat-file', '--batch-check', in: reader, raise_on_failure: false)
459
+ # end
460
+ #
461
+ # @param content [String] text to write to the process's stdin
462
+ #
463
+ # @return [Object] the value returned by the block
464
+ #
465
+ # @yield [reader] the read end of the pipe
466
+ #
467
+ # @yieldparam reader [IO] the read end of the pipe; valid only for the
468
+ # duration of the block
469
+ #
470
+ # @yieldreturn [Object] the block's return value, which becomes the method's return value
471
+ #
472
+ def with_stdin(content)
473
+ reader, writer = IO.pipe
474
+ writer_thread = start_stdin_writer(content, writer)
475
+ yield reader
476
+ ensure
477
+ reader.close unless reader.closed?
478
+ writer_thread&.join
479
+ end
480
+
481
+ # Spawns a thread that writes content to writer then closes it.
482
+ # Rescues EPIPE/IOError so the thread exits cleanly when the subprocess
483
+ # closes its stdin early (e.g. on error exit before reading all input).
484
+ #
485
+ # @param content [String] text to write to the pipe
486
+ #
487
+ # @param writer [IO] write end of the pipe
488
+ #
489
+ # @return [Thread] thread writing content to the pipe
490
+ #
491
+ def start_stdin_writer(content, writer)
492
+ Thread.new do
493
+ writer.write(content) unless content.empty?
494
+ rescue Errno::EPIPE, IOError
495
+ nil # subprocess closed stdin early
496
+ ensure
497
+ writer.close unless writer.closed?
498
+ end
499
+ end
500
+ end
501
+ end
502
+ end
@@ -0,0 +1,102 @@
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
+ # Executes the git branch --copy command to copy a branch
51
+ #
52
+ # @overload call(new_branch, **options)
53
+ #
54
+ # Copies the current branch to the new_branch
55
+ #
56
+ # @param new_branch [String] the new name for the copied branch
57
+ #
58
+ # @param options [Hash] command options
59
+ #
60
+ # @option options [Boolean, nil] :force (nil) allow copying even if new_branch already
61
+ # exists
62
+ #
63
+ # Alias: :f
64
+ #
65
+ # @return [Git::CommandLine::Result] the result of calling `git branch --copy`
66
+ #
67
+ # @raise [ArgumentError] if unsupported options are provided
68
+ #
69
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
70
+ #
71
+ # @api public
72
+ #
73
+ # @overload call(old_branch, new_branch, **options)
74
+ #
75
+ # Copies old_branch to new_branch
76
+ #
77
+ # @param old_branch [String] branch to copy from
78
+ #
79
+ # @param new_branch [String] the new name for the copied branch
80
+ #
81
+ # @param options [Hash] command options
82
+ #
83
+ # @option options [Boolean, nil] :force (nil) allow copying even if new_branch already
84
+ # exists
85
+ #
86
+ # Alias: :f
87
+ #
88
+ # @return [Git::CommandLine::Result] the result of calling `git branch --copy`
89
+ #
90
+ # @raise [ArgumentError] if unsupported options are provided
91
+ #
92
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
93
+ #
94
+ # @api public
95
+ #
96
+ def call(*, **)
97
+ super
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end