git 4.3.2 → 5.0.0.beta.1

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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.github/copilot-instructions.md +67 -2705
  3. data/.github/pull_request_template.md +3 -1
  4. data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
  5. data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
  6. data/.github/skills/command-implementation/REFERENCE.md +993 -0
  7. data/.github/skills/command-implementation/SKILL.md +229 -0
  8. data/.github/skills/command-test-conventions/SKILL.md +660 -0
  9. data/.github/skills/command-yard-documentation/SKILL.md +426 -0
  10. data/.github/skills/dependency-management/SKILL.md +72 -0
  11. data/.github/skills/development-workflow/SKILL.md +506 -0
  12. data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
  13. data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
  14. data/.github/skills/facade-implementation/REFERENCE.md +840 -0
  15. data/.github/skills/facade-implementation/SKILL.md +260 -0
  16. data/.github/skills/facade-test-conventions/SKILL.md +380 -0
  17. data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
  18. data/.github/skills/make-skill-template/SKILL.md +176 -0
  19. data/.github/skills/pr-readiness-review/SKILL.md +185 -0
  20. data/.github/skills/project-context/SKILL.md +313 -0
  21. data/.github/skills/pull-request-review/SKILL.md +168 -0
  22. data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
  23. data/.github/skills/release-management/SKILL.md +125 -0
  24. data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
  25. data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
  26. data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
  27. data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
  28. data/.github/skills/reviewing-skills/SKILL.md +189 -0
  29. data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
  30. data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
  31. data/.github/skills/test-debugging/SKILL.md +160 -0
  32. data/.github/skills/yard-documentation/SKILL.md +793 -0
  33. data/.github/workflows/continuous_integration.yml +3 -2
  34. data/.github/workflows/enforce_conventional_commits.yml +1 -1
  35. data/.github/workflows/experimental_continuous_integration.yml +2 -2
  36. data/.github/workflows/release.yml +3 -4
  37. data/.gitignore +8 -0
  38. data/.husky/pre-commit +13 -0
  39. data/.release-please-manifest.json +1 -1
  40. data/.rspec +3 -0
  41. data/.rubocop.yml +7 -3
  42. data/.rubocop_todo.yml +23 -5
  43. data/.yardopts +1 -0
  44. data/CHANGELOG.md +0 -40
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -5
  47. data/Rakefile +61 -9
  48. data/commitlint.test +4 -0
  49. data/git.gemspec +14 -8
  50. data/lib/git/args_builder.rb +0 -8
  51. data/lib/git/base.rb +486 -410
  52. data/lib/git/branch.rb +380 -43
  53. data/lib/git/branch_delete_failure.rb +31 -0
  54. data/lib/git/branch_delete_result.rb +63 -0
  55. data/lib/git/branch_info.rb +178 -0
  56. data/lib/git/branches.rb +130 -24
  57. data/lib/git/command_line/base.rb +245 -0
  58. data/lib/git/command_line/capturing.rb +249 -0
  59. data/lib/git/command_line/result.rb +96 -0
  60. data/lib/git/command_line/streaming.rb +194 -0
  61. data/lib/git/command_line.rb +43 -322
  62. data/lib/git/command_line_result.rb +4 -88
  63. data/lib/git/commands/add.rb +131 -0
  64. data/lib/git/commands/am/abort.rb +43 -0
  65. data/lib/git/commands/am/apply.rb +252 -0
  66. data/lib/git/commands/am/continue.rb +43 -0
  67. data/lib/git/commands/am/quit.rb +43 -0
  68. data/lib/git/commands/am/retry.rb +47 -0
  69. data/lib/git/commands/am/show_current_patch.rb +64 -0
  70. data/lib/git/commands/am/skip.rb +42 -0
  71. data/lib/git/commands/am.rb +33 -0
  72. data/lib/git/commands/apply.rb +237 -0
  73. data/lib/git/commands/archive/list_formats.rb +46 -0
  74. data/lib/git/commands/archive.rb +140 -0
  75. data/lib/git/commands/arguments.rb +3510 -0
  76. data/lib/git/commands/base.rb +403 -0
  77. data/lib/git/commands/branch/copy.rb +94 -0
  78. data/lib/git/commands/branch/create.rb +173 -0
  79. data/lib/git/commands/branch/delete.rb +80 -0
  80. data/lib/git/commands/branch/list.rb +162 -0
  81. data/lib/git/commands/branch/move.rb +94 -0
  82. data/lib/git/commands/branch/set_upstream.rb +86 -0
  83. data/lib/git/commands/branch/show_current.rb +49 -0
  84. data/lib/git/commands/branch/unset_upstream.rb +57 -0
  85. data/lib/git/commands/branch.rb +34 -0
  86. data/lib/git/commands/cat_file/batch.rb +364 -0
  87. data/lib/git/commands/cat_file/filtered.rb +105 -0
  88. data/lib/git/commands/cat_file/raw.rb +210 -0
  89. data/lib/git/commands/cat_file.rb +49 -0
  90. data/lib/git/commands/checkout/branch.rb +151 -0
  91. data/lib/git/commands/checkout/files.rb +115 -0
  92. data/lib/git/commands/checkout.rb +38 -0
  93. data/lib/git/commands/checkout_index.rb +105 -0
  94. data/lib/git/commands/clean.rb +100 -0
  95. data/lib/git/commands/clone.rb +240 -0
  96. data/lib/git/commands/commit.rb +272 -0
  97. data/lib/git/commands/commit_tree.rb +100 -0
  98. data/lib/git/commands/config_option_syntax/add.rb +83 -0
  99. data/lib/git/commands/config_option_syntax/get.rb +117 -0
  100. data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
  101. data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
  102. data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
  103. data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
  104. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
  105. data/lib/git/commands/config_option_syntax/list.rb +107 -0
  106. data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
  107. data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
  108. data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
  109. data/lib/git/commands/config_option_syntax/set.rb +114 -0
  110. data/lib/git/commands/config_option_syntax/unset.rb +89 -0
  111. data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
  112. data/lib/git/commands/config_option_syntax.rb +56 -0
  113. data/lib/git/commands/describe.rb +155 -0
  114. data/lib/git/commands/diff.rb +656 -0
  115. data/lib/git/commands/diff_files.rb +518 -0
  116. data/lib/git/commands/diff_index.rb +496 -0
  117. data/lib/git/commands/fetch.rb +352 -0
  118. data/lib/git/commands/fsck.rb +136 -0
  119. data/lib/git/commands/gc.rb +132 -0
  120. data/lib/git/commands/grep.rb +338 -0
  121. data/lib/git/commands/init.rb +99 -0
  122. data/lib/git/commands/log.rb +632 -0
  123. data/lib/git/commands/ls_files.rb +191 -0
  124. data/lib/git/commands/ls_remote.rb +155 -0
  125. data/lib/git/commands/ls_tree.rb +131 -0
  126. data/lib/git/commands/maintenance/register.rb +75 -0
  127. data/lib/git/commands/maintenance/run.rb +104 -0
  128. data/lib/git/commands/maintenance/start.rb +66 -0
  129. data/lib/git/commands/maintenance/stop.rb +55 -0
  130. data/lib/git/commands/maintenance/unregister.rb +79 -0
  131. data/lib/git/commands/maintenance.rb +31 -0
  132. data/lib/git/commands/merge/abort.rb +44 -0
  133. data/lib/git/commands/merge/continue.rb +44 -0
  134. data/lib/git/commands/merge/quit.rb +46 -0
  135. data/lib/git/commands/merge/start.rb +245 -0
  136. data/lib/git/commands/merge.rb +28 -0
  137. data/lib/git/commands/merge_base.rb +86 -0
  138. data/lib/git/commands/mv.rb +77 -0
  139. data/lib/git/commands/name_rev.rb +114 -0
  140. data/lib/git/commands/pull.rb +377 -0
  141. data/lib/git/commands/push.rb +246 -0
  142. data/lib/git/commands/read_tree.rb +149 -0
  143. data/lib/git/commands/remote/add.rb +91 -0
  144. data/lib/git/commands/remote/get_url.rb +66 -0
  145. data/lib/git/commands/remote/list.rb +54 -0
  146. data/lib/git/commands/remote/prune.rb +61 -0
  147. data/lib/git/commands/remote/remove.rb +52 -0
  148. data/lib/git/commands/remote/rename.rb +69 -0
  149. data/lib/git/commands/remote/set_branches.rb +63 -0
  150. data/lib/git/commands/remote/set_head.rb +82 -0
  151. data/lib/git/commands/remote/set_url.rb +71 -0
  152. data/lib/git/commands/remote/set_url_add.rb +61 -0
  153. data/lib/git/commands/remote/set_url_delete.rb +64 -0
  154. data/lib/git/commands/remote/show.rb +71 -0
  155. data/lib/git/commands/remote/update.rb +72 -0
  156. data/lib/git/commands/remote.rb +42 -0
  157. data/lib/git/commands/repack.rb +277 -0
  158. data/lib/git/commands/reset.rb +147 -0
  159. data/lib/git/commands/rev_parse.rb +297 -0
  160. data/lib/git/commands/revert/abort.rb +45 -0
  161. data/lib/git/commands/revert/continue.rb +57 -0
  162. data/lib/git/commands/revert/quit.rb +47 -0
  163. data/lib/git/commands/revert/skip.rb +44 -0
  164. data/lib/git/commands/revert/start.rb +153 -0
  165. data/lib/git/commands/revert.rb +29 -0
  166. data/lib/git/commands/rm.rb +114 -0
  167. data/lib/git/commands/show.rb +632 -0
  168. data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
  169. data/lib/git/commands/show_ref/exists.rb +78 -0
  170. data/lib/git/commands/show_ref/list.rb +145 -0
  171. data/lib/git/commands/show_ref/verify.rb +120 -0
  172. data/lib/git/commands/show_ref.rb +42 -0
  173. data/lib/git/commands/stash/apply.rb +75 -0
  174. data/lib/git/commands/stash/branch.rb +65 -0
  175. data/lib/git/commands/stash/clear.rb +41 -0
  176. data/lib/git/commands/stash/create.rb +58 -0
  177. data/lib/git/commands/stash/drop.rb +67 -0
  178. data/lib/git/commands/stash/list.rb +39 -0
  179. data/lib/git/commands/stash/pop.rb +78 -0
  180. data/lib/git/commands/stash/push.rb +103 -0
  181. data/lib/git/commands/stash/show.rb +149 -0
  182. data/lib/git/commands/stash/store.rb +63 -0
  183. data/lib/git/commands/stash.rb +38 -0
  184. data/lib/git/commands/status.rb +169 -0
  185. data/lib/git/commands/symbolic_ref/delete.rb +68 -0
  186. data/lib/git/commands/symbolic_ref/read.rb +95 -0
  187. data/lib/git/commands/symbolic_ref/update.rb +76 -0
  188. data/lib/git/commands/symbolic_ref.rb +38 -0
  189. data/lib/git/commands/tag/create.rb +139 -0
  190. data/lib/git/commands/tag/delete.rb +55 -0
  191. data/lib/git/commands/tag/list.rb +143 -0
  192. data/lib/git/commands/tag/verify.rb +71 -0
  193. data/lib/git/commands/tag.rb +26 -0
  194. data/lib/git/commands/update_ref/batch.rb +140 -0
  195. data/lib/git/commands/update_ref/delete.rb +92 -0
  196. data/lib/git/commands/update_ref/update.rb +106 -0
  197. data/lib/git/commands/update_ref.rb +42 -0
  198. data/lib/git/commands/version.rb +52 -0
  199. data/lib/git/commands/worktree/add.rb +140 -0
  200. data/lib/git/commands/worktree/list.rb +64 -0
  201. data/lib/git/commands/worktree/lock.rb +58 -0
  202. data/lib/git/commands/worktree/management_base.rb +51 -0
  203. data/lib/git/commands/worktree/move.rb +66 -0
  204. data/lib/git/commands/worktree/prune.rb +67 -0
  205. data/lib/git/commands/worktree/remove.rb +63 -0
  206. data/lib/git/commands/worktree/repair.rb +76 -0
  207. data/lib/git/commands/worktree/unlock.rb +47 -0
  208. data/lib/git/commands/worktree.rb +43 -0
  209. data/lib/git/commands/write_tree.rb +68 -0
  210. data/lib/git/commands.rb +89 -0
  211. data/lib/git/detached_head_info.rb +54 -0
  212. data/lib/git/diff.rb +297 -7
  213. data/lib/git/diff_file_numstat_info.rb +29 -0
  214. data/lib/git/diff_file_patch_info.rb +134 -0
  215. data/lib/git/diff_file_raw_info.rb +127 -0
  216. data/lib/git/diff_info.rb +169 -0
  217. data/lib/git/diff_path_status.rb +78 -19
  218. data/lib/git/diff_result.rb +32 -0
  219. data/lib/git/diff_stats.rb +59 -14
  220. data/lib/git/dirstat_info.rb +86 -0
  221. data/lib/git/errors.rb +65 -2
  222. data/lib/git/execution_context/global.rb +56 -0
  223. data/lib/git/execution_context/repository.rb +147 -0
  224. data/lib/git/execution_context.rb +482 -0
  225. data/lib/git/file_ref.rb +74 -0
  226. data/lib/git/fsck_object.rb +9 -9
  227. data/lib/git/fsck_result.rb +1 -1
  228. data/lib/git/lib.rb +1606 -1028
  229. data/lib/git/log.rb +15 -2
  230. data/lib/git/object.rb +92 -22
  231. data/lib/git/parsers/branch.rb +224 -0
  232. data/lib/git/parsers/cat_file.rb +111 -0
  233. data/lib/git/parsers/diff.rb +585 -0
  234. data/lib/git/parsers/fsck.rb +133 -0
  235. data/lib/git/parsers/grep.rb +42 -0
  236. data/lib/git/parsers/ls_tree.rb +58 -0
  237. data/lib/git/parsers/stash.rb +208 -0
  238. data/lib/git/parsers/tag.rb +257 -0
  239. data/lib/git/remote.rb +133 -9
  240. data/lib/git/repository/branching.rb +572 -0
  241. data/lib/git/repository/committing.rb +191 -0
  242. data/lib/git/repository/configuring.rb +156 -0
  243. data/lib/git/repository/diffing.rb +775 -0
  244. data/lib/git/repository/inspecting.rb +153 -0
  245. data/lib/git/repository/logging.rb +247 -0
  246. data/lib/git/repository/merging.rb +295 -0
  247. data/lib/git/repository/object_operations.rb +1101 -0
  248. data/lib/git/repository/path_resolver.rb +207 -0
  249. data/lib/git/repository/remote_operations.rb +753 -0
  250. data/lib/git/repository/shared_private.rb +51 -0
  251. data/lib/git/repository/staging.rb +390 -0
  252. data/lib/git/repository/stashing.rb +107 -0
  253. data/lib/git/repository/status_operations.rb +180 -0
  254. data/lib/git/repository/worktree_operations.rb +159 -0
  255. data/lib/git/repository.rb +264 -1
  256. data/lib/git/stash.rb +85 -4
  257. data/lib/git/stash_info.rb +104 -0
  258. data/lib/git/stashes.rb +130 -13
  259. data/lib/git/status.rb +224 -18
  260. data/lib/git/tag_delete_failure.rb +31 -0
  261. data/lib/git/tag_delete_result.rb +63 -0
  262. data/lib/git/tag_info.rb +105 -0
  263. data/lib/git/version.rb +109 -2
  264. data/lib/git/version_constraint.rb +81 -0
  265. data/lib/git/worktree.rb +120 -5
  266. data/lib/git/worktrees.rb +107 -7
  267. data/lib/git.rb +114 -18
  268. data/redesign/1_architecture_existing.md +54 -18
  269. data/redesign/2_architecture_redesign.md +365 -46
  270. data/redesign/3_architecture_implementation.md +1451 -54
  271. data/tasks/gem_tasks.rake +4 -0
  272. data/tasks/npm_tasks.rake +7 -0
  273. data/tasks/rspec.rake +48 -0
  274. data/tasks/test.rake +13 -1
  275. data/tasks/yard.rake +34 -7
  276. metadata +349 -20
  277. data/lib/git/index.rb +0 -6
  278. data/lib/git/path.rb +0 -38
  279. data/lib/git/working_directory.rb +0 -6
  280. /data/{release-please-config.json → .release-please-config.json} +0 -0
