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,559 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'git/command_line'
5
+
6
+ module Git
7
+ # Base class for execution contexts that run git commands
8
+ #
9
+ # An execution context bundles three concerns that together describe *how* and
10
+ # *where* a git command runs:
11
+ #
12
+ # 1. **Repository scope** — the public accessors `git_dir`, `git_work_dir`,
13
+ # `git_index_file`, and `git_ssh` identify which repository git targets and
14
+ # which SSH wrapper to use. Their values are translated into `GIT_*` environment
15
+ # variable overrides by the private `env_overrides` method. A `nil` value
16
+ # unsets the variable (see `Process.spawn` semantics).
17
+ #
18
+ # 2. **CLI global options** — the private `global_opts` method returns the array
19
+ # of git flags prepended to every invocation: `--git-dir` / `--work-tree` when
20
+ # those attributes are set, plus the static options in {STATIC_GLOBAL_OPTS} that
21
+ # ensure deterministic, script-friendly output.
22
+ #
23
+ # 3. **Execution defaults** — {COMMAND_CAPTURING_ARG_DEFAULTS} and
24
+ # {COMMAND_STREAMING_ARG_DEFAULTS} define the default values for I/O, encoding,
25
+ # and behavioral options (`in:`, `out:`, `normalize:`, `timeout:`, etc.) accepted
26
+ # by {#command_capturing} and {#command_streaming}.
27
+ #
28
+ # Subclasses override the repository-scope accessors to supply context-specific
29
+ # values. The `env_overrides` and `global_opts` methods are implemented here and
30
+ # call those accessors, so subclasses do not need to override them directly.
31
+ #
32
+ # Concrete subclasses:
33
+ # - {Git::ExecutionContext::Repository} — for repository-bound commands (`add`, `commit`, …)
34
+ # - {Git::ExecutionContext::Global} — for commands that do not require an existing repository
35
+ # (`init`, `clone`, `version`)
36
+ #
37
+ # @example Using a concrete subclass
38
+ # context = Git::ExecutionContext::Global.new(binary_path: '/usr/local/bin/git2')
39
+ # context.binary_path #=> "/usr/local/bin/git2"
40
+ #
41
+ # @api private
42
+ #
43
+ class ExecutionContext
44
+ # Default keyword arguments accepted by {#command_capturing}.
45
+ #
46
+ # Derived from {Git::CommandLine::Capturing::RUN_OPTION_DEFAULTS} with two
47
+ # overrides: `normalize: true` and `chomp: true` so callers receive clean
48
+ # UTF-8 strings by default. New options added to the CommandLine layer are
49
+ # automatically accepted here without requiring a coordinated edit.
50
+ #
51
+ # `timeout: nil` is intentional — the global timeout from {Git.config} is
52
+ # applied at call-time so that changes to the config are respected.
53
+ #
54
+ COMMAND_CAPTURING_ARG_DEFAULTS =
55
+ Git::CommandLine::Capturing::RUN_OPTION_DEFAULTS
56
+ .merge(normalize: true, chomp: true)
57
+ .freeze
58
+
59
+ # Default keyword arguments accepted by {#command_streaming}.
60
+ #
61
+ # Identical to {Git::CommandLine::Streaming::RUN_OPTION_DEFAULTS}. Defined
62
+ # here so callers interact with a stable constant on this class, and so that
63
+ # new options added to the CommandLine layer are automatically accepted.
64
+ #
65
+ COMMAND_STREAMING_ARG_DEFAULTS =
66
+ Git::CommandLine::Streaming::RUN_OPTION_DEFAULTS.dup.freeze
67
+
68
+ # Static git global options applied to every invocation.
69
+ #
70
+ # These ensure deterministic, script-friendly output regardless of the
71
+ # user's local git configuration.
72
+ #
73
+ STATIC_GLOBAL_OPTS = %w[
74
+ -c core.quotePath=true
75
+ -c core.editor=false
76
+ -c color.ui=false
77
+ -c color.advice=false
78
+ -c color.diff=false
79
+ -c color.grep=false
80
+ -c color.push=false
81
+ -c color.remote=false
82
+ -c color.showBranch=false
83
+ -c color.status=false
84
+ -c color.transport=false
85
+ ].freeze
86
+
87
+ # Creates a new execution context
88
+ #
89
+ # @param binary_path [String, :use_global_config] path to the git binary
90
+ #
91
+ # Give `:use_global_config` (the default) to use `Git::Config.instance.binary_path`.
92
+ #
93
+ # Passing `nil` raises `ArgumentError` — there is no "unset the
94
+ # binary" semantic.
95
+ #
96
+ # @param git_ssh [String, nil, :use_global_config] the SSH wrapper path
97
+ #
98
+ # Give `nil` to unset `GIT_SSH`, or `:use_global_config` (default)
99
+ # to use `Git::Config.instance.git_ssh`.
100
+ #
101
+ # @param logger [Logger, nil] the logger to use in the CommandLine layer
102
+ #
103
+ # Give `nil` to use a null logger (`Logger.new(nil)`).
104
+ #
105
+ # @raise [NotImplementedError] if called directly on {Git::ExecutionContext}
106
+ # rather than a subclass
107
+ #
108
+ # @raise [ArgumentError] if `binary_path` is `nil`
109
+ #
110
+ def initialize(binary_path: :use_global_config, git_ssh: :use_global_config, logger: nil)
111
+ if instance_of?(Git::ExecutionContext)
112
+ raise NotImplementedError, 'Git::ExecutionContext is an abstract base class'
113
+ end
114
+ raise ArgumentError, 'binary_path must not be nil' if binary_path.nil?
115
+
116
+ @binary_path = binary_path
117
+ @git_ssh = git_ssh
118
+ @logger = logger || Logger.new(nil)
119
+ end
120
+
121
+ # Returns the `GIT_DIR` path for this context
122
+ #
123
+ # `nil` means `GIT_DIR` will be explicitly **unset** in the child process
124
+ # (per `Process.spawn` semantics — unset is not the same as inherited).
125
+ # Subclasses override this to supply a repository-specific path.
126
+ #
127
+ # @example Base class returns nil; subclasses return the actual path
128
+ # context = Git::ExecutionContext::Global.new
129
+ # context.git_dir #=> nil
130
+ #
131
+ # @return [String, nil] the `GIT_DIR` path, or `nil` to unset the variable
132
+ #
133
+ def git_dir = nil
134
+
135
+ # Returns the `GIT_WORK_TREE` path for this context
136
+ #
137
+ # `nil` means `GIT_WORK_TREE` will be explicitly **unset** in the child process.
138
+ #
139
+ # @example Base class returns nil; subclasses return the actual path
140
+ # context = Git::ExecutionContext::Global.new
141
+ # context.git_work_dir #=> nil
142
+ #
143
+ # @return [String, nil] the `GIT_WORK_TREE` path, or `nil` to unset the variable
144
+ #
145
+ def git_work_dir = nil
146
+
147
+ # Returns the `GIT_INDEX_FILE` path for this context
148
+ #
149
+ # `nil` means `GIT_INDEX_FILE` will be explicitly **unset** in the child process.
150
+ #
151
+ # @example Base class returns nil; subclasses return the actual path
152
+ # context = Git::ExecutionContext::Global.new
153
+ # context.git_index_file #=> nil
154
+ #
155
+ # @return [String, nil] the `GIT_INDEX_FILE` path, or `nil` to unset the variable
156
+ #
157
+ def git_index_file = nil
158
+
159
+ # Returns the resolved git binary path for this context
160
+ #
161
+ # `:use_global_config` is resolved to `Git::Config.instance.binary_path` each time a
162
+ # command method is called, so runtime changes to
163
+ # `Git.configure { |c| c.binary_path = ... }`
164
+ # are reflected per command invocation.
165
+ #
166
+ # @example With the default sentinel (resolves from Git::Config.instance at call-time)
167
+ # context = Git::ExecutionContext::Global.new
168
+ # context.binary_path #=> "git"
169
+ #
170
+ # @example With an explicit path
171
+ # context = Git::ExecutionContext::Global.new(binary_path: '/usr/local/bin/git2')
172
+ # context.binary_path #=> "/usr/local/bin/git2"
173
+ #
174
+ # @return [String] the resolved git binary path
175
+ #
176
+ def binary_path
177
+ return Git::Config.instance.binary_path if @binary_path == :use_global_config
178
+
179
+ @binary_path
180
+ end
181
+
182
+ # Returns the resolved `GIT_SSH` wrapper path for this context
183
+ #
184
+ # `:use_global_config` is resolved to `Git::Config.instance.git_ssh` each time a
185
+ # command method is called, so runtime changes to
186
+ # `Git.configure { |c| c.git_ssh = ... }`
187
+ # are reflected per command invocation. `nil` means the variable will be
188
+ # explicitly unset.
189
+ #
190
+ # @example With the default sentinel (resolves from Git::Config.instance at call-time)
191
+ # context = Git::ExecutionContext::Global.new
192
+ # context.git_ssh #=> nil
193
+ #
194
+ # @example With an explicit path
195
+ # context = Git::ExecutionContext::Global.new(git_ssh: '/usr/bin/ssh-wrapper')
196
+ # context.git_ssh #=> "/usr/bin/ssh-wrapper"
197
+ #
198
+ # @return [String, nil] the resolved `GIT_SSH` wrapper path, or `nil` to unset
199
+ #
200
+ def git_ssh
201
+ return Git::Config.instance.git_ssh if @git_ssh == :use_global_config
202
+
203
+ @git_ssh
204
+ end
205
+
206
+ # Returns the logger used by this context
207
+ #
208
+ # @example
209
+ # context = Git::ExecutionContext::Repository.new(git_dir: '/repo/.git', logger: my_logger)
210
+ # context.logger #=> my_logger
211
+ #
212
+ # @return [Logger] the logger instance; never `nil`
213
+ #
214
+ # @api private
215
+ #
216
+ attr_reader :logger
217
+
218
+ # Runs a git command and returns the result
219
+ #
220
+ # By default, raises {Git::FailedError} if the command exits with a non-zero
221
+ # status. Pass `raise_on_failure: false` to suppress this behavior.
222
+ #
223
+ # @overload command_capturing(*args, **options_hash)
224
+ #
225
+ # Args should exclude the 'git' command itself and global options. Remember to
226
+ # splat the arguments if given as an array.
227
+ #
228
+ # @example Run git log
229
+ # result = command_capturing('log', '--pretty=oneline')
230
+ # result.stdout #=> "abc123 First commit\ndef456 Second commit\n"
231
+ #
232
+ # @example Using an array of arguments
233
+ # args = ['log', '--pretty=oneline']
234
+ # result = command_capturing(*args)
235
+ #
236
+ # @example Suppress raising on failure
237
+ # result = command_capturing('show', 'nonexistent', raise_on_failure: false)
238
+ # result.status.success? #=> false
239
+ #
240
+ # @param args [Array<String>] the command and its arguments
241
+ #
242
+ # @return [Git::CommandLine::Result] the result of the command
243
+ #
244
+ # @param options_hash [Hash] the options to pass to the command
245
+ #
246
+ # @option options_hash [IO, nil] :in the IO object to use as stdin, or nil to
247
+ # inherit the parent process stdin
248
+ #
249
+ # Must be a real IO object with a file descriptor.
250
+ #
251
+ # @option options_hash [IO, String, #write, nil] :out the destination for
252
+ # captured stdout
253
+ #
254
+ # @option options_hash [IO, String, #write, nil] :err the destination for
255
+ # captured stderr
256
+ #
257
+ # @option options_hash [Boolean, nil] :normalize (true) normalize the output
258
+ # encoding to UTF-8
259
+ #
260
+ # @option options_hash [Boolean, nil] :chomp (true) remove trailing newlines
261
+ # from the output
262
+ #
263
+ # @option options_hash [Boolean, nil] :merge (false) merge stdout and stderr
264
+ # into a single output
265
+ #
266
+ # @option options_hash [String, nil] :chdir the directory to run the command in
267
+ #
268
+ # @option options_hash [Hash] :env additional environment variable overrides
269
+ # for this command
270
+ #
271
+ # @option options_hash [Boolean, nil] :raise_on_failure (true) whether to raise on
272
+ # non-zero exit
273
+ #
274
+ # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for
275
+ # the command to complete
276
+ #
277
+ # If timeout is nil, the global timeout from {Git::Config} is used.
278
+ #
279
+ # If timeout is zero, the timeout will not be enforced.
280
+ #
281
+ # If the command times out, it is killed via a `SIGKILL` signal and
282
+ # `Git::TimeoutError` is raised.
283
+ #
284
+ # If the command does not respond to SIGKILL, it will hang this method.
285
+ #
286
+ # @raise [ArgumentError] if an unknown option is passed
287
+ #
288
+ # @raise [Git::FailedError] if the command failed (when raise_on_failure is
289
+ # true)
290
+ #
291
+ # @raise [Git::SignaledError] if the command was signaled
292
+ #
293
+ # @raise [Git::TimeoutError] if the command times out
294
+ #
295
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting
296
+ # subprocess output
297
+ #
298
+ # The exception's `result` attribute is a {Git::CommandLine::Result} which will
299
+ # contain the result of the command including the exit status, stdout, and stderr.
300
+ #
301
+ # @note Individual command classes (under {Git::Commands}) can selectively expose
302
+ # `:timeout` and `:env` and other options to their callers by declaring them as
303
+ # execution options in their Arguments DSL definition and forwarding them to
304
+ # this method. See {Git::Commands::Clone#call} for an example of a command that
305
+ # exposes `:timeout`.
306
+ #
307
+ # @see Git::CommandLine::Capturing#run
308
+ #
309
+ def command_capturing(*, **options_hash)
310
+ options_hash = COMMAND_CAPTURING_ARG_DEFAULTS.merge(options_hash)
311
+ options_hash[:timeout] ||= Git.config.timeout
312
+
313
+ extra_options = options_hash.keys - COMMAND_CAPTURING_ARG_DEFAULTS.keys
314
+ raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
315
+
316
+ env = options_hash.delete(:env)
317
+ raise_on_failure = options_hash.delete(:raise_on_failure)
318
+ command_line_capturing.run(*, raise_on_failure: raise_on_failure, env: env, **options_hash)
319
+ end
320
+
321
+ # Runs a git command using the streaming (non-capturing) execution path
322
+ #
323
+ # Unlike {#command_capturing}, stdout is NOT buffered in memory. It is
324
+ # written only to the IO object provided via the `out:` option. Stderr is
325
+ # captured internally via a StringIO for error diagnostics.
326
+ #
327
+ # Use this entry point when you want to stream large output (e.g. blob
328
+ # content from cat-file) without creating memory pressure.
329
+ #
330
+ # @overload command_streaming(*args, **options_hash)
331
+ #
332
+ # Streams a git command's output to the provided IO object
333
+ #
334
+ # @example Stream blob content to a file
335
+ # File.open('blob.bin', 'wb') do |f|
336
+ # command_streaming('cat-file', 'blob', 'HEAD:large_file.bin', out: f)
337
+ # end
338
+ #
339
+ # @param args [Array<String>] the git command and its arguments
340
+ #
341
+ # @return [Git::CommandLine::Result] the result of the command
342
+ #
343
+ # @param options_hash [Hash] the options to pass to the command
344
+ #
345
+ # @option options_hash [IO, nil] :in the IO object to use as stdin, or nil to
346
+ # inherit the parent process stdin
347
+ #
348
+ # Must be a real IO object with a file descriptor.
349
+ #
350
+ # @option options_hash [#write, nil] :out destination for streamed stdout
351
+ #
352
+ # @option options_hash [#write, nil] :err an optional additional destination
353
+ # to receive stderr output in real time
354
+ #
355
+ # Stderr is always captured internally; when `err:` is supplied, writes are
356
+ # teed to both the internal buffer and this destination. `result.stderr`
357
+ # always reflects the internal capture.
358
+ #
359
+ # @option options_hash [String, nil] :chdir the directory to run the command in
360
+ #
361
+ # @option options_hash [Hash] :env additional environment variable overrides
362
+ # for this command
363
+ #
364
+ # @option options_hash [Boolean, nil] :raise_on_failure (true) whether to raise on
365
+ # non-zero exit
366
+ #
367
+ # @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for
368
+ # the command to complete
369
+ #
370
+ # If timeout is nil, the global timeout from {Git::Config} is used.
371
+ #
372
+ # If timeout is zero, the timeout will not be enforced.
373
+ #
374
+ # If the command times out, it is killed via a `SIGKILL` signal and
375
+ # `Git::TimeoutError` is raised.
376
+ #
377
+ # If the command does not respond to SIGKILL, it will hang this method.
378
+ #
379
+ # `result.stdout` will always be `''` — stdout was streamed to `out:`.
380
+ #
381
+ # `result.stderr` contains any stderr output captured for diagnostics.
382
+ #
383
+ # @raise [ArgumentError] if an unknown option is passed
384
+ #
385
+ # @raise [Git::FailedError] if the command failed (when raise_on_failure is true)
386
+ #
387
+ # @raise [Git::SignaledError] if the command was signaled
388
+ #
389
+ # @raise [Git::TimeoutError] if the command times out
390
+ #
391
+ # @raise [Git::ProcessIOError] if an exception was raised while collecting
392
+ # subprocess output
393
+ #
394
+ # @see Git::CommandLine::Streaming#run
395
+ #
396
+ def command_streaming(*, **options_hash)
397
+ options_hash = COMMAND_STREAMING_ARG_DEFAULTS.merge(options_hash)
398
+ options_hash[:timeout] ||= Git.config.timeout
399
+
400
+ extra_options = options_hash.keys - COMMAND_STREAMING_ARG_DEFAULTS.keys
401
+ raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
402
+
403
+ env = options_hash.delete(:env)
404
+ raise_on_failure = options_hash.delete(:raise_on_failure)
405
+ command_line_streaming.run(*, raise_on_failure: raise_on_failure, env: env, **options_hash)
406
+ end
407
+
408
+ # Returns the installed git version
409
+ #
410
+ # The result is memoized per instance. Accepts an optional timeout used
411
+ # only when the version has not yet been fetched for this context.
412
+ #
413
+ # @param timeout [Numeric, nil] seconds to wait for `git version`; `nil`
414
+ # falls back to the global {Git::Config} timeout; `0` disables the timeout
415
+ # entirely (the command runs until it completes or the process is killed)
416
+ #
417
+ # @return [Git::Version] the installed git version
418
+ #
419
+ # @raise [Git::UnexpectedResultError] if the version string cannot be parsed
420
+ #
421
+ def git_version(timeout: nil)
422
+ @git_version ||= begin
423
+ call_opts = timeout.nil? ? {} : { timeout: timeout }
424
+ Git::Version.parse(Git::Commands::Version.new(self).call(**call_opts).stdout)
425
+ end
426
+ end
427
+
428
+ # Returns a Hash of environment variable overrides for this context
429
+ #
430
+ # Builds the standard git environment from the public accessor methods
431
+ # ({#git_dir}, {#git_work_dir}, {#git_index_file}, {#git_ssh}), then
432
+ # merges any per-call `additional_overrides` on top.
433
+ #
434
+ # Per `Process.spawn` semantics, a value of `nil` unsets the variable.
435
+ #
436
+ # @param additional_overrides [Hash{String => String, nil}] per-call
437
+ # environment overrides keyed by variable name
438
+ #
439
+ # Pass string environment variable names. Ruby preserves string keys when
440
+ # callers forward a string-keyed Hash with `**`.
441
+ #
442
+ # @option additional_overrides [String, nil] :"ENV_VAR" value for an arbitrary
443
+ # environment variable name
444
+ #
445
+ # @return [Hash{String => String, nil}] the merged environment variable overrides
446
+ #
447
+ # @api private
448
+ #
449
+ def env_overrides(**additional_overrides)
450
+ {
451
+ 'GIT_DIR' => git_dir,
452
+ 'GIT_WORK_TREE' => git_work_dir,
453
+ 'GIT_INDEX_FILE' => git_index_file,
454
+ 'GIT_SSH' => git_ssh,
455
+ 'GIT_EDITOR' => 'true',
456
+ # Pin the locale so git's behavior does not depend on the user's environment.
457
+ # Added for issue #753, where a German user's `git branch` output
458
+ # ("* (HEAD losgelöst bei origin/25.1)") broke branch parsing.
459
+ #
460
+ # The pin has two halves, and the load-bearing one is not the obvious one:
461
+ #
462
+ # - Messages: keeps git's human-readable text in English. This is now only
463
+ # defense-in-depth — lib/ reads `--format=%(refname:short)` rather than
464
+ # git's prose, and matches no English git message.
465
+ # - Ctype: makes git's regex engine match *characters* rather than *bytes*.
466
+ # This is the half that matters. Under a C ctype, `grep`, `log`, and the
467
+ # `config` value regexes silently return wrong answers — with exit status
468
+ # zero — for any pattern whose metacharacters span non-ASCII text.
469
+ #
470
+ # So do not "simplify" this to `C`: that yields English messages and a broken
471
+ # ctype, which is the worst of both. A pinned locale the host does not have
472
+ # degrades to that same C ctype, which is why each branch below has to name a
473
+ # locale that actually exists on the platform it applies to:
474
+ #
475
+ # - Non-Darwin: `C.UTF-8`. Stock Debian, Ubuntu, and RHEL images generate no
476
+ # `en_US.UTF-8`, so pinning it there produced exactly the silent breakage
477
+ # above. (musl ignores the locale name for ctype, so Alpine is UTF-8 either
478
+ # way.)
479
+ # - Darwin: `en_US.UTF-8`, which every macOS release ships. `C.UTF-8` did not
480
+ # arrive until macOS 15, so macOS 11–14 — including Intel Macs that are
481
+ # hardware-capped below 15 — would break under it. Delete this branch once
482
+ # macOS 14 and earlier are out of support.
483
+ #
484
+ # Windows takes the non-Darwin branch, where the value makes no difference:
485
+ # Git for Windows folds case and runs PCRE in UTF mode under every value, and
486
+ # matches bytes for `.` and POSIX classes under every value — measured on git
487
+ # 2.55.0 against `en_US.UTF-8`, `C.UTF-8`, `C`, and no pin at all.
488
+ #
489
+ # RHEL 7 (glibc 2.17) has neither locale and gets a C ctype whatever is pinned.
490
+ # It is EOL, and there is deliberately no public override for this value.
491
+ 'LC_ALL' => darwin_platform? ? 'en_US.UTF-8' : 'C.UTF-8'
492
+ }.merge(additional_overrides)
493
+ end
494
+
495
+ private
496
+
497
+ # Whether this process is running on macOS
498
+ #
499
+ # Checks `RUBY_DESCRIPTION` as well as `RUBY_PLATFORM` because JRuby reports
500
+ # `RUBY_PLATFORM` as `"java"` on every host and records the real platform only in
501
+ # `RUBY_DESCRIPTION` (as, for example, `"... [arm64-darwin]"`). Testing
502
+ # `RUBY_PLATFORM` alone would put JRuby on macOS onto the non-Darwin branch of the
503
+ # `LC_ALL` pin, which is the one platform pairing that branch must never be given —
504
+ # see {#env_overrides}.
505
+ #
506
+ # Test the platform plainly rather than sniffing the Darwin version: `RUBY_PLATFORM`
507
+ # records the version Ruby was *built* against, so a Ruby built on macOS 14 still
508
+ # reports `darwin23` when run on macOS 15.
509
+ #
510
+ # @return [Boolean] true if this process is running on macOS
511
+ #
512
+ # @api private
513
+ #
514
+ def darwin_platform?
515
+ RUBY_PLATFORM.include?('darwin') || RUBY_DESCRIPTION.include?('darwin')
516
+ end
517
+
518
+ # Returns the Array of git global option strings for this context
519
+ #
520
+ # Prepends `--git-dir` and `--work-tree` when the corresponding attributes
521
+ # are set, then appends {STATIC_GLOBAL_OPTS}.
522
+ #
523
+ # @return [Array<String>] the global options to prepend to every git invocation
524
+ #
525
+ def global_opts
526
+ [].tap do |opts|
527
+ opts << "--git-dir=#{git_dir}" unless git_dir.nil?
528
+ opts << "--work-tree=#{git_work_dir}" unless git_work_dir.nil?
529
+ opts.concat(STATIC_GLOBAL_OPTS)
530
+ end
531
+ end
532
+
533
+ # Creates a {Git::CommandLine::Capturing} instance for the current invocation.
534
+ #
535
+ # A new instance is created per call so that {#binary_path} — resolved from
536
+ # `Git::Config.instance` when set to `:use_global_config` — and {#env_overrides}
537
+ # — including {#git_ssh} resolution for `:use_global_config` — reflect the
538
+ # state of {Git::Config.instance} at the time of each command invocation.
539
+ #
540
+ # @return [Git::CommandLine::Capturing] the capturing command line instance
541
+ #
542
+ def command_line_capturing
543
+ Git::CommandLine::Capturing.new(env_overrides, binary_path, global_opts, @logger)
544
+ end
545
+
546
+ # Creates a {Git::CommandLine::Streaming} instance for the current invocation.
547
+ #
548
+ # A new instance is created per call so that {#binary_path} — resolved from
549
+ # `Git::Config.instance` when set to `:use_global_config` — and {#env_overrides}
550
+ # — including {#git_ssh} resolution for `:use_global_config` — reflect the
551
+ # state of {Git::Config.instance} at the time of each command invocation.
552
+ #
553
+ # @return [Git::CommandLine::Streaming] the streaming command line instance
554
+ #
555
+ def command_line_streaming
556
+ Git::CommandLine::Streaming.new(env_overrides, binary_path, global_opts, @logger)
557
+ end
558
+ end
559
+ end