git 4.3.2 → 5.0.0.beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +73 -2705
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/pull_request_template.md +3 -1
  5. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  6. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  7. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  8. data/.github/skills/command-implementation/SKILL.md +229 -0
  9. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  10. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  11. data/.github/skills/dependency-management/SKILL.md +72 -0
  12. data/.github/skills/development-workflow/SKILL.md +506 -0
  13. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  14. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  15. data/.github/skills/extract-facade-from-base-lib/SKILL.md +600 -0
  16. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  17. data/.github/skills/facade-implementation/SKILL.md +274 -0
  18. data/.github/skills/facade-test-conventions/SKILL.md +394 -0
  19. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  20. data/.github/skills/make-skill-template/SKILL.md +176 -0
  21. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  22. data/.github/skills/project-context/SKILL.md +313 -0
  23. data/.github/skills/pull-request-review/SKILL.md +168 -0
  24. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  25. data/.github/skills/release-management/SKILL.md +125 -0
  26. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  27. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  28. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  29. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  30. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  31. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  32. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  33. data/.github/skills/test-debugging/SKILL.md +160 -0
  34. data/.github/skills/yard-documentation/SKILL.md +793 -0
  35. data/.github/workflows/continuous_integration.yml +3 -2
  36. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  37. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  38. data/.github/workflows/release.yml +3 -4
  39. data/.gitignore +8 -0
  40. data/.husky/pre-commit +13 -0
  41. data/.release-please-manifest.json +1 -1
  42. data/.rspec +3 -0
  43. data/.rubocop.yml +12 -3
  44. data/.rubocop_todo.yml +23 -5
  45. data/.yardopts +1 -0
  46. data/CHANGELOG.md +0 -40
  47. data/CONTRIBUTING.md +694 -53
  48. data/README.md +67 -15
  49. data/Rakefile +61 -9
  50. data/UPGRADING.md +141 -0
  51. data/commitlint.test +4 -0
  52. data/git.gemspec +18 -7
  53. data/lib/git/args_builder.rb +0 -8
  54. data/lib/git/branch.rb +369 -43
  55. data/lib/git/branch_delete_failure.rb +31 -0
  56. data/lib/git/branch_delete_result.rb +63 -0
  57. data/lib/git/branch_info.rb +178 -0
  58. data/lib/git/branches.rb +122 -24
  59. data/lib/git/command_line/base.rb +255 -0
  60. data/lib/git/command_line/capturing.rb +251 -0
  61. data/lib/git/command_line/result.rb +96 -0
  62. data/lib/git/command_line/streaming.rb +196 -0
  63. data/lib/git/command_line.rb +43 -322
  64. data/lib/git/command_line_result.rb +4 -88
  65. data/lib/git/commands/add.rb +131 -0
  66. data/lib/git/commands/am/abort.rb +43 -0
  67. data/lib/git/commands/am/apply.rb +252 -0
  68. data/lib/git/commands/am/continue.rb +43 -0
  69. data/lib/git/commands/am/quit.rb +43 -0
  70. data/lib/git/commands/am/retry.rb +47 -0
  71. data/lib/git/commands/am/show_current_patch.rb +64 -0
  72. data/lib/git/commands/am/skip.rb +42 -0
  73. data/lib/git/commands/am.rb +33 -0
  74. data/lib/git/commands/apply.rb +237 -0
  75. data/lib/git/commands/archive/list_formats.rb +46 -0
  76. data/lib/git/commands/archive.rb +140 -0
  77. data/lib/git/commands/arguments.rb +3510 -0
  78. data/lib/git/commands/base.rb +404 -0
  79. data/lib/git/commands/branch/copy.rb +94 -0
  80. data/lib/git/commands/branch/create.rb +173 -0
  81. data/lib/git/commands/branch/delete.rb +80 -0
  82. data/lib/git/commands/branch/list.rb +162 -0
  83. data/lib/git/commands/branch/move.rb +94 -0
  84. data/lib/git/commands/branch/set_upstream.rb +86 -0
  85. data/lib/git/commands/branch/show_current.rb +49 -0
  86. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  87. data/lib/git/commands/branch.rb +34 -0
  88. data/lib/git/commands/cat_file/batch.rb +369 -0
  89. data/lib/git/commands/cat_file/filtered.rb +105 -0
  90. data/lib/git/commands/cat_file/raw.rb +216 -0
  91. data/lib/git/commands/cat_file.rb +49 -0
  92. data/lib/git/commands/checkout/branch.rb +151 -0
  93. data/lib/git/commands/checkout/files.rb +115 -0
  94. data/lib/git/commands/checkout.rb +38 -0
  95. data/lib/git/commands/checkout_index.rb +105 -0
  96. data/lib/git/commands/clean.rb +100 -0
  97. data/lib/git/commands/clone.rb +240 -0
  98. data/lib/git/commands/commit.rb +272 -0
  99. data/lib/git/commands/commit_tree.rb +100 -0
  100. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  101. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  102. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  103. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  104. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  105. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  106. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  108. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  109. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  110. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  111. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  112. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  113. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  114. data/lib/git/commands/config_option_syntax.rb +56 -0
  115. data/lib/git/commands/describe.rb +155 -0
  116. data/lib/git/commands/diff.rb +656 -0
  117. data/lib/git/commands/diff_files.rb +518 -0
  118. data/lib/git/commands/diff_index.rb +496 -0
  119. data/lib/git/commands/fetch.rb +352 -0
  120. data/lib/git/commands/fsck.rb +136 -0
  121. data/lib/git/commands/gc.rb +132 -0
  122. data/lib/git/commands/grep.rb +338 -0
  123. data/lib/git/commands/init.rb +99 -0
  124. data/lib/git/commands/log.rb +632 -0
  125. data/lib/git/commands/ls_files.rb +191 -0
  126. data/lib/git/commands/ls_remote.rb +155 -0
  127. data/lib/git/commands/ls_tree.rb +131 -0
  128. data/lib/git/commands/maintenance/register.rb +75 -0
  129. data/lib/git/commands/maintenance/run.rb +104 -0
  130. data/lib/git/commands/maintenance/start.rb +66 -0
  131. data/lib/git/commands/maintenance/stop.rb +55 -0
  132. data/lib/git/commands/maintenance/unregister.rb +79 -0
  133. data/lib/git/commands/maintenance.rb +31 -0
  134. data/lib/git/commands/merge/abort.rb +44 -0
  135. data/lib/git/commands/merge/continue.rb +44 -0
  136. data/lib/git/commands/merge/quit.rb +46 -0
  137. data/lib/git/commands/merge/start.rb +245 -0
  138. data/lib/git/commands/merge.rb +28 -0
  139. data/lib/git/commands/merge_base.rb +86 -0
  140. data/lib/git/commands/mv.rb +77 -0
  141. data/lib/git/commands/name_rev.rb +114 -0
  142. data/lib/git/commands/pull.rb +377 -0
  143. data/lib/git/commands/push.rb +246 -0
  144. data/lib/git/commands/read_tree.rb +149 -0
  145. data/lib/git/commands/remote/add.rb +91 -0
  146. data/lib/git/commands/remote/get_url.rb +66 -0
  147. data/lib/git/commands/remote/list.rb +54 -0
  148. data/lib/git/commands/remote/prune.rb +61 -0
  149. data/lib/git/commands/remote/remove.rb +52 -0
  150. data/lib/git/commands/remote/rename.rb +69 -0
  151. data/lib/git/commands/remote/set_branches.rb +63 -0
  152. data/lib/git/commands/remote/set_head.rb +82 -0
  153. data/lib/git/commands/remote/set_url.rb +71 -0
  154. data/lib/git/commands/remote/set_url_add.rb +61 -0
  155. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  156. data/lib/git/commands/remote/show.rb +71 -0
  157. data/lib/git/commands/remote/update.rb +72 -0
  158. data/lib/git/commands/remote.rb +42 -0
  159. data/lib/git/commands/repack.rb +277 -0
  160. data/lib/git/commands/reset.rb +147 -0
  161. data/lib/git/commands/rev_parse.rb +297 -0
  162. data/lib/git/commands/revert/abort.rb +45 -0
  163. data/lib/git/commands/revert/continue.rb +57 -0
  164. data/lib/git/commands/revert/quit.rb +47 -0
  165. data/lib/git/commands/revert/skip.rb +44 -0
  166. data/lib/git/commands/revert/start.rb +153 -0
  167. data/lib/git/commands/revert.rb +29 -0
  168. data/lib/git/commands/rm.rb +114 -0
  169. data/lib/git/commands/show.rb +632 -0
  170. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  171. data/lib/git/commands/show_ref/exists.rb +78 -0
  172. data/lib/git/commands/show_ref/list.rb +145 -0
  173. data/lib/git/commands/show_ref/verify.rb +120 -0
  174. data/lib/git/commands/show_ref.rb +42 -0
  175. data/lib/git/commands/stash/apply.rb +75 -0
  176. data/lib/git/commands/stash/branch.rb +65 -0
  177. data/lib/git/commands/stash/clear.rb +41 -0
  178. data/lib/git/commands/stash/create.rb +58 -0
  179. data/lib/git/commands/stash/drop.rb +67 -0
  180. data/lib/git/commands/stash/list.rb +39 -0
  181. data/lib/git/commands/stash/pop.rb +78 -0
  182. data/lib/git/commands/stash/push.rb +103 -0
  183. data/lib/git/commands/stash/show.rb +149 -0
  184. data/lib/git/commands/stash/store.rb +63 -0
  185. data/lib/git/commands/stash.rb +38 -0
  186. data/lib/git/commands/status.rb +169 -0
  187. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  188. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  189. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  190. data/lib/git/commands/symbolic_ref.rb +38 -0
  191. data/lib/git/commands/tag/create.rb +139 -0
  192. data/lib/git/commands/tag/delete.rb +55 -0
  193. data/lib/git/commands/tag/list.rb +143 -0
  194. data/lib/git/commands/tag/verify.rb +71 -0
  195. data/lib/git/commands/tag.rb +26 -0
  196. data/lib/git/commands/update_ref/batch.rb +140 -0
  197. data/lib/git/commands/update_ref/delete.rb +92 -0
  198. data/lib/git/commands/update_ref/update.rb +106 -0
  199. data/lib/git/commands/update_ref.rb +42 -0
  200. data/lib/git/commands/version.rb +57 -0
  201. data/lib/git/commands/worktree/add.rb +140 -0
  202. data/lib/git/commands/worktree/list.rb +64 -0
  203. data/lib/git/commands/worktree/lock.rb +58 -0
  204. data/lib/git/commands/worktree/management_base.rb +51 -0
  205. data/lib/git/commands/worktree/move.rb +66 -0
  206. data/lib/git/commands/worktree/prune.rb +67 -0
  207. data/lib/git/commands/worktree/remove.rb +63 -0
  208. data/lib/git/commands/worktree/repair.rb +76 -0
  209. data/lib/git/commands/worktree/unlock.rb +47 -0
  210. data/lib/git/commands/worktree.rb +43 -0
  211. data/lib/git/commands/write_tree.rb +68 -0
  212. data/lib/git/commands.rb +87 -0
  213. data/lib/git/config.rb +17 -0
  214. data/lib/git/config_entry_info.rb +106 -0
  215. data/lib/git/configuring.rb +665 -0
  216. data/lib/git/deprecation.rb +9 -0
  217. data/lib/git/detached_head_info.rb +54 -0
  218. data/lib/git/diff.rb +293 -7
  219. data/lib/git/diff_file_numstat_info.rb +29 -0
  220. data/lib/git/diff_file_patch_info.rb +134 -0
  221. data/lib/git/diff_file_raw_info.rb +127 -0
  222. data/lib/git/diff_info.rb +169 -0
  223. data/lib/git/diff_path_status.rb +67 -19
  224. data/lib/git/diff_result.rb +32 -0
  225. data/lib/git/diff_stats.rb +51 -14
  226. data/lib/git/dirstat_info.rb +86 -0
  227. data/lib/git/errors.rb +65 -2
  228. data/lib/git/execution_context/global.rb +31 -0
  229. data/lib/git/execution_context/repository.rb +136 -0
  230. data/lib/git/execution_context.rb +501 -0
  231. data/lib/git/file_ref.rb +74 -0
  232. data/lib/git/fsck_object.rb +9 -9
  233. data/lib/git/fsck_result.rb +1 -1
  234. data/lib/git/log.rb +9 -2
  235. data/lib/git/object.rb +90 -27
  236. data/lib/git/parsers/branch.rb +224 -0
  237. data/lib/git/parsers/cat_file.rb +111 -0
  238. data/lib/git/parsers/config_entry.rb +110 -0
  239. data/lib/git/parsers/diff.rb +585 -0
  240. data/lib/git/parsers/fsck.rb +133 -0
  241. data/lib/git/parsers/grep.rb +42 -0
  242. data/lib/git/parsers/ls_remote.rb +79 -0
  243. data/lib/git/parsers/ls_tree.rb +58 -0
  244. data/lib/git/parsers/stash.rb +208 -0
  245. data/lib/git/parsers/tag.rb +257 -0
  246. data/lib/git/remote.rb +120 -9
  247. data/lib/git/repository/branching.rb +743 -0
  248. data/lib/git/repository/committing.rb +187 -0
  249. data/lib/git/repository/configuring.rb +351 -0
  250. data/lib/git/repository/context_helpers.rb +264 -0
  251. data/lib/git/repository/diffing.rb +775 -0
  252. data/lib/git/repository/factories.rb +682 -0
  253. data/lib/git/repository/inspecting.rb +252 -0
  254. data/lib/git/repository/logging.rb +247 -0
  255. data/lib/git/repository/maintenance.rb +65 -0
  256. data/lib/git/repository/merging.rb +357 -0
  257. data/lib/git/repository/object_operations.rb +1199 -0
  258. data/lib/git/repository/path_resolver.rb +207 -0
  259. data/lib/git/repository/remote_operations.rb +898 -0
  260. data/lib/git/repository/shared_private.rb +51 -0
  261. data/lib/git/repository/staging.rb +554 -0
  262. data/lib/git/repository/stashing.rb +143 -0
  263. data/lib/git/repository/status_operations.rb +201 -0
  264. data/lib/git/repository/worktree_operations.rb +159 -0
  265. data/lib/git/repository.rb +203 -1
  266. data/lib/git/stash.rb +78 -4
  267. data/lib/git/stash_info.rb +104 -0
  268. data/lib/git/stashes.rb +125 -13
  269. data/lib/git/status.rb +215 -18
  270. data/lib/git/tag_delete_failure.rb +31 -0
  271. data/lib/git/tag_delete_result.rb +63 -0
  272. data/lib/git/tag_info.rb +105 -0
  273. data/lib/git/version.rb +109 -2
  274. data/lib/git/version_constraint.rb +81 -0
  275. data/lib/git/worktree.rb +108 -6
  276. data/lib/git/worktrees.rb +94 -7
  277. data/lib/git.rb +260 -61
  278. data/redesign/1_architecture_existing.md +54 -18
  279. data/redesign/2_architecture_redesign.md +365 -46
  280. data/redesign/3_architecture_implementation.md +1497 -63
  281. data/redesign/Phase 4 - Step A.md +360 -0
  282. data/redesign/beta_release.md +107 -0
  283. data/redesign/c1c2_audit.md +566 -0
  284. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  285. data/redesign/config_design.rb +501 -0
  286. data/tasks/gem_tasks.rake +4 -0
  287. data/tasks/npm_tasks.rake +7 -0
  288. data/tasks/rspec.rake +48 -0
  289. data/tasks/test.rake +13 -1
  290. data/tasks/yard.rake +34 -7
  291. metadata +365 -22
  292. data/lib/git/base.rb +0 -1128
  293. data/lib/git/index.rb +0 -6
  294. data/lib/git/lib.rb +0 -2277
  295. data/lib/git/path.rb +0 -38
  296. data/lib/git/working_directory.rb +0 -6
  297. /data/{release-please-config.json → .release-please-config.json} +0 -0