data/lib/git/log.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'git/base'
4
+
3
5
  module Git
4
6
  # Builds and executes a `git log` query
5
7
  #
@@ -42,7 +44,7 @@ module Git
42
44
  # git = Git.open('.')
43
45
  # Git::Log.new(git)
44
46
  #
45
- # @param base [Git::Base] the git repository object
47
+ # @param base [Git::Repository, Git::Base] the git repository object
46
48
  # @param max_count [Integer, Symbol, nil] the number of commits to return, or
47
49
  # `:all` or `nil` to return all
48
50
  #
@@ -157,10 +159,21 @@ module Git
157
159
  self
158
160
  end
159
161
 
162
+ # Returns the facade interface for log operations.
163
+ #
164
+ # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
165
+ # The `is_a?` guard will be removed when {Git::Base} is deleted in Phase 4.
166
+ #
167
+ # @return [Git::Repository]
168
+ #
169
+ def log_repository
170
+ @base.is_a?(Git::Base) ? @base.facade_repository : @base
171
+ end
172
+
160
173
  def run_log_if_dirty
161
174
  return unless @dirty
162
175
 
163
- log_data = @base.lib.full_log_commits(@options)
176
+ log_data = log_repository.full_log_commits(@options)
164
177
  @commits = log_data.map { |c| Git::Object::Commit.new(@base, c['sha'], c) }
