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,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/branch_info'
4
+ require 'git/branch_delete_result'
5
+ require 'git/branch_delete_failure'
6
+
7
+ module Git
8
+ # Internal parsers that translate raw git command output into structured Ruby objects.
9
+ #
10
+ # @api private
11
+ #
12
+ module Parsers
13
+ # Parser for git branch command output
14
+ #
15
+ # Handles parsing of `git branch --list` and `git branch --delete` output
16
+ # into structured data objects.
17
+ #
18
+ # ## Design Note: Namespace Organization
19
+ #
20
+ # This parser creates and returns {Git::BranchInfo} and {Git::BranchDeleteResult}
21
+ # objects, which live at the top-level `Git::` namespace rather than within
22
+ # `Git::Parsers::`. This is intentional:
23
+ #
24
+ # - **Parsers are infrastructure** - marked `@api private`, users shouldn't
25
+ # interact with them directly
26
+ # - **Info/Result classes are public API** - returned by commands and used
27
+ # throughout the codebase
28
+ # - **Info classes are domain entities** - represent core git concepts
29
+ # (branches as data)
30
+ # - **Result classes are operation outcomes** - represent command results,
31
+ # not parsing details
32
+ #
33
+ # Keeping Info/Result classes at `Git::` improves discoverability and correctly
34
+ # reflects their role as public types rather than parser internals.
35
+ #
36
+ # @api private
37
+ #
38
+ module Branch
39
+ # Format string for git branch --format
40
+ #
41
+ # Fields (null-delimited):
42
+ # 1. refname - full ref name (e.g., refs/heads/main, refs/remotes/origin/main)
43
+ # 2. objectname - full SHA of the commit the branch points to
44
+ # 3. HEAD - '*' if current branch, empty otherwise
45
+ # 4. worktreepath - path if checked out in another worktree, empty otherwise
46
+ # 5. symref - target ref if symbolic reference, empty otherwise
47
+ # 6. upstream - full upstream ref (e.g., refs/remotes/origin/main), empty if none
48
+ #
49
+ # Null bytes (%00) are used as field delimiters so that worktree paths
50
+ # containing special characters (including '|') parse correctly.
51
+ #
52
+ FORMAT_STRING = '%(refname)%00%(objectname)%00%(HEAD)%00%(worktreepath)%00%(symref)%00%(upstream)'
53
+
54
+ # Delimiter used in format output
55
+ FIELD_DELIMITER = "\0"
56
+
57
+ # Regex to parse successful deletion lines from stdout
58
+ # Matches: Deleted branch branchname (was abc123).
59
+ # Matches: Deleted remote-tracking branch origin/branchname (was abc123).
60
+ # Uses non-greedy match to capture branch names containing spaces
61
+ DELETED_BRANCH_REGEX = /^Deleted (?:remote-tracking )?branch (.+?) \(was/
62
+
63
+ # Regex to parse error messages from stderr
64
+ # Matches: error: branch 'branchname' not found.
65
+ ERROR_BRANCH_REGEX = /^error: branch '([^']+)'(.*)$/
66
+
67
+ module_function
68
+
69
+ # Parse git branch --list output into BranchInfo objects
70
+ #
71
+ # @example Parse NUL-delimited branch list output
72
+ # Git::Parsers::Branch.parse_list(
73
+ # "refs/heads/main\0abc1234\0*\0\0\0\n" \
74
+ # "refs/heads/feature\0def5678\0\0\0\0\n"
75
+ # )
76
+ # # => [#<data Git::BranchInfo refname="refs/heads/main", ...>,
77
+ # # #<data Git::BranchInfo refname="refs/heads/feature", ...>]
78
+ #
79
+ # @param stdout [String] output from git branch --list --format=...
80
+ #
81
+ # @param remote_names [Array<String>] configured remote names used to resolve
82
+ # remote-tracking refs with slash-containing remote names
83
+ #
84
+ # @return [Array<Git::BranchInfo>] parsed branch information
85
+ #
86
+ def parse_list(stdout, remote_names: [])
87
+ stdout.split("\n").filter_map { |line| parse_branch_line(line, remote_names: remote_names) }
88
+ end
89
+
90
+ # Parse a single formatted branch line
91
+ #
92
+ # @param line [String] the line to parse (NUL-delimited fields)
93
+ #
94
+ # @param remote_names [Array<String>] configured remote names used to resolve
95
+ # remote-tracking refs with slash-containing remote names
96
+ #
97
+ # @return [Git::BranchInfo, nil] branch info object, or nil if line should be skipped
98
+ #
99
+ def parse_branch_line(line, remote_names: [])
100
+ fields = line.split(FIELD_DELIMITER, 6)
101
+
102
+ return nil if non_branch_entry?(fields[0])
103
+
104
+ build_branch_info(fields, remote_names: remote_names)
105
+ end
106
+
107
+ # Build a BranchInfo from parsed fields
108
+ #
109
+ # @param fields [Array<String>] the parsed fields:
110
+ # [refname, objectname, head, worktreepath, symref, upstream]
111
+ #
112
+ # @param remote_names [Array<String>] configured remote names used to resolve
113
+ # remote-tracking refs with slash-containing remote names
114
+ #
115
+ # @return [Git::BranchInfo] the branch info object
116
+ #
117
+ def build_branch_info(fields, remote_names: [])
118
+ raw_refname, objectname, head, worktreepath, symref, upstream = fields
119
+ Git::BranchInfo.new(
120
+ refname: raw_refname,
121
+ target_oid: presence(objectname),
122
+ current: head == '*',
123
+ worktree_path: head == '*' ? nil : presence(worktreepath),
124
+ symref: presence(symref),
125
+ upstream: build_upstream_info(upstream),
126
+ remote_name: resolve_remote_name(raw_refname, remote_names)
127
+ )
128
+ end
129
+
130
+ # Resolve a remote-tracking refname to a configured remote name
131
+ #
132
+ # @param refname [String] the branch refname to inspect
133
+ #
134
+ # @param remote_names [Array<String>] configured remote names
135
+ #
136
+ # @return [String, nil] the resolved remote name, or nil for local branches
137
+ #
138
+ def resolve_remote_name(refname, remote_names)
139
+ remote_path = refname[%r{\A(?:refs/)?remotes/(.+)\z}, 1]
140
+ return nil if remote_path.nil?
141
+
142
+ configured_remote_name = remote_names
143
+ .select { |remote_name| remote_path.start_with?("#{remote_name}/") }
144
+ .max_by(&:length)
145
+
146
+ configured_remote_name || Git::BranchInfo.fallback_remote_name(refname)
147
+ end
148
+
149
+ # Check if the refname represents a detached HEAD state or non-branch entry
150
+ #
151
+ # Git outputs special entries for detached HEAD and non-branch states:
152
+ # - "(HEAD detached at <ref>)" when in detached HEAD state
153
+ # - "(not a branch)" for non-branch entries
154
+ #
155
+ # @param refname [String] the refname to check
156
+ #
157
+ # @return [Boolean] true if this is a non-branch entry
158
+ #
159
+ def non_branch_entry?(refname)
160
+ refname.match?(/^\(HEAD detached/) || refname.match?(/^\(not a branch\)/)
161
+ end
162
+
163
+ # Return the raw upstream refname string, or nil if empty
164
+ #
165
+ # @param upstream_ref [String, nil] the upstream ref (e.g., 'refs/remotes/origin/main')
166
+ #
167
+ # @return [String, nil] the raw upstream refname, or nil
168
+ #
169
+ def build_upstream_info(upstream_ref)
170
+ return nil if upstream_ref.nil? || upstream_ref.empty?
171
+
172
+ upstream_ref
173
+ end
174
+
175
+ # Return value if non-empty, nil otherwise
176
+ #
177
+ # @param value [String, nil] the value to check
178
+ #
179
+ # @return [String, nil] the value or nil
180
+ #
181
+ def presence(value)
182
+ value.nil? || value.empty? ? nil : value
183
+ end
184
+
185
+ # Parse deleted branch names from stdout
186
+ #
187
+ # @example
188
+ # BranchParser.parse_deleted_branches("Deleted branch feature (was abc123).\n")
189
+ # # => ["feature"]
190
+ #
191
+ # @param stdout [String] command stdout
192
+ #
193
+ # @return [Array<String>] names of successfully deleted branches
194
+ #
195
+ def parse_deleted_branches(stdout)
196
+ stdout.scan(DELETED_BRANCH_REGEX).flatten
197
+ end
198
+
199
+ # Parse error messages from stderr into a map
200
+ #
201
+ # @example
202
+ # BranchParser.parse_error_messages("error: branch 'missing' not found.\n")
203
+ # # => {"missing" => "error: branch 'missing' not found."}
204
+ #
205
+ # @param stderr [String] command stderr
206
+ #
207
+ # @return [Hash<String, String>] map of branch name to error message
208
+ #
209
+ def parse_error_messages(stderr)
210
+ stderr.each_line.with_object({}) do |line, hash|
211
+ match = line.match(ERROR_BRANCH_REGEX)
212
+ hash[match[1]] = line.strip if match
213
+ end
214
+ end
215
+
216
+ # Build the BranchDeleteResult from parsed data
217
+ #
218
+ # @param requested_names [Array<String>] originally requested branch names
219
+ #
220
+ # @param existing_branches [Hash<String, Git::BranchInfo>] branches that existed before delete
221
+ #
222
+ # @param deleted_names [Array<String>] names confirmed deleted in stdout
223
+ #
224
+ # @param error_map [Hash<String, String>] map of branch name to error message
225
+ #
226
+ # @return [Git::BranchDeleteResult] the result object
227
+ #
228
+ def build_delete_result(requested_names, existing_branches, deleted_names, error_map)
229
+ deleted = deleted_names.filter_map { |name| existing_branches[name] }
230
+
231
+ not_deleted = (requested_names - deleted_names).map do |name|
232
+ error_message = error_map[name] || "branch '#{name}' could not be deleted"
233
+ Git::BranchDeleteFailure.new(name: name, error_message: error_message)
234
+ end
235
+
236
+ Git::BranchDeleteResult.new(deleted: deleted, not_deleted: not_deleted)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Parsers
5
+ # Parser for `git cat-file` commit and tag output
6
+ #
7
+ # Provides class methods that transform raw `git cat-file` output lines into
8
+ # structured Hash objects consumed by the `Git::Repository::ObjectOperations`
9
+ # facade.
10
+ #
11
+ # @api private
12
+ #
13
+ module CatFile
14
+ module_function
15
+
16
+ # Matches a single `git cat-file` header line
17
+ #
18
+ # @api private
19
+ #
20
+ CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
21
+
22
+ # Parse `git cat-file commit` output into a structured Hash
23
+ #
24
+ # @param lines [Array<String>] mutable cat-file output lines, consumed
25
+ # in place during header parsing
26
+ #
27
+ # @param sha [String] the object name passed by the caller
28
+ #
29
+ # @return [Hash] commit data hash with string keys
30
+ #
31
+ # @api private
32
+ #
33
+ def parse_commit(lines, sha)
34
+ headers = parse_commit_headers(lines)
35
+ message = "#{lines.join("\n")}\n"
36
+ { 'sha' => sha, 'message' => message }.merge(headers)
37
+ end
38
+
39
+ # Parse `git cat-file tag` output into a structured Hash
40
+ #
41
+ # @param lines [Array<String>] mutable cat-file output lines, consumed
42
+ # in place during header parsing; remaining lines become the message
43
+ #
44
+ # @param name [String] the tag name passed by the caller
45
+ #
46
+ # @return [Hash] tag data hash with string keys
47
+ #
48
+ # @api private
49
+ #
50
+ def parse_tag(lines, name)
51
+ hsh = { 'name' => name }
52
+ each_header(lines) { |key, value| hsh[key] = value }
53
+ hsh['message'] = "#{lines.join("\n")}\n"
54
+ hsh
55
+ end
56
+
57
+ # Extracts and returns commit headers from the front of `lines`
58
+ #
59
+ # Mutates `lines` in place, consuming header lines and the blank
60
+ # separator line. After the call `lines` contains only message lines.
61
+ #
62
+ # @param lines [Array<String>] mutable cat-file output lines
63
+ #
64
+ # @return [Hash] parsed header key/value pairs; `parent` is always
65
+ # an Array
66
+ #
67
+ # @api private
68
+ #
69
+ def parse_commit_headers(lines)
70
+ headers = { 'parent' => [] }
71
+ each_header(lines) do |key, value|
72
+ if key == 'parent'
73
+ headers['parent'] << value
74
+ else
75
+ headers[key] = value
76
+ end
77
+ end
78
+ headers
79
+ end
80
+
81
+ # Yields parsed header key/value pairs from `git cat-file` output lines
82
+ #
83
+ # Consumes header lines from the front of `lines` until a blank line is
84
+ # encountered. Continuation lines that begin with a space are folded
85
+ # into the previous header value using newline separators.
86
+ #
87
+ # @param lines [Array<String>] mutable output lines from a cat-file response
88
+ #
89
+ # @return [void]
90
+ #
91
+ # @yield [key, value] each parsed header pair
92
+ #
93
+ # @yieldparam key [String] header field name
94
+ #
95
+ # @yieldparam value [String] unfolded header value text
96
+ #
97
+ # @yieldreturn [void]
98
+ #
99
+ # @api private
100
+ #
101
+ def each_header(lines)
102
+ while (line = lines.shift) && (match = CAT_FILE_HEADER_LINE.match(line))
103
+ key = match[:key]
104
+ value_lines = [match[:value]]
105
+ value_lines << lines.shift.lstrip while lines.first&.start_with?(' ')
106
+ yield key, value_lines.join("\n")
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/config_entry_info'
4
+
5
+ module Git
6
+ module Parsers
7
+ # Parser for `git config` output
8
+ #
9
+ # Each public method corresponds to a different `git config` sub-command
10
+ # variant and handles the specific output format that variant produces.
11
+ # Most variants are called with `--show-scope --show-origin --null`;
12
+ # `parse_urlmatch` handles the `--get-urlmatch` format where `--show-origin`
13
+ # is unsupported and therefore absent.
14
+ #
15
+ # @api private
16
+ #
17
+ module ConfigEntry
18
+ module_function
19
+
20
+ # Parse `git config --get --show-scope --show-origin --null` output
21
+ #
22
+ # Output format (per entry): `scope\0origin\0value\0`
23
+ #
24
+ # The key name is absent from `--get` output and must be supplied by the
25
+ # caller.
26
+ #
27
+ # @param key [String] the config key name that was queried
28
+ #
29
+ # @param output [String] raw stdout from the command
30
+ #
31
+ # @return [Git::ConfigEntryInfo, nil] the parsed entry, or `nil` when not found
32
+ #
33
+ def parse_get(key, output)
34
+ return nil if output.empty?
35
+
36
+ scope, origin, value = output.split("\0", -1)
37
+ Git::ConfigEntryInfo.new(scope: scope, origin: origin, key: key, value: value)
38
+ end
39
+
40
+ # Parse `git config --get-all --show-scope --show-origin --null` output
41
+ #
42
+ # Output format (per entry): `scope\0origin\0value\0`
43
+ #
44
+ # The key name is absent from `--get-all` output and must be supplied by
45
+ # the caller. Multiple entries appear back-to-back in the same string.
46
+ #
47
+ # @param key [String] the config key name that was queried
48
+ #
49
+ # @param output [String] raw stdout from the command
50
+ #
51
+ # @return [Array<Git::ConfigEntryInfo>] the parsed entries
52
+ #
53
+ def parse_get_all(key, output)
54
+ return [] if output.empty?
55
+
56
+ tokens = output.split("\0", -1)
57
+ tokens.pop if tokens.last == ''
58
+ tokens.each_slice(3).map do |scope, origin, value|
59
+ Git::ConfigEntryInfo.new(scope: scope, origin: origin, key: key, value: value)
60
+ end
61
+ end
62
+
63
+ # Parse `git config --list --show-scope --show-origin --null` output
64
+ #
65
+ # Also handles `--get-regexp` output, which shares the same format.
66
+ #
67
+ # Output format (per entry): `scope\0origin\0key\nvalue\0`
68
+ #
69
+ # Multiple entries appear back-to-back in the same string.
70
+ #
71
+ # @param output [String] raw stdout from the command
72
+ #
73
+ # @return [Array<Git::ConfigEntryInfo>] the parsed entries
74
+ #
75
+ def parse_list(output)
76
+ return [] if output.empty?
77
+
78
+ tokens = output.split("\0", -1)
79
+ tokens.pop if tokens.last == ''
80
+ tokens.each_slice(3).map do |scope, origin, key_value|
81
+ key, value = key_value.split("\n", 2)
82
+ Git::ConfigEntryInfo.new(scope: scope, origin: origin, key: key, value: value || '')
83
+ end
84
+ end
85
+
86
+ # Parse `git config --get-urlmatch --show-scope --null` output
87
+ #
88
+ # Handles the two-field-per-entry format produced by `--get-urlmatch` when
89
+ # `--show-scope` is used but `--show-origin` is not (git does not support
90
+ # `--show-origin` with `--get-urlmatch`).
91
+ #
92
+ # Output format (per entry): `scope\0key\nvalue\0`
93
+ #
94
+ # @param output [String] raw stdout from the command
95
+ #
96
+ # @return [Array<Git::ConfigEntryInfo>] the parsed entries; `origin` is `nil` for each
97
+ #
98
+ def parse_urlmatch(output)
99
+ return [] if output.empty?
100
+
101
+ tokens = output.split("\0", -1)
102
+ tokens.pop if tokens.last == ''
103
+ tokens.each_slice(2).map do |scope, key_value|
104
+ key, value = key_value.split("\n", 2)
105
+ Git::ConfigEntryInfo.new(scope: scope, origin: nil, key: key, value: value || '')
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end