data/lib/git/branch.rb CHANGED
@@ -1,102 +1,376 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'git/path'
3
+ require_relative 'branch_info'
4
4
 
5
5
  module Git
6
6
  # Represents a Git branch
7
+ #
8
+ # Branch objects provide access to branch metadata and operations like checkout,
9
+ # delete, and merge. They should be obtained via {Git::Repository#branch} or
10
+ # {Git::Repository#branches}, not constructed directly.
11
+ #
12
+ # @example Getting a branch
13
+ # git = Git.open('.')
14
+ # branch = git.branch('main')
15
+ # branch.checkout
16
+ #
17
+ # @example Listing branches
18
+ # git.branches.each { |b| puts b.name }
19
+ #
20
+ # @api public
21
+ #
7
22
  class Branch
8
- attr_accessor :full, :remote, :name
23
+ # The full refname of this branch
24
+ #
25
+ # For local branches this is the short name (e.g. `'main'`). For
26
+ # remote-tracking branches obtained via {Git::Repository#branches} this includes
27
+ # the `remotes/` prefix (e.g. `'remotes/origin/main'`). Branches constructed
28
+ # by {Git::Remote#branch} use the `<remote>/<branch>` form (e.g.
29
+ # `'origin/main'`) which does **not** populate {#remote}.
30
+ #
31
+ # @example Local and remote-tracking branch full refnames
32
+ # git.branch('main').full #=> 'main'
33
+ # git.branch('remotes/origin/main').full #=> 'remotes/origin/main'
34
+ #
35
+ # @return [String] the full refname
36
+ #
37
+ attr_accessor :full
9
38
 