165
178
  @dirty = false
166
179
  end
data/lib/git/object.rb CHANGED
@@ -4,6 +4,7 @@ require 'git/author'
4
4
  require 'git/diff'
5
5
  require 'git/errors'
6
6
  require 'git/log'
7
+ require 'git/base'
7
8
 
8
9
  module Git
9
10
  # represents a git object
@@ -24,24 +25,55 @@ module Git
24
25
  end
25
26
 
26
27
  def sha
27
- @sha ||= @base.lib.rev_parse(@objectish)
28
+ @sha ||= object_repository.rev_parse(@objectish)
28
29
  end
29
30
 
30
31
  def size
31
- @size ||= @base.lib.cat_file_size(@objectish)
32
+ @size ||= object_repository.cat_file_size(@objectish)
32
33
  end
33
34
 
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.
35
+ # Returns the raw content of this git object or streams it into a temporary file
36
+ #
37
+ # Without a block, the full content is buffered in memory and cached, then
38
+ # returned as a `String`. With a block, git output is streamed directly to a
39
+ # temporary file on disk — suitable for large objects.
40
+ #
41
+ # @api public
42
+ #
43
+ # @overload contents
44
+ # Returns the cached content as a string.
45
+ #
46
+ # @return [String] the raw content of the object, cached after first call
47
+ #
48
+ # @raise [Git::FailedError] if the object does not exist or the command fails
49
+ #
50
+ # @example Get the contents of a blob
51
+ # git.object('HEAD:README.md').contents # => "This is a README file\n"
52
+ #
53
+ # @overload contents(&block)
54
+ # Streams the content to a temporary file and yields it.
55
+ #
56
+ # Git output is written directly to a file without buffering in
57
+ # memory. Use this form for large blobs to avoid memory pressure.
58
+ #
59
+ # @yield [file] the temporary file, positioned at the start of the content
60
+ #
61
+ # @yieldparam file [File] readable `IO` object positioned at the beginning
62
+ #
63
+ # @yieldreturn [Object] the value to return from this method
64
+ #
65
+ # @return [Object] the value returned by the block
66
+ #
67
+ # @raise [Git::FailedError] if the object does not exist or the command fails
68
+ #
69
+ # @example Read a large blob without loading it into memory
70
+ # git.object('HEAD:large_file.bin').contents { |f| upload(f) }
38
71
  #
