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,144 @@
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
+ #
64
+ # @return [Git::FsckResult] the parsed result
65
+ #
66
+ def parse(stdout)
67
+ result = { dangling: [], missing: [], unreachable: [], warnings: [], root: [], tagged: [] }
68
+ stdout.each_line { |line| parse_line(line.strip, result) }
69
+ Git::FsckResult.new(**result)
70
+ end
71
+
72
+ # Parse a single line of fsck output
73
+ #
74
+ # @param line [String] a line of output
75
+ #
76
+ # @param result [Hash] the result hash to populate
77
+ #
78
+ # @return [Boolean] true if the line was parsed
79
+ #
80
+ def parse_line(line, result)
81
+ parse_object_line(line, result) ||
82
+ parse_warning_line(line, result) ||
83
+ parse_root_line(line, result) ||
84
+ parse_tagged_line(line, result)
85
+ end
86
+
87
+ # Parse a dangling/missing/unreachable object line
88
+ #
89
+ # @param line [String] a line of output
90
+ #
91
+ # @param result [Hash] the result hash to populate
92
+ #
93
+ # @return [Boolean] true if the line was parsed
94
+ #
95
+ def parse_object_line(line, result)
96
+ return unless (match = OBJECT_PATTERN.match(line))
97
+
98
+ result[match[1].to_sym] << Git::FsckObject.new(type: match[2].to_sym, oid: match[3], name: match[4])
99
+ end
100
+
101
+ # Parse a warning line
102
+ #
103
+ # @param line [String] a line of output
104
+ #
105
+ # @param result [Hash] the result hash to populate
106
+ #
107
+ # @return [Boolean] true if the line was parsed
108
+ #
109
+ def parse_warning_line(line, result)
110
+ return unless (match = WARNING_PATTERN.match(line))
111
+
112
+ result[:warnings] << Git::FsckObject.new(type: match[1].to_sym, oid: match[2], message: match[3])
113
+ end
114
+
115
+ # Parse a root line
116
+ #
117
+ # @param line [String] a line of output
118
+ #
119
+ # @param result [Hash] the result hash to populate
120
+ #
121
+ # @return [Boolean] true if the line was parsed
122
+ #
123
+ def parse_root_line(line, result)
124
+ return unless (match = ROOT_PATTERN.match(line))
125
+
126
+ result[:root] << Git::FsckObject.new(type: :commit, oid: match[1])
127
+ end
128
+
129
+ # Parse a tagged line
130
+ #
131
+ # @param line [String] a line of output
132
+ #
133
+ # @param result [Hash] the result hash to populate
134
+ #
135
+ # @return [Boolean] true if the line was parsed
136
+ #
137
+ def parse_tagged_line(line, result)
138
+ return unless (match = TAGGED_PATTERN.match(line))
139
+
140
+ result[:tagged] << Git::FsckObject.new(type: match[1].to_sym, oid: match[2], name: match[3])
141
+ end
142
+ end
143
+ end
144
+ 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
+ _, type, name = info.split('/', 3)
51
+
52
+ type ||= 'head'
53
+ type = 'branches' if type == 'heads'
54
+
55
+ value = { ref: info, 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,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/config_entry_info'
4
+ require 'git/remote_info'
5
+
6
+ module Git
7
+ module Parsers
8
+ # Parser that builds {Git::RemoteInfo} objects from git config entries
9
+ #
10
+ # Accepts `Array<Git::ConfigEntryInfo>` as returned by
11
+ # {Git::Configuring#config_list} and groups entries by remote name,
12
+ # collecting multi-value fields and coercing boolean values.
13
+ #
14
+ # @api private
15
+ #
16
+ module Remote
17
+ module_function
18
+
19
+ # Parse an array of config entries into an array of RemoteInfo objects
20
+ #
21
+ # Groups entries whose key matches `remote.<name>.<variable>` by remote
22
+ # name and builds one {Git::RemoteInfo} per unique remote name.
23
+ #
24
+ # Non-remote config entries (keys that do not start with `remote.`) are
25
+ # silently ignored.
26
+ #
27
+ # @param config_entries [Array<Git::ConfigEntryInfo>] all config entries
28
+ # to inspect; may contain non-remote entries
29
+ #
30
+ # @return [Array<Git::RemoteInfo>] one entry per unique remote name, in
31
+ # the order the first entry for each name appeared in `config_entries`
32
+ #
33
+ # @raise [ArgumentError] if a boolean config field carries an unrecognized
34
+ # value (mirrors git's own fatal-error behavior)
35
+ #
36
+ def parse_list(config_entries)
37
+ group_by_remote(config_entries).map { |name, pairs| build_remote_info(name, pairs) }
38
+ end
39
+
40
+ # Group config entries by remote name, collecting variable/value pairs
41
+ #
42
+ # @param config_entries [Array<Git::ConfigEntryInfo>]
43
+ #
44
+ # @return [Hash{String => Array}] remote name to variable/value pairs
45
+ #
46
+ # @api private
47
+ #
48
+ def group_by_remote(config_entries)
49
+ config_entries.each_with_object({}) do |entry, groups|
50
+ next unless entry.section == 'remote' && !entry.subsection.empty?
51
+
52
+ remote_name = entry.subsection
53
+ variable = entry.variable.downcase
54
+ groups[remote_name] ||= []
55
+ groups[remote_name] << [variable, entry.value]
56
+ end
57
+ end
58
+
59
+ # Config variable names that hold arrays of strings
60
+ ARRAY_VARIABLES = %w[url pushurl fetch push].freeze
61
+ private_constant :ARRAY_VARIABLES
62
+
63
+ # Config variable names that hold boolean values
64
+ BOOLEAN_VARIABLES = %w[mirror skipdefaultupdate prune prunetags promisor].freeze
65
+ private_constant :BOOLEAN_VARIABLES
66
+
67
+ # Map from git config variable name to RemoteInfo field name (symbol)
68
+ #
69
+ # All keys are lowercase because `git config --list` unconditionally
70
+ # lowercases section names and variable names (subsection names are
71
+ # case-preserved, but are never used as lookup keys here).
72
+ VARIABLE_TO_FIELD = {
73
+ 'url' => :url,
74
+ 'pushurl' => :push_url,
75
+ 'fetch' => :fetch,
76
+ 'push' => :push,
77
+ 'mirror' => :mirror,
78
+ 'skipdefaultupdate' => :skip_default_update,
79
+ 'tagopt' => :tag_opt,
80
+ 'prune' => :prune,
81
+ 'prunetags' => :prune_tags,
82
+ 'receivepack' => :receivepack,
83
+ 'uploadpack' => :uploadpack,
84
+ 'promisor' => :promisor,
85
+ 'partialclonefilter' => :partial_clone_filter,
86
+ 'vcs' => :vcs
87
+ }.freeze
88
+ private_constant :VARIABLE_TO_FIELD
89
+
90
+ # True string values per git's boolean rules
91
+ BOOL_TRUE_VALUES = %w[true yes on 1].freeze
92
+ private_constant :BOOL_TRUE_VALUES
93
+
94
+ # False string values per git's boolean rules
95
+ BOOL_FALSE_VALUES = %w[false no off 0].freeze
96
+ private_constant :BOOL_FALSE_VALUES
97
+
98
+ # Build a RemoteInfo from grouped variable/value pairs
99
+ #
100
+ # @param remote_name [String]
101
+ #
102
+ # @param pairs [Array] variable/value pairs collected from config entries
103
+ #
104
+ # @return [Git::RemoteInfo]
105
+ #
106
+ # @api private
107
+ #
108
+ def build_remote_info(remote_name, pairs)
109
+ attrs = { name: remote_name }
110
+ pairs.each { |variable, value| apply_pair(attrs, variable, value) }
111
+ Git::RemoteInfo.new(**attrs)
112
+ end
113
+
114
+ # Apply a single variable/value pair to an attrs hash
115
+ #
116
+ # @param attrs [Hash] accumulator of RemoteInfo keyword arguments
117
+ #
118
+ # @param variable [String] git config variable name (e.g. 'url', 'tagopt')
119
+ #
120
+ # @param value [String] raw config value
121
+ #
122
+ # @return [void]
123
+ #
124
+ # @api private
125
+ #
126
+ def apply_pair(attrs, variable, value)
127
+ field = VARIABLE_TO_FIELD[variable]
128
+ return unless field # ignore unknown variables
129
+
130
+ if ARRAY_VARIABLES.include?(variable)
131
+ attrs[field] ||= []
132
+ attrs[field] << value
133
+ elsif BOOLEAN_VARIABLES.include?(variable)
134
+ attrs[field] = coerce_boolean(variable, value)
135
+ else
136
+ attrs[field] = value
137
+ end
138
+ end
139
+
140
+ # Coerce a git config boolean string to a Ruby boolean
141
+ #
142
+ # @param variable [String] the config variable name (for error messages)
143
+ #
144
+ # @param value [String] the raw string value from git config
145
+ #
146
+ # @return [Boolean] `true` or `false`
147
+ #
148
+ # @raise [ArgumentError] if the value is not a recognized boolean
149
+ #
150
+ # @api private
151
+ #
152
+ def coerce_boolean(variable, value)
153
+ normalized = value.downcase
154
+ return true if BOOL_TRUE_VALUES.include?(normalized) || normalized == ''
155
+ return false if BOOL_FALSE_VALUES.include?(normalized)
156
+
157
+ raise ArgumentError,
158
+ "unrecognized boolean value #{value.inspect} for config variable #{variable.inspect}"
159
+ end
160
+ end
161
+ end
162
+ end