git 4.4.0 → 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 -53
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -17
  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 +488 -412
  52. data/lib/git/branch.rb +117 -59
  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 +226 -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 +117 -56
  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 +348 -19
  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,6 +1,7 @@
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
@@ -28,7 +29,7 @@ module Git
28
29
  # by {Git::Remote#branch} use the `<remote>/<branch>` form (e.g.
29
30
  # `'origin/main'`) which does **not** populate {#remote}.
30
31
  #
31
- # @example
32
+ # @example Local and remote-tracking branch full refnames
32
33
  # git.branch('main').full #=> 'main'
33
34
  # git.branch('remotes/origin/main').full #=> 'remotes/origin/main'
34
35
  #
@@ -43,7 +44,7 @@ module Git
43
44
  # branches and for remote-tracking branches in `<remote>/<branch>` form
44
45
  # (such as those returned by {Git::Remote#branch}).
45
46
  #
46
- # @example
47
+ # @example Local and remote-tracking branches
47
48
  # git.branch('main').remote #=> nil
48
49
  # git.branch('remotes/origin/main').remote #=> #<Git::Remote 'origin'>
49
50
  # git.remote('origin').branch('main').remote #=> nil # uses 'origin/main' form
@@ -54,37 +55,39 @@ module Git
54
55
 
55
56
  # The short branch name without the remote prefix
56
57
  #
57
- # For branches initialized with a `remotes/` or `refs/remotes/` prefix, the
58
- # prefix is stripped and this returns the bare branch name (e.g. `'main'`
59
- # rather than `'remotes/origin/main'`). For branches in the
60
- # `<remote>/<branch>` form (such as those created by {Git::Remote#branch}),
61
- # no stripping occurs and `name` returns the full form (e.g. `'origin/main'`).
58
+ # For both local and remote-tracking branches this is the bare branch
59
+ # name (e.g. `'main'` rather than `'remotes/origin/main'`).
62
60
  #
63
- # @example
64
- # git.branch('main').name #=> 'main'
65
- # git.branch('remotes/origin/main').name #=> 'main'
66
- # git.remote('origin').branch('main').name #=> 'origin/main'
61
+ # @example Local and remote-tracking branch short names
62
+ # git.branch('main').name #=> 'main'
63
+ # git.branch('remotes/origin/main').name #=> 'main'
67
64
  #
68
- # @return [String] the branch name
65
+ # @return [String] the short branch name
69
66
  #
70
67
  attr_accessor :name
71
68
 
72
69
  # Initialize a new Branch object
73
70
  #
74
- # @api private
71
+ # @param base [Git::Base, Git::Repository] the git repository
75
72
  #
76
- # @note Use {Git::Base#branch} or {Git::Base#branches} instead of constructing directly
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.
77
76
  #
78
- # @param base [Git::Base] the git repository
77
+ # @param branch_info_or_name [Git::BranchInfo, String] branch info object or name string
79
78
  #
80
- # @param name [String] the full or short branch name
79
+ # Passing a BranchInfo is preferred; String support is for backward compatibility.
81
80
  #
82
- def initialize(base, name)
83
- @full = name
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)
84
86
  @base = base
85
87
  @gcommit = nil
86
88
  @stashes = nil
87
- @remote, @name = parse_name(name)
89
+
90
+ initialize_from_argument(branch_info_or_name)
88
91
  end
89
92
 
90
93
  # Returns the commit at the tip of this branch
@@ -97,7 +100,7 @@ module Git
97
100
  # @return [Git::Object] the commit at the tip of this branch
98
101
  #
99
102
  def gcommit
100
- @gcommit ||= @base.gcommit(@full)
103
+ @gcommit ||= branch_repository.gcommit(@full)
101
104
  @gcommit
102
105
  end
103
106
 
@@ -111,7 +114,7 @@ module Git
111
114
  # @return [Git::Stashes] the stash list
112
115
  #
113
116
  def stashes
