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
data/lib/git/object.rb CHANGED
@@ -24,24 +24,55 @@ module Git
24
24
  end
25
25
 
26
26
  def sha
27
- @sha ||= @base.lib.rev_parse(@objectish)
27
+ @sha ||= object_repository.rev_parse(@objectish)
28
28
  end
29
29
 
30
30
  def size
31
- @size ||= @base.lib.cat_file_size(@objectish)
31
+ @size ||= object_repository.cat_file_size(@objectish)
32
32
  end
33
33
 
34
- # Get the object's contents.
35
- # If no block is given, the contents are cached in memory and returned as a string.
36
- # If a block is given, it yields an IO object (via IO::popen) which could be used to
37
- # read a large file in chunks.
34
+ # Returns the raw content of this git object or streams it into a temporary file
35
+ #
36
+ # Without a block, the full content is buffered in memory and cached, then
37
+ # returned as a `String`. With a block, git output is streamed directly to a
38
+ # temporary file on disk — suitable for large objects.
39
+ #
40
+ # @api public
41
+ #
42
+ # @overload contents
43
+ # Returns the cached content as a string.
44
+ #
45
+ # @return [String] the raw content of the object, cached after first call
46
+ #
47
+ # @raise [Git::FailedError] if the object does not exist or the command fails
48
+ #
49
+ # @example Get the contents of a blob
50
+ # git.object('HEAD:README.md').contents # => "This is a README file\n"
51
+ #
52
+ # @overload contents(&block)
53
+ # Streams the content to a temporary file and yields it.
54
+ #
55
+ # Git output is written directly to a file without buffering in
56
+ # memory. Use this form for large blobs to avoid memory pressure.
57
+ #
58
+ # @yield [file] the temporary file, positioned at the start of the content
59
+ #
60
+ # @yieldparam file [File] readable `IO` object positioned at the beginning
61
+ #
62
+ # @yieldreturn [Object] the value to return from this method
63
+ #
64
+ # @return [Object] the value returned by the block
65
+ #
66
+ # @raise [Git::FailedError] if the object does not exist or the command fails
67
+ #
68
+ # @example Read a large blob without loading it into memory
69
+ # git.object('HEAD:large_file.bin').contents { |f| upload(f) }
38
70
  #
39
- # Use this for large files so that they are not held in memory.
40
71
  def contents(&)
41
72
  if block_given?
42
- @base.lib.cat_file_contents(@objectish, &)
73
+ object_repository.cat_file_contents(@objectish, &)
43
74
  else
44
- @contents ||= @base.lib.cat_file_contents(@objectish)
75
+ @contents ||= object_repository.cat_file_contents(@objectish)
45
76
  end
46
77
  end
47
78
 
@@ -54,8 +85,7 @@ module Git
54
85
  end
55
86
 
56
87
  def grep(string, path_limiter = nil, opts = {})
57
- opts = { object: sha, path_limiter: path_limiter }.merge(opts)
58
- @base.lib.grep(string, opts)
88
+ object_repository.grep(string, path_limiter, opts.merge(object: sha))
59
89
  end
60
90
 
61
91
  def diff(objectish)
@@ -66,9 +96,23 @@ module Git
66
96
  Git::Log.new(@base, count).object(@objectish)
67
97
  end
68
98
 
69
- # creates an archive of this object (tree)
99
+ # Creates an archive of this object and writes it to a file
100
+ #
101
+ # @api public
102
+ #
103
+ # @param file [String, nil] destination file path; a temp file is created if `nil`
104
+ #
105
+ # @param opts [Hash] archive options (see {Git::Repository#archive})
106
+ #
107
+ # @return [String] the path to the written archive file
108
+ #
109
+ # @raise [Git::FailedError] if `git archive` fails
110
+ #
111
+ # @example Archive a tree to a zip file
112
+ # git.object('v1.0').archive('/tmp/release.zip', format: 'zip')
113
+ #
70
114
  def archive(file = nil, opts = {})
71
- @base.lib.archive(@objectish, file, opts)
115
+ object_repository.archive(@objectish, file, opts)
72
116
  end
73
117
 
74
118
  def tree? = false
@@ -78,6 +122,14 @@ module Git
78
122
  def commit? = false
79
123
 
80
124
  def tag? = false
125
+
126
+ private
127
+
128
+ # @return [Git::Repository]
129
+ #
130
+ def object_repository
131
+ @base
132
+ end
81
133
  end