10
- def initialize(base, name)
11
- @full = name
39
+ # The remote for this branch, or `nil` for local or bare-name remote-tracking branches
40
+ #
41
+ # Set to a {Git::Remote} object only when this branch was initialized with a
42
+ # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix. `nil` for local
43
+ # branches and for remote-tracking branches in `<remote>/<branch>` form
44
+ # (such as those returned by {Git::Remote#branch}).
45
+ #
46
+ # @example Local and remote-tracking branches
47
+ # git.branch('main').remote #=> nil
48
+ # git.branch('remotes/origin/main').remote #=> #<Git::Remote 'origin'>
49
+ # git.remote('origin').branch('main').remote #=> nil # uses 'origin/main' form
50
+ #
51
+ # @return [Git::Remote, nil] the remote object, or `nil`
52
+ #
53
+ attr_accessor :remote
54
+
55
+ # The short branch name without the remote prefix
56
+ #
57
+ # For both local and remote-tracking branches this is the bare branch
58
+ # name (e.g. `'main'` rather than `'remotes/origin/main'`).
59
+ #
60
+ # @example Local and remote-tracking branch short names
61
+ # git.branch('main').name #=> 'main'
62
+ # git.branch('remotes/origin/main').name #=> 'main'
63
+ #
64
+ # @return [String] the short branch name
65
+ #
66
+ attr_accessor :name
67
+
68
+ # Initialize a new Branch object
69
+ #
70
+ # @param base [Git::Repository] the git repository
71
+ #
72
+ # @param branch_info_or_name [Git::BranchInfo, String] branch info object or name string
73
+ #
74
+ # Passing a BranchInfo is preferred; String support is for backward compatibility.
75
+ #
76
+ # @note Use {Git::Repository#branch} or {Git::Repository#branches} instead of constructing directly
77
+ #
78
+ # @api private
79
+ #
80
+ def initialize(base, branch_info_or_name)
12
81
  @base = base
