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
@@ -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
data/lib/git/branches.rb CHANGED
@@ -1,68 +1,166 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Git
4
- # object that holds all the available branches
4
+ # Collection of all Git branches in a repository
5
+ #
6
+ # Wraps both local and remote-tracking branches and provides filtering,
7
+ # enumeration, and name-based lookup.
8
+ #
9
+ # @example Enumerate all branches
10
+ # branches = repo.branches
11
+ # branches.each { |b| puts b.name }
12
+ #
13
+ # @api public
14
+ #
5
15
  class Branches
6
16
  include Enumerable
7
17
 
18
+ # Creates a new Branches collection populated from the given repository
19
+ #
20
+ # @param base [Git::Repository] the repository to enumerate
21
+ # branches from
22
+ #
23
+ # @return [void]
24
+ #
25
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
26
+ #
8
27
  def initialize(base)
9
28
  @branches = {}
29
+ @lookup = {}
10
30
 
11
31
  @base = base
12
32
 
13
- @base.lib.branches_all.each do |b|
14
- @branches[b[0]] = Git::Branch.new(@base, b[0])
33
+ branch_repository.branches_all.each do |branch_info|
34
+ branch = Git::Branch.new(base, branch_info)
35
+
36
+ @branches[branch_info.refname] = branch
37
+ index_branch_lookup(branch, refname: branch_info.refname)
15
38
  end
16
39
  end
17
40
 
41
+ # Returns all local (non-remote-tracking) branches
42
+ #
43
+ # @example List local branch names
44
+ # repo.branches.local.map(&:name)
45
+ #
46
+ # @return [Array<Git::Branch>] the local branches
47
+ #
18
48
  def local
19
49
  reject(&:remote)
20
50
  end
21
51
 
52
+ # Returns all remote-tracking branches
53
+ #
54
+ # @example List remote branch names
55
+ # repo.branches.remote.map(&:name)
56
+ #
57
+ # @return [Array<Git::Branch>] the remote-tracking branches
58
+ #
22
59
  def remote
23
60
  self.select(&:remote)
24
61
  end
25
62
 
26
- # array like methods
27
-
63
+ # Returns the number of branches in the collection
64
+ #
65
+ # @example Count all branches
66
+ # repo.branches.size # => 3
67
+ #
68
+ # @return [Integer] the total number of branches
69
+ #
28
70
  def size
29
71
  @branches.size
30
72
  end
31
73
 
74
+ # Iterates over every branch in the collection
75
+ #
76
+ # @overload each
77
+ #
78
+ # @example Get an enumerator over all branches
79
+ # enum = repo.branches.each
80
+ #
81
+ # @return [Enumerator<Git::Branch>] an enumerator over all branches
82
+ #
83
+ # @overload each(&block)
84
+ #
85
+ # @example Print every branch name
86
+ # repo.branches.each { |b| puts b.name }
87
+ #
88
+ # @return [Array<Git::Branch>] the full list of branches
89
+ #
90
+ # @yield [branch] passes each branch to the block
91
+ #
92
+ # @yieldparam branch [Git::Branch] a branch in the repository
93
+ #
94
+ # @yieldreturn [void]
95
+ #
32
96
  def each(&)
33
97
  @branches.values.each(&)
34
98
  end
35
99
 
36
- # Returns the target branch
100
+ # Returns the branch with the given name
101
+ #
102
+ # Supports short names (`'main'`), remote-qualified names
103
+ # (`'working/master'`), and full refspec names
104
+ # (`'remotes/working/master'`).
37
105
  #
38
- # Example:
39
- # Given (git branch -a):
40
- # master
41
- # remotes/working/master
106
+ # @example Look up a branch by short name
107
+ # repo.branches['main']
42
108
  #
43
- # g.branches['master'].full #=> 'master'
44
- # g.branches['working/master'].full => 'remotes/working/master'
45
- # g.branches['remotes/working/master'].full => 'remotes/working/master'
109
+ # @example Look up a remote-tracking branch
110
+ # repo.branches['working/master']
111
+ #
112
+ # @param branch_name [#to_s] the name of the branch to retrieve
113
+ #
114
+ # @return [Git::Branch, nil] the matching branch, or `nil` if not found
46
115
  #
