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
data/lib/git/branch.rb CHANGED
@@ -1,102 +1,381 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'git/path'
3
+ require 'git/base'
4
+ require_relative 'branch_info'
4
5
 
5
6
  module Git
6
7
  # Represents a Git branch
8
+ #
9
+ # Branch objects provide access to branch metadata and operations like checkout,
10
+ # delete, and merge. They should be obtained via {Git::Base#branch} or
11
+ # {Git::Base#branches}, not constructed directly.
12
+ #
13
+ # @example Getting a branch
14
+ # git = Git.open('.')
15
+ # branch = git.branch('main')
16
+ # branch.checkout
17
+ #
18
+ # @example Listing branches
19
+ # git.branches.each { |b| puts b.name }
20
+ #
21
+ # @api public
22
+ #
7
23
  class Branch
8
- attr_accessor :full, :remote, :name
24
+ # The full refname of this branch
25
+ #
26
+ # For local branches this is the short name (e.g. `'main'`). For
27
+ # remote-tracking branches obtained via {Git::Base#branches} this includes
28
+ # the `remotes/` prefix (e.g. `'remotes/origin/main'`). Branches constructed
29
+ # by {Git::Remote#branch} use the `<remote>/<branch>` form (e.g.
30
+ # `'origin/main'`) which does **not** populate {#remote}.
31
+ #
32
+ # @example Local and remote-tracking branch full refnames
33
+ # git.branch('main').full #=> 'main'
34
+ # git.branch('remotes/origin/main').full #=> 'remotes/origin/main'
35
+ #
36
+ # @return [String] the full refname
37
+ #
38
+ attr_accessor :full
9
39
 
10
- def initialize(base, name)
11
- @full = name
40
+ # The remote for this branch, or `nil` for local or bare-name remote-tracking branches
41
+ #
42
+ # Set to a {Git::Remote} object only when this branch was initialized with a
43
+ # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix. `nil` for local
44
+ # branches and for remote-tracking branches in `<remote>/<branch>` form
45
+ # (such as those returned by {Git::Remote#branch}).
46
+ #
47
+ # @example Local and remote-tracking branches
48
+ # git.branch('main').remote #=> nil
49
+ # git.branch('remotes/origin/main').remote #=> #<Git::Remote 'origin'>
50
+ # git.remote('origin').branch('main').remote #=> nil # uses 'origin/main' form
51
+ #
52
+ # @return [Git::Remote, nil] the remote object, or `nil`
53
+ #
54
+ attr_accessor :remote
55
+
56
+ # The short branch name without the remote prefix
57
+ #
58
+ # For both local and remote-tracking branches this is the bare branch
59
+ # name (e.g. `'main'` rather than `'remotes/origin/main'`).
60
+ #
61
+ # @example Local and remote-tracking branch short names
62
+ # git.branch('main').name #=> 'main'
63
+ # git.branch('remotes/origin/main').name #=> 'main'
64
+ #
65
+ # @return [String] the short branch name
66
+ #
67
+ attr_accessor :name
68
+
69
+ # Initialize a new Branch object
70
+ #
71
+ # @param base [Git::Base, Git::Repository] the git repository
72
+ #
73
+ # Accepts either a {Git::Base} (legacy) or a {Git::Repository} (new form).
74
+ # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
75
+ # in Phase 4.
76
+ #
77
+ # @param branch_info_or_name [Git::BranchInfo, String] branch info object or name string
78
+ #
79
+ # Passing a BranchInfo is preferred; String support is for backward compatibility.
80
+ #
81
+ # @note Use {Git::Base#branch} or {Git::Base#branches} instead of constructing directly
82
+ #
83
+ # @api private
84
+ #
85
+ def initialize(base, branch_info_or_name)
12
86
  @base = base
13
87
  @gcommit = nil
14
88
  @stashes = nil
15
- @remote, @name = parse_name(name)
89
+
90
+ initialize_from_argument(branch_info_or_name)
16
91
  end
