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,964 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'git/branch'
5
+ require 'git/branch_info'
6
+ require 'git/branches'
7
+ require 'git/commands/branch/create'
8
+ require 'git/commands/branch/delete'
9
+ require 'git/commands/branch/list'
10
+ require 'git/commands/branch/show_current'
11
+ require 'git/commands/checkout/branch'
12
+ require 'git/commands/checkout/files'
13
+ require 'git/commands/checkout_index'
14
+ require 'git/commands/rev_parse'
15
+ require 'git/commands/update_ref/update'
16
+ require 'git/commands/symbolic_ref/update'
17
+ require 'git/parsers/branch'
18
+ require 'git/repository/shared_private'
19
+
20
+ module Git
21
+ class Repository
22
+ # Facade methods for branching operations: creating, checking out, querying,
23
+ # deleting, and updating branches
24
+ #
25
+ # Included by {Git::Repository}.
26
+ #
27
+ # @api private
28
+ #
29
+ module Branching # rubocop:disable Metrics/ModuleLength
30
+ # Represents the state of HEAD in a repository
31
+ #
32
+ # @api private
33
+ #
34
+ # @!attribute [r] state
35
+ # @return [Symbol] one of `:active`, `:unborn`, or `:detached`
36
+ #
37
+ # @!attribute [r] name
38
+ # @return [String] the branch name, or `'HEAD'` when detached
39
+ #
40
+ HeadState = Data.define(:state, :name)
41
+
42
+ # Option keys accepted by {#checkout}
43
+ #
44
+ CHECKOUT_ALLOWED_OPTS = %i[force f new_branch b start_point orphan].freeze
45
+ private_constant :CHECKOUT_ALLOWED_OPTS
46
+
47
+ # Option keys accepted by {#checkout_index}
48
+ #
49
+ CHECKOUT_INDEX_ALLOWED_OPTS = %i[prefix force all path_limiter].freeze
50
+ private_constant :CHECKOUT_INDEX_ALLOWED_OPTS
51
+
52
+ # Returns the name of the current branch
53
+ #
54
+ # @example Get the current branch name
55
+ # repo.current_branch # => "main"
56
+ #
57
+ # @example In detached HEAD state
58
+ # repo.current_branch # => "HEAD"
59
+ #
60
+ # @return [String] the current branch name, or `'HEAD'` when in detached
61
+ # HEAD state
62
+ #
63
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
64
+ #
65
+ def current_branch
66
+ result = Git::Commands::Branch::ShowCurrent.new(@execution_context).call
67
+ name = result.stdout.strip
68
+ name.empty? ? 'HEAD' : name
69
+ end
70
+
71
+ # Returns the current HEAD state as a structured value object
72
+ #
73
+ # HEAD can be in one of three states:
74
+ #
75
+ # - **`:active`** — HEAD points to a branch ref that has at least one commit.
76
+ # - **`:unborn`** — HEAD points to a branch ref that has been created but has
77
+ # no commits yet (e.g. immediately after `git init` before any commit).
78
+ # - **`:detached`** — HEAD points directly to a commit SHA rather than a branch.
79
+ #
80
+ # @example Active branch
81
+ # repo.current_branch_state
82
+ # # => #<data Git::Repository::Branching::HeadState state=:active, name="main">
83
+ #
84
+ # @example Unborn branch (no commits yet)
85
+ # repo.current_branch_state
86
+ # # => #<data Git::Repository::Branching::HeadState state=:unborn, name="main">
87
+ #
88
+ # @example Detached HEAD
89
+ # repo.current_branch_state
90
+ # # => #<data Git::Repository::Branching::HeadState state=:detached, name="HEAD">
91
+ #
92
+ # @return [Git::Repository::Branching::HeadState] the current HEAD state
93
+ #
94
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
95
+ #
96
+ def current_branch_state
97
+ branch_name = Git::Commands::Branch::ShowCurrent.new(@execution_context).call.stdout.strip
98
+ return HeadState.new(state: :detached, name: 'HEAD') if branch_name.empty?
99
+
100
+ state = Private.get_branch_state(@execution_context, branch_name)
101
+ HeadState.new(state: state, name: branch_name)
102
+ end
103
+
104
+ # Restore working tree files from a tree-ish
105
+ #
106
+ # @example Restore README.md to its HEAD state
107
+ # repo.checkout_file('HEAD', 'README.md')
108
+ #
109
+ # @param version [String] the tree-ish (branch, tag, commit SHA, etc.) to
110
+ # restore the file from
111
+ #
112
+ # @param file [String] the path to the file to restore
113
+ #
114
+ # @return [String] git's stdout from the checkout
115
+ #
116
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
117
+ #
118
+ def checkout_file(version, file)
119
+ Git::Commands::Checkout::Files.new(@execution_context).call(version, pathspec: [file]).stdout
120
+ end
121
+
122
+ # Switch branches or restore working tree files
123
+ #
124
+ # @example Check out an existing branch
125
+ # repo.checkout('main')
126
+ #
127
+ # @example Create and check out a new branch from main
128
+ # repo.checkout('new-feature', new_branch: true, start_point: 'main')
129
+ #
130
+ # @example Create a new branch with a name different from the start point
131
+ # repo.checkout('main', new_branch: 'new-feature')
132
+ #
133
+ # @example Create and check out an unborn branch with no history
134
+ # repo.checkout('gh-pages', orphan: true)
135
+ #
136
+ # @example Force checkout discarding local changes
137
+ # repo.checkout('main', force: true)
138
+ #
139
+ # @param branch [String, nil] the branch to check out; defaults to nil
140
+ # (i.e. restore HEAD state)
141
+ #
142
+ # @param opts [Hash] options for the checkout command
143
+ #
144
+ # @option opts [Boolean, nil] :force (nil) discard local changes when
145
+ # switching branches
146
+ #
147
+ # @option opts [Boolean, String, nil] :new_branch (nil) when `true`,
148
+ # creates a new branch named `branch` from `:start_point`
149
+ #
150
+ # When a `String`, creates a new branch with that name, using `branch`
151
+ # as the start point.
152
+ #
153
+ # @option opts [Boolean, String, nil] :b (nil) alias for `:new_branch`
154
+ #
155
+ # @option opts [Boolean, nil] :f (nil) alias for `:force`
156
+ #
157
+ # @option opts [Boolean, String, nil] :orphan (nil) when `true`, creates a
158
+ # new unborn branch named `branch` whose first commit has no parents
159
+ #
160
+ # When a `String`, creates an unborn branch with that name, using
161
+ # `branch` as the start point for the working tree and index.
162
+ #
163
+ # `false` and `nil` are both treated as unset. A blank branch name is
164
+ # rejected rather than ignored.
165
+ #
166
+ # @option opts [String, nil] :start_point (nil) the commit or branch to
167
+ # start the new branch from; used together with `new_branch: true` or
168
+ # `orphan: true`
169
+ #
170
+ # @return [String] git's stdout from the checkout
171
+ #
172
+ # @raise [ArgumentError] if unsupported options are provided
173
+ #
174
+ # @raise [ArgumentError] if `:orphan` is given a blank or missing branch name
175
+ #
176
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
177
+ #
178
+ def checkout(branch = nil, opts = {})
179
+ if branch.is_a?(Hash) && opts.empty?
180
+ opts = branch
181
+ branch = nil
182
+ end
183
+
184
+ SharedPrivate.assert_valid_opts!(CHECKOUT_ALLOWED_OPTS, **opts)
185
+
186
+ target, translated_opts = Private.translate_checkout_opts(branch, opts)
187
+ Git::Commands::Checkout::Branch.new(@execution_context).call(target, **translated_opts).stdout
188
+ end
189
+
190
+ # Run a block with the given branch checked out, then restore the original branch
191
+ #
192
+ # Records the current branch (or the current commit when HEAD is detached),
193
+ # checks out `branch`, and yields to the block. If the block returns a truthy
194
+ # value, all pending changes are committed with `message` (see
195
+ # {#commit_all}); if it returns a falsy value, the index and working tree are
196
+ # hard-reset instead (see {#reset}). The original branch or commit is then
197
+ # checked out again. The hard reset discards changes to tracked files only;
198
+ # untracked files created by the block are left in place.
199
+ #
200
+ # Unlike `Git::Branch#in_branch`, this method does not create `branch`. The
201
+ # branch must be an existing local branch. Unlike {#checkout}, a commit SHA,
202
+ # tag, or remote-tracking branch is rejected before any checkout happens:
203
+ # those detach HEAD, and a commit made there would be left dangling once the
204
+ # original branch is restored. HEAD must
205
+ # be on a branch with at least one commit, or detached: an unborn branch (no
206
+ # commits yet) cannot be checked out again by name, so it is rejected before
207
+ # any checkout happens.
208
+ #
209
+ # **Note:** the restore checkout is not wrapped in `ensure`. If the block,
210
+ # the commit, or the reset raises an exception, the repository is left
211
+ # checked out on `branch` rather than restored to the original branch.
212
+ #
213
+ # @example Commit a new file on a feature branch
214
+ # repo.in_branch('feature', 'Add README') do
215
+ # File.write('README.md', '# Hello')
216
+ # repo.add('README.md')
217
+ # true # commit and return to the original branch
218
+ # end
219
+ #
220
+ # @example Discard experimental changes to a tracked file
221
+ # repo.in_branch('scratch') do
222
+ # File.write('README.md', '# Try something')
223
+ # false # hard-reset and return to the original branch
224
+ # end
225
+ #
226
+ # @param branch [String] the name of an existing local branch to check out
227
+ #
228
+ # @param message [String] the commit message used when the block returns a
229
+ # truthy value
230
+ #
231
+ # @return [String] git's stdout from the final checkout back to the original
232
+ # branch or commit
233
+ #
234
+ # @raise [ArgumentError] if `branch` is not an existing local branch
235
+ #
236
+ # @raise [Git::Error] if HEAD is on an unborn branch
237
+ #
238
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
239
+ #
240
+ # @yield executes the block with `branch` checked out
241
+ #
242
+ # @yieldreturn [Object] a truthy value to commit all changes, a falsy value to
243
+ # hard-reset
244
+ #
245
+ def in_branch(branch, message = 'in branch work')
246
+ SharedPrivate.assert_local_branch!(self, branch)
247
+ restore_point = SharedPrivate.head_restore_point(self)
248
+ checkout(branch)
249
+ if yield
250
+ commit_all(message)
251
+ else
252
+ reset(nil, hard: true)
253
+ end
254
+ checkout(restore_point)
255
+ end
256
+
257
+ # Populate the working tree from the index
258
+ #
259
+ # @example Check out all files from the index
260
+ # repo.checkout_index(all: true)
261
+ #
262
+ # @example Force check out a specific file
263
+ # repo.checkout_index(force: true, path_limiter: 'README.md')
264
+ #
265
+ # @example Check out files to a staging prefix
266
+ # repo.checkout_index(prefix: 'tmp/stage/', all: true)
267
+ #
268
+ # @param options [Hash] options for the checkout-index command
269
+ #
270
+ # @option options [Boolean, nil] :all (nil) check out all files in the index
271
+ #
272
+ # @option options [Boolean, nil] :force (nil) overwrite existing files
273
+ #
274
+ # @option options [String, nil] :prefix (nil) write files under this path prefix
275
+ # rather than the working directory root
276
+ #
277
+ # @option options [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
278
+ # limit the check out to the given path(s)
279
+ #
280
+ # @return [String] git's stdout from the checkout-index command
281
+ #
282
+ # @raise [ArgumentError] if unsupported options are provided
283
+ #
284
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
285
+ #
286
+ def checkout_index(options = {})
287
+ SharedPrivate.assert_valid_opts!(CHECKOUT_INDEX_ALLOWED_OPTS, **options)
288
+
289
+ paths = Private.normalize_pathspecs(options[:path_limiter], 'path_limiter')
290
+ keyword_opts = options.except(:path_limiter)
291
+ Git::Commands::CheckoutIndex.new(@execution_context).call(*paths.to_a, **keyword_opts).stdout
292
+ end
293
+
294
+ # Returns `true` if the named branch exists as a local branch
295
+ #
296
+ # @example Check whether main exists locally
297
+ # repo.local_branch?('main') # => true
298
+ #
299
+ # @param branch [String] the local branch name to look up
300
+ #
301
+ # @return [Boolean] `true` if the branch exists locally, `false` otherwise
302
+ #
303
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
304
+ #
305
+ def local_branch?(branch)
306
+ result = Git::Commands::Branch::List.new(@execution_context).call(branch, format: '%(refname:short)')
307
+ result.stdout.chomp == branch
308
+ end
309
+
310
+ # Returns `true` if the named branch exists as a remote-tracking branch
311
+ #
312
+ # The `branch` argument must be the **short branch name** (e.g. `'master'`),
313
+ # not the combined `remote/branch` form (e.g. `'origin/master'`).
314
+ #
315
+ # @example Check whether master exists on any remote
316
+ # repo.remote_branch?('master') # => true
317
+ #
318
+ # @param branch [String] the short branch name to look up across all remotes
319
+ #
320
+ # @return [Boolean] `true` if a remote-tracking branch with that short name
321
+ # exists, `false` otherwise
322
+ #
323
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
324
+ #
325
+ def remote_branch?(branch)
326
+ result = Git::Commands::Branch::List.new(@execution_context)
327
+ .call("*/#{branch}", remotes: true, format: '%(refname:lstrip=3)')
328
+ result.stdout.each_line.any? { |line| line.chomp == branch }
329
+ end
330
+
331
+ # Returns `true` if the named branch exists locally or as a remote-tracking branch
332
+ #
333
+ # @example Check whether main exists anywhere
334
+ # repo.branch?('main') # => true
335
+ #
336
+ # @param branch [String] the branch name to look up
337
+ #
338
+ # @return [Boolean] `true` if the branch exists locally or remotely,
339
+ # `false` otherwise
340
+ #
341
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
342
+ #
343
+ def branch?(branch)
344
+ local_branch?(branch) || remote_branch?(branch)
345
+ end
346
+
347
+ # Checks whether the named branch exists locally
348
+ #
349
+ # @example Check whether main exists locally
350
+ # repo.is_local_branch?('main') # => true
351
+ #
352
+ # @param branch [String] the local branch name to look up
353
+ #
354
+ # @return [Boolean] `true` if the branch exists locally, `false` otherwise
355
+ #
356
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
357
+ #
358
+ # @deprecated use {#local_branch?} instead
359
+ #
360
+ def is_local_branch?(branch) # rubocop:disable Naming/PredicatePrefix
361
+ Git::Deprecation.warn(
362
+ 'Git::Repository#is_local_branch? is deprecated and will be removed in v6.0.0. ' \
363
+ 'Use Git::Repository#local_branch? instead.'
364
+ )
365
+ local_branch?(branch)
366
+ end
367
+
368
+ # Checks whether the named branch exists as a remote-tracking branch
369
+ #
370
+ # @example Check whether master exists on any remote
371
+ # repo.is_remote_branch?('master') # => true
372
+ #
373
+ # @param branch [String] the short branch name to look up across all remotes
374
+ #
375
+ # @return [Boolean] `true` if a remote-tracking branch with that short name
376
+ # exists, `false` otherwise
377
+ #
378
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
379
+ #
380
+ # @deprecated use {#remote_branch?} instead
381
+ #
382
+ def is_remote_branch?(branch) # rubocop:disable Naming/PredicatePrefix
383
+ Git::Deprecation.warn(
384
+ 'Git::Repository#is_remote_branch? is deprecated and will be removed in v6.0.0. ' \
385
+ 'Use Git::Repository#remote_branch? instead.'
386
+ )
387
+ remote_branch?(branch)
388
+ end
389
+
390
+ # Checks whether the named branch exists locally or as a remote-tracking branch
391
+ #
392
+ # @example Check whether main exists anywhere
393
+ # repo.is_branch?('main') # => true
394
+ #
395
+ # @param branch [String] the branch name to look up
396
+ #
397
+ # @return [Boolean] `true` if the branch exists locally or remotely,
398
+ # `false` otherwise
399
+ #
400
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
401
+ #
402
+ # @deprecated use {#branch?} instead
403
+ #
404
+ def is_branch?(branch) # rubocop:disable Naming/PredicatePrefix
405
+ Git::Deprecation.warn(
406
+ 'Git::Repository#is_branch? is deprecated and will be removed in v6.0.0. ' \
407
+ 'Use Git::Repository#branch? instead.'
408
+ )
409
+ branch?(branch)
410
+ end
411
+
412
+ # Option keys accepted by {#branch_new}
413
+ #
414
+ BRANCH_NEW_ALLOWED_OPTS = %i[].freeze
415
+ private_constant :BRANCH_NEW_ALLOWED_OPTS
416
+
417
+ # Create a new branch
418
+ #
419
+ # @example Create a new branch from the current HEAD
420
+ # repo.branch_new('feature')
421
+ #
422
+ # @example Create a new branch from a specific commit or branch
423
+ # repo.branch_new('feature', 'main')
424
+ #
425
+ # @param branch [String] the name of the branch to create
426
+ #
427
+ # @param start_point [String, nil] the commit, branch, or tag to start the
428
+ # new branch from; defaults to the current HEAD when `nil`
429
+ #
430
+ # @param branch_options [Hash] reserved; must be empty — no options are currently
431
+ # supported
432
+ #
433
+ # @return [void]
434
+ #
435
+ # @raise [ArgumentError] if unsupported options are provided
436
+ #
437
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
438
+ #
439
+ def branch_new(branch, start_point = nil, branch_options = {})
440
+ if start_point.is_a?(Hash) && branch_options.empty?
441
+ branch_options = start_point
442
+ start_point = nil
443
+ end
444
+
445
+ SharedPrivate.assert_valid_opts!(BRANCH_NEW_ALLOWED_OPTS, **branch_options)
446
+ Git::Commands::Branch::Create.new(@execution_context).call(branch, start_point, **branch_options)
447
+
448
+ nil
449
+ end
450
+
451
+ # Option keys accepted by {#branch_delete}
452
+ #
453
+ BRANCH_DELETE_ALLOWED_OPTS = %i[force remotes].freeze
454
+ private_constant :BRANCH_DELETE_ALLOWED_OPTS
455
+
456
+ # Delete one or more local or remote-tracking branches
457
+ #
458
+ # @example Delete a single branch
459
+ # repo.branch_delete('feature') # => "Deleted branch feature (was abc1234)."
460
+ #
461
+ # @example Delete multiple branches at once
462
+ # repo.branch_delete('feature-1', 'feature-2')
463
+ #
464
+ # @example Force-delete an unmerged branch
465
+ # repo.branch_delete('unmerged-branch', force: true)
466
+ #
467
+ # @example Delete a remote-tracking branch
468
+ # repo.branch_delete('origin/feature', remotes: true)
469
+ #
470
+ # @param branches [Array<String>] the name(s) of the branch(es) to delete
471
+ #
472
+ # @param options [Hash] options for the delete command
473
+ #
474
+ # @option options [Boolean, nil] :force (true) allow deleting the branch
475
+ # irrespective of its merged status
476
+ #
477
+ # Defaults to `true` to match the 4.x behavior.
478
+ #
479
+ # @option options [Boolean, nil] :remotes (nil) delete remote-tracking
480
+ # branches
481
+ #
482
+ # Use together with a `remote/branch` name.
483
+ #
484
+ # @return [String] the stdout output from the delete command, e.g.
485
+ # `"Deleted branch feature (was abc1234)."`
486
+ #
487
+ # @raise [ArgumentError] if unsupported options are provided
488
+ #
489
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
490
+ #
491
+ # @raise [Git::Error] if git reports a deletion failure
492
+ #
493
+ def branch_delete(*branches, **options)
494
+ options = { force: true }.merge(options)
495
+ SharedPrivate.assert_valid_opts!(BRANCH_DELETE_ALLOWED_OPTS, **options)
496
+
497
+ result = Git::Commands::Branch::Delete.new(@execution_context).call(*branches, **options)
498
+
499
+ raise Git::Error, result.stderr.strip unless result.status.success?
500
+
501
+ result.stdout.strip
502
+ end
503
+
504
+ # Writes the HEAD symbolic ref to point at the given branch
505
+ #
506
+ # Sets `HEAD` to `refs/heads/<branch_name>` via `git symbolic-ref`. This is
507
+ # equivalent to running `git symbolic-ref HEAD refs/heads/<branch_name>` on
508
+ # the command line and is the mechanism git uses internally for branch
509
+ # renaming and orphan-branch checkout.
510
+ #
511
+ # @example Change HEAD to point to an existing branch
512
+ # repo.change_head_branch('main')
513
+ #
514
+ # @example Initialize a repository with a custom default branch name (unborn-branch pattern)
515
+ # repo = Git.init('/path/to/repo')
516
+ # repo.change_head_branch('my-branch')
517
+ # # HEAD now points at refs/heads/my-branch before any commits exist
518
+ #
519
+ # @param branch_name [String] the branch name to point HEAD at
520
+ #
521
+ # @return [void]
522
+ #
523
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
524
+ #
525
+ # @note Pointing HEAD at a branch that does not yet exist places the
526
+ # repository in unborn-branch state. This is intentional for repository
527
+ # initialization workflows — for example, setting a custom default branch
528
+ # name before any commits land — but is unexpected if done by mistake.
529
+ # The repository will appear to have no commits until the first commit is
530
+ # made on the new branch.
531
+ #
532
+ def change_head_branch(branch_name)
533
+ Git::Commands::SymbolicRef::Update.new(@execution_context).call('HEAD', "refs/heads/#{branch_name}")
534
+ nil
535
+ end
536
+
537
+ # Returns the `git branch --list --contains` stdout for a given commit
538
+ #
539
+ # The output format is the human-readable `git branch` listing: each
540
+ # matching branch name appears on its own line, prefixed with two spaces,
541
+ # or `* ` if it is the currently checked-out branch. This is the same
542
+ # format returned by `Git::Lib#branch_contains` in the 4.x gem series.
543
+ #
544
+ # @example List all branches that contain a commit
545
+ # repo.branch_contains('abc1234')
546
+ # # => " main\n"
547
+ #
548
+ # @example The current branch is marked with an asterisk
549
+ # repo.branch_contains('abc1234')
550
+ # # => "* main\n feature\n"
551
+ #
552
+ # @example Limit the search to branches matching a shell wildcard pattern
553
+ # repo.branch_contains('abc1234', 'feature/*')
554
+ #
555
+ # @example Typical usage: check whether any branch contains the commit
556
+ # repo.branch_contains('abc1234').empty? # => false
557
+ #
558
+ # @param commit [String] the commit SHA or ref to look up
559
+ #
560
+ # @param branch_name [String, nil] a shell wildcard pattern to limit which
561
+ # branches are searched
562
+ #
563
+ # When empty or `nil`, all local branches are searched.
564
+ #
565
+ # @return [String] the `git branch --list --contains` stdout
566
+ #
567
+ # Each matching branch appears on its own line, prefixed with two
568
+ # spaces, or `* ` for the currently checked-out branch. Returns an
569
+ # empty string when no matching branch contains the commit.
570
+ #
571
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
572
+ #
573
+ def branch_contains(commit, branch_name = '')
574
+ branch_name = branch_name.to_s
575
+ pattern = branch_name.empty? ? nil : branch_name
576
+ Git::Commands::Branch::List.new(@execution_context)
577
+ .call(*[pattern].compact, contains: commit, no_color: true)
578
+ .stdout
579
+ end
580
+
581
+ # Returns all local and remote-tracking branches as structured objects
582
+ #
583
+ # @example List all branches
584
+ # repo.branch_list
585
+ # # => [#<data Git::BranchInfo refname="refs/heads/main", current=true, ...>,
586
+ # # #<data Git::BranchInfo refname="refs/remotes/origin/main", current=false, ...>]
587
+ #
588
+ # @example Find the currently checked-out branch
589
+ # repo.branch_list.find(&:current)
590
+ #
591
+ # @example List only local branches
592
+ # repo.branch_list.reject(&:remote?)
593
+ #
594
+ # @example Filter to an exact branch name
595
+ # repo.branch_list('feature/auth')
596
+ #
597
+ # @example Filter using glob patterns
598
+ # repo.branch_list('feature/*', 'hotfix/*')
599
+ #
600
+ # @param patterns [Array<String>] optional shell wildcard patterns passed
601
+ # directly to `git branch --list`; when empty (the default) all branches
602
+ # are returned. Pattern matching follows git's own rules; behavior may
603
+ # differ between local and remote-tracking branches.
604
+ #
605
+ # @param remote_names [Array<String>, nil] configured remote names used to
606
+ # resolve remote-tracking refs
607
+ #
608
+ # Especially useful for remotes whose remote names contain slashes. When
609
+ # omitted, the repository's configured remote names are fetched automatically.
610
+ #
611
+ # @return [Array<Git::BranchInfo>] parsed branch information for every
612
+ # local and remote-tracking branch matching the pattern
613
+ #
614
+ # Returns an empty array when the repository has no branches or no branches
615
+ # match the given pattern.
616
+ #
617
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
618
+ #
619
+ def branch_list(*patterns, remote_names: nil)
620
+ remote_names ||= self.remote_names
621
+ result = Git::Commands::Branch::List.new(@execution_context).call(
622
+ *patterns, all: true, format: Git::Parsers::Branch::FORMAT_STRING
623
+ )
624
+ Git::Parsers::Branch.parse_list(result.stdout, remote_names:)
625
+ end
626
+
627
+ # Returns all local and remote-tracking branches in the 4.x-compatible format
628
+ #
629
+ # Each entry is a 4-element array: `[refname, current, worktree, symref]`.
630
+ # The `refname` uses the short form (`main`, `remotes/origin/main`) to
631
+ # match the output of the legacy `Git::Lib#branches_all` method.
632
+ #
633
+ # @return [Array<Array>] array of `[refname, current, worktree, symref]` tuples
634
+ #
635
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
636
+ #
637
+ # @deprecated Use {#branch_list} instead, which returns richer
638
+ # {Git::BranchInfo} objects.
639
+ #
640
+ def branches_all
641
+ Git::Deprecation.warn(
642
+ 'Git::Repository#branches_all is deprecated and will be removed in v6.0.0. ' \
643
+ 'Use Git::Repository#branch_list instead.'
644
+ )
645
+ branch_list.map do |info|
646
+ refname = info.remote? ? "remotes/#{info.remote_name}/#{info.short_name}" : info.short_name
647
+ [refname, info.current, info.other_worktree?, info.symref]
648
+ end
649
+ end
650
+
651
+ # Update a branch ref to point to a new commit
652
+ #
653
+ # Derives the full ref from the `branch` argument:
654
+ #
655
+ # - `remotes/<remote>/<name>` or `refs/remotes/<remote>/<name>` →
656
+ # writes to `refs/remotes/<remote>/<name>` (remote-tracking branch)
657
+ # - Any other value → writes to `refs/heads/<branch>` (local branch)
658
+ #
659
+ # @example Advance a local branch to the current HEAD
660
+ # repo.update_ref('feature', repo.rev_parse('HEAD'))
661
+ #
662
+ # @example Reset a local branch to an older commit
663
+ # repo.update_ref('main', 'abc1234def5678')
664
+ #
665
+ # @example Update a remote-tracking branch ref
666
+ # repo.update_ref('remotes/origin/main', 'abc1234def5678')
667
+ #
668
+ # @param branch [String] a local or remote-tracking branch name
669
+ #
670
+ # Short local names (e.g. `'main'`) resolve to `refs/heads/<branch>`.
671
+ # Remote-tracking names with a `remotes/<remote>/` or
672
+ # `refs/remotes/<remote>/` prefix (e.g. `'remotes/origin/main'`)
673
+ # resolve to `refs/remotes/<remote>/<name>`.
674
+ #
675
+ # @param commit [String] the commit SHA to point the branch at
676
+ #
677
+ # @return [Git::CommandLine::Result] the result of calling `git update-ref`
678
+ #
679
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
680
+ #
681
+ def update_ref(branch, commit)
682
+ ref = Private.build_update_ref(branch)
683
+ Git::Commands::UpdateRef::Update.new(@execution_context).call(ref, commit)
684
+ end
685
+
686
+ # Returns a {Git::Branch} object for the given branch name
687
+ #
688
+ # @example Get a branch object for 'main'
689
+ # repo.branch('main') #=> #<Git::Branch 'main'>
690
+ #
691
+ # @example Get a branch object for the current branch
692
+ # repo.branch #=> #<Git::Branch 'main'>
693
+ #
694
+ # @param branch_name [String] the branch name (defaults to the current branch)
695
+ #
696
+ # @return [Git::Branch] the branch object
697
+ #
698
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
699
+ #
700
+ # @deprecated Use `branch_list(name).first` and the name-based branch
701
+ # operations instead
702
+ #
703
+ # {#branch_list} returns immutable {Git::BranchInfo} value objects
704
+ # rather than {Git::Branch}. It takes `git branch --list` patterns, so
705
+ # pass the short name of a local branch or `"#{remote}/#{name}"` for a
706
+ # remote-tracking branch; the `remotes/` and `refs/` prefixes this
707
+ # method accepts match nothing. A `"#{remote}/#{name}"` pattern also
708
+ # matches a local branch of that name, so take `find(&:remote?)` rather
709
+ # than `first` for a remote-tracking branch. With no argument this
710
+ # method wraps {#current_branch}, which is `'HEAD'` when HEAD is
711
+ # detached; {#branch_list} has no entry for a detached or unborn HEAD,
712
+ # so use {#current_branch_state} in those states. Call the
713
+ # corresponding {Git::Repository} method (e.g. {#checkout},
714
+ # {#branch_new}, {#branch_delete}) for operations on a branch.
715
+ #
716
+ # @see #branch_list
717
+ #
718
+ def branch(branch_name = current_branch)
719
+ Git::Deprecation.warn(
720
+ 'Git::Repository#branch is deprecated and will be removed in v6.0.0. ' \
721
+ 'Use Git::Repository#branch_list(name).first for a local branch, ' \
722
+ 'Git::Repository#branch_list("remote/name").find(&:remote?) for a remote-tracking branch, ' \
723
+ 'and the name-based branch operations instead.'
724
+ )
725
+ Git::Branch.new(self, branch_name)
726
+ end
727
+
728
+ # Returns a {Git::Branches} collection of all branches in the repository
729
+ #
730
+ # @example List all branches
731
+ # repo.branches
732
+ # # => #<Git::Branches ...>
733
+ #
734
+ # @example Iterate over all branches
735
+ # repo.branches.each { |b| puts b.name }
736
+ #
737
+ # @example Access local branches only
738
+ # repo.branches.local
739
+ #
740
+ # @example Access remote-tracking branches only
741
+ # repo.branches.remote
742
+ #
743
+ # @example Look up a branch by name
744
+ # repo.branches['main'] # => #<Git::Branch 'main'>
745
+ #
746
+ # @return [Git::Branches] a collection wrapping all local and
747
+ # remote-tracking branches in the repository
748
+ #
749
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
750
+ #
751
+ # @deprecated Use {#branch_list} instead
752
+ #
753
+ # {#branch_list} returns `Array<Git::BranchInfo>` (immutable value
754
+ # objects) rather than a {Git::Branches} collection. Filter it with
755
+ # `select(&:remote?)` or `reject(&:remote?)` in place of
756
+ # `branches.remote` and `branches.local`, and look a branch up by name
757
+ # with `branch_list(name).first` in place of `branches[name]`.
758
+ #
759
+ # @see #branch_list
760
+ #
761
+ def branches
762
+ Git::Deprecation.warn(
763
+ 'Git::Repository#branches is deprecated and will be removed in v6.0.0. ' \
764
+ 'Use Git::Repository#branch_list instead.'
765
+ )
766
+ Git::Branches.new(self)
767
+ end
768
+
769
+ # Private helpers local to {Git::Repository::Branching}
770
+ #
771
+ # @api private
772
+ module Private
773
+ module_function
774
+
775
+ # Determines whether the given branch ref points to an existing commit
776
+ #
777
+ # Returns `:active` when the branch ref resolves successfully. Returns
778
+ # `:unborn` when the branch ref exists but has no commits yet (exit
779
+ # status 1 with empty stderr from `git rev-parse --verify --quiet`).
780
+ # Re-raises for any other failure.
781
+ #
782
+ # @param execution_context [Git::ExecutionContext::Repository] the
783
+ # execution context for git commands
784
+ #
785
+ # The full `refs/heads/<name>` ref is verified rather than the bare name.
786
+ # A bare name follows the gitrevisions search order, in which
787
+ # `refs/tags/<name>` is tried before `refs/heads/<name>`, so an unborn
788
+ # branch that shares its name with a tag would be reported as `:active`.
789
+ #
790
+ # @param branch_name [String] the branch name to verify
791
+ #
792
+ # @return [:active, :unborn] the branch ref state
793
+ #
794
+ # @raise [Git::FailedError] if git exits with a failure unrelated to an
795
+ # unborn branch
796
+ #
797
+ # @api private
798
+ #
799
+ def get_branch_state(execution_context, branch_name)
800
+ Git::Commands::RevParse.new(execution_context).call("refs/heads/#{branch_name}", verify: true, quiet: true)
801
+ :active
802
+ rescue Git::FailedError => e
803
+ raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
804
+
805
+ :unborn
806
+ end
807
+
808
+ # Translates {#checkout} options to the new command interface
809
+ #
810
+ # Legacy callers passed combinations like:
811
+ # checkout('branch', new_branch: true, start_point: 'main')
812
+ # which should map to:
813
+ # checkout('main', b: 'branch')
814
+ #
815
+ # `orphan: true` follows the same shape, naming the unborn branch:
816
+ # checkout('branch', orphan: true, start_point: 'main')
817
+ # maps to:
818
+ # checkout('main', orphan: 'branch')
819
+ #
820
+ # @param branch [String, nil] the branch argument passed to {#checkout}
821
+ #
822
+ # @param checkout_options [Hash] the raw options passed to {#checkout}
823
+ #
824
+ # @return [Array((String, nil), Hash)] a two-element tuple
825
+ # `[target, options]` containing the translated checkout arguments
826
+ #
827
+ # `target` (`String` or `nil`) is the branch or commit to check out.
828
+ # `options` is a `Hash` of keyword arguments for
829
+ # `Git::Commands::Checkout::Branch#call`
830
+ #
831
+ # @api private
832
+ #
833
+ def translate_checkout_opts(branch, checkout_options)
834
+ checkout_options = normalize_orphan_option(checkout_options)
835
+
836
+ if checkout_options[:new_branch] == true || checkout_options[:b] == true
837
+ [checkout_options[:start_point], checkout_options.except(:new_branch, :b, :start_point).merge(b: branch)]
838
+ elsif checkout_options[:new_branch].is_a?(String)
839
+ [branch, checkout_options.except(:new_branch).merge(b: checkout_options[:new_branch])]
840
+ elsif checkout_options[:orphan] == true
841
+ translate_orphan_opts(branch, checkout_options)
842
+ else
843
+ [branch, checkout_options]
844
+ end
845
+ end
846
+
847
+ # Normalizes the `:orphan` option, rejecting names that git would never see
848
+ #
849
+ # `:orphan` is a value option on the underlying command, so a literal
850
+ # `false` would be emitted as `--orphan false` and create a branch named
851
+ # "false". Flag options such as `:force` already ignore `false`; this
852
+ # gives `:orphan` the same behavior.
853
+ #
854
+ # A blank name is rejected rather than dropped: the argument DSL omits
855
+ # empty values, so `orphan: ''` would otherwise degrade silently into a
856
+ # plain checkout.
857
+ #
858
+ # @param checkout_options [Hash] the raw options passed to {#checkout}
859
+ #
860
+ # @return [Hash] the options with a `false` `:orphan` key removed
861
+ #
862
+ # @raise [ArgumentError] if `:orphan` is given a blank branch name
863
+ #
864
+ # @api private
865
+ #
866
+ def normalize_orphan_option(checkout_options)
867
+ orphan = checkout_options[:orphan]
868
+ return checkout_options.except(:orphan) if orphan == false
869
+ raise ArgumentError, 'orphan requires a non-empty branch name' if orphan.is_a?(String) && orphan.strip.empty?
870
+
871
+ checkout_options
872
+ end
873
+
874
+ # Translates `orphan: true` into the command's `:orphan` value option
875
+ #
876
+ # `orphan: true` names the unborn branch from the positional argument and
877
+ # takes its start point from `:start_point`, mirroring `new_branch: true`.
878
+ #
879
+ # @param branch [String, nil] the branch argument passed to {#checkout}
880
+ #
881
+ # @param checkout_options [Hash] the raw options passed to {#checkout}
882
+ #
883
+ # @return [Array((String, nil), Hash)] a two-element tuple
884
+ # `[target, options]` containing the translated checkout arguments
885
+ #
886
+ # @raise [ArgumentError] if `branch` is blank (`nil`, empty, or whitespace
887
+ # only), since the unborn branch would otherwise have no name
888
+ #
889
+ # @api private
890
+ #
891
+ def translate_orphan_opts(branch, checkout_options)
892
+ raise ArgumentError, 'orphan: true requires a branch name' if branch.to_s.strip.empty?
893
+
894
+ [checkout_options[:start_point], checkout_options.except(:start_point).merge(orphan: branch)]
895
+ end
896
+
897
+ # Normalizes path specifications for Git commands
898
+ #
899
+ # @param pathspecs [String, Pathname, Array<String, Pathname>, nil]
900
+ # the path(s) to normalize
901
+ #
902
+ # @param arg_name [String] the argument name used in error messages
903
+ #
904
+ # @return [Array<String>, nil] the normalized paths, or `nil` if none are valid
905
+ #
906
+ # @raise [ArgumentError] when any path is not a `String` or `Pathname`
907
+ #
908
+ # @api private
909
+ #
910
+ def normalize_pathspecs(pathspecs, arg_name)
911
+ return nil unless pathspecs
912
+
913
+ normalized = Array(pathspecs)
914
+ validate_pathspec_types(normalized, arg_name)
915
+
916
+ normalized = normalized.map(&:to_s).reject(&:empty?)
917
+ return nil if normalized.empty?
918
+
919
+ normalized
920
+ end
921
+
922
+ # Raises an error if any element of `pathspecs` is not a `String` or `Pathname`
923
+ #
924
+ # @param pathspecs [Array] the path elements to validate
925
+ #
926
+ # @param arg_name [String] the argument name used in error messages
927
+ #
928
+ # @return [void]
929
+ #
930
+ # @raise [ArgumentError] when any element is not a `String` or `Pathname`
931
+ #
932
+ # @api private
933
+ #
934
+ def validate_pathspec_types(pathspecs, arg_name)
935
+ return if pathspecs.all? { |path| path.is_a?(String) || path.is_a?(Pathname) }
936
+
937
+ raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
938
+ end
939
+
940
+ # Builds the full git ref string from a branch name argument
941
+ #
942
+ # Mirrors the routing logic of `Git::Branch#update_ref` for backward
943
+ # compatibility:
944
+ #
945
+ # - `remotes/<remote>/<name>` or `refs/remotes/<remote>/<name>` →
946
+ # `refs/remotes/<remote>/<name>`
947
+ # - Any other value → `refs/heads/<branch>`
948
+ #
949
+ # @param branch [String] a short local branch name or a remote-tracking
950
+ # branch name with a `remotes/` or `refs/remotes/` prefix
951
+ #
952
+ # @return [String] the full git ref string
953
+ #
954
+ # @api private
955
+ #
956
+ def build_update_ref(branch)
957
+ match = branch.match(%r{\A(?:refs/)?remotes/([^/]+)/(.+)\z})
958
+ match ? "refs/remotes/#{match[1]}/#{match[2]}" : "refs/heads/#{branch}"
959
+ end
960
+ end
961
+ private_constant :Private
962
+ end
963
+ end
964
+ end