114
- @stashes ||= Git::Stashes.new(@base)
117
+ @stashes ||= Git::Stashes.new(branch_repository)
115
118
  end
116
119
 
117
120
  # Checks out this branch, attempting to create it first if it does not already exist
@@ -120,8 +123,10 @@ module Git
120
123
  # step is silently ignored and the checkout proceeds regardless.
121
124
  #
122
125
  # **Note:** for remote-tracking branches (where {#remote} is not `nil`),
123
- # {#full} is a ref such as `'remotes/origin/main'`. Checking out a
124
- # remote-tracking ref places the repository in a **detached HEAD** state.
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).
125
130
  #
126
131
  # @example Check out a branch
127
132
  # git = Git.open('.')
@@ -133,7 +138,7 @@ module Git
133
138
  #
134
139
  def checkout
135
140
  check_if_create
136
- @base.checkout(@full)
141
+ branch_repository.checkout(@full)
137
142
  end
138
143
 
139
144
  # Archives this branch and writes the result to a file
@@ -153,7 +158,7 @@ module Git
153
158
  # @raise [Git::FailedError] if `git archive` fails
154
159
  #
155
160
  def archive(file, opts = {})
156
- @base.lib.archive(@full, file, opts)
161
+ branch_repository.archive(@full, file, opts)
157
162
  end
158
163
 
159
164
  # Checks out this branch for the duration of a block, then restores the original branch
@@ -184,14 +189,14 @@ module Git
184
189
  # @raise [Git::FailedError] if any of the underlying git operations (checkout, commit, reset) fail
185
190
  #
186
191
  def in_branch(message = 'in branch work')
187
- old_current = @base.lib.branch_current
192
+ old_current = branch_repository.current_branch
188
193
  checkout
189
194
  if yield
190
- @base.commit_all(message)
195
+ branch_repository.commit_all(message)
191
196
  else
192
- @base.reset_hard
197
+ branch_repository.reset(nil, hard: true)
193
198
  end
194
- @base.checkout(old_current)
199
+ branch_repository.checkout(old_current)
195
200
  end
196
201
 
197
202
  # Creates this branch if it does not already exist
@@ -202,8 +207,7 @@ module Git
202
207
  # @example Create a new branch
203
208
  # git.branch('feature').create
204
209
  #
205
- # @return [String, nil] git's stdout from branch creation (typically empty),
206
- # or `nil` if an error was rescued
210
+ # @return [nil]
207
211
  #
208
212
  def create
209
213
  check_if_create
@@ -211,22 +215,22 @@ module Git
211
215
 
212
216
  # Deletes this branch
213
217
  #
214
- # **Note:** this method only works correctly for local branches. Calling it on
215
- # a remote-tracking branch (one where {#remote} is not `nil`) will attempt to
216
- # delete a *local* branch with the same short name rather than the
217
- # remote-tracking ref, which is almost certainly not what you want.
218
- # See [ruby-git#1280](https://github.com/ruby-git/ruby-git/issues/1280) for
219
- # the planned fix.
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
220
  #
221
221
  # @example Delete a local branch
222
222
  # git.branch('old-feature').delete
223
223
  #
224
224
  # @return [String] git's deletion output
225
225
  #
226
- # @raise [Git::FailedError] if the branch cannot be deleted
226
+ # @raise [Git::Error] if the branch cannot be deleted
227
227
  #
228
228
  def delete
229
- @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
230
234
  end
231
235
 
232
236
  # Returns true if this is the currently checked-out branch
@@ -241,11 +245,11 @@ module Git
241
245
  # git.branch('main').current #=> true
242
246
  #
243
247
  # @return [Boolean] whether this branch is currently checked out
244
- # @raise [Git::FailedError] if git exits with a non-zero exit status
245
248
  #
249
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
246
250
  #
247
251
  def current # rubocop:disable Naming/PredicateMethod
248
- @base.lib.branch_current == @name
252
+ branch_repository.current_branch == @name
249
253
  end