17
92
 
93
+ # Returns the commit at the tip of this branch
94
+ #
95
+ # The result is memoized after the first call.
96
+ #
97
+ # @example Get the tip commit
98
+ # git.branch('main').gcommit #=> #<Git::Object ...>
99
+ #
100
+ # @return [Git::Object] the commit at the tip of this branch
101
+ #
18
102
  def gcommit
19
- @gcommit ||= @base.gcommit(@full)
103
+ @gcommit ||= branch_repository.gcommit(@full)
20
104
  @gcommit
21
105
  end
22
106
 
107
+ # Returns the stash list for this repository
108
+ #
109
+ # The result is memoized after the first call.
110
+ #
111
+ # @example Iterate over stash entries
112
+ # git.branch('main').stashes.each { |s| puts s }
113
+ #
114
+ # @return [Git::Stashes] the stash list
115
+ #
23
116
  def stashes
24
- @stashes ||= Git::Stashes.new(@base)
117
+ @stashes ||= Git::Stashes.new(branch_repository)
25
118
  end
26
119
 
120
+ # Checks out this branch, attempting to create it first if it does not already exist
121
+ #
122
+ # Branch creation is attempted via {#check_if_create}; any error from that
123
+ # step is silently ignored and the checkout proceeds regardless.
124
+ #
125
+ # **Note:** for remote-tracking branches (where {#remote} is not `nil`),
126
+ # `check_if_create` will attempt to create a *local* branch named {#name}
127
+ # as a side-effect before checking out {#full} (which typically results in
128
+ # a detached HEAD). This is a known limitation; see
129
+ # [ruby-git#1280](https://github.com/ruby-git/ruby-git/issues/1280).
130
+ #
131
+ # @example Check out a branch
132
+ # git = Git.open('.')
133
+ # git.branch('main').checkout
134
+ #
135
+ # @return [String] git's stdout from the checkout
136
+ #
137
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
138
+ #
27
139
  def checkout
28
140
  check_if_create
29
- @base.checkout(@full)
141
+ branch_repository.checkout(@full)
30
142
  end
31
143
 
144
+ # Archives this branch and writes the result to a file
145
+ #
146
+ # @example Archive to a tar file
147
+ # git.branch('main').archive('/tmp/main.tar')
148
+ #
149
+ # @example Archive to a zip file
150
+ # git.branch('main').archive('/tmp/main.zip', format: 'zip')
151
+ #
152
+ # @param file [String] path to the destination archive file
153
+ #
154
+ # @param opts [Hash] archive options (see {Git::Base#archive})
155
+ #
156
+ # @return [String] the path to the written archive file
157
+ #
158
+ # @raise [Git::FailedError] if `git archive` fails
159
+ #
32
160
  def archive(file, opts = {})
33
- @base.lib.archive(@full, file, opts)
161
+ branch_repository.archive(@full, file, opts)
34
162
  end
35
163
 
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
164
+ # Checks out this branch for the duration of a block, then restores the original branch
165
+ #
166
+ # If the block returns a truthy value, all pending changes are committed with the
167
+ # given message before switching back to the original branch. If the block returns
168
+ # a falsy value, a hard reset is performed before switching back.
169
+ #
170
+ # **Note:** the restore checkout is not wrapped in `ensure`. If the block,
171
+ # the commit, or the reset raises an exception, the repository will be left
172
+ # checked out on this branch rather than restored to the original.
173
+ #
174
+ # @example Commit a new file on a feature branch
175
+ # git.branch('feature').in_branch('Add README') do
176
+ # File.write('README.md', '# Hello')
177
+ # git.add('README.md')
178
+ # true # commit and return to original branch
179
+ # end
180
+ #
181
+ # @param message [String] commit message used when the block returns truthy
182
+ #
183
+ # @yield Executes the block with this branch checked out
184
+ #
185
+ # @yieldreturn [Object] return a truthy value to commit all changes, a falsy value to hard-reset
186
+ #
187
+ # @return [String] git's stdout from the final checkout back to the original branch
188
+ #
189
+ # @raise [Git::FailedError] if any of the underlying git operations (checkout, commit, reset) fail
190
+ #
41
191
  def in_branch(message = 'in branch work')