39
- # Use this for large files so that they are not held in memory.
40
72
  def contents(&)
41
73
  if block_given?
42
- @base.lib.cat_file_contents(@objectish, &)
74
+ object_repository.cat_file_contents(@objectish, &)
43
75
  else
44
- @contents ||= @base.lib.cat_file_contents(@objectish)
76
+ @contents ||= object_repository.cat_file_contents(@objectish)
45
77
  end
46
78
  end
47
79
 
@@ -54,8 +86,7 @@ module Git
54
86
  end
55
87
 
56
88
  def grep(string, path_limiter = nil, opts = {})
57
- opts = { object: sha, path_limiter: path_limiter }.merge(opts)
58
- @base.lib.grep(string, opts)
89
+ object_repository.grep(string, path_limiter, opts.merge(object: sha))
59
90
  end
60
91
 
61
92
  def diff(objectish)
@@ -66,9 +97,23 @@ module Git
66
97
  Git::Log.new(@base, count).object(@objectish)
67
98
  end
68
99
 
69
- # creates an archive of this object (tree)
100
+ # Creates an archive of this object and writes it to a file
101
+ #
102
+ # @api public
103
+ #
104
+ # @param file [String, nil] destination file path; a temp file is created if `nil`
105
+ #
106
+ # @param opts [Hash] archive options (see {Git::Lib#archive})
107
+ #
108
+ # @return [String] the path to the written archive file
109
+ #
110
+ # @raise [Git::FailedError] if `git archive` fails
111
+ #
112
+ # @example Archive a tree to a zip file
113
+ # git.object('v1.0').archive('/tmp/release.zip', format: 'zip')
114
+ #
70
115
  def archive(file = nil, opts = {})
