git 1.19.1 → 5.5.0

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 (265) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -1
  3. data/AI_POLICY.md +24 -0
  4. data/CHANGELOG.md +689 -0
  5. data/CODE_OF_CONDUCT.md +25 -0
  6. data/CONTRIBUTING.md +1175 -97
  7. data/GOVERNANCE.md +106 -0
  8. data/LICENSE +1 -1
  9. data/MAINTAINERS.md +17 -4
  10. data/README.md +476 -320
  11. data/UPGRADING.md +1138 -0
  12. data/git.gemspec +124 -36
  13. data/lib/git/author.rb +39 -7
  14. data/lib/git/author_info.rb +66 -0
  15. data/lib/git/branch.rb +615 -65
  16. data/lib/git/branch_delete_failure.rb +34 -0
  17. data/lib/git/branch_delete_result.rb +66 -0
  18. data/lib/git/branch_info.rb +237 -0
  19. data/lib/git/branches.rb +167 -44
  20. data/lib/git/command_line/base.rb +247 -0
  21. data/lib/git/command_line/capturing.rb +308 -0
  22. data/lib/git/command_line/result.rb +88 -0
  23. data/lib/git/command_line/streaming.rb +236 -0
  24. data/lib/git/command_line.rb +52 -0
  25. data/lib/git/commands/add.rb +139 -0
  26. data/lib/git/commands/am/abort.rb +43 -0
  27. data/lib/git/commands/am/apply.rb +263 -0
  28. data/lib/git/commands/am/continue.rb +43 -0
  29. data/lib/git/commands/am/quit.rb +43 -0
  30. data/lib/git/commands/am/retry.rb +49 -0
  31. data/lib/git/commands/am/show_current_patch.rb +64 -0
  32. data/lib/git/commands/am/skip.rb +42 -0
  33. data/lib/git/commands/am.rb +33 -0
  34. data/lib/git/commands/apply.rb +242 -0
  35. data/lib/git/commands/archive/list_formats.rb +46 -0
  36. data/lib/git/commands/archive.rb +145 -0
  37. data/lib/git/commands/arguments.rb +4521 -0
  38. data/lib/git/commands/base.rb +502 -0
  39. data/lib/git/commands/branch/copy.rb +102 -0
  40. data/lib/git/commands/branch/create.rb +177 -0
  41. data/lib/git/commands/branch/delete.rb +88 -0
  42. data/lib/git/commands/branch/list.rb +178 -0
  43. data/lib/git/commands/branch/move.rb +102 -0
  44. data/lib/git/commands/branch/set_upstream.rb +86 -0
  45. data/lib/git/commands/branch/show_current.rb +49 -0
  46. data/lib/git/commands/branch/unset_upstream.rb +53 -0
  47. data/lib/git/commands/branch.rb +34 -0
  48. data/lib/git/commands/cat_file/batch.rb +385 -0
  49. data/lib/git/commands/cat_file/filtered.rb +105 -0
  50. data/lib/git/commands/cat_file/raw.rb +271 -0
  51. data/lib/git/commands/cat_file.rb +49 -0
  52. data/lib/git/commands/checkout/branch.rb +153 -0
  53. data/lib/git/commands/checkout/files.rb +116 -0
  54. data/lib/git/commands/checkout.rb +38 -0
  55. data/lib/git/commands/checkout_index.rb +106 -0
  56. data/lib/git/commands/clean.rb +102 -0
  57. data/lib/git/commands/clone.rb +241 -0
  58. data/lib/git/commands/commit.rb +273 -0
  59. data/lib/git/commands/commit_tree.rb +101 -0
  60. data/lib/git/commands/config_option_syntax/add.rb +86 -0
  61. data/lib/git/commands/config_option_syntax/get.rb +121 -0
  62. data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
  63. data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
  64. data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
  65. data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
  66. data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
  67. data/lib/git/commands/config_option_syntax/list.rb +111 -0
  68. data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
  69. data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
  70. data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
  71. data/lib/git/commands/config_option_syntax/set.rb +119 -0
  72. data/lib/git/commands/config_option_syntax/unset.rb +92 -0
  73. data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
  74. data/lib/git/commands/config_option_syntax.rb +56 -0
  75. data/lib/git/commands/describe.rb +156 -0
  76. data/lib/git/commands/diff.rb +657 -0
  77. data/lib/git/commands/diff_files.rb +519 -0
  78. data/lib/git/commands/diff_index.rb +499 -0
  79. data/lib/git/commands/fetch.rb +354 -0
  80. data/lib/git/commands/fsck.rb +138 -0
  81. data/lib/git/commands/gc.rb +134 -0
  82. data/lib/git/commands/grep.rb +339 -0
  83. data/lib/git/commands/init.rb +101 -0
  84. data/lib/git/commands/log.rb +634 -0
  85. data/lib/git/commands/ls_files.rb +195 -0
  86. data/lib/git/commands/ls_remote.rb +161 -0
  87. data/lib/git/commands/ls_tree.rb +135 -0
  88. data/lib/git/commands/maintenance/register.rb +77 -0
  89. data/lib/git/commands/maintenance/run.rb +109 -0
  90. data/lib/git/commands/maintenance/start.rb +71 -0
  91. data/lib/git/commands/maintenance/stop.rb +60 -0
  92. data/lib/git/commands/maintenance/unregister.rb +84 -0
  93. data/lib/git/commands/maintenance.rb +31 -0
  94. data/lib/git/commands/merge/abort.rb +44 -0
  95. data/lib/git/commands/merge/continue.rb +44 -0
  96. data/lib/git/commands/merge/quit.rb +46 -0
  97. data/lib/git/commands/merge/start.rb +250 -0
  98. data/lib/git/commands/merge.rb +28 -0
  99. data/lib/git/commands/merge_base.rb +91 -0
  100. data/lib/git/commands/mv.rb +82 -0
  101. data/lib/git/commands/name_rev.rb +119 -0
  102. data/lib/git/commands/pull.rb +382 -0
  103. data/lib/git/commands/push.rb +251 -0
  104. data/lib/git/commands/read_tree.rb +154 -0
  105. data/lib/git/commands/remote/add.rb +96 -0
  106. data/lib/git/commands/remote/get_url.rb +68 -0
  107. data/lib/git/commands/remote/list.rb +56 -0
  108. data/lib/git/commands/remote/prune.rb +63 -0
  109. data/lib/git/commands/remote/remove.rb +52 -0
  110. data/lib/git/commands/remote/rename.rb +76 -0
  111. data/lib/git/commands/remote/set_branches.rb +70 -0
  112. data/lib/git/commands/remote/set_head.rb +89 -0
  113. data/lib/git/commands/remote/set_url.rb +78 -0
  114. data/lib/git/commands/remote/set_url_add.rb +70 -0
  115. data/lib/git/commands/remote/set_url_delete.rb +71 -0
  116. data/lib/git/commands/remote/show.rb +77 -0
  117. data/lib/git/commands/remote/update.rb +79 -0
  118. data/lib/git/commands/remote.rb +42 -0
  119. data/lib/git/commands/repack.rb +281 -0
  120. data/lib/git/commands/reset.rb +154 -0
  121. data/lib/git/commands/rev_parse.rb +304 -0
  122. data/lib/git/commands/revert/abort.rb +45 -0
  123. data/lib/git/commands/revert/continue.rb +62 -0
  124. data/lib/git/commands/revert/quit.rb +47 -0
  125. data/lib/git/commands/revert/skip.rb +44 -0
  126. data/lib/git/commands/revert/start.rb +158 -0
  127. data/lib/git/commands/revert.rb +29 -0
  128. data/lib/git/commands/rm.rb +113 -0
  129. data/lib/git/commands/show.rb +632 -0
  130. data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
  131. data/lib/git/commands/show_ref/exists.rb +80 -0
  132. data/lib/git/commands/show_ref/list.rb +149 -0
  133. data/lib/git/commands/show_ref/verify.rb +122 -0
  134. data/lib/git/commands/show_ref.rb +42 -0
  135. data/lib/git/commands/stash/apply.rb +81 -0
  136. data/lib/git/commands/stash/branch.rb +67 -0
  137. data/lib/git/commands/stash/clear.rb +43 -0
  138. data/lib/git/commands/stash/create.rb +60 -0
  139. data/lib/git/commands/stash/drop.rb +73 -0
  140. data/lib/git/commands/stash/list.rb +43 -0
  141. data/lib/git/commands/stash/pop.rb +87 -0
  142. data/lib/git/commands/stash/push.rb +112 -0
  143. data/lib/git/commands/stash/show.rb +158 -0
  144. data/lib/git/commands/stash/store.rb +72 -0
  145. data/lib/git/commands/stash.rb +38 -0
  146. data/lib/git/commands/status.rb +174 -0
  147. data/lib/git/commands/symbolic_ref/delete.rb +72 -0
  148. data/lib/git/commands/symbolic_ref/read.rb +99 -0
  149. data/lib/git/commands/symbolic_ref/update.rb +79 -0
  150. data/lib/git/commands/symbolic_ref.rb +38 -0
  151. data/lib/git/commands/tag/create.rb +142 -0
  152. data/lib/git/commands/tag/delete.rb +57 -0
  153. data/lib/git/commands/tag/list.rb +146 -0
  154. data/lib/git/commands/tag/verify.rb +71 -0
  155. data/lib/git/commands/tag.rb +26 -0
  156. data/lib/git/commands/update_ref/batch.rb +145 -0
  157. data/lib/git/commands/update_ref/delete.rb +90 -0
  158. data/lib/git/commands/update_ref/update.rb +103 -0
  159. data/lib/git/commands/update_ref.rb +42 -0
  160. data/lib/git/commands/version.rb +60 -0
  161. data/lib/git/commands/worktree/add.rb +139 -0
  162. data/lib/git/commands/worktree/list.rb +64 -0
  163. data/lib/git/commands/worktree/lock.rb +58 -0
  164. data/lib/git/commands/worktree/management_base.rb +51 -0
  165. data/lib/git/commands/worktree/move.rb +66 -0
  166. data/lib/git/commands/worktree/prune.rb +67 -0
  167. data/lib/git/commands/worktree/remove.rb +63 -0
  168. data/lib/git/commands/worktree/repair.rb +76 -0
  169. data/lib/git/commands/worktree/unlock.rb +47 -0
  170. data/lib/git/commands/worktree.rb +43 -0
  171. data/lib/git/commands/write_tree.rb +68 -0
  172. data/lib/git/commands.rb +88 -0
  173. data/lib/git/config.rb +72 -5
  174. data/lib/git/config_entry_info.rb +106 -0
  175. data/lib/git/configuring.rb +795 -0
  176. data/lib/git/detached_head_info.rb +57 -0
  177. data/lib/git/diff.rb +437 -86
  178. data/lib/git/diff_file_numstat_info.rb +31 -0
  179. data/lib/git/diff_file_patch_info.rb +136 -0
  180. data/lib/git/diff_file_raw_info.rb +129 -0
  181. data/lib/git/diff_info.rb +162 -0
  182. data/lib/git/diff_path_status.rb +107 -0
  183. data/lib/git/diff_result.rb +34 -0
  184. data/lib/git/diff_stats.rb +111 -0
  185. data/lib/git/dirstat_info.rb +102 -0
  186. data/lib/git/encoding_utils.rb +32 -1
  187. data/lib/git/errors.rb +285 -0
  188. data/lib/git/escaped_path.rb +57 -5
  189. data/lib/git/execution_context/global.rb +31 -0
  190. data/lib/git/execution_context/repository.rb +151 -0
  191. data/lib/git/execution_context.rb +559 -0
  192. data/lib/git/factories.rb +813 -0
  193. data/lib/git/file_ref.rb +77 -0
  194. data/lib/git/fsck_object.rb +56 -0
  195. data/lib/git/fsck_result.rb +132 -0
  196. data/lib/git/log.rb +306 -90
  197. data/lib/git/object.rb +563 -141
  198. data/lib/git/parsers/branch.rb +240 -0
  199. data/lib/git/parsers/cat_file.rb +111 -0
  200. data/lib/git/parsers/config_entry.rb +110 -0
  201. data/lib/git/parsers/diff.rb +792 -0
  202. data/lib/git/parsers/fsck.rb +144 -0
  203. data/lib/git/parsers/grep.rb +42 -0
  204. data/lib/git/parsers/ls_remote.rb +79 -0
  205. data/lib/git/parsers/ls_tree.rb +58 -0
  206. data/lib/git/parsers/remote.rb +162 -0
  207. data/lib/git/parsers/stash.rb +292 -0
  208. data/lib/git/parsers/status.rb +251 -0
  209. data/lib/git/parsers/tag.rb +341 -0
  210. data/lib/git/parsers/worktree.rb +185 -0
  211. data/lib/git/path_resolver.rb +206 -0
  212. data/lib/git/remote.rb +165 -12
  213. data/lib/git/remote_info.rb +203 -0
  214. data/lib/git/repository/branching.rb +964 -0
  215. data/lib/git/repository/committing.rb +246 -0
  216. data/lib/git/repository/context_helpers.rb +293 -0
  217. data/lib/git/repository/diffing.rb +785 -0
  218. data/lib/git/repository/inspecting.rb +252 -0
  219. data/lib/git/repository/logging.rb +410 -0
  220. data/lib/git/repository/maintenance.rb +65 -0
  221. data/lib/git/repository/merging.rb +451 -0
  222. data/lib/git/repository/object_operations.rb +1551 -0
  223. data/lib/git/repository/remote_operations.rb +984 -0
  224. data/lib/git/repository/shared_private.rb +120 -0
  225. data/lib/git/repository/staging.rb +587 -0
  226. data/lib/git/repository/stashing.rb +623 -0
  227. data/lib/git/repository/status_operations.rb +249 -0
  228. data/lib/git/repository/worktree_operations.rb +339 -0
  229. data/lib/git/repository.rb +484 -2
  230. data/lib/git/stash.rb +109 -12
  231. data/lib/git/stash_info.rb +102 -0
  232. data/lib/git/stashes.rb +169 -26
  233. data/lib/git/status.rb +308 -122
  234. data/lib/git/status_file_info.rb +258 -0
  235. data/lib/git/status_info.rb +189 -0
  236. data/lib/git/tag_delete_failure.rb +34 -0
  237. data/lib/git/tag_delete_result.rb +66 -0
  238. data/lib/git/tag_info.rb +99 -0
  239. data/lib/git/url.rb +15 -8
  240. data/lib/git/version.rb +113 -2
  241. data/lib/git/version_constraint.rb +85 -0
  242. data/lib/git/worktree.rb +150 -8
  243. data/lib/git/worktree_info.rb +128 -0
  244. data/lib/git/worktrees.rb +118 -13
  245. data/lib/git.rb +632 -234
  246. metadata +369 -54
  247. data/.github/stale.yml +0 -25
  248. data/.github/workflows/continuous_integration.yml +0 -49
  249. data/.gitignore +0 -10
  250. data/Dockerfile.changelog-rs +0 -12
  251. data/Gemfile +0 -5
  252. data/ISSUE_TEMPLATE.md +0 -15
  253. data/PULL_REQUEST_TEMPLATE.md +0 -9
  254. data/RELEASING.md +0 -70
  255. data/Rakefile +0 -60
  256. data/lib/git/base/factory.rb +0 -99
  257. data/lib/git/base.rb +0 -711
  258. data/lib/git/command_line_result.rb +0 -86
  259. data/lib/git/failed_error.rb +0 -53
  260. data/lib/git/git_execute_error.rb +0 -7
  261. data/lib/git/index.rb +0 -5
  262. data/lib/git/lib.rb +0 -1328
  263. data/lib/git/path.rb +0 -31
  264. data/lib/git/signaled_error.rb +0 -50
  265. data/lib/git/working_directory.rb +0 -4
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/command_line/result'
4
+ require 'git/errors'
5
+ require 'process_executer'
6
+
7
+ module Git
8
+ module CommandLine
9
+ # Abstract base class for git command-line execution strategies
10
+ #
11
+ # Concrete subclasses must implement {#run} to execute a git command and
12
+ # return a {Git::CommandLine::Result}. Two implementations are provided:
13
+ #
14
+ # * {Git::CommandLine::Capturing} — buffers stdout and stderr in memory
15
+ # * {Git::CommandLine::Streaming} — streams stdout to a caller-supplied IO
16
+ #
17
+ # @example Instantiate a concrete subclass
18
+ # env = { 'GIT_DIR' => '/path/to/git/dir' }
19
+ # binary_path = '/usr/bin/git'
20
+ # global_opts = %w[--git-dir /path/to/git/dir]
21
+ # logger = Logger.new($stdout)
22
+ # cli = Git::CommandLine::Capturing.new(env, binary_path, global_opts, logger)
23
+ # cli.run('version') #=> #<Git::CommandLine::Result ...>
24
+ #
25
+ # @abstract Subclass and implement {#run}
26
+ #
27
+ # @api private
28
+ #
29
+ class Base
30
+ # Create a Base (or subclass) object
31
+ #
32
+ # @param env [Hash{String => String, nil}] environment variables to set or
33
+ # unset. String values set the variable; `nil` values unset it.
34
+ #
35
+ # @param binary_path [String] the path to the git binary
36
+ #
37
+ # @param global_opts [Array<String>] global options to pass to git
38
+ #
39
+ # @param logger [Logger] used to log git commands and their results
40
+ #
41
+ def initialize(env, binary_path, global_opts, logger)
42
+ @env = env
43
+ @binary_path = binary_path
44
+ @global_opts = global_opts
45
+ @logger = logger
46
+ end
47
+
48
+ # Execute a git command and return the result
49
+ #
50
+ # Concrete subclasses must override this method.
51
+ #
52
+ # @raise [NotImplementedError] always — must be implemented by subclasses
53
+ #
54
+ def run(*)
55
+ raise NotImplementedError, "#{self.class}#run is not implemented"
56
+ end
57
+
58
+ # Variables to set (or unset) in the git command's environment
59
+ #
60
+ # @example
61
+ # env = { 'GIT_DIR' => '/path/to/git/dir' }
62
+ # cli = Git::CommandLine::Capturing.new(env, '/usr/bin/git', [], Logger.new(nil))
63
+ # cli.env #=> { 'GIT_DIR' => '/path/to/git/dir' }
64
+ #
65
+ # @return [Hash{String => String, nil}]
66
+ #
67
+ # @see https://ruby-doc.org/3.2.1/Process.html#method-c-spawn Process.spawn
68
+ # for details on how to set environment variables using the `env` parameter
69
+ #
70
+ attr_reader :env
71
+
72
+ # The path to the command line binary to run
73
+ #
74
+ # @example
75
+ # cli = Git::CommandLine::Capturing.new({}, '/usr/bin/git', [], Logger.new(nil))
76
+ # cli.binary_path #=> '/usr/bin/git'
77
+ #
78
+ # @return [String]
79
+ #
80
+ attr_reader :binary_path
81
+
82
+ # The global options to pass to git
83
+ #
84
+ # These are options that are passed to git before the command name and
85
+ # arguments. For example, in `git --git-dir /path/to/git/dir version`, the
86
+ # global options are %w[--git-dir /path/to/git/dir].
87
+ #
88
+ # @example
89
+ # global_opts = %w[--git-dir /path/to/git/dir]
90
+ # cli = Git::CommandLine::Capturing.new({}, '/usr/bin/git', global_opts, Logger.new(nil))
91
+ # cli.global_opts #=> %w[--git-dir /path/to/git/dir]
92
+ #
93
+ # @return [Array<String>]
94
+ #
95
+ attr_reader :global_opts
96
+
97
+ # The logger to use for logging git commands and results
98
+ #
99
+ # @example
100
+ # logger = Logger.new(nil)
101
+ # cli = Git::CommandLine::Capturing.new({}, '/usr/bin/git', [], logger)
102
+ # cli.logger == logger #=> true
103
+ #
104
+ # @return [Logger]
105
+ #
106
+ attr_reader :logger
107
+
108
+ private
109
+
110
+ # Merge caller-supplied options into `defaults` and raise if any unknown keys are present
111
+ #
112
+ # @param defaults [Hash] the allowed keys and their default values (e.g. RUN_OPTION_DEFAULTS)
113
+ #
114
+ # @param options_hash [Hash] caller-supplied options
115
+ #
116
+ # @return [Hash] defaults with any supplied values overridden
117
+ #
118
+ # @raise [ArgumentError] if options_hash contains keys not present in defaults
119
+ #
120
+ # @api private
121
+ #
122
+ def merge_and_validate_options(defaults, options_hash)
123
+ merged = defaults.merge(options_hash)
124
+ extra = merged.keys - defaults.keys
125
+ raise ArgumentError, "Unknown options: #{extra.join(', ')}" if extra.any?
126
+
127
+ merged
128
+ end
129
+
130
+ # Merge the instance-level env with any per-call overrides in options_hash[:env]
131
+ #
132
+ # @param options_hash [Hash] options that may include an :env override
133
+ #
134
+ # @return [Hash{String => String, nil}]
135
+ #
136
+ # @api private
137
+ #
138
+ def merged_env(options_hash)
139
+ env.merge(options_hash[:env] || {})
140
+ end
141
+
142
+ # Yield to a block that calls ProcessExecuter and translate any ProcessExecuter
143
+ # errors to their ruby-git equivalents
144
+ #
145
+ # @return [Object] the return value of the block
146
+ #
147
+ # @raise [ArgumentError] in place of ProcessExecuter::ArgumentError
148
+ #
149
+ # @raise [Git::Error] in place of ProcessExecuter::SpawnError (binary not found or
150
+ # failed to launch)
151
+ #
152
+ # @raise [Git::ProcessIOError] in place of ProcessExecuter::ProcessIOError
153
+ #
154
+ # @raise [Git::ProcessIOError] when a timeout race causes Errno::ESRCH. On Ruby 4.0+,
155
+ # `Process.kill` raises `Errno::ESRCH` when the spawned process exits between the
156
+ # timeout firing and the kill signal being delivered. This is a race in
157
+ # process_executer's timeout handling and is semantically equivalent to a timeout.
158
+ #
159
+ # @api private
160
+ #
161
+ def run_process_executer
162
+ yield
163
+ rescue ProcessExecuter::ArgumentError => e
164
+ raise ::ArgumentError, e.message
165
+ rescue ProcessExecuter::SpawnError => e
166
+ raise Git::Error, e.message, cause: e.cause
167
+ rescue ProcessExecuter::ProcessIOError => e
168
+ raise Git::ProcessIOError, e.message, cause: e.cause
169
+ rescue Errno::ESRCH => e
170
+ # Ruby 4.0+: Process.kill raises Errno::ESRCH when the spawned process exits
171
+ # in the narrow window between the timeout firing and the kill signal being sent.
172
+ # This is a known race in process_executer's timeout handling.
173
+ raise Git::ProcessIOError, "Git process no longer exists (timeout race): #{e.message}", cause: e
174
+ end
175
+
176
+ # Build the git command line from the available sources to send to `Process.spawn`
177
+ #
178
+ # @param args [Array<String>] command-line arguments to append after global options
179
+ #
180
+ # @return [Array<String>]
181
+ #
182
+ # @raise [ArgumentError] if any element of args is itself an Array
183
+ #
184
+ # @api private
185
+ #
186
+ def build_git_cmd(args)
187
+ raise ArgumentError, 'The args array can not contain an array' if args.any?(Array)
188
+
189
+ [binary_path, *global_opts, *args].map(&:to_s)
190
+ end
191
+
192
+ # Log the result of a git command at info/debug level
193
+ #
194
+ # @param result [ProcessExecuter::Result] the raw process result
195
+ #
196
+ # @param command [Array<String>] the full command that was run
197
+ #
198
+ # @param processed_out [String] the post-processed stdout string
199
+ #
200
+ # @param processed_err [String] the post-processed stderr string
201
+ #
202
+ # @return [void]
203
+ #
204
+ # @api private
205
+ #
206
+ def log_result(result, command, processed_out, processed_err)
207
+ logger.info { "#{command} exited with status #{result}" }
208
+ logger.debug { "stdout:\n#{processed_out.inspect}\nstderr:\n#{processed_err.inspect}" }
209
+ end
210
+
211
+ # Build a {Git::CommandLine::Result} and raise on timeout, signal, or failure
212
+ #
213
+ # @param command [Array<String>] the full command that was run
214
+ #
215
+ # @param result [ProcessExecuter::Result] the raw process result
216
+ #
217
+ # @param processed_out [String] processed stdout string
218
+ #
219
+ # @param processed_err [String] processed stderr string
220
+ #
221
+ # @param timeout [Numeric, nil] the timeout value (for error context)
222
+ #
223
+ # @param raise_on_failure [Boolean] whether to raise on non-zero exit status
224
+ #
225
+ # @return [Git::CommandLine::Result]
226
+ #
227
+ # @raise [Git::TimeoutError] if the command timed out
228
+ #
229
+ # @raise [Git::SignaledError] if the command was terminated by a signal
230
+ #
231
+ # @raise [Git::FailedError] if the command failed and raise_on_failure is true
232
+ #
233
+ # @api private
234
+ #
235
+ # rubocop:disable-next Metrics/ParameterLists
236
+ def command_line_result(command, result, processed_out, processed_err, timeout, raise_on_failure)
237
+ Git::CommandLine::Result.new(command, result, processed_out, processed_err).tap do |processed_result|
238
+ raise Git::TimeoutError.new(processed_result, timeout) if result.timed_out?
239
+
240
+ raise Git::SignaledError, processed_result if result.signaled?
241
+
242
+ raise Git::FailedError, processed_result if raise_on_failure && !result.success?
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,308 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git/command_line/base'
4
+ require 'git/encoding_utils'
5
+
6
+ module Git
7
+ module CommandLine
8
+ # Executes a git command and captures both stdout and stderr in memory
9
+ #
10
+ # {Git::CommandLine::Capturing} is the buffering strategy: it calls
11
+ # `ProcessExecuter.run_with_capture`, which reads all subprocess output into
12
+ # `String` objects before returning. Use this class (via
13
+ # {Git::ExecutionContext#command_capturing}) for the vast majority of git subcommands whose
14
+ # output fits comfortably in memory.
15
+ #
16
+ # {Git::CommandLine::Streaming} is the complementary strategy for commands
17
+ # (such as `cat-file -p <blob>`) whose stdout may be too large to buffer.
18
+ #
19
+ # @example
20
+ # capturing = Git::CommandLine::Capturing.new(
21
+ # {}, '/usr/bin/git', %w[--git-dir /repo/.git], Logger.new($stdout)
22
+ # )
23
+ # result = capturing.run('log', '--oneline', '-5')
24
+ # result.stdout # => "abc1234 Initial commit\n..."
25
+ # result.stderr # => ""
26
+ #
27
+ # @see Git::ExecutionContext#command_capturing
28
+ #
29
+ # @see Git::CommandLine::Streaming
30
+ #
31
+ # @api private
32
+ #
33
+ class Capturing < 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
+ normalize: false,
46
+ chomp: false,
47
+ merge: false
48
+ }.freeze
49
+
50
+ # Execute a git command, capture stdout and stderr, and return the result
51
+ #
52
+ # Non-option command-line arguments to pass to git. If you collect the
53
+ # arguments in an array, splat the array into the parameter list.
54
+ #
55
+ # NORMALIZATION
56
+ #
57
+ # The command output is returned as a Unicode string containing the binary
58
+ # output from the command. If the binary output is not valid UTF-8, the
59
+ # output will cause problems because the encoding will be invalid.
60
+ #
61
+ # Normalization is a process that tries to convert the binary output to a
62
+ # valid UTF-8 string. It uses the `rchardet` gem to detect the encoding of
63
+ # the binary output and then converts it to UTF-8.
64
+ #
65
+ # Normalization is not enabled by default. Pass `normalize: true` to enable
66
+ # it. When enabled, normalization is applied to both stdout and stderr in
67
+ # the returned result object, regardless of the `out:` or `err:` options.
68
+ # Only the captured in-memory strings are normalized; any external IO you
69
+ # provide will receive the raw subprocess output.
70
+ #
71
+ # @example Run a command and return the output
72
+ # result = capturing.run('version')
73
+ # result.stdout #=> "git version 2.39.1\n"
74
+ #
75
+ # @example The args array should be splatted into the parameter list
76
+ # args = %w[log -n 1 --oneline]
77
+ # result = capturing.run(*args)
78
+ # result.stdout #=> "f5baa11 beginning of Ruby/Git project\n"
79
+ #
80
+ # @example Run a command and return the chomped output
81
+ # result = capturing.run('version', chomp: true)
82
+ # result.stdout #=> "git version 2.39.1"
83
+ #
84
+ # @example Run a command without normalizing the output
85
+ # capturing.run('version', normalize: false) #=> "git version 2.39.1\n"
86
+ #
87
+ # @example Capture stdout in a temporary file
88
+ # require 'tempfile'
89
+ # Tempfile.create('git') do |file|
90
+ # capturing.run('version', out: file)
91
+ # file.rewind
92
+ # file.read #=> "git version 2.39.1\n"
93
+ # end
94
+ #
95
+ # @example Capture stderr in a StringIO object
96
+ # require 'stringio'
97
+ # stderr = StringIO.new
98
+ # begin
99
+ # capturing.run('log', 'nonexistent-branch', err: stderr)
100
+ # rescue Git::FailedError => e
101
+ # stderr.string #=> "unknown revision or path not in the working tree.\n"
102
+ # end
103
+ #
104
+ # @param options_hash [Hash] the options to pass to the command
105
+ #
106
+ # @option options_hash [IO, nil] :in the IO object to use as stdin for the
107
+ # command, or nil to inherit the parent process stdin. Must be a real IO
108
+ # object with a file descriptor (not StringIO).
109
+ #
110
+ # @option options_hash [#write, nil] :out the object to write stdout to, or
111
+ # nil to capture stdout in the returned result.
112
+ #
113
+ # If this is a `StringIO` object, `stdout_writer.string` will be returned.
114
+ #
115
+ # In general, only specify a `stdout_writer` when you want to redirect
116
+ # stdout to a file or other `#write`-responding object. The default
117
+ # behaviour returns the command output.
118
+ #
119
+ # @option options_hash [#write, nil] :err the object to write stderr to, or
120
+ # nil to capture stderr in the returned result.
121
+ #
122
+ # @option options_hash [Boolean] :normalize (false) whether to normalize the
123
+ # encoding of stdout and stderr output
124
+ #
125
+ # @option options_hash [Boolean] :chomp (false) whether to chomp both stdout
126
+ # and stderr output
127
+ #
128
+ # @option options_hash [Boolean] :merge (false) whether to merge stdout and
129
+ # stderr in the returned string
130
+ #
131
+ # @option options_hash [String, nil] :chdir the directory to run the command in
132
+ #
133
+ # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for
134
+ # the command to complete. Zero means no timeout. A timeout kills the
135
+ # process via `SIGKILL` and raises {Git::TimeoutError}.
136
+ #
137
+ # @option options_hash [Boolean] :raise_on_failure (true) whether to raise
138
+ # {Git::FailedError} on non-zero exit status.
139
+ # {Git::TimeoutError} and {Git::SignaledError} are always raised regardless.
140
+ #
141
+ # @option options_hash [Hash] :env ({}) additional environment variable
142
+ # overrides for this command. String keys map to String values (to set) or
143
+ # `nil` (to unset).
144
+ #
145
+ # @return [Git::CommandLine::Result] the result of the command
146
+ #
147
+ # @raise [ArgumentError] if `args` contains an array or an unknown option is
148
+ # passed
149
+ #
150
+ # @raise [Git::SignaledError] if the command was terminated by an uncaught signal
151
+ #
152
+ # @raise [Git::FailedError] if the command returned a non-zero exit status
153
+ #
154
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting
155
+ # subprocess output, or (Ruby 4.0+) if a timeout-handling race causes
156
+ # `Errno::ESRCH` when the spawned process exits between the timeout
157
+ # firing and the kill signal being delivered
158
+ #
159
+ # @raise [Git::TimeoutError] if the command times out
160
+ #
161
+ def run(*, **options_hash)
162
+ options = merge_and_validate_options(RUN_OPTION_DEFAULTS, options_hash)
163
+
164
+ result = execute(*, **options)
165
+ process_result(result, options)
166
+ end
167
+
168
+ private
169
+
170
+ # Execute the git command with the given arguments and options, capturing the output
171
+ #
172
+ # @param args [Array<String>] the git command arguments
173
+ #
174
+ # @param options_hash [Hash] the merged run options forwarded from {#run}
175
+ #
176
+ # Only the keys consumed by this method are listed below; remaining keys
177
+ # from {RUN_OPTION_DEFAULTS} (`:normalize`, `:chomp`, `:raise_on_failure`)
178
+ # are present in the hash but are not used here.
179
+ #
180
+ # @option options_hash [IO, nil] :in stdin IO object for the subprocess
181
+ #
182
+ # @option options_hash [#write, nil] :out stdout redirect target
183
+ #
184
+ # @option options_hash [#write, nil] :err stderr redirect target
185
+ #
186
+ # @option options_hash [String, nil] :chdir working directory for the subprocess
187
+ #
188
+ # @option options_hash [Numeric, nil] :timeout execution timeout in seconds
189
+ #
190
+ # @option options_hash [Boolean] :merge (false) merge stdout into stderr
191
+ #
192
+ # @option options_hash [Hash] :env ({}) environment variable overrides
193
+ #
194
+ # @return [ProcessExecuter::ResultWithCapture] the process result with captured output
195
+ #
196
+ # @api private
197
+ def execute(*args, **options_hash)
198
+ git_cmd = build_git_cmd(args)
199
+ options = execution_options(**options_hash)
200
+ run_process_executer do
201
+ ProcessExecuter.run_with_capture(merged_env(options_hash), *git_cmd, **options)
202
+ end
203
+ end
204
+
205
+ # Build the ProcessExecuter options hash for a capturing run
206
+ #
207
+ # @param options_hash [Hash] the merged run options forwarded from {#run}
208
+ #
209
+ # @option options_hash [IO, nil] :in stdin IO object for the subprocess
210
+ #
211
+ # @option options_hash [#write, nil] :out stdout redirect target
212
+ #
213
+ # @option options_hash [#write, nil] :err stderr redirect target
214
+ #
215
+ # @option options_hash [String, nil] :chdir working directory for the subprocess
216
+ #
217
+ # @option options_hash [Numeric, nil] :timeout execution timeout in seconds
218
+ #
219
+ # @option options_hash [Boolean] :merge (false) merge stdout into stderr
220
+ #
221
+ # @return [Hash]
222
+ #
223
+ # @api private
224
+ def execution_options(**options_hash)
225
+ chdir = options_hash[:chdir] || :not_set
226
+ timeout_after = options_hash[:timeout]
227
+ merge_output = options_hash[:merge] || false
228
+
229
+ { chdir:, timeout_after:, merge_output:, raise_errors: false }.tap do |options|
230
+ redirect_options(options_hash).each { |k, v| options[k] = v }
231
+ end
232
+ end
233
+
234
+ # Extract non-nil redirect options (`:in`, `:out`, `:err`) from `options`
235
+ #
236
+ # @param options [Hash] the options hash containing potential redirect options
237
+ #
238
+ # @option options [IO, nil] :in the input IO stream
239
+ #
240
+ # @option options [#write, nil] :out the output IO stream
241
+ #
242
+ # @option options [#write, nil] :err the error IO stream
243
+ #
244
+ # @return [Hash] the non-nil redirect options
245
+ #
246
+ # @api private
247
+ def redirect_options(options)
248
+ %i[in out err].filter_map do |key|
249
+ val = options[key]
250
+ [key, val] unless val.nil?
251
+ end.to_h
252
+ end
253
+
254
+ # Post-process and return the stdout/stderr strings from the captured result,
255
+ # then log and raise on failure if required.
256
+ #
257
+ # @param result [ProcessExecuter::ResultWithCapture] the raw result
258
+ #
259
+ # @param options [Hash] the merged run options forwarded from {#run}
260
+ #
261
+ # @option options [Boolean] :normalize (false) normalize encoding of captured output
262
+ #
263
+ # @option options [Boolean] :chomp (false) chomp trailing newlines from captured output
264
+ #
265
+ # @option options [Numeric, nil] :timeout execution timeout used to construct the result
266
+ #
267
+ # @option options [Boolean] :raise_on_failure (true) raise {Git::FailedError} on non-zero exit
268
+ #
269
+ # @return [Git::CommandLine::Result]
270
+ #
271
+ # @raise [Git::FailedError] if the command failed and raise_on_failure is true
272
+ #
273
+ # @raise [Git::SignaledError] if the command was signaled
274
+ #
275
+ # @raise [Git::TimeoutError] if the command timed out
276
+ #
277
+ # @api private
278
+ def process_result(result, options)
279
+ command = result.command
280
+ processed_out, processed_err = post_process_output(result, options[:normalize], options[:chomp])
281
+ log_result(result, command, processed_out, processed_err)
282
+ command_line_result(
283
+ command, result, processed_out, processed_err, options[:timeout], options[:raise_on_failure]
284
+ )
285
+ end
286
+
287
+ # Normalize and/or chomp the raw stdout and stderr strings.
288
+ #
289
+ # @param result [ProcessExecuter::ResultWithCapture] the raw result
290
+ #
291
+ # @param normalize [Boolean]
292
+ #
293
+ # @param chomp [Boolean]
294
+ #
295
+ # @return [Array<String>] two-element array: [processed_stdout, processed_stderr]
296
+ #
297
+ # @api private
298
+ def post_process_output(result, normalize, chomp)
299
+ [result.stdout, result.stderr].map do |raw_output|
300
+ output = raw_output.dup
301
+ output = output.lines.map { |l| Git::EncodingUtils.normalize_encoding(l) }.join if normalize
302
+ output.chomp! if chomp
303
+ output
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,88 @@
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
+ # The git command that was executed
39
+ #
40
+ # @example
41
+ # git_cmd = %w[git version]
42
+ # result = Git::CommandLine::Result.new(git_cmd, nil, '', '')
43
+ # result.git_cmd #=> ["git", "version"]
44
+ #
45
+ # @return [Array<String>]
46
+ #
47
+ attr_reader :git_cmd
48
+
49
+ # The process result object returned by ProcessExecuter
50
+ #
51
+ # In practice this is a `ProcessExecuter::ResultWithCapture` (from
52
+ # {Git::CommandLine::Capturing}) or a `ProcessExecuter::Result` (from
53
+ # {Git::CommandLine::Streaming}). Both respond to `success?`, `timed_out?`,
54
+ # and `signaled?`.
55
+ #
56
+ # @example
57
+ # status = instance_double(ProcessExecuter::Result, success?: true)
58
+ # result = Git::CommandLine::Result.new(%w[git version], status, '', '')
59
+ # result.status == status #=> true
60
+ #
61
+ # @return [ProcessExecuter::Result]
62
+ #
63
+ attr_reader :status
64
+
65
+ # The output of the process
66
+ #
67
+ # @example
68
+ # stdout = "git version 2.39.1\n"
69
+ # result = Git::CommandLine::Result.new([], nil, stdout, '')
70
+ # result.stdout #=> "git version 2.39.1\n"
71
+ #
72
+ # @return [String]
73
+ #
74
+ attr_reader :stdout
75
+
76
+ # The error output of the process
77
+ #
78
+ # @example
79
+ # stderr = "Tag not found\n"
80
+ # result = Git::CommandLine::Result.new([], nil, '', stderr)
81
+ # result.stderr #=> "Tag not found\n"
82
+ #
83
+ # @return [String]
84
+ #
85
+ attr_reader :stderr
86
+ end
87
+ end
88
+ end