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,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/tag_info'
4
+ require 'git/tag_delete_result'
5
+ require 'git/tag_delete_failure'
6
+
7
+ module Git
8
+ module Parsers
9
+ # Parser for git tag command output
10
+ #
11
+ # Handles parsing of `git tag --list` and `git tag --delete` output
12
+ # into structured data objects.
13
+ #
14
+ # @note Known limitation: If a tag message contains the field delimiter
15
+ # character (\x1f, ASCII unit separator), it will be preserved correctly
16
+ # since the message is the last field. However, messages are rarely crafted
17
+ # with non-printable control characters.
18
+ #
19
+ # ## Design Note: Namespace Organization
20
+ #
21
+ # This parser creates and returns {Git::TagInfo} and {Git::TagDeleteResult}
22
+ # objects, which live at the top-level `Git::` namespace rather than within
23
+ # `Git::Parsers::`. This is intentional:
24
+ #
25
+ # - **Parsers are infrastructure** - marked `@api private`, users shouldn't
26
+ # interact with them directly
27
+ # - **Info/Result classes are public API** - returned by commands and used
28
+ # throughout the codebase
29
+ # - **Info classes are domain entities** - represent core git concepts
30
+ # (tags as data)
31
+ # - **Result classes are operation outcomes** - represent command results,
32
+ # not parsing details
33
+ #
34
+ # Keeping Info/Result classes at `Git::` improves discoverability and correctly
35
+ # reflects their role as public types rather than parser internals.
36
+ #
37
+ # @api private
38
+ #
39
+ module Tag
40
+ # Delimiter for separating fields in git tag --format output
41
+ # Field separator used in custom format output
42
+ # Using the ASCII unit separator (US, 0x1F / "\x1f"), a non-printable character,
43
+ # minimizes the chance of collisions with tag names or messages and remains
44
+ # safe to pass through Process.spawn and shell argument boundaries.
45
+ FIELD_DELIMITER = "\x1f"
46
+
47
+ # Delimiter for separating records (tags) in output
48
+ # Using the ASCII record separator (RS, 0x1E / "\x1e") to delimit complete tag records.
49
+ # This allows multi-line messages (which contain newlines) to be parsed correctly
50
+ # since we split by record separator first, then by field delimiter.
51
+ RECORD_DELIMITER = "\x1e"
52
+
53
+ # Number of fields expected in the parsed output
54
+ FIELD_COUNT = 8
55
+
56
+ # Format string for git tag --format
57
+ #
58
+ # Fields:
59
+ # - %(refname:short) - tag name
60
+ # - %(objectname) - SHA of the tag object (for annotated) or commit (for lightweight)
61
+ # - %(*objectname) - Dereferenced SHA (commit ID for annotated tags, empty for lightweight)
62
+ # - %(objecttype) - 'tag' for annotated tags, target object type (commit/tree/blob/etc.) for lightweight tags
63
+ # - %(taggername) - tagger name (empty for lightweight tags)
64
+ # - %(taggeremail) - tagger email (empty for lightweight tags)
65
+ # - %(taggerdate:iso8601-strict) - tagger date in strict ISO 8601 format
66
+ # - %(contents) - full tag message (can be multi-line)
67
+ #
68
+ # Each tag record is terminated by the RECORD_DELIMITER to allow multi-line messages.
69
+ FORMAT_STRING = [
70
+ '%(refname:short)',
71
+ '%(objectname)',
72
+ '%(*objectname)',
73
+ '%(objecttype)',
74
+ '%(taggername)',
75
+ '%(taggeremail)',
76
+ '%(taggerdate:iso8601-strict)',
77
+ '%(contents)'
78
+ ].join(FIELD_DELIMITER) + RECORD_DELIMITER
79
+
80
+ # Regex to parse successful deletion lines from stdout
81
+ # Matches: Deleted tag 'tagname' (was abc123)
82
+ DELETED_TAG_REGEX = /^Deleted tag '([^']+)'/
83
+
84
+ # Regex to parse error messages from stderr
85
+ # Matches: error: tag 'tagname' not found.
86
+ ERROR_TAG_REGEX = /^error: tag '([^']+)'(.*)$/
87
+
88
+ module_function
89
+
90
+ # Parse git tag --list output into TagInfo objects
91
+ #
92
+ # @example
93
+ # TagParser.parse_list("v1.0.0\x1f...\x1e\n")
94
+ # # => [#<Git::TagInfo name: "v1.0.0", ...>]
95
+ #
96
+ # @param stdout [String] output from git tag --list --format=...
97
+ # @return [Array<Git::TagInfo>] parsed tag information
98
+ #
99
+ # @raise [Git::UnexpectedResultError] if any record has unexpected format
100
+ #
101
+ def parse_list(stdout)
102
+ # Split by record separator
103
+ # Each record may have a leading newline from the previous record's %(contents) output
104
+ # Use lstrip to remove leading whitespace (which includes the newline) from each record
105
+ records = stdout.split(RECORD_DELIMITER).map(&:lstrip).reject(&:empty?)
106
+ records.map.with_index { |record, index| parse_tag_record(record, index, records) }
107
+ end
108
+
109
+ # Parse a single formatted tag record
110
+ #
111
+ # The record format is:
112
+ # name<FS>sha<FS>deref<FS>objecttype<FS>tagger_name<FS>tagger_email<FS>tagger_date<FS>message
113
+ # where <FS> is the unit separator character ("\x1f").
114
+ #
115
+ # For lightweight tags, Git emits empty strings for the tagger fields and message;
116
+ # these are converted to nil by {#parse_optional_field} and {#parse_message}.
117
+ #
118
+ # @param record [String] a single tag record from git tag --format output
119
+ # @param index [Integer] record index for error reporting
120
+ # @param all_records [Array<String>] all output records for error messages
121
+ # @return [Git::TagInfo] tag info with all fields populated
122
+ #
123
+ # @raise [Git::UnexpectedResultError] if record format is unexpected
124
+ #
125
+ def parse_tag_record(record, index, all_records)
126
+ parts = record.split(FIELD_DELIMITER, FIELD_COUNT)
127
+
128
+ unless parts.length == FIELD_COUNT
129
+ raise Git::UnexpectedResultError, unexpected_tag_record_error(all_records, record, index)
130
+ end
131
+
132
+ build_tag_info(parts)
133
+ end
134
+
135
+ # Build a TagInfo object from parsed parts
136
+ #
137
+ # @param parts [Array<String>] the parsed format fields
138
+ # @return [Git::TagInfo]
139
+ #
140
+ # @note For annotated tags:
141
+ # - oid = %(objectname) (the tag object's ID)
142
+ # - target_oid = %(*objectname) (the dereferenced commit ID)
143
+ #
144
+ # @note For lightweight tags:
145
+ # - oid = nil (lightweight tags are not objects)
146
+ # - target_oid = %(objectname) (the commit ID)
147
+ #
148
+ def build_tag_info(parts)
149
+ oid, target_oid = resolve_oids(parts[3], parts[1], parts[2])
150
+ build_tag_info_object(parts, oid, target_oid)
151
+ end
152
+
153
+ def resolve_oids(objecttype, objectname, dereferenced)
154
+ objecttype == 'tag' ? [objectname, dereferenced] : [nil, objectname]
155
+ end
156
+
157
+ def build_tag_info_object(parts, oid, target_oid)
158
+ Git::TagInfo.new(
159
+ name: parts[0], oid: oid, target_oid: target_oid, objecttype: parts[3],
160
+ tagger_name: parse_optional_field(parts[4]), tagger_email: parse_optional_field(parts[5]),
161
+ tagger_date: parse_optional_field(parts[6]), message: parse_message(parts[3], parts[7])
162
+ )
163
+ end
164
+
165
+ # Parse an optional field, returning nil if empty
166
+ #
167
+ # @param value [String] the field value
168
+ # @return [String, nil] the value or nil if empty
169
+ #
170
+ def parse_optional_field(value)
171
+ value.empty? ? nil : value
172
+ end
173
+
174
+ # Parse message field, returning nil for lightweight tags or empty messages
175
+ # Strips trailing newlines that git adds to %(contents) output
176
+ #
177
+ # @param objecttype [String] the object type ('tag' or 'commit')
178
+ # @param message [String] the raw message field
179
+ # @return [String, nil] the message or nil
180
+ #
181
+ def parse_message(objecttype, message)
182
+ stripped = message.chomp
183
+ objecttype == 'tag' && !stripped.empty? ? stripped : nil
184
+ end
185
+
186
+ # Parse deleted tag names from stdout
187
+ #
188
+ # @example
189
+ # TagParser.parse_deleted_tags("Deleted tag 'v1.0.0' (was abc123)\n")
190
+ # # => ["v1.0.0"]
191
+ #
192
+ # @param stdout [String] command stdout
193
+ # @return [Array<String>] names of successfully deleted tags
194
+ #
195
+ def parse_deleted_tags(stdout)
196
+ stdout.scan(DELETED_TAG_REGEX).flatten
197
+ end
198
+
199
+ # Parse error messages from stderr into a map
200
+ #
201
+ # @example
202
+ # TagParser.parse_error_messages("error: tag 'missing' not found.\n")
203
+ # # => {"missing" => "error: tag 'missing' not found."}
204
+ #
205
+ # @param stderr [String] command stderr
206
+ # @return [Hash<String, String>] map of tag name to error message
207
+ #
208
+ def parse_error_messages(stderr)
209
+ stderr.each_line.with_object({}) do |line, hash|
210
+ match = line.match(ERROR_TAG_REGEX)
211
+ hash[match[1]] = line.strip if match
212
+ end
213
+ end
214
+
215
+ # Build the TagDeleteResult from parsed data
216
+ #
217
+ # @param requested_names [Array<String>] originally requested tag names
218
+ # @param existing_tags [Hash<String, Git::TagInfo>] tags that existed before delete
219
+ # @param deleted_names [Array<String>] names confirmed deleted in stdout
220
+ # @param error_map [Hash<String, String>] map of tag name to error message
221
+ # @return [Git::TagDeleteResult] the result object
222
+ #
223
+ def build_delete_result(requested_names, existing_tags, deleted_names, error_map)
224
+ deleted = deleted_names.filter_map { |name| existing_tags[name] }
225
+
226
+ not_deleted = (requested_names - deleted_names).map do |name|
227
+ error_message = error_map[name] || "tag '#{name}' could not be deleted"
228
+ Git::TagDeleteFailure.new(name: name, error_message: error_message)
229
+ end
230
+
231
+ Git::TagDeleteResult.new(deleted: deleted, not_deleted: not_deleted)
232
+ end
233
+
234
+ # Generate error message for unexpected tag record format
235
+ #
236
+ # @param records [Array<String>] all output records
237
+ # @param record [String] the problematic record
238
+ # @param index [Integer] the record index
239
+ # @return [String] formatted error message
240
+ #
241
+ def unexpected_tag_record_error(records, record, index)
242
+ format_str = FORMAT_STRING.gsub(FIELD_DELIMITER, '<FS>').gsub(RECORD_DELIMITER, '<RS>')
243
+ <<~ERROR
244
+ Unexpected record in output from `git tag --list --format=#{format_str}`, at index #{index}
245
+
246
+ Expected #{FIELD_COUNT} fields separated by '\\x1f' (unit separator), got #{record.split(FIELD_DELIMITER, -1).length}
247
+
248
+ Full output:
249
+ #{records.join("\n ")}
250
+
251
+ Record at index #{index}:
252
+ "#{record}"
253
+ ERROR
254
+ end
255
+ end
256
+ end
257
+ end
data/lib/git/remote.rb CHANGED
@@ -1,39 +1,150 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'git/branch'
4
+ require 'git/branch_info'
5
+
3
6
  module Git