42
- old_current = @base.lib.branch_current
192
+ old_current = branch_repository.current_branch
43
193
  checkout
44
194
  if yield
45
- @base.commit_all(message)
195
+ branch_repository.commit_all(message)
46
196
  else
47
- @base.reset_hard
197
+ branch_repository.reset(nil, hard: true)
48
198
  end
49
- @base.checkout(old_current)
199
+ branch_repository.checkout(old_current)
50
200
  end
51
201
 
202
+ # Creates this branch if it does not already exist
203
+ #
204
+ # Silently ignores any error raised during branch creation (including the case
205
+ # where the branch already exists).
206
+ #
207
+ # @example Create a new branch
208
+ # git.branch('feature').create
209
+ #
210
+ # @return [nil]
211
+ #
52
212
  def create
53
213
  check_if_create
54
214
  end
55
215
 
216
+ # Deletes this branch
217
+ #
218
+ # Remote-tracking branches (one where {#remote} is not `nil`) delete the
219
+ # local remote-tracking ref; they do not push a deletion to the remote.
220
+ #
221
+ # @example Delete a local branch
222
+ # git.branch('old-feature').delete
223
+ #
224
+ # @return [String] git's deletion output
225
+ #
226
+ # @raise [Git::Error] if the branch cannot be deleted
227
+ #
56
228
  def delete
57
- @base.lib.branch_delete(@name)
229
+ if @remote
230
+ branch_repository.branch_delete("#{@remote.name}/#{@name}", remotes: true)
231
+ else
232
+ branch_repository.branch_delete(@name)
233
+ end
58
234
  end
59
235
 
236
+ # Returns true if this is the currently checked-out branch
237
+ #
238
+ # **Note:** this compares the current branch's short name against {#name}.
239
+ # For a remote-tracking branch (where {#remote} is not `nil`), {#name} is
240
+ # still the bare short name (e.g. `'main'`), so this will return `true`
241
+ # whenever the *local* branch with that name is checked out — not the
242
+ # remote-tracking ref itself.
243
+ #
244
+ # @example Check whether currently on main
245
+ # git.branch('main').current #=> true
246
+ #
247
+ # @return [Boolean] whether this branch is currently checked out
248
+ #
249
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
250
+ #
60
251
  def current # rubocop:disable Naming/PredicateMethod
61
- @base.lib.branch_current == @name
252
+ branch_repository.current_branch == @name
62
253
  end
63
254
 
255
+ # Returns true if this branch contains the given commit
256
+ #
257
+ # **Note:** this queries local branches by short name. For a remote-tracking
258
+ # branch (where {#remote} is not `nil`), it checks the *local* branch with
259
+ # the same {#name} rather than the remote-tracking ref, which may give an
260
+ # inaccurate result.
261
+ #
262
+ # @example Check if a commit is reachable from this branch
263
+ # git.branch('main').contains?('abc1234') #=> true
264
+ #
265
+ # @param commit [String] the commit SHA or ref to check
266
+ #
267
+ # @return [Boolean] whether this branch contains the given commit
268
+ #
269
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
270
+ #
64
271
  def contains?(commit)
65
- !@base.lib.branch_contains(commit, name).empty?
272
+ !branch_repository.branch_contains(commit, name).empty?
66
273
  end
67
274
 