47
- # @param [#to_s] branch_name the target branch name.
48
- # @return [Git::Branch] the target branch.
49
116
  def [](branch_name)
50
- @branches.values.each_with_object(@branches) do |branch, branches|
51
- branches[branch.full] ||= branch
52
-
53
- # This is how Git (version 1.7.9.5) works.
54
- # Lets you ignore the 'remotes' if its at the beginning of the branch full
55
- # name (even if is not a real remote branch).
56
- branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ %r{^remotes/.+}
57
- end[branch_name.to_s]
117
+ @lookup[branch_name.to_s]
58
118
  end
59
119
 
120
+ # Returns a string listing all branches, prefixed with `*` for the current branch
121
+ #
122
+ # @example Display all branches
123
+ # puts repo.branches.to_s
124
+ #
125
+ # @return [String] a formatted branch listing
126
+ #
60
127
  def to_s
61
- out = ''
128
+ out = +''
62
129
  @branches.each_value do |b|
63
130
  out << (b.current ? '* ' : ' ') << b.to_s << "\n"
64
131
  end
65
132
  out
66
133
  end
134
+
135
+ private
136
+
137
+ # @return [Git::Repository] the repository used to enumerate branches
138
+ #
139
+ # @api private
140
+ #
141
+ def branch_repository
142
+ @base
143
+ end
144
+
145
+ # Indexes all supported lookup keys for a branch without mutating
146
+ # the canonical `@branches` collection used by enumeration
147
+ #
148
+ # @param branch [Git::Branch] the branch to index
149
+ #
150
+ # @param refname [String] the full refname key to use for primary lookup
151
+ #
152
+ # @return [void]
153
+ #
154
+ # @api private
155
+ #
156
+ def index_branch_lookup(branch, refname:)
157
+ @lookup[refname] ||= branch
158
+ @lookup[branch.full] ||= branch
159
+
160
+ return unless branch.full.start_with?('remotes/')
161
+
162
+ # Mirror git compatibility: allow omitting a leading "remotes/".
163
+ @lookup[branch.full.delete_prefix('remotes/')] ||= branch
164
+ end
67
165
  end
68
166
  end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/command_line'
