git 5.0.0.beta.1 → 5.0.0.beta.2
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.
- checksums.yaml +4 -4
- data/.github/copilot-instructions.md +6 -0
- data/.github/prompts/iteratively-address-copilot-reviews.prompt.md +188 -0
- data/.github/skills/extract-facade-from-base-lib/KEYWORD_ARG_REMEDIATION.md +22 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +28 -14
- data/.github/skills/facade-implementation/SKILL.md +14 -0
- data/.github/skills/facade-test-conventions/SKILL.md +14 -0
- data/.rubocop.yml +5 -0
- data/README.md +51 -11
- data/UPGRADING.md +141 -0
- data/git.gemspec +5 -0
- data/lib/git/branch.rb +7 -18
- data/lib/git/branches.rb +2 -10
- data/lib/git/command_line/base.rb +10 -0
- data/lib/git/command_line/capturing.rb +5 -3
- data/lib/git/command_line/streaming.rb +5 -3
- data/lib/git/command_line.rb +3 -3
- data/lib/git/commands/base.rb +7 -6
- data/lib/git/commands/cat_file/batch.rb +6 -1
- data/lib/git/commands/cat_file/raw.rb +7 -1
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +5 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +1 -1
- data/lib/git/commands/update_ref/batch.rb +1 -1
- data/lib/git/commands/version.rb +5 -0
- data/lib/git/commands.rb +5 -7
- data/lib/git/config.rb +17 -0
- data/lib/git/config_entry_info.rb +106 -0
- data/lib/git/configuring.rb +665 -0
- data/lib/git/deprecation.rb +9 -0
- data/lib/git/diff.rb +4 -8
- data/lib/git/diff_path_status.rb +2 -13
- data/lib/git/diff_stats.rb +1 -9
- data/lib/git/execution_context/global.rb +3 -28
- data/lib/git/execution_context/repository.rb +30 -41
- data/lib/git/execution_context.rb +43 -24
- data/lib/git/log.rb +3 -9
- data/lib/git/object.rb +14 -21
- data/lib/git/parsers/config_entry.rb +110 -0
- data/lib/git/parsers/ls_remote.rb +79 -0
- data/lib/git/remote.rb +7 -20
- data/lib/git/repository/branching.rb +183 -12
- data/lib/git/repository/committing.rb +64 -68
- data/lib/git/repository/configuring.rb +208 -13
- data/lib/git/repository/context_helpers.rb +264 -0
- data/lib/git/repository/factories.rb +682 -0
- data/lib/git/repository/inspecting.rb +99 -0
- data/lib/git/repository/maintenance.rb +65 -0
- data/lib/git/repository/merging.rb +63 -1
- data/lib/git/repository/object_operations.rb +133 -35
- data/lib/git/repository/path_resolver.rb +1 -1
- data/lib/git/repository/remote_operations.rb +166 -21
- data/lib/git/repository/staging.rb +187 -23
- data/lib/git/repository/stashing.rb +39 -3
- data/lib/git/repository/status_operations.rb +21 -0
- data/lib/git/repository.rb +68 -129
- data/lib/git/stash.rb +2 -9
- data/lib/git/stashes.rb +2 -7
- data/lib/git/status.rb +8 -17
- data/lib/git/version.rb +2 -2
- data/lib/git/worktree.rb +2 -15
- data/lib/git/worktrees.rb +2 -15
- data/lib/git.rb +180 -77
- data/redesign/3_architecture_implementation.md +148 -111
- data/redesign/Phase 4 - Step A.md +360 -0
- data/redesign/beta_release.md +107 -0
- data/redesign/c1c2_audit.md +566 -0
- data/redesign/c1c2_bucket6_lib_orphans.md +626 -0
- data/redesign/config_design.rb +501 -0
- metadata +19 -5
- data/lib/git/base.rb +0 -1204
- data/lib/git/lib.rb +0 -2855
data/lib/git/lib.rb
DELETED
|
@@ -1,2855 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'args_builder'
|
|
4
|
-
require_relative 'commands'
|
|
5
|
-
|
|
6
|
-
require 'git/command_line'
|
|
7
|
-
require 'git/errors'
|
|
8
|
-
require 'git/parsers/branch'
|
|
9
|
-
require 'git/parsers/fsck'
|
|
10
|
-
require 'git/parsers/grep'
|
|
11
|
-
require 'git/parsers/stash'
|
|
12
|
-
require 'git/parsers/tag'
|
|
13
|
-
require 'git/url'
|
|
14
|
-
require 'logger'
|
|
15
|
-
require 'pathname'
|
|
16
|
-
require 'pp'
|
|
17
|
-
require 'process_executer'
|
|
18
|
-
require 'stringio'
|
|
19
|
-
require 'tempfile'
|
|
20
|
-
require 'zlib'
|
|
21
|
-
|
|
22
|
-
module Git
|
|
23
|
-
# Internal git operations
|
|
24
|
-
# @api private
|
|
25
|
-
class Lib
|
|
26
|
-
# Thread-safe cache for git versions, keyed by binary path
|
|
27
|
-
@git_version_cache_mutex = Mutex.new
|
|
28
|
-
@git_version_cache = {}
|
|
29
|
-
|
|
30
|
-
# The path to the Git working copy. The default is '"./.git"'.
|
|
31
|
-
#
|
|
32
|
-
# @return [Pathname] the path to the Git working copy.
|
|
33
|
-
#
|
|
34
|
-
# @see [Git working tree](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefworkingtreeaworkingtree)
|
|
35
|
-
#
|
|
36
|
-
attr_reader :git_work_dir
|
|
37
|
-
|
|
38
|
-
# The path to the Git repository directory. The default is
|
|
39
|
-
# `"#{git_work_dir}/.git"`.
|
|
40
|
-
#
|
|
41
|
-
# @return [Pathname] the Git repository directory.
|
|
42
|
-
#
|
|
43
|
-
# @see [Git repository](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefrepositoryarepository)
|
|
44
|
-
#
|
|
45
|
-
attr_reader :git_dir
|
|
46
|
-
|
|
47
|
-
# The Git index file used to stage changes (using `git add`) before they
|
|
48
|
-
# are committed.
|
|
49
|
-
#
|
|
50
|
-
# @return [Pathname] the Git index file
|
|
51
|
-
#
|
|
52
|
-
# @see [Git index file](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefindexaindex)
|
|
53
|
-
#
|
|
54
|
-
attr_reader :git_index_file
|
|
55
|
-
|
|
56
|
-
# Create a new Git::Lib object
|
|
57
|
-
#
|
|
58
|
-
# @overload initialize(base, logger)
|
|
59
|
-
#
|
|
60
|
-
# @param base [Hash] the hash containing paths to the Git working copy,
|
|
61
|
-
# the Git repository directory, and the Git index file.
|
|
62
|
-
#
|
|
63
|
-
# @option base [Pathname] :working_directory
|
|
64
|
-
# @option base [Pathname] :repository
|
|
65
|
-
# @option base [Pathname] :index
|
|
66
|
-
#
|
|
67
|
-
# @param [Logger] logger
|
|
68
|
-
#
|
|
69
|
-
# @overload initialize(base, logger)
|
|
70
|
-
#
|
|
71
|
-
# @param base [#dir, #repo, #index] an object with methods to get the Git worktree (#dir),
|
|
72
|
-
# the Git repository directory (#repo), and the Git index file (#index).
|
|
73
|
-
#
|
|
74
|
-
# @param [Logger] logger
|
|
75
|
-
#
|
|
76
|
-
def initialize(base = nil, logger = nil)
|
|
77
|
-
@logger = logger || Logger.new(nil)
|
|
78
|
-
@git_ssh = :use_global_config
|
|
79
|
-
|
|
80
|
-
case base
|
|
81
|
-
when Git::Base
|
|
82
|
-
initialize_from_base(base)
|
|
83
|
-
when Hash
|
|
84
|
-
initialize_from_hash(base)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Creates or reinitializes the repository in the current directory
|
|
89
|
-
#
|
|
90
|
-
# This is a low-level method that just runs `git init` with the given options.
|
|
91
|
-
# For full repository initialization including directory creation and path
|
|
92
|
-
# resolution, use Git.init instead.
|
|
93
|
-
#
|
|
94
|
-
# @param opts [Hash] command options
|
|
95
|
-
#
|
|
96
|
-
# @option opts [Boolean, nil] :bare (nil) create a bare repository
|
|
97
|
-
#
|
|
98
|
-
# @option opts [String, nil] :initial_branch (nil) use the specified name for the initial branch
|
|
99
|
-
#
|
|
100
|
-
# @option opts [String, nil] :separate_git_dir (nil) path to put the .git directory (`--separate-git-dir`)
|
|
101
|
-
#
|
|
102
|
-
# @option opts [String, nil] :repository (nil) deprecated — use `:separate_git_dir` instead
|
|
103
|
-
#
|
|
104
|
-
# @return [String] the command output
|
|
105
|
-
#
|
|
106
|
-
def init(opts = {})
|
|
107
|
-
opts = opts.dup
|
|
108
|
-
opts[:separate_git_dir] ||= opts.delete(:repository)
|
|
109
|
-
Git::Commands::Init.new(self).call(**opts).stdout
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Clones a repository into a newly created directory
|
|
113
|
-
#
|
|
114
|
-
# @param [String] repository_url the URL of the repository to clone
|
|
115
|
-
#
|
|
116
|
-
# @param [String, nil] directory the directory to clone into
|
|
117
|
-
#
|
|
118
|
-
# If nil, the repository is cloned into a directory with the same name as
|
|
119
|
-
# the repository.
|
|
120
|
-
#
|
|
121
|
-
# @param [Hash] opts the options for this command
|
|
122
|
-
#
|
|
123
|
-
# @option opts [Boolean, nil] :bare (nil) if true, clone as a bare repository
|
|
124
|
-
#
|
|
125
|
-
# @option opts [String, nil] :branch (nil) the branch to checkout
|
|
126
|
-
#
|
|
127
|
-
# @option opts [String, Array<String>, nil] :config (nil) one or more configuration options to set
|
|
128
|
-
#
|
|
129
|
-
# @option opts [Integer, nil] :depth (nil) the number of commits back to pull
|
|
130
|
-
#
|
|
131
|
-
# @option opts [String, nil] :filter (nil) specify partial clone
|
|
132
|
-
#
|
|
133
|
-
# @option opts [String, nil] :git_ssh (nil) SSH command or binary to use for git over SSH
|
|
134
|
-
#
|
|
135
|
-
# @option opts [Logger, nil] :log (nil) Logger instance to use for git operations
|
|
136
|
-
#
|
|
137
|
-
# @option opts [Boolean, nil] :mirror (nil) set up a mirror of the source repository
|
|
138
|
-
#
|
|
139
|
-
# @option opts [String, nil] :origin (nil) the name of the remote
|
|
140
|
-
#
|
|
141
|
-
# @option opts [String, nil] :chdir (nil) run `git clone` from this directory
|
|
142
|
-
#
|
|
143
|
-
# When given, `directory` (or the repository basename when `directory` is nil)
|
|
144
|
-
# is resolved relative to `:chdir`, just as if you had `cd`'d into it before
|
|
145
|
-
# running `git clone`. The returned path is the join of `:chdir` and the
|
|
146
|
-
# cloned directory path.
|
|
147
|
-
#
|
|
148
|
-
# @option opts [String] :path deprecated: use `:chdir` instead.
|
|
149
|
-
#
|
|
150
|
-
# @option opts [String] :remote deprecated: use `:origin` instead.
|
|
151
|
-
#
|
|
152
|
-
# @option opts [Boolean, String, Array<String>, nil] :recurse_submodules (nil) initialize
|
|
153
|
-
# submodules after cloning; pass `true` for all submodules, or a pathspec string/array
|
|
154
|
-
# for a subset
|
|
155
|
-
#
|
|
156
|
-
# @option opts [Boolean, String, Array<String>, nil] :recursive (nil) deprecated: use `:recurse_submodules` instead
|
|
157
|
-
#
|
|
158
|
-
# @option opts [Numeric, nil] :timeout (nil) the number of seconds to wait for the
|
|
159
|
-
# command to complete
|
|
160
|
-
#
|
|
161
|
-
# See {Git::Lib#command} for more information about :timeout
|
|
162
|
-
#
|
|
163
|
-
# @return [Hash] the options to pass to {Git::Base.new}
|
|
164
|
-
#
|
|
165
|
-
# @todo make this work with SSH password or auth_key
|
|
166
|
-
#
|
|
167
|
-
def clone(repository_url, directory = nil, opts = {})
|
|
168
|
-
opts = opts.dup
|
|
169
|
-
deprecate_clone_options!(opts)
|
|
170
|
-
chdir = opts.delete(:chdir)
|
|
171
|
-
execution_opts = extract_clone_execution_context_opts(opts)
|
|
172
|
-
opts[:chdir] = chdir if chdir
|
|
173
|
-
command_line_result = Git::Commands::Clone.new(self).call(repository_url, directory, **opts)
|
|
174
|
-
result = build_clone_result(command_line_result, execution_opts)
|
|
175
|
-
prefix_clone_result_paths!(result, chdir)
|
|
176
|
-
result
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Returns the name of the default branch of the given repository
|
|
180
|
-
#
|
|
181
|
-
# @param repository [URI, Pathname, String] The (possibly remote) repository to clone from
|
|
182
|
-
#
|
|
183
|
-
# @return [String] the name of the default branch
|
|
184
|
-
#
|
|
185
|
-
def repository_default_branch(repository)
|
|
186
|
-
output = Git::Commands::LsRemote.new(self).call(repository, 'HEAD', symref: true).stdout
|
|
187
|
-
|
|
188
|
-
match_data = output.match(%r{^ref: refs/remotes/origin/(?<default_branch>[^\t]+)\trefs/remotes/origin/HEAD$})
|
|
189
|
-
return match_data[:default_branch] if match_data
|
|
190
|
-
|
|
191
|
-
match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
|
|
192
|
-
return match_data[:default_branch] if match_data
|
|
193
|
-
|
|
194
|
-
raise Git::UnexpectedResultError, 'Unable to determine the default branch'
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
## READ COMMANDS ##
|
|
198
|
-
|
|
199
|
-
# Finds most recent tag that is reachable from a commit
|
|
200
|
-
#
|
|
201
|
-
# @see https://git-scm.com/docs/git-describe git-describe
|
|
202
|
-
#
|
|
203
|
-
# @param commit_ish [String, nil] target commit sha or object name
|
|
204
|
-
#
|
|
205
|
-
# @param opts [Hash] the given options
|
|
206
|
-
#
|
|
207
|
-
# @option opts [Boolean, nil] :all (nil) use refs from all branches, tags, and remotes
|
|
208
|
-
# @option opts [Boolean, nil] :tags (nil) consider any tag, not just annotated ones
|
|
209
|
-
# @option opts [Boolean, nil] :contains (nil) find the tag that comes after the commit
|
|
210
|
-
# @option opts [Boolean, nil] :debug (nil) enable verbose searching strategy output
|
|
211
|
-
# @option opts [Boolean, nil] :long (nil) always output the long format
|
|
212
|
-
# @option opts [Boolean, nil] :always (nil) show uniquely abbreviated commit as a fallback
|
|
213
|
-
# @option opts [Boolean, nil] :exact_match (nil) only output exact tag matches
|
|
214
|
-
# @option opts :dirty [true, String]
|
|
215
|
-
# @option opts :abbrev [String]
|
|
216
|
-
# @option opts :candidates [String]
|
|
217
|
-
# @option opts :match [String]
|
|
218
|
-
#
|
|
219
|
-
# @return [String] the tag name
|
|
220
|
-
#
|
|
221
|
-
# @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
|
|
222
|
-
#
|
|
223
|
-
def describe(commit_ish = nil, opts = {})
|
|
224
|
-
assert_args_are_not_options('commit-ish object', commit_ish)
|
|
225
|
-
|
|
226
|
-
# Translate legacy :"exact-match" (hyphenated) key to :exact_match (underscored)
|
|
227
|
-
opts = opts.dup
|
|
228
|
-
opts[:exact_match] ||= opts.delete(:'exact-match') if opts.key?(:'exact-match')
|
|
229
|
-
|
|
230
|
-
commit_ishes = Array(commit_ish).compact
|
|
231
|
-
Git::Commands::Describe.new(self).call(*commit_ishes, **opts).stdout
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
# Return the commits that are within the given revision range
|
|
235
|
-
#
|
|
236
|
-
# @see https://git-scm.com/docs/git-log git-log
|
|
237
|
-
#
|
|
238
|
-
# @param opts [Hash] the given options
|
|
239
|
-
#
|
|
240
|
-
# @option opts :count [Integer] the maximum number of commits to return (maps to
|
|
241
|
-
# max-count)
|
|
242
|
-
#
|
|
243
|
-
# @option opts [Boolean, nil] :all (nil) include commits reachable from any ref
|
|
244
|
-
#
|
|
245
|
-
# @option opts [Boolean, nil] :cherry (nil) omit commits equivalent to cherry-picked commits
|
|
246
|
-
#
|
|
247
|
-
# @option opts :since [String]
|
|
248
|
-
#
|
|
249
|
-
# @option opts :until [String]
|
|
250
|
-
#
|
|
251
|
-
# @option opts :grep [String]
|
|
252
|
-
#
|
|
253
|
-
# @option opts :author [String]
|
|
254
|
-
#
|
|
255
|
-
# @option opts :between [Array<String>] an array of two commit-ish strings to
|
|
256
|
-
# specify a revision range
|
|
257
|
-
#
|
|
258
|
-
# Only :between or :object options can be used, not both.
|
|
259
|
-
#
|
|
260
|
-
# @option opts :object [String] the revision range for the git log command
|
|
261
|
-
#
|
|
262
|
-
# Only :between or :object options can be used, not both.
|
|
263
|
-
#
|
|
264
|
-
# @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only include commits that
|
|
265
|
-
# impact files from the specified paths
|
|
266
|
-
#
|
|
267
|
-
# @option opts :skip [Integer]
|
|
268
|
-
#
|
|
269
|
-
# @return [Array<Hash>] the log output parsed into an array of hashs for each commit
|
|
270
|
-
#
|
|
271
|
-
# Each hash contains the following keys:
|
|
272
|
-
#
|
|
273
|
-
# * 'sha' [String] the commit sha
|
|
274
|
-
# * 'author' [String] the author of the commit
|
|
275
|
-
# * 'message' [String] the commit message
|
|
276
|
-
# * 'parent' [Array<String>] the commit shas of the parent commits
|
|
277
|
-
# * 'tree' [String] the tree sha
|
|
278
|
-
# * 'author' [String] the author of the commit and timestamp of when the
|
|
279
|
-
# changes were created
|
|
280
|
-
# * 'committer' [String] the committer of the commit and timestamp of when the
|
|
281
|
-
# commit was applied
|
|
282
|
-
# * 'merges' [Boolean] if truthy, only include merge commits (aka commits with
|
|
283
|
-
# 2 or more parents)
|
|
284
|
-
#
|
|
285
|
-
# @raise [ArgumentError] if the revision range (specified with :between or
|
|
286
|
-
# :object) is a string starting with a hyphen
|
|
287
|
-
#
|
|
288
|
-
def full_log_commits(opts = {})
|
|
289
|
-
assert_valid_opts(opts, FULL_LOG_ALLOWED_OPTS)
|
|
290
|
-
validate_log_count_option!(opts)
|
|
291
|
-
|
|
292
|
-
call_opts = log_base_call_options(opts, skip: opts[:skip], merges: opts[:merges])
|
|
293
|
-
run_log_command(log_revision_range_args(opts), call_opts)
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
# Verify and resolve a Git revision to its full SHA
|
|
297
|
-
#
|
|
298
|
-
# @see https://git-scm.com/docs/git-rev-parse git-rev-parse
|
|
299
|
-
# @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions
|
|
300
|
-
# @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem
|
|
301
|
-
# Ref disambiguation rules
|
|
302
|
-
#
|
|
303
|
-
# @example
|
|
304
|
-
# lib.rev_parse('HEAD') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
|
|
305
|
-
# lib.rev_parse('9b9b31e') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
|
|
306
|
-
#
|
|
307
|
-
# @param revision [String] the revision to resolve
|
|
308
|
-
#
|
|
309
|
-
# @return [String] the full commit hash
|
|
310
|
-
#
|
|
311
|
-
# @raise [Git::FailedError] if the revision cannot be resolved
|
|
312
|
-
#
|
|
313
|
-
def rev_parse(revision)
|
|
314
|
-
Git::Commands::RevParse.new(self).call(revision, '--', revs_only: true).stdout
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
# For backwards compatibility with the old method name
|
|
318
|
-
alias revparse rev_parse
|
|
319
|
-
|
|
320
|
-
# Find the first symbolic name for given commit_ish
|
|
321
|
-
#
|
|
322
|
-
# @param commit_ish [String] the commit_ish to find the symbolic name of
|
|
323
|
-
#
|
|
324
|
-
# @return [String, nil] the first symbolic name or nil if the commit_ish isn't found
|
|
325
|
-
#
|
|
326
|
-
# @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
|
|
327
|
-
#
|
|
328
|
-
def name_rev(commit_ish)
|
|
329
|
-
assert_args_are_not_options('commit_ish', commit_ish)
|
|
330
|
-
|
|
331
|
-
Git::Commands::NameRev.new(self).call(commit_ish).stdout.split[1]
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
alias namerev name_rev
|
|
335
|
-
|
|
336
|
-
# Returns the raw content of a git object, or streams it into a tempfile
|
|
337
|
-
#
|
|
338
|
-
# Without a block, the full content is buffered in memory and returned as a
|
|
339
|
-
# `String`. With a block, git output is streamed directly to disk without memory
|
|
340
|
-
# buffering — safe for large blobs.
|
|
341
|
-
#
|
|
342
|
-
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
343
|
-
#
|
|
344
|
-
# @overload cat_file_contents(object)
|
|
345
|
-
# Returns the object's raw content as a string.
|
|
346
|
-
#
|
|
347
|
-
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
348
|
-
#
|
|
349
|
-
# @return [String] the raw content of the object
|
|
350
|
-
#
|
|
351
|
-
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
352
|
-
#
|
|
353
|
-
# @raise [Git::FailedError] if the object does not exist or the command fails
|
|
354
|
-
#
|
|
355
|
-
# @example Get the contents of a blob
|
|
356
|
-
# lib.cat_file_contents('HEAD:README.md') # => "This is a README file\n"
|
|
357
|
-
#
|
|
358
|
-
# @overload cat_file_contents(object, &block)
|
|
359
|
-
# Streams the object's raw content to a temporary file and yields it.
|
|
360
|
-
#
|
|
361
|
-
# Git output is written directly to a file on disk without being
|
|
362
|
-
# buffered in memory first, then the file is rewound and yielded to the block.
|
|
363
|
-
# The return value is whatever the block returns.
|
|
364
|
-
#
|
|
365
|
-
# @param object [String] the object name (SHA, ref, `HEAD`, treeish path, etc.)
|
|
366
|
-
#
|
|
367
|
-
# @yield [file] the temporary file containing the streamed content, positioned at the start
|
|
368
|
-
#
|
|
369
|
-
# @yieldparam file [File] readable `IO` object positioned at the beginning of the content
|
|
370
|
-
#
|
|
371
|
-
# @yieldreturn [Object] the value to return from this method
|
|
372
|
-
#
|
|
373
|
-
# @return [Object] the value returned by the block
|
|
374
|
-
#
|
|
375
|
-
# @raise [ArgumentError] if `object` starts with a hyphen
|
|
376
|
-
#
|
|
377
|
-
# @raise [Git::FailedError] if the object does not exist or the command fails
|
|
378
|
-
#
|
|
379
|
-
# @example Read a large blob without buffering it in memory
|
|
380
|
-
# lib.cat_file_contents('HEAD:large_file.bin') { |f| process(f) }
|
|
381
|
-
#
|
|
382
|
-
def cat_file_contents(object)
|
|
383
|
-
assert_args_are_not_options('object', object)
|
|
384
|
-
|
|
385
|
-
return Git::Commands::CatFile::Raw.new(self).call(object, p: true).stdout unless block_given?
|
|
386
|
-
|
|
387
|
-
# Stream git output directly to a tempfile to avoid buffering large
|
|
388
|
-
# object content in memory when a block is given.
|
|
389
|
-
Tempfile.create do |file|
|
|
390
|
-
file.binmode
|
|
391
|
-
Git::Commands::CatFile::Raw.new(self).call(object, p: true, out: file)
|
|
392
|
-
file.rewind
|
|
393
|
-
yield file
|
|
394
|
-
end
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
alias object_contents cat_file_contents
|
|
398
|
-
|
|
399
|
-
# Get the type for the given object
|
|
400
|
-
#
|
|
401
|
-
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
402
|
-
#
|
|
403
|
-
# @param object [String] the object to get the type
|
|
404
|
-
#
|
|
405
|
-
# @return [String] the object type
|
|
406
|
-
#
|
|
407
|
-
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
408
|
-
#
|
|
409
|
-
def cat_file_type(object)
|
|
410
|
-
assert_args_are_not_options('object', object)
|
|
411
|
-
|
|
412
|
-
cat_file_object_meta(object)[:type]
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
alias object_type cat_file_type
|
|
416
|
-
|
|
417
|
-
# Get the size for the given object
|
|
418
|
-
#
|
|
419
|
-
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
420
|
-
#
|
|
421
|
-
# @param object [String] the object to get the size of
|
|
422
|
-
#
|
|
423
|
-
# @return [Integer] the object size in bytes
|
|
424
|
-
#
|
|
425
|
-
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
426
|
-
#
|
|
427
|
-
def cat_file_size(object)
|
|
428
|
-
assert_args_are_not_options('object', object)
|
|
429
|
-
|
|
430
|
-
cat_file_object_meta(object)[:size]
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
alias object_size cat_file_size
|
|
434
|
-
|
|
435
|
-
# Return a hash of commit data
|
|
436
|
-
#
|
|
437
|
-
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
438
|
-
#
|
|
439
|
-
# @param object [String] the object to get the type
|
|
440
|
-
#
|
|
441
|
-
# @return [Hash] commit data
|
|
442
|
-
#
|
|
443
|
-
# The returned commit data has the following keys:
|
|
444
|
-
# * tree [String]
|
|
445
|
-
# * parent [Array<String>]
|
|
446
|
-
# * author [String] the author name, email, and commit timestamp
|
|
447
|
-
# * committer [String] the committer name, email, and merge timestamp
|
|
448
|
-
# * message [String] the commit message
|
|
449
|
-
# * gpgsig [String] the public signing key of the commit (if signed)
|
|
450
|
-
#
|
|
451
|
-
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
452
|
-
#
|
|
453
|
-
def cat_file_commit(object)
|
|
454
|
-
assert_args_are_not_options('object', object)
|
|
455
|
-
|
|
456
|
-
cdata = Git::Commands::CatFile::Raw.new(self).call('commit', object).stdout.split("\n")
|
|
457
|
-
process_commit_data(cdata, object)
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
alias commit_data cat_file_commit
|
|
461
|
-
|
|
462
|
-
def process_commit_data(data, sha)
|
|
463
|
-
# process_commit_headers consumes the header lines from the `data` array,
|
|
464
|
-
# leaving only the message lines behind.
|
|
465
|
-
headers = process_commit_headers(data)
|
|
466
|
-
message = "#{data.join("\n")}\n"
|
|
467
|
-
|
|
468
|
-
{ 'sha' => sha, 'message' => message }.merge(headers)
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
|
|
472
|
-
|
|
473
|
-
# Yields parsed header key/value pairs from `git cat-file` output lines
|
|
474
|
-
#
|
|
475
|
-
# Consumes header lines from the front of `data` until a non-header line is
|
|
476
|
-
# encountered. Continuation lines that begin with a space are folded into the
|
|
477
|
-
# previous header value using newline separators.
|
|
478
|
-
#
|
|
479
|
-
# @param data [Array<String>] mutable output lines from a cat-file response
|
|
480
|
-
#
|
|
481
|
-
# @yield [key, value] each parsed header pair
|
|
482
|
-
#
|
|
483
|
-
# @yieldparam key [String] header field name
|
|
484
|
-
#
|
|
485
|
-
# @yieldparam value [String] unfolded header value text
|
|
486
|
-
#
|
|
487
|
-
# @yieldreturn [void]
|
|
488
|
-
#
|
|
489
|
-
# @return [void]
|
|
490
|
-
#
|
|
491
|
-
# @raise [NoMethodError] if `data` contains non-string entries
|
|
492
|
-
#
|
|
493
|
-
def each_cat_file_header(data)
|
|
494
|
-
while (match = CAT_FILE_HEADER_LINE.match(data.shift))
|
|
495
|
-
key = match[:key]
|
|
496
|
-
value_lines = [match[:value]]
|
|
497
|
-
|
|
498
|
-
value_lines << data.shift.lstrip while data.first.start_with?(' ')
|
|
499
|
-
|
|
500
|
-
yield key, value_lines.join("\n")
|
|
501
|
-
end
|
|
502
|
-
end
|
|
503
|
-
|
|
504
|
-
# Return a hash of annotated tag data
|
|
505
|
-
#
|
|
506
|
-
# Does not work with lightweight tags. List all annotated tags in your repository
|
|
507
|
-
# with the following command:
|
|
508
|
-
#
|
|
509
|
-
# ```sh
|
|
510
|
-
# git for-each-ref --format='%(refname:strip=2)' refs/tags | \
|
|
511
|
-
# while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done
|
|
512
|
-
# ```
|
|
513
|
-
#
|
|
514
|
-
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
515
|
-
#
|
|
516
|
-
# @param object [String] the tag to retrieve
|
|
517
|
-
#
|
|
518
|
-
# @return [Hash] tag data
|
|
519
|
-
#
|
|
520
|
-
# Example tag data returned:
|
|
521
|
-
# ```ruby
|
|
522
|
-
# {
|
|
523
|
-
# "name" => "annotated_tag",
|
|
524
|
-
# "object" => "46abbf07e3c564c723c7c039a43ab3a39e5d02dd",
|
|
525
|
-
# "type" => "commit",
|
|
526
|
-
# "tag" => "annotated_tag",
|
|
527
|
-
# "tagger" => "Scott Chacon <schacon@gmail.com> 1724799270 -0700",
|
|
528
|
-
# "message" => "Creating an annotated tag\n"
|
|
529
|
-
# }
|
|
530
|
-
# ```
|
|
531
|
-
#
|
|
532
|
-
# The returned commit data has the following keys:
|
|
533
|
-
# * object [String] the sha of the tag object
|
|
534
|
-
# * type [String]
|
|
535
|
-
# * tag [String] tag name
|
|
536
|
-
# * tagger [String] the name and email of the user who created the tag
|
|
537
|
-
# and the timestamp of when the tag was created
|
|
538
|
-
# * message [String] the tag message
|
|
539
|
-
#
|
|
540
|
-
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
541
|
-
#
|
|
542
|
-
def cat_file_tag(object)
|
|
543
|
-
assert_args_are_not_options('object', object)
|
|
544
|
-
|
|
545
|
-
tdata = Git::Commands::CatFile::Raw.new(self).call('tag', object).stdout.split("\n")
|
|
546
|
-
process_tag_data(tdata, object)
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
alias tag_data cat_file_tag
|
|
550
|
-
|
|
551
|
-
def cat_file_object_meta(object)
|
|
552
|
-
stdout = Git::Commands::CatFile::Batch.new(self).call(object, batch_check: true).stdout
|
|
553
|
-
parse_cat_file_meta(stdout, object)
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
def parse_cat_file_meta(output, object)
|
|
557
|
-
line = output.to_s.lines.first.to_s.chomp
|
|
558
|
-
|
|
559
|
-
request_object_to_raise_error!(object) if line == "#{object} missing"
|
|
560
|
-
|
|
561
|
-
match = /\A\S+ (?<type>\S+) (?<size>\d+)\z/.match(line)
|
|
562
|
-
raise Git::UnexpectedResultError, "unexpected git cat-file metadata output: #{line.inspect}" if match.nil?
|
|
563
|
-
|
|
564
|
-
{
|
|
565
|
-
type: match[:type],
|
|
566
|
-
size: match[:size].to_i
|
|
567
|
-
}
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
# Re-request the missing object via non-batch cat-file so git produces a
|
|
571
|
-
# real non-zero exit and a FailedError with an accurate stderr message.
|
|
572
|
-
def request_object_to_raise_error!(object)
|
|
573
|
-
Git::Commands::CatFile::Raw.new(self).call(object, p: true)
|
|
574
|
-
raise Git::UnexpectedResultError,
|
|
575
|
-
"expected git cat-file to raise Git::FailedError for missing object #{object.inspect}"
|
|
576
|
-
end
|
|
577
|
-
|
|
578
|
-
def process_tag_data(data, name)
|
|
579
|
-
hsh = { 'name' => name }
|
|
580
|
-
|
|
581
|
-
each_cat_file_header(data) do |key, value|
|
|
582
|
-
hsh[key] = value
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
hsh['message'] = "#{data.join("\n")}\n"
|
|
586
|
-
|
|
587
|
-
hsh
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
def process_commit_log_data(data)
|
|
591
|
-
RawLogParser.new(data).parse
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
# A private parser class to process the output of `git log --pretty=raw`
|
|
595
|
-
# @api private
|
|
596
|
-
class RawLogParser
|
|
597
|
-
def initialize(lines)
|
|
598
|
-
@lines = lines
|
|
599
|
-
@commits = []
|
|
600
|
-
@current_commit = nil
|
|
601
|
-
@in_message = false
|
|
602
|
-
end
|
|
603
|
-
|
|
604
|
-
def parse
|
|
605
|
-
@lines.each { |line| process_line(line.chomp) }
|
|
606
|
-
finalize_commit
|
|
607
|
-
@commits
|
|
608
|
-
end
|
|
609
|
-
|
|
610
|
-
private
|
|
611
|
-
|
|
612
|
-
def process_line(line)
|
|
613
|
-
if line.empty?
|
|
614
|
-
@in_message = !@in_message
|
|
615
|
-
return
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
@in_message = false if @in_message && !line.start_with?(' ')
|
|
619
|
-
|
|
620
|
-
@in_message ? process_message_line(line) : process_metadata_line(line)
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
def process_message_line(line)
|
|
624
|
-
@current_commit['message'] << "#{line[4..]}\n"
|
|
625
|
-
end
|
|
626
|
-
|
|
627
|
-
def process_metadata_line(line)
|
|
628
|
-
key, *value = line.split
|
|
629
|
-
value = value.join(' ')
|
|
630
|
-
|
|
631
|
-
case key
|
|
632
|
-
when 'commit'
|
|
633
|
-
start_new_commit(value)
|
|
634
|
-
when 'parent'
|
|
635
|
-
@current_commit['parent'] << value
|
|
636
|
-
else
|
|
637
|
-
@current_commit[key] = value
|
|
638
|
-
end
|
|
639
|
-
end
|
|
640
|
-
|
|
641
|
-
def start_new_commit(sha)
|
|
642
|
-
finalize_commit
|
|
643
|
-
@current_commit = { 'sha' => sha, 'message' => +'', 'parent' => [] }
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
def finalize_commit
|
|
647
|
-
@commits << @current_commit if @current_commit
|
|
648
|
-
end
|
|
649
|
-
end
|
|
650
|
-
private_constant :RawLogParser
|
|
651
|
-
|
|
652
|
-
# Allowed option keys for {#ls_tree}
|
|
653
|
-
LS_TREE_ALLOWED_OPTS = %i[recursive path].freeze
|
|
654
|
-
|
|
655
|
-
# Lists the objects in a git tree
|
|
656
|
-
#
|
|
657
|
-
# @param sha [String] the tree-ish object to list
|
|
658
|
-
#
|
|
659
|
-
# @param opts [Hash] additional options
|
|
660
|
-
#
|
|
661
|
-
# @return [Hash<String, Hash<String, Hash>>] parsed ls-tree output
|
|
662
|
-
#
|
|
663
|
-
# @api private
|
|
664
|
-
#
|
|
665
|
-
def ls_tree(sha, opts = {})
|
|
666
|
-
assert_valid_opts(opts, LS_TREE_ALLOWED_OPTS)
|
|
667
|
-
r_value = opts[:recursive]
|
|
668
|
-
paths = Array(opts[:path]).compact
|
|
669
|
-
safe_options = {}
|
|
670
|
-
safe_options[:r] = r_value unless r_value.nil?
|
|
671
|
-
result = Git::Commands::LsTree.new(self).call(sha, *paths, **safe_options)
|
|
672
|
-
parse_ls_tree_output(result.stdout)
|
|
673
|
-
end
|
|
674
|
-
|
|
675
|
-
def parse_ls_tree_output(output)
|
|
676
|
-
data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
|
|
677
|
-
output.split("\n").each do |line|
|
|
678
|
-
(info, filenm) = split_status_line(line)
|
|
679
|
-
(mode, type, entry_sha) = info.split
|
|
680
|
-
data[type][filenm] = { mode: mode, sha: entry_sha }
|
|
681
|
-
end
|
|
682
|
-
data
|
|
683
|
-
end
|
|
684
|
-
private :parse_ls_tree_output
|
|
685
|
-
|
|
686
|
-
# @return [String] the command output
|
|
687
|
-
#
|
|
688
|
-
def mv(source, destination, options = {})
|
|
689
|
-
Git::Commands::Mv.new(self).call(*Array(source), destination, verbose: true, **options).stdout
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
def full_tree(sha)
|
|
693
|
-
Git::Commands::LsTree.new(self).call(sha, r: true).stdout.split("\n")
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
def tree_depth(sha)
|
|
697
|
-
full_tree(sha).size
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
def change_head_branch(branch_name)
|
|
701
|
-
Git::Commands::SymbolicRef::Update.new(self).call('HEAD', "refs/heads/#{branch_name}")
|
|
702
|
-
end
|
|
703
|
-
|
|
704
|
-
def branches_all
|
|
705
|
-
result = Git::Commands::Branch::List.new(self).call(all: true, format: Git::Parsers::Branch::FORMAT_STRING)
|
|
706
|
-
Git::Parsers::Branch.parse_list(result.stdout)
|
|
707
|
-
end
|
|
708
|
-
|
|
709
|
-
def worktrees_all
|
|
710
|
-
arr = []
|
|
711
|
-
directory = ''
|
|
712
|
-
# Output example for `worktree list --porcelain`:
|
|
713
|
-
# worktree /code/public/ruby-git
|
|
714
|
-
# HEAD 4bef5abbba073c77b4d0ccc1ffcd0ed7d48be5d4
|
|
715
|
-
# branch refs/heads/master
|
|
716
|
-
#
|
|
717
|
-
# worktree /tmp/worktree-1
|
|
718
|
-
# HEAD b8c63206f8d10f57892060375a86ae911fad356e
|
|
719
|
-
# detached
|
|
720
|
-
#
|
|
721
|
-
Git::Commands::Worktree::List.new(self).call(porcelain: true).stdout.split("\n").each do |w|
|
|
722
|
-
s = w.split
|
|
723
|
-
directory = s[1] if s[0] == 'worktree'
|
|
724
|
-
arr << [directory, s[1]] if s[0] == 'HEAD'
|
|
725
|
-
end
|
|
726
|
-
arr
|
|
727
|
-
end
|
|
728
|
-
|
|
729
|
-
def worktree_add(dir, commitish = nil)
|
|
730
|
-
if commitish.nil?
|
|
731
|
-
Git::Commands::Worktree::Add.new(self).call(dir).stdout
|
|
732
|
-
else
|
|
733
|
-
Git::Commands::Worktree::Add.new(self).call(dir, commitish).stdout
|
|
734
|
-
end
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
def worktree_remove(dir)
|
|
738
|
-
Git::Commands::Worktree::Remove.new(self).call(dir).stdout
|
|
739
|
-
end
|
|
740
|
-
|
|
741
|
-
def worktree_prune
|
|
742
|
-
Git::Commands::Worktree::Prune.new(self).call.stdout
|
|
743
|
-
end
|
|
744
|
-
|
|
745
|
-
def list_files(ref_dir)
|
|
746
|
-
dir = File.join(@git_dir, 'refs', ref_dir)
|
|
747
|
-
Dir.glob('**/*', base: dir).select { |f| File.file?(File.join(dir, f)) }
|
|
748
|
-
end
|
|
749
|
-
|
|
750
|
-
# The state and name of branch pointed to by `HEAD`
|
|
751
|
-
#
|
|
752
|
-
# HEAD can be in the following states:
|
|
753
|
-
#
|
|
754
|
-
# **:active**: `HEAD` points to a branch reference which in turn points to a
|
|
755
|
-
# commit representing the tip of that branch. This is the typical state when
|
|
756
|
-
# working on a branch.
|
|
757
|
-
#
|
|
758
|
-
# **:unborn**: `HEAD` points to a branch reference that does not yet exist
|
|
759
|
-
# because no commits have been made on that branch. This state occurs in two
|
|
760
|
-
# scenarios:
|
|
761
|
-
#
|
|
762
|
-
# * When a repository is newly initialized, and no commits have been made on the
|
|
763
|
-
# initial branch.
|
|
764
|
-
# * When a new branch is created using `git checkout --orphan <branch>`, starting
|
|
765
|
-
# a new branch with no history.
|
|
766
|
-
#
|
|
767
|
-
# **:detached**: `HEAD` points directly to a specific commit (identified by its
|
|
768
|
-
# SHA) rather than a branch reference. This state occurs when you check out a
|
|
769
|
-
# commit, a tag, or any state that is not directly associated with a branch. The
|
|
770
|
-
# branch name in this case is `HEAD`.
|
|
771
|
-
#
|
|
772
|
-
HeadState = Struct.new(:state, :name)
|
|
773
|
-
|
|
774
|
-
# The current branch state which is the state of `HEAD`
|
|
775
|
-
#
|
|
776
|
-
# @return [HeadState] the state and name of the current branch
|
|
777
|
-
#
|
|
778
|
-
def current_branch_state
|
|
779
|
-
branch_name = Git::Commands::Branch::ShowCurrent.new(self).call.stdout
|
|
780
|
-
return HeadState.new(:detached, 'HEAD') if branch_name.empty?
|
|
781
|
-
|
|
782
|
-
state = get_branch_state(branch_name)
|
|
783
|
-
HeadState.new(state, branch_name)
|
|
784
|
-
end
|
|
785
|
-
|
|
786
|
-
def branch_current
|
|
787
|
-
result = Git::Commands::Branch::ShowCurrent.new(self).call
|
|
788
|
-
name = result.stdout.strip
|
|
789
|
-
name.empty? ? 'HEAD' : name
|
|
790
|
-
end
|
|
791
|
-
|
|
792
|
-
def branch_contains(commit, branch_name = '')
|
|
793
|
-
branch_name = branch_name.to_s
|
|
794
|
-
pattern = branch_name.empty? ? nil : branch_name
|
|
795
|
-
Git::Commands::Branch::List.new(self).call(*[pattern].compact, contains: commit, no_color: true).stdout
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
GREP_ALLOWED_OPTS = %i[ignore_case i invert_match v extended_regexp E object path_limiter].freeze
|
|
799
|
-
|
|
800
|
-
def grep(pattern, opts = {})
|
|
801
|
-
assert_valid_opts(opts, GREP_ALLOWED_OPTS)
|
|
802
|
-
|
|
803
|
-
opts = normalize_grep_opts(opts)
|
|
804
|
-
object = opts.delete(:object) || 'HEAD'
|
|
805
|
-
result = Git::Commands::Grep.new(self).call(
|
|
806
|
-
object, pattern:, **opts, no_color: true, line_number: true, null: true
|
|
807
|
-
)
|
|
808
|
-
exitstatus = result.status.exitstatus
|
|
809
|
-
|
|
810
|
-
# Exit status 1 with empty stderr means no lines matched (not an error)
|
|
811
|
-
return {} if exitstatus == 1 && result.stderr.empty?
|
|
812
|
-
|
|
813
|
-
# Exit status 1 with non-empty stderr is a real error (e.g. bad object reference)
|
|
814
|
-
raise Git::FailedError, result if exitstatus == 1
|
|
815
|
-
|
|
816
|
-
parse_grep_output(result.stdout)
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
# Validate that the given arguments cannot be mistaken for a command-line option
|
|
820
|
-
#
|
|
821
|
-
# @param arg_name [String] the name of the arguments to mention in the error message
|
|
822
|
-
# @param args [Array<String, nil>] the arguments to validate
|
|
823
|
-
#
|
|
824
|
-
# @raise [ArgumentError] if any of the parameters are a string starting with a hyphen
|
|
825
|
-
# @return [void]
|
|
826
|
-
#
|
|
827
|
-
def assert_args_are_not_options(arg_name, *args)
|
|
828
|
-
invalid_args = args.select { |arg| arg&.start_with?('-') }
|
|
829
|
-
return unless invalid_args.any?
|
|
830
|
-
|
|
831
|
-
raise ArgumentError, "Invalid #{arg_name}: '#{invalid_args.join("', '")}'"
|
|
832
|
-
end
|
|
833
|
-
|
|
834
|
-
# Normalizes path specifications for Git commands
|
|
835
|
-
#
|
|
836
|
-
# Converts a single path or array of paths into a consistent array format
|
|
837
|
-
# suitable for appending to Git command arguments after '--'. Empty strings
|
|
838
|
-
# are filtered out after conversion.
|
|
839
|
-
#
|
|
840
|
-
# @param pathspecs [String, Pathname, Array<String, Pathname>, nil] path(s) to normalize
|
|
841
|
-
# @param arg_name [String] name of the argument for error messages
|
|
842
|
-
# @return [Array<String>, nil] normalized array of path strings, or nil if empty/nil input
|
|
843
|
-
# @raise [ArgumentError] if any path is not a String or Pathname
|
|
844
|
-
#
|
|
845
|
-
def normalize_pathspecs(pathspecs, arg_name)
|
|
846
|
-
return nil unless pathspecs
|
|
847
|
-
|
|
848
|
-
normalized = Array(pathspecs)
|
|
849
|
-
validate_pathspec_types(normalized, arg_name)
|
|
850
|
-
|
|
851
|
-
normalized = normalized.map(&:to_s).reject(&:empty?)
|
|
852
|
-
return nil if normalized.empty?
|
|
853
|
-
|
|
854
|
-
normalized
|
|
855
|
-
end
|
|
856
|
-
|
|
857
|
-
# Validates that all pathspecs are String or Pathname objects
|
|
858
|
-
#
|
|
859
|
-
# @param pathspecs [Array] the pathspecs to validate
|
|
860
|
-
# @param arg_name [String] name of the argument for error messages
|
|
861
|
-
# @raise [ArgumentError] if any path is not a String or Pathname
|
|
862
|
-
#
|
|
863
|
-
def validate_pathspec_types(pathspecs, arg_name)
|
|
864
|
-
return if pathspecs.all? { |path| path.is_a?(String) || path.is_a?(Pathname) }
|
|
865
|
-
|
|
866
|
-
raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
|
|
867
|
-
end
|
|
868
|
-
|
|
869
|
-
# Allowed option keys for {#full_log_commits}
|
|
870
|
-
FULL_LOG_ALLOWED_OPTS = %i[count all cherry since until grep author between object path_limiter skip merges].freeze
|
|
871
|
-
|
|
872
|
-
# Allowed option keys for {#diff_full}
|
|
873
|
-
DIFF_FULL_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
874
|
-
|
|
875
|
-
# Allowed option keys for {#diff_stats}
|
|
876
|
-
DIFF_STATS_ALLOWED_OPTS = %i[path_limiter].freeze
|
|
877
|
-
|
|
878
|
-
# Allowed option keys for {#diff_path_status}
|
|
879
|
-
DIFF_PATH_STATUS_ALLOWED_OPTS = %i[path_limiter path].freeze
|
|
880
|
-
|
|
881
|
-
# Handle deprecated :path option in favor of :path_limiter
|
|
882
|
-
#
|
|
883
|
-
# @param opts [Hash] options hash that may contain :path or :path_limiter
|
|
884
|
-
#
|
|
885
|
-
# @return [String, Pathname, Array<String, Pathname>, nil] the resolved path limiter
|
|
886
|
-
#
|
|
887
|
-
def handle_deprecated_path_option(opts)
|
|
888
|
-
if opts.key?(:path_limiter)
|
|
889
|
-
opts[:path_limiter]
|
|
890
|
-
elsif opts.key?(:path)
|
|
891
|
-
Git::Deprecation.warn(
|
|
892
|
-
'Git::Lib#diff_path_status :path option is deprecated. Use :path_limiter instead.'
|
|
893
|
-
)
|
|
894
|
-
opts[:path]
|
|
895
|
-
end
|
|
896
|
-
end
|
|
897
|
-
|
|
898
|
-
# Validate that opts contains only allowed keys
|
|
899
|
-
#
|
|
900
|
-
# @param opts [Hash] options hash to validate
|
|
901
|
-
#
|
|
902
|
-
# @param allowed [Array<Symbol>] allowed option keys
|
|
903
|
-
#
|
|
904
|
-
# @raise [ArgumentError] if unknown keys are present
|
|
905
|
-
#
|
|
906
|
-
def assert_valid_opts(opts, allowed)
|
|
907
|
-
unknown = opts.keys - allowed
|
|
908
|
-
raise ArgumentError, "Unknown options: #{unknown.join(', ')}" if unknown.any?
|
|
909
|
-
end
|
|
910
|
-
|
|
911
|
-
# Show full diff patch output between commits or the working tree
|
|
912
|
-
#
|
|
913
|
-
# Delegates to {Git::Commands::Diff}.
|
|
914
|
-
#
|
|
915
|
-
# @param obj1 [String] first commit reference (default: 'HEAD')
|
|
916
|
-
#
|
|
917
|
-
# @param obj2 [String, nil] second commit reference (default: nil)
|
|
918
|
-
#
|
|
919
|
-
# @param opts [Hash] options
|
|
920
|
-
#
|
|
921
|
-
# @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
|
|
922
|
-
# pathspecs to limit the diff
|
|
923
|
-
#
|
|
924
|
-
# @return [String] the unified diff patch output
|
|
925
|
-
#
|
|
926
|
-
# @raise [Git::FailedError] if git returns exit code > 2
|
|
927
|
-
#
|
|
928
|
-
# @see Git::Commands::Diff
|
|
929
|
-
#
|
|
930
|
-
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
931
|
-
assert_valid_opts(opts, DIFF_FULL_ALLOWED_OPTS)
|
|
932
|
-
pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter')
|
|
933
|
-
result = Git::Commands::Diff.new(self).call(
|
|
934
|
-
*[obj1, obj2].compact,
|
|
935
|
-
patch: true, numstat: true, shortstat: true,
|
|
936
|
-
src_prefix: 'a/', dst_prefix: 'b/',
|
|
937
|
-
path: pathspecs
|
|
938
|
-
)
|
|
939
|
-
extract_patch_text(result.stdout)
|
|
940
|
-
end
|
|
941
|
-
|
|
942
|
-
# Show numstat diff output between commits or the working tree
|
|
943
|
-
#
|
|
944
|
-
# Delegates to {Git::Commands::Diff}.
|
|
945
|
-
#
|
|
946
|
-
# @param obj1 [String] first commit reference (default: 'HEAD')
|
|
947
|
-
#
|
|
948
|
-
# @param obj2 [String, nil] second commit reference (default: nil)
|
|
949
|
-
#
|
|
950
|
-
# @param opts [Hash] options
|
|
951
|
-
#
|
|
952
|
-
# @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
|
|
953
|
-
# pathspecs to limit the diff
|
|
954
|
-
#
|
|
955
|
-
# @return [Hash] diff statistics with the shape:
|
|
956
|
-
# `{ total: { insertions:, deletions:, lines:, files: }, files: { ... } }`
|
|
957
|
-
#
|
|
958
|
-
# @raise [Git::FailedError] if git returns exit code > 2
|
|
959
|
-
#
|
|
960
|
-
# @see Git::Commands::Diff
|
|
961
|
-
#
|
|
962
|
-
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
963
|
-
assert_valid_opts(opts, DIFF_STATS_ALLOWED_OPTS)
|
|
964
|
-
pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter')
|
|
965
|
-
result = Git::Commands::Diff.new(self).call(
|
|
966
|
-
*[obj1, obj2].compact,
|
|
967
|
-
numstat: true, shortstat: true,
|
|
968
|
-
src_prefix: 'a/', dst_prefix: 'b/',
|
|
969
|
-
path: pathspecs
|
|
970
|
-
)
|
|
971
|
-
output_lines = extract_numstat_lines(result.stdout)
|
|
972
|
-
parse_diff_stats_output(output_lines)
|
|
973
|
-
end
|
|
974
|
-
|
|
975
|
-
# Show path status (name-status) for diff between commits or the working tree
|
|
976
|
-
#
|
|
977
|
-
# Delegates to {Git::Commands::Diff} and extracts status letters and
|
|
978
|
-
# paths from the raw output lines.
|
|
979
|
-
#
|
|
980
|
-
# @param reference1 [String, nil] first commit reference (default: nil)
|
|
981
|
-
#
|
|
982
|
-
# @param reference2 [String, nil] second commit reference (default: nil)
|
|
983
|
-
#
|
|
984
|
-
# @param opts [Hash] options
|
|
985
|
-
#
|
|
986
|
-
# @option opts [String, Pathname, Array<String, Pathname>] :path_limiter (nil)
|
|
987
|
-
# pathspecs to limit the diff
|
|
988
|
-
#
|
|
989
|
-
# @option opts [String, Pathname, Array<String, Pathname>] :path (nil)
|
|
990
|
-
# deprecated; use :path_limiter instead
|
|
991
|
-
#
|
|
992
|
-
# @return [Hash] mapping of file paths to status letters
|
|
993
|
-
# (e.g. `{ "lib/foo.rb" => "M", "README.md" => "A" }`)
|
|
994
|
-
#
|
|
995
|
-
# @raise [Git::FailedError] if git returns exit code > 2
|
|
996
|
-
#
|
|
997
|
-
# @see Git::Commands::Diff
|
|
998
|
-
#
|
|
999
|
-
def diff_path_status(reference1 = nil, reference2 = nil, opts = {})
|
|
1000
|
-
assert_valid_opts(opts, DIFF_PATH_STATUS_ALLOWED_OPTS)
|
|
1001
|
-
|
|
1002
|
-
path_limiter = handle_deprecated_path_option(opts)
|
|
1003
|
-
pathspecs = normalize_pathspecs(path_limiter, 'path limiter')
|
|
1004
|
-
result = Git::Commands::Diff.new(self).call(
|
|
1005
|
-
*[reference1, reference2].compact,
|
|
1006
|
-
raw: true, numstat: true, shortstat: true,
|
|
1007
|
-
src_prefix: 'a/', dst_prefix: 'b/',
|
|
1008
|
-
path: pathspecs
|
|
1009
|
-
)
|
|
1010
|
-
extract_name_status_from_raw(result.stdout)
|
|
1011
|
-
end
|
|
1012
|
-
|
|
1013
|
-
# compares the index and the working directory
|
|
1014
|
-
def diff_files
|
|
1015
|
-
Git::Commands::Status.new(self).call
|
|
1016
|
-
parse_raw_diff_output(Git::Commands::DiffFiles.new(self).call.stdout)
|
|
1017
|
-
end
|
|
1018
|
-
|
|
1019
|
-
# compares the index and the repository
|
|
1020
|
-
def diff_index(treeish)
|
|
1021
|
-
Git::Commands::Status.new(self).call
|
|
1022
|
-
parse_raw_diff_output(Git::Commands::DiffIndex.new(self).call(treeish).stdout)
|
|
1023
|
-
end
|
|
1024
|
-
|
|
1025
|
-
# List all files that are in the index
|
|
1026
|
-
#
|
|
1027
|
-
# @param location [String] the location to list the files from
|
|
1028
|
-
#
|
|
1029
|
-
# @return [Hash<String, Hash>] a hash of files in the index
|
|
1030
|
-
# * key: file [String] the file path
|
|
1031
|
-
# * value: file_info [Hash] the file information containing the following keys:
|
|
1032
|
-
# * :path [String] the file path
|
|
1033
|
-
# * :mode_index [String] the file mode
|
|
1034
|
-
# * :sha_index [String] the file sha
|
|
1035
|
-
# * :stage [String] the file stage
|
|
1036
|
-
#
|
|
1037
|
-
def ls_files(location = nil)
|
|
1038
|
-
location ||= '.'
|
|
1039
|
-
{}.tap do |files|
|
|
1040
|
-
Git::Commands::LsFiles.new(self).call(location, stage: true).stdout.split("\n").each do |line|
|
|
1041
|
-
(info, file) = split_status_line(line)
|
|
1042
|
-
(mode, sha, stage) = info.split
|
|
1043
|
-
files[file] = {
|
|
1044
|
-
path: file, mode_index: mode, sha_index: sha, stage: stage
|
|
1045
|
-
}
|
|
1046
|
-
end
|
|
1047
|
-
end
|
|
1048
|
-
end
|
|
1049
|
-
|
|
1050
|
-
# Unescape a path if it is quoted
|
|
1051
|
-
#
|
|
1052
|
-
# Git commands that output paths (e.g. ls-files, diff), will escape unusual
|
|
1053
|
-
# characters.
|
|
1054
|
-
#
|
|
1055
|
-
# @example
|
|
1056
|
-
# lib.unescape_if_quoted('"quoted_file_\\342\\230\\240"') # => 'quoted_file_☠'
|
|
1057
|
-
# lib.unescape_if_quoted('unquoted_file') # => 'unquoted_file'
|
|
1058
|
-
#
|
|
1059
|
-
# @param path [String] the path to unescape if quoted
|
|
1060
|
-
#
|
|
1061
|
-
# @return [String] the unescaped path if quoted otherwise the original path
|
|
1062
|
-
#
|
|
1063
|
-
# @api private
|
|
1064
|
-
#
|
|
1065
|
-
def unescape_quoted_path(path)
|
|
1066
|
-
if path.start_with?('"') && path.end_with?('"')
|
|
1067
|
-
Git::EscapedPath.new(path[1..-2]).unescape
|
|
1068
|
-
else
|
|
1069
|
-
path
|
|
1070
|
-
end
|
|
1071
|
-
end
|
|
1072
|
-
|
|
1073
|
-
def ls_remote(location = nil, opts = {})
|
|
1074
|
-
repository = location || '.'
|
|
1075
|
-
output_lines = Git::Commands::LsRemote.new(self).call(repository, **opts).stdout.split("\n")
|
|
1076
|
-
parse_ls_remote_output(output_lines)
|
|
1077
|
-
end
|
|
1078
|
-
|
|
1079
|
-
def ignored_files
|
|
1080
|
-
Git::Commands::LsFiles.new(self).call(
|
|
1081
|
-
others: true, ignored: true, exclude_standard: true
|
|
1082
|
-
).stdout.split("\n").map { |f| unescape_quoted_path(f) }
|
|
1083
|
-
end
|
|
1084
|
-
|
|
1085
|
-
def untracked_files
|
|
1086
|
-
Git::Commands::LsFiles.new(self).call(
|
|
1087
|
-
others: true, exclude_standard: true, chdir: @git_work_dir
|
|
1088
|
-
).stdout.split("\n").map { |f| unescape_quoted_path(f) }
|
|
1089
|
-
end
|
|
1090
|
-
|
|
1091
|
-
def config_remote(name)
|
|
1092
|
-
hsh = {}
|
|
1093
|
-
config_list.each do |key, value|
|
|
1094
|
-
hsh[key.gsub("remote.#{name}.", '')] = value if /remote.#{name}/.match(key)
|
|
1095
|
-
end
|
|
1096
|
-
hsh
|
|
1097
|
-
end
|
|
1098
|
-
|
|
1099
|
-
def config_get(name)
|
|
1100
|
-
result = Git::Commands::ConfigOptionSyntax::Get.new(self).call(name)
|
|
1101
|
-
raise Git::FailedError, result if result.status.exitstatus != 0
|
|
1102
|
-
|
|
1103
|
-
result.stdout
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
def global_config_get(name)
|
|
1107
|
-
result = Git::Commands::ConfigOptionSyntax::Get.new(self).call(name, global: true)
|
|
1108
|
-
raise Git::FailedError, result if result.status.exitstatus != 0
|
|
1109
|
-
|
|
1110
|
-
result.stdout
|
|
1111
|
-
end
|
|
1112
|
-
|
|
1113
|
-
def config_list
|
|
1114
|
-
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call.stdout.split("\n")
|
|
1115
|
-
end
|
|
1116
|
-
|
|
1117
|
-
def global_config_list
|
|
1118
|
-
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call(global: true).stdout.split("\n")
|
|
1119
|
-
end
|
|
1120
|
-
|
|
1121
|
-
def parse_config_list(lines)
|
|
1122
|
-
hsh = {}
|
|
1123
|
-
lines.each do |line|
|
|
1124
|
-
(key, *values) = line.split('=')
|
|
1125
|
-
hsh[key] = values.join('=')
|
|
1126
|
-
end
|
|
1127
|
-
hsh
|
|
1128
|
-
end
|
|
1129
|
-
|
|
1130
|
-
def parse_config(file)
|
|
1131
|
-
parse_config_list Git::Commands::ConfigOptionSyntax::List.new(self).call(file: file).stdout.split("\n")
|
|
1132
|
-
end
|
|
1133
|
-
|
|
1134
|
-
# Shows objects
|
|
1135
|
-
#
|
|
1136
|
-
# @param [String|NilClass] objectish the target object reference (nil == HEAD)
|
|
1137
|
-
# @param [String|NilClass] path the path of the file to be shown
|
|
1138
|
-
# @return [String] the object information
|
|
1139
|
-
def show(objectish = nil, path = nil)
|
|
1140
|
-
object = path ? "#{objectish}:#{path}" : objectish
|
|
1141
|
-
Git::Commands::Show.new(self).call(*[object].compact).stdout
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
|
-
## WRITE COMMANDS ##
|
|
1145
|
-
|
|
1146
|
-
CONFIG_SET_ALLOWED_OPTS = %i[file].freeze
|
|
1147
|
-
|
|
1148
|
-
def config_set(name, value, options = {})
|
|
1149
|
-
assert_valid_opts(options, CONFIG_SET_ALLOWED_OPTS)
|
|
1150
|
-
Git::Commands::ConfigOptionSyntax::Set.new(self).call(name, value, **options.slice(*CONFIG_SET_ALLOWED_OPTS))
|
|
1151
|
-
end
|
|
1152
|
-
|
|
1153
|
-
def global_config_set(name, value)
|
|
1154
|
-
Git::Commands::ConfigOptionSyntax::Set.new(self).call(name, value, global: true)
|
|
1155
|
-
end
|
|
1156
|
-
|
|
1157
|
-
# Update the index from the current worktree to prepare the for the next commit
|
|
1158
|
-
#
|
|
1159
|
-
# @example
|
|
1160
|
-
# lib.add('path/to/file')
|
|
1161
|
-
# lib.add(['path/to/file1','path/to/file2'])
|
|
1162
|
-
# lib.add(:all => true)
|
|
1163
|
-
#
|
|
1164
|
-
# @param [String, Array<String>] paths files to be added to the repository (relative to the worktree root)
|
|
1165
|
-
# @param [Hash] options
|
|
1166
|
-
#
|
|
1167
|
-
# @option options [Boolean, nil] :all (nil) add, modify, and remove index entries to match the worktree
|
|
1168
|
-
# @option options [Boolean, nil] :force (nil) allow adding otherwise ignored files
|
|
1169
|
-
#
|
|
1170
|
-
# @return [String] the command output (typically empty on success)
|
|
1171
|
-
#
|
|
1172
|
-
def add(paths = '.', options = {})
|
|
1173
|
-
Git::Commands::Add.new(self).call(*Array(paths), **options).stdout
|
|
1174
|
-
end
|
|
1175
|
-
|
|
1176
|
-
# Remove files from the working tree and from the index
|
|
1177
|
-
#
|
|
1178
|
-
# @param path [String, Array<String>] files or directories to remove
|
|
1179
|
-
# @param opts [Hash] command options
|
|
1180
|
-
#
|
|
1181
|
-
# @option opts [Boolean, nil] :force (nil) force removal, bypassing the up-to-date check; alias: `:f`
|
|
1182
|
-
# @option opts [Boolean, nil] :recursive (nil) remove directories and their contents recursively
|
|
1183
|
-
# @option opts [Boolean, nil] :cached (nil) only remove from the index, keeping working tree files
|
|
1184
|
-
#
|
|
1185
|
-
# @return [String] the command output
|
|
1186
|
-
#
|
|
1187
|
-
def rm(path = '.', opts = {})
|
|
1188
|
-
Git::Commands::Rm.new(self).call(*Array(path), **opts).stdout
|
|
1189
|
-
end
|
|
1190
|
-
|
|
1191
|
-
# Returns true if the repository is empty (meaning it has no commits)
|
|
1192
|
-
#
|
|
1193
|
-
# @return [Boolean]
|
|
1194
|
-
#
|
|
1195
|
-
def empty?
|
|
1196
|
-
Git::Commands::RevParse.new(self).call('HEAD', verify: true)
|
|
1197
|
-
false
|
|
1198
|
-
rescue Git::FailedError => e
|
|
1199
|
-
raise unless e.result.status.exitstatus == 128 &&
|
|
1200
|
-
e.result.stderr == 'fatal: Needed a single revision'
|
|
1201
|
-
|
|
1202
|
-
true
|
|
1203
|
-
end
|
|
1204
|
-
|
|
1205
|
-
# Takes the commit message with the options and executes the commit command
|
|
1206
|
-
#
|
|
1207
|
-
# accepts options:
|
|
1208
|
-
# :amend
|
|
1209
|
-
# :all
|
|
1210
|
-
# :allow_empty
|
|
1211
|
-
# :author
|
|
1212
|
-
# :date
|
|
1213
|
-
# :no_verify
|
|
1214
|
-
# :allow_empty_message
|
|
1215
|
-
# :gpg_sign (accepts true or a gpg key ID as a String)
|
|
1216
|
-
# :no_gpg_sign (conflicts with :gpg_sign)
|
|
1217
|
-
#
|
|
1218
|
-
# @param [String] message the commit message to be used
|
|
1219
|
-
# @param [Hash] opts the commit options to be used
|
|
1220
|
-
#
|
|
1221
|
-
def commit(message, opts = {})
|
|
1222
|
-
opts = opts.merge(message: message) if message
|
|
1223
|
-
deprecate_commit_add_all_option!(opts)
|
|
1224
|
-
Git::Commands::Commit.new(self).call(no_edit: true, **opts).stdout
|
|
1225
|
-
end
|
|
1226
|
-
|
|
1227
|
-
# @return [String] the command output
|
|
1228
|
-
#
|
|
1229
|
-
def reset(commit = nil, opts = {})
|
|
1230
|
-
Git::Commands::Reset.new(self).call(commit, **opts).stdout
|
|
1231
|
-
end
|
|
1232
|
-
|
|
1233
|
-
# @return [String] the command output
|
|
1234
|
-
#
|
|
1235
|
-
def clean(opts = {})
|
|
1236
|
-
opts = migrate_clean_legacy_options(opts)
|
|
1237
|
-
Git::Commands::Clean.new(self).call(**opts).stdout
|
|
1238
|
-
end
|
|
1239
|
-
|
|
1240
|
-
REVERT_ALLOWED_OPTS = %i[no_edit].freeze
|
|
1241
|
-
|
|
1242
|
-
def revert(commitish, opts = {})
|
|
1243
|
-
assert_valid_opts(opts, REVERT_ALLOWED_OPTS)
|
|
1244
|
-
opts = { no_edit: true }.merge(opts)
|
|
1245
|
-
Git::Commands::Revert::Start.new(self).call(commitish, **opts).stdout
|
|
1246
|
-
end
|
|
1247
|
-
|
|
1248
|
-
def apply(patch_file)
|
|
1249
|
-
Git::Commands::Apply.new(self).call(*[patch_file].compact, chdir: @git_work_dir).stdout
|
|
1250
|
-
end
|
|
1251
|
-
|
|
1252
|
-
def apply_mail(patch_file)
|
|
1253
|
-
Git::Commands::Am::Apply.new(self).call(*[patch_file].compact, chdir: @git_work_dir).stdout
|
|
1254
|
-
end
|
|
1255
|
-
|
|
1256
|
-
# Returns all stash entries as an array of index and message pairs
|
|
1257
|
-
#
|
|
1258
|
-
# List all stash entries in the repository ordered from oldest to newest
|
|
1259
|
-
#
|
|
1260
|
-
# The index is a sequential number starting from 0 for the oldest stash, and the
|
|
1261
|
-
# message is the description of the stash entry.
|
|
1262
|
-
#
|
|
1263
|
-
# @example List all stashes (oldest first)
|
|
1264
|
-
# lib.stashes_all # => [[0, "Fix bug"], [1, "Add feature"]]
|
|
1265
|
-
#
|
|
1266
|
-
# @return [Array<Array(Integer, String)>] array of [index, message] pairs where
|
|
1267
|
-
# index is the sequential position (0 is oldest) and message is the stash description
|
|
1268
|
-
#
|
|
1269
|
-
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1270
|
-
#
|
|
1271
|
-
def stashes_all
|
|
1272
|
-
result = Git::Commands::Stash::List.new(self).call
|
|
1273
|
-
stashes = Git::Parsers::Stash.parse_list(result.stdout)
|
|
1274
|
-
stashes.reverse.each_with_index.map { |info, i| stash_info_to_legacy(info, i) }
|
|
1275
|
-
end
|
|
1276
|
-
|
|
1277
|
-
# Save the current working directory and index state to a new stash
|
|
1278
|
-
#
|
|
1279
|
-
# This method preserves v4.0.0 backward compatibility by returning a truthy/falsy
|
|
1280
|
-
# value indicating whether a stash was created.
|
|
1281
|
-
#
|
|
1282
|
-
# @param message [String] the stash message
|
|
1283
|
-
#
|
|
1284
|
-
# @return [Boolean] true if changes were stashed, false if there were no local changes to save
|
|
1285
|
-
#
|
|
1286
|
-
# @example Save current changes
|
|
1287
|
-
# lib.stash_save('WIP: feature work')
|
|
1288
|
-
#
|
|
1289
|
-
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1290
|
-
#
|
|
1291
|
-
def stash_save(message) # rubocop:disable Naming/PredicateMethod
|
|
1292
|
-
result = Git::Commands::Stash::Push.new(self).call(message: message)
|
|
1293
|
-
!result.stdout.include?('No local changes to save')
|
|
1294
|
-
end
|
|
1295
|
-
|
|
1296
|
-
# Apply a stash to the working directory
|
|
1297
|
-
#
|
|
1298
|
-
# This method preserves v4.0.0 backward compatibility by returning the command output.
|
|
1299
|
-
#
|
|
1300
|
-
# @param id [String, Integer, nil] the stash identifier (e.g., 'stash@\\{0}', 0) or nil for latest
|
|
1301
|
-
#
|
|
1302
|
-
# @return [String] the output from the git stash apply command
|
|
1303
|
-
#
|
|
1304
|
-
# @example Apply the latest stash
|
|
1305
|
-
# lib.stash_apply
|
|
1306
|
-
#
|
|
1307
|
-
# @example Apply a specific stash
|
|
1308
|
-
# lib.stash_apply('stash@{1}')
|
|
1309
|
-
#
|
|
1310
|
-
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1311
|
-
#
|
|
1312
|
-
def stash_apply(id = nil)
|
|
1313
|
-
result = Git::Commands::Stash::Apply.new(self).call(id)
|
|
1314
|
-
result.stdout
|
|
1315
|
-
end
|
|
1316
|
-
|
|
1317
|
-
# Remove all stash entries
|
|
1318
|
-
#
|
|
1319
|
-
# This method preserves v4.0.0 backward compatibility by returning the command output.
|
|
1320
|
-
#
|
|
1321
|
-
# @return [String] the output from the git stash clear command
|
|
1322
|
-
#
|
|
1323
|
-
# @example Clear all stashes
|
|
1324
|
-
# lib.stash_clear
|
|
1325
|
-
#
|
|
1326
|
-
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1327
|
-
#
|
|
1328
|
-
def stash_clear
|
|
1329
|
-
result = Git::Commands::Stash::Clear.new(self).call
|
|
1330
|
-
result.stdout
|
|
1331
|
-
end
|
|
1332
|
-
|
|
1333
|
-
# List all stash entries in standard git stash list format
|
|
1334
|
-
#
|
|
1335
|
-
# This method preserves v4.0.0 backward compatibility by returning a formatted
|
|
1336
|
-
# string matching the output of `git stash list`.
|
|
1337
|
-
#
|
|
1338
|
-
# @return [String] newline-separated list of stash entries in the format
|
|
1339
|
-
# "stash@\\{n}: <message>", or an empty string if no stashes exist
|
|
1340
|
-
#
|
|
1341
|
-
# @example List all stashes
|
|
1342
|
-
# lib.stash_list # => "stash@\\{0}: On main: WIP\nstash@\\{1}: On feature: test"
|
|
1343
|
-
#
|
|
1344
|
-
# @see https://git-scm.com/docs/git-stash git-stash documentation
|
|
1345
|
-
#
|
|
1346
|
-
def stash_list
|
|
1347
|
-
result = Git::Commands::Stash::List.new(self).call
|
|
1348
|
-
stashes = Git::Parsers::Stash.parse_list(result.stdout)
|
|
1349
|
-
stashes.map { |info| "#{info.name}: #{info.message}" }.join("\n")
|
|
1350
|
-
end
|
|
1351
|
-
|
|
1352
|
-
# Create a new branch
|
|
1353
|
-
#
|
|
1354
|
-
# @param branch [String] the name of the branch to create
|
|
1355
|
-
# @param start_point [String, nil] the commit, branch, or tag to start the new branch from
|
|
1356
|
-
# @param options [Hash] command options (see {Git::Commands::Branch::Create#call})
|
|
1357
|
-
#
|
|
1358
|
-
# @return [nil]
|
|
1359
|
-
#
|
|
1360
|
-
def branch_new(branch, start_point = nil, options = {})
|
|
1361
|
-
Git::Commands::Branch::Create.new(self).call(branch, start_point, **options)
|
|
1362
|
-
nil
|
|
1363
|
-
end
|
|
1364
|
-
|
|
1365
|
-
# Delete one or more branches
|
|
1366
|
-
#
|
|
1367
|
-
# @param branches [Array<String>] the name(s) of the branch(es) to delete
|
|
1368
|
-
# @param options [Hash] command options (see {Git::Commands::Branch::Delete#call})
|
|
1369
|
-
# @option options [Boolean, nil] :force (nil) allow deleting unmerged branches (defaults to `true` when not given)
|
|
1370
|
-
# @option options [Boolean, nil] :remotes (nil) delete remote-tracking branches
|
|
1371
|
-
#
|
|
1372
|
-
# @return [String] newline-separated list of "Deleted branch <name> (was <sha>)." messages
|
|
1373
|
-
#
|
|
1374
|
-
# @raise [Git::Error] if any branch fails to delete
|
|
1375
|
-
#
|
|
1376
|
-
def branch_delete(*branches, **options)
|
|
1377
|
-
options = { force: true }.merge(options)
|
|
1378
|
-
result = Git::Commands::Branch::Delete.new(self).call(*branches, **options)
|
|
1379
|
-
|
|
1380
|
-
raise Git::Error, result.stderr.strip unless result.status.success?
|
|
1381
|
-
|
|
1382
|
-
result.stdout.strip
|
|
1383
|
-
end
|
|
1384
|
-
|
|
1385
|
-
# Runs checkout command to checkout or create branch
|
|
1386
|
-
#
|
|
1387
|
-
# accepts options:
|
|
1388
|
-
# :new_branch / :b - create a new branch with the given name (true = legacy, string = new)
|
|
1389
|
-
# :force / :f - proceed even with uncommitted changes
|
|
1390
|
-
# :start_point - start the new branch at this commit (used with :new_branch in legacy mode)
|
|
1391
|
-
#
|
|
1392
|
-
# @param [String] branch the branch to checkout, or nil
|
|
1393
|
-
# @param [Hash] opts options for the checkout command
|
|
1394
|
-
# @return [String] the command output
|
|
1395
|
-
#
|
|
1396
|
-
def checkout(branch = nil, opts = {})
|
|
1397
|
-
if branch.is_a?(Hash) && opts.empty?
|
|
1398
|
-
opts = branch
|
|
1399
|
-
branch = nil
|
|
1400
|
-
end
|
|
1401
|
-
|
|
1402
|
-
target, translated_opts = translate_checkout_opts(branch, opts)
|
|
1403
|
-
Git::Commands::Checkout::Branch.new(self).call(target, **translated_opts).stdout
|
|
1404
|
-
end
|
|
1405
|
-
|
|
1406
|
-
# Translates legacy checkout options to the new command interface.
|
|
1407
|
-
# Legacy: checkout('branch', new_branch: true, start_point: 'main')
|
|
1408
|
-
# New: checkout('main', b: 'branch')
|
|
1409
|
-
def translate_checkout_opts(branch, opts)
|
|
1410
|
-
if opts[:new_branch] == true || opts[:b] == true
|
|
1411
|
-
[opts[:start_point], opts.except(:new_branch, :b, :start_point).merge(b: branch)]
|
|
1412
|
-
elsif opts[:new_branch].is_a?(String)
|
|
1413
|
-
[branch, opts.except(:new_branch).merge(b: opts[:new_branch])]
|
|
1414
|
-
else
|
|
1415
|
-
[branch, opts]
|
|
1416
|
-
end
|
|
1417
|
-
end
|
|
1418
|
-
private :translate_checkout_opts
|
|
1419
|
-
|
|
1420
|
-
# Checkout a specific version of a file
|
|
1421
|
-
#
|
|
1422
|
-
# @param version [String] the tree-ish (commit, branch, tag) to restore from
|
|
1423
|
-
# @param file [String] the file path to restore
|
|
1424
|
-
# @return [String] the command output
|
|
1425
|
-
#
|
|
1426
|
-
def checkout_file(version, file)
|
|
1427
|
-
Git::Commands::Checkout::Files.new(self).call(version, pathspec: [file]).stdout
|
|
1428
|
-
end
|
|
1429
|
-
|
|
1430
|
-
# Merge one or more branches into the current branch
|
|
1431
|
-
#
|
|
1432
|
-
# @param branch [String, Array<String>] branch name(s) to merge
|
|
1433
|
-
# @param message [String, nil] commit message for merge commit
|
|
1434
|
-
# @param opts [Hash] merge options
|
|
1435
|
-
#
|
|
1436
|
-
# @option opts [Boolean, nil] :no_commit (nil) stop before creating merge commit
|
|
1437
|
-
# (deprecated: use no_commit: true instead)
|
|
1438
|
-
# @option opts [Boolean, nil] :no_ff (nil) create merge commit even for fast-forward
|
|
1439
|
-
# (deprecated: use no_ff: true instead)
|
|
1440
|
-
# @option opts [String] :m (nil) commit message (deprecated: use message: option)
|
|
1441
|
-
# @option opts [Boolean, nil] :commit (nil) true for --commit (`--commit`)
|
|
1442
|
-
# @option opts [Boolean, nil] :ff (nil) true for --ff (`--ff`)
|
|
1443
|
-
# @option opts [Boolean, nil] :ff_only (nil) only merge if fast-forward possible
|
|
1444
|
-
# @option opts [Boolean, nil] :squash (nil) squash commits into single commit
|
|
1445
|
-
# @option opts [String] :message (nil) commit message
|
|
1446
|
-
# @option opts [String] :strategy (nil) merge strategy (e.g., 'ort', 'ours')
|
|
1447
|
-
# @option opts [String, Array<String>] :strategy_option (nil) strategy-specific options
|
|
1448
|
-
# @option opts [Boolean, nil] :allow_unrelated_histories (nil) allow merging unrelated histories
|
|
1449
|
-
#
|
|
1450
|
-
# @return [String] the command output
|
|
1451
|
-
#
|
|
1452
|
-
def merge(branch, message = nil, opts = {})
|
|
1453
|
-
# Handle legacy positional message argument
|
|
1454
|
-
opts = opts.merge(message: message) if message
|
|
1455
|
-
|
|
1456
|
-
# Map legacy option names to new interface
|
|
1457
|
-
opts = translate_merge_options(opts)
|
|
1458
|
-
|
|
1459
|
-
Git::Commands::Merge::Start.new(self).call(*Array(branch), no_edit: true, **opts).stdout
|
|
1460
|
-
end
|
|
1461
|
-
|
|
1462
|
-
# Find common ancestor commit(s) for merge
|
|
1463
|
-
#
|
|
1464
|
-
# @overload merge_base(*commits, options = {})
|
|
1465
|
-
#
|
|
1466
|
-
# @param commits [Array<String>] commits to find common ancestor(s) of
|
|
1467
|
-
#
|
|
1468
|
-
# @param options [Hash] merge-base options
|
|
1469
|
-
#
|
|
1470
|
-
# @option options [Boolean, nil] :octopus (nil) compute best ancestor for n-way merge
|
|
1471
|
-
# @option options [Boolean, nil] :independent (nil) list commits not reachable from others
|
|
1472
|
-
# @option options [Boolean, nil] :fork_point (nil) find fork point
|
|
1473
|
-
# @option options [Boolean, nil] :all (nil) output all merge bases
|
|
1474
|
-
#
|
|
1475
|
-
# @return [Array<String>] array of commit SHAs
|
|
1476
|
-
#
|
|
1477
|
-
def merge_base(*args)
|
|
1478
|
-
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1479
|
-
result = Git::Commands::MergeBase.new(self).call(*args, **opts)
|
|
1480
|
-
result.stdout.lines.map(&:strip).reject(&:empty?)
|
|
1481
|
-
end
|
|
1482
|
-
|
|
1483
|
-
# List paths that remain unmerged after a failed or partial merge
|
|
1484
|
-
#
|
|
1485
|
-
# Delegates to {Git::Commands::Diff}.
|
|
1486
|
-
#
|
|
1487
|
-
# @return [Array<String>] paths of files with unresolved merge conflicts
|
|
1488
|
-
#
|
|
1489
|
-
# @raise [Git::FailedError] if git returns exit code > 2
|
|
1490
|
-
#
|
|
1491
|
-
# @see Git::Commands::Diff
|
|
1492
|
-
#
|
|
1493
|
-
def unmerged
|
|
1494
|
-
result = Git::Commands::Diff.new(self).call(cached: true)
|
|
1495
|
-
result.stdout.split("\n").filter_map do |line|
|
|
1496
|
-
::Regexp.last_match(1) if line =~ /^\* Unmerged path (.*)/
|
|
1497
|
-
end
|
|
1498
|
-
end
|
|
1499
|
-
|
|
1500
|
-
def conflicts # :yields: file, your, their
|
|
1501
|
-
unmerged.each do |file_path|
|
|
1502
|
-
Tempfile.create(['YOUR-', File.basename(file_path)]) do |your_file|
|
|
1503
|
-
write_staged_content(file_path, 2, your_file).flush
|
|
1504
|
-
|
|
1505
|
-
Tempfile.create(['THEIR-', File.basename(file_path)]) do |their_file|
|
|
1506
|
-
write_staged_content(file_path, 3, their_file).flush
|
|
1507
|
-
yield(file_path, your_file.path, their_file.path)
|
|
1508
|
-
end
|
|
1509
|
-
end
|
|
1510
|
-
end
|
|
1511
|
-
end
|
|
1512
|
-
|
|
1513
|
-
def remote_add(name, url, opts = {})
|
|
1514
|
-
translated_opts = opts.dup
|
|
1515
|
-
translated_opts[:fetch] = translated_opts.delete(:with_fetch) if translated_opts.key?(:with_fetch)
|
|
1516
|
-
|
|
1517
|
-
Git::Commands::Remote::Add.new(self).call(name, url, **translated_opts)
|
|
1518
|
-
end
|
|
1519
|
-
|
|
1520
|
-
def remote_set_branches(name, branches, opts = {})
|
|
1521
|
-
Git::Commands::Remote::SetBranches.new(self).call(name, *Array(branches).flatten, **opts)
|
|
1522
|
-
end
|
|
1523
|
-
|
|
1524
|
-
def remote_set_url(name, url, opts = {})
|
|
1525
|
-
Git::Commands::Remote::SetUrl.new(self).call(name, url, **opts)
|
|
1526
|
-
end
|
|
1527
|
-
|
|
1528
|
-
def remote_remove(name)
|
|
1529
|
-
Git::Commands::Remote::Remove.new(self).call(name)
|
|
1530
|
-
end
|
|
1531
|
-
|
|
1532
|
-
def remotes
|
|
1533
|
-
Git::Commands::Remote::List.new(self).call.stdout.split("\n")
|
|
1534
|
-
end
|
|
1535
|
-
|
|
1536
|
-
# List all tags in the repository
|
|
1537
|
-
#
|
|
1538
|
-
# @see https://git-scm.com/docs/git-tag git-tag
|
|
1539
|
-
#
|
|
1540
|
-
# @return [Array<String>] tag names
|
|
1541
|
-
#
|
|
1542
|
-
def tags
|
|
1543
|
-
result = Git::Commands::Tag::List.new(self).call(format: Git::Parsers::Tag::FORMAT_STRING)
|
|
1544
|
-
Git::Parsers::Tag.parse_list(result.stdout).map(&:name)
|
|
1545
|
-
end
|
|
1546
|
-
|
|
1547
|
-
# Create or delete a tag
|
|
1548
|
-
#
|
|
1549
|
-
# When the `:d` or `:delete` option is set, deletes the named tag.
|
|
1550
|
-
# Otherwise, creates a new tag pointing at HEAD or the specified target.
|
|
1551
|
-
#
|
|
1552
|
-
# @see https://git-scm.com/docs/git-tag git-tag
|
|
1553
|
-
#
|
|
1554
|
-
# @overload tag(name, target, opts = {})
|
|
1555
|
-
#
|
|
1556
|
-
# Create a tag on the specified target
|
|
1557
|
-
#
|
|
1558
|
-
# @param name [String] the tag name to create
|
|
1559
|
-
#
|
|
1560
|
-
# @param target [String] the commit or object to tag
|
|
1561
|
-
#
|
|
1562
|
-
# @param opts [Hash] options for creating the tag
|
|
1563
|
-
#
|
|
1564
|
-
# @option opts [Boolean, nil] :annotate (nil) create an unsigned, annotated tag object.
|
|
1565
|
-
# Requires `:message` or `:file`.
|
|
1566
|
-
#
|
|
1567
|
-
# Alias: `:a`
|
|
1568
|
-
#
|
|
1569
|
-
# @option opts [Boolean, nil] :sign (nil) create a GPG-signed tag. Requires `:message` or `:file`.
|
|
1570
|
-
#
|
|
1571
|
-
# Alias: `:s`
|
|
1572
|
-
#
|
|
1573
|
-
# @option opts [Boolean, nil] :force (nil) replace an existing tag with the given name.
|
|
1574
|
-
#
|
|
1575
|
-
# Alias: `:f`
|
|
1576
|
-
#
|
|
1577
|
-
# @option opts [String] :message (nil) use the given string as the tag message.
|
|
1578
|
-
# Implies annotated tag if none of `:annotate`, `:sign`, or `:local_user` is given.
|
|
1579
|
-
#
|
|
1580
|
-
# Alias: `:m`
|
|
1581
|
-
#
|
|
1582
|
-
# @overload tag(name, opts = {})
|
|
1583
|
-
#
|
|
1584
|
-
# Create a lightweight tag on HEAD
|
|
1585
|
-
#
|
|
1586
|
-
# @param name [String] the tag name to create
|
|
1587
|
-
#
|
|
1588
|
-
# @param opts [Hash] options for creating the tag
|
|
1589
|
-
#
|
|
1590
|
-
# @option opts [Boolean, nil] :annotate (nil) create an unsigned, annotated tag object.
|
|
1591
|
-
# Requires `:message` or `:file`.
|
|
1592
|
-
#
|
|
1593
|
-
# Alias: `:a`
|
|
1594
|
-
#
|
|
1595
|
-
# @option opts [Boolean, nil] :sign (nil) create a GPG-signed tag. Requires `:message` or `:file`.
|
|
1596
|
-
#
|
|
1597
|
-
# Alias: `:s`
|
|
1598
|
-
#
|
|
1599
|
-
# @option opts [Boolean, nil] :force (nil) replace an existing tag with the given name.
|
|
1600
|
-
#
|
|
1601
|
-
# Alias: `:f`
|
|
1602
|
-
#
|
|
1603
|
-
# @option opts [String] :message (nil) use the given string as the tag message.
|
|
1604
|
-
# Implies annotated tag if none of `:annotate`, `:sign`, or `:local_user` is given.
|
|
1605
|
-
#
|
|
1606
|
-
# Alias: `:m`
|
|
1607
|
-
#
|
|
1608
|
-
# @overload tag(name, opts = {})
|
|
1609
|
-
#
|
|
1610
|
-
# Delete the named tag
|
|
1611
|
-
#
|
|
1612
|
-
# @param name [String] the tag name to delete
|
|
1613
|
-
#
|
|
1614
|
-
# @param opts [Hash] options
|
|
1615
|
-
#
|
|
1616
|
-
# @option opts [Boolean, nil] :delete (nil) delete the named tag.
|
|
1617
|
-
#
|
|
1618
|
-
# Alias: `:d`
|
|
1619
|
-
#
|
|
1620
|
-
# @return [String] command output
|
|
1621
|
-
#
|
|
1622
|
-
# @raise [ArgumentError] if creating an annotated or signed tag without a message
|
|
1623
|
-
#
|
|
1624
|
-
# @raise [Git::FailedError] if the tag already exists (without `:force`) or if
|
|
1625
|
-
# the tag to delete does not exist
|
|
1626
|
-
#
|
|
1627
|
-
def tag(name, *args)
|
|
1628
|
-
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1629
|
-
target = args.first
|
|
1630
|
-
|
|
1631
|
-
if opts[:d] || opts[:delete]
|
|
1632
|
-
delete_tag(name)
|
|
1633
|
-
else
|
|
1634
|
-
validate_tag_options!(opts)
|
|
1635
|
-
create_tag(name, target, opts)
|
|
1636
|
-
end
|
|
1637
|
-
end
|
|
1638
|
-
|
|
1639
|
-
def fetch(remote, opts)
|
|
1640
|
-
opts = opts.dup
|
|
1641
|
-
refspecs = Array(opts.delete(:ref)).compact
|
|
1642
|
-
positionals = [*([remote] if remote), *refspecs]
|
|
1643
|
-
Git::Commands::Fetch.new(self).call(*positionals, **opts, merge: true).stdout
|
|
1644
|
-
end
|
|
1645
|
-
|
|
1646
|
-
PUSH_ALLOWED_OPTS = %i[mirror delete force f push_option all tags].freeze
|
|
1647
|
-
|
|
1648
|
-
# Push refs to a remote repository
|
|
1649
|
-
#
|
|
1650
|
-
# @overload push(options = {})
|
|
1651
|
-
# Push using the current branch's default remote and push configuration
|
|
1652
|
-
#
|
|
1653
|
-
# @param options [Hash] push options
|
|
1654
|
-
#
|
|
1655
|
-
# @option options [Boolean, nil] :all (nil) push all branches
|
|
1656
|
-
#
|
|
1657
|
-
# @option options [Boolean, nil] :mirror (nil) push all refs
|
|
1658
|
-
#
|
|
1659
|
-
# @option options [Boolean, nil] :tags (nil) push all tags
|
|
1660
|
-
#
|
|
1661
|
-
# @option options [Boolean, nil] :force (nil) force updates
|
|
1662
|
-
#
|
|
1663
|
-
# @option options [Boolean, nil] :delete (nil) delete the named remote ref
|
|
1664
|
-
#
|
|
1665
|
-
# @option options [String, Array<String>] :push_option (nil) server-side push option values
|
|
1666
|
-
#
|
|
1667
|
-
# @return [String] the stdout from the final `git push` invocation
|
|
1668
|
-
#
|
|
1669
|
-
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1670
|
-
#
|
|
1671
|
-
# @overload push(remote, options = {})
|
|
1672
|
-
# Push to the given remote using the current branch's default push configuration
|
|
1673
|
-
#
|
|
1674
|
-
# @param remote [String] the remote name or URL to push to
|
|
1675
|
-
#
|
|
1676
|
-
# @param options [Hash] push options
|
|
1677
|
-
#
|
|
1678
|
-
# @option options [Boolean, nil] :all (nil) push all branches
|
|
1679
|
-
#
|
|
1680
|
-
# @option options [Boolean, nil] :mirror (nil) push all refs
|
|
1681
|
-
#
|
|
1682
|
-
# @option options [Boolean, nil] :tags (nil) push all tags
|
|
1683
|
-
#
|
|
1684
|
-
# @option options [Boolean, nil] :force (nil) force updates
|
|
1685
|
-
#
|
|
1686
|
-
# @option options [Boolean, nil] :delete (nil) delete the named remote ref
|
|
1687
|
-
#
|
|
1688
|
-
# @option options [String, Array<String>] :push_option (nil) server-side push option values
|
|
1689
|
-
#
|
|
1690
|
-
# @return [String] the stdout from the final `git push` invocation
|
|
1691
|
-
#
|
|
1692
|
-
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1693
|
-
#
|
|
1694
|
-
# @overload push(remote, branch, options = {})
|
|
1695
|
-
# Push a branch or refspec to the given remote
|
|
1696
|
-
#
|
|
1697
|
-
# @param remote [String] the remote name or URL to push to
|
|
1698
|
-
#
|
|
1699
|
-
# @param branch [String] the branch name or refspec to push
|
|
1700
|
-
#
|
|
1701
|
-
# @param options [Hash] push options
|
|
1702
|
-
#
|
|
1703
|
-
# @option options [Boolean, nil] :all (nil) push all branches
|
|
1704
|
-
#
|
|
1705
|
-
# @option options [Boolean, nil] :mirror (nil) push all refs
|
|
1706
|
-
#
|
|
1707
|
-
# @option options [Boolean, nil] :tags (nil) push all tags
|
|
1708
|
-
#
|
|
1709
|
-
# @option options [Boolean, nil] :force (nil) force updates
|
|
1710
|
-
#
|
|
1711
|
-
# @option options [Boolean, nil] :delete (nil) delete the named remote ref
|
|
1712
|
-
#
|
|
1713
|
-
# @option options [String, Array<String>] :push_option (nil) server-side push option values
|
|
1714
|
-
#
|
|
1715
|
-
# @return [String] the stdout from the final `git push` invocation
|
|
1716
|
-
#
|
|
1717
|
-
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1718
|
-
#
|
|
1719
|
-
# @raise [ArgumentError] if `remote` is nil
|
|
1720
|
-
#
|
|
1721
|
-
# @overload push(remote, branch, tags)
|
|
1722
|
-
# Backward-compatible shorthand for `push(remote, branch, tags: tags)`
|
|
1723
|
-
#
|
|
1724
|
-
# @param remote [String] the remote name or URL to push to
|
|
1725
|
-
#
|
|
1726
|
-
# @param branch [String] the branch name or refspec to push
|
|
1727
|
-
#
|
|
1728
|
-
# @param tags [Boolean] whether to push all tags
|
|
1729
|
-
#
|
|
1730
|
-
# @return [String] the stdout from the final `git push` invocation
|
|
1731
|
-
#
|
|
1732
|
-
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
1733
|
-
#
|
|
1734
|
-
# @raise [ArgumentError] if `remote` is nil
|
|
1735
|
-
#
|
|
1736
|
-
def push(remote = nil, branch = nil, opts = nil)
|
|
1737
|
-
remote, branch, opts = normalize_push_args(remote, branch, opts)
|
|
1738
|
-
validate_push_args!(remote, branch, opts)
|
|
1739
|
-
|
|
1740
|
-
first_result = push_refs(remote, branch, opts)
|
|
1741
|
-
return first_result.stdout unless push_tags_separately?(opts)
|
|
1742
|
-
|
|
1743
|
-
push_tags(remote, opts).stdout
|
|
1744
|
-
end
|
|
1745
|
-
|
|
1746
|
-
PULL_ALLOWED_OPTS = %i[allow_unrelated_histories].freeze
|
|
1747
|
-
|
|
1748
|
-
def pull(remote = nil, branch = nil, opts = {})
|
|
1749
|
-
raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
|
|
1750
|
-
|
|
1751
|
-
assert_valid_opts(opts, PULL_ALLOWED_OPTS)
|
|
1752
|
-
allowed_opts = opts.slice(*PULL_ALLOWED_OPTS)
|
|
1753
|
-
positional_args = [remote, branch].compact
|
|
1754
|
-
Git::Commands::Pull.new(self).call(*positional_args, no_edit: true, no_progress: true, **allowed_opts).stdout
|
|
1755
|
-
end
|
|
1756
|
-
|
|
1757
|
-
# Return the SHA of a tag reference
|
|
1758
|
-
#
|
|
1759
|
-
# Looks up the tag first in the local refs directory, then falls back to
|
|
1760
|
-
# `git show-ref`. Returns an empty string if the tag does not exist.
|
|
1761
|
-
#
|
|
1762
|
-
# @param tag_name [String] the tag name to look up
|
|
1763
|
-
#
|
|
1764
|
-
# @return [String] the SHA of the tag, or an empty string if not found
|
|
1765
|
-
#
|
|
1766
|
-
def tag_sha(tag_name)
|
|
1767
|
-
head = File.join(@git_dir, 'refs', 'tags', tag_name)
|
|
1768
|
-
return File.read(head).chomp if File.exist?(head)
|
|
1769
|
-
|
|
1770
|
-
result = Git::Commands::ShowRef::List.new(self).call(tag_name, tags: true, hash: true)
|
|
1771
|
-
result.stdout
|
|
1772
|
-
end
|
|
1773
|
-
|
|
1774
|
-
def repack
|
|
1775
|
-
Git::Commands::Repack.new(self).call(a: true, d: true)
|
|
1776
|
-
end
|
|
1777
|
-
|
|
1778
|
-
def gc
|
|
1779
|
-
Git::Commands::Gc.new(self).call(prune: true, aggressive: true, auto: true)
|
|
1780
|
-
end
|
|
1781
|
-
|
|
1782
|
-
# Execute git fsck to verify repository integrity
|
|
1783
|
-
#
|
|
1784
|
-
# @param objects [Array<String>] optional object identifiers to check
|
|
1785
|
-
# @param opts [Hash] command options (see {Git::Commands::Fsck#call})
|
|
1786
|
-
#
|
|
1787
|
-
# @return [Git::FsckResult] the structured result
|
|
1788
|
-
#
|
|
1789
|
-
# rubocop:disable Style/ArgumentsForwarding
|
|
1790
|
-
def fsck(*objects, **opts)
|
|
1791
|
-
result = Git::Commands::Fsck.new(self).call(*objects, no_progress: true, **opts)
|
|
1792
|
-
Git::Parsers::Fsck.parse(result.stdout)
|
|
1793
|
-
end
|
|
1794
|
-
# rubocop:enable Style/ArgumentsForwarding
|
|
1795
|
-
|
|
1796
|
-
READ_TREE_ALLOWED_OPTS = %i[prefix].freeze
|
|
1797
|
-
|
|
1798
|
-
def read_tree(treeish, opts = {})
|
|
1799
|
-
assert_valid_opts(opts, READ_TREE_ALLOWED_OPTS)
|
|
1800
|
-
allowed_opts = opts.slice(*READ_TREE_ALLOWED_OPTS)
|
|
1801
|
-
Git::Commands::ReadTree.new(self).call(treeish, **allowed_opts)
|
|
1802
|
-
end
|
|
1803
|
-
|
|
1804
|
-
def write_tree
|
|
1805
|
-
Git::Commands::WriteTree.new(self).call.stdout
|
|
1806
|
-
end
|
|
1807
|
-
|
|
1808
|
-
COMMIT_TREE_ALLOWED_OPTS = %i[p parent parents m message].freeze
|
|
1809
|
-
|
|
1810
|
-
def commit_tree(tree, opts = {})
|
|
1811
|
-
assert_valid_opts(opts, COMMIT_TREE_ALLOWED_OPTS)
|
|
1812
|
-
actual_opts = normalize_commit_tree_opts(opts, tree)
|
|
1813
|
-
Git::Commands::CommitTree.new(self).call(tree, **actual_opts).stdout
|
|
1814
|
-
end
|
|
1815
|
-
|
|
1816
|
-
def update_ref(ref, commit)
|
|
1817
|
-
Git::Commands::UpdateRef::Update.new(self).call(ref, commit)
|
|
1818
|
-
end
|
|
1819
|
-
|
|
1820
|
-
def checkout_index(opts = {})
|
|
1821
|
-
paths = normalize_pathspecs(opts[:path_limiter], 'path_limiter')
|
|
1822
|
-
keyword_opts = opts.except(:path_limiter)
|
|
1823
|
-
Git::Commands::CheckoutIndex.new(self).call(*paths.to_a, **keyword_opts)
|
|
1824
|
-
end
|
|
1825
|
-
|
|
1826
|
-
ARCHIVE_ALLOWED_OPTS = %i[prefix remote path format add_gzip].freeze
|
|
1827
|
-
|
|
1828
|
-
# Creates an archive of the given tree-ish and writes it to a file
|
|
1829
|
-
#
|
|
1830
|
-
# Delegates to {Git::Commands::Archive} for CLI execution. Format coercion
|
|
1831
|
-
# (`tgz` → `tar` + gzip), temp file management, and gzip post-processing
|
|
1832
|
-
# remain in this adapter.
|
|
1833
|
-
#
|
|
1834
|
-
# @see https://git-scm.com/docs/git-archive git-archive
|
|
1835
|
-
#
|
|
1836
|
-
# @param sha [String] tree-ish to archive (commit, tag, branch, or tree SHA)
|
|
1837
|
-
#
|
|
1838
|
-
# @param file [String, nil] destination file path; a unique temp file is
|
|
1839
|
-
# created and returned if `nil`
|
|
1840
|
-
#
|
|
1841
|
-
# @param opts [Hash] archive options
|
|
1842
|
-
#
|
|
1843
|
-
# @option opts [String] :prefix prefix to prepend to each filename in the archive
|
|
1844
|
-
#
|
|
1845
|
-
# @option opts [String] :remote URL of a remote repository to archive from
|
|
1846
|
-
#
|
|
1847
|
-
# @option opts [String] :path limit the archive to a path within the tree
|
|
1848
|
-
#
|
|
1849
|
-
# @option opts [String] :format archive format — `'tar'`, `'tgz'`, or `'zip'`
|
|
1850
|
-
# (default: `'zip'`)
|
|
1851
|
-
#
|
|
1852
|
-
# @option opts [Boolean, nil] :add_gzip (nil) wrap the archive in gzip compression
|
|
1853
|
-
#
|
|
1854
|
-
# @return [String] the path to the written archive file
|
|
1855
|
-
#
|
|
1856
|
-
# @raise [Git::FailedError] if `git archive` fails
|
|
1857
|
-
#
|
|
1858
|
-
def archive(sha, file = nil, opts = {})
|
|
1859
|
-
assert_valid_opts(opts, ARCHIVE_ALLOWED_OPTS)
|
|
1860
|
-
file ||= temp_file_name
|
|
1861
|
-
format, gzip = parse_archive_format_options(opts)
|
|
1862
|
-
|
|
1863
|
-
command_opts = opts.slice(:prefix, :remote).merge(format: format)
|
|
1864
|
-
path_args = opts[:path] ? [opts[:path]] : []
|
|
1865
|
-
|
|
1866
|
-
File.open(file, 'wb') do |f|
|
|
1867
|
-
Git::Commands::Archive.new(self).call(sha, *path_args, **command_opts, out: f)
|
|
1868
|
-
end
|
|
1869
|
-
apply_gzip(file) if gzip
|
|
1870
|
-
|
|
1871
|
-
file
|
|
1872
|
-
end
|
|
1873
|
-
|
|
1874
|
-
# Returns the git version as a Git::Version
|
|
1875
|
-
#
|
|
1876
|
-
# Parses the output of `git version`, strips platform suffixes (like
|
|
1877
|
-
# `.windows.1` or `.vfs.0`), and pads two-segment versions to three segments.
|
|
1878
|
-
#
|
|
1879
|
-
# Results are cached globally (keyed by binary path). It is assumed that the
|
|
1880
|
-
# git version doesn't change during runtime for a given binary.
|
|
1881
|
-
#
|
|
1882
|
-
# @return [Git::Version] the parsed git version
|
|
1883
|
-
#
|
|
1884
|
-
# @raise [Git::UnexpectedResultError] if the version string cannot be parsed
|
|
1885
|
-
#
|
|
1886
|
-
# @example
|
|
1887
|
-
# lib.git_version #=> Git::Version.new(2, 42, 1)
|
|
1888
|
-
#
|
|
1889
|
-
def git_version
|
|
1890
|
-
self.class.cached_git_version(Git::Base.config.binary_path) do
|
|
1891
|
-
output = Git::Commands::Version.new(self).call.stdout
|
|
1892
|
-
Git::Version.parse(output)
|
|
1893
|
-
end
|
|
1894
|
-
end
|
|
1895
|
-
|
|
1896
|
-
# Class-level cache for git versions, keyed by binary path
|
|
1897
|
-
#
|
|
1898
|
-
# Thread-safe for JRuby/TruffleRuby where true parallelism exists.
|
|
1899
|
-
#
|
|
1900
|
-
# @api private
|
|
1901
|
-
#
|
|
1902
|
-
def self.cached_git_version(binary_path, &block)
|
|
1903
|
-
@git_version_cache_mutex.synchronize do
|
|
1904
|
-
@git_version_cache[binary_path] ||= block.call
|
|
1905
|
-
end
|
|
1906
|
-
end
|
|
1907
|
-
|
|
1908
|
-
# Clear the git version cache (primarily for testing)
|
|
1909
|
-
#
|
|
1910
|
-
# @api private
|
|
1911
|
-
#
|
|
1912
|
-
def self.clear_git_version_cache
|
|
1913
|
-
@git_version_cache_mutex.synchronize do
|
|
1914
|
-
@git_version_cache.clear
|
|
1915
|
-
end
|
|
1916
|
-
end
|
|
1917
|
-
|
|
1918
|
-
# Returns the current version of git, as an Array<Integer>
|
|
1919
|
-
#
|
|
1920
|
-
# @deprecated Use {Git.git_version} instead, which returns a {Git::Version} (not an Array).
|
|
1921
|
-
# For the legacy array shape, call: `Git.git_version.to_a`
|
|
1922
|
-
#
|
|
1923
|
-
def current_command_version
|
|
1924
|
-
Git::Deprecation.warn(
|
|
1925
|
-
'Git::Lib#current_command_version is deprecated and will be removed in 6.0. ' \
|
|
1926
|
-
'Use Git.git_version instead, which returns a Git::Version (not an Array). ' \
|
|
1927
|
-
'For the legacy array shape, call: Git.git_version.to_a'
|
|
1928
|
-
)
|
|
1929
|
-
git_version.to_a
|
|
1930
|
-
end
|
|
1931
|
-
|
|
1932
|
-
# Returns current_command_version <=> other_version
|
|
1933
|
-
#
|
|
1934
|
-
# @example
|
|
1935
|
-
# lib.compare_version_to(2, 41, 0) #=> 1
|
|
1936
|
-
# lib.compare_version_to(2, 42, 0) #=> 0
|
|
1937
|
-
# lib.compare_version_to(2, 43, 0) #=> -1
|
|
1938
|
-
#
|
|
1939
|
-
# @param other_version [Array<Integer>] the other version to compare to
|
|
1940
|
-
# @return [Integer] -1 if this version is less than other_version, 0 if equal, or 1 if greater than
|
|
1941
|
-
#
|
|
1942
|
-
# @deprecated Use {Git.git_version} with {Git::Version} comparison operators instead,
|
|
1943
|
-
# e.g. `Git.git_version <=> Git::Version.new(2, 41, 0)`
|
|
1944
|
-
#
|
|
1945
|
-
def compare_version_to(*other_version)
|
|
1946
|
-
Git::Deprecation.warn(
|
|
1947
|
-
'Git::Lib#compare_version_to is deprecated and will be removed in 6.0. ' \
|
|
1948
|
-
'Use Git.git_version with Git::Version comparison operators instead, ' \
|
|
1949
|
-
'e.g. Git.git_version <=> Git::Version.new(2, 41, 0)'
|
|
1950
|
-
)
|
|
1951
|
-
git_version.to_a <=> other_version
|
|
1952
|
-
end
|
|
1953
|
-
|
|
1954
|
-
# @deprecated Use {Git::MINIMUM_GIT_VERSION} constant instead, which returns a {Git::Version}
|
|
1955
|
-
# (not an Array). For the legacy array shape, call: `Git::MINIMUM_GIT_VERSION.to_a.first(2)`
|
|
1956
|
-
#
|
|
1957
|
-
def required_command_version
|
|
1958
|
-
Git::Deprecation.warn(
|
|
1959
|
-
'Git::Lib#required_command_version is deprecated and will be removed in 6.0. ' \
|
|
1960
|
-
'Use the Git::MINIMUM_GIT_VERSION constant instead, which returns a Git::Version ' \
|
|
1961
|
-
'(not an Array). For the legacy array shape, call: Git::MINIMUM_GIT_VERSION.to_a.first(2)'
|
|
1962
|
-
)
|
|
1963
|
-
Git::MINIMUM_GIT_VERSION.to_a.first(2)
|
|
1964
|
-
end
|
|
1965
|
-
|
|
1966
|
-
# @deprecated For a boolean check, use `Git.git_version >= Git::MINIMUM_GIT_VERSION`.
|
|
1967
|
-
# For enforcement, no action is needed: {Git::Commands::Base#call} automatically
|
|
1968
|
-
# invokes `validate_version!`, which raises {Git::VersionError} on failure.
|
|
1969
|
-
#
|
|
1970
|
-
def meets_required_version?
|
|
1971
|
-
Git::Deprecation.warn(
|
|
1972
|
-
'Git::Lib#meets_required_version? is deprecated and will be removed in 6.0. ' \
|
|
1973
|
-
'For a boolean check, use: Git.git_version >= Git::MINIMUM_GIT_VERSION. ' \
|
|
1974
|
-
'For enforcement, no action is needed: Git::Commands::Base#call automatically ' \
|
|
1975
|
-
'invokes validate_version!, which raises Git::VersionError on failure.'
|
|
1976
|
-
)
|
|
1977
|
-
git_version >= Git::MINIMUM_GIT_VERSION
|
|
1978
|
-
end
|
|
1979
|
-
|
|
1980
|
-
# @deprecated Version validation is now handled automatically by
|
|
1981
|
-
# {Git::Commands::Base#validate_version!}, which raises {Git::VersionError} on failure.
|
|
1982
|
-
# Callers wanting the old warn-and-continue behavior must implement it themselves
|
|
1983
|
-
# using: `Git.git_version >= Git::MINIMUM_GIT_VERSION`.
|
|
1984
|
-
#
|
|
1985
|
-
def self.warn_if_old_command(_lib) # rubocop:disable Metrics/MethodLength, Naming/PredicateMethod
|
|
1986
|
-
Git::Deprecation.warn(
|
|
1987
|
-
'Git::Lib.warn_if_old_command is deprecated and will be removed in 6.0. ' \
|
|
1988
|
-
'Version validation is now handled automatically by Git::Commands::Base#validate_version!, ' \
|
|
1989
|
-
'which RAISES Git::VersionError on failure (the old method only printed a warning ' \
|
|
1990
|
-
'once per process and continued). Callers wanting the old warn-and-continue behavior ' \
|
|
1991
|
-
'must implement it themselves using: Git.git_version >= Git::MINIMUM_GIT_VERSION.'
|
|
1992
|
-
)
|
|
1993
|
-
|
|
1994
|
-
return true if @version_checked
|
|
1995
|
-
|
|
1996
|
-
@version_checked = true
|
|
1997
|
-
git_version = Git.git_version
|
|
1998
|
-
unless git_version >= Git::MINIMUM_GIT_VERSION
|
|
1999
|
-
warn "The git gem requires git #{Git::MINIMUM_GIT_VERSION} or later, " \
|
|
2000
|
-
"but only found #{git_version}. You should probably upgrade."
|
|
2001
|
-
end
|
|
2002
|
-
true
|
|
2003
|
-
end
|
|
2004
|
-
|
|
2005
|
-
COMMAND_CAPTURING_ARG_DEFAULTS = {
|
|
2006
|
-
in: nil,
|
|
2007
|
-
out: nil,
|
|
2008
|
-
err: nil,
|
|
2009
|
-
normalize: true,
|
|
2010
|
-
chomp: true,
|
|
2011
|
-
merge: false,
|
|
2012
|
-
chdir: nil,
|
|
2013
|
-
timeout: nil, # Don't set to Git.config.timeout here since it is mutable
|
|
2014
|
-
env: {},
|
|
2015
|
-
raise_on_failure: true
|
|
2016
|
-
}.freeze
|
|
2017
|
-
|
|
2018
|
-
STATIC_GLOBAL_OPTS = %w[
|
|
2019
|
-
-c core.quotePath=true
|
|
2020
|
-
-c core.editor=false
|
|
2021
|
-
-c color.ui=false
|
|
2022
|
-
-c color.advice=false
|
|
2023
|
-
-c color.diff=false
|
|
2024
|
-
-c color.grep=false
|
|
2025
|
-
-c color.push=false
|
|
2026
|
-
-c color.remote=false
|
|
2027
|
-
-c color.showBranch=false
|
|
2028
|
-
-c color.status=false
|
|
2029
|
-
-c color.transport=false
|
|
2030
|
-
].freeze
|
|
2031
|
-
|
|
2032
|
-
# Runs a git command and returns the result
|
|
2033
|
-
#
|
|
2034
|
-
# By default, raises {Git::FailedError} if the command exits with a non-zero
|
|
2035
|
-
# status. Pass `raise_on_failure: false` to suppress this behavior.
|
|
2036
|
-
#
|
|
2037
|
-
# @overload command_capturing(*args, **options_hash)
|
|
2038
|
-
# Runs a git command and returns the result
|
|
2039
|
-
#
|
|
2040
|
-
# Args should exclude the 'git' command itself and global options.
|
|
2041
|
-
# Remember to splat the arguments if given as an array.
|
|
2042
|
-
#
|
|
2043
|
-
# @example Run git log
|
|
2044
|
-
# result = command_capturing('log', '--pretty=oneline')
|
|
2045
|
-
# result.stdout #=> "abc123 First commit\ndef456 Second commit\n"
|
|
2046
|
-
#
|
|
2047
|
-
# @example Using an array of arguments
|
|
2048
|
-
# args = ['log', '--pretty=oneline']
|
|
2049
|
-
# result = command_capturing(*args)
|
|
2050
|
-
#
|
|
2051
|
-
# @example Suppress raising on failure
|
|
2052
|
-
# result = command_capturing('show', 'nonexistent', raise_on_failure: false)
|
|
2053
|
-
# result.status.success? #=> false
|
|
2054
|
-
#
|
|
2055
|
-
# @param args [Array<String>] the command and its arguments
|
|
2056
|
-
#
|
|
2057
|
-
# @param options_hash [Hash] the options to pass to the command
|
|
2058
|
-
#
|
|
2059
|
-
# @option options_hash [IO, nil] :in the IO object to use as stdin for the command, or nil to
|
|
2060
|
-
# inherit the parent process stdin. Must be a real IO object with a file descriptor.
|
|
2061
|
-
#
|
|
2062
|
-
# @option options_hash [IO, String, #write, nil] :out the destination for captured stdout
|
|
2063
|
-
#
|
|
2064
|
-
# @option options_hash [IO, String, #write, nil] :err the destination for captured stderr
|
|
2065
|
-
#
|
|
2066
|
-
# @option options_hash [Boolean, nil] :normalize (true) normalize the output encoding to UTF-8
|
|
2067
|
-
#
|
|
2068
|
-
# @option options_hash [Boolean, nil] :chomp (true) remove trailing newlines from the output
|
|
2069
|
-
#
|
|
2070
|
-
# @option options_hash [Boolean, nil] :merge (false) merge stdout and stderr into a single output
|
|
2071
|
-
#
|
|
2072
|
-
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2073
|
-
#
|
|
2074
|
-
# @option options_hash [Hash] :env additional environment variable overrides for this command
|
|
2075
|
-
#
|
|
2076
|
-
# @option options_hash [Boolean, nil] :raise_on_failure (true) whether to raise on non-zero exit
|
|
2077
|
-
#
|
|
2078
|
-
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
|
|
2079
|
-
#
|
|
2080
|
-
# If timeout is nil, the global timeout from {Git::Config} is used.
|
|
2081
|
-
#
|
|
2082
|
-
# If timeout is zero, the timeout will not be enforced.
|
|
2083
|
-
#
|
|
2084
|
-
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
|
|
2085
|
-
#
|
|
2086
|
-
# If the command does not respond to SIGKILL, it will hang this method.
|
|
2087
|
-
#
|
|
2088
|
-
# @note Individual command classes (under {Git::Commands}) can selectively
|
|
2089
|
-
# expose `:timeout` and `:env` to their callers by declaring them as
|
|
2090
|
-
# execution options in their Arguments DSL definition and forwarding
|
|
2091
|
-
# them to this method. See {Git::Commands::Clone#call} for an example
|
|
2092
|
-
# of a command that exposes `:timeout`.
|
|
2093
|
-
#
|
|
2094
|
-
# @see Git::CommandLine::Capturing#run
|
|
2095
|
-
#
|
|
2096
|
-
# @see #command_line_capturing
|
|
2097
|
-
#
|
|
2098
|
-
# @return [Git::CommandLineResult] the result of the command
|
|
2099
|
-
#
|
|
2100
|
-
# @raise [ArgumentError] if an unknown option is passed
|
|
2101
|
-
#
|
|
2102
|
-
# @raise [Git::FailedError] if the command failed (when raise_on_failure is true)
|
|
2103
|
-
#
|
|
2104
|
-
# @raise [Git::SignaledError] if the command was signaled
|
|
2105
|
-
#
|
|
2106
|
-
# @raise [Git::TimeoutError] if the command times out
|
|
2107
|
-
#
|
|
2108
|
-
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2109
|
-
#
|
|
2110
|
-
# The exception's `result` attribute is a {Git::CommandLineResult} which will
|
|
2111
|
-
# contain the result of the command including the exit status, stdout, and
|
|
2112
|
-
# stderr.
|
|
2113
|
-
#
|
|
2114
|
-
def command_capturing(*, **options_hash)
|
|
2115
|
-
options_hash = COMMAND_CAPTURING_ARG_DEFAULTS.merge(options_hash)
|
|
2116
|
-
options_hash[:timeout] ||= Git.config.timeout
|
|
2117
|
-
|
|
2118
|
-
extra_options = options_hash.keys - COMMAND_CAPTURING_ARG_DEFAULTS.keys
|
|
2119
|
-
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2120
|
-
|
|
2121
|
-
env_overrides = options_hash.delete(:env)
|
|
2122
|
-
raise_on_failure = options_hash.delete(:raise_on_failure)
|
|
2123
|
-
command_line_capturing.run(*, raise_on_failure: raise_on_failure, env: env_overrides, **options_hash)
|
|
2124
|
-
end
|
|
2125
|
-
|
|
2126
|
-
COMMAND_STREAMING_ARG_DEFAULTS = {
|
|
2127
|
-
in: nil,
|
|
2128
|
-
out: nil,
|
|
2129
|
-
err: nil,
|
|
2130
|
-
chdir: nil,
|
|
2131
|
-
timeout: nil,
|
|
2132
|
-
env: {},
|
|
2133
|
-
raise_on_failure: true
|
|
2134
|
-
}.freeze
|
|
2135
|
-
|
|
2136
|
-
# Runs a git command using the streaming (non-capturing) execution path
|
|
2137
|
-
#
|
|
2138
|
-
# Unlike {#command_capturing}, stdout is NOT buffered in memory. It is
|
|
2139
|
-
# written only to the IO object provided via the `out:` option. Stderr is
|
|
2140
|
-
# captured internally via a StringIO for error diagnostics.
|
|
2141
|
-
#
|
|
2142
|
-
# Use this entry point when you want to stream large output (e.g. blob
|
|
2143
|
-
# content from cat-file) without creating memory pressure.
|
|
2144
|
-
#
|
|
2145
|
-
# @overload command_streaming(*args, **options_hash)
|
|
2146
|
-
#
|
|
2147
|
-
# @param args [Array<String>] the git command and its arguments
|
|
2148
|
-
#
|
|
2149
|
-
# @param options_hash [Hash] the options to pass to the command
|
|
2150
|
-
#
|
|
2151
|
-
# @option options_hash [IO, nil] :in stdin IO object
|
|
2152
|
-
#
|
|
2153
|
-
# @option options_hash [#write, nil] :out destination for streamed stdout
|
|
2154
|
-
#
|
|
2155
|
-
# @option options_hash [#write, nil] :err an optional additional destination to receive stderr output
|
|
2156
|
-
# in real time. Stderr is always captured internally; when `err:` is supplied, writes are teed
|
|
2157
|
-
# to both the internal buffer and this destination. `result.stderr` always reflects the internal capture.
|
|
2158
|
-
#
|
|
2159
|
-
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2160
|
-
#
|
|
2161
|
-
# @option options_hash [Hash] :env additional environment variable overrides for this command
|
|
2162
|
-
#
|
|
2163
|
-
# @option options_hash [Boolean, nil] :raise_on_failure (true) whether to raise on non-zero exit
|
|
2164
|
-
#
|
|
2165
|
-
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command
|
|
2166
|
-
#
|
|
2167
|
-
# If nil, the global timeout from {Git::Config} is used.
|
|
2168
|
-
#
|
|
2169
|
-
# @return [Git::CommandLineResult] the result of the command
|
|
2170
|
-
#
|
|
2171
|
-
# `result.stdout` will always be `''` — stdout was streamed to `out:`.
|
|
2172
|
-
# `result.stderr` contains any stderr output captured for diagnostics.
|
|
2173
|
-
#
|
|
2174
|
-
# @raise [ArgumentError] if an unknown option is passed
|
|
2175
|
-
#
|
|
2176
|
-
# @raise [Git::FailedError] if the command failed (when raise_on_failure is true)
|
|
2177
|
-
#
|
|
2178
|
-
# @raise [Git::SignaledError] if the command was signaled
|
|
2179
|
-
#
|
|
2180
|
-
# @raise [Git::TimeoutError] if the command times out
|
|
2181
|
-
#
|
|
2182
|
-
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2183
|
-
#
|
|
2184
|
-
# @see Git::CommandLine::Streaming#run
|
|
2185
|
-
#
|
|
2186
|
-
# @see #command_line_streaming
|
|
2187
|
-
#
|
|
2188
|
-
def command_streaming(*, **options_hash)
|
|
2189
|
-
options_hash = COMMAND_STREAMING_ARG_DEFAULTS.merge(options_hash)
|
|
2190
|
-
options_hash[:timeout] ||= Git.config.timeout
|
|
2191
|
-
|
|
2192
|
-
extra_options = options_hash.keys - COMMAND_STREAMING_ARG_DEFAULTS.keys
|
|
2193
|
-
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2194
|
-
|
|
2195
|
-
env_overrides = options_hash.delete(:env)
|
|
2196
|
-
raise_on_failure = options_hash.delete(:raise_on_failure)
|
|
2197
|
-
command_line_streaming.run(*, raise_on_failure: raise_on_failure, env: env_overrides, **options_hash)
|
|
2198
|
-
end
|
|
2199
|
-
|
|
2200
|
-
private
|
|
2201
|
-
|
|
2202
|
-
def migrate_clean_legacy_options(opts)
|
|
2203
|
-
opts = deprecate_clean_option(opts, :ff, ':ff option is deprecated. Use force: 2 instead.')
|
|
2204
|
-
deprecate_clean_option(opts, :force_force, ':force_force option is deprecated. Use force: 2 instead.')
|
|
2205
|
-
end
|
|
2206
|
-
|
|
2207
|
-
def deprecate_clean_option(opts, key, message)
|
|
2208
|
-
return opts unless opts.key?(key)
|
|
2209
|
-
|
|
2210
|
-
opts = opts.dup
|
|
2211
|
-
deprecated_value = opts.delete(key)
|
|
2212
|
-
validate_deprecated_clean_option_value!(key, deprecated_value)
|
|
2213
|
-
|
|
2214
|
-
Git::Deprecation.warn(message)
|
|
2215
|
-
return opts unless deprecated_value
|
|
2216
|
-
|
|
2217
|
-
opts[:force] = merge_clean_force_option(opts[:force], force_specified: force_option_specified?(opts))
|
|
2218
|
-
opts
|
|
2219
|
-
end
|
|
2220
|
-
|
|
2221
|
-
def force_option_specified?(opts)
|
|
2222
|
-
opts.key?(:force) && !opts[:force].nil?
|
|
2223
|
-
end
|
|
2224
|
-
|
|
2225
|
-
def validate_deprecated_clean_option_value!(key, value)
|
|
2226
|
-
return if value.nil? || value == true || value == false
|
|
2227
|
-
|
|
2228
|
-
raise ArgumentError, "#{key} option only accepts true, false, or nil"
|
|
2229
|
-
end
|
|
2230
|
-
|
|
2231
|
-
def merge_clean_force_option(existing_force, force_specified: false)
|
|
2232
|
-
return 2 unless force_specified
|
|
2233
|
-
|
|
2234
|
-
normalized_force = normalize_clean_force_option(existing_force)
|
|
2235
|
-
|
|
2236
|
-
case normalized_force
|
|
2237
|
-
when Integer then merge_integer_clean_force_option(normalized_force)
|
|
2238
|
-
when false
|
|
2239
|
-
2
|
|
2240
|
-
else
|
|
2241
|
-
normalized_force
|
|
2242
|
-
end
|
|
2243
|
-
end
|
|
2244
|
-
|
|
2245
|
-
def merge_integer_clean_force_option(normalized_force)
|
|
2246
|
-
return normalized_force if normalized_force < 1
|
|
2247
|
-
|
|
2248
|
-
[normalized_force, 2].max
|
|
2249
|
-
end
|
|
2250
|
-
|
|
2251
|
-
def normalize_clean_force_option(value)
|
|
2252
|
-
case value
|
|
2253
|
-
when true then 1
|
|
2254
|
-
else value
|
|
2255
|
-
end
|
|
2256
|
-
end
|
|
2257
|
-
|
|
2258
|
-
# Build a result hash from clone options for Git::Base.new
|
|
2259
|
-
#
|
|
2260
|
-
# Parses the clone directory from the git command's stderr output, which
|
|
2261
|
-
# contains either:
|
|
2262
|
-
# Cloning into '<directory>'...
|
|
2263
|
-
# Cloning into bare repository '<directory>'...
|
|
2264
|
-
#
|
|
2265
|
-
# @param command_line_result [Git::CommandLineResult] the result of the git clone command
|
|
2266
|
-
#
|
|
2267
|
-
# @param opts [Hash] execution context options (:log, :git_ssh)
|
|
2268
|
-
#
|
|
2269
|
-
# @return [Hash] result hash with directory, log, and git_ssh keys
|
|
2270
|
-
#
|
|
2271
|
-
def build_clone_result(command_line_result, opts)
|
|
2272
|
-
clone_dir, bare = parse_clone_stderr(command_line_result.stderr)
|
|
2273
|
-
result = bare ? { repository: clone_dir } : { working_directory: clone_dir }
|
|
2274
|
-
result[:log] = opts[:log] if opts[:log]
|
|
2275
|
-
result[:git_ssh] = opts[:git_ssh] if opts.key?(:git_ssh)
|
|
2276
|
-
result
|
|
2277
|
-
end
|
|
2278
|
-
|
|
2279
|
-
# Parse the clone directory and bare status from git clone's stderr output
|
|
2280
|
-
#
|
|
2281
|
-
# Git outputs the directory in an unencoded way (no `core.quotePath` or
|
|
2282
|
-
# similar escaping applies to clone's stderr message). The message format
|
|
2283
|
-
# is always:
|
|
2284
|
-
#
|
|
2285
|
-
# Cloning into '<directory>'...
|
|
2286
|
-
# Cloning into bare repository '<directory>'...
|
|
2287
|
-
#
|
|
2288
|
-
# Because the directory name is not escaped, a name containing the
|
|
2289
|
-
# literal sequence `'...` (single-quote followed by three dots) would
|
|
2290
|
-
# be ambiguous. In practice this is extremely unlikely.
|
|
2291
|
-
#
|
|
2292
|
-
# @param stderr [String] stderr output from git clone
|
|
2293
|
-
#
|
|
2294
|
-
# @return [Array(String, Boolean)] the clone directory and whether it's a bare repository
|
|
2295
|
-
#
|
|
2296
|
-
# @raise [Git::UnexpectedResultError] if the stderr output cannot be parsed
|
|
2297
|
-
#
|
|
2298
|
-
def parse_clone_stderr(stderr)
|
|
2299
|
-
match = stderr.match(/Cloning into (?:(bare repository) )?'(.+)'\.\.\./)
|
|
2300
|
-
raise Git::UnexpectedResultError, "Unable to determine clone directory from: #{stderr}" unless match
|
|
2301
|
-
|
|
2302
|
-
[match[2], !match[1].nil?]
|
|
2303
|
-
end
|
|
2304
|
-
|
|
2305
|
-
# Prefixes clone result path values with the chdir directory.
|
|
2306
|
-
#
|
|
2307
|
-
# Mutates the given result hash in place, updating any :working_directory
|
|
2308
|
-
# and :repository entries to be rooted under the provided +chdir+ directory.
|
|
2309
|
-
# If +chdir+ is nil, the hash is left unchanged.
|
|
2310
|
-
#
|
|
2311
|
-
# @param result [Hash] clone result hash containing path information
|
|
2312
|
-
# @param chdir [String, nil] directory under which the repository was cloned
|
|
2313
|
-
# @return [nil]
|
|
2314
|
-
#
|
|
2315
|
-
def prefix_clone_result_paths!(result, chdir)
|
|
2316
|
-
return unless chdir
|
|
2317
|
-
|
|
2318
|
-
%i[working_directory repository].each do |key|
|
|
2319
|
-
result[key] = File.join(chdir, result[key]) if result.key?(key)
|
|
2320
|
-
end
|
|
2321
|
-
end
|
|
2322
|
-
|
|
2323
|
-
# Handles the deprecated :path option for Git::Lib#clone.
|
|
2324
|
-
#
|
|
2325
|
-
# If opts contains :path, emits a deprecation warning and migrates the
|
|
2326
|
-
# value to :chdir (unless :chdir is already set). Mutates opts in place.
|
|
2327
|
-
#
|
|
2328
|
-
# @param opts [Hash] clone options, possibly containing :path
|
|
2329
|
-
# @return [nil]
|
|
2330
|
-
#
|
|
2331
|
-
def deprecate_clone_path_option!(opts)
|
|
2332
|
-
return unless opts.key?(:path)
|
|
2333
|
-
|
|
2334
|
-
Git::Deprecation.warn('The :path option for Git::Lib#clone is deprecated, use :chdir instead')
|
|
2335
|
-
path = opts.delete(:path)
|
|
2336
|
-
opts[:chdir] ||= path
|
|
2337
|
-
end
|
|
2338
|
-
|
|
2339
|
-
def deprecate_clone_recursive_option!(opts)
|
|
2340
|
-
return unless opts.key?(:recursive)
|
|
2341
|
-
|
|
2342
|
-
Git::Deprecation.warn('The :recursive option for Git::Lib#clone is deprecated, use :recurse_submodules instead')
|
|
2343
|
-
opts[:recurse_submodules] = opts.delete(:recursive)
|
|
2344
|
-
end
|
|
2345
|
-
|
|
2346
|
-
def deprecate_clone_remote_option!(opts)
|
|
2347
|
-
return unless opts.key?(:remote)
|
|
2348
|
-
|
|
2349
|
-
Git::Deprecation.warn('The :remote option for Git::Lib#clone is deprecated, use :origin instead')
|
|
2350
|
-
opts[:origin] = opts.delete(:remote)
|
|
2351
|
-
end
|
|
2352
|
-
|
|
2353
|
-
def deprecate_clone_options!(opts)
|
|
2354
|
-
deprecate_clone_path_option!(opts)
|
|
2355
|
-
deprecate_clone_recursive_option!(opts)
|
|
2356
|
-
deprecate_clone_remote_option!(opts)
|
|
2357
|
-
end
|
|
2358
|
-
|
|
2359
|
-
def deprecate_commit_add_all_option!(opts)
|
|
2360
|
-
return unless opts.key?(:add_all)
|
|
2361
|
-
|
|
2362
|
-
Git::Deprecation.warn('The :add_all option for Git::Lib#commit is deprecated, use :all instead')
|
|
2363
|
-
opts[:all] = opts.delete(:add_all)
|
|
2364
|
-
end
|
|
2365
|
-
|
|
2366
|
-
# Extracts execution context options from clone options.
|
|
2367
|
-
#
|
|
2368
|
-
# @param opts [Hash] clone options
|
|
2369
|
-
# @return [Hash] hash with :log and :git_ssh keys if present
|
|
2370
|
-
#
|
|
2371
|
-
def extract_clone_execution_context_opts(opts)
|
|
2372
|
-
result = {}
|
|
2373
|
-
result[:log] = opts.delete(:log) if opts[:log]
|
|
2374
|
-
result[:git_ssh] = opts.delete(:git_ssh) if opts.key?(:git_ssh)
|
|
2375
|
-
result
|
|
2376
|
-
end
|
|
2377
|
-
|
|
2378
|
-
# Translate legacy merge option names to new interface
|
|
2379
|
-
#
|
|
2380
|
-
# @param opts [Hash] options with possibly legacy keys
|
|
2381
|
-
# @return [Hash] options with new keys
|
|
2382
|
-
#
|
|
2383
|
-
def translate_merge_options(opts)
|
|
2384
|
-
result = opts.dup
|
|
2385
|
-
|
|
2386
|
-
# :message => 'msg' becomes :m => 'msg' (git merge uses -m, not --message)
|
|
2387
|
-
result[:m] = result.delete(:message) if result.key?(:message)
|
|
2388
|
-
|
|
2389
|
-
result
|
|
2390
|
-
end
|
|
2391
|
-
|
|
2392
|
-
# Extract name-status data from --raw output lines
|
|
2393
|
-
#
|
|
2394
|
-
# Raw lines have the format:
|
|
2395
|
-
# :old_mode new_mode old_sha new_sha status\tpath
|
|
2396
|
-
# or for renames/copies:
|
|
2397
|
-
# :old_mode new_mode old_sha new_sha Rxx\told_path\tnew_path
|
|
2398
|
-
#
|
|
2399
|
-
# @param output [String] raw diff output
|
|
2400
|
-
#
|
|
2401
|
-
# @return [Hash] mapping of file paths to status tokens
|
|
2402
|
-
#
|
|
2403
|
-
def extract_name_status_from_raw(output)
|
|
2404
|
-
output.split("\n").each_with_object({}) do |line, memo|
|
|
2405
|
-
next unless line.start_with?(':')
|
|
2406
|
-
|
|
2407
|
-
parts = line[1..].split(/\s+/, 5)
|
|
2408
|
-
status_and_paths = parts[4].split("\t")
|
|
2409
|
-
status = status_and_paths[0]
|
|
2410
|
-
path = status_and_paths.length > 2 ? status_and_paths[2] : status_and_paths[1]
|
|
2411
|
-
memo[unescape_quoted_path(path)] = status
|
|
2412
|
-
end
|
|
2413
|
-
end
|
|
2414
|
-
|
|
2415
|
-
# Extract only the patch text from combined numstat + shortstat + patch output
|
|
2416
|
-
#
|
|
2417
|
-
# When {Git::Commands::Diff} is called with `patch: true, numstat: true, shortstat: true`,
|
|
2418
|
-
# the output contains numstat, shortstat, and patch sections. This method extracts
|
|
2419
|
-
# only the patch portion (starting at "diff --git").
|
|
2420
|
-
#
|
|
2421
|
-
# @param output [String] combined command output
|
|
2422
|
-
#
|
|
2423
|
-
# @return [String] only the patch text
|
|
2424
|
-
#
|
|
2425
|
-
def extract_patch_text(output)
|
|
2426
|
-
match = output.match(/^diff --git /m)
|
|
2427
|
-
match ? output[match.begin(0)..] : output
|
|
2428
|
-
end
|
|
2429
|
-
|
|
2430
|
-
# Extract only the numstat lines from combined numstat + shortstat output
|
|
2431
|
-
#
|
|
2432
|
-
# When {Git::Commands::Diff} is called with `numstat: true, shortstat: true`,
|
|
2433
|
-
# the output contains numstat lines followed by a shortstat summary line. This method
|
|
2434
|
-
# filters out the shortstat line and empty lines, returning only the numstat lines.
|
|
2435
|
-
#
|
|
2436
|
-
# @param output [String] combined command output
|
|
2437
|
-
#
|
|
2438
|
-
# @return [Array<String>] only the numstat lines
|
|
2439
|
-
#
|
|
2440
|
-
def extract_numstat_lines(output)
|
|
2441
|
-
output.split("\n").reject { |l| l.empty? || l.match?(/^\s*\d+\s+files?\s+changed/) }
|
|
2442
|
-
end
|
|
2443
|
-
|
|
2444
|
-
def build_args(opts, option_map)
|
|
2445
|
-
Git::ArgsBuilder.new(opts, option_map).build
|
|
2446
|
-
end
|
|
2447
|
-
|
|
2448
|
-
def validate_tag_options!(opts)
|
|
2449
|
-
needs_message = %i[a annotate s sign u local_user].any? { |k| opts[k] }
|
|
2450
|
-
has_message = opts[:m] || opts[:message]
|
|
2451
|
-
|
|
2452
|
-
return unless needs_message && !has_message
|
|
2453
|
-
|
|
2454
|
-
raise ArgumentError, 'Cannot create an annotated or signed tag without a message.'
|
|
2455
|
-
end
|
|
2456
|
-
|
|
2457
|
-
def delete_tag(name)
|
|
2458
|
-
result = Git::Commands::Tag::Delete.new(self).call(name)
|
|
2459
|
-
raise Git::FailedError, result if result.status.exitstatus.positive?
|
|
2460
|
-
|
|
2461
|
-
result.stdout
|
|
2462
|
-
end
|
|
2463
|
-
|
|
2464
|
-
def create_tag(name, target, opts)
|
|
2465
|
-
Git::Commands::Tag::Create.new(self).call(name, target, **opts).stdout
|
|
2466
|
-
end
|
|
2467
|
-
|
|
2468
|
-
def initialize_from_base(base_object)
|
|
2469
|
-
@git_dir = base_object.repo.to_s
|
|
2470
|
-
@git_index_file = base_object.index&.to_s
|
|
2471
|
-
@git_work_dir = base_object.dir&.to_s
|
|
2472
|
-
@git_ssh = base_object.git_ssh
|
|
2473
|
-
end
|
|
2474
|
-
|
|
2475
|
-
def initialize_from_hash(base_hash)
|
|
2476
|
-
@git_dir = base_hash[:repository]
|
|
2477
|
-
@git_index_file = base_hash[:index]
|
|
2478
|
-
@git_work_dir = base_hash[:working_directory]
|
|
2479
|
-
@git_ssh = base_hash.key?(:git_ssh) ? base_hash[:git_ssh] : :use_global_config
|
|
2480
|
-
end
|
|
2481
|
-
|
|
2482
|
-
def process_commit_headers(data)
|
|
2483
|
-
headers = { 'parent' => [] } # Pre-initialize for multiple parents
|
|
2484
|
-
each_cat_file_header(data) do |key, value|
|
|
2485
|
-
if key == 'parent'
|
|
2486
|
-
headers['parent'] << value
|
|
2487
|
-
else
|
|
2488
|
-
headers[key] = value
|
|
2489
|
-
end
|
|
2490
|
-
end
|
|
2491
|
-
headers
|
|
2492
|
-
end
|
|
2493
|
-
|
|
2494
|
-
def get_branch_state(branch_name)
|
|
2495
|
-
Git::Commands::RevParse.new(self).call(branch_name, verify: true, quiet: true)
|
|
2496
|
-
:active
|
|
2497
|
-
rescue Git::FailedError => e
|
|
2498
|
-
# An exit status of 1 with empty stderr from `rev-parse --verify`
|
|
2499
|
-
# indicates a ref that exists but does not yet point to a commit.
|
|
2500
|
-
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
|
|
2501
|
-
|
|
2502
|
-
:unborn
|
|
2503
|
-
end
|
|
2504
|
-
|
|
2505
|
-
def normalize_grep_opts(opts)
|
|
2506
|
-
opts = opts.dup
|
|
2507
|
-
opts[:pathspec] = opts.delete(:path_limiter) if opts.key?(:path_limiter)
|
|
2508
|
-
opts
|
|
2509
|
-
end
|
|
2510
|
-
|
|
2511
|
-
def parse_grep_output(output)
|
|
2512
|
-
Git::Parsers::Grep.parse(output)
|
|
2513
|
-
end
|
|
2514
|
-
|
|
2515
|
-
def parse_diff_stats_output(lines)
|
|
2516
|
-
file_stats = parse_stat_lines(lines)
|
|
2517
|
-
build_final_stats_hash(file_stats)
|
|
2518
|
-
end
|
|
2519
|
-
|
|
2520
|
-
def parse_stat_lines(lines)
|
|
2521
|
-
lines.map do |line|
|
|
2522
|
-
insertions_s, deletions_s, filename = split_status_line(line)
|
|
2523
|
-
{
|
|
2524
|
-
filename: filename,
|
|
2525
|
-
insertions: insertions_s.to_i,
|
|
2526
|
-
deletions: deletions_s.to_i
|
|
2527
|
-
}
|
|
2528
|
-
end
|
|
2529
|
-
end
|
|
2530
|
-
|
|
2531
|
-
def split_status_line(line)
|
|
2532
|
-
parts = line.split("\t")
|
|
2533
|
-
parts[-1] = unescape_quoted_path(parts[-1]) if parts.any?
|
|
2534
|
-
parts
|
|
2535
|
-
end
|
|
2536
|
-
|
|
2537
|
-
def parse_raw_diff_output(stdout)
|
|
2538
|
-
stdout.split("\n").each_with_object({}) do |line, memo|
|
|
2539
|
-
info, file = split_status_line(line)
|
|
2540
|
-
mode_src, mode_dest, sha_src, sha_dest, type = info.split
|
|
2541
|
-
memo[file] = {
|
|
2542
|
-
mode_index: mode_dest, mode_repo: mode_src.to_s[1, 7],
|
|
2543
|
-
path: file, sha_repo: sha_src, sha_index: sha_dest,
|
|
2544
|
-
type: type
|
|
2545
|
-
}
|
|
2546
|
-
end
|
|
2547
|
-
end
|
|
2548
|
-
|
|
2549
|
-
def build_final_stats_hash(file_stats)
|
|
2550
|
-
{
|
|
2551
|
-
total: build_total_stats(file_stats),
|
|
2552
|
-
files: build_files_hash(file_stats)
|
|
2553
|
-
}
|
|
2554
|
-
end
|
|
2555
|
-
|
|
2556
|
-
def build_total_stats(file_stats)
|
|
2557
|
-
insertions = file_stats.sum { |s| s[:insertions] }
|
|
2558
|
-
deletions = file_stats.sum { |s| s[:deletions] }
|
|
2559
|
-
{
|
|
2560
|
-
insertions: insertions,
|
|
2561
|
-
deletions: deletions,
|
|
2562
|
-
lines: insertions + deletions,
|
|
2563
|
-
files: file_stats.size
|
|
2564
|
-
}
|
|
2565
|
-
end
|
|
2566
|
-
|
|
2567
|
-
def build_files_hash(file_stats)
|
|
2568
|
-
file_stats.to_h { |s| [s[:filename], s.slice(:insertions, :deletions)] }
|
|
2569
|
-
end
|
|
2570
|
-
|
|
2571
|
-
def parse_ls_remote_output(lines)
|
|
2572
|
-
lines.each_with_object(Hash.new { |h, k| h[k] = {} }) do |line, hsh|
|
|
2573
|
-
type, name, value = parse_ls_remote_line(line)
|
|
2574
|
-
if name
|
|
2575
|
-
hsh[type][name] = value
|
|
2576
|
-
else # Handles the HEAD entry, which has no name
|
|
2577
|
-
hsh[type].update(value)
|
|
2578
|
-
end
|
|
2579
|
-
end
|
|
2580
|
-
end
|
|
2581
|
-
|
|
2582
|
-
def parse_ls_remote_line(line)
|
|
2583
|
-
sha, info = line.split("\t", 2)
|
|
2584
|
-
ref, type, name = info.split('/', 3)
|
|
2585
|
-
|
|
2586
|
-
type ||= 'head'
|
|
2587
|
-
type = 'branches' if type == 'heads'
|
|
2588
|
-
|
|
2589
|
-
value = { ref: ref, sha: sha }
|
|
2590
|
-
|
|
2591
|
-
[type, name, value]
|
|
2592
|
-
end
|
|
2593
|
-
|
|
2594
|
-
# Convert a StashInfo to the legacy [index, message] format
|
|
2595
|
-
#
|
|
2596
|
-
# The legacy format strips the "WIP on <branch>:" or "On <branch>:" prefix
|
|
2597
|
-
# from the message and returns only the suffix.
|
|
2598
|
-
#
|
|
2599
|
-
# @param info [Git::StashInfo] the stash info object
|
|
2600
|
-
# @return [Array(Integer, String)] `[index, message]` pair with prefix stripped
|
|
2601
|
-
#
|
|
2602
|
-
# @api private
|
|
2603
|
-
#
|
|
2604
|
-
def stash_info_to_legacy(info, index = info.index)
|
|
2605
|
-
full_message = info.message
|
|
2606
|
-
match_data = full_message.match(/^[^:]+:(.*)$/)
|
|
2607
|
-
message = match_data ? match_data[1] : full_message
|
|
2608
|
-
|
|
2609
|
-
[index, message.strip]
|
|
2610
|
-
end
|
|
2611
|
-
|
|
2612
|
-
# Streams the staged content of a file at a given index stage to an IO object
|
|
2613
|
-
#
|
|
2614
|
-
# Uses the streaming execution path so content is written directly to `out_io`
|
|
2615
|
-
# without being buffered in memory.
|
|
2616
|
-
#
|
|
2617
|
-
# @api private
|
|
2618
|
-
#
|
|
2619
|
-
# @param path [String] the path to the file in the index
|
|
2620
|
-
#
|
|
2621
|
-
# @param stage [Integer] the index stage to read (e.g., `1` ancestor, `2` ours, `3` theirs)
|
|
2622
|
-
#
|
|
2623
|
-
# @param out_io [IO] the `IO` object to stream the staged content into
|
|
2624
|
-
#
|
|
2625
|
-
# @return [IO] `out_io`, as passed in
|
|
2626
|
-
#
|
|
2627
|
-
# @raise [Git::FailedError] if the object does not exist or git exits non-zero
|
|
2628
|
-
#
|
|
2629
|
-
# @raise [Git::TimeoutError] if the command exceeds the configured timeout
|
|
2630
|
-
#
|
|
2631
|
-
def write_staged_content(path, stage, out_io)
|
|
2632
|
-
Git::Commands::Show.new(self).call(":#{stage}:#{path}", out: out_io)
|
|
2633
|
-
out_io
|
|
2634
|
-
end
|
|
2635
|
-
|
|
2636
|
-
def normalize_push_args(remote, branch, opts)
|
|
2637
|
-
if branch.is_a?(Hash)
|
|
2638
|
-
opts = branch
|
|
2639
|
-
branch = nil
|
|
2640
|
-
elsif remote.is_a?(Hash)
|
|
2641
|
-
opts = remote
|
|
2642
|
-
remote = nil
|
|
2643
|
-
end
|
|
2644
|
-
|
|
2645
|
-
opts ||= {}
|
|
2646
|
-
# Backwards compatibility for `push(remote, branch, true)`
|
|
2647
|
-
opts = { tags: opts } if [true, false].include?(opts)
|
|
2648
|
-
[remote, branch, opts]
|
|
2649
|
-
end
|
|
2650
|
-
|
|
2651
|
-
def validate_push_args!(remote, branch, opts)
|
|
2652
|
-
assert_valid_opts(opts, PUSH_ALLOWED_OPTS)
|
|
2653
|
-
raise ArgumentError, 'remote is required if branch is specified' if !remote && branch
|
|
2654
|
-
end
|
|
2655
|
-
|
|
2656
|
-
def push_refs(remote, branch, opts)
|
|
2657
|
-
positionals = [remote, branch].compact
|
|
2658
|
-
Git::Commands::Push.new(self).call(*positionals, **opts.except(:tags))
|
|
2659
|
-
end
|
|
2660
|
-
|
|
2661
|
-
def push_tags_separately?(opts)
|
|
2662
|
-
opts[:tags] && !opts[:mirror]
|
|
2663
|
-
end
|
|
2664
|
-
|
|
2665
|
-
def push_tags(remote, opts)
|
|
2666
|
-
Git::Commands::Push.new(self).call(*[remote].compact, **opts)
|
|
2667
|
-
end
|
|
2668
|
-
|
|
2669
|
-
def temp_file_name
|
|
2670
|
-
tempfile = Tempfile.new('archive')
|
|
2671
|
-
file = tempfile.path
|
|
2672
|
-
tempfile.close! # Prevents Ruby from deleting the file on garbage collection
|
|
2673
|
-
file
|
|
2674
|
-
end
|
|
2675
|
-
|
|
2676
|
-
def parse_archive_format_options(opts)
|
|
2677
|
-
format = opts[:format] || 'zip'
|
|
2678
|
-
gzip = opts[:add_gzip] == true || format == 'tgz'
|
|
2679
|
-
format = 'tar' if format == 'tgz'
|
|
2680
|
-
[format, gzip]
|
|
2681
|
-
end
|
|
2682
|
-
|
|
2683
|
-
def apply_gzip(file)
|
|
2684
|
-
file_content = File.read(file)
|
|
2685
|
-
Zlib::GzipWriter.open(file) { |gz| gz.write(file_content) }
|
|
2686
|
-
end
|
|
2687
|
-
|
|
2688
|
-
# Returns a hash of environment variable overrides for git commands
|
|
2689
|
-
#
|
|
2690
|
-
# This method builds a hash of environment variables that control git's behavior,
|
|
2691
|
-
# such as the git directory, working tree, and index file locations.
|
|
2692
|
-
#
|
|
2693
|
-
# @param additional_overrides [Hash] additional environment variables to set or unset
|
|
2694
|
-
#
|
|
2695
|
-
# Keys should be environment variable names (String) and values should be either:
|
|
2696
|
-
# * A String value to set the environment variable
|
|
2697
|
-
# * `nil` to unset the environment variable
|
|
2698
|
-
#
|
|
2699
|
-
# Per Process.spawn semantics, setting a key to `nil` will unset that environment
|
|
2700
|
-
# variable, removing it from the environment passed to the git command.
|
|
2701
|
-
#
|
|
2702
|
-
# @return [Hash<String, String|nil>] environment variable overrides
|
|
2703
|
-
#
|
|
2704
|
-
# @example Basic usage with default environment variables
|
|
2705
|
-
# env_overrides
|
|
2706
|
-
# # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree', ... }
|
|
2707
|
-
#
|
|
2708
|
-
# @example Adding a custom environment variable
|
|
2709
|
-
# env_overrides('GIT_TRACE' => '1')
|
|
2710
|
-
# # => { 'GIT_DIR' => '/path/to/.git', ..., 'GIT_TRACE' => '1' }
|
|
2711
|
-
#
|
|
2712
|
-
# @example Unsetting an environment variable (used by worktree_command_line)
|
|
2713
|
-
# env_overrides('GIT_INDEX_FILE' => nil)
|
|
2714
|
-
# # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree',
|
|
2715
|
-
# # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' => 'en_US.UTF-8' }
|
|
2716
|
-
# # When passed to Process.spawn, GIT_INDEX_FILE will be unset in the environment
|
|
2717
|
-
#
|
|
2718
|
-
# @see https://ruby-doc.org/core/Process.html#method-c-spawn Process.spawn
|
|
2719
|
-
#
|
|
2720
|
-
# @api private
|
|
2721
|
-
#
|
|
2722
|
-
def env_overrides(**additional_overrides)
|
|
2723
|
-
{
|
|
2724
|
-
'GIT_DIR' => @git_dir,
|
|
2725
|
-
'GIT_WORK_TREE' => @git_work_dir,
|
|
2726
|
-
'GIT_INDEX_FILE' => @git_index_file,
|
|
2727
|
-
'GIT_SSH' => resolved_git_ssh,
|
|
2728
|
-
'GIT_EDITOR' => 'true', # Use a no-op editor so Git skips interactive editing but continues
|
|
2729
|
-
'LC_ALL' => 'en_US.UTF-8'
|
|
2730
|
-
}.merge(additional_overrides)
|
|
2731
|
-
end
|
|
2732
|
-
|
|
2733
|
-
# Resolve the git_ssh value to use for this instance
|
|
2734
|
-
#
|
|
2735
|
-
# @return [String, nil] the resolved git_ssh value
|
|
2736
|
-
#
|
|
2737
|
-
# Returns the global config value if @git_ssh is the sentinel :use_global_config,
|
|
2738
|
-
# otherwise returns @git_ssh (which may be nil or a string)
|
|
2739
|
-
#
|
|
2740
|
-
# @api private
|
|
2741
|
-
#
|
|
2742
|
-
def resolved_git_ssh
|
|
2743
|
-
return Git::Base.config.git_ssh if @git_ssh == :use_global_config
|
|
2744
|
-
|
|
2745
|
-
@git_ssh
|
|
2746
|
-
end
|
|
2747
|
-
|
|
2748
|
-
def global_opts
|
|
2749
|
-
[].tap do |global_opts|
|
|
2750
|
-
global_opts << "--git-dir=#{@git_dir}" unless @git_dir.nil?
|
|
2751
|
-
global_opts << "--work-tree=#{@git_work_dir}" unless @git_work_dir.nil?
|
|
2752
|
-
global_opts.concat(STATIC_GLOBAL_OPTS)
|
|
2753
|
-
end
|
|
2754
|
-
end
|
|
2755
|
-
|
|
2756
|
-
# Returns the {Git::CommandLine::Capturing} instance used for capturing execution
|
|
2757
|
-
#
|
|
2758
|
-
# Memoized factory for the capturing execution path. Instantiates
|
|
2759
|
-
# {Git::CommandLine::Capturing} with the current environment, binary path,
|
|
2760
|
-
# global options, and logger.
|
|
2761
|
-
#
|
|
2762
|
-
# @return [Git::CommandLine::Capturing]
|
|
2763
|
-
#
|
|
2764
|
-
# @see Git::CommandLine::Capturing#run
|
|
2765
|
-
#
|
|
2766
|
-
def command_line_capturing
|
|
2767
|
-
@command_line_capturing ||=
|
|
2768
|
-
Git::CommandLine::Capturing.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2769
|
-
end
|
|
2770
|
-
|
|
2771
|
-
# Returns the {Git::CommandLine::Streaming} instance used for streaming execution
|
|
2772
|
-
#
|
|
2773
|
-
# Memoized factory for the streaming execution path. Instantiates
|
|
2774
|
-
# {Git::CommandLine::Streaming} with the current environment, binary path,
|
|
2775
|
-
# global options, and logger.
|
|
2776
|
-
#
|
|
2777
|
-
# @return [Git::CommandLine::Streaming]
|
|
2778
|
-
#
|
|
2779
|
-
# @see Git::CommandLine::Streaming#run
|
|
2780
|
-
#
|
|
2781
|
-
def command_line_streaming
|
|
2782
|
-
@command_line_streaming ||=
|
|
2783
|
-
Git::CommandLine::Streaming.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2784
|
-
end
|
|
2785
|
-
|
|
2786
|
-
# Validates the :count option for log commands.
|
|
2787
|
-
#
|
|
2788
|
-
def validate_log_count_option!(opts)
|
|
2789
|
-
return unless opts[:count] && !opts[:count].is_a?(Integer)
|
|
2790
|
-
|
|
2791
|
-
raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
|
|
2792
|
-
end
|
|
2793
|
-
|
|
2794
|
-
# Builds the positional revision range argument(s) from opts for Git::Commands::Log
|
|
2795
|
-
#
|
|
2796
|
-
# @param opts [Hash]
|
|
2797
|
-
# @return [Array<String>] zero or one element array with the revision range expression
|
|
2798
|
-
def log_revision_range_args(opts)
|
|
2799
|
-
if opts[:between]
|
|
2800
|
-
["#{opts[:between][0]}..#{opts[:between][1]}"]
|
|
2801
|
-
elsif opts[:object].is_a?(String)
|
|
2802
|
-
[opts[:object]]
|
|
2803
|
-
else
|
|
2804
|
-
[]
|
|
2805
|
-
end
|
|
2806
|
-
end
|
|
2807
|
-
|
|
2808
|
-
# Builds the common keyword options for Git::Commands::Log from opts
|
|
2809
|
-
#
|
|
2810
|
-
# @param opts [Hash]
|
|
2811
|
-
# @param extra [Hash] additional options to merge in (caller-specific)
|
|
2812
|
-
# @return [Hash] keyword arguments for Git::Commands::Log#call
|
|
2813
|
-
def log_base_call_options(opts, extra = {})
|
|
2814
|
-
{
|
|
2815
|
-
all: opts[:all],
|
|
2816
|
-
cherry: opts[:cherry],
|
|
2817
|
-
since: opts[:since],
|
|
2818
|
-
until: opts[:until],
|
|
2819
|
-
grep: opts[:grep],
|
|
2820
|
-
author: opts[:author],
|
|
2821
|
-
max_count: opts[:count],
|
|
2822
|
-
path: opts[:path_limiter] ? Array(opts[:path_limiter]) : nil
|
|
2823
|
-
}.merge(extra).compact
|
|
2824
|
-
end
|
|
2825
|
-
|
|
2826
|
-
def run_log_command(revision_range_args, call_opts)
|
|
2827
|
-
log_or_empty_on_unborn do
|
|
2828
|
-
result = Git::Commands::Log.new(self).call(
|
|
2829
|
-
*revision_range_args,
|
|
2830
|
-
no_color: true, pretty: 'raw',
|
|
2831
|
-
**call_opts
|
|
2832
|
-
)
|
|
2833
|
-
process_commit_log_data(result.stdout.split("\n"))
|
|
2834
|
-
end
|
|
2835
|
-
end
|
|
2836
|
-
|
|
2837
|
-
def log_or_empty_on_unborn
|
|
2838
|
-
yield
|
|
2839
|
-
rescue Git::FailedError => e
|
|
2840
|
-
raise unless e.result.status.exitstatus == 128 &&
|
|
2841
|
-
e.result.stderr =~ /does not have any commits yet/
|
|
2842
|
-
|
|
2843
|
-
[]
|
|
2844
|
-
end
|
|
2845
|
-
|
|
2846
|
-
def normalize_commit_tree_opts(opts, tree)
|
|
2847
|
-
opts.dup.tap do |actual_opts|
|
|
2848
|
-
actual_opts[:p] = actual_opts.delete(:parents) if actual_opts.key?(:parents)
|
|
2849
|
-
actual_opts[:p] = actual_opts.delete(:parent) if actual_opts.key?(:parent)
|
|
2850
|
-
actual_opts[:m] = actual_opts.delete(:message) if actual_opts.key?(:message)
|
|
2851
|
-
actual_opts[:m] = "commit tree #{tree}" if actual_opts[:m].nil?
|
|
2852
|
-
end
|
|
2853
|
-
end
|
|
2854
|
-
end
|
|
2855
|
-
end
|