4
7
  # A remote in a Git repository
8
+ #
9
+ # Remote objects provide access to remote metadata and operations like fetch,
10
+ # merge, and remove. They should be obtained via `Git::Repository#remote`,
11
+ # not constructed directly.
12
+ #
13
+ # @example Getting a remote
14
+ # git = Git.open('.')
15
+ # remote = git.remote('origin')
16
+ # remote.fetch
17
+ #
18
+ # @api public
19
+ #
5
20
  class Remote
6
- attr_accessor :name, :url, :fetch_opts
21
+ # The name of this remote (e.g. `'origin'`)
22
+ #
23
+ # @return [String] the remote name
24
+ #
25
+ attr_accessor :name
26
+
27
+ # The URL of this remote
28
+ #
29
+ # @return [String, nil] the remote URL
30
+ #
31
+ attr_accessor :url
7
32
 
33
+ # The fetch refspec for this remote
34
+ #
35
+ # @return [String, nil] the fetch options string
36
+ #
37
+ attr_accessor :fetch_opts
38
+
39
+ # Initialize a new Remote object
40
+ #
41
+ # @param base [Git::Repository] the git repository
42
+ #
43
+ # @param name [String] the remote name (e.g. `'origin'`)
44
+ #
45
+ # @note Use `Git::Repository#remote` instead of constructing directly
46
+ #
47
+ # @api private
48
+ #
8
49
  def initialize(base, name)