4
+ require 'git/errors'
5
+
6
+ module Git
7
+ module CommandLine
8
+ # Abstract base class for git command-line execution strategies
9
+ #
10
+ # Concrete subclasses must implement {#run} to execute a git command and
11
+ # return a {Git::CommandLine::Result}. Two implementations are provided:
12
+ #
13
+ # * {Git::CommandLine::Capturing} — buffers stdout and stderr in memory
14
+ # * {Git::CommandLine::Streaming} — streams stdout to a caller-supplied IO
15
+ #
16
+ # @example Instantiate a concrete subclass
17
+ # env = { 'GIT_DIR' => '/path/to/git/dir' }
18
+ # binary_path = '/usr/bin/git'
19
+ # global_opts = %w[--git-dir /path/to/git/dir]
20
+ # logger = Logger.new($stdout)
21
+ # cli = Git::CommandLine::Capturing.new(env, binary_path, global_opts, logger)
22
+ # cli.run('version') #=> #<Git::CommandLine::Result ...>
23
+ #
24
+ # @abstract Subclass and implement {#run}
25
+ #
26
+ # @api public
27
+ #
28
+ class Base
29
+ # Create a Base (or subclass) object
30
+ #
31
+ # @param env [Hash{String => String, nil}] environment variables to set or
32
+ # unset. String values set the variable; `nil` values unset it.
33
+ #
34
+ # @param binary_path [String] the path to the git binary
35
+ #
36
+ # @param global_opts [Array<String>] global options to pass to git
37
+ #
38
+ # @param logger [Logger] the logger to use
39
+ #
40
+ def initialize(env, binary_path, global_opts, logger)
41
+ @env = env
42
+ @binary_path = binary_path
43
+ @global_opts = global_opts
44
+ @logger = logger
45
+ end
46
+
47
+ # Execute a git command and return the result
48
+ #
49
+ # Concrete subclasses must override this method.
50
+ #
51
+ # @raise [NotImplementedError] always — must be implemented by subclasses
52
+ #
53
+ def run(*)
54
+ raise NotImplementedError, "#{self.class}#run is not implemented"
55
+ end
56
+
57
+ # @attribute [r] env
58
+ #
59
+ # Variables to set (or unset) in the git command's environment
60
+ #
61
+ # @example
62
+ # env = { 'GIT_DIR' => '/path/to/git/dir' }
63
+ # cli = Git::CommandLine::Capturing.new(env, '/usr/bin/git', [], Logger.new(nil))
64
+ # cli.env #=> { 'GIT_DIR' => '/path/to/git/dir' }
65
+ #
66
+ # @return [Hash{String => String, nil}]
67
+ #
68
+ # @see https://ruby-doc.org/3.2.1/Process.html#method-c-spawn Process.spawn
69
+ # for details on how to set environment variables using the `env` parameter
70
+ #
71
+ attr_reader :env
72
+
73
+ # @attribute [r] binary_path
74
+ #
75
+ # The path to the command line binary to run
76
+ #
77
+ # @example
78
+ # cli = Git::CommandLine::Capturing.new({}, '/usr/bin/git', [], Logger.new(nil))
79
+ # cli.binary_path #=> '/usr/bin/git'
80
+ #
81
+ # @return [String]
82
+ #
83
+ attr_reader :binary_path
84
+
85
+ # @attribute [r] global_opts
86
+ #
87
+ # The global options to pass to git
88
+ #
89
+ # These are options that are passed to git before the command name and
90
+ # arguments. For example, in `git --git-dir /path/to/git/dir version`, the
91
+ # global options are %w[--git-dir /path/to/git/dir].
92
+ #
93
+ # @example
94
+ # global_opts = %w[--git-dir /path/to/git/dir]
95
+ # cli = Git::CommandLine::Capturing.new({}, '/usr/bin/git', global_opts, Logger.new(nil))
96
+ # cli.global_opts #=> %w[--git-dir /path/to/git/dir]
97
+ #
98
+ # @return [Array<String>]
99
+ #
100
+ attr_reader :global_opts
101
+
102
+ # @attribute [r] logger
103
+ #
104
+ # The logger to use for logging git commands and results
105
+ #
106
+ # @example
107
+ # logger = Logger.new(nil)
108
+ # cli = Git::CommandLine::Capturing.new({}, '/usr/bin/git', [], logger)
109
+ # cli.logger == logger #=> true
110
+ #
111
+ # @return [Logger]
112
+ #
113
+ attr_reader :logger
114
+
115
+ private
116
+
117
+ # Merge caller-supplied options into `defaults` and raise if any unknown keys are present
118
+ #
119
+ # @param defaults [Hash] the allowed keys and their default values (e.g. RUN_OPTION_DEFAULTS)
120
+ #
121
+ # @param options_hash [Hash] caller-supplied options
122
+ #
123
+ # @return [Hash] defaults with any supplied values overridden
124
+ #
125
+ # @raise [ArgumentError] if options_hash contains keys not present in defaults
126
+ #
127
+ # @api private
128
+ #
129
+ def merge_and_validate_options(defaults, options_hash)
130
+ merged = defaults.merge(options_hash)
131
+ extra = merged.keys - defaults.keys
132
+ raise ArgumentError, "Unknown options: #{extra.join(', ')}" if extra.any?
133
+
134
+ merged
135
+ end
136
+
137
+ # Merge the instance-level env with any per-call overrides in options_hash[:env]
138
+ #
139
+ # @param options_hash [Hash] options that may include an :env override
140
+ #
141
+ # @return [Hash{String => String, nil}]
142
+ #
143
+ # @api private
144
+ #
145
+ def merged_env(options_hash)
146
+ env.merge(options_hash[:env] || {})
147
+ end
148
+
149
+ # Yield to a block that calls ProcessExecuter and translate any ProcessExecuter
150
+ # errors to their ruby-git equivalents
151
+ #
152
+ # @raise [ArgumentError] in place of ProcessExecuter::ArgumentError
153
+ #
154
+ # @raise [Git::Error] in place of ProcessExecuter::SpawnError (binary not found or
155
+ # failed to launch)
156
+ #
157
+ # @raise [Git::ProcessIOError] in place of ProcessExecuter::ProcessIOError
158
+ #
159
+ # @raise [Git::ProcessIOError] when a timeout race causes Errno::ESRCH. On Ruby 4.0+,
160
+ # `Process.kill` raises `Errno::ESRCH` when the spawned process exits between the
161
+ # timeout firing and the kill signal being delivered. This is a race in
162
+ # process_executer's timeout handling and is semantically equivalent to a timeout.
163
+ #
164
+ # @return [Object] the return value of the block
165
+ #
166
+ # @api private
167
+ #
168
+ def run_process_executer
169
+ yield
170
+ rescue ProcessExecuter::ArgumentError => e
171
+ raise ::ArgumentError, e.message
172
+ rescue ProcessExecuter::SpawnError => e
173
+ raise Git::Error, e.message, cause: e.cause
174
+ rescue ProcessExecuter::ProcessIOError => e
175
+ raise Git::ProcessIOError, e.message, cause: e.cause
176
+ rescue Errno::ESRCH => e
177
+ # Ruby 4.0+: Process.kill raises Errno::ESRCH when the spawned process exits
178
+ # in the narrow window between the timeout firing and the kill signal being sent.
179
+ # This is a known race in process_executer's timeout handling.
180
+ raise Git::ProcessIOError, "Git process no longer exists (timeout race): #{e.message}", cause: e
181
+ end
182
+
183
+ # Build the git command line from the available sources to send to `Process.spawn`
184
+ #
185
+ # @param args [Array<String>] command-line arguments to append after global options
186
+ #
187
+ # @return [Array<String>]
188
+ #
189
+ # @raise [ArgumentError] if any element of args is itself an Array
190
+ #
191
+ # @api private
192
+ #
193
+ def build_git_cmd(args)
194
+ raise ArgumentError, 'The args array can not contain an array' if args.any?(Array)
195
+
196
+ [binary_path, *global_opts, *args].map(&:to_s)
197
+ end
198
+
199
+ # Log the result of a git command at info/debug level
200
+ #
201
+ # @param result [ProcessExecuter::Result] the raw process result
202
+ #
203
+ # @param command [Array<String>] the full command that was run
204
+ #
205
+ # @param processed_out [String] the post-processed stdout string
206
+ #
207
+ # @param processed_err [String] the post-processed stderr string
208
+ #
209
+ # @return [void]
210
+ #
211
+ # @api private
212
+ #
213
+ def log_result(result, command, processed_out, processed_err)
214
+ logger.info { "#{command} exited with status #{result}" }
215
+ logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" }
216
+ end
217
+
218
+ # Build a {Git::CommandLine::Result} and raise on timeout, signal, or failure
219
+ #
220
+ # @param command [Array<String>] the full command that was run
221
+ #
222
+ # @param result [ProcessExecuter::Result] the raw process result
223
+ #
224
+ # @param processed_out [String] processed stdout string
225
+ #
226
+ # @param processed_err [String] processed stderr string
227
+ #
228
+ # @param timeout [Numeric, nil] the timeout value (for error context)
229
+ #
230
+ # @param raise_on_failure [Boolean] whether to raise on non-zero exit status
231
+ #
232
+ # @return [Git::CommandLine::Result]
233
+ #
234
+ # @raise [Git::TimeoutError] if the command timed out
235
+ #
236
+ # @raise [Git::SignaledError] if the command was terminated by a signal
237
+ #
238
+ # @raise [Git::FailedError] if the command failed and raise_on_failure is true
239
+ #
240
+ # @api private
241
+ #
242
+ # rubocop:disable Metrics/ParameterLists
243
+ def command_line_result(command, result, processed_out, processed_err, timeout, raise_on_failure)
244
+ Git::CommandLine::Result.new(command, result, processed_out, processed_err).tap do |processed_result|
245
+ raise Git::TimeoutError.new(processed_result, timeout) if result.timed_out?
246
+
247
+ raise Git::SignaledError, processed_result if result.signaled?
248
+
249
+ raise Git::FailedError, processed_result if raise_on_failure && !result.success?
250
+ end
251
+ end
252
+ # rubocop:enable Metrics/ParameterLists
253
+ end
254
+ end
255
+ end