13
82
  @gcommit = nil
14
83
  @stashes = nil
15
- @remote, @name = parse_name(name)
84
+
85
+ initialize_from_argument(branch_info_or_name)
16
86
  end
17
87
 
88
+ # Returns the commit at the tip of this branch
89
+ #
90
+ # The result is memoized after the first call.
91
+ #
92
+ # @example Get the tip commit
93
+ # git.branch('main').gcommit #=> #<Git::Object ...>
94
+ #
95
+ # @return [Git::Object] the commit at the tip of this branch
96
+ #
18
97
  def gcommit
19
- @gcommit ||= @base.gcommit(@full)
98
+ @gcommit ||= branch_repository.gcommit(@full)
20
99
  @gcommit
21
100
  end
22
101
 
102
+ # Returns the stash list for this repository
103
+ #
104
+ # The result is memoized after the first call.
105
+ #
106
+ # @example Iterate over stash entries
107
+ # git.branch('main').stashes.each { |s| puts s }
108
+ #
109
+ # @return [Git::Stashes] the stash list
110
+ #
23
111
  def stashes
24
- @stashes ||= Git::Stashes.new(@base)
112
+ @stashes ||= Git::Stashes.new(branch_repository)
25
113
  end
26
114
 
115
+ # Checks out this branch, attempting to create it first if it does not already exist
116
+ #
117
+ # Branch creation is attempted via {#check_if_create}; any error from that
118
+ # step is silently ignored and the checkout proceeds regardless.
119
+ #
120
+ # **Note:** for remote-tracking branches (where {#remote} is not `nil`),
121
+ # `check_if_create` will attempt to create a *local* branch named {#name}
122
+ # as a side-effect before checking out {#full} (which typically results in
123
+ # a detached HEAD). This is a known limitation; see
124
+ # [ruby-git#1280](https://github.com/ruby-git/ruby-git/issues/1280).
125
+ #
126
+ # @example Check out a branch
127
+ # git = Git.open('.')
128
+ # git.branch('main').checkout
129
+ #
130
+ # @return [String] git's stdout from the checkout
131
+ #
132
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
133
+ #
27
134
  def checkout