250
254
 
251
255
  # Returns true if this branch contains the given commit
@@ -261,11 +265,11 @@ module Git
261
265
  # @param commit [String] the commit SHA or ref to check
262
266
  #
263
267
  # @return [Boolean] whether this branch contains the given commit
264
- # @raise [Git::FailedError] if git exits with a non-zero exit status
265
268
  #
269
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
266
270
  #
267
271
  def contains?(commit)
268
- !@base.lib.branch_contains(commit, name).empty?
272
+ !branch_repository.branch_contains(commit, name).empty?
269
273
  end
270
274
 
271
275
  # Merges a branch into this branch, or merges this branch into the current branch
@@ -297,26 +301,25 @@ module Git
297
301
  #
298
302
  # @return [String] git's stdout from the merge command
299
303
  #
300
- # @raise [Git::FailedError] if git exits with a non-zero exit status during
301
- # the merge, checkout, commit, or reset operations
304
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
302
305
  #
303
306
  def merge(branch = nil, message = nil)
304
307
  if branch
305
308
  in_branch do
306
- @base.merge(branch, message)
309
+ branch_repository.merge(branch, message)
307
310
  false
308
311
  end
309
312
  # merge a branch into this one
310
313
  else
311
314
  # merge this branch into the current one
312
- @base.merge(@name)
315
+ branch_repository.merge(@name)
313
316
  end
314
317
  end
315
318
 
316
319
  # Updates the git ref for this branch to point to the given commit
317
320
  #
318
321
  # The target ref depends on whether {#remote} is set:
319
- # - When {#remote} is not `nil` (i.e. the branch was initialised with a
322
+ # - When {#remote} is not `nil` (i.e. the branch was initialized with a
320
323
  # `remotes/<remote>/` or `refs/remotes/<remote>/` prefix), updates
321
324
  # `refs/remotes/<remote>/<name>`.
322
325
  # - Otherwise updates `refs/heads/<name>`. Note that branches in the
@@ -329,15 +332,15 @@ module Git
329
332
  #
330
333
  # @param commit [String] the commit SHA to point this branch at
331
334
  #
332
- # @return [String] the stdout output from `git update-ref`
335
+ # @return [Git::CommandLineResult] the result of calling `git update-ref`
333
336
  #
334
337
  # @raise [Git::FailedError] if git exits with a non-zero exit status
335
338
  #
336
339
  def update_ref(commit)
337
340
  if @remote
338
- @base.lib.update_ref("refs/remotes/#{@remote.name}/#{@name}", commit)
341
+ branch_repository.update_ref("remotes/#{@remote.name}/#{@name}", commit)
339
342
  else
340
- @base.lib.update_ref("refs/heads/#{@name}", commit)
343
+ branch_repository.update_ref(@name, commit)
341
344
  end
342
345
  end
343
346
 
@@ -383,11 +386,51 @@ module Git
383
386
 
384
387
  private
385
388
 
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]
394
+ #
395
+ # @api private
396
+ #
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)
406
+ #
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
+
386
428
  # Parses a full branch name into remote and short branch name components
387
429
  #
388
- # Strips an optional `remotes/` or `refs/remotes/` prefix. Only inputs that begin
389
- # with one of those prefixes yield a remote object; all other inputs (including
390
- # `'origin/master'`) are treated as local branch names with a `nil` remote.
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.
391
434
  #
392
435
  # @example Local branches
393
436
  # parse_name('master') #=> [nil, 'master']
@@ -399,8 +442,9 @@ module Git
399
442
  #
400
443
  # @param name [String] the full branch name to parse
401
444
  #
402
- # @return [Array(Git::Remote, String)] a two-element array with the remote object
403
- # (or `nil`) and the short branch name
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
404
448
  #
405
449
  def parse_name(name)
406
450
  # Expect this will always match
@@ -412,12 +456,26 @@ module Git
412
456
 
