git 1.19.1 → 5.5.0

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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -1
  3. data/AI_POLICY.md +24 -0
  4. data/CHANGELOG.md +689 -0
  5. data/CODE_OF_CONDUCT.md +25 -0
  6. data/CONTRIBUTING.md +1175 -97
  7. data/GOVERNANCE.md +106 -0
  8. data/LICENSE +1 -1
  9. data/MAINTAINERS.md +17 -4
  10. data/README.md +476 -320
  11. data/UPGRADING.md +1138 -0
  12. data/git.gemspec +124 -36
  13. data/lib/git/author.rb +39 -7
  14. data/lib/git/author_info.rb +66 -0
  15. data/lib/git/branch.rb +615 -65
  16. data/lib/git/branch_delete_failure.rb +34 -0
  17. data/lib/git/branch_delete_result.rb +66 -0
  18. data/lib/git/branch_info.rb +237 -0
  19. data/lib/git/branches.rb +167 -44
  20. data/lib/git/command_line/base.rb +247 -0
  21. data/lib/git/command_line/capturing.rb +308 -0
  22. data/lib/git/command_line/result.rb +88 -0
  23. data/lib/git/command_line/streaming.rb +236 -0
  24. data/lib/git/command_line.rb +52 -0
  25. data/lib/git/commands/add.rb +139 -0
  26. data/lib/git/commands/am/abort.rb +43 -0
  27. data/lib/git/commands/am/apply.rb +263 -0
  28. data/lib/git/commands/am/continue.rb +43 -0
  29. data/lib/git/commands/am/quit.rb +43 -0
  30. data/lib/git/commands/am/retry.rb +49 -0
  31. data/lib/git/commands/am/show_current_patch.rb +64 -0
  32. data/lib/git/commands/am/skip.rb +42 -0
  33. data/lib/git/commands/am.rb +33 -0
  34. data/lib/git/commands/apply.rb +242 -0
  35. data/lib/git/commands/archive/list_formats.rb +46 -0
  36. data/lib/git/commands/archive.rb +145 -0
  37. data/lib/git/commands/arguments.rb +4521 -0
  38. data/lib/git/commands/base.rb +502 -0
  39. data/lib/git/commands/branch/copy.rb +102 -0
  40. data/lib/git/commands/branch/create.rb +177 -0
  41. data/lib/git/commands/branch/delete.rb +88 -0
  42. data/lib/git/commands/branch/list.rb +178 -0
  43. data/lib/git/commands/branch/move.rb +102 -0
  44. data/lib/git/commands/branch/set_upstream.rb +86 -0
  45. data/lib/git/commands/branch/show_current.rb +49 -0
  46. data/lib/git/commands/branch/unset_upstream.rb +53 -0
  47. data/lib/git/commands/branch.rb +34 -0
  48. data/lib/git/commands/cat_file/batch.rb +385 -0
  49. data/lib/git/commands/cat_file/filtered.rb +105 -0
  50. data/lib/git/commands/cat_file/raw.rb +271 -0
  51. data/lib/git/commands/cat_file.rb +49 -0
  52. data/lib/git/commands/checkout/branch.rb +153 -0
  53. data/lib/git/commands/checkout/files.rb +116 -0
  54. data/lib/git/commands/checkout.rb +38 -0
  55. data/lib/git/commands/checkout_index.rb +106 -0
  56. data/lib/git/commands/clean.rb +102 -0
  57. data/lib/git/commands/clone.rb +241 -0
  58. data/lib/git/commands/commit.rb +273 -0
  59. data/lib/git/commands/commit_tree.rb +101 -0
  60. data/lib/git/commands/config_option_syntax/add.rb +86 -0
  61. data/lib/git/commands/config_option_syntax/get.rb +121 -0
  62. data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
  63. data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
  64. data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
  65. data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
  66. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
  67. data/lib/git/commands/config_option_syntax/list.rb +111 -0
  68. data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
  69. data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
  70. data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
  71. data/lib/git/commands/config_option_syntax/set.rb +119 -0
  72. data/lib/git/commands/config_option_syntax/unset.rb +92 -0
  73. data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
  74. data/lib/git/commands/config_option_syntax.rb +56 -0
  75. data/lib/git/commands/describe.rb +156 -0
  76. data/lib/git/commands/diff.rb +657 -0
  77. data/lib/git/commands/diff_files.rb +519 -0
  78. data/lib/git/commands/diff_index.rb +499 -0
  79. data/lib/git/commands/fetch.rb +354 -0
  80. data/lib/git/commands/fsck.rb +138 -0
  81. data/lib/git/commands/gc.rb +134 -0
  82. data/lib/git/commands/grep.rb +339 -0
  83. data/lib/git/commands/init.rb +101 -0
  84. data/lib/git/commands/log.rb +634 -0
  85. data/lib/git/commands/ls_files.rb +195 -0
  86. data/lib/git/commands/ls_remote.rb +161 -0
  87. data/lib/git/commands/ls_tree.rb +135 -0
  88. data/lib/git/commands/maintenance/register.rb +77 -0
  89. data/lib/git/commands/maintenance/run.rb +109 -0
  90. data/lib/git/commands/maintenance/start.rb +71 -0
  91. data/lib/git/commands/maintenance/stop.rb +60 -0
  92. data/lib/git/commands/maintenance/unregister.rb +84 -0
  93. data/lib/git/commands/maintenance.rb +31 -0
  94. data/lib/git/commands/merge/abort.rb +44 -0
  95. data/lib/git/commands/merge/continue.rb +44 -0
  96. data/lib/git/commands/merge/quit.rb +46 -0
  97. data/lib/git/commands/merge/start.rb +250 -0
  98. data/lib/git/commands/merge.rb +28 -0
  99. data/lib/git/commands/merge_base.rb +91 -0
  100. data/lib/git/commands/mv.rb +82 -0
  101. data/lib/git/commands/name_rev.rb +119 -0
  102. data/lib/git/commands/pull.rb +382 -0
  103. data/lib/git/commands/push.rb +251 -0
  104. data/lib/git/commands/read_tree.rb +154 -0
  105. data/lib/git/commands/remote/add.rb +96 -0
  106. data/lib/git/commands/remote/get_url.rb +68 -0
  107. data/lib/git/commands/remote/list.rb +56 -0
  108. data/lib/git/commands/remote/prune.rb +63 -0
  109. data/lib/git/commands/remote/remove.rb +52 -0
  110. data/lib/git/commands/remote/rename.rb +76 -0
  111. data/lib/git/commands/remote/set_branches.rb +70 -0
  112. data/lib/git/commands/remote/set_head.rb +89 -0
  113. data/lib/git/commands/remote/set_url.rb +78 -0
  114. data/lib/git/commands/remote/set_url_add.rb +70 -0
  115. data/lib/git/commands/remote/set_url_delete.rb +71 -0
  116. data/lib/git/commands/remote/show.rb +77 -0
  117. data/lib/git/commands/remote/update.rb +79 -0
  118. data/lib/git/commands/remote.rb +42 -0
  119. data/lib/git/commands/repack.rb +281 -0
  120. data/lib/git/commands/reset.rb +154 -0
  121. data/lib/git/commands/rev_parse.rb +304 -0
  122. data/lib/git/commands/revert/abort.rb +45 -0
  123. data/lib/git/commands/revert/continue.rb +62 -0
  124. data/lib/git/commands/revert/quit.rb +47 -0
  125. data/lib/git/commands/revert/skip.rb +44 -0
  126. data/lib/git/commands/revert/start.rb +158 -0
  127. data/lib/git/commands/revert.rb +29 -0
  128. data/lib/git/commands/rm.rb +113 -0
  129. data/lib/git/commands/show.rb +632 -0
  130. data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
  131. data/lib/git/commands/show_ref/exists.rb +80 -0
  132. data/lib/git/commands/show_ref/list.rb +149 -0
  133. data/lib/git/commands/show_ref/verify.rb +122 -0
  134. data/lib/git/commands/show_ref.rb +42 -0
  135. data/lib/git/commands/stash/apply.rb +81 -0
  136. data/lib/git/commands/stash/branch.rb +67 -0
  137. data/lib/git/commands/stash/clear.rb +43 -0
  138. data/lib/git/commands/stash/create.rb +60 -0
  139. data/lib/git/commands/stash/drop.rb +73 -0
  140. data/lib/git/commands/stash/list.rb +43 -0
  141. data/lib/git/commands/stash/pop.rb +87 -0
  142. data/lib/git/commands/stash/push.rb +112 -0
  143. data/lib/git/commands/stash/show.rb +158 -0
  144. data/lib/git/commands/stash/store.rb +72 -0
  145. data/lib/git/commands/stash.rb +38 -0
  146. data/lib/git/commands/status.rb +174 -0
  147. data/lib/git/commands/symbolic_ref/delete.rb +72 -0
  148. data/lib/git/commands/symbolic_ref/read.rb +99 -0
  149. data/lib/git/commands/symbolic_ref/update.rb +79 -0
  150. data/lib/git/commands/symbolic_ref.rb +38 -0
  151. data/lib/git/commands/tag/create.rb +142 -0
  152. data/lib/git/commands/tag/delete.rb +57 -0
  153. data/lib/git/commands/tag/list.rb +146 -0
  154. data/lib/git/commands/tag/verify.rb +71 -0
  155. data/lib/git/commands/tag.rb +26 -0
  156. data/lib/git/commands/update_ref/batch.rb +145 -0
  157. data/lib/git/commands/update_ref/delete.rb +90 -0
  158. data/lib/git/commands/update_ref/update.rb +103 -0
  159. data/lib/git/commands/update_ref.rb +42 -0
  160. data/lib/git/commands/version.rb +60 -0
  161. data/lib/git/commands/worktree/add.rb +139 -0
  162. data/lib/git/commands/worktree/list.rb +64 -0
  163. data/lib/git/commands/worktree/lock.rb +58 -0
  164. data/lib/git/commands/worktree/management_base.rb +51 -0
  165. data/lib/git/commands/worktree/move.rb +66 -0
  166. data/lib/git/commands/worktree/prune.rb +67 -0
  167. data/lib/git/commands/worktree/remove.rb +63 -0
  168. data/lib/git/commands/worktree/repair.rb +76 -0
  169. data/lib/git/commands/worktree/unlock.rb +47 -0
  170. data/lib/git/commands/worktree.rb +43 -0
  171. data/lib/git/commands/write_tree.rb +68 -0
  172. data/lib/git/commands.rb +88 -0
  173. data/lib/git/config.rb +72 -5
  174. data/lib/git/config_entry_info.rb +106 -0
  175. data/lib/git/configuring.rb +795 -0
  176. data/lib/git/detached_head_info.rb +57 -0
  177. data/lib/git/diff.rb +437 -86
  178. data/lib/git/diff_file_numstat_info.rb +31 -0
  179. data/lib/git/diff_file_patch_info.rb +136 -0
  180. data/lib/git/diff_file_raw_info.rb +129 -0
  181. data/lib/git/diff_info.rb +162 -0
  182. data/lib/git/diff_path_status.rb +107 -0
  183. data/lib/git/diff_result.rb +34 -0
  184. data/lib/git/diff_stats.rb +111 -0
  185. data/lib/git/dirstat_info.rb +102 -0
  186. data/lib/git/encoding_utils.rb +32 -1
  187. data/lib/git/errors.rb +285 -0
  188. data/lib/git/escaped_path.rb +57 -5
  189. data/lib/git/execution_context/global.rb +31 -0
  190. data/lib/git/execution_context/repository.rb +151 -0
  191. data/lib/git/execution_context.rb +559 -0
  192. data/lib/git/factories.rb +813 -0
  193. data/lib/git/file_ref.rb +77 -0
  194. data/lib/git/fsck_object.rb +56 -0
  195. data/lib/git/fsck_result.rb +132 -0
  196. data/lib/git/log.rb +306 -90
  197. data/lib/git/object.rb +563 -141
  198. data/lib/git/parsers/branch.rb +240 -0
  199. data/lib/git/parsers/cat_file.rb +111 -0
  200. data/lib/git/parsers/config_entry.rb +110 -0
  201. data/lib/git/parsers/diff.rb +792 -0
  202. data/lib/git/parsers/fsck.rb +144 -0
  203. data/lib/git/parsers/grep.rb +42 -0
  204. data/lib/git/parsers/ls_remote.rb +79 -0
  205. data/lib/git/parsers/ls_tree.rb +58 -0
  206. data/lib/git/parsers/remote.rb +162 -0
  207. data/lib/git/parsers/stash.rb +292 -0
  208. data/lib/git/parsers/status.rb +251 -0
  209. data/lib/git/parsers/tag.rb +341 -0
  210. data/lib/git/parsers/worktree.rb +185 -0
  211. data/lib/git/path_resolver.rb +206 -0
  212. data/lib/git/remote.rb +165 -12
  213. data/lib/git/remote_info.rb +203 -0
  214. data/lib/git/repository/branching.rb +964 -0
  215. data/lib/git/repository/committing.rb +246 -0
  216. data/lib/git/repository/context_helpers.rb +293 -0
  217. data/lib/git/repository/diffing.rb +785 -0
  218. data/lib/git/repository/inspecting.rb +252 -0
  219. data/lib/git/repository/logging.rb +410 -0
  220. data/lib/git/repository/maintenance.rb +65 -0
  221. data/lib/git/repository/merging.rb +451 -0
  222. data/lib/git/repository/object_operations.rb +1551 -0
  223. data/lib/git/repository/remote_operations.rb +984 -0
  224. data/lib/git/repository/shared_private.rb +120 -0
  225. data/lib/git/repository/staging.rb +587 -0
  226. data/lib/git/repository/stashing.rb +623 -0
  227. data/lib/git/repository/status_operations.rb +249 -0
  228. data/lib/git/repository/worktree_operations.rb +339 -0
  229. data/lib/git/repository.rb +484 -2
  230. data/lib/git/stash.rb +109 -12
  231. data/lib/git/stash_info.rb +102 -0
  232. data/lib/git/stashes.rb +169 -26
  233. data/lib/git/status.rb +308 -122
  234. data/lib/git/status_file_info.rb +258 -0
  235. data/lib/git/status_info.rb +189 -0
  236. data/lib/git/tag_delete_failure.rb +34 -0
  237. data/lib/git/tag_delete_result.rb +66 -0
  238. data/lib/git/tag_info.rb +99 -0
  239. data/lib/git/url.rb +15 -8
  240. data/lib/git/version.rb +113 -2
  241. data/lib/git/version_constraint.rb +85 -0
  242. data/lib/git/worktree.rb +150 -8
  243. data/lib/git/worktree_info.rb +128 -0
  244. data/lib/git/worktrees.rb +118 -13
  245. data/lib/git.rb +632 -234
  246. metadata +369 -54
  247. data/.github/stale.yml +0 -25
  248. data/.github/workflows/continuous_integration.yml +0 -49
  249. data/.gitignore +0 -10
  250. data/Dockerfile.changelog-rs +0 -12
  251. data/Gemfile +0 -5
  252. data/ISSUE_TEMPLATE.md +0 -15
  253. data/PULL_REQUEST_TEMPLATE.md +0 -9
  254. data/RELEASING.md +0 -70
  255. data/Rakefile +0 -60
  256. data/lib/git/base/factory.rb +0 -99
  257. data/lib/git/base.rb +0 -711
  258. data/lib/git/command_line_result.rb +0 -86
  259. data/lib/git/failed_error.rb +0 -53
  260. data/lib/git/git_execute_error.rb +0 -7
  261. data/lib/git/index.rb +0 -5
  262. data/lib/git/lib.rb +0 -1328
  263. data/lib/git/path.rb +0 -31
  264. data/lib/git/signaled_error.rb +0 -50
  265. data/lib/git/working_directory.rb +0 -4