275
+ # Merges a branch into this branch, or merges this branch into the current branch
276
+ #
277
+ # @overload merge(branch, message = nil)
278
+ #
279
+ # Temporarily checks out this branch, merges the given branch into it,
280
+ # then restores the original branch.
281
+ #
282
+ # **Note:** if `self` is a remote-tracking branch (where {#remote} is not
283
+ # `nil`), this delegates to {#checkout} which has the detached-HEAD
284
+ # side-effect described there. The remote-tracking ref will not be updated.
285
+ #
286
+ # @example Merge a feature branch into main
287
+ # git.branch('main').merge('feature')
288
+ #
289
+ # @param branch [String] the name of the branch to merge into this one
290
+ #
291
+ # @param message [String, nil] commit message for the merge commit
292
+ #
293
+ # @return [String] git's stdout from the final checkout back to the original branch
294
+ #
295
+ # @overload merge()
296
+ #
297
+ # Merges this branch into the currently checked-out branch.
298
+ #
299
+ # @example Merge main into the current branch
300
+ # git.branch('main').merge
301
+ #
302
+ # @return [String] git's stdout from the merge command
303
+ #
304
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
305
+ #
68
306
  def merge(branch = nil, message = nil)
69
307
  if branch
70
308
  in_branch do
71
- @base.merge(branch, message)
309
+ branch_repository.merge(branch, message)
72
310
  false
73
311
  end
74
312
  # merge a branch into this one
75
313
  else
76
314
  # merge this branch into the current one
77
- @base.merge(@name)
315
+ branch_repository.merge(@name)
78
316
  end
79
317
  end
80
318
 
319
+ # Updates the git ref for this branch to point to the given commit
320
+ #
321
+ # The target ref depends on whether {#remote} is set:
322
+ # - When {#remote} is not `nil` (i.e. the branch was initialized with a
323
+ # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix), updates
324
+ # `refs/remotes/<remote>/<name>`.
325
+ # - Otherwise updates `refs/heads/<name>`. Note that branches in the
326
+ # `<remote>/<branch>` form (e.g. those returned by {Git::Remote#branch})
327
+ # have `remote == nil` and therefore update `refs/heads/<remote>/<name>`,
328
+ # **not** `refs/remotes/...`.
329
+ #
330
+ # @example Advance a local branch to a new commit
331
+ # git.branch('feature').update_ref('abc1234def5678')
332
+ #
333
+ # @param commit [String] the commit SHA to point this branch at
334
+ #
335
+ # @return [Git::CommandLineResult] the result of calling `git update-ref`
336
+ #
337
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
338
+ #
81
339
  def update_ref(commit)
82
340
  if @remote
83
- @base.lib.update_ref("refs/remotes/#{@remote.name}/#{@name}", commit)
341
+ branch_repository.update_ref("remotes/#{@remote.name}/#{@name}", commit)
84
342
  else
85
- @base.lib.update_ref("refs/heads/#{@name}", commit)
343
+ branch_repository.update_ref(@name, commit)
86
344
  end
87
345
  end
88
346
 
347
+ # Returns this branch as a single-element array containing its full refname
348
+ #
349
+ # @example Get branch as array
350
+ # git.branch('main').to_a #=> ['main']
351
+ #
352
+ # @return [Array<String>] a single-element array containing the full refname
353
+ #
89
354
  def to_a
90
355
  [@full]
91
356
  end
92
357
 
358
+ # Returns the full refname of this branch as a string
359
+ #
360
+ # @example Get branch as string
361
+ # git.branch('main').to_s #=> 'main'
362
+ #
363
+ # @return [String] the full refname
364
+ #
93
365
  def to_s
94
366
  @full
95
367
  end
96
368
 
