git 4.3.2 → 5.0.0.beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +73 -2705
  3. data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
  4. data/.github/pull_request_template.md +3 -1
  5. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  6. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  7. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  8. data/.github/skills/command-implementation/SKILL.md +229 -0
  9. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  10. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  11. data/.github/skills/dependency-management/SKILL.md +72 -0
  12. data/.github/skills/development-workflow/SKILL.md +506 -0
  13. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  14. data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
  15. data/.github/skills/extract-facade-from-base-lib/SKILL.md +600 -0
  16. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  17. data/.github/skills/facade-implementation/SKILL.md +274 -0
  18. data/.github/skills/facade-test-conventions/SKILL.md +394 -0
  19. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  20. data/.github/skills/make-skill-template/SKILL.md +176 -0
  21. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  22. data/.github/skills/project-context/SKILL.md +313 -0
  23. data/.github/skills/pull-request-review/SKILL.md +168 -0
  24. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  25. data/.github/skills/release-management/SKILL.md +125 -0
  26. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  27. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  28. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  29. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  30. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  31. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  32. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  33. data/.github/skills/test-debugging/SKILL.md +160 -0
  34. data/.github/skills/yard-documentation/SKILL.md +793 -0
  35. data/.github/workflows/continuous_integration.yml +3 -2
  36. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  37. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  38. data/.github/workflows/release.yml +3 -4
  39. data/.gitignore +8 -0
  40. data/.husky/pre-commit +13 -0
  41. data/.release-please-manifest.json +1 -1
  42. data/.rspec +3 -0
  43. data/.rubocop.yml +12 -3
  44. data/.rubocop_todo.yml +23 -5
  45. data/.yardopts +1 -0
  46. data/CHANGELOG.md +0 -40
  47. data/CONTRIBUTING.md +694 -53
  48. data/README.md +67 -15
  49. data/Rakefile +61 -9
  50. data/UPGRADING.md +141 -0
  51. data/commitlint.test +4 -0
  52. data/git.gemspec +18 -7
  53. data/lib/git/args_builder.rb +0 -8
  54. data/lib/git/branch.rb +369 -43
  55. data/lib/git/branch_delete_failure.rb +31 -0
  56. data/lib/git/branch_delete_result.rb +63 -0
  57. data/lib/git/branch_info.rb +178 -0
  58. data/lib/git/branches.rb +122 -24
  59. data/lib/git/command_line/base.rb +255 -0
  60. data/lib/git/command_line/capturing.rb +251 -0
  61. data/lib/git/command_line/result.rb +96 -0
  62. data/lib/git/command_line/streaming.rb +196 -0
  63. data/lib/git/command_line.rb +43 -322
  64. data/lib/git/command_line_result.rb +4 -88
  65. data/lib/git/commands/add.rb +131 -0
  66. data/lib/git/commands/am/abort.rb +43 -0
  67. data/lib/git/commands/am/apply.rb +252 -0
  68. data/lib/git/commands/am/continue.rb +43 -0
  69. data/lib/git/commands/am/quit.rb +43 -0
  70. data/lib/git/commands/am/retry.rb +47 -0
  71. data/lib/git/commands/am/show_current_patch.rb +64 -0
  72. data/lib/git/commands/am/skip.rb +42 -0
  73. data/lib/git/commands/am.rb +33 -0
  74. data/lib/git/commands/apply.rb +237 -0
  75. data/lib/git/commands/archive/list_formats.rb +46 -0
  76. data/lib/git/commands/archive.rb +140 -0
  77. data/lib/git/commands/arguments.rb +3510 -0
  78. data/lib/git/commands/base.rb +404 -0
  79. data/lib/git/commands/branch/copy.rb +94 -0
  80. data/lib/git/commands/branch/create.rb +173 -0
  81. data/lib/git/commands/branch/delete.rb +80 -0
  82. data/lib/git/commands/branch/list.rb +162 -0
  83. data/lib/git/commands/branch/move.rb +94 -0
  84. data/lib/git/commands/branch/set_upstream.rb +86 -0
  85. data/lib/git/commands/branch/show_current.rb +49 -0
  86. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  87. data/lib/git/commands/branch.rb +34 -0
  88. data/lib/git/commands/cat_file/batch.rb +369 -0
  89. data/lib/git/commands/cat_file/filtered.rb +105 -0
  90. data/lib/git/commands/cat_file/raw.rb +216 -0
  91. data/lib/git/commands/cat_file.rb +49 -0
  92. data/lib/git/commands/checkout/branch.rb +151 -0
  93. data/lib/git/commands/checkout/files.rb +115 -0
  94. data/lib/git/commands/checkout.rb +38 -0
  95. data/lib/git/commands/checkout_index.rb +105 -0
  96. data/lib/git/commands/clean.rb +100 -0
  97. data/lib/git/commands/clone.rb +240 -0
  98. data/lib/git/commands/commit.rb +272 -0
  99. data/lib/git/commands/commit_tree.rb +100 -0
  100. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  101. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  102. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  103. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  104. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  105. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  106. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +107 -0
  107. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  108. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  109. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  110. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  111. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  112. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  113. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  114. data/lib/git/commands/config_option_syntax.rb +56 -0
  115. data/lib/git/commands/describe.rb +155 -0
  116. data/lib/git/commands/diff.rb +656 -0
  117. data/lib/git/commands/diff_files.rb +518 -0
  118. data/lib/git/commands/diff_index.rb +496 -0
  119. data/lib/git/commands/fetch.rb +352 -0
  120. data/lib/git/commands/fsck.rb +136 -0
  121. data/lib/git/commands/gc.rb +132 -0
  122. data/lib/git/commands/grep.rb +338 -0
  123. data/lib/git/commands/init.rb +99 -0
  124. data/lib/git/commands/log.rb +632 -0
  125. data/lib/git/commands/ls_files.rb +191 -0
  126. data/lib/git/commands/ls_remote.rb +155 -0
  127. data/lib/git/commands/ls_tree.rb +131 -0
  128. data/lib/git/commands/maintenance/register.rb +75 -0
  129. data/lib/git/commands/maintenance/run.rb +104 -0
  130. data/lib/git/commands/maintenance/start.rb +66 -0
  131. data/lib/git/commands/maintenance/stop.rb +55 -0
  132. data/lib/git/commands/maintenance/unregister.rb +79 -0
  133. data/lib/git/commands/maintenance.rb +31 -0
  134. data/lib/git/commands/merge/abort.rb +44 -0
  135. data/lib/git/commands/merge/continue.rb +44 -0
  136. data/lib/git/commands/merge/quit.rb +46 -0
  137. data/lib/git/commands/merge/start.rb +245 -0
  138. data/lib/git/commands/merge.rb +28 -0
  139. data/lib/git/commands/merge_base.rb +86 -0
  140. data/lib/git/commands/mv.rb +77 -0
  141. data/lib/git/commands/name_rev.rb +114 -0
  142. data/lib/git/commands/pull.rb +377 -0
  143. data/lib/git/commands/push.rb +246 -0
  144. data/lib/git/commands/read_tree.rb +149 -0
  145. data/lib/git/commands/remote/add.rb +91 -0
  146. data/lib/git/commands/remote/get_url.rb +66 -0
  147. data/lib/git/commands/remote/list.rb +54 -0
  148. data/lib/git/commands/remote/prune.rb +61 -0
  149. data/lib/git/commands/remote/remove.rb +52 -0
  150. data/lib/git/commands/remote/rename.rb +69 -0
  151. data/lib/git/commands/remote/set_branches.rb +63 -0
  152. data/lib/git/commands/remote/set_head.rb +82 -0
  153. data/lib/git/commands/remote/set_url.rb +71 -0
  154. data/lib/git/commands/remote/set_url_add.rb +61 -0
  155. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  156. data/lib/git/commands/remote/show.rb +71 -0
  157. data/lib/git/commands/remote/update.rb +72 -0
  158. data/lib/git/commands/remote.rb +42 -0
  159. data/lib/git/commands/repack.rb +277 -0
  160. data/lib/git/commands/reset.rb +147 -0
  161. data/lib/git/commands/rev_parse.rb +297 -0
  162. data/lib/git/commands/revert/abort.rb +45 -0
  163. data/lib/git/commands/revert/continue.rb +57 -0
  164. data/lib/git/commands/revert/quit.rb +47 -0
  165. data/lib/git/commands/revert/skip.rb +44 -0
  166. data/lib/git/commands/revert/start.rb +153 -0
  167. data/lib/git/commands/revert.rb +29 -0
  168. data/lib/git/commands/rm.rb +114 -0
  169. data/lib/git/commands/show.rb +632 -0
  170. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  171. data/lib/git/commands/show_ref/exists.rb +78 -0
  172. data/lib/git/commands/show_ref/list.rb +145 -0
  173. data/lib/git/commands/show_ref/verify.rb +120 -0
  174. data/lib/git/commands/show_ref.rb +42 -0
  175. data/lib/git/commands/stash/apply.rb +75 -0
  176. data/lib/git/commands/stash/branch.rb +65 -0
  177. data/lib/git/commands/stash/clear.rb +41 -0
  178. data/lib/git/commands/stash/create.rb +58 -0
  179. data/lib/git/commands/stash/drop.rb +67 -0
  180. data/lib/git/commands/stash/list.rb +39 -0
  181. data/lib/git/commands/stash/pop.rb +78 -0
  182. data/lib/git/commands/stash/push.rb +103 -0
  183. data/lib/git/commands/stash/show.rb +149 -0
  184. data/lib/git/commands/stash/store.rb +63 -0
  185. data/lib/git/commands/stash.rb +38 -0
  186. data/lib/git/commands/status.rb +169 -0
  187. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  188. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  189. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  190. data/lib/git/commands/symbolic_ref.rb +38 -0
  191. data/lib/git/commands/tag/create.rb +139 -0
  192. data/lib/git/commands/tag/delete.rb +55 -0
  193. data/lib/git/commands/tag/list.rb +143 -0
  194. data/lib/git/commands/tag/verify.rb +71 -0
  195. data/lib/git/commands/tag.rb +26 -0
  196. data/lib/git/commands/update_ref/batch.rb +140 -0
  197. data/lib/git/commands/update_ref/delete.rb +92 -0
  198. data/lib/git/commands/update_ref/update.rb +106 -0
  199. data/lib/git/commands/update_ref.rb +42 -0
  200. data/lib/git/commands/version.rb +57 -0
  201. data/lib/git/commands/worktree/add.rb +140 -0
  202. data/lib/git/commands/worktree/list.rb +64 -0
  203. data/lib/git/commands/worktree/lock.rb +58 -0
  204. data/lib/git/commands/worktree/management_base.rb +51 -0
  205. data/lib/git/commands/worktree/move.rb +66 -0
  206. data/lib/git/commands/worktree/prune.rb +67 -0
  207. data/lib/git/commands/worktree/remove.rb +63 -0
  208. data/lib/git/commands/worktree/repair.rb +76 -0
  209. data/lib/git/commands/worktree/unlock.rb +47 -0
  210. data/lib/git/commands/worktree.rb +43 -0
  211. data/lib/git/commands/write_tree.rb +68 -0
  212. data/lib/git/commands.rb +87 -0
  213. data/lib/git/config.rb +17 -0
  214. data/lib/git/config_entry_info.rb +106 -0
  215. data/lib/git/configuring.rb +665 -0
  216. data/lib/git/deprecation.rb +9 -0
  217. data/lib/git/detached_head_info.rb +54 -0
  218. data/lib/git/diff.rb +293 -7
  219. data/lib/git/diff_file_numstat_info.rb +29 -0
  220. data/lib/git/diff_file_patch_info.rb +134 -0
  221. data/lib/git/diff_file_raw_info.rb +127 -0
  222. data/lib/git/diff_info.rb +169 -0
  223. data/lib/git/diff_path_status.rb +67 -19
  224. data/lib/git/diff_result.rb +32 -0
  225. data/lib/git/diff_stats.rb +51 -14
  226. data/lib/git/dirstat_info.rb +86 -0
  227. data/lib/git/errors.rb +65 -2
  228. data/lib/git/execution_context/global.rb +31 -0
  229. data/lib/git/execution_context/repository.rb +136 -0
  230. data/lib/git/execution_context.rb +501 -0
  231. data/lib/git/file_ref.rb +74 -0
  232. data/lib/git/fsck_object.rb +9 -9
  233. data/lib/git/fsck_result.rb +1 -1
  234. data/lib/git/log.rb +9 -2
  235. data/lib/git/object.rb +90 -27
  236. data/lib/git/parsers/branch.rb +224 -0
  237. data/lib/git/parsers/cat_file.rb +111 -0
  238. data/lib/git/parsers/config_entry.rb +110 -0
  239. data/lib/git/parsers/diff.rb +585 -0
  240. data/lib/git/parsers/fsck.rb +133 -0
  241. data/lib/git/parsers/grep.rb +42 -0
  242. data/lib/git/parsers/ls_remote.rb +79 -0
  243. data/lib/git/parsers/ls_tree.rb +58 -0
  244. data/lib/git/parsers/stash.rb +208 -0
  245. data/lib/git/parsers/tag.rb +257 -0
  246. data/lib/git/remote.rb +120 -9
  247. data/lib/git/repository/branching.rb +743 -0
  248. data/lib/git/repository/committing.rb +187 -0
  249. data/lib/git/repository/configuring.rb +351 -0
  250. data/lib/git/repository/context_helpers.rb +264 -0
  251. data/lib/git/repository/diffing.rb +775 -0
  252. data/lib/git/repository/factories.rb +682 -0
  253. data/lib/git/repository/inspecting.rb +252 -0
  254. data/lib/git/repository/logging.rb +247 -0
  255. data/lib/git/repository/maintenance.rb +65 -0
  256. data/lib/git/repository/merging.rb +357 -0
  257. data/lib/git/repository/object_operations.rb +1199 -0
  258. data/lib/git/repository/path_resolver.rb +207 -0
  259. data/lib/git/repository/remote_operations.rb +898 -0
  260. data/lib/git/repository/shared_private.rb +51 -0
  261. data/lib/git/repository/staging.rb +554 -0
  262. data/lib/git/repository/stashing.rb +143 -0
  263. data/lib/git/repository/status_operations.rb +201 -0
  264. data/lib/git/repository/worktree_operations.rb +159 -0
  265. data/lib/git/repository.rb +203 -1
  266. data/lib/git/stash.rb +78 -4
  267. data/lib/git/stash_info.rb +104 -0
  268. data/lib/git/stashes.rb +125 -13
  269. data/lib/git/status.rb +215 -18
  270. data/lib/git/tag_delete_failure.rb +31 -0
  271. data/lib/git/tag_delete_result.rb +63 -0
  272. data/lib/git/tag_info.rb +105 -0
  273. data/lib/git/version.rb +109 -2
  274. data/lib/git/version_constraint.rb +81 -0
  275. data/lib/git/worktree.rb +108 -6
  276. data/lib/git/worktrees.rb +94 -7
  277. data/lib/git.rb +260 -61
  278. data/redesign/1_architecture_existing.md +54 -18
  279. data/redesign/2_architecture_redesign.md +365 -46
  280. data/redesign/3_architecture_implementation.md +1497 -63
  281. data/redesign/Phase 4 - Step A.md +360 -0
  282. data/redesign/beta_release.md +107 -0
  283. data/redesign/c1c2_audit.md +566 -0
  284. data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
  285. data/redesign/config_design.rb +501 -0
  286. data/tasks/gem_tasks.rake +4 -0
  287. data/tasks/npm_tasks.rake +7 -0
  288. data/tasks/rspec.rake +48 -0
  289. data/tasks/test.rake +13 -1
  290. data/tasks/yard.rake +34 -7
  291. metadata +365 -22
  292. data/lib/git/base.rb +0 -1128
  293. data/lib/git/index.rb +0 -6
  294. data/lib/git/lib.rb +0 -2277
  295. data/lib/git/path.rb +0 -38
  296. data/lib/git/working_directory.rb +0 -6
  297. /data/{release-please-config.json → .release-please-config.json} +0 -0
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/fsck_object'
4
+ require 'git/fsck_result'
5
+
6
+ module Git
7
+ module Parsers
8
+ # Parser for git fsck command output
9
+ #
10
+ # Handles parsing of `git fsck` output into structured data objects
11
+ # for dangling, missing, unreachable objects, warnings, roots, and tagged objects.
12
+ #
13
+ # ## Design Note: Namespace Organization
14
+ #
15
+ # This parser creates and returns {Git::FsckObject} and {Git::FsckResult}
16
+ # objects, which live at the top-level `Git::` namespace rather than within
17
+ # `Git::Parsers::`. This is intentional:
18
+ #
19
+ # - **Parsers are infrastructure** - marked `@api private`, users shouldn't
20
+ # interact with them directly
21
+ # - **Result classes are public API** - returned by commands and used
22
+ # throughout the codebase
23
+ # - **FsckObject and FsckResult represent operation outcomes** - they describe
24
+ # repository integrity status, not parsing details
25
+ #
26
+ # Keeping these classes at `Git::` improves discoverability and correctly
27
+ # reflects their role as public types rather than parser internals.
28
+ #
29
+ # @api private
30
+ #
31
+ module Fsck
32
+ # Pattern matcher for dangling/missing/unreachable object lines
33
+ # Matches lines like:
34
+ # dangling commit abc123...
35
+ # missing blob def456...
36
+ # unreachable tree 789abc... (name)
37
+ OBJECT_PATTERN = /\A(dangling|missing|unreachable) (\w+) ([0-9a-f]{40})(?: \((.+)\))?\z/
38
+
39
+ # Pattern matcher for warning lines
40
+ # Matches lines like:
41
+ # warning in commit abc123...: message here
42
+ WARNING_PATTERN = /\Awarning in (\w+) ([0-9a-f]{40}): (.+)\z/
43
+
44
+ # Pattern matcher for root commit lines
45
+ # Matches lines like:
46
+ # root abc123...
47
+ ROOT_PATTERN = /\Aroot ([0-9a-f]{40})\z/
48
+
49
+ # Pattern matcher for tagged object lines
50
+ # Matches lines like:
51
+ # tagged commit abc123... (tagname) in def456...
52
+ TAGGED_PATTERN = /\Atagged (\w+) ([0-9a-f]{40}) \((.+)\) in ([0-9a-f]{40})\z/
53
+
54
+ module_function
55
+
56
+ # Parse git fsck output into a FsckResult object
57
+ #
58
+ # @example
59
+ # FsckParser.parse("dangling commit abc123...\nmissing blob def456...\n")
60
+ # # => #<Git::FsckResult dangling: [...], missing: [...]>
61
+ #
62
+ # @param stdout [String] output from git fsck command
63
+ # @return [Git::FsckResult] the parsed result
64
+ #
65
+ def parse(stdout)
66
+ result = { dangling: [], missing: [], unreachable: [], warnings: [], root: [], tagged: [] }
67
+ stdout.each_line { |line| parse_line(line.strip, result) }
68
+ Git::FsckResult.new(**result)
69
+ end
70
+
71
+ # Parse a single line of fsck output
72
+ #
73
+ # @param line [String] a line of output
74
+ # @param result [Hash] the result hash to populate
75
+ # @return [Boolean] true if the line was parsed
76
+ #
77
+ def parse_line(line, result)
78
+ parse_object_line(line, result) ||
79
+ parse_warning_line(line, result) ||
80
+ parse_root_line(line, result) ||
81
+ parse_tagged_line(line, result)
82
+ end
83
+
84
+ # Parse a dangling/missing/unreachable object line
85
+ #
86
+ # @param line [String] a line of output
87
+ # @param result [Hash] the result hash to populate
88
+ # @return [Boolean] true if the line was parsed
89
+ #
90
+ def parse_object_line(line, result)
91
+ return unless (match = OBJECT_PATTERN.match(line))
92
+
93
+ result[match[1].to_sym] << Git::FsckObject.new(type: match[2].to_sym, oid: match[3], name: match[4])
94
+ end
95
+
96
+ # Parse a warning line
97
+ #
98
+ # @param line [String] a line of output
99
+ # @param result [Hash] the result hash to populate
100
+ # @return [Boolean] true if the line was parsed
101
+ #
102
+ def parse_warning_line(line, result)
103
+ return unless (match = WARNING_PATTERN.match(line))
104
+
105
+ result[:warnings] << Git::FsckObject.new(type: match[1].to_sym, oid: match[2], message: match[3])
106
+ end
107
+
108
+ # Parse a root line
109
+ #
110
+ # @param line [String] a line of output
111
+ # @param result [Hash] the result hash to populate
112
+ # @return [Boolean] true if the line was parsed
113
+ #
114
+ def parse_root_line(line, result)
115
+ return unless (match = ROOT_PATTERN.match(line))
116
+
117
+ result[:root] << Git::FsckObject.new(type: :commit, oid: match[1])
118
+ end
119
+
120
+ # Parse a tagged line
121
+ #
122
+ # @param line [String] a line of output
123
+ # @param result [Hash] the result hash to populate
124
+ # @return [Boolean] true if the line was parsed
125
+ #
126
+ def parse_tagged_line(line, result)
127
+ return unless (match = TAGGED_PATTERN.match(line))
128
+
129
+ result[:tagged] << Git::FsckObject.new(type: match[1].to_sym, oid: match[2], name: match[3])
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Parsers
5
+ # Parser for `git grep` output
6
+ #
7
+ # Provides a class method that transforms raw `git grep --null` output into a
8
+ # structured Hash consumed by the `Git::Repository::ObjectOperations` facade.
9
+ #
10
+ # This parser is a pure text transformer with no exit-status logic. The
11
+ # calling facade is responsible for interpreting the command's exit status
12
+ # before delegating output parsing to this class.
13
+ #
14
+ # @api private
15
+ #
16
+ module Grep
17
+ module_function
18
+
19
+ # Parse `git grep --line-number --null --no-color` output into a match hash
20
+ #
21
+ # With `--null`, git separates the path and line number fields with NUL
22
+ # bytes: `treeish:filename\0linenum\0text\n`. This keeps filenames that
23
+ # contain `:<digits>:` from being confused with the line-number delimiter.
24
+ #
25
+ # @param output [String] raw output from `git grep --null --line-number`
26
+ #
27
+ # @return [Hash<String, Array<Array(Integer, String)>>] hash mapping
28
+ # `"treeish:filename"` keys to arrays of `[line_number, text]` pairs
29
+ #
30
+ # @api private
31
+ #
32
+ def parse(output)
33
+ output.each_line.with_object(Hash.new { |h, k| h[k] = [] }) do |line, hsh|
34
+ filename, line_num, text = line.chomp.split("\0", 3)
35
+ next unless text && line_num&.match?(/\A\d+\z/)
36
+
37
+ hsh[filename] << [line_num.to_i, text]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Parsers
5
+ # Parser for `git ls-remote` command output
6
+ #
7
+ # @api private
8
+ #
9
+ module LsRemote
10
+ module_function
11
+
12
+ # Parse `git ls-remote` stdout lines into a structured Hash
13
+ #
14
+ # @param lines [Array<String>] the individual stdout lines
15
+ #
16
+ # @return [Hash{String => Hash}] a map of ref type to ref data
17
+ #
18
+ # The structure differs by key:
19
+ #
20
+ # - `"head"` maps directly to `{ ref: String, sha: String }`
21
+ # - Other keys (e.g. `"branches"`, `"tags"`) map to
22
+ # `{ name => { ref: String, sha: String } }`
23
+ #
24
+ def parse_output(lines)
25
+ lines.each_with_object(Hash.new { |h, k| h[k] = {} }) do |line, hsh|
26
+ type, name, value = parse_line(line)
27
+ if name
28
+ hsh[type][name] = value
29
+ else
30
+ hsh[type].update(value)
31
+ end
32
+ end
33
+ end
34
+
35
+ # Parse a single `git ls-remote` output line
36
+ #
37
+ # @param line [String] a single line from ls-remote stdout
38
+ #
39
+ # @return [Array(String, String|nil, Hash)] `[type, name, value]` where
40
+ # `value` is `{ ref: String, sha: String }`
41
+ #
42
+ # @raise [Git::UnexpectedResultError] if the line is not in `<sha>\t<ref>` format
43
+ #
44
+ def parse_line(line)
45
+ unless line.include?("\t") && line.match?(/\A[0-9a-f]{4,}\t/)
46
+ raise Git::UnexpectedResultError, "Unexpected ls-remote output line: #{line.inspect}"
47
+ end
48
+
49
+ sha, info = line.split("\t", 2)
50
+ ref, type, name = info.split('/', 3)
51
+
52
+ type ||= 'head'
53
+ type = 'branches' if type == 'heads'
54
+
55
+ value = { ref: ref, sha: sha }
56
+
57
+ [type, name, value]
58
+ end
59
+
60
+ # Parse `git ls-remote --symref <repo> HEAD` output into a branch name
61
+ #
62
+ # @param output [String] command stdout
63
+ #
64
+ # @return [String] the default branch name
65
+ #
66
+ # @raise [Git::UnexpectedResultError] when the branch cannot be determined
67
+ #
68
+ def parse_default_branch(output)
69
+ match_data = output.match(%r{^ref: refs/remotes/[^/]+/(?<default_branch>[^\t]+)\t})
70
+ return match_data[:default_branch] if match_data
71
+
72
+ match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
73
+ return match_data[:default_branch] if match_data
74
+
75
+ raise Git::UnexpectedResultError, 'Unable to determine the default branch'
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/escaped_path'
4
+
5
+ module Git
6
+ module Parsers
7
+ # Parser for `git ls-tree` output
8
+ #
9
+ # Provides a class method that transforms raw `git ls-tree` output into a
10
+ # structured Hash consumed by the `Git::Repository::ObjectOperations` facade.
11
+ #
12
+ # @api private
13
+ #
14
+ module LsTree
15
+ module_function
16
+
17
+ # Parse `git ls-tree` output into a type-keyed hash of entries
18
+ #
19
+ # Each line of output is expected in the format produced by
20
+ # `git ls-tree`: `<mode> <type> <sha>\t<file>`.
21
+ #
22
+ # @param output [String] raw stdout from `git ls-tree`
23
+ #
24
+ # @return [Hash<String, Hash<String, Hash>>] hash keyed by object type
25
+ # (`'blob'`, `'tree'`, `'commit'`), then by filename, holding
26
+ # `:mode` and `:sha` values
27
+ #
28
+ # @api private
29
+ #
30
+ def parse(output)
31
+ data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
32
+ output.split("\n").each do |line|
33
+ info, filenm = line.split("\t", 2)
34
+ filenm = unescape_path(filenm) if filenm
35
+ mode, type, entry_sha = info.split
36
+ data[type][filenm] = { mode: mode, sha: entry_sha }
37
+ end
38
+ data
39
+ end
40
+
41
+ # Converts a git-quoted path back to its original form
42
+ #
43
+ # @param path [String] the path, possibly git-quoted
44
+ #
45
+ # @return [String] the unquoted path
46
+ #
47
+ # @api private
48
+ #
49
+ def unescape_path(path)
50
+ if path.start_with?('"') && path.end_with?('"')
51
+ Git::EscapedPath.new(path[1..-2]).unescape
52
+ else
53
+ path
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/stash_info'
4
+
5
+ module Git
6
+ module Parsers
7
+ # Parser for git stash command output
8
+ #
9
+ # Handles parsing of `git stash list` output into structured data objects.
10
+ #
11
+ # @note Known limitation: If a stash message contains the field separator
12
+ # character (\x1f, ASCII unit separator), parsing will fail or produce
13
+ # incorrect results. This is extremely rare in practice since \x1f is a
14
+ # non-printable control character.
15
+ #
16
+ # ## Design Note: Namespace Organization
17
+ #
18
+ # This parser creates and returns {Git::StashInfo} objects, which live at
19
+ # the top-level `Git::` namespace rather than within `Git::Parsers::`. This
20
+ # is intentional:
21
+ #
22
+ # - **Parsers are infrastructure** - marked `@api private`, users shouldn't
23
+ # interact with them directly
24
+ # - **Info classes are public API** - returned by commands and used throughout
25
+ # the codebase
26
+ # - **Info classes are domain entities** - represent core git concepts
27
+ # (stashes as data)
28
+ #
29
+ # Keeping Info classes at `Git::` improves discoverability and correctly
30
+ # reflects their role as public types rather than parser internals.
31
+ #
32
+ # @api private
33
+ #
34
+ module Stash
35
+ # Field separator used in custom format output
36
+ # Using a non-printable unit separator (US, 0x1F) to avoid collisions with
37
+ # stash messages and author/committer fields, while still working with
38
+ # Process.spawn (which doesn't allow NUL bytes in arguments)
39
+ FIELD_SEPARATOR = "\x1f"
40
+
41
+ # Custom format for git stash list that extracts all available metadata
42
+ # %H = full commit SHA
43
+ # %h = abbreviated commit SHA
44
+ # %gd = reflog selector (stash@\\{n})
45
+ # %gs = reflog subject (the stash message)
46
+ # %an = author name
47
+ # %ae = author email
48
+ # %aI = author date (ISO 8601 format)
49
+ # %cn = committer name
50
+ # %ce = committer email
51
+ # %cI = committer date (ISO 8601 format)
52
+ STASH_FORMAT = [
53
+ '%H', # 0: full SHA
54
+ '%h', # 1: short SHA
55
+ '%gd', # 2: reflog selector
56
+ '%gs', # 3: reflog subject (message)
57
+ '%an', # 4: author name
58
+ '%ae', # 5: author email
59
+ '%aI', # 6: author date
60
+ '%cn', # 7: committer name
61
+ '%ce', # 8: committer email
62
+ '%cI' # 9: committer date
63
+ ].join(FIELD_SEPARATOR)
64
+
65
+ # Number of fields expected in the parsed output
66
+ FIELD_COUNT = 10
67
+
68
+ # Field indices for parsed output
69
+ module Fields
70
+ OID = 0
71
+ SHORT_OID = 1
72
+ REFLOG = 2
73
+ MESSAGE = 3
74
+ AUTHOR_NAME = 4
75
+ AUTHOR_EMAIL = 5
76
+ AUTHOR_DATE = 6
77
+ COMMITTER_NAME = 7
78
+ COMMITTER_EMAIL = 8
79
+ COMMITTER_DATE = 9
80
+ end
81
+
82
+ # Pattern to extract branch from standard stash messages
83
+ # Matches "WIP on <branch>:" or "On <branch>:" at the start
84
+ BRANCH_PATTERN = /^(?:WIP on|On)\s+([^:]+):/
85
+
86
+ module_function
87
+
88
+ # Parse git stash list output into StashInfo objects
89
+ #
90
+ # @example
91
+ # StashParser.parse_list("abc123\x1fabc\x1fstash@\\{0}\x1fWIP on main: msg\x1f...\n")
92
+ # # => [#<Git::StashInfo index: 0, ...>]
93
+ #
94
+ # @param stdout [String] output from git stash list --format=...
95
+ # @return [Array<Git::StashInfo>] parsed stash information
96
+ #
97
+ # @raise [Git::UnexpectedResultError] if stash output cannot be parsed
98
+ #
99
+ def parse_list(stdout)
100
+ lines = stdout.split("\n")
101
+ lines.each_with_index.map { |line, idx| parse_stash_line(line, idx, lines) }
102
+ end
103
+
104
+ # Parse a single stash list line into a StashInfo object
105
+ #
106
+ # @param line [String] a line from git stash list output (custom format)
107
+ # @param expected_index [Integer] the expected stash index for validation
108
+ # @param all_lines [Array<String>] all output lines (for error messages)
109
+ #
110
+ # @return [Git::StashInfo] parsed stash info
111
+ #
112
+ # @raise [Git::UnexpectedResultError] if line format is unexpected
113
+ #
114
+ def parse_stash_line(line, expected_index, all_lines)
115
+ parts = line.split(FIELD_SEPARATOR, FIELD_COUNT)
116
+ return build_stash_info(parts, expected_index) if parts.length == FIELD_COUNT
117
+
118
+ raise Git::UnexpectedResultError, unexpected_stash_line_error(all_lines, line, expected_index)
119
+ end
120
+
121
+ # Build a StashInfo from parsed format parts
122
+ #
123
+ # @param parts [Array<String>] the parsed format fields
124
+ # @param expected_index [Integer] fallback index if not parseable from reflog
125
+ # @return [Git::StashInfo]
126
+ #
127
+ def build_stash_info(parts, expected_index)
128
+ index = extract_index(parts[Fields::REFLOG]) || expected_index
129
+
130
+ Git::StashInfo.new(**stash_info_attrs(parts, index))
131
+ end
132
+
133
+ # Build StashInfo attributes hash from parsed parts
134
+ #
135
+ # @param parts [Array<String>] the parsed format fields
136
+ # @param index [Integer] the resolved stash index
137
+ # @return [Hash] attributes for StashInfo.new
138
+ #
139
+ def stash_info_attrs(parts, index)
140
+ core_attrs(parts, index).merge(author_attrs(parts)).merge(committer_attrs(parts))
141
+ end
142
+
143
+ def core_attrs(parts, index)
144
+ {
145
+ index: index, name: parts[Fields::REFLOG], oid: parts[Fields::OID],
146
+ short_oid: parts[Fields::SHORT_OID], branch: extract_branch(parts[Fields::MESSAGE]),
147
+ message: parts[Fields::MESSAGE]
148
+ }
149
+ end
150
+
151
+ def author_attrs(parts)
152
+ {
153
+ author_name: parts[Fields::AUTHOR_NAME], author_email: parts[Fields::AUTHOR_EMAIL],
154
+ author_date: parts[Fields::AUTHOR_DATE]
155
+ }
156
+ end
157
+
158
+ def committer_attrs(parts)
159
+ {
160
+ committer_name: parts[Fields::COMMITTER_NAME], committer_email: parts[Fields::COMMITTER_EMAIL],
161
+ committer_date: parts[Fields::COMMITTER_DATE]
162
+ }
163
+ end
164
+
165
+ # Extract the stash index from a reflog selector
166
+ #
167
+ # @param reflog_selector [String] e.g., "stash@\\{0}"
168
+ # @return [Integer, nil] the index or nil if not found
169
+ #
170
+ def extract_index(reflog_selector)
171
+ match = reflog_selector&.match(/stash@\{(\d+)\}/)
172
+ match ? match[1].to_i : nil
173
+ end
174
+
175
+ # Extract the branch name from a stash message
176
+ #
177
+ # @param message [String] the stash message
178
+ # @return [String, nil] the branch name or nil for custom messages
179
+ #
180
+ def extract_branch(message)
181
+ match = BRANCH_PATTERN.match(message)
182
+ match ? match[1] : nil
183
+ end
184
+
185
+ # Generate error message for unexpected stash line format
186
+ #
187
+ # @param lines [Array<String>] all output lines
188
+ # @param line [String] the problematic line
189
+ # @param index [Integer] the stash index
190
+ # @return [String] formatted error message
191
+ #
192
+ def unexpected_stash_line_error(lines, line, index)
193
+ format_str = STASH_FORMAT.gsub(FIELD_SEPARATOR, '<FS>')
194
+ <<~ERROR
195
+ Unexpected line in output from `git stash list --format=#{format_str}`, at index #{index}
196
+
197
+ Expected #{FIELD_COUNT} fields separated by '\\x1f' (unit separator), got #{line.split(FIELD_SEPARATOR, -1).length}
198
+
199
+ Full output:
200
+ #{lines.join("\n ")}
201
+
202
+ Line at index #{index}:
203
+ "#{line}"
204
+ ERROR
205
+ end
206
+ end
207
+ end
208
+ end