71
- @base.lib.archive(@objectish, file, opts)
116
+ object_repository.archive(@objectish, file, opts)
72
117
  end
73
118
 
74
119
  def tree? = false
@@ -78,6 +123,19 @@ module Git
78
123
  def commit? = false
79
124
 
80
125
  def tag? = false
126
+
127
+ private
128
+
129
+ # Returns the facade interface for git object queries.
130
+ #
131
+ # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
132
+ # The `is_a?` guard will be removed when {Git::Base} is deleted in Phase 4.
133
+ #
134
+ # @return [Git::Repository]
135
+ #
136
+ def object_repository
137
+ @base.is_a?(Git::Base) ? @base.facade_repository : @base
138
+ end
81
139
  end
82
140
 
83
141
  # A Git blob object
@@ -117,11 +175,11 @@ module Git
117
175
  alias subdirectories trees
118
176
 
119
177
  def full_tree
120
- @base.lib.full_tree(@objectish)
178
+ object_repository.full_tree(@objectish)
121
179
  end
122
180
 
123
181
  def depth
124
- @base.lib.tree_depth(@objectish)
182
+ object_repository.tree_depth(@objectish)
125
183
  end
126
184
 
127
185
  def tree?
@@ -135,7 +193,7 @@ module Git
135
193
  @trees = {}
