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,572 @@
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/update_ref/update'
15
+ require 'git/parsers/branch'
16
+ require 'git/repository/shared_private'
17
+
18
+ module Git
19
+ class Repository
20
+ # Facade methods for branching operations: creating, checking out, querying,
21
+ # deleting, and updating branches
22
+ #
23
+ # Included by {Git::Repository}.
24
+ #
25
+ # @api public
26
+ #
27
+ module Branching
28
+ # Option keys accepted by {#checkout}
29
+ #
30
+ CHECKOUT_ALLOWED_OPTS = %i[force f new_branch b start_point].freeze
31
+ private_constant :CHECKOUT_ALLOWED_OPTS
32
+
33
+ # Option keys accepted by {#checkout_index}
34
+ #
35
+ CHECKOUT_INDEX_ALLOWED_OPTS = %i[prefix force all path_limiter].freeze
36
+ private_constant :CHECKOUT_INDEX_ALLOWED_OPTS
37
+
38
+ # Returns the name of the current branch
39
+ #
40
+ # @example Get the current branch name
41
+ # repo.current_branch # => "main"
42
+ #
43
+ # @example In detached HEAD state
44
+ # repo.current_branch # => "HEAD"
45
+ #
46
+ # @return [String] the current branch name, or `'HEAD'` when in detached
47
+ # HEAD state
48
+ #
49
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
50
+ #
51
+ def current_branch
52
+ result = Git::Commands::Branch::ShowCurrent.new(@execution_context).call
53
+ name = result.stdout.strip
54
+ name.empty? ? 'HEAD' : name
55
+ end
56
+
57
+ # Restore working tree files from a tree-ish
58
+ #
59
+ # @example Restore README.md to its HEAD state
60
+ # repo.checkout_file('HEAD', 'README.md')
61
+ #
62
+ # @param version [String] the tree-ish (branch, tag, commit SHA, etc.) to
63
+ # restore the file from
64
+ #
65
+ # @param file [String] the path to the file to restore
66
+ #
67
+ # @return [String] git's stdout from the checkout
68
+ #
69
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
70
+ #
71
+ def checkout_file(version, file)
72
+ Git::Commands::Checkout::Files.new(@execution_context).call(version, pathspec: [file]).stdout
73
+ end
74
+
75
+ # Switch branches or restore working tree files
76
+ #
77
+ # @example Check out an existing branch
78
+ # repo.checkout('main')
79
+ #
80
+ # @example Create and check out a new branch from main
81
+ # repo.checkout('new-feature', new_branch: true, start_point: 'main')
82
+ #
83
+ # @example Create a new branch with a name different from the start point
84
+ # repo.checkout('main', new_branch: 'new-feature')
85
+ #
86
+ # @example Force checkout discarding local changes
87
+ # repo.checkout('main', force: true)
88
+ #
89
+ # @param branch [String, nil] the branch to check out; defaults to nil
90
+ # (i.e. restore HEAD state)
91
+ #
92
+ # @param options [Hash] options for the checkout command
93
+ #
94
+ # @option options [Boolean, nil] :force (nil) discard local changes when
95
+ # switching branches
96
+ #
97
+ # @option options [Boolean, String, nil] :new_branch (nil) when `true`,
98
+ # creates a new branch named `branch` from `:start_point`
99
+ #
100
+ # When a `String`, creates a new branch with that name, using `branch`
101
+ # as the start point.
102
+ #
103
+ # @option options [Boolean, String, nil] :b (nil) alias for `:new_branch`
104
+ #
105
+ # @option options [Boolean, nil] :f (nil) alias for `:force`
106
+ #
107
+ # @option options [String, nil] :start_point (nil) the commit or branch to
108
+ # start the new branch from; used together with `new_branch: true`
109
+ #
110
+ # @return [String] git's stdout from the checkout
111
+ #
112
+ # @raise [ArgumentError] if unsupported options are provided
113
+ #
114
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
115
+ #
116
+ def checkout(branch = nil, options = {})
117
+ if branch.is_a?(Hash) && options.empty?
118
+ options = branch
119
+ branch = nil
120
+ end
121
+
122
+ SharedPrivate.assert_valid_opts!(CHECKOUT_ALLOWED_OPTS, **options)
123
+
124
+ target, translated_opts = Private.translate_checkout_opts(branch, options)
125
+ Git::Commands::Checkout::Branch.new(@execution_context).call(target, **translated_opts).stdout
126
+ end
127
+
128
+ # Populate the working tree from the index
129
+ #
130
+ # @example Check out all files from the index
131
+ # repo.checkout_index(all: true)
132
+ #
133
+ # @example Force check out a specific file
134
+ # repo.checkout_index(force: true, path_limiter: 'README.md')
135
+ #
136
+ # @example Check out files to a staging prefix
137
+ # repo.checkout_index(prefix: 'tmp/stage/', all: true)
138
+ #
139
+ # @param options [Hash] options for the checkout-index command
140
+ #
141
+ # @option options [Boolean, nil] :all (nil) check out all files in the index
142
+ #
143
+ # @option options [Boolean, nil] :force (nil) overwrite existing files
144
+ #
145
+ # @option options [String, nil] :prefix (nil) write files under this path prefix
146
+ # rather than the working directory root
147
+ #
148
+ # @option options [String, Pathname, Array<String, Pathname>, nil] :path_limiter (nil)
149
+ # limit the check out to the given path(s)
150
+ #
151
+ # @return [String] git's stdout from the checkout-index command
152
+ #
153
+ # @raise [ArgumentError] if unsupported options are provided
154
+ #
155
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
156
+ #
157
+ def checkout_index(options = {})
158
+ SharedPrivate.assert_valid_opts!(CHECKOUT_INDEX_ALLOWED_OPTS, **options)
159
+
160
+ paths = Private.normalize_pathspecs(options[:path_limiter], 'path_limiter')
161
+ keyword_opts = options.except(:path_limiter)
162
+ Git::Commands::CheckoutIndex.new(@execution_context).call(*paths.to_a, **keyword_opts).stdout
163
+ end
164
+
165
+ # Returns `true` if the named branch exists as a local branch
166
+ #
167
+ # @example Check whether main exists locally
168
+ # repo.local_branch?('main') # => true
169
+ #
170
+ # @param branch [String] the local branch name to look up
171
+ #
172
+ # @return [Boolean] `true` if the branch exists locally, `false` otherwise
173
+ #
174
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
175
+ #
176
+ def local_branch?(branch)
177
+ result = Git::Commands::Branch::List.new(@execution_context).call(branch, format: '%(refname:short)')
178
+ result.stdout.chomp == branch
179
+ end
180
+
181
+ # Returns `true` if the named branch exists as a remote-tracking branch
182
+ #
183
+ # The `branch` argument must be the **short branch name** (e.g. `'master'`),
184
+ # not the combined `remote/branch` form (e.g. `'origin/master'`).
185
+ #
186
+ # @example Check whether master exists on any remote
187
+ # repo.remote_branch?('master') # => true
188
+ #
189
+ # @param branch [String] the short branch name to look up across all remotes
190
+ #
191
+ # @return [Boolean] `true` if a remote-tracking branch with that short name
192
+ # exists, `false` otherwise
193
+ #
194
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
195
+ #
196
+ def remote_branch?(branch)
197
+ result = Git::Commands::Branch::List.new(@execution_context)
198
+ .call("*/#{branch}", remotes: true, format: '%(refname:lstrip=3)')
199
+ result.stdout.each_line.any? { |line| line.chomp == branch }
200
+ end
201
+
202
+ # Returns `true` if the named branch exists locally or as a remote-tracking branch
203
+ #
204
+ # @example Check whether main exists anywhere
205
+ # repo.branch?('main') # => true
206
+ #
207
+ # @param branch [String] the branch name to look up
208
+ #
209
+ # @return [Boolean] `true` if the branch exists locally or remotely,
210
+ # `false` otherwise
211
+ #
212
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
213
+ #
214
+ def branch?(branch)
215
+ local_branch?(branch) || remote_branch?(branch)
216
+ end
217
+
218
+ # Option keys accepted by {#branch_new}
219
+ #
220
+ BRANCH_NEW_ALLOWED_OPTS = %i[].freeze
221
+ private_constant :BRANCH_NEW_ALLOWED_OPTS
222
+
223
+ # Create a new branch
224
+ #
225
+ # @example Create a new branch from the current HEAD
226
+ # repo.branch_new('feature')
227
+ #
228
+ # @example Create a new branch from a specific commit or branch
229
+ # repo.branch_new('feature', 'main')
230
+ #
231
+ # @param branch [String] the name of the branch to create
232
+ #
233
+ # @param start_point [String, nil] the commit, branch, or tag to start the
234
+ # new branch from; defaults to the current HEAD when `nil`
235
+ #
236
+ # @param options [Hash] reserved; must be empty — no options are currently
237
+ # supported
238
+ #
239
+ # @return [void]
240
+ #
241
+ # @raise [ArgumentError] if unsupported options are provided
242
+ #
243
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
244
+ #
245
+ def branch_new(branch, start_point = nil, options = {})
246
+ if start_point.is_a?(Hash) && options.empty?
247
+ options = start_point
248
+ start_point = nil
249
+ end
250
+
251
+ SharedPrivate.assert_valid_opts!(BRANCH_NEW_ALLOWED_OPTS, **options)
252
+ Git::Commands::Branch::Create.new(@execution_context).call(branch, start_point, **options)
253
+
254
+ nil
255
+ end
256
+
257
+ # Option keys accepted by {#branch_delete}
258
+ #
259
+ BRANCH_DELETE_ALLOWED_OPTS = %i[force remotes].freeze
260
+ private_constant :BRANCH_DELETE_ALLOWED_OPTS
261
+
262
+ # Delete one or more local or remote-tracking branches
263
+ #
264
+ # @example Delete a single branch
265
+ # repo.branch_delete('feature') # => "Deleted branch feature (was abc1234)."
266
+ #
267
+ # @example Delete multiple branches at once
268
+ # repo.branch_delete('feature-1', 'feature-2')
269
+ #
270
+ # @example Force-delete an unmerged branch
271
+ # repo.branch_delete('unmerged-branch', force: true)
272
+ #
273
+ # @example Delete a remote-tracking branch
274
+ # repo.branch_delete('origin/feature', remotes: true)
275
+ #
276
+ # @param branches [Array<String>] the name(s) of the branch(es) to delete
277
+ #
278
+ # @param options [Hash] options for the delete command
279
+ #
280
+ # @option options [Boolean, nil] :force (true) allow deleting the branch
281
+ # irrespective of its merged status
282
+ #
283
+ # Defaults to `true` to match the 4.x behavior.
284
+ #
285
+ # @option options [Boolean, nil] :remotes (nil) delete remote-tracking
286
+ # branches
287
+ #
288
+ # Use together with a `remote/branch` name.
289
+ #
290
+ # @return [String] the stdout output from the delete command, e.g.
291
+ # `"Deleted branch feature (was abc1234)."`
292
+ #
293
+ # @raise [ArgumentError] if unsupported options are provided
294
+ #
295
+ # @raise [Git::FailedError] if git exits outside the allowed range (exit code > 1)
296
+ #
297
+ # @raise [Git::Error] if git reports a deletion failure
298
+ #
299
+ def branch_delete(*branches, **options)
300
+ options = { force: true }.merge(options)
301
+ SharedPrivate.assert_valid_opts!(BRANCH_DELETE_ALLOWED_OPTS, **options)
302
+
303
+ result = Git::Commands::Branch::Delete.new(@execution_context).call(*branches, **options)
304
+
305
+ raise Git::Error, result.stderr.strip unless result.status.success?
306
+
307
+ result.stdout.strip
308
+ end
309
+
310
+ # Returns the `git branch --list --contains` stdout for a given commit
311
+ #
312
+ # The output format is the human-readable `git branch` listing: each
313
+ # matching branch name appears on its own line, prefixed with two spaces,
314
+ # or `* ` if it is the currently checked-out branch. This is the same
315
+ # format returned by `Git::Lib#branch_contains` in the 4.x gem series.
316
+ #
317
+ # @example List all branches that contain a commit
318
+ # repo.branch_contains('abc1234')
319
+ # # => " main\n"
320
+ #
321
+ # @example The current branch is marked with an asterisk
322
+ # repo.branch_contains('abc1234')
323
+ # # => "* main\n feature\n"
324
+ #
325
+ # @example Limit the search to branches matching a shell wildcard pattern
326
+ # repo.branch_contains('abc1234', 'feature/*')
327
+ #
328
+ # @example Typical usage: check whether any branch contains the commit
329
+ # repo.branch_contains('abc1234').empty? # => false
330
+ #
331
+ # @param commit [String] the commit SHA or ref to look up
332
+ #
333
+ # @param branch_name [String, nil] a shell wildcard pattern to limit which
334
+ # branches are searched
335
+ #
336
+ # When empty or `nil`, all local branches are searched.
337
+ #
338
+ # @return [String] the `git branch --list --contains` stdout
339
+ #
340
+ # Each matching branch appears on its own line, prefixed with two
341
+ # spaces, or `* ` for the currently checked-out branch. Returns an
342
+ # empty string when no matching branch contains the commit.
343
+ #
344
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
345
+ #
346
+ def branch_contains(commit, branch_name = '')
347
+ branch_name = branch_name.to_s
348
+ pattern = branch_name.empty? ? nil : branch_name
349
+ Git::Commands::Branch::List.new(@execution_context)
350
+ .call(*[pattern].compact, contains: commit, no_color: true)
351
+ .stdout
352
+ end
353
+
354
+ # Returns all local and remote-tracking branches as structured objects
355
+ #
356
+ # @example List all branches
357
+ # repo.branches_all
358
+ # # => [#<data Git::BranchInfo refname="main", current=true, ...>,
359
+ # # #<data Git::BranchInfo refname="remotes/origin/main", current=false, ...>]
360
+ #
361
+ # @example Find the currently checked-out branch
362
+ # repo.branches_all.find(&:current)
363
+ #
364
+ # @example List only local branches
365
+ # repo.branches_all.reject(&:remote?)
366
+ #
367
+ # @return [Array<Git::BranchInfo>] parsed branch information for every
368
+ # local and remote-tracking branch
369
+ #
370
+ # Returns an empty array when the repository has no branches.
371
+ #
372
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
373
+ #
374
+ def branches_all
375
+ result = Git::Commands::Branch::List.new(@execution_context).call(
376
+ all: true, format: Git::Parsers::Branch::FORMAT_STRING
377
+ )
378
+ Git::Parsers::Branch.parse_list(result.stdout)
379
+ end
380
+
381
+ # Update a branch ref to point to a new commit
382
+ #
383
+ # Derives the full ref from the `branch` argument:
384
+ #
385
+ # - `remotes/<remote>/<name>` or `refs/remotes/<remote>/<name>` →
386
+ # writes to `refs/remotes/<remote>/<name>` (remote-tracking branch)
387
+ # - Any other value → writes to `refs/heads/<branch>` (local branch)
388
+ #
389
+ # @example Advance a local branch to the current HEAD
390
+ # repo.update_ref('feature', repo.rev_parse('HEAD'))
391
+ #
392
+ # @example Reset a local branch to an older commit
393
+ # repo.update_ref('main', 'abc1234def5678')
394
+ #
395
+ # @example Update a remote-tracking branch ref
396
+ # repo.update_ref('remotes/origin/main', 'abc1234def5678')
397
+ #
398
+ # @param branch [String] a local or remote-tracking branch name
399
+ #
400
+ # Short local names (e.g. `'main'`) resolve to `refs/heads/<branch>`.
401
+ # Remote-tracking names with a `remotes/<remote>/` or
402
+ # `refs/remotes/<remote>/` prefix (e.g. `'remotes/origin/main'`)
403
+ # resolve to `refs/remotes/<remote>/<name>`.
404
+ #
405
+ # @param commit [String] the commit SHA to point the branch at
406
+ #
407
+ # @return [Git::CommandLineResult] the result of calling `git update-ref`
408
+ #
409
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
410
+ #
411
+ def update_ref(branch, commit)
412
+ ref = Private.build_update_ref(branch)
413
+ Git::Commands::UpdateRef::Update.new(@execution_context).call(ref, commit)
414
+ end
415
+
416
+ # Returns a {Git::Branch} object for the given branch name
417
+ #
418
+ # @example Get a branch object for 'main'
419
+ # repo.branch('main') #=> #<Git::Branch 'main'>
420
+ #
421
+ # @example Get a branch object for the current branch
422
+ # repo.branch #=> #<Git::Branch 'main'>
423
+ #
424
+ # @param branch_name [String] the branch name (defaults to the current branch)
425
+ #
426
+ # @return [Git::Branch] the branch object
427
+ #
428
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
429
+ #
430
+ def branch(branch_name = current_branch)
431
+ branch_info = Git::BranchInfo.new(
432
+ refname: branch_name,
433
+ target_oid: nil,
434
+ current: false,
435
+ worktree: false,
436
+ symref: nil,
437
+ upstream: nil
438
+ )
439
+ Git::Branch.new(self, branch_info)
440
+ end
441
+
442
+ # Returns a {Git::Branches} collection of all branches in the repository
443
+ #
444
+ # @example List all branches
445
+ # repo.branches
446
+ # # => #<Git::Branches ...>
447
+ #
448
+ # @example Iterate over all branches
449
+ # repo.branches.each { |b| puts b.name }
450
+ #
451
+ # @example Access local branches only
452
+ # repo.branches.local
453
+ #
454
+ # @example Access remote-tracking branches only
455
+ # repo.branches.remote
456
+ #
457
+ # @example Look up a branch by name
458
+ # repo.branches['main'] # => #<Git::Branch 'main'>
459
+ #
460
+ # @return [Git::Branches] a collection wrapping all local and
461
+ # remote-tracking branches in the repository
462
+ #
463
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
464
+ #
465
+ def branches
466
+ Git::Branches.new(self)
467
+ end
468
+
469
+ # Private helpers local to {Git::Repository::Branching}
470
+ #
471
+ # @api private
472
+ module Private
473
+ module_function
474
+
475
+ # Translates legacy checkout options to the new command interface
476
+ #
477
+ # Legacy callers passed combinations like:
478
+ # checkout('branch', new_branch: true, start_point: 'main')
479
+ # which should map to:
480
+ # checkout('main', b: 'branch')
481
+ #
482
+ # @param branch [String, nil] the branch argument passed to {#checkout}
483
+ #
484
+ # @param options [Hash] the raw options passed to {#checkout}
485
+ #
486
+ # @return [Array] a two-element tuple `[target, options]` containing the
487
+ # translated checkout arguments
488
+ #
489
+ # `target` (`String` or `nil`) is the branch or commit to check out.
490
+ # `options` is a `Hash` of keyword arguments for
491
+ # `Git::Commands::Checkout::Branch#call`
492
+ #
493
+ # @api private
494
+ #
495
+ def translate_checkout_opts(branch, options)
496
+ if options[:new_branch] == true || options[:b] == true
497
+ [options[:start_point], options.except(:new_branch, :b, :start_point).merge(b: branch)]
498
+ elsif options[:new_branch].is_a?(String)
499
+ [branch, options.except(:new_branch).merge(b: options[:new_branch])]
500
+ else
501
+ [branch, options]
502
+ end
503
+ end
504
+
505
+ # Normalizes path specifications for Git commands
506
+ #
507
+ # @param pathspecs [String, Pathname, Array<String, Pathname>, nil]
508
+ # the path(s) to normalize
509
+ #
510
+ # @param arg_name [String] the argument name used in error messages
511
+ #
512
+ # @return [Array<String>, nil] the normalized paths, or `nil` if none are valid
513
+ #
514
+ # @raise [ArgumentError] when any path is not a `String` or `Pathname`
515
+ #
516
+ # @api private
517
+ #
518
+ def normalize_pathspecs(pathspecs, arg_name)
519
+ return nil unless pathspecs
520
+
521
+ normalized = Array(pathspecs)
522
+ validate_pathspec_types(normalized, arg_name)
523
+
524
+ normalized = normalized.map(&:to_s).reject(&:empty?)
525
+ return nil if normalized.empty?
526
+
527
+ normalized
528
+ end
529
+
530
+ # Raises an error if any element of `pathspecs` is not a `String` or `Pathname`
531
+ #
532
+ # @param pathspecs [Array] the path elements to validate
533
+ #
534
+ # @param arg_name [String] the argument name used in error messages
535
+ #
536
+ # @return [void]
537
+ #
538
+ # @raise [ArgumentError] when any element is not a `String` or `Pathname`
539
+ #
540
+ # @api private
541
+ #
542
+ def validate_pathspec_types(pathspecs, arg_name)
543
+ return if pathspecs.all? { |path| path.is_a?(String) || path.is_a?(Pathname) }
544
+
545
+ raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
546
+ end
547
+
548
+ # Builds the full git ref string from a branch name argument
549
+ #
550
+ # Mirrors the routing logic of `Git::Branch#update_ref` for backward
551
+ # compatibility:
552
+ #
553
+ # - `remotes/<remote>/<name>` or `refs/remotes/<remote>/<name>` →
554
+ # `refs/remotes/<remote>/<name>`
555
+ # - Any other value → `refs/heads/<branch>`
556
+ #
557
+ # @param branch [String] a short local branch name or a remote-tracking
558
+ # branch name with a `remotes/` or `refs/remotes/` prefix
559
+ #
560
+ # @return [String] the full git ref string
561
+ #
562
+ # @api private
563
+ #
564
+ def build_update_ref(branch)
565
+ match = branch.match(%r{\A(?:refs/)?remotes/([^/]+)/(.+)\z})
566
+ match ? "refs/remotes/#{match[1]}/#{match[2]}" : "refs/heads/#{branch}"
567
+ end
568
+ end
569
+ private_constant :Private
570
+ end
571
+ end
572
+ end