369
+ # Regular expression for parsing branch refnames
370
+ #
371
+ # Matches full and short refnames, capturing an optional remote name and the
372
+ # branch name. Used internally to identify remote-tracking branches.
373
+ #
374
+ # @api private
375
+ #
97
376
  BRANCH_NAME_REGEXP = %r{
98
377
  ^
99
- # Optional 'refs/remotes/' at the beggining to specify a remote tracking branch
378
+ # Optional 'remotes/' or 'refs/remotes/' at the beginning to specify a remote tracking branch
100
379
  # with a <remote_name>. <remote_name> is nil if not present.
101
380
  (?:
102
381
  (?:(?:refs/)?remotes/)(?<remote_name>[^/]+)/
@@ -107,26 +386,66 @@ module Git
107
386
 
108
387
  private
109
388
 
110
- # Given a full branch name return an Array containing the remote and branch names.
389
+ # Dispatches initialization to the appropriate strategy
390
+ #
391
+ # @param branch_info_or_name [Git::BranchInfo, String] branch info or name string
392
+ #
393
+ # @return [nil]
111
394
  #
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 '/').
395
+ # @api private
115
396
  #
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']
397
+ def initialize_from_argument(branch_info_or_name)
398
+ if branch_info_or_name.is_a?(Git::BranchInfo)
399
+ initialize_from_branch_info(branch_info_or_name)
400
+ else
401
+ initialize_from_name(branch_info_or_name)
402
+ end
403
+ end
404
+
405
+ # Initialize from a BranchInfo object (preferred path)
121
406
  #
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']
407
+ # @param branch_info [Git::BranchInfo] the branch info
408
+ #
409
+ # @return [nil]
410
+ #
411
+ def initialize_from_branch_info(branch_info)
412
+ @full = branch_info.refname
413
+ @name = branch_info.short_name
414
+ @remote = branch_info.remote_name ? Git::Remote.new(@base, branch_info.remote_name) : nil
415
+ end
416
+
417
+ # Initialize from a string name (legacy path for backward compatibility)
418
+ #
419
+ # @param name [String] the branch name
420
+ #
421
+ # @return [nil]
422
+ #
423
+ def initialize_from_name(name)
424
+ @full = name
425
+ @remote, @name = parse_name(name)
426
+ end
427
+
428
+ # Parses a full branch name into remote and short branch name components
429
+ #
430
+ # Strips an optional `remotes/` or `refs/remotes/` prefix. Only inputs that
431
+ # begin with one of those prefixes yield a remote object; all other inputs
432
+ # (including `'origin/master'`) are treated as local branch names with a
433
+ # `nil` remote.
434
+ #
435
+ # @example Local branches
436
+ # parse_name('master') #=> [nil, 'master']
437
+ # parse_name('origin/master') #=> [nil, 'origin/master']
438
+ #
439
+ # @example Remote-tracking branches
440
+ # parse_name('remotes/origin/master') #=> [#<Git::Remote 'origin'>, 'master']
441
+ # parse_name('refs/remotes/origin/master') #=> [#<Git::Remote 'origin'>, 'master']
442
+ #
443
+ # @param name [String] the full branch name to parse
444
+ #
445
+ # @return [Array(Git::Remote, String)] a two-element array; the first element is
446
+ # a {Git::Remote} for remote-tracking branches or `nil` for local branches,
447
+ # and the second element is the short branch name
127
448
  #
128
- # param [String] name branch full name.
129
- # return [<Git::Remote,NilClass,String>] an Array containing the remote and branch names.
130
449
  def parse_name(name)
131
450
  # Expect this will always match
132
451
  match = name.match(BRANCH_NAME_REGEXP)
@@ -135,10 +454,28 @@ module Git
135
454
  [remote, branch_name]
136
455
  end
137
456
 
457
+ # Creates the branch if it does not already exist, ignoring errors
458
+ #
459
+ # @return [nil]
460
+ #
138
461
  def check_if_create
139
- @base.lib.branch_new(@name)
462
+ branch_repository.branch_new(@name)
140
463
  rescue StandardError
141
464
  nil
142
465
  end
466
+
467
+ # Resolves the {Git::Repository} for this branch
468
+ #
469
+ # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
470
+ # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
471
+ # in Phase 4.
472
+ #
473
+ # @return [Git::Repository]
474
+ #
475
+ # @api private
476
+ #
477
+ def branch_repository
478
+ @base.is_a?(Git::Base) ? @base.facade_repository : @base
479
+ end
143
480
  end
144
481
  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