9
50
  @base = base
10
- config = @base.lib.config_remote(name)
51
+ config = remote_repository.config_remote(name)
11
52
  @name = name
12
53
  @url = config['url']
13
54
  @fetch_opts = config['fetch']
14
55
  end
15
56
 
57
+ # Fetches from this remote
58
+ #
59
+ # @example Fetch from origin
60
+ # git.remote('origin').fetch
61
+ #
62
+ # @param opts [Hash] fetch options (see `Git::Repository#fetch`)
63
+ #
64
+ # @return [String] git's stdout from the fetch
65
+ #
66
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
67
+ #
16
68
  def fetch(opts = {})
17
- @base.fetch(@name, opts)
69
+ remote_repository.fetch(@name, opts)
18
70
  end
19
71
 
20
- # merge this remote locally
21
- def merge(branch = @base.current_branch)
72
+ # Merges this remote into the given (or current) local branch
73
+ #
74
+ # @example Merge origin/main into the current branch
75
+ # git.remote('origin').merge('main')
76
+ #
77
+ # @param branch [String] the local branch to merge into (defaults to current branch)
78
+ #
79
+ # @return [String] git's stdout from the merge
80
+ #
81
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
82
+ #
83
+ def merge(branch = nil)
84
+ branch ||= remote_repository.current_branch
22
85
  remote_tracking_branch = "#{@name}/#{branch}"