82
134
 
83
135
  # A Git blob object
@@ -117,11 +169,11 @@ module Git
117
169
  alias subdirectories trees
118
170
 
119
171
  def full_tree
120
- @base.lib.full_tree(@objectish)
172
+ object_repository.full_tree(@objectish)
121
173
  end
122
174
 
123
175
  def depth
124
- @base.lib.tree_depth(@objectish)
176
+ object_repository.tree_depth(@objectish)
125
177
  end
126
178
 
127
179
  def tree?
@@ -135,7 +187,7 @@ module Git
135
187
  @trees = {}
136
188
  @blobs = {}
137
189
 
138
- data = @base.lib.ls_tree(@objectish)
190
+ data = object_repository.ls_tree(@objectish)
139
191
 
140
192
  data['tree'].each do |key, tree|
141
193
  @trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode])
@@ -169,7 +221,7 @@ module Git
169
221
  end
170
222
 
171
223
  def name
172
- @base.lib.name_rev(sha)
224
+ object_repository.name_rev(sha)
173
225
  end
174
226
 
175
227
  def gtree
@@ -239,7 +291,7 @@ module Git
239
291
  def check_commit
240
292
  return if @tree
241
293
 
242
- data = @base.lib.cat_file_commit(@objectish)
294
+ data = object_repository.cat_file_commit(@objectish)
243
295
  from_data(data)
244
296
  end
245
297
  end
@@ -257,18 +309,23 @@ module Git
257
309
  attr_accessor :name
258
310
 
259
311
  # @overload initialize(base, name)
260
- # @param base [Git::Base] The Git base object
261
- # @param name [String] The name of the tag
312
+ #
313
+ # @param base [Git::Repository] the git repository
314
+ #
315
+ # @param name [String] the name of the tag
262
316
  #
263
317
  # @overload initialize(base, sha, name)
264
- # @param base [Git::Base] The Git base object
265
- # @param sha [String] The SHA of the tag object
266
- # @param name [String] The name of the tag
318
+ #
319
+ # @param base [Git::Repository] the git repository
320
+ #
321
+ # @param sha [String] the SHA of the tag object
322
+ #
323
+ # @param name [String] the name of the tag
267
324
  #
268
325
  def initialize(base, sha, name = nil)
269
326
  if name.nil?
270
327
  name = sha
271
- sha = base.lib.tag_sha(name)
328
+ sha = base.tag_sha(name)
272
329
  raise Git::UnexpectedResultError, "Tag '#{name}' does not exist." if sha == ''
273
330
  end
274
331
 
@@ -280,7 +337,7 @@ module Git
280
337
  end
281
338
 
282
339
  def annotated?
283
- @annotated = @annotated.nil? ? (@base.lib.cat_file_type(name) == 'tag') : @annotated
340
+ @annotated = @annotated.nil? ? (object_repository.cat_file_type(name) == 'tag') : @annotated
284
341
  end
285
342
 
286
343
  def message
@@ -303,7 +360,7 @@ module Git
303
360
  return if @loaded
304
361
 
305
362
  if annotated?
306
- tdata = @base.lib.cat_file_tag(@name)
363
+ tdata = object_repository.cat_file_tag(@name)
307
364
  @message = tdata['message'].chomp
308
365
  @tagger = Git::Author.new(tdata['tagger'])
309
366
  else
@@ -319,7 +376,7 @@ module Git
319
376
  def self.new(base, objectish, type = nil, is_tag = false) # rubocop:disable Style/OptionalBooleanParameter
320
377
  return new_tag(base, objectish) if is_tag
321
378
 
322
- type ||= base.lib.cat_file_type(objectish)
379
+ type ||= object_repository_for(base).cat_file_type(objectish)
323
380
  # TODO: why not handle tag case here too?
324
381
  klass =
325
382
  case type
@@ -334,5 +391,11 @@ module Git
334
391
  Git::Deprecation.warn('Git::Object.new with is_tag argument is deprecated. Use Git::Object::Tag.new instead.')
335
392
  Git::Object::Tag.new(base, objectish)
336
393
  end
394
+
395
+ # @return [Git::Repository]
396
+ #
397
+ private_class_method def self.object_repository_for(base)
398
+ base
399
+ end
337
400
  end
338
401
  end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/branch_info'
