git 4.4.1 → 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 -67
  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 -66
  45. data/CONTRIBUTING.md +694 -53
  46. data/README.md +17 -17
  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 +488 -412
  52. data/lib/git/branch.rb +117 -59
  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 -1085
  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 +226 -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 +117 -56
  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,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
@@ -1,330 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'git/base'
4
- require 'git/command_line_result'
5
- require 'git/errors'
6
- require 'stringio'
7
-
8
3
  module Git
9
- # Runs a git command and returns the result
4
+ # Namespace module for git command-line execution strategies
5
+ #
6
+ # This module groups the classes responsible for invoking git subprocesses
7
+ # and handling their output. Choose a concrete class based on your buffering
8
+ # needs:
9
+ #
10
+ # * {Git::CommandLine::Capturing} — buffers stdout and stderr in memory.
11
+ # Use this for the vast majority of git commands whose output fits in memory.
12
+ #
13
+ # * {Git::CommandLine::Streaming} — streams stdout to a caller-supplied IO.
14
+ # Use this for commands (e.g. `cat-file -p <blob>`) whose output may be
15
+ # too large to buffer.
16
+ #
17
+ # Both classes inherit from {Git::CommandLine::Base} and are instantiated
18
+ # via factory helpers in {Git::Lib}: {Git::Lib#command_capturing} and
19
+ # {Git::Lib#command_streaming}.
20
+ #
21
+ # Results are returned as {Git::CommandLine::Result} objects (also accessible
22
+ # as {Git::CommandLineResult} for backward compatibility).
10
23
  #
11
24
  # @api public
12
25
  #
