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