28
135
  check_if_create
29
- @base.checkout(@full)
136
+ branch_repository.checkout(@full)
30
137
  end
31
138
 
139
+ # Archives this branch and writes the result to a file
140
+ #
141
+ # @example Archive to a tar file
142
+ # git.branch('main').archive('/tmp/main.tar')
143
+ #
144
+ # @example Archive to a zip file
145
+ # git.branch('main').archive('/tmp/main.zip', format: 'zip')
146
+ #
147
+ # @param file [String] path to the destination archive file
148
+ #
149
+ # @param opts [Hash] archive options (see {Git::Repository#archive})
150
+ #
151
+ # @return [String] the path to the written archive file
152
+ #
153
+ # @raise [Git::FailedError] if `git archive` fails
154
+ #
32
155
  def archive(file, opts = {})
33
- @base.lib.archive(@full, file, opts)
156
+ branch_repository.archive(@full, file, opts)
34
157
  end
35
158
 
36
- # g.branch('new_branch').in_branch do
37
- # # create new file
38
- # # do other stuff
39
- # return true # auto commits and switches back
40
- # end
159
+ # Checks out this branch for the duration of a block, then restores the original branch
160
+ #
161
+ # If the block returns a truthy value, all pending changes are committed with the
162
+ # given message before switching back to the original branch. If the block returns
163
+ # a falsy value, a hard reset is performed before switching back.
164
+ #
165
+ # **Note:** the restore checkout is not wrapped in `ensure`. If the block,
166
+ # the commit, or the reset raises an exception, the repository will be left
167
+ # checked out on this branch rather than restored to the original.
168
+ #
169
+ # @example Commit a new file on a feature branch
170
+ # git.branch('feature').in_branch('Add README') do
171
+ # File.write('README.md', '# Hello')
172
+ # git.add('README.md')
173
+ # true # commit and return to original branch
174
+ # end
175
+ #
176
+ # @param message [String] commit message used when the block returns truthy
177
+ #
178
+ # @yield Executes the block with this branch checked out
179
+ #
180
+ # @yieldreturn [Object] return a truthy value to commit all changes, a falsy value to hard-reset
181
+ #
182
+ # @return [String] git's stdout from the final checkout back to the original branch
183
+ #
184
+ # @raise [Git::FailedError] if any of the underlying git operations (checkout, commit, reset) fail
185
+ #
41
186
  def in_branch(message = 'in branch work')
42
- old_current = @base.lib.branch_current
187
+ old_current = branch_repository.current_branch
43
188
  checkout
44
189
  if yield
45
- @base.commit_all(message)
190
+ branch_repository.commit_all(message)
46
191
  else
47
- @base.reset_hard
192
+ branch_repository.reset(nil, hard: true)
48
193
  end
49
- @base.checkout(old_current)
194
+ branch_repository.checkout(old_current)
50
195
  end
51
196
 
197
+ # Creates this branch if it does not already exist
198
+ #
199
+ # Silently ignores any error raised during branch creation (including the case
200
+ # where the branch already exists).
201
+ #
202
+ # @example Create a new branch
203
+ # git.branch('feature').create
204
+ #
205
+ # @return [nil]
206
+ #
52
207
  def create