@@ -0,0 +1,34 @@
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
+ #
19
+ # @see Git::Commands::Branch::Delete
20
+ #
21
+ # @api public
22
+ #
23
+ # @!attribute [r] name
24
+ # The name of the branch that failed to be deleted
25
+ #
26
+ # @return [String]
27
+ #
28
+ # @!attribute [r] error_message
29
+ # The error message from git explaining why the branch could not be deleted
30
+ #
31
+ # @return [String]
32
+ #
33
+ BranchDeleteFailure = Data.define(:name, :error_message)
34
+ end
@@ -0,0 +1,66 @@
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
+ #
33
+ # @see Git::BranchDeleteFailure
34
+ #
35
+ # @see Git::Commands::Branch::Delete
36
+ #
37
+ # @api public
38
+ #
39
+ # @!attribute [r] deleted
40
+ # Branches that were successfully deleted
41
+ #
42
+ # @return [Array<Git::BranchInfo>]
43
+ #
44
+ # @!attribute [r] not_deleted
45
+ # Branches that could not be deleted, with the reason for each failure
46
+ #
47
+ # @return [Array<Git::BranchDeleteFailure>]
48
+ #
49
+ BranchDeleteResult = Data.define(:deleted, :not_deleted) do
50
+ # Returns true if all requested branches were successfully deleted
51
+ #
52
+ # @example Check if the delete operation succeeded
53
+ # result = branch_delete.call('feature-branch')
54
+ # if result.success?
55
+ # puts "All branches deleted successfully"
56
+ # else
57
+ # puts "Some branches could not be deleted:"
58
+ # result.not_deleted.each { |f| puts " #{f.name}: #{f.error_message}" }
59
+ # end
60
+ #
61
+ # @return [Boolean] true if no branches failed to delete, false otherwise
62
+ def success?
63
+ not_deleted.empty?
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,237 @@
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
+ # @example Parse branch refnames
11
+ # 'main' => { remote_name: nil, branch_name: 'main' }
12
+ # 'refs/heads/main' => { remote_name: nil, branch_name: 'main' }
13
+ # 'remotes/origin/main' => { remote_name: 'origin', branch_name: 'main' }
14
+ # 'refs/remotes/origin/main' => { remote_name: 'origin', branch_name: 'main' }
15
+ # 'feature/foo' => { remote_name: nil, branch_name: 'feature/foo' }
16
+ # 'remotes/origin/feature/bar' => { remote_name: 'origin', branch_name: 'feature/bar' }
17
+ #
18
+ # @note This regex handles both raw full refs (e.g., `refs/heads/main`) as stored in
19
+ # {Git::BranchInfo#refname} and normalized short-form refs (e.g., `main`,
20
+ # `remotes/origin/main`) used elsewhere.
21
+ #
22
+ # @note This regex is a fallback for branch refnames parsed without configured
23
+ # remote context. Remote names containing '/' can only be resolved reliably
24
+ # when the parser is given the configured remote names. See:
25
+ # https://github.com/ruby-git/ruby-git/issues/919
26
+ #
27
+ # @api private
28
+ BRANCH_REFNAME_REGEXP = %r{
29
+ \A # start of string
30
+ (?:refs/heads/)? # optional refs/heads/ prefix (stripped)
31
+ (?:(?:refs/)?remotes/(?<remote_name>[^/]+)/)? # optional refs?/remotes/<remote_name>/
32
+ (?<branch_name>.+) # branch name (everything else)
33
+ \z # end of string
34
+ }x
35
+
36
+ # Sentinel for distinguishing omitted BranchInfo remote_name from explicit nil
37
+ REMOTE_NAME_NOT_GIVEN = Object.new.freeze
38
+ private_constant :REMOTE_NAME_NOT_GIVEN
39
+
40
+ # Value object representing branch metadata from git branch output
41
+ #
42
+ # This is a lightweight, immutable data structure returned by branch listing
43
+ # commands. It contains only the data parsed from git output without any
44
+ # repository context or operations.
45
+ #
46
+ # @example Local branch with upstream tracking
47
+ # info = Git::BranchInfo.new(
48
+ # refname: 'refs/heads/main',
49
+ # target_oid: 'abc123def456789012345678901234567890abcd',
50
+ # current: true,
51
+ # worktree_path: nil,
52
+ # symref: nil,
53
+ # upstream: 'refs/remotes/origin/main'
54
+ # )
55
+ # info.current? #=> true
56
+ # info.remote? #=> false
57
+ # info.short_name #=> 'main'
58
+ # info.upstream #=> 'refs/remotes/origin/main'
59
+ #
60
+ # @example Remote-tracking branch
61
+ # info = Git::BranchInfo.new(
62
+ # refname: 'refs/remotes/origin/main',
63
+ # target_oid: 'abc123def456789012345678901234567890abcd',
64
+ # current: false,
65
+ # worktree_path: nil,
66
+ # symref: nil,
67
+ # upstream: nil
68
+ # )
69
+ # info.remote? #=> true
70
+ # info.remote_name #=> 'origin'
71
+ # info.short_name #=> 'main'
72
+ #
73
+ # @see Git::Repository::Branching#branch_list for the repository method that returns these
74
+ #
75
+ # @see Git::Commands::Branch::List for the command that produces these
76
+ #
77
+ # @api public
78
+ #
79
+ # @!attribute [r] refname
80
+ #
81
+ # The full reference name of the branch
82
+ #
83
+ # Must be the full refname as returned by git (e.g., 'refs/heads/main',
84
+ # 'refs/remotes/origin/main') because the short name alone is not guaranteed to
85
+ # be unique (e.g., 'main' could exist as both a local and remote branch).
86
+ #
87
+ # @return [String] the branch refname (e.g., 'refs/heads/main',
88
+ # 'refs/remotes/origin/main')
89
+ #
90
+ # @!attribute [r] remote_name
91
+ #
92
+ # @return [String, nil] the resolved or fallback-derived remote name, or nil
93
+ # for local branches
94
+ #
95
+ # @!attribute [r] target_oid
96
+ #
97
+ # The commit object ID (SHA) that this branch points to (aka HEAD)
98
+ #
99
+ # @return [String, nil] the full 40-character object ID, or nil if branch is
100
+ # unborn (no commits yet)
101
+ #
102
+ # @!attribute [r] current
103
+ #
104
+ # Whether this branch is currently checked out in the current worktree
105
+ #
106
+ # @return [Boolean] true if this is the current branch
107
+ #
108
+ # @note A branch can be current ({#current?} true) or in another worktree
109
+ # ({#other_worktree?} true), but never both. A branch not checked out
110
+ # anywhere has both false.
111
+ #
112
+ # @!attribute [r] worktree_path
113
+ #
114
+ # The absolute path of the *other* linked worktree this branch is checked
115
+ # out in, or nil.
116
+ #
117
+ # This is nil in two distinct cases:
118
+ # - The branch is the current branch in this worktree (use {#current?} to
119
+ # distinguish that case)
120
+ # - The branch is not checked out in any worktree
121
+ #
122
+ # This path is suppressed for the current branch even though git reports it
123
+ # via `%(worktreepath)`, because the current worktree's path is already
124
+ # known from the repository object and storing it here would make
125
+ # {#other_worktree?} incorrect.
126
+ #
127
+ # @return [String, nil] the absolute path of the linked worktree root
128
+ # directory (e.g., `'/home/user/projects/my-repo-hotfix'`), or nil if
129
+ # the branch is not checked out in a different linked worktree
130
+ #
131
+ # @!attribute [r] symref
132
+ #
133
+ # The target reference if this is a symbolic reference
134
+ #
135
+ # @return [String, nil] the target ref (e.g., 'refs/heads/main'), or nil if not a symref
136
+ #
137
+ # @!attribute [r] upstream
138
+ #
139
+ # The configured upstream/tracking branch refname as reported by git
140
+ #
141
+ # @return [String, nil] the raw upstream refname from `%(upstream)`
142
+ # (e.g., `'refs/remotes/origin/main'`), or nil if no upstream is configured
143
+ #
144
+ # @note Remote-tracking branches (e.g., `'refs/remotes/origin/main'`) have upstream: nil
145
+ #
146
+ # @note This is the raw refname snapshot from when the branch list was read.
147
+ # It does not reflect live git state after the snapshot was taken.
148
+ #
149
+ BranchInfo = Data.define(:refname, :remote_name, :target_oid, :current, :worktree_path, :symref, :upstream) do
150
+ # @param refname [String] the full branch refname
151
+ #
152
+ # @param remote_name [String, nil] resolved remote name, nil for local branches,
153
+ # or omitted to derive from `refname`
154
+ #
155
+ # @param target_oid [String, nil] the commit object ID, or nil for unborn branches
156
+ #
157
+ # @param current [Boolean] whether this branch is currently checked out
158
+ #
159
+ # @param worktree_path [String, nil] path to another linked worktree, or nil
160
+ #
161
+ # @param symref [String, nil] symbolic reference target, or nil
162
+ #
163
+ # @param upstream [String, nil] upstream refname, or nil
164
+ #
165
+ def initialize(refname:, target_oid:, current:, worktree_path:, symref:, upstream:, # rubocop:disable Metrics/ParameterLists
166
+ remote_name: REMOTE_NAME_NOT_GIVEN)
167
+ remote_name = self.class.fallback_remote_name(refname) if remote_name.equal?(REMOTE_NAME_NOT_GIVEN)
168
+ self.class.validate_remote_name!(refname, remote_name)
169
+
170
+ super
171
+ end
172
+
173
+ # @param refname [String] the branch refname to validate
174
+ #
175
+ # @param remote_name [String, nil] the remote name to validate
176
+ #
177
+ # @return [void]
178
+ #
179
+ # @raise [ArgumentError] if the remote name contradicts the refname type
180
+ def self.validate_remote_name!(refname, remote_name)
181
+ if remote_tracking_refname?(refname)
182
+ unless remote_name.is_a?(String) && !remote_name.empty?
183
+ raise ArgumentError, 'remote_name must be a non-empty String for remote-tracking refname'
184
+ end
185
+
186
+ remote_ref_prefix = %r{\A(?:refs/)?remotes/#{Regexp.escape(remote_name)}/}
187
+ raise ArgumentError, 'remote_name must match remote-tracking refname' unless refname.match?(remote_ref_prefix)
188
+ elsif !remote_name.nil?
189
+ raise ArgumentError, 'remote_name must be nil for local branch refname'
190
+ end
191
+ end
192
+
193
+ # @param refname [String] the branch refname to parse
194
+ #
195
+ # @return [String, nil] the regex-derived remote name, or nil for local branches
196
+ def self.fallback_remote_name(refname)
197
+ refname.match(Git::BRANCH_REFNAME_REGEXP)[:remote_name]
198
+ end
199
+
200
+ # @param refname [String] the branch refname to inspect
201
+ #
202
+ # @return [Boolean] true if the refname is a remote-tracking refname
203
+ def self.remote_tracking_refname?(refname)
204
+ refname.match?(%r{\A(?:refs/)?remotes/[^/]+/.+})
205
+ end
206
+
207
+ # @return [Boolean] always false for BranchInfo (see DetachedHeadInfo for detached state)
208
+ def detached? = false
209
+
210
+ # @return [Boolean] true if this is an unborn branch (no commits yet)
211
+ def unborn? = target_oid.nil?
212
+
213
+ # @return [String] the short branch name without any remote or heads prefix
214
+ # (e.g., 'main' or 'feature/foo')
215
+ def short_name
216
+ return refname.delete_prefix('refs/heads/') if remote_name.nil?
217
+
218
+ remote_ref_prefix = %r{\A(?:refs/)?remotes/#{Regexp.escape(remote_name)}/}
219
+ refname.sub(remote_ref_prefix, '')
220
+ end
221
+
222
+ # @return [Boolean] true if this is the currently checked out branch
223
+ def current? = current
224
+
225
+ # @return [Boolean] true if this branch is checked out in another linked worktree
226
+ def other_worktree? = !worktree_path.nil?
227
+
228
+ # @return [Boolean] true if this is a symbolic reference
229
+ def symref? = !symref.nil?
230
+
231
+ # @return [Boolean] true if this is a remote-tracking branch
232
+ def remote? = !remote_name.nil?
233
+
234
+ # @return [String] string representation (the full refname)
235
+ def to_s = refname
236
+ end
237
+ end
data/lib/git/branches.rb CHANGED
@@ -1,71 +1,194 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Git
2
-
3
- # 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
+ # @deprecated Use {Git::Repository::Branching#branch_list} instead
14
+ #
15
+ # {Git::Repository::Branching#branch_list} returns `Array<Git::BranchInfo>`
16
+ # (immutable value objects). Filter it with `select(&:remote?)` or
17
+ # `reject(&:remote?)` in place of {#remote} and {#local}, and look a
18
+ # branch up by name with `branch_list(name).first` in place of {#[]}.
19
+ # Constructing a `Git::Branches` emits a deprecation warning.
20
+ #
21
+ # @api public
22
+ #
4
23
  class Branches
5
-
6
24
  include Enumerable
7
-
25
+
26
+ # Creates a new Branches collection populated from the given repository
27
+ #
28
+ # @param base [Git::Repository] the repository to enumerate
29
+ # branches from
30
+ #
31
+ # @return [void]
32
+ #
33
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
34
+ #
35
+ # @deprecated Use {Git::Repository::Branching#branch_list} instead
36
+ #
37
+ # @see Git::Repository::Branching#branch_list
38
+ #
8
39
  def initialize(base)
40
+ Git::Deprecation.warn(
41
+ 'Git::Branches is deprecated and will be removed in v6.0.0. ' \
42
+ 'Use Git::Repository#branch_list instead.'
43
+ )
9
44
  @branches = {}
10
-
45
+ @lookup = {}
46
+
11
47
  @base = base
12
-
13
- @base.lib.branches_all.each do |b|
14
- @branches[b[0]] = Git::Branch.new(@base, b[0])
15
- end
48
+
49
+ load_branches
16
50
  end
17
51
 
52
+ # Returns all local (non-remote-tracking) branches
53
+ #
54
+ # @example List local branch names
55
+ # repo.branches.local.map(&:name)
56
+ #
57
+ # @return [Array<Git::Branch>] the local branches
58
+ #
18
59
  def local
19
- self.select { |b| !b.remote }
60
+ reject(&:remote)
20
61
  end
21
-
62
+
63
+ # Returns all remote-tracking branches
64
+ #
65
+ # @example List remote branch names
66
+ # repo.branches.remote.map(&:name)
67
+ #
68
+ # @return [Array<Git::Branch>] the remote-tracking branches
69
+ #
22
70
  def remote
23
- self.select { |b| b.remote }
71
+ self.select(&:remote)
24
72
  end
25
-
26
- # array like methods
27
73
 
74
+ # Returns the number of branches in the collection
75
+ #
76
+ # @example Count all branches
77
+ # repo.branches.size # => 3
78
+ #
79
+ # @return [Integer] the total number of branches
80
+ #
28
81
  def size
29
82
  @branches.size
30
- end
31
-
32
- def each(&block)
33
- @branches.values.each(&block)
34
83
  end
35
-
36
- # Returns the target branch
84
+
85
+ # Iterates over every branch in the collection
86
+ #
87
+ # @overload each
88
+ #
89
+ # @example Get an enumerator over all branches
90
+ # enum = repo.branches.each
91
+ #
92
+ # @return [Enumerator<Git::Branch>] an enumerator over all branches
93
+ #
94
+ # @overload each(&block)
95
+ #
96
+ # @example Print every branch name
97
+ # repo.branches.each { |b| puts b.name }
98
+ #
99
+ # @return [Array<Git::Branch>] the full list of branches
100
+ #
101
+ # @yield [branch] passes each branch to the block
102
+ #
103
+ # @yieldparam branch [Git::Branch] a branch in the repository
104
+ #
105
+ # @yieldreturn [void]
106
+ #
107
+ def each(&)
108
+ @branches.values.each(&)
109
+ end
110
+
111
+ # Returns the branch with the given name
112
+ #
113
+ # Supports short names (`'main'`), remote-qualified names
114
+ # (`'working/master'`), and full refspec names
115
+ # (`'remotes/working/master'`).
37
116
  #
38
- # Example:
39
- # Given (git branch -a):
40
- # master
41
- # remotes/working/master
117
+ # @example Look up a branch by short name
118
+ # repo.branches['main']
42
119
  #
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'
120
+ # @example Look up a remote-tracking branch
121
+ # repo.branches['working/master']
122
+ #
123
+ # @param branch_name [#to_s] the name of the branch to retrieve
124
+ #
125
+ # @return [Git::Branch, nil] the matching branch, or `nil` if not found
46
126
  #
47
- # @param [#to_s] branch_name the target branch name.
48
- # @return [Git::Branch] the target branch.
49
127
  def [](branch_name)
50
- @branches.values.inject(@branches) do |branches, branch|
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 name (even if is not a real remote branch).
55
- branches[branch.full.sub('remotes/', '')] ||= branch if branch.full =~ /^remotes\/.+/
56
-
57
- branches
58
- end[branch_name.to_s]
128
+ @lookup[branch_name.to_s]
59
129
  end
60
-
130
+
131
+ # Returns a string listing all branches, prefixed with `*` for the current branch
132
+ #
133
+ # @example Display all branches
134
+ # puts repo.branches.to_s
135
+ #
136
+ # @return [String] a formatted branch listing
137
+ #
61
138
  def to_s
62
- out = ''
63
- @branches.each do |k, b|
64
- out << (b.current ? '* ' : ' ') << b.to_s << "\n"
139
+ out = +''
140
+ @branches.each_value do |b|
141
+ # Git::Branch#current is deprecated too; silence it so one to_s call emits one warning
142
+ current = Git::Deprecation.silence { b.current }
143
+ out << (current ? '* ' : ' ') << b.to_s << "\n"
65
144
  end
66
145
  out
67
146
  end
68
-
69
- end
70
147
 
148
+ private
149
+
150
+ # Builds a Git::Branch for every branch in the repository and indexes it
151
+ #
152
+ # @return [void]
153
+ #
154
+ # @api private
155
+ #
156
+ def load_branches
157
+ branch_repository.branch_list.each do |branch_info|
158
+ branch = Git::Branch.new(@base, branch_info)
159
+
160
+ @branches[branch_info.refname] = branch
161
+ index_branch_lookup(branch, refname: branch_info.refname)
162
+ end
163
+ end
164
+
165
+ # @return [Git::Repository] the repository used to enumerate branches
166
+ #
167
+ # @api private
168
+ #
169
+ def branch_repository
170
+ @base
171
+ end
172
+
173
+ # Indexes all supported lookup keys for a branch without mutating
174
+ # the canonical `@branches` collection used by enumeration
175
+ #
176
+ # @param branch [Git::Branch] the branch to index
177
+ #
178
+ # @param refname [String] the full refname key to use for primary lookup
179
+ #
180
+ # @return [void]
181
+ #
182
+ # @api private
183
+ #
184
+ def index_branch_lookup(branch, refname:)
185
+ @lookup[refname] ||= branch
186
+ @lookup[branch.full] ||= branch
187
+
188
+ return unless branch.full.start_with?('remotes/')
189
+
190
+ # Mirror git compatibility: allow omitting a leading "remotes/".
191
+ @lookup[branch.full.delete_prefix('remotes/')] ||= branch
192
+ end
193
+ end
71
194
  end