23
- @base.merge(remote_tracking_branch)
86
+ remote_repository.merge(remote_tracking_branch)
24
87
  end
25
88
 
26
- def branch(branch = @base.current_branch)
89
+ # Returns a {Git::Branch} object for the given branch on this remote
90
+ #
91
+ # @example Get the remote-tracking branch object
92
+ # git.remote('origin').branch('main') #=> #<Git::Branch 'origin/main'>
93
+ #
94
+ # @param branch [String] the branch name on this remote (defaults to current branch)
95
+ #
96
+ # @return [Git::Branch] a branch object representing `<remote>/<branch>`
97
+ #
98
+ def branch(branch = nil)
99
+ branch ||= remote_repository.current_branch
27
100
  remote_tracking_branch = "#{@name}/#{branch}"
28
- Git::Branch.new(@base, remote_tracking_branch)
101
+ branch_info = build_branch_info(remote_tracking_branch)
102
+ Git::Branch.new(@base, branch_info)
29
103
  end
30
104
 
105
+ # Removes this remote from the repository
106
+ #
107
+ # @example Remove the upstream remote
108
+ # git.remote('upstream').remove
109
+ #
110
+ # @return [Git::CommandLineResult] the result of `git remote remove`
111
+ #
112
+ # @raise [Git::FailedError] if git exits with a non-zero exit status
113
+ #
31
114
  def remove
32
- @base.lib.remote_remove(@name)
115
+ remote_repository.remote_remove(@name)
33
116
  end
34
117
 
118
+ # Returns the name of this remote as a string
119
+ #
120
+ # @example Get the remote name as a string
121
+ # git.remote('origin').to_s #=> 'origin'
122
+ #
123
+ # @return [String] the remote name
124
+ #
35
125
  def to_s
36
126
  @name
37
127
  end
128
+
129
+ private
130
+
131
+ # @return [Git::Repository]
132
+ #
133
+ # @api private
134
+ #
135
+ def remote_repository
136
+ @base
137
+ end
138
+
139
+ def build_branch_info(refname)
140
+ Git::BranchInfo.new(
141
+ refname: refname,
142
+ target_oid: nil,
143
+ current: false,
144
+ worktree: false,
145
+ symref: nil,
146
+ upstream: nil
147
+ )
148
+ end
38
149
  end
39
150
  end