4
+ require 'git/branch_delete_result'
5
+ require 'git/branch_delete_failure'
6
+
7
+ module Git
8
+ module Parsers
9
+ # Parser for git branch command output
10
+ #
11
+ # Handles parsing of `git branch --list` and `git branch --delete` output
12
+ # into structured data objects.
13
+ #
14
+ # ## Design Note: Namespace Organization
15
+ #
16
+ # This parser creates and returns {Git::BranchInfo} and {Git::BranchDeleteResult}
17
+ # objects, which live at the top-level `Git::` namespace rather than within
18
+ # `Git::Parsers::`. This is intentional:
19
+ #
20
+ # - **Parsers are infrastructure** - marked `@api private`, users shouldn't
21
+ # interact with them directly
22
+ # - **Info/Result classes are public API** - returned by commands and used
23
+ # throughout the codebase
24
+ # - **Info classes are domain entities** - represent core git concepts
25
+ # (branches as data)
26
+ # - **Result classes are operation outcomes** - represent command results,
27
+ # not parsing details
28
+ #
29
+ # Keeping Info/Result 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 Branch
35
+ # Format string for git branch --format
36
+ #
37
+ # Fields (pipe-delimited):
38
+ # 1. refname - full ref name (e.g., refs/heads/main, refs/remotes/origin/main)
39
+ # 2. objectname - full SHA of the commit the branch points to
40
+ # 3. HEAD - '*' if current branch, empty otherwise
41
+ # 4. worktreepath - path if checked out in another worktree, empty otherwise
42
+ # 5. symref - target ref if symbolic reference, empty otherwise
43
+ # 6. upstream - full upstream ref (e.g., refs/remotes/origin/main), empty if none
44
+ #
45
+ FORMAT_STRING = '%(refname)|%(objectname)|%(HEAD)|%(worktreepath)|%(symref)|%(upstream)'
46
+
47
+ # Delimiter used in format output
48
+ FIELD_DELIMITER = '|'
49
+
50
+ # Regex to parse successful deletion lines from stdout
51
+ # Matches: Deleted branch branchname (was abc123).
52
+ # Matches: Deleted remote-tracking branch origin/branchname (was abc123).
53
+ # Uses non-greedy match to capture branch names containing spaces
54
+ DELETED_BRANCH_REGEX = /^Deleted (?:remote-tracking )?branch (.+?) \(was/
55
+
56
+ # Regex to parse error messages from stderr
57
+ # Matches: error: branch 'branchname' not found.
58
+ ERROR_BRANCH_REGEX = /^error: branch '([^']+)'(.*)$/
59
+
60
+ module_function
61
+
62
+ # Parse git branch --list output into BranchInfo objects
63
+ #
64
+ # @example
65
+ # Git::Parsers::Branch.parse_list("refs/heads/main|abc1234|*|||\nrefs/heads/feature|def5678||||\n")
66
+ # # => [#<data Git::BranchInfo refname="main", ...>, #<data Git::BranchInfo refname="feature", ...>]
67
+ #
68
+ # @param stdout [String] output from git branch --list --format=...
69
+ #
70
+ # @return [Array<Git::BranchInfo>] parsed branch information
71
+ #
72
+ def parse_list(stdout)
73
+ stdout.split("\n").filter_map { |line| parse_branch_line(line) }
74
+ end
75
+
76
+ # Parse a single formatted branch line
77
+ #
78
+ # @param line [String] the line to parse (pipe-delimited fields)
79
+ # @return [Git::BranchInfo, nil] branch info object, or nil if line should be skipped
80
+ #
81
+ def parse_branch_line(line)
82
+ fields = line.split(FIELD_DELIMITER, 6)
83
+
84
+ return nil if non_branch_entry?(fields[0])
85
+
86
+ build_branch_info(fields)
87
+ end
88
+
89
+ # Build a BranchInfo from parsed fields
90
+ #
91
+ # @param fields [Array<String>] the parsed fields: [refname, objectname, head, worktreepath, symref, upstream]
92
+ # @return [Git::BranchInfo] the branch info object
93
+ #
94
+ def build_branch_info(fields)
95
+ raw_refname, objectname, head, worktreepath, symref, upstream = fields
96
+ current = head == '*'
97
+
98
+ Git::BranchInfo.new(
99
+ refname: normalize_refname(raw_refname),
100
+ target_oid: presence(objectname),
101
+ current: current,
102
+ worktree: in_other_worktree?(worktreepath, current),
103
+ symref: presence(symref),
104
+ upstream: build_upstream_info(upstream)
105
+ )
106
+ end
107
+
108
+ # Check if the refname represents a detached HEAD state or non-branch entry
109
+ #
110
+ # Git outputs special entries for detached HEAD and non-branch states:
111
+ # - "(HEAD detached at <ref>)" when in detached HEAD state
112
+ # - "(not a branch)" for non-branch entries
113
+ #
114
+ # @param refname [String] the refname to check
115
+ # @return [Boolean] true if this is a non-branch entry
116
+ #
117
+ def non_branch_entry?(refname)
118
+ refname.match?(/^\(HEAD detached/) || refname.match?(/^\(not a branch\)/)
119
+ end
120
+
121
+ # Normalize a full refname to the expected format
122
+ #
123
+ # Converts:
124
+ # - refs/heads/main -> main
125
+ # - refs/remotes/origin/main -> remotes/origin/main
126
+ #
127
+ # @param refname [String] the full refname from git
128
+ # @return [String] normalized refname
129
+ #
130
+ def normalize_refname(refname)
131
+ refname.sub(%r{^refs/heads/}, '').sub(%r{^refs/}, '')
132
+ end
133
+
134
+ # Check if the branch is checked out in another worktree
135
+ #
136
+ # worktree is true when the branch is checked out in ANOTHER worktree
137
+ # (worktreepath is non-empty AND it's not the current branch)
138
+ #
139
+ # @param worktreepath [String, nil] the worktree path from git output
140
+ # @param current [Boolean] whether this is the current branch
141
+ # @return [Boolean] true if checked out in another worktree
142
+ #
143
+ def in_other_worktree?(worktreepath, current)
144
+ has_worktree = !worktreepath.nil? && !worktreepath.empty?
145
+ has_worktree && !current
146
+ end
147
+
148
+ # Build upstream BranchInfo from upstream refname
149
+ #
150
+ # @param upstream_ref [String, nil] the upstream ref (e.g., 'refs/remotes/origin/main')
151
+ # @return [Git::BranchInfo, nil] upstream branch info or nil
152
+ #
153
+ def build_upstream_info(upstream_ref)
154
+ return nil if upstream_ref.nil? || upstream_ref.empty?
155
+
156
+ Git::BranchInfo.new(
157
+ refname: normalize_refname(upstream_ref),
158
+ target_oid: nil, # We don't have upstream's OID from this format
159
+ current: false,
160
+ worktree: false,
161
+ symref: nil,
162
+ upstream: nil # Upstream branches don't have their own upstream in this context
163
+ )
164
+ end
165
+
166
+ # Return value if non-empty, nil otherwise
167
+ #
168
+ # @param value [String, nil] the value to check
169
+ # @return [String, nil] the value or nil
170
+ #
171
+ def presence(value)
172
+ value.nil? || value.empty? ? nil : value
173
+ end
174
+
175
+ # Parse deleted branch names from stdout
176
+ #
177
+ # @example
178
+ # BranchParser.parse_deleted_branches("Deleted branch feature (was abc123).\n")
179
+ # # => ["feature"]
180
+ #
181
+ # @param stdout [String] command stdout
182
+ # @return [Array<String>] names of successfully deleted branches
183
+ #
184
+ def parse_deleted_branches(stdout)
185
+ stdout.scan(DELETED_BRANCH_REGEX).flatten
186
+ end
187
+
188
+ # Parse error messages from stderr into a map
189
+ #
190
+ # @example
191
+ # BranchParser.parse_error_messages("error: branch 'missing' not found.\n")
192
+ # # => {"missing" => "error: branch 'missing' not found."}
193
+ #
194
+ # @param stderr [String] command stderr
195
+ # @return [Hash<String, String>] map of branch name to error message
196
+ #
197
+ def parse_error_messages(stderr)
198
+ stderr.each_line.with_object({}) do |line, hash|
199
+ match = line.match(ERROR_BRANCH_REGEX)
200
+ hash[match[1]] = line.strip if match
201
+ end
202
+ end
203
+
204
+ # Build the BranchDeleteResult from parsed data
205
+ #
206
+ # @param requested_names [Array<String>] originally requested branch names
207
+ # @param existing_branches [Hash<String, Git::BranchInfo>] branches that existed before delete
208
+ # @param deleted_names [Array<String>] names confirmed deleted in stdout
209
+ # @param error_map [Hash<String, String>] map of branch name to error message
210
+ # @return [Git::BranchDeleteResult] the result object
211
+ #
212
+ def build_delete_result(requested_names, existing_branches, deleted_names, error_map)
213
+ deleted = deleted_names.filter_map { |name| existing_branches[name] }
214
+
215
+ not_deleted = (requested_names - deleted_names).map do |name|
216
+ error_message = error_map[name] || "branch '#{name}' could not be deleted"
217
+ Git::BranchDeleteFailure.new(name: name, error_message: error_message)
218
+ end
219
+
220
+ Git::BranchDeleteResult.new(deleted: deleted, not_deleted: not_deleted)
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module Parsers
5
+ # Parser for `git cat-file` commit and tag output
6
+ #
7
+ # Provides class methods that transform raw `git cat-file` output lines into
8
+ # structured Hash objects consumed by the `Git::Repository::ObjectOperations`
9
+ # facade.
10
+ #
11
+ # @api private
12
+ #
13
+ module CatFile
14
+ module_function
15
+
16
+ # Matches a single `git cat-file` header line
17
+ #
18
+ # @api private
19
+ #
20
+ CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
21
+
22
+ # Parse `git cat-file commit` output into a structured Hash
23
+ #
24
+ # @param lines [Array<String>] mutable cat-file output lines, consumed
25
+ # in place during header parsing
26
+ #
27
+ # @param sha [String] the object name passed by the caller
28
+ #
29
+ # @return [Hash] commit data hash with string keys
30
+ #
31
+ # @api private
32
+ #
33
+ def parse_commit(lines, sha)
34
+ headers = parse_commit_headers(lines)
35
+ message = "#{lines.join("\n")}\n"
36
+ { 'sha' => sha, 'message' => message }.merge(headers)
37
+ end
38
+
39
+ # Parse `git cat-file tag` output into a structured Hash
40
+ #
41
+ # @param lines [Array<String>] mutable cat-file output lines, consumed
42
+ # in place during header parsing; remaining lines become the message
43
+ #
44
+ # @param name [String] the tag name passed by the caller
45
+ #
46
+ # @return [Hash] tag data hash with string keys
47
+ #
48
+ # @api private
49
+ #
50
+ def parse_tag(lines, name)
51
+ hsh = { 'name' => name }
52
+ each_header(lines) { |key, value| hsh[key] = value }
53
+ hsh['message'] = "#{lines.join("\n")}\n"
54
+ hsh
55
+ end
56
+
57
+ # Extracts and returns commit headers from the front of `lines`
58
+ #
59
+ # Mutates `lines` in place, consuming header lines and the blank
60
+ # separator line. After the call `lines` contains only message lines.
61
+ #
62
+ # @param lines [Array<String>] mutable cat-file output lines
63
+ #
64
+ # @return [Hash] parsed header key/value pairs; `parent` is always
65
+ # an Array
66
+ #
67
+ # @api private
68
+ #
69
+ def parse_commit_headers(lines)
70
+ headers = { 'parent' => [] }
71
+ each_header(lines) do |key, value|
72
+ if key == 'parent'
73
+ headers['parent'] << value
74
+ else
75
+ headers[key] = value
76
+ end
77
+ end
78
+ headers
79
+ end
80
+
81
+ # Yields parsed header key/value pairs from `git cat-file` output lines
82
+ #
83
+ # Consumes header lines from the front of `lines` until a blank line is
84
+ # encountered. Continuation lines that begin with a space are folded
85
+ # into the previous header value using newline separators.
86
+ #
87
+ # @param lines [Array<String>] mutable output lines from a cat-file response
88
+ #
89
+ # @yield [key, value] each parsed header pair
90
+ #
91
+ # @yieldparam key [String] header field name
92
+ #
93
+ # @yieldparam value [String] unfolded header value text
94
+ #
95
+ # @yieldreturn [void]
96
+ #
97
+ # @return [void]
98
+ #
99
+ # @api private
100
+ #
101
+ def each_header(lines)
102
+ while (line = lines.shift) && (match = CAT_FILE_HEADER_LINE.match(line))
103
+ key = match[:key]
104
+ value_lines = [match[:value]]
105
+ value_lines << lines.shift.lstrip while lines.first&.start_with?(' ')
106
+ yield key, value_lines.join("\n")
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/config_entry_info'
4
+
5
+ module Git
6
+ module Parsers
7
+ # Parser for `git config` output
8
+ #
9
+ # Each public method corresponds to a different `git config` sub-command
10
+ # variant and handles the specific output format that variant produces.
11
+ # Most variants are called with `--show-scope --show-origin --null`;
12
+ # `parse_urlmatch` handles the `--get-urlmatch` format where `--show-origin`
13
+ # is unsupported and therefore absent.
14
+ #
15
+ # @api private
16
+ #
17
+ module ConfigEntry
18
+ module_function
19
+
20
+ # Parse `git config --get --show-scope --show-origin --null` output
21
+ #
22
+ # Output format (per entry): `scope\0origin\0value\0`
23
+ #
24
+ # The key name is absent from `--get` output and must be supplied by the
25
+ # caller.
26
+ #
27
+ # @param key [String] the config key name that was queried
28
+ #
29
+ # @param output [String] raw stdout from the command
30
+ #
31
+ # @return [Git::ConfigEntryInfo, nil] the parsed entry, or `nil` when not found
32
+ #
33
+ def parse_get(key, output)
34
+ return nil if output.empty?
35
+
36
+ scope, origin, value = output.split("\0", -1)
37
+ Git::ConfigEntryInfo.new(scope: scope, origin: origin, key: key, value: value)
38
+ end
39
+
40
+ # Parse `git config --get-all --show-scope --show-origin --null` output
41
+ #
42
+ # Output format (per entry): `scope\0origin\0value\0`
43
+ #
44
+ # The key name is absent from `--get-all` output and must be supplied by
45
+ # the caller. Multiple entries appear back-to-back in the same string.
46
+ #
47
+ # @param key [String] the config key name that was queried
48
+ #
49
+ # @param output [String] raw stdout from the command
50
+ #
51
+ # @return [Array<Git::ConfigEntryInfo>] the parsed entries
52
+ #
53
+ def parse_get_all(key, output)
54
+ return [] if output.empty?
55
+
56
+ tokens = output.split("\0", -1)
57
+ tokens.pop if tokens.last == ''
58
+ tokens.each_slice(3).map do |scope, origin, value|
59
+ Git::ConfigEntryInfo.new(scope: scope, origin: origin, key: key, value: value)
60
+ end
61
+ end
62
+
63
+ # Parse `git config --list --show-scope --show-origin --null` output
64
+ #
65
+ # Also handles `--get-regexp` output, which shares the same format.
66
+ #
67
+ # Output format (per entry): `scope\0origin\0key\nvalue\0`
68
+ #
69
+ # Multiple entries appear back-to-back in the same string.
70
+ #
71
+ # @param output [String] raw stdout from the command
72
+ #
73
+ # @return [Array<Git::ConfigEntryInfo>] the parsed entries
74
+ #
75
+ def parse_list(output)
76
+ return [] if output.empty?
77
+
78
+ tokens = output.split("\0", -1)
79
+ tokens.pop if tokens.last == ''
80
+ tokens.each_slice(3).map do |scope, origin, key_value|
81
+ key, value = key_value.split("\n", 2)
82
+ Git::ConfigEntryInfo.new(scope: scope, origin: origin, key: key, value: value || '')
83
+ end
84
+ end
85
+
86
+ # Parse `git config --get-urlmatch --show-scope --null` output
87
+ #
88
+ # Handles the two-field-per-entry format produced by `--get-urlmatch` when
89
+ # `--show-scope` is used but `--show-origin` is not (git does not support
90
+ # `--show-origin` with `--get-urlmatch`).
91
+ #
92
+ # Output format (per entry): `scope\0key\nvalue\0`
93
+ #
94
+ # @param output [String] raw stdout from the command
95
+ #
96
+ # @return [Array<Git::ConfigEntryInfo>] the parsed entries; `origin` is `nil` for each
97
+ #
98
+ def parse_urlmatch(output)
99
+ return [] if output.empty?
100
+
101
+ tokens = output.split("\0", -1)
102
+ tokens.pop if tokens.last == ''
103
+ tokens.each_slice(2).map do |scope, key_value|
104
+ key, value = key_value.split("\n", 2)
105
+ Git::ConfigEntryInfo.new(scope: scope, origin: nil, key: key, value: value || '')
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end