413
457
  # Creates the branch if it does not already exist, ignoring errors
414
458
  #
415
- # @return [String, nil] stdout from branch creation, or `nil` if an error was rescued
459
+ # @return [nil]
416
460
  #
417
461
  def check_if_create
418
- @base.lib.branch_new(@name)
462
+ branch_repository.branch_new(@name)
419
463
  rescue StandardError
420
464
  nil
421
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
422
480
  end
423
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
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Regular expression for parsing branch refnames
5
+ #
6
+ # Captures:
7
+ # - remote_name: the remote name (e.g., 'origin') for remote branches, nil for local
8
+ # - branch_name: the branch name without the remote prefix
9
+ #
10
+ # @note This regex is similar to Git::Branch::BRANCH_NAME_REGEXP but uses \A/\z anchors
11
+ # instead of ^/$ for stricter matching. As part of the architectural redesign,
12
+ # Git::Branch will eventually be refactored to use BranchInfo internally, at which
13
+ # point this will become the single source of truth for branch name parsing.
14
+ #
15
+ # @note This regex assumes remote names do not contain '/'. If a remote name
16
+ # contains '/', parsing will be incorrect. For example, 'remotes/team/upstream/main'
17
+ # would parse as remote_name='team' instead of 'team/upstream'. This is an inherent
18
+ # ambiguity in git refnames that can only be resolved with knowledge of configured
19
+ # remotes. See: https://github.com/ruby-git/ruby-git/issues/919
20
+ #
21
+ # @example
22
+ # 'main' => { remote_name: nil, branch_name: 'main' }
23
+ # 'remotes/origin/main' => { remote_name: 'origin', branch_name: 'main' }
24
+ # 'feature/foo' => { remote_name: nil, branch_name: 'feature/foo' }
25
+ # 'remotes/origin/feature/bar' => { remote_name: 'origin', branch_name: 'feature/bar' }
26
+ #
27
+ # @api private
28
+ BRANCH_REFNAME_REGEXP = %r{
29
+ \A # start of string
30
+ (?:(?:refs/)?remotes/(?<remote_name>[^/]+)/)? # optional 'refs?/remotes/<remote_name>/'
31
+ (?<branch_name>.+) # branch name (everything else)
32
+ \z # end of string
33
+ }x
34
+
35
+ # Value object representing branch metadata from git branch output
36
+ #
37
+ # This is a lightweight, immutable data structure returned by branch listing
38
+ # commands. It contains only the data parsed from git output without any
39
+ # repository context or operations.
40
+ #
41
+ # @example Creating from git branch output
42
+ # info = Git::BranchInfo.new(
43
+ # refname: 'main',
44
+ # target_oid: 'abc123def456789012345678901234567890abcd',
45
+ # current: true,
46
+ # worktree: false,
47
+ # symref: nil,
48
+ # upstream: nil
49
+ # )
50
+ # info.current? #=> true
51
+ # info.remote? #=> false
52
+ # info.short_name #=> 'main'
53
+ #
54
+ # @example Remote branch
55
+ # info = Git::BranchInfo.new(
56
+ # refname: 'remotes/origin/main',
57
+ # target_oid: 'abc123def456789012345678901234567890abcd',
58
+ # current: false,
59
+ # worktree: false,
60
+ # symref: nil,
61
+ # upstream: nil
62
+ # )
63
+ # info.remote? #=> true
64
+ # info.remote_name #=> 'origin'
65
+ # info.short_name #=> 'main'
66
+ #
67
+ # @example Local branch with upstream tracking
68
+ # upstream_info = Git::BranchInfo.new(
69
+ # refname: 'remotes/origin/main',
70
+ # target_oid: 'abc123def456789012345678901234567890abcd',
71
+ # current: false,
72
+ # worktree: false,
73
+ # symref: nil,
74
+ # upstream: nil
75
+ # )
76
+ # info = Git::BranchInfo.new(
77
+ # refname: 'main',
78
+ # target_oid: 'abc123def456789012345678901234567890abcd',
79
+ # current: true,
80
+ # worktree: false,
81
+ # symref: nil,
82
+ # upstream: upstream_info
83
+ # )
84
+ # info.upstream.remote_name #=> 'origin'
85
+ #
86
+ # @see Git::Branch for the full-featured branch object with operations
87
+ #
88
+ # @see Git::Commands::Branch::List for the command that produces these
89
+ #
90
+ # @api public
91
+ #
92
+ # @!attribute [r] refname
93
+ #
94
+ # The full reference name of the branch
95
+ #
96
+ # @return [String] the branch refname (e.g., 'main', 'remotes/origin/main')
97
+ #
98
+ # @!attribute [r] target_oid
99
+ #
100
+ # The commit object ID (SHA) that this branch points to
101
+ #
102
+ # @return [String, nil] the full 40-character object ID, or nil if unavailable
103
+ #
104
+ # @!attribute [r] current
105
+ #
106
+ # Whether this branch is currently checked out in the current worktree
107
+ #
108
+ # @return [Boolean] true if this is the current branch
109
+ #
110
+ # @!attribute [r] worktree
111
+ #
112
+ # Whether this branch is checked out in another linked worktree
113
+ #
114
+ # @return [Boolean] true if checked out in a different worktree
115
+ #
116
+ # @!attribute [r] symref
117
+ #
118
+ # The target reference if this is a symbolic reference
119
+ #
120
+ # @return [String, nil] the target ref (e.g., 'refs/heads/main'), or nil if not a symref
121
+ #
122
+ # @!attribute [r] upstream
123
+ #
124
+ # The configured upstream/tracking branch
125
+ #
126
+ # @return [Git::BranchInfo, nil] the upstream branch info, or nil if no upstream is configured
127
+ #
128
+ # @note Remote-tracking branches (e.g., 'origin/main') have upstream: nil
129
+ #
130
+ # @note When upstream exists but the remote-tracking branch hasn't been fetched,
131
+ # the upstream's target_oid may be nil
132
+ #
133
+ BranchInfo = Data.define(:refname, :target_oid, :current, :worktree, :symref, :upstream) do
134
+ # @return [Boolean] always false for BranchInfo (see DetachedHeadInfo for detached state)
135
+ def detached? = false
136
+
137
+ # @return [Boolean] true if this is an unborn branch (no commits yet)
138
+ def unborn? = target_oid.nil?
139
+
140
+ # @return [Boolean] true if this is the currently checked out branch
141
+ def current? = current
142
+
143
+ # @return [Boolean] true if this branch is checked out in another worktree
144
+ def worktree? = worktree
145
+
146
+ # @return [Boolean] true if this is a symbolic reference
147
+ def symref? = !symref.nil?
148
+
149
+ # @return [Boolean] true if this is a remote-tracking branch
150
+ def remote? = !remote_name.nil?
151
+
152
+ # @return [String, nil] the name of the remote (e.g., 'origin'), or nil for local branches
153
+ def remote_name
154
+ parse_refname[:remote_name]
155
+ end
156
+
157
+ # @return [String] the branch name without remote prefix (e.g., 'main' or 'feature/foo')
158
+ def short_name
159
+ parse_refname[:branch_name]
160
+ end
161
+
162
+ # @return [String] string representation (the full refname)
163
+ def to_s = refname
164
+
165
+ private
166
+
167
+ # Parse the refname and return match data
168
+ #
169
+ # The regex is guaranteed to match any non-empty string due to the `.+` pattern,
170
+ # so we don't need nil checking. If refname is empty/nil, this would fail at
171
+ # object creation time since refname is a required attribute.
172
+ #
173
+ # @return [MatchData] the match result
174
+ def parse_refname
175
+ refname.match(Git::BRANCH_REFNAME_REGEXP)
176
+ end
177
+ end
178
+ end