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
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/command_line'
4
+
5
+ module Git
6
+ module CommandLine
7
+ # Executes a git command and captures both stdout and stderr in memory
8
+ #
9
+ # {Git::CommandLine::Capturing} is the buffering strategy: it calls
10
+ # `ProcessExecuter.run_with_capture`, which reads all subprocess output into
11
+ # `String` objects before returning. Use this class (via
12
+ # {Git::Lib#command_capturing}) for the vast majority of git subcommands whose
13
+ # output fits comfortably in memory.
14
+ #
15
+ # {Git::CommandLine::Streaming} is the complementary strategy for commands
16
+ # (such as `cat-file -p <blob>`) whose stdout may be too large to buffer.
17
+ #
18
+ # @example
19
+ # capturing = Git::CommandLine::Capturing.new(
20
+ # {}, '/usr/bin/git', %w[--git-dir /repo/.git], Logger.new($stdout)
21
+ # )
22
+ # result = capturing.run('log', '--oneline', '-5')
23
+ # result.stdout # => "abc1234 Initial commit\n..."
24
+ # result.stderr # => ""
25
+ #
26
+ # @see Git::Lib#command_capturing
27
+ #
28
+ # @see Git::CommandLine::Streaming
29
+ #
30
+ class Capturing < Git::CommandLine::Base
31
+ # Default options accepted by {#run}
32
+ #
33
+ # @api private
34
+ RUN_OPTION_DEFAULTS = {
35
+ in: nil,
36
+ out: nil,
37
+ err: nil,
38
+ chdir: nil,
39
+ timeout: nil,
40
+ raise_on_failure: true,
41
+ env: {},
42
+ normalize: false,
43
+ chomp: false,
44
+ merge: false
45
+ }.freeze
46
+
47
+ # Execute a git command, capture stdout and stderr, and return the result
48
+ #
49
+ # Non-option command-line arguments to pass to git. If you collect the
50
+ # arguments in an array, splat the array into the parameter list.
51
+ #
52
+ # NORMALIZATION
53
+ #
54
+ # The command output is returned as a Unicode string containing the binary
55
+ # output from the command. If the binary output is not valid UTF-8, the
56
+ # output will cause problems because the encoding will be invalid.
57
+ #
58
+ # Normalization is a process that tries to convert the binary output to a
59
+ # valid UTF-8 string. It uses the `rchardet` gem to detect the encoding of
60
+ # the binary output and then converts it to UTF-8.
61
+ #
62
+ # Normalization is not enabled by default. Pass `normalize: true` to enable
63
+ # it. When enabled, normalization is applied to both stdout and stderr in
64
+ # the returned result object, regardless of the `out:` or `err:` options.
65
+ # Only the captured in-memory strings are normalized; any external IO you
66
+ # provide will receive the raw subprocess output.
67
+ #
68
+ # @example Run a command and return the output
69
+ # result = capturing.run('version')
70
+ # result.stdout #=> "git version 2.39.1\n"
71
+ #
72
+ # @example The args array should be splatted into the parameter list
73
+ # args = %w[log -n 1 --oneline]
74
+ # result = capturing.run(*args)
75
+ # result.stdout #=> "f5baa11 beginning of Ruby/Git project\n"
76
+ #
77
+ # @example Run a command and return the chomped output
78
+ # result = capturing.run('version', chomp: true)
79
+ # result.stdout #=> "git version 2.39.1"
80
+ #
81
+ # @example Run a command without normalizing the output
82
+ # capturing.run('version', normalize: false) #=> "git version 2.39.1\n"
83
+ #
84
+ # @example Capture stdout in a temporary file
85
+ # require 'tempfile'
86
+ # Tempfile.create('git') do |file|
87
+ # capturing.run('version', out: file)
88
+ # file.rewind
89
+ # file.read #=> "git version 2.39.1\n"
90
+ # end
91
+ #
92
+ # @example Capture stderr in a StringIO object
93
+ # require 'stringio'
94
+ # stderr = StringIO.new
95
+ # begin
96
+ # capturing.run('log', 'nonexistent-branch', err: stderr)
97
+ # rescue Git::FailedError => e
98
+ # stderr.string #=> "unknown revision or path not in the working tree.\n"
99
+ # end
100
+ #
101
+ # @param options_hash [Hash] the options to pass to the command
102
+ #
103
+ # @option options_hash [IO, nil] :in the IO object to use as stdin for the
104
+ # command, or nil to inherit the parent process stdin. Must be a real IO
105
+ # object with a file descriptor (not StringIO).
106
+ #
107
+ # @option options_hash [#write, nil] :out the object to write stdout to, or
108
+ # nil to capture stdout in the returned result.
109
+ #
110
+ # If this is a `StringIO` object, `stdout_writer.string` will be returned.
111
+ #
112
+ # In general, only specify a `stdout_writer` when you want to redirect
113
+ # stdout to a file or other `#write`-responding object. The default
114
+ # behaviour returns the command output.
115
+ #
116
+ # @option options_hash [#write, nil] :err the object to write stderr to, or
117
+ # nil to capture stderr in the returned result.
118
+ #
119
+ # @option options_hash [Boolean] :normalize (false) whether to normalize the
120
+ # encoding of stdout and stderr output
121
+ #
122
+ # @option options_hash [Boolean] :chomp (false) whether to chomp both stdout
123
+ # and stderr output
124
+ #
125
+ # @option options_hash [Boolean] :merge (false) whether to merge stdout and
126
+ # stderr in the returned string
127
+ #
128
+ # @option options_hash [String, nil] :chdir the directory to run the command in
129
+ #
130
+ # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for
131
+ # the command to complete. Zero means no timeout. A timeout kills the
132
+ # process via `SIGKILL` and raises {Git::TimeoutError}.
133
+ #
134
+ # @option options_hash [Boolean] :raise_on_failure (true) whether to raise
135
+ # {Git::FailedError} on non-zero exit status.
136
+ # {Git::TimeoutError} and {Git::SignaledError} are always raised regardless.
137
+ #
138
+ # @option options_hash [Hash] :env ({}) additional environment variable
139
+ # overrides for this command. String keys map to String values (to set) or
140
+ # `nil` (to unset).
141
+ #
142
+ # @return [Git::CommandLineResult] the result of the command
143
+ #
144
+ # @raise [ArgumentError] if `args` contains an array or an unknown option is
145
+ # passed
146
+ #
147
+ # @raise [Git::SignaledError] if the command was terminated by an uncaught signal
148
+ #
149
+ # @raise [Git::FailedError] if the command returned a non-zero exit status
150
+ #
151
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting
152
+ # subprocess output
153
+ #
154
+ # @raise [Git::TimeoutError] if the command times out
155
+ #
156
+ def run(*, **options_hash)
157
+ options = merge_and_validate_options(RUN_OPTION_DEFAULTS, options_hash)
158
+
159
+ result = execute(*, **options)
160
+ process_result(result, options)
161
+ end
162
+
163
+ private
164
+
165
+ # @return [ProcessExecuter::ResultWithCapture] the process result with captured output
166
+ #
167
+ # @api private
168
+ def execute(*args, **options_hash)
169
+ git_cmd = build_git_cmd(args)
170
+ options = execute_options(**options_hash)
171
+ run_process_executer do
172
+ ProcessExecuter.run_with_capture(merged_env(options_hash), *git_cmd, **options)
173
+ end
174
+ end
175
+
176
+ # Build the ProcessExecuter options hash for a capturing run
177
+ #
178
+ # @return [Hash]
179
+ #
180
+ # @api private
181
+ def execute_options(**options_hash)
182
+ chdir = options_hash[:chdir] || :not_set
183
+ timeout_after = options_hash[:timeout]
184
+ merge_output = options_hash[:merge] || false
185
+
186
+ { chdir:, timeout_after:, merge_output:, raise_errors: false }.tap do |options|
187
+ redirect_options(options_hash).each { |k, v| options[k] = v }
188
+ end
189
+ end
190
+
191
+ # Extract non-nil redirect options (`:in`, `:out`, `:err`) from options_hash
192
+ #
193
+ # @return [Hash]
194
+ #
195
+ # @api private
196
+ def redirect_options(options_hash)
197
+ %i[in out err].filter_map do |key|
198
+ val = options_hash[key]
199
+ [key, val] unless val.nil?
200
+ end.to_h
201
+ end
202
+
203
+ # Post-process and return the stdout/stderr strings from the captured result,
204
+ # then log and raise on failure if required.
205
+ #
206
+ # @param result [ProcessExecuter::ResultWithCapture] the raw result
207
+ #
208
+ # @param options [Hash] the merged run options
209
+ #
210
+ # @return [Git::CommandLineResult]
211
+ #
212
+ # @raise [Git::FailedError] if the command failed and raise_on_failure is true
213
+ #
214
+ # @raise [Git::SignaledError] if the command was signaled
215
+ #
216
+ # @raise [Git::TimeoutError] if the command timed out
217
+ #
218
+ # @api private
219
+ def process_result(result, options)
220
+ command = result.command
221
+ processed_out, processed_err = post_process_output(result, options[:normalize], options[:chomp])
222
+ log_result(result, command, processed_out, processed_err)
223
+ command_line_result(
224
+ command, result, processed_out, processed_err, options[:timeout], options[:raise_on_failure]
225
+ )
226
+ end
227
+
228
+ # Normalize and/or chomp the raw stdout and stderr strings.
229
+ #
230
+ # @param result [ProcessExecuter::ResultWithCapture] the raw result
231
+ #
232
+ # @param normalize [Boolean]
233
+ #
234
+ # @param chomp [Boolean]
235
+ #
236
+ # @return [Array<String>] two-element array: [processed_stdout, processed_stderr]
237
+ #
238
+ # @api private
239
+ def post_process_output(result, normalize, chomp)
240
+ [result.stdout, result.stderr].map do |raw_output|
241
+ output = raw_output.dup
242
+ output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize
243
+ output.chomp! if chomp
244
+ output
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ module CommandLine
5
+ # The result of running a git command
6
+ #
7
+ # This object stores the Git command executed and its status, stdout, and stderr.
8
+ #
9
+ # @api public
10
+ #
11
+ class Result
12
+ # Create a Result object
13
+ #
14
+ # @example
15
+ # git_cmd = %w[git version]
16
+ # status = instance_double(ProcessExecuter::Result)
17
+ # stdout = "git version 2.39.1\n"
18
+ # stderr = ""
19
+ # result = Git::CommandLine::Result.new(git_cmd, status, stdout, stderr)
20
+ #
21
+ # @param git_cmd [Array<String>] the git command that was executed
22
+ #
23
+ # @param status [ProcessExecuter::Result] the process result object returned
24
+ # by `ProcessExecuter.run` or `ProcessExecuter.run_with_capture`.
25
+ # Responds to `timed_out?`, `signaled?`, and `success?`.
26
+ #
27
+ # @param stdout [String] the processed stdout of the process
28
+ #
29
+ # @param stderr [String] the processed stderr of the process
30
+ #
31
+ def initialize(git_cmd, status, stdout, stderr)
32
+ @git_cmd = git_cmd
33
+ @status = status
34
+ @stdout = stdout
35
+ @stderr = stderr
36
+ end
37
+
38
+ # @attribute [r] git_cmd
39
+ #
40
+ # The git command that was executed
41
+ #
42
+ # @example
43
+ # git_cmd = %w[git version]
44
+ # result = Git::CommandLine::Result.new(git_cmd, nil, '', '')
45
+ # result.git_cmd #=> ["git", "version"]
46
+ #
47
+ # @return [Array<String>]
48
+ #
49
+ attr_reader :git_cmd
50
+
51
+ # @attribute [r] status
52
+ #
53
+ # The process result object returned by ProcessExecuter
54
+ #
55
+ # In practice this is a `ProcessExecuter::ResultWithCapture` (from
56
+ # {Git::CommandLine::Capturing}) or a `ProcessExecuter::Result` (from
57
+ # {Git::CommandLine::Streaming}). Both respond to `success?`, `timed_out?`,
58
+ # and `signaled?`.
59
+ #
60
+ # @example
61
+ # status = instance_double(ProcessExecuter::Result, success?: true)
62
+ # result = Git::CommandLine::Result.new(%w[git version], status, '', '')
63
+ # result.status == status #=> true
64
+ #
65
+ # @return [ProcessExecuter::Result]
66
+ #
67
+ attr_reader :status
68
+
69
+ # @attribute [r] stdout
70
+ #
71
+ # The output of the process
72
+ #
73
+ # @example
74
+ # stdout = "git version 2.39.1\n"
75
+ # result = Git::CommandLine::Result.new([], nil, stdout, '')
76
+ # result.stdout #=> "git version 2.39.1\n"
77
+ #
78
+ # @return [String]
79
+ #
80
+ attr_reader :stdout
81
+
82
+ # @attribute [r] stderr
83
+ #
84
+ # The error output of the process
85
+ #
86
+ # @example
87
+ # stderr = "Tag not found\n"
88
+ # result = Git::CommandLine::Result.new([], nil, '', stderr)
89
+ # result.stderr #=> "Tag not found\n"
90
+ #
91
+ # @return [String]
92
+ #
93
+ attr_reader :stderr
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/command_line'
4
+ require 'stringio'
5
+
6
+ module Git
7
+ module CommandLine
8
+ # Executes a git command in streaming mode without buffering stdout in memory
9
+ #
10
+ # {Git::CommandLine::Streaming} is the non-buffering strategy: it calls
11
+ # `ProcessExecuter.run` and streams stdout directly to the caller-supplied `out:`
12
+ # IO object. Stderr is always captured internally in a `StringIO` for error
13
+ # diagnostics and is available as `result.stderr`.
14
+ #
15
+ # Use this class (via {Git::Lib#command_streaming}) for commands such as
16
+ # `cat-file -p <blob>` whose stdout may be too large to buffer in memory.
17
+ #
18
+ # {Git::CommandLine::Capturing} is the complementary strategy for the common case
19
+ # where buffering stdout is acceptable.
20
+ #
21
+ # @example Stream a blob to a file
22
+ # streaming = Git::CommandLine::Streaming.new(
23
+ # {}, '/usr/bin/git', %w[--git-dir /repo/.git], Logger.new($stdout)
24
+ # )
25
+ # File.open('/tmp/blob', 'wb') do |f|
26
+ # streaming.run('cat-file', 'blob', sha, out: f)
27
+ # end
28
+ #
29
+ # @see Git::Lib#command_streaming
30
+ #
31
+ # @see Git::CommandLine::Capturing
32
+ #
33
+ class Streaming < Git::CommandLine::Base
34
+ # Default options accepted by {#run}
35
+ #
36
+ # @api private
37
+ RUN_OPTION_DEFAULTS = {
38
+ in: nil,
39
+ out: nil,
40
+ err: nil,
41
+ chdir: nil,
42
+ timeout: nil,
43
+ raise_on_failure: true,
44
+ env: {}
45
+ }.freeze
46
+
47
+ # Execute a git command in streaming mode and return the result
48
+ #
49
+ # Unlike {Git::CommandLine::Capturing#run}, this method does **not** buffer
50
+ # stdout in memory. Stdout is written only to the IO object provided via the
51
+ # `out:` option. Stderr is captured internally via a `StringIO` for error
52
+ # diagnostics.
53
+ #
54
+ # Use this entry point for commands that stream large content (e.g. blobs)
55
+ # where capturing stdout in memory would be unacceptable.
56
+ #
57
+ # @example Stream a blob to a file
58
+ # file = File.open('/tmp/blob', 'wb')
59
+ # streaming.run('cat-file', 'blob', sha, out: file)
60
+ #
61
+ # @param options_hash [Hash] the options to pass to the command
62
+ #
63
+ # @option options_hash [IO, nil] :in the IO object to use as stdin for the
64
+ # command, or nil to inherit the parent process stdin. Must be a real IO
65
+ # object with a file descriptor (not StringIO).
66
+ #
67
+ # @option options_hash [#write, nil] :out the IO/object to stream stdout into.
68
+ # Stdout is NOT buffered in the returned result; this is the only way to
69
+ # read it.
70
+ #
71
+ # @option options_hash [#write, nil] :err an optional additional destination to
72
+ # receive stderr output in real time (e.g. `$stderr` or a `File`). Stderr is
73
+ # always captured internally in a `StringIO` for error diagnostics. When
74
+ # `err:` is provided, writes are teed to both the internal buffer and this
75
+ # destination. `result.stderr` always reflects what was captured in the
76
+ # internal buffer, regardless of whether `err:` is supplied.
77
+ #
78
+ # @option options_hash [String, nil] :chdir the directory to run the command in
79
+ #
80
+ # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for
81
+ # the command to complete. Zero means no timeout. A timeout kills the
82
+ # process via `SIGKILL` and raises {Git::TimeoutError}.
83
+ #
84
+ # @option options_hash [Boolean] :raise_on_failure (true) whether to raise
85
+ # {Git::FailedError} on non-zero exit status.
86
+ # {Git::TimeoutError} and {Git::SignaledError} are always raised regardless.
87
+ #
88
+ # @option options_hash [Hash] :env ({}) additional environment variable
89
+ # overrides for this command. String keys map to String values (to set) or
90
+ # `nil` (to unset).
91
+ #
92
+ # @return [Git::CommandLineResult] the result of the command
93
+ #
94
+ # `result.stdout` will always be `''` (empty) — stdout was streamed to `out:`.
95
+ # `result.stderr` contains any stderr output captured for diagnostics.
96
+ #
97
+ # @raise [ArgumentError] if `args` contains an array or an unknown option is
98
+ # passed
99
+ #
100
+ # @raise [Git::SignaledError] if the command was terminated by an uncaught signal
101
+ #
102
+ # @raise [Git::FailedError] if the command returned a non-zero exit status
103
+ #
104
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting
105
+ # subprocess output
106
+ #
107
+ # @raise [Git::TimeoutError] if the command times out
108
+ #
109
+ def run(*, **options_hash)
110
+ options = merge_and_validate_options(RUN_OPTION_DEFAULTS, options_hash)
111
+
112
+ internal_err = StringIO.new
113
+ # Tee stderr to the caller-provided destination (if any) AND the internal
114
+ # StringIO. This ensures result.stderr is always available even when err:
115
+ # is a non-StringIO IO object.
116
+ err_dest = options[:err] ? build_stderr_tee(internal_err, options[:err]) : internal_err
117
+ result = execute(*, err_io: err_dest, **options)
118
+ process_result(result, internal_err, options)
119
+ end
120
+
121
+ private
122
+
123
+ # @return [ProcessExecuter::Result] the result of running the command (non-capturing)
124
+ #
125
+ # @api private
126
+ def execute(*args, err_io:, **options_hash)
127
+ git_cmd = build_git_cmd(args)
128
+ options = execute_options(err_io:, **options_hash)
129
+ run_process_executer do
130
+ ProcessExecuter.run(merged_env(options_hash), *git_cmd, **options)
131
+ end
132
+ end
133
+
134
+ # Build the ProcessExecuter options hash for a streaming run
135
+ #
136
+ # @return [Hash]
137
+ #
138
+ # @api private
139
+ def execute_options(err_io:, **options_hash)
140
+ chdir = options_hash[:chdir] || :not_set
141
+ timeout_after = options_hash[:timeout]
142
+
143
+ { chdir:, timeout_after:, raise_errors: false, err: err_io }.tap do |options|
144
+ options[:in] = options_hash[:in] unless options_hash[:in].nil?
145
+ options[:out] = options_hash[:out] unless options_hash[:out].nil?
146
+ end
147
+ end
148
+
149
+ # Build a tee writer that forwards #write calls to two destinations simultaneously.
150
+ #
151
+ # Used to capture stderr in an internal StringIO while also streaming to a
152
+ # caller-provided destination.
153
+ #
154
+ # @param primary [StringIO] the internal capture buffer
155
+ #
156
+ # @param secondary [#write] the caller-supplied destination
157
+ #
158
+ # @return [#write] an object whose #write method delegates to both destinations
159
+ #
160
+ # @api private
161
+ def build_stderr_tee(primary, secondary)
162
+ ::Object.new.tap do |tee|
163
+ tee.define_singleton_method(:write) do |data|
164
+ primary.write(data)
165
+ secondary.write(data)
166
+ data.bytesize
167
+ end
168
+ end
169
+ end
170
+
171
+ # Process the result of a streaming command and return a Git::CommandLineResult
172
+ #
173
+ # Constructs stdout as `''` (not captured) and stderr from the internal StringIO.
174
+ #
175
+ # @param result [ProcessExecuter::Result] the raw process result
176
+ #
177
+ # @param err_io [StringIO] the internal StringIO that captured stderr
178
+ #
179
+ # @param options [Hash] the merged run options
180
+ #
181
+ # @return [Git::CommandLineResult]
182
+ #
183
+ # @api private
184
+ def process_result(result, err_io, options)
185
+ command = result.command
186
+ stderr = err_io.string
187
+ log_result(result, command, '', stderr)
188
+ command_line_result(
189
+ command, result, '', stderr, options[:timeout], options[:raise_on_failure]
190
+ )
191
+ end
192
+ end
193
+ end
194
+ end