136
194
  @blobs = {}
137
195
 
138
- data = @base.lib.ls_tree(@objectish)
196
+ data = object_repository.ls_tree(@objectish)
139
197
 
140
198
  data['tree'].each do |key, tree|
141
199
  @trees[key] = Git::Object::Tree.new(@base, tree[:sha], tree[:mode])
@@ -169,7 +227,7 @@ module Git
169
227
  end
170
228
 
171
229
  def name
172
- @base.lib.name_rev(sha)
230
+ object_repository.name_rev(sha)
173
231
  end
174
232
 
175
233
  def gtree
@@ -239,7 +297,7 @@ module Git
239
297
  def check_commit
240
298
  return if @tree
241
299
 
242
- data = @base.lib.cat_file_commit(@objectish)
300
+ data = object_repository.cat_file_commit(@objectish)
243
301
  from_data(data)
244
302
  end
245
303
  end
@@ -268,7 +326,8 @@ module Git
268
326
  def initialize(base, sha, name = nil)
269
327
  if name.nil?
270
328
  name = sha
271
- sha = base.lib.tag_sha(name)
329
+ repo = base.is_a?(Git::Base) ? base.facade_repository : base
330
+ sha = repo.tag_sha(name)
272
331
  raise Git::UnexpectedResultError, "Tag '#{name}' does not exist." if sha == ''
273
332
  end
274
333
 
@@ -280,7 +339,7 @@ module Git
280
339
  end
281
340
 
282
341
  def annotated?
283
- @annotated = @annotated.nil? ? (@base.lib.cat_file_type(name) == 'tag') : @annotated
342
+ @annotated = @annotated.nil? ? (object_repository.cat_file_type(name) == 'tag') : @annotated
284
343
  end
285
344
 
286
345
  def message
@@ -303,7 +362,7 @@ module Git
303
362
  return if @loaded
304
363
 
305
364
  if annotated?
306
- tdata = @base.lib.cat_file_tag(@name)
365
+ tdata = object_repository.cat_file_tag(@name)
307
366
  @message = tdata['message'].chomp
308
367
  @tagger = Git::Author.new(tdata['tagger'])
309
368
  else
@@ -319,7 +378,7 @@ module Git
319
378
  def self.new(base, objectish, type = nil, is_tag = false) # rubocop:disable Style/OptionalBooleanParameter
320
379
  return new_tag(base, objectish) if is_tag
321
380
 
322
- type ||= base.lib.cat_file_type(objectish)
381
+ type ||= object_repository_for(base).cat_file_type(objectish)
323
382
  # TODO: why not handle tag case here too?
324
383
  klass =
325
384
  case type
@@ -334,5 +393,16 @@ module Git
334
393
  Git::Deprecation.warn('Git::Object.new with is_tag argument is deprecated. Use Git::Object::Tag.new instead.')
335
394
  Git::Object::Tag.new(base, objectish)
336
395
  end
396
+
397
+ # Returns the facade interface for git object queries.
398
+ #
399
+ # Accepts either a {Git::Repository} (new form) or a {Git::Base} (legacy).
400
+ # The `is_a?` guard will be removed when {Git::Base} is deleted in Phase 4.
401
+ #
402
+ # @return [Git::Repository]
403
+ #
404
+ private_class_method def self.object_repository_for(base)
405
+ base.is_a?(Git::Base) ? base.facade_repository : base
406
+ end
337
407
  end
338
408
  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