git 4.3.2 → 5.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +486 -410
  52. data/lib/git/branch.rb +380 -43
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +224 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +114 -18
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +349 -20
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,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,174 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'git/base'
4
+
3
5
  module Git
4
- # object that holds all the available branches
6
+ # Collection of all Git branches in a repository
7
+ #
8
+ # Wraps both local and remote-tracking branches and provides filtering,
9
+ # enumeration, and name-based lookup.
10
+ #
11
+ # @example Enumerate all branches
12
+ # branches = repo.branches
13
+ # branches.each { |b| puts b.name }
14
+ #
15
+ # @api public
16
+ #
5
17
  class Branches
6
18
  include Enumerable
7
19
 
20
+ # Creates a new Branches collection populated from the given repository
21
+ #
22
+ # @param base [Git::Base, Git::Repository] the repository to enumerate
23
+ # branches from
24
+ #
25
+ # @return [void]
26
+ #
27
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
28
+ #
8
29
  def initialize(base)
9
30
  @branches = {}
31
+ @lookup = {}
10
32
 
11
33
  @base = base
12
34
 
13
- @base.lib.branches_all.each do |b|
14
- @branches[b[0]] = Git::Branch.new(@base, b[0])
35
+ branch_repository.branches_all.each do |branch_info|
36
+ branch = Git::Branch.new(base, branch_info)
37
+
38
+ @branches[branch_info.refname] = branch
39
+ index_branch_lookup(branch, refname: branch_info.refname)
15
40
  end
16
41
  end
17
42
 
43
+ # Returns all local (non-remote-tracking) branches
44
+ #
45
+ # @example List local branch names
46
+ # repo.branches.local.map(&:name)
47
+ #
48
+ # @return [Array<Git::Branch>] the local branches
49
+ #
18
50
  def local
19
51
  reject(&:remote)
20
52
  end
21
53
 
54
+ # Returns all remote-tracking branches
55
+ #
56
+ # @example List remote branch names
57
+ # repo.branches.remote.map(&:name)
58
+ #
59
+ # @return [Array<Git::Branch>] the remote-tracking branches
60
+ #
22
61
  def remote
23
62
  self.select(&:remote)
24
63
  end
25
64
 
26
- # array like methods
27
-
65
+ # Returns the number of branches in the collection
66
+ #
67
+ # @example Count all branches
68
+ # repo.branches.size # => 3
69
+ #
70
+ # @return [Integer] the total number of branches
71
+ #
28
72
  def size
29
73
  @branches.size
30
74
  end
31
75
 
76
+ # Iterates over every branch in the collection
77
+ #
78
+ # @overload each
79
+ #
80
+ # @example Get an enumerator over all branches
81
+ # enum = repo.branches.each
82
+ #
83
+ # @return [Enumerator<Git::Branch>] an enumerator over all branches
84
+ #
85
+ # @overload each(&block)
86
+ #
87
+ # @example Print every branch name
88
+ # repo.branches.each { |b| puts b.name }
89
+ #
90
+ # @return [Array<Git::Branch>] the full list of branches
91
+ #
92
+ # @yield [branch] passes each branch to the block
93
+ #
94
+ # @yieldparam branch [Git::Branch] a branch in the repository
95
+ #
96
+ # @yieldreturn [void]
97
+ #
32
98
  def each(&)
33
99
  @branches.values.each(&)
34
100
  end
35
101
 
36
- # Returns the target branch
102
+ # Returns the branch with the given name
103
+ #
104
+ # Supports short names (`'main'`), remote-qualified names
105
+ # (`'working/master'`), and full refspec names
106
+ # (`'remotes/working/master'`).
37
107
  #
38
- # Example:
39
- # Given (git branch -a):
40
- # master
41
- # remotes/working/master
108
+ # @example Look up a branch by short name
109
+ # repo.branches['main']
42
110
  #
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'
111
+ # @example Look up a remote-tracking branch
112
+ # repo.branches['working/master']
113
+ #
114
+ # @param branch_name [#to_s] the name of the branch to retrieve
115
+ #
116
+ # @return [Git::Branch, nil] the matching branch, or `nil` if not found
46
117
  #
47
- # @param [#to_s] branch_name the target branch name.
48
- # @return [Git::Branch] the target branch.
49
118
  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]
119
+ @lookup[branch_name.to_s]
58
120
  end
59
121
 
122
+ # Returns a string listing all branches, prefixed with `*` for the current branch
123
+ #
124
+ # @example Display all branches
125
+ # puts repo.branches.to_s
126
+ #
127
+ # @return [String] a formatted branch listing
128
+ #
60
129
  def to_s
61
- out = ''
130
+ out = +''
62
131
  @branches.each_value do |b|
63
132
  out << (b.current ? '* ' : ' ') << b.to_s << "\n"