53
208
  check_if_create
54
209
  end
55
210
 
211
+ # Deletes this branch
212
+ #
213
+ # Remote-tracking branches (one where {#remote} is not `nil`) delete the
214
+ # local remote-tracking ref; they do not push a deletion to the remote.
215
+ #
216
+ # @example Delete a local branch
217
+ # git.branch('old-feature').delete
218
+ #
219
+ # @return [String] git's deletion output
220
+ #
221
+ # @raise [Git::Error] if the branch cannot be deleted
222
+ #
56
223
  def delete
57
- @base.lib.branch_delete(@name)
224
+ if @remote
225
+ branch_repository.branch_delete("#{@remote.name}/#{@name}", remotes: true)
226
+ else
227
+ branch_repository.branch_delete(@name)
228
+ end
58
229
  end
59
230
 
231
+ # Returns true if this is the currently checked-out branch
232
+ #
233
+ # **Note:** this compares the current branch's short name against {#name}.
234
+ # For a remote-tracking branch (where {#remote} is not `nil`), {#name} is
235
+ # still the bare short name (e.g. `'main'`), so this will return `true`
236
+ # whenever the *local* branch with that name is checked out — not the
237
+ # remote-tracking ref itself.
238
+ #
239
+ # @example Check whether currently on main
240
+ # git.branch('main').current #=> true
241
+ #
242
+ # @return [Boolean] whether this branch is currently checked out
243
+ #
244
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
245
+ #
60
246
  def current # rubocop:disable Naming/PredicateMethod
61
- @base.lib.branch_current == @name
247
+ branch_repository.current_branch == @name
62
248
  end
63
249
 
250
+ # Returns true if this branch contains the given commit
251
+ #
252
+ # **Note:** this queries local branches by short name. For a remote-tracking
253
+ # branch (where {#remote} is not `nil`), it checks the *local* branch with
254
+ # the same {#name} rather than the remote-tracking ref, which may give an
255
+ # inaccurate result.
256
+ #
257
+ # @example Check if a commit is reachable from this branch
258
+ # git.branch('main').contains?('abc1234') #=> true
259
+ #
260
+ # @param commit [String] the commit SHA or ref to check
261
+ #
262
+ # @return [Boolean] whether this branch contains the given commit
263
+ #
264
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
265
+ #
64
266
  def contains?(commit)
65
- !@base.lib.branch_contains(commit, name).empty?
267
+ !branch_repository.branch_contains(commit, name).empty?
66
268
  end
67
269
 
270
+ # Merges a branch into this branch, or merges this branch into the current branch
271
+ #
272
+ # @overload merge(branch, message = nil)
273
+ #
274
+ # Temporarily checks out this branch, merges the given branch into it,
275
+ # then restores the original branch.
276
+ #
277
+ # **Note:** if `self` is a remote-tracking branch (where {#remote} is not
278
+ # `nil`), this delegates to {#checkout} which has the detached-HEAD
279
+ # side-effect described there. The remote-tracking ref will not be updated.
280
+ #
281
+ # @example Merge a feature branch into main
282
+ # git.branch('main').merge('feature')
283
+ #
284
+ # @param branch [String] the name of the branch to merge into this one
285
+ #
286
+ # @param message [String, nil] commit message for the merge commit
287
+ #
288
+ # @return [String] git's stdout from the final checkout back to the original branch
289
+ #
290
+ # @overload merge()
291
+ #
292
+ # Merges this branch into the currently checked-out branch.
293
+ #
294
+ # @example Merge main into the current branch
295
+ # git.branch('main').merge
296
+ #
297
+ # @return [String] git's stdout from the merge command
298
+ #
299
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
300
+ #
68
301
  def merge(branch = nil, message = nil)
69
302
  if branch
70
303
  in_branch do
71
- @base.merge(branch, message)
304
+ branch_repository.merge(branch, message)
72
305
  false
73
306
  end
74
307
  # merge a branch into this one
75
308
  else
76
309
  # merge this branch into the current one
77
- @base.merge(@name)
310
+ branch_repository.merge(@name)
78
311
  end
79
312
  end
80
313
 
314
+ # Updates the git ref for this branch to point to the given commit
315
+ #
316
+ # The target ref depends on whether {#remote} is set:
317
+ # - When {#remote} is not `nil` (i.e. the branch was initialized with a
318
+ # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix), updates
319
+ # `refs/remotes/<remote>/<name>`.
320
+ # - Otherwise updates `refs/heads/<name>`. Note that branches in the
321
+ # `<remote>/<branch>` form (e.g. those returned by {Git::Remote#branch})
322
+ # have `remote == nil` and therefore update `refs/heads/<remote>/<name>`,
323
+ # **not** `refs/remotes/...`.
324
+ #
325
+ # @example Advance a local branch to a new commit
326
+ # git.branch('feature').update_ref('abc1234def5678')
327
+ #
328
+ # @param commit [String] the commit SHA to point this branch at
329
+ #
330
+ # @return [Git::CommandLineResult] the result of calling `git update-ref`
331
+ #
332
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
333
+ #
81
334
  def update_ref(commit)