13
- class CommandLine
14
- # Create a Git::CommandLine object
15
- #
16
- # @example
17
- # env = { 'GIT_DIR' => '/path/to/git/dir' }
18
- # binary_path = '/usr/bin/git'
19
- # global_opts = %w[--git-dir /path/to/git/dir]
20
- # logger = Logger.new(STDOUT)
21
- # cli = CommandLine.new(env, binary_path, global_opts, logger)
22
- # cli.run('version') #=> #<Git::CommandLineResult:0x00007f9b0c0b0e00
23
- #
24
- # @param env [Hash<String, String>] environment variables to set
25
- # @param global_opts [Array<String>] global options to pass to git
26
- # @param logger [Logger] the logger to use
27
- #
28
- def initialize(env, binary_path, global_opts, logger)
29
- @env = env
30
- @binary_path = binary_path
31
- @global_opts = global_opts
32
- @logger = logger
33
- end
34
-
35
- # @attribute [r] env
36
- #
37
- # Variables to set (or unset) in the git command's environment
38
- #
39
- # @example
40
- # env = { 'GIT_DIR' => '/path/to/git/dir' }
41
- # command_line = Git::CommandLine.new(env, '/usr/bin/git', [], Logger.new(STDOUT))
42
- # command_line.env #=> { 'GIT_DIR' => '/path/to/git/dir' }
43
- #
44
- # @return [Hash<String, String>]
45
- #
46
- # @see https://ruby-doc.org/3.2.1/Process.html#method-c-spawn Process.spawn
47
- # for details on how to set environment variables using the `env` parameter
48
- #
49
- attr_reader :env
50
-
51
- # @attribute [r] binary_path
52
- #
53
- # The path to the command line binary to run
54
- #
55
- # @example
56
- # binary_path = '/usr/bin/git'
57
- # command_line = Git::CommandLine.new({}, binary_path, ['version'], Logger.new(STDOUT))
58
- # command_line.binary_path #=> '/usr/bin/git'
59
- #
60
- # @return [String]
61
- #
62
- attr_reader :binary_path
63
-
64
- # @attribute [r] global_opts
65
- #
66
- # The global options to pass to git
67
- #
68
- # These are options that are passed to git before the command name and
69
- # arguments. For example, in `git --git-dir /path/to/git/dir version`, the
70
- # global options are %w[--git-dir /path/to/git/dir].
71
- #
72
- # @example
73
- # env = {}
74
- # global_opts = %w[--git-dir /path/to/git/dir]
75
- # logger = Logger.new(nil)
76
- # cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger)
77
- # cli.global_opts #=> %w[--git-dir /path/to/git/dir]
78
- #
79
- # @return [Array<String>]
80
- #
81
- attr_reader :global_opts
82
-
83
- # @attribute [r] logger
84
- #
85
- # The logger to use for logging git commands and results
86
- #
87
- # @example
88
- # env = {}
89
- # global_opts = %w[]
90
- # logger = Logger.new(STDOUT)
91
- # cli = CommandLine.new(env, '/usr/bin/git', global_opts, logger)
92
- # cli.logger == logger #=> true
93
- #
94
- # @return [Logger]
95
- #
96
- attr_reader :logger
97
-
98
- # Execute a git command, wait for it to finish, and return the result
99
- #
100
- # Non-option the command line arguements to pass to git. If you collect
101
- # the command line arguments in an array, make sure you splat the array
102
- # into the parameter list.
103
- #
104
- # NORMALIZATION
105
- #
106
- # The command output is returned as a Unicde string containing the binary output
107
- # from the command. If the binary output is not valid UTF-8, the output will
108
- # cause problems because the encoding will be invalid.
109
- #
110
- # Normalization is a process that trys to convert the binary output to a valid
111
- # UTF-8 string. It uses the `rchardet` gem to detect the encoding of the binary
112
- # output and then converts it to UTF-8.
113
- #
114
- # Normalization is not enabled by default. Pass `normalize: true` to Git::CommandLine#run
115
- # to enable it. Normalization will only be performed on stdout and only if the `out:`` option
116
- # is nil or is a StringIO object. If the out: option is set to a file or other IO object,
117
- # the normalize option will be ignored.
118
- #
119
- # @example Run a command and return the output
120
- # cli.run('version') #=> "git version 2.39.1\n"
121
- #
122
- # @example The args array should be splatted into the parameter list
123
- # args = %w[log -n 1 --oneline]
124
- # cli.run(*args) #=> "f5baa11 beginning of Ruby/Git project\n"
125
- #
126
- # @example Run a command and return the chomped output
127
- # cli.run('version', chomp: true) #=> "git version 2.39.1"
128
- #
129
- # @example Run a command and without normalizing the output
130
- # cli.run('version', normalize: false) #=> "git version 2.39.1\n"
131
- #
132
- # @example Capture stdout in a temporary file
133
- # require 'tempfile'
134
- # tempfile = Tempfile.create('git') do |file|
135
- # cli.run('version', out: file)
136
- # file.rewind
137
- # file.read #=> "git version 2.39.1\n"
138
- # end
139
- #
140
- # @example Capture stderr in a StringIO object
141
- # require 'stringio'
142
- # stderr = StringIO.new
143
- # begin
144
- # cli.run('log', 'nonexistent-branch', err: stderr)
145
- # rescue Git::FailedError => e
146
- # stderr.string #=> "unknown revision or path not in the working tree.\n"
147
- # end
148
- #
149
- # @param options_hash [Hash] the options to pass to the command
150
- #
151
- # @option options_hash [#write, nil] :out the object to write stdout to or nil to ignore stdout
152
- #
153
- # If this is a 'StringIO' object, then `stdout_writer.string` will be returned.
154
- #
155
- # In general, only specify a `stdout_writer` object when you want to redirect
156
- # stdout to a file or some other object that responds to `#write`. The default
157
- # behavior will return the output of the command.
158
- #
159
- # @option options_hash [#write, nil] :err the object to write stderr to or nil to ignore stderr
160
- #
161
- # If this is a 'StringIO' object and `merged_output` is `true`, then
162
- # `stderr_writer.string` will be merged into the output returned by this method.
163
- #
164
- # @option options_hash [Boolean] :normalize whether to normalize the output of stdout and stderr
165
- #
166
- # @option options_hash [Boolean] :chomp whether to chomp both stdout and stderr output
167
- #
168
- # @option options_hash [Boolean] :merge whether to merge stdout and stderr in the string returned
169
- #
170
- # @option options_hash [String, nil] :chdir the directory to run the command in
171
- #
172
- # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
173
- #
174
- # If timeout is zero, the timeout will not be enforced.
175
- #
176
- # If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
177
- #
178
- # If the command does not respond to SIGKILL, it will hang this method.
179
- #
180
- # @return [Git::CommandLineResult] the output of the command
181
- #
182
- # This result of running the command.
183
- #
184
- # @raise [ArgumentError] if `args` is not an array of strings
185
- #
186
- # @raise [Git::SignaledError] if the command was terminated because of an uncaught signal
187
- #
188
- # @raise [Git::FailedError] if the command returned a non-zero exitstatus
189
- #
190
- # @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
191
- #
192
- # @raise [Git::TimeoutError] if the command times out
193
- #
194
- def run(*, **options_hash)
195
- options_hash = RUN_ARGS.merge(options_hash)
196
- extra_options = options_hash.keys - RUN_ARGS.keys
197
- raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
198
-
199
- result = run_with_capture(*, **options_hash)
200
- process_result(result, options_hash[:normalize], options_hash[:chomp], options_hash[:timeout])
201
- end
202
-
203
- # @return [Git::CommandLineResult] the result of running the command
204
- #
205
- # @api private
206
- #
207
- def run_with_capture(*args, **options_hash)
208
- git_cmd = build_git_cmd(args)
209
- options = run_with_capture_options(**options_hash)
210
- ProcessExecuter.run_with_capture(env, *git_cmd, **options)
211
- rescue ProcessExecuter::ProcessIOError => e
212
- raise Git::ProcessIOError.new(e.message), cause: e.exception.cause
213
- end
214
-
215
- def run_with_capture_options(**options_hash)
216
- chdir = options_hash[:chdir] || :not_set
217
- timeout_after = options_hash[:timeout]
218
- out = options_hash[:out]
219
- err = options_hash[:err]
220
- merge_output = options_hash[:merge] || false
221
-
222
- { chdir:, timeout_after:, merge_output:, raise_errors: false }.tap do |options|
223
- options[:out] = out unless out.nil?
224
- options[:err] = err unless err.nil?
225
- end
226
- end
227
-
228
- RUN_ARGS = {
229
- normalize: false,
230
- chomp: false,
231
- merge: false,
232
- out: nil,
233
- err: nil,
234
- chdir: nil,
235
- timeout: nil
236
- }.freeze
237
-
238
- private
239
-
240
- # Build the git command line from the available sources to send to `Process.spawn`
241
- # @return [Array<String>]
242
- # @api private
243
- #
244
- def build_git_cmd(args)
245
- raise ArgumentError, 'The args array can not contain an array' if args.any?(Array)
246
-
247
- [binary_path, *global_opts, *args].map(&:to_s)
248
- end
249
-
250
- # Process the result of the command and return a Git::CommandLineResult
251
- #
252
- # Post process output, log the command and result, and raise an error if the
253
- # command failed.
254
- #
255
- # @param result [ProcessExecuter::Command::Result] the result it is a
256
- # Process::Status and include command, stdout, and stderr
257
- #
258
- # @param normalize [Boolean] whether to normalize the output of each writer
259
- #
260
- # @param chomp [Boolean] whether to chomp the output of each writer
261
- #
262
- # @param timeout [Numeric, nil] the maximum seconds to wait for the command to
263
- # complete
264
- #
265
- # @return [Git::CommandLineResult] the result of the command to return to the
266
- # caller
267
- #
268
- # @raise [Git::FailedError] if the command failed
269
- #
270
- # @raise [Git::SignaledError] if the command was signaled
271
- #
272
- # @raise [Git::TimeoutError] if the command times out
273
- #
274
- # @raise [Git::ProcessIOError] if an exception was raised while collecting
275
- # subprocess output
276
- #
277
- # @api private
278
- #
279
- def process_result(result, normalize, chomp, timeout)
280
- command = result.command
281
- processed_out, processed_err = post_process_output(result, normalize, chomp)
282
- log_result(result, command, processed_out, processed_err)
283
- command_line_result(command, result, processed_out, processed_err, timeout)
284
- end
285
-
286
- def log_result(result, command, processed_out, processed_err)
287
- logger.info { "#{command} exited with status #{result}" }
288
- logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" }
289
- end
290
-
291
- def command_line_result(command, result, processed_out, processed_err, timeout)
292
- Git::CommandLineResult.new(command, result, processed_out, processed_err).tap do |processed_result|
293
- raise Git::TimeoutError.new(processed_result, timeout) if result.timeout?
294
-
295
- raise Git::SignaledError, processed_result if result.signaled?
296
-
297
- raise Git::FailedError, processed_result unless result.success?
298
- end
299
- end
300
-
301
- # Post-process and return an array of raw output strings
302
- #
303
- # For each raw output string:
304
- #
305
- # * If normalize: is true, normalize the encoding by transcoding each line from
306
- # the detected encoding to UTF-8.
307
- # * If chomp: is true chomp the output after normalization.
308
- #
309
- # Even if no post-processing is done based on the options, the strings returned
310
- # are a copy of the raw output strings. The raw output strings are not modified.
311
- #
312
- # @param result [ProcessExecuter::ResultWithCapture] the command's output to post-process
313
- #
314
- # @param normalize [Boolean] whether to normalize the output of each writer
315
- # @param chomp [Boolean] whether to chomp the output of each writer
316
- #
317
- # @return [Array<String>]
318
- #
319
- # @api private
320
- #
321
- def post_process_output(result, normalize, chomp)
322
- [result.stdout, result.stderr].map do |raw_output|
323
- output = raw_output.dup
324
- output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize
325
- output.chomp! if chomp
326
- output
327
- end
328
- end
26
+ # @example Buffered command via Git::CommandLine::Capturing
27
+ # cli = Git::CommandLine::Capturing.new(
28
+ # {}, '/usr/bin/git', [], Logger.new(nil)
29
+ # )
30
+ # result = cli.run('version')
31
+ # result.stdout #=> "git version 2.39.1\n"
32
+ #
33
+ # @example Streaming command via Git::CommandLine::Streaming
34
+ # cli = Git::CommandLine::Streaming.new(
35
+ # {}, '/usr/bin/git', [], Logger.new(nil)
36
+ # )
37
+ # File.open('/tmp/blob', 'wb') do |f|
38
+ # cli.run('cat-file', 'blob', sha, out: f)
39
+ # end
40
+ #
41
+ # @see Git::CommandLine::Base
42
+ # @see Git::CommandLine::Result
43
+ #
44
+ module CommandLine
329
45
  end
330
46
  end
47
+
48
+ require 'git/command_line/result'
49
+ require 'git/command_line/base'
50
+ require 'git/command_line/capturing'
51
+ require 'git/command_line/streaming'