64
133
  end
65
134
  out
66
135
  end
136
+
137
+ private
138
+
139
+ # Resolves the {Git::Repository} for this collection of branches
140
+ #
141
+ # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
142
+ # The `is_a?(Git::Base)` guard will be removed when {Git::Base} is deleted
143
+ # in Phase 4.
144
+ #
145
+ # @return [Git::Repository] the repository used to enumerate branches
146
+ #
147
+ # @api private
148
+ #
149
+ def branch_repository
150
+ @base.is_a?(Git::Base) ? @base.facade_repository : @base
151
+ end
152
+
153
+ # Indexes all supported lookup keys for a branch without mutating
154
+ # the canonical `@branches` collection used by enumeration
155
+ #
156
+ # @param branch [Git::Branch] the branch to index
157
+ #
158
+ # @param refname [String] the full refname key to use for primary lookup
159
+ #
160
+ # @return [void]
161
+ #
162
+ # @api private
163
+ #
164
+ def index_branch_lookup(branch, refname:)
165
+ @lookup[refname] ||= branch
166
+ @lookup[branch.full] ||= branch
167
+
168
+ return unless branch.full.start_with?('remotes/')
169
+
170
+ # Mirror git compatibility: allow omitting a leading "remotes/".
171
+ @lookup[branch.full.delete_prefix('remotes/')] ||= branch
172
+ end
67
173
  end
68
174
  end
@@ -0,0 +1,245 @@
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
+ # @return [Object] the return value of the block
160
+ #
161
+ # @api private
162
+ #
163
+ def run_process_executer
164
+ yield
165
+ rescue ProcessExecuter::ArgumentError => e
166
+ raise ::ArgumentError, e.message
167
+ rescue ProcessExecuter::SpawnError => e
168
+ raise Git::Error, e.message, cause: e.cause
169
+ rescue ProcessExecuter::ProcessIOError => e
170
+ raise Git::ProcessIOError, e.message, cause: e.cause
171
+ end
172
+
173
+ # Build the git command line from the available sources to send to `Process.spawn`
174
+ #
175
+ # @param args [Array<String>] command-line arguments to append after global options
176
+ #
177
+ # @return [Array<String>]
178
+ #
179
+ # @raise [ArgumentError] if any element of args is itself an Array
180
+ #
181
+ # @api private
182
+ #
183
+ def build_git_cmd(args)
184
+ raise ArgumentError, 'The args array can not contain an array' if args.any?(Array)
185
+
186
+ [binary_path, *global_opts, *args].map(&:to_s)
187
+ end
188
+
189
+ # Log the result of a git command at info/debug level
190
+ #
191
+ # @param result [ProcessExecuter::Result] the raw process result
192
+ #
193
+ # @param command [Array<String>] the full command that was run
194
+ #
195
+ # @param processed_out [String] the post-processed stdout string
196
+ #
197
+ # @param processed_err [String] the post-processed stderr string
198
+ #
199
+ # @return [void]
200
+ #
201
+ # @api private
202
+ #
203
+ def log_result(result, command, processed_out, processed_err)
204
+ logger.info { "#{command} exited with status #{result}" }
205
+ logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" }
206
+ end
207
+
208
+ # Build a {Git::CommandLine::Result} and raise on timeout, signal, or failure
209
+ #
210
+ # @param command [Array<String>] the full command that was run
211
+ #
212
+ # @param result [ProcessExecuter::Result] the raw process result
213
+ #
214
+ # @param processed_out [String] processed stdout string
215
+ #
216
+ # @param processed_err [String] processed stderr string
217
+ #
218
+ # @param timeout [Numeric, nil] the timeout value (for error context)
219
+ #
220
+ # @param raise_on_failure [Boolean] whether to raise on non-zero exit status
221
+ #
222
+ # @return [Git::CommandLine::Result]
223
+ #
224
+ # @raise [Git::TimeoutError] if the command timed out
225
+ #
226
+ # @raise [Git::SignaledError] if the command was terminated by a signal
227
+ #
228
+ # @raise [Git::FailedError] if the command failed and raise_on_failure is true
229
+ #
230
+ # @api private
231
+ #
232
+ # rubocop:disable Metrics/ParameterLists
233
+ def command_line_result(command, result, processed_out, processed_err, timeout, raise_on_failure)
234
+ Git::CommandLine::Result.new(command, result, processed_out, processed_err).tap do |processed_result|
235
+ raise Git::TimeoutError.new(processed_result, timeout) if result.timed_out?
236
+
237
+ raise Git::SignaledError, processed_result if result.signaled?
238
+
239
+ raise Git::FailedError, processed_result if raise_on_failure && !result.success?
240
+ end
241
+ end
242
+ # rubocop:enable Metrics/ParameterLists
243
+ end
244
+ end
245
+ end