82
335
  if @remote
83
- @base.lib.update_ref("refs/remotes/#{@remote.name}/#{@name}", commit)
336
+ branch_repository.update_ref("remotes/#{@remote.name}/#{@name}", commit)
84
337
  else
85
- @base.lib.update_ref("refs/heads/#{@name}", commit)
338
+ branch_repository.update_ref(@name, commit)
86
339
  end
87
340
  end
88
341
 
342
+ # Returns this branch as a single-element array containing its full refname
343
+ #
344
+ # @example Get branch as array
345
+ # git.branch('main').to_a #=> ['main']
346
+ #
347
+ # @return [Array<String>] a single-element array containing the full refname
348
+ #
89
349
  def to_a
90
350
  [@full]
91
351
  end
92
352
 
353
+ # Returns the full refname of this branch as a string
354
+ #
355
+ # @example Get branch as string
356
+ # git.branch('main').to_s #=> 'main'
357
+ #
358
+ # @return [String] the full refname
359
+ #
93
360
  def to_s
94
361
  @full
95
362
  end
96
363
 
364
+ # Regular expression for parsing branch refnames
365
+ #
366
+ # Matches full and short refnames, capturing an optional remote name and the
367
+ # branch name. Used internally to identify remote-tracking branches.
368
+ #
369
+ # @api private
370
+ #
97
371
  BRANCH_NAME_REGEXP = %r{
98
372
  ^
99
- # Optional 'refs/remotes/' at the beggining to specify a remote tracking branch
373
+ # Optional 'remotes/' or 'refs/remotes/' at the beginning to specify a remote tracking branch
100
374
  # with a <remote_name>. <remote_name> is nil if not present.
101
375
  (?:
102
376
  (?:(?:refs/)?remotes/)(?<remote_name>[^/]+)/
@@ -107,26 +381,66 @@ module Git
107
381
 
108
382
  private
109
383
 
110
- # Given a full branch name return an Array containing the remote and branch names.
384
+ # Dispatches initialization to the appropriate strategy
111
385
  #
112
- # Removes 'remotes' from the beggining of the name (if present).
113
- # Takes the second part (splittign by '/') as the remote name.
114
- # Takes the rest as the repo name (can also hold one or more '/').
386
+ # @param branch_info_or_name [Git::BranchInfo, String] branch info or name string
387
+ #
388
+ # @return [nil]
389
+ #
390
+ # @api private
391
+ #
392
+ def initialize_from_argument(branch_info_or_name)
393
+ if branch_info_or_name.is_a?(Git::BranchInfo)
394
+ initialize_from_branch_info(branch_info_or_name)
395
+ else
396
+ initialize_from_name(branch_info_or_name)
397
+ end
398
+ end
399
+
400
+ # Initialize from a BranchInfo object (preferred path)
401
+ #
402
+ # @param branch_info [Git::BranchInfo] the branch info
403
+ #
404
+ # @return [nil]
405
+ #
406
+ def initialize_from_branch_info(branch_info)
407
+ @full = branch_info.refname
408
+ @name = branch_info.short_name
409
+ @remote = branch_info.remote_name ? Git::Remote.new(@base, branch_info.remote_name) : nil
410
+ end
411
+
412
+ # Initialize from a string name (legacy path for backward compatibility)
115
413
  #
116
- # Example:
117
- # # local branches
118
- # parse_name('master') #=> [nil, 'master']
119
- # parse_name('origin/master') #=> [nil, 'origin/master']
120
- # parse_name('origin/master/v2') #=> [nil, 'origin/master']
414
+ # @param name [String] the branch name
121
415
  #
122
- # # remote branches
123
- # parse_name('remotes/origin/master') #=> ['origin', 'master']
124
- # parse_name('remotes/origin/master/v2') #=> ['origin', 'master/v2']
125
- # parse_name('refs/remotes/origin/master') #=> ['origin', 'master']
126
- # parse_name('refs/remotes/origin/master/v2') #=> ['origin', 'master/v2']
416
+ # @return [nil]
417
+ #
418
+ def initialize_from_name(name)
419
+ @full = name
420
+ @remote, @name = parse_name(name)
421
+ end
422
+
423
+ # Parses a full branch name into remote and short branch name components
424
+ #
425
+ # Strips an optional `remotes/` or `refs/remotes/` prefix. Only inputs that
426
+ # begin with one of those prefixes yield a remote object; all other inputs
427
+ # (including `'origin/master'`) are treated as local branch names with a
428
+ # `nil` remote.
429
+ #
430
+ # @example Local branches
431
+ # parse_name('master') #=> [nil, 'master']
432
+ # parse_name('origin/master') #=> [nil, 'origin/master']
433
+ #
434
+ # @example Remote-tracking branches
435
+ # parse_name('remotes/origin/master') #=> [#<Git::Remote 'origin'>, 'master']
436
+ # parse_name('refs/remotes/origin/master') #=> [#<Git::Remote 'origin'>, 'master']
437
+ #
438
+ # @param name [String] the full branch name to parse
439
+ #
440
+ # @return [Array(Git::Remote, String)] a two-element array; the first element is
441
+ # a {Git::Remote} for remote-tracking branches or `nil` for local branches,
442
+ # and the second element is the short branch name
127
443
  #
128
- # param [String] name branch full name.
129
- # return [<Git::Remote,NilClass,String>] an Array containing the remote and branch names.
130
444
  def parse_name(name)
131
445
  # Expect this will always match
132
446
  match = name.match(BRANCH_NAME_REGEXP)
@@ -135,10 +449,22 @@ module Git
135
449
  [remote, branch_name]
136
450
  end
137
451
 
452
+ # Creates the branch if it does not already exist, ignoring errors
453
+ #
454
+ # @return [nil]
455
+ #
138
456
  def check_if_create
139
- @base.lib.branch_new(@name)
457
+ branch_repository.branch_new(@name)
140
458
  rescue StandardError
141
459
  nil
142
460
  end
461
+
462
+ # @return [Git::Repository]
463
+ #
464
+ # @api private
465
+ #
466
+ def branch_repository
467
+ @base
468
+ end
143
469
  end
144
470
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Represents a branch that failed to be deleted
5
+ #
6
+ # This is an immutable data object returned as part of {Git::BranchDeleteResult}
7
+ # when one or more branches could not be deleted.
8
+ #
9
+ # @example
10
+ # failure = Git::BranchDeleteFailure.new(
11
+ # name: 'nonexistent',
12
+ # error_message: "branch 'nonexistent' not found."
13
+ # )
14
+ # failure.name #=> 'nonexistent'
15
+ # failure.error_message #=> "branch 'nonexistent' not found."
16
+ #
17
+ # @see Git::BranchDeleteResult
18
+ # @see Git::Commands::Branch::Delete
19
+ #
20
+ # @api public
21
+ #
22
+ # @!attribute [r] name
23
+ # The name of the branch that failed to be deleted
24
+ # @return [String]
25
+ #
26
+ # @!attribute [r] error_message
27
+ # The error message from git explaining why the branch could not be deleted
28
+ # @return [String]
29
+ #
30
+ BranchDeleteFailure = Data.define(:name, :error_message)
31
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/branch_info'
4
+ require 'git/branch_delete_failure'
5
+
6
+ module Git
7
+ # Represents the result of a branch delete operation
8
+ #
9
+ # This is an immutable data object returned by {Git::Commands::Branch::Delete#call}.
10
+ # It contains information about which branches were successfully deleted and which
11
+ # failed to be deleted, along with the reason for each failure.
12
+ #
13
+ # Git's `git branch -d` command uses "best effort" semantics - it deletes as many
14
+ # branches as possible and reports errors for those that couldn't be deleted. This
15
+ # result object reflects that behavior, allowing callers to inspect both
16
+ # successes and failures.
17
+ #
18
+ # @example Successful deletion of all branches
19
+ # result = branch_delete.call('feature-1', 'feature-2')
20
+ # result.success? #=> true
21
+ # result.deleted.map(&:name) #=> ['feature-1', 'feature-2']
22
+ # result.not_deleted #=> []
23
+ #
24
+ # @example Partial failure (some branches deleted, some not found)
25
+ # result = branch_delete.call('feature-1', 'nonexistent', 'feature-2')
26
+ # result.success? #=> false
27
+ # result.deleted.map(&:name) #=> ['feature-1', 'feature-2']
28
+ # result.not_deleted.first.name #=> 'nonexistent'
29
+ # result.not_deleted.first.error_message #=> "branch 'nonexistent' not found."
30
+ #
31
+ # @see Git::BranchInfo
32
+ # @see Git::BranchDeleteFailure
33
+ # @see Git::Commands::Branch::Delete
34
+ #
35
+ # @api public
36
+ #
37
+ # @!attribute [r] deleted
38
+ # Branches that were successfully deleted
39
+ # @return [Array<Git::BranchInfo>]
40
+ #
41
+ # @!attribute [r] not_deleted
42
+ # Branches that could not be deleted, with the reason for each failure
43
+ # @return [Array<Git::BranchDeleteFailure>]
44
+ #
45
+ BranchDeleteResult = Data.define(:deleted, :not_deleted) do
46
+ # Returns true if all requested branches were successfully deleted
47
+ #
48
+ # @return [Boolean] true if no branches failed to delete, false otherwise
49
+ #
50
+ # @example
51
+ # result = branch_delete.call('feature-branch')
52
+ # if result.success?
53
+ # puts "All branches deleted successfully"
54
+ # else
55
+ # puts "Some branches could not be deleted:"
56
+ # result.not_deleted.each { |f| puts " #{f.name}: #{f.error_message}" }
57
+ # end
58
+ #
59
+ def success?
60
+ not_deleted.empty?
61
+ end
62
+ end
63
+ end