git 1.19.1 → 4.4.5
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/.commitlintrc.yml +38 -0
- data/.github/copilot-instructions.md +2733 -0
- data/.github/pull_request_template.md +17 -0
- data/.github/workflows/continuous_integration.yml +92 -21
- data/.github/workflows/enforce_conventional_commits.yml +29 -0
- data/.github/workflows/experimental_continuous_integration.yml +59 -0
- data/.github/workflows/release.yml +53 -0
- data/.gitignore +5 -0
- data/.husky/commit-msg +1 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +55 -0
- data/.rubocop_todo.yml +12 -0
- data/.yardopts +4 -1
- data/AI_POLICY.md +24 -0
- data/CHANGELOG.md +501 -0
- data/CODE_OF_CONDUCT.md +25 -0
- data/CONTRIBUTING.md +323 -102
- data/GOVERNANCE.md +106 -0
- data/LICENSE +1 -1
- data/MAINTAINERS.md +17 -4
- data/README.md +575 -246
- data/Rakefile +13 -55
- data/git.gemspec +36 -30
- data/lib/git/args_builder.rb +111 -0
- data/lib/git/author.rb +9 -7
- data/lib/git/base.rb +602 -173
- data/lib/git/branch.rb +318 -38
- data/lib/git/branches.rb +21 -24
- data/lib/git/command_line.rb +330 -0
- data/lib/git/command_line_result.rb +9 -3
- data/lib/git/config.rb +10 -6
- data/lib/git/diff.rb +149 -81
- data/lib/git/diff_path_status.rb +46 -0
- data/lib/git/diff_stats.rb +59 -0
- data/lib/git/errors.rb +212 -0
- data/lib/git/escaped_path.rb +2 -2
- data/lib/git/fsck_object.rb +48 -0
- data/lib/git/fsck_result.rb +121 -0
- data/lib/git/index.rb +2 -1
- data/lib/git/lib.rb +1648 -643
- data/lib/git/log.rb +143 -106
- data/lib/git/object.rb +151 -125
- data/lib/git/path.rb +23 -16
- data/lib/git/remote.rb +5 -4
- data/lib/git/repository.rb +2 -2
- data/lib/git/stash.rb +11 -12
- data/lib/git/stashes.rb +16 -15
- data/lib/git/status.rb +104 -143
- data/lib/git/url.rb +3 -3
- data/lib/git/version.rb +3 -1
- data/lib/git/working_directory.rb +2 -0
- data/lib/git/worktree.rb +6 -5
- data/lib/git/worktrees.rb +6 -6
- data/lib/git.rb +131 -28
- data/package.json +10 -0
- data/redesign/1_architecture_existing.md +66 -0
- data/redesign/2_architecture_redesign.md +130 -0
- data/redesign/3_architecture_implementation.md +138 -0
- data/redesign/index.md +34 -0
- data/release-please-config.json +36 -0
- data/tasks/gem_tasks.rake +10 -0
- data/tasks/rubocop.rake +12 -0
- data/tasks/test.rake +13 -0
- data/tasks/test_gem.rake +12 -0
- data/tasks/yard.rake +23 -0
- metadata +114 -37
- data/.github/stale.yml +0 -25
- data/Dockerfile.changelog-rs +0 -12
- data/PULL_REQUEST_TEMPLATE.md +0 -9
- data/RELEASING.md +0 -70
- data/lib/git/base/factory.rb +0 -99
- data/lib/git/failed_error.rb +0 -53
- data/lib/git/git_execute_error.rb +0 -7
- data/lib/git/signaled_error.rb +0 -50
- /data/{ISSUE_TEMPLATE.md → .github/issue_template.md} +0 -0
data/lib/git/lib.rb
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'args_builder'
|
|
4
|
+
|
|
5
|
+
require 'git/command_line'
|
|
6
|
+
require 'git/errors'
|
|
2
7
|
require 'logger'
|
|
8
|
+
require 'pathname'
|
|
9
|
+
require 'pp'
|
|
10
|
+
require 'process_executer'
|
|
11
|
+
require 'stringio'
|
|
3
12
|
require 'tempfile'
|
|
4
13
|
require 'zlib'
|
|
5
|
-
require 'open3'
|
|
6
14
|
|
|
7
15
|
module Git
|
|
16
|
+
# Internal git operations
|
|
17
|
+
# @api private
|
|
8
18
|
class Lib
|
|
9
|
-
|
|
10
|
-
@@semaphore = Mutex.new
|
|
11
|
-
|
|
12
19
|
# The path to the Git working copy. The default is '"./.git"'.
|
|
13
20
|
#
|
|
14
21
|
# @return [Pathname] the path to the Git working copy.
|
|
@@ -37,33 +44,41 @@ module Git
|
|
|
37
44
|
|
|
38
45
|
# Create a new Git::Lib object
|
|
39
46
|
#
|
|
40
|
-
# @
|
|
41
|
-
#
|
|
47
|
+
# @overload initialize(base, logger)
|
|
48
|
+
#
|
|
49
|
+
# @param base [Hash] the hash containing paths to the Git working copy,
|
|
50
|
+
# the Git repository directory, and the Git index file.
|
|
51
|
+
#
|
|
52
|
+
# @option base [Pathname] :working_directory
|
|
53
|
+
# @option base [Pathname] :repository
|
|
54
|
+
# @option base [Pathname] :index
|
|
55
|
+
#
|
|
56
|
+
# @param [Logger] logger
|
|
57
|
+
#
|
|
58
|
+
# @overload initialize(base, logger)
|
|
42
59
|
#
|
|
43
|
-
#
|
|
60
|
+
# @param base [#dir, #repo, #index] an object with methods to get the Git worktree (#dir),
|
|
61
|
+
# the Git repository directory (#repo), and the Git index file (#index).
|
|
44
62
|
#
|
|
45
|
-
#
|
|
46
|
-
# @option base [Pathname] :repository
|
|
47
|
-
# @option base [Pathname] :index
|
|
63
|
+
# @param [Logger] logger
|
|
48
64
|
#
|
|
49
65
|
def initialize(base = nil, logger = nil)
|
|
50
|
-
@git_dir = nil
|
|
51
|
-
@git_index_file = nil
|
|
52
|
-
@git_work_dir = nil
|
|
53
|
-
@path = nil
|
|
54
66
|
@logger = logger || Logger.new(nil)
|
|
67
|
+
@git_ssh = :use_global_config
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@git_dir = base[:repository]
|
|
62
|
-
@git_index_file = base[:index]
|
|
63
|
-
@git_work_dir = base[:working_directory]
|
|
69
|
+
case base
|
|
70
|
+
when Git::Base
|
|
71
|
+
initialize_from_base(base)
|
|
72
|
+
when Hash
|
|
73
|
+
initialize_from_hash(base)
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
|
|
77
|
+
INIT_OPTION_MAP = [
|
|
78
|
+
{ keys: [:bare], flag: '--bare', type: :boolean },
|
|
79
|
+
{ keys: [:initial_branch], flag: '--initial-branch', type: :valued_equals }
|
|
80
|
+
].freeze
|
|
81
|
+
|
|
67
82
|
# creates or reinitializes the repository
|
|
68
83
|
#
|
|
69
84
|
# options:
|
|
@@ -71,62 +86,94 @@ module Git
|
|
|
71
86
|
# :working_directory
|
|
72
87
|
# :initial_branch
|
|
73
88
|
#
|
|
74
|
-
def init(opts={})
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
89
|
+
def init(opts = {})
|
|
90
|
+
args = build_args(opts, INIT_OPTION_MAP)
|
|
91
|
+
command('init', *args)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
CLONE_OPTION_MAP = [
|
|
95
|
+
{ keys: [:bare], flag: '--bare', type: :boolean },
|
|
96
|
+
{ keys: [:recursive], flag: '--recursive', type: :boolean },
|
|
97
|
+
{ keys: [:mirror], flag: '--mirror', type: :boolean },
|
|
98
|
+
{ keys: [:branch], flag: '--branch', type: :valued_space },
|
|
99
|
+
{ keys: [:filter], flag: '--filter', type: :valued_space },
|
|
100
|
+
{ keys: %i[remote origin], flag: '--origin', type: :valued_space },
|
|
101
|
+
{ keys: [:config], flag: '--config', type: :repeatable_valued_space },
|
|
102
|
+
{
|
|
103
|
+
keys: [:single_branch],
|
|
104
|
+
type: :custom,
|
|
105
|
+
validator: ->(value) { [nil, true, false].include?(value) },
|
|
106
|
+
builder: lambda do |value|
|
|
107
|
+
case value
|
|
108
|
+
when true
|
|
109
|
+
['--single-branch']
|
|
110
|
+
when false
|
|
111
|
+
['--no-single-branch']
|
|
112
|
+
else
|
|
113
|
+
[]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
keys: [:depth],
|
|
119
|
+
type: :custom,
|
|
120
|
+
builder: ->(value) { ['--depth', value.to_i] if value }
|
|
121
|
+
}
|
|
122
|
+
].freeze
|
|
81
123
|
|
|
82
|
-
#
|
|
124
|
+
# Clones a repository into a newly created directory
|
|
83
125
|
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
126
|
+
# @param [String] repository_url the URL of the repository to clone
|
|
127
|
+
#
|
|
128
|
+
# @param [String, nil] directory the directory to clone into
|
|
129
|
+
#
|
|
130
|
+
# If nil, the repository is cloned into a directory with the same name as
|
|
131
|
+
# the repository.
|
|
132
|
+
#
|
|
133
|
+
# @param [Hash] opts the options for this command
|
|
134
|
+
#
|
|
135
|
+
# @option opts [Boolean] :bare (false) if true, clone as a bare repository
|
|
136
|
+
#
|
|
137
|
+
# @option opts [String] :branch the branch to checkout
|
|
138
|
+
#
|
|
139
|
+
# @option opts [String, Array] :config one or more configuration options to set
|
|
140
|
+
#
|
|
141
|
+
# @option opts [Integer] :depth the number of commits back to pull
|
|
93
142
|
#
|
|
94
|
-
#
|
|
143
|
+
# @option opts [String] :filter specify partial clone
|
|
144
|
+
#
|
|
145
|
+
# @option opts [String] :mirror set up a mirror of the source repository
|
|
146
|
+
#
|
|
147
|
+
# @option opts [String] :origin the name of the remote
|
|
148
|
+
#
|
|
149
|
+
# @option opts [String] :path an optional prefix for the directory parameter
|
|
150
|
+
#
|
|
151
|
+
# @option opts [String] :remote the name of the remote
|
|
152
|
+
#
|
|
153
|
+
# @option opts [Boolean] :recursive after the clone is created, initialize all
|
|
154
|
+
# within, using their default settings
|
|
155
|
+
#
|
|
156
|
+
# @option opts [Numeric, nil] :timeout the number of seconds to wait for the
|
|
157
|
+
# command to complete
|
|
158
|
+
#
|
|
159
|
+
# See {Git::Lib#command} for more information about :timeout
|
|
95
160
|
#
|
|
96
161
|
# @return [Hash] the options to pass to {Git::Base.new}
|
|
97
162
|
#
|
|
163
|
+
# @todo make this work with SSH password or auth_key
|
|
164
|
+
#
|
|
98
165
|
def clone(repository_url, directory, opts = {})
|
|
99
166
|
@path = opts[:path] || '.'
|
|
100
167
|
clone_dir = opts[:path] ? File.join(@path, directory) : directory
|
|
101
168
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
arr_opts << '--branch' << opts[:branch] if opts[:branch]
|
|
105
|
-
arr_opts << '--depth' << opts[:depth].to_i if opts[:depth] && opts[:depth].to_i > 0
|
|
106
|
-
arr_opts << '--filter' << opts[:filter] if opts[:filter]
|
|
107
|
-
Array(opts[:config]).each { |c| arr_opts << '--config' << c }
|
|
108
|
-
arr_opts << '--origin' << opts[:remote] || opts[:origin] if opts[:remote] || opts[:origin]
|
|
109
|
-
arr_opts << '--recursive' if opts[:recursive]
|
|
110
|
-
arr_opts << '--mirror' if opts[:mirror]
|
|
111
|
-
|
|
112
|
-
arr_opts << '--'
|
|
169
|
+
args = build_args(opts, CLONE_OPTION_MAP)
|
|
170
|
+
args.push('--', repository_url, clone_dir)
|
|
113
171
|
|
|
114
|
-
|
|
115
|
-
arr_opts << clone_dir
|
|
116
|
-
|
|
117
|
-
command('clone', *arr_opts)
|
|
172
|
+
command('clone', *args, timeout: opts[:timeout])
|
|
118
173
|
|
|
119
174
|
return_base_opts_from_clone(clone_dir, opts)
|
|
120
175
|
end
|
|
121
176
|
|
|
122
|
-
def return_base_opts_from_clone(clone_dir, opts)
|
|
123
|
-
base_opts = {}
|
|
124
|
-
base_opts[:repository] = clone_dir if (opts[:bare] || opts[:mirror])
|
|
125
|
-
base_opts[:working_directory] = clone_dir unless (opts[:bare] || opts[:mirror])
|
|
126
|
-
base_opts[:log] = opts[:log] if opts[:log]
|
|
127
|
-
base_opts
|
|
128
|
-
end
|
|
129
|
-
|
|
130
177
|
# Returns the name of the default branch of the given repository
|
|
131
178
|
#
|
|
132
179
|
# @param repository [URI, Pathname, String] The (possibly remote) repository to clone from
|
|
@@ -142,124 +189,331 @@ module Git
|
|
|
142
189
|
match_data = output.match(%r{^ref: refs/heads/(?<default_branch>[^\t]+)\tHEAD$})
|
|
143
190
|
return match_data[:default_branch] if match_data
|
|
144
191
|
|
|
145
|
-
raise 'Unable to determine the default branch'
|
|
192
|
+
raise Git::UnexpectedResultError, 'Unable to determine the default branch'
|
|
146
193
|
end
|
|
147
194
|
|
|
148
195
|
## READ COMMANDS ##
|
|
149
196
|
|
|
197
|
+
# The map defining how to translate user options to git command arguments.
|
|
198
|
+
DESCRIBE_OPTION_MAP = [
|
|
199
|
+
{ keys: [:all], flag: '--all', type: :boolean },
|
|
200
|
+
{ keys: [:tags], flag: '--tags', type: :boolean },
|
|
201
|
+
{ keys: [:contains], flag: '--contains', type: :boolean },
|
|
202
|
+
{ keys: [:debug], flag: '--debug', type: :boolean },
|
|
203
|
+
{ keys: [:long], flag: '--long', type: :boolean },
|
|
204
|
+
{ keys: [:always], flag: '--always', type: :boolean },
|
|
205
|
+
{ keys: %i[exact_match exact-match], flag: '--exact-match', type: :boolean },
|
|
206
|
+
{ keys: [:abbrev], flag: '--abbrev', type: :valued_equals },
|
|
207
|
+
{ keys: [:candidates], flag: '--candidates', type: :valued_equals },
|
|
208
|
+
{ keys: [:match], flag: '--match', type: :valued_equals },
|
|
209
|
+
{
|
|
210
|
+
keys: [:dirty],
|
|
211
|
+
type: :custom,
|
|
212
|
+
builder: lambda do |value|
|
|
213
|
+
return '--dirty' if value == true
|
|
214
|
+
|
|
215
|
+
"--dirty=#{value}" if value.is_a?(String)
|
|
216
|
+
end
|
|
217
|
+
}
|
|
218
|
+
].freeze
|
|
219
|
+
|
|
220
|
+
# Finds most recent tag that is reachable from a commit
|
|
150
221
|
#
|
|
151
|
-
#
|
|
222
|
+
# @see https://git-scm.com/docs/git-describe git-describe
|
|
152
223
|
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
arr_opts << '--contains' if opts[:contains]
|
|
176
|
-
arr_opts << '--debug' if opts[:debug]
|
|
177
|
-
arr_opts << '--long' if opts[:long]
|
|
178
|
-
arr_opts << '--always' if opts[:always]
|
|
179
|
-
arr_opts << '--exact-match' if opts[:exact_match] || opts[:"exact-match"]
|
|
180
|
-
|
|
181
|
-
arr_opts << '--dirty' if opts[:dirty] == true
|
|
182
|
-
arr_opts << "--dirty=#{opts[:dirty]}" if opts[:dirty].is_a?(String)
|
|
183
|
-
|
|
184
|
-
arr_opts << "--abbrev=#{opts[:abbrev]}" if opts[:abbrev]
|
|
185
|
-
arr_opts << "--candidates=#{opts[:candidates]}" if opts[:candidates]
|
|
186
|
-
arr_opts << "--match=#{opts[:match]}" if opts[:match]
|
|
224
|
+
# @param commit_ish [String, nil] target commit sha or object name
|
|
225
|
+
#
|
|
226
|
+
# @param opts [Hash] the given options
|
|
227
|
+
#
|
|
228
|
+
# @option opts :all [Boolean]
|
|
229
|
+
# @option opts :tags [Boolean]
|
|
230
|
+
# @option opts :contains [Boolean]
|
|
231
|
+
# @option opts :debug [Boolean]
|
|
232
|
+
# @option opts :long [Boolean]
|
|
233
|
+
# @option opts :always [Boolean]
|
|
234
|
+
# @option opts :exact_match [Boolean]
|
|
235
|
+
# @option opts :dirty [true, String]
|
|
236
|
+
# @option opts :abbrev [String]
|
|
237
|
+
# @option opts :candidates [String]
|
|
238
|
+
# @option opts :match [String]
|
|
239
|
+
#
|
|
240
|
+
# @return [String] the tag name
|
|
241
|
+
#
|
|
242
|
+
# @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
|
|
243
|
+
#
|
|
244
|
+
def describe(commit_ish = nil, opts = {})
|
|
245
|
+
assert_args_are_not_options('commit-ish object', commit_ish)
|
|
187
246
|
|
|
188
|
-
|
|
247
|
+
args = build_args(opts, DESCRIBE_OPTION_MAP)
|
|
248
|
+
args << commit_ish if commit_ish
|
|
189
249
|
|
|
190
|
-
|
|
250
|
+
command('describe', *args)
|
|
191
251
|
end
|
|
192
252
|
|
|
193
|
-
|
|
253
|
+
# Return the commits that are within the given revision range
|
|
254
|
+
#
|
|
255
|
+
# @see https://git-scm.com/docs/git-log git-log
|
|
256
|
+
#
|
|
257
|
+
# @param opts [Hash] the given options
|
|
258
|
+
#
|
|
259
|
+
# @option opts :count [Integer] the maximum number of commits to return (maps to max-count)
|
|
260
|
+
# @option opts :all [Boolean]
|
|
261
|
+
# @option opts :cherry [Boolean]
|
|
262
|
+
# @option opts :since [String]
|
|
263
|
+
# @option opts :until [String]
|
|
264
|
+
# @option opts :grep [String]
|
|
265
|
+
# @option opts :author [String]
|
|
266
|
+
# @option opts :between [Array<String>] an array of two commit-ish strings to specify a revision range
|
|
267
|
+
#
|
|
268
|
+
# Only :between or :object options can be used, not both.
|
|
269
|
+
#
|
|
270
|
+
# @option opts :object [String] the revision range for the git log command
|
|
271
|
+
#
|
|
272
|
+
# Only :between or :object options can be used, not both.
|
|
273
|
+
#
|
|
274
|
+
# @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only
|
|
275
|
+
# include commits that impact files from the specified paths
|
|
276
|
+
#
|
|
277
|
+
# @return [Array<String>] the log output
|
|
278
|
+
#
|
|
279
|
+
# @raise [ArgumentError] if the resulting revision range is a string starting with a hyphen
|
|
280
|
+
#
|
|
281
|
+
def log_commits(opts = {})
|
|
282
|
+
assert_args_are_not_options('between', opts[:between]&.first)
|
|
283
|
+
assert_args_are_not_options('object', opts[:object])
|
|
284
|
+
|
|
194
285
|
arr_opts = log_common_options(opts)
|
|
195
286
|
|
|
196
287
|
arr_opts << '--pretty=oneline'
|
|
197
288
|
|
|
198
289
|
arr_opts += log_path_options(opts)
|
|
199
290
|
|
|
200
|
-
command_lines('log', *arr_opts).map { |l| l.split.first }
|
|
291
|
+
log_or_empty_on_unborn { command_lines('log', *arr_opts).map { |l| l.split.first } }
|
|
201
292
|
end
|
|
202
293
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
294
|
+
FULL_LOG_EXTRA_OPTIONS_MAP = [
|
|
295
|
+
{ type: :static, flag: '--pretty=raw' },
|
|
296
|
+
{ keys: [:skip], flag: '--skip', type: :valued_equals },
|
|
297
|
+
{ keys: [:merges], flag: '--merges', type: :boolean }
|
|
298
|
+
].freeze
|
|
208
299
|
|
|
209
|
-
|
|
300
|
+
# Return the commits that are within the given revision range
|
|
301
|
+
#
|
|
302
|
+
# @see https://git-scm.com/docs/git-log git-log
|
|
303
|
+
#
|
|
304
|
+
# @param opts [Hash] the given options
|
|
305
|
+
#
|
|
306
|
+
# @option opts :count [Integer] the maximum number of commits to return (maps to
|
|
307
|
+
# max-count)
|
|
308
|
+
#
|
|
309
|
+
# @option opts :all [Boolean]
|
|
310
|
+
#
|
|
311
|
+
# @option opts :cherry [Boolean]
|
|
312
|
+
#
|
|
313
|
+
# @option opts :since [String]
|
|
314
|
+
#
|
|
315
|
+
# @option opts :until [String]
|
|
316
|
+
#
|
|
317
|
+
# @option opts :grep [String]
|
|
318
|
+
#
|
|
319
|
+
# @option opts :author [String]
|
|
320
|
+
#
|
|
321
|
+
# @option opts :between [Array<String>] an array of two commit-ish strings to
|
|
322
|
+
# specify a revision range
|
|
323
|
+
#
|
|
324
|
+
# Only :between or :object options can be used, not both.
|
|
325
|
+
#
|
|
326
|
+
# @option opts :object [String] the revision range for the git log command
|
|
327
|
+
#
|
|
328
|
+
# Only :between or :object options can be used, not both.
|
|
329
|
+
#
|
|
330
|
+
# @option opts :path_limiter [String, Pathname, Array<String, Pathname>] only include commits that
|
|
331
|
+
# impact files from the specified paths
|
|
332
|
+
#
|
|
333
|
+
# @option opts :skip [Integer]
|
|
334
|
+
#
|
|
335
|
+
# @return [Array<Hash>] the log output parsed into an array of hashs for each commit
|
|
336
|
+
#
|
|
337
|
+
# Each hash contains the following keys:
|
|
338
|
+
#
|
|
339
|
+
# * 'sha' [String] the commit sha
|
|
340
|
+
# * 'author' [String] the author of the commit
|
|
341
|
+
# * 'message' [String] the commit message
|
|
342
|
+
# * 'parent' [Array<String>] the commit shas of the parent commits
|
|
343
|
+
# * 'tree' [String] the tree sha
|
|
344
|
+
# * 'author' [String] the author of the commit and timestamp of when the
|
|
345
|
+
# changes were created
|
|
346
|
+
# * 'committer' [String] the committer of the commit and timestamp of when the
|
|
347
|
+
# commit was applied
|
|
348
|
+
# * 'merges' [Boolean] if truthy, only include merge commits (aka commits with
|
|
349
|
+
# 2 or more parents)
|
|
350
|
+
#
|
|
351
|
+
# @raise [ArgumentError] if the revision range (specified with :between or
|
|
352
|
+
# :object) is a string starting with a hyphen
|
|
353
|
+
#
|
|
354
|
+
def full_log_commits(opts = {})
|
|
355
|
+
assert_args_are_not_options('between', opts[:between]&.first)
|
|
356
|
+
assert_args_are_not_options('object', opts[:object])
|
|
210
357
|
|
|
211
|
-
|
|
358
|
+
args = log_common_options(opts)
|
|
359
|
+
args += build_args(opts, FULL_LOG_EXTRA_OPTIONS_MAP)
|
|
360
|
+
args += log_path_options(opts)
|
|
212
361
|
|
|
213
|
-
|
|
362
|
+
log_or_empty_on_unborn do
|
|
363
|
+
full_log = command_lines('log', *args)
|
|
364
|
+
process_commit_log_data(full_log)
|
|
365
|
+
end
|
|
214
366
|
end
|
|
215
367
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
368
|
+
# Verify and resolve a Git revision to its full SHA
|
|
369
|
+
#
|
|
370
|
+
# @see https://git-scm.com/docs/git-rev-parse git-rev-parse
|
|
371
|
+
# @see https://git-scm.com/docs/git-rev-parse#_specifying_revisions Valid ways to specify revisions
|
|
372
|
+
# @see https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emltrefnamegtemegemmasterememheadsmasterememrefsheadsmasterem
|
|
373
|
+
# Ref disambiguation rules
|
|
374
|
+
#
|
|
375
|
+
# @example
|
|
376
|
+
# lib.rev_parse('HEAD') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
|
|
377
|
+
# lib.rev_parse('9b9b31e') # => '9b9b31e704c0b85ffdd8d2af2ded85170a5af87d'
|
|
378
|
+
#
|
|
379
|
+
# @param revision [String] the revision to resolve
|
|
380
|
+
#
|
|
381
|
+
# @return [String] the full commit hash
|
|
382
|
+
#
|
|
383
|
+
# @raise [Git::FailedError] if the revision cannot be resolved
|
|
384
|
+
# @raise [ArgumentError] if the revision is a string starting with a hyphen
|
|
385
|
+
#
|
|
386
|
+
def rev_parse(revision)
|
|
387
|
+
assert_args_are_not_options('rev', revision)
|
|
388
|
+
|
|
389
|
+
command('rev-parse', '--revs-only', '--end-of-options', revision, '--')
|
|
225
390
|
end
|
|
226
391
|
|
|
227
|
-
|
|
228
|
-
|
|
392
|
+
# For backwards compatibility with the old method name
|
|
393
|
+
alias revparse rev_parse
|
|
394
|
+
|
|
395
|
+
# Find the first symbolic name for given commit_ish
|
|
396
|
+
#
|
|
397
|
+
# @param commit_ish [String] the commit_ish to find the symbolic name of
|
|
398
|
+
#
|
|
399
|
+
# @return [String, nil] the first symbolic name or nil if the commit_ish isn't found
|
|
400
|
+
#
|
|
401
|
+
# @raise [ArgumentError] if the commit_ish is a string starting with a hyphen
|
|
402
|
+
#
|
|
403
|
+
def name_rev(commit_ish)
|
|
404
|
+
assert_args_are_not_options('commit_ish', commit_ish)
|
|
405
|
+
|
|
406
|
+
command('name-rev', commit_ish).split[1]
|
|
229
407
|
end
|
|
230
408
|
|
|
231
|
-
|
|
232
|
-
|
|
409
|
+
alias namerev name_rev
|
|
410
|
+
|
|
411
|
+
# Output the contents or other properties of one or more objects.
|
|
412
|
+
#
|
|
413
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
414
|
+
#
|
|
415
|
+
# @example Get the contents of a file without a block
|
|
416
|
+
# lib.cat_file_contents('README.md') # => "This is a README file\n"
|
|
417
|
+
#
|
|
418
|
+
# @example Get the contents of a file with a block
|
|
419
|
+
# lib.cat_file_contents('README.md') { |f| f.read } # => "This is a README file\n"
|
|
420
|
+
#
|
|
421
|
+
# @param object [String] the object whose contents to return
|
|
422
|
+
#
|
|
423
|
+
# @return [String] the object contents
|
|
424
|
+
#
|
|
425
|
+
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
426
|
+
#
|
|
427
|
+
def cat_file_contents(object)
|
|
428
|
+
assert_args_are_not_options('object', object)
|
|
429
|
+
|
|
430
|
+
if block_given?
|
|
431
|
+
Tempfile.create do |file|
|
|
432
|
+
# If a block is given, write the output from the process to a temporary
|
|
433
|
+
# file and then yield the file to the block
|
|
434
|
+
#
|
|
435
|
+
command('cat-file', '-p', object, out: file, err: file)
|
|
436
|
+
file.rewind
|
|
437
|
+
yield file
|
|
438
|
+
end
|
|
439
|
+
else
|
|
440
|
+
# If a block is not given, return the file contents as a string
|
|
441
|
+
command('cat-file', '-p', object)
|
|
442
|
+
end
|
|
233
443
|
end
|
|
234
444
|
|
|
235
|
-
|
|
236
|
-
|
|
445
|
+
alias object_contents cat_file_contents
|
|
446
|
+
|
|
447
|
+
# Get the type for the given object
|
|
448
|
+
#
|
|
449
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
450
|
+
#
|
|
451
|
+
# @param object [String] the object to get the type
|
|
452
|
+
#
|
|
453
|
+
# @return [String] the object type
|
|
454
|
+
#
|
|
455
|
+
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
456
|
+
#
|
|
457
|
+
def cat_file_type(object)
|
|
458
|
+
assert_args_are_not_options('object', object)
|
|
459
|
+
|
|
460
|
+
command('cat-file', '-t', object)
|
|
237
461
|
end
|
|
238
462
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
463
|
+
alias object_type cat_file_type
|
|
464
|
+
|
|
465
|
+
# Get the size for the given object
|
|
466
|
+
#
|
|
467
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
468
|
+
#
|
|
469
|
+
# @param object [String] the object to get the type
|
|
470
|
+
#
|
|
471
|
+
# @return [String] the object type
|
|
472
|
+
#
|
|
473
|
+
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
474
|
+
#
|
|
475
|
+
def cat_file_size(object)
|
|
476
|
+
assert_args_are_not_options('object', object)
|
|
477
|
+
|
|
478
|
+
command('cat-file', '-s', object).to_i
|
|
244
479
|
end
|
|
245
480
|
|
|
246
|
-
|
|
247
|
-
hsh = {
|
|
248
|
-
'sha' => sha,
|
|
249
|
-
'parent' => []
|
|
250
|
-
}
|
|
481
|
+
alias object_size cat_file_size
|
|
251
482
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
483
|
+
# Return a hash of commit data
|
|
484
|
+
#
|
|
485
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
486
|
+
#
|
|
487
|
+
# @param object [String] the object to get the type
|
|
488
|
+
#
|
|
489
|
+
# @return [Hash] commit data
|
|
490
|
+
#
|
|
491
|
+
# The returned commit data has the following keys:
|
|
492
|
+
# * tree [String]
|
|
493
|
+
# * parent [Array<String>]
|
|
494
|
+
# * author [String] the author name, email, and commit timestamp
|
|
495
|
+
# * committer [String] the committer name, email, and merge timestamp
|
|
496
|
+
# * message [String] the commit message
|
|
497
|
+
# * gpgsig [String] the public signing key of the commit (if signed)
|
|
498
|
+
#
|
|
499
|
+
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
500
|
+
#
|
|
501
|
+
def cat_file_commit(object)
|
|
502
|
+
assert_args_are_not_options('object', object)
|
|
503
|
+
|
|
504
|
+
cdata = command_lines('cat-file', 'commit', object)
|
|
505
|
+
process_commit_data(cdata, object)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
alias commit_data cat_file_commit
|
|
259
509
|
|
|
260
|
-
|
|
510
|
+
def process_commit_data(data, sha)
|
|
511
|
+
# process_commit_headers consumes the header lines from the `data` array,
|
|
512
|
+
# leaving only the message lines behind.
|
|
513
|
+
headers = process_commit_headers(data)
|
|
514
|
+
message = "#{data.join("\n")}\n"
|
|
261
515
|
|
|
262
|
-
|
|
516
|
+
{ 'sha' => sha, 'message' => message }.merge(headers)
|
|
263
517
|
end
|
|
264
518
|
|
|
265
519
|
CAT_FILE_HEADER_LINE = /\A(?<key>\w+) (?<value>.*)\z/
|
|
@@ -269,20 +523,59 @@ module Git
|
|
|
269
523
|
key = match[:key]
|
|
270
524
|
value_lines = [match[:value]]
|
|
271
525
|
|
|
272
|
-
while data.first.start_with?(' ')
|
|
273
|
-
value_lines << data.shift.lstrip
|
|
274
|
-
end
|
|
526
|
+
value_lines << data.shift.lstrip while data.first.start_with?(' ')
|
|
275
527
|
|
|
276
528
|
yield key, value_lines.join("\n")
|
|
277
529
|
end
|
|
278
530
|
end
|
|
279
531
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
532
|
+
# Return a hash of annotated tag data
|
|
533
|
+
#
|
|
534
|
+
# Does not work with lightweight tags. List all annotated tags in your repository
|
|
535
|
+
# with the following command:
|
|
536
|
+
#
|
|
537
|
+
# ```sh
|
|
538
|
+
# git for-each-ref --format='%(refname:strip=2)' refs/tags | \
|
|
539
|
+
# while read tag; do git cat-file tag $tag >/dev/null 2>&1 && echo $tag; done
|
|
540
|
+
# ```
|
|
541
|
+
#
|
|
542
|
+
# @see https://git-scm.com/docs/git-cat-file git-cat-file
|
|
543
|
+
#
|
|
544
|
+
# @param object [String] the tag to retrieve
|
|
545
|
+
#
|
|
546
|
+
# @return [Hash] tag data
|
|
547
|
+
#
|
|
548
|
+
# Example tag data returned:
|
|
549
|
+
# ```ruby
|
|
550
|
+
# {
|
|
551
|
+
# "name" => "annotated_tag",
|
|
552
|
+
# "object" => "46abbf07e3c564c723c7c039a43ab3a39e5d02dd",
|
|
553
|
+
# "type" => "commit",
|
|
554
|
+
# "tag" => "annotated_tag",
|
|
555
|
+
# "tagger" => "Scott Chacon <schacon@gmail.com> 1724799270 -0700",
|
|
556
|
+
# "message" => "Creating an annotated tag\n"
|
|
557
|
+
# }
|
|
558
|
+
# ```
|
|
559
|
+
#
|
|
560
|
+
# The returned commit data has the following keys:
|
|
561
|
+
# * object [String] the sha of the tag object
|
|
562
|
+
# * type [String]
|
|
563
|
+
# * tag [String] tag name
|
|
564
|
+
# * tagger [String] the name and email of the user who created the tag
|
|
565
|
+
# and the timestamp of when the tag was created
|
|
566
|
+
# * message [String] the tag message
|
|
567
|
+
#
|
|
568
|
+
# @raise [ArgumentError] if object is a string starting with a hyphen
|
|
569
|
+
#
|
|
570
|
+
def cat_file_tag(object)
|
|
571
|
+
assert_args_are_not_options('object', object)
|
|
572
|
+
|
|
573
|
+
tdata = command_lines('cat-file', 'tag', object)
|
|
574
|
+
process_tag_data(tdata, object)
|
|
284
575
|
end
|
|
285
576
|
|
|
577
|
+
alias tag_data cat_file_tag
|
|
578
|
+
|
|
286
579
|
def process_tag_data(data, name)
|
|
287
580
|
hsh = { 'name' => name }
|
|
288
581
|
|
|
@@ -290,63 +583,88 @@ module Git
|
|
|
290
583
|
hsh[key] = value
|
|
291
584
|
end
|
|
292
585
|
|
|
293
|
-
hsh['message'] = data.join("\n")
|
|
586
|
+
hsh['message'] = "#{data.join("\n")}\n"
|
|
294
587
|
|
|
295
|
-
|
|
588
|
+
hsh
|
|
296
589
|
end
|
|
297
590
|
|
|
298
591
|
def process_commit_log_data(data)
|
|
299
|
-
|
|
592
|
+
RawLogParser.new(data).parse
|
|
593
|
+
end
|
|
300
594
|
|
|
301
|
-
|
|
595
|
+
# A private parser class to process the output of `git log --pretty=raw`
|
|
596
|
+
# @api private
|
|
597
|
+
class RawLogParser
|
|
598
|
+
def initialize(lines)
|
|
599
|
+
@lines = lines
|
|
600
|
+
@commits = []
|
|
601
|
+
@current_commit = nil
|
|
602
|
+
@in_message = false
|
|
603
|
+
end
|
|
302
604
|
|
|
303
|
-
|
|
605
|
+
def parse
|
|
606
|
+
@lines.each { |line| process_line(line.chomp) }
|
|
607
|
+
finalize_commit
|
|
608
|
+
@commits
|
|
609
|
+
end
|
|
304
610
|
|
|
305
|
-
|
|
306
|
-
line = line.chomp
|
|
611
|
+
private
|
|
307
612
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
613
|
+
def process_line(line)
|
|
614
|
+
if line.empty?
|
|
615
|
+
@in_message = !@in_message
|
|
616
|
+
return
|
|
311
617
|
end
|
|
312
618
|
|
|
313
|
-
in_message = false if in_message && line
|
|
619
|
+
@in_message = false if @in_message && !line.start_with?(' ')
|
|
314
620
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
621
|
+
@in_message ? process_message_line(line) : process_metadata_line(line)
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def process_message_line(line)
|
|
625
|
+
@current_commit['message'] << "#{line[4..]}\n"
|
|
626
|
+
end
|
|
319
627
|
|
|
628
|
+
def process_metadata_line(line)
|
|
320
629
|
key, *value = line.split
|
|
321
630
|
value = value.join(' ')
|
|
322
631
|
|
|
323
632
|
case key
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
hsh[key] = value
|
|
633
|
+
when 'commit'
|
|
634
|
+
start_new_commit(value)
|
|
635
|
+
when 'parent'
|
|
636
|
+
@current_commit['parent'] << value
|
|
637
|
+
else
|
|
638
|
+
@current_commit[key] = value
|
|
331
639
|
end
|
|
332
640
|
end
|
|
333
641
|
|
|
334
|
-
|
|
642
|
+
def start_new_commit(sha)
|
|
643
|
+
finalize_commit
|
|
644
|
+
@current_commit = { 'sha' => sha, 'message' => +'', 'parent' => [] }
|
|
645
|
+
end
|
|
335
646
|
|
|
336
|
-
|
|
647
|
+
def finalize_commit
|
|
648
|
+
@commits << @current_commit if @current_commit
|
|
649
|
+
end
|
|
337
650
|
end
|
|
651
|
+
private_constant :RawLogParser
|
|
338
652
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
653
|
+
LS_TREE_OPTION_MAP = [
|
|
654
|
+
{ keys: [:recursive], flag: '-r', type: :boolean }
|
|
655
|
+
].freeze
|
|
342
656
|
|
|
343
|
-
def ls_tree(sha)
|
|
657
|
+
def ls_tree(sha, opts = {})
|
|
344
658
|
data = { 'blob' => {}, 'tree' => {}, 'commit' => {} }
|
|
659
|
+
args = build_args(opts, LS_TREE_OPTION_MAP)
|
|
345
660
|
|
|
346
|
-
|
|
347
|
-
|
|
661
|
+
args.unshift(sha)
|
|
662
|
+
args << opts[:path] if opts[:path]
|
|
663
|
+
|
|
664
|
+
command_lines('ls-tree', *args).each do |line|
|
|
665
|
+
(info, filenm) = split_status_line(line)
|
|
348
666
|
(mode, type, sha) = info.split
|
|
349
|
-
data[type][filenm] = {:
|
|
667
|
+
data[type][filenm] = { mode: mode, sha: sha }
|
|
350
668
|
end
|
|
351
669
|
|
|
352
670
|
data
|
|
@@ -380,7 +698,7 @@ module Git
|
|
|
380
698
|
# The branch's full refname
|
|
381
699
|
(?:
|
|
382
700
|
(?<not_a_branch>\(not[[:blank:]]a[[:blank:]]branch\)) |
|
|
383
|
-
|
|
701
|
+
(?:\(HEAD[[:blank:]]detached[[:blank:]]at[[:blank:]](?<detached_ref>[^)]+)\)) |
|
|
384
702
|
(?<refname>[^[[:blank:]]]+)
|
|
385
703
|
)
|
|
386
704
|
|
|
@@ -393,17 +711,10 @@ module Git
|
|
|
393
711
|
/x
|
|
394
712
|
|
|
395
713
|
def branches_all
|
|
396
|
-
command_lines('branch', '-a')
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
[
|
|
401
|
-
match_data[:refname],
|
|
402
|
-
!match_data[:current].nil?,
|
|
403
|
-
!match_data[:worktree].nil?,
|
|
404
|
-
match_data[:symref]
|
|
405
|
-
]
|
|
406
|
-
end.compact
|
|
714
|
+
lines = command_lines('branch', '-a')
|
|
715
|
+
lines.each_with_index.filter_map do |line, index|
|
|
716
|
+
parse_branch_line(line, index, lines)
|
|
717
|
+
end
|
|
407
718
|
end
|
|
408
719
|
|
|
409
720
|
def worktrees_all
|
|
@@ -419,7 +730,7 @@ module Git
|
|
|
419
730
|
# detached
|
|
420
731
|
#
|
|
421
732
|
command_lines('worktree', 'list', '--porcelain').each do |w|
|
|
422
|
-
s = w.split
|
|
733
|
+
s = w.split
|
|
423
734
|
directory = s[1] if s[0] == 'worktree'
|
|
424
735
|
arr << [directory, s[1]] if s[0] == 'HEAD'
|
|
425
736
|
end
|
|
@@ -427,103 +738,216 @@ module Git
|
|
|
427
738
|
end
|
|
428
739
|
|
|
429
740
|
def worktree_add(dir, commitish = nil)
|
|
430
|
-
return
|
|
431
|
-
|
|
741
|
+
return worktree_command('worktree', 'add', dir, commitish) unless commitish.nil?
|
|
742
|
+
|
|
743
|
+
worktree_command('worktree', 'add', dir)
|
|
432
744
|
end
|
|
433
745
|
|
|
434
746
|
def worktree_remove(dir)
|
|
435
|
-
|
|
747
|
+
worktree_command('worktree', 'remove', dir)
|
|
436
748
|
end
|
|
437
749
|
|
|
438
750
|
def worktree_prune
|
|
439
|
-
|
|
751
|
+
worktree_command('worktree', 'prune')
|
|
440
752
|
end
|
|
441
753
|
|
|
442
754
|
def list_files(ref_dir)
|
|
443
755
|
dir = File.join(@git_dir, 'refs', ref_dir)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
756
|
+
Dir.glob('**/*', base: dir).select { |f| File.file?(File.join(dir, f)) }
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# The state and name of branch pointed to by `HEAD`
|
|
760
|
+
#
|
|
761
|
+
# HEAD can be in the following states:
|
|
762
|
+
#
|
|
763
|
+
# **:active**: `HEAD` points to a branch reference which in turn points to a
|
|
764
|
+
# commit representing the tip of that branch. This is the typical state when
|
|
765
|
+
# working on a branch.
|
|
766
|
+
#
|
|
767
|
+
# **:unborn**: `HEAD` points to a branch reference that does not yet exist
|
|
768
|
+
# because no commits have been made on that branch. This state occurs in two
|
|
769
|
+
# scenarios:
|
|
770
|
+
#
|
|
771
|
+
# * When a repository is newly initialized, and no commits have been made on the
|
|
772
|
+
# initial branch.
|
|
773
|
+
# * When a new branch is created using `git checkout --orphan <branch>`, starting
|
|
774
|
+
# a new branch with no history.
|
|
775
|
+
#
|
|
776
|
+
# **:detached**: `HEAD` points directly to a specific commit (identified by its
|
|
777
|
+
# SHA) rather than a branch reference. This state occurs when you check out a
|
|
778
|
+
# commit, a tag, or any state that is not directly associated with a branch. The
|
|
779
|
+
# branch name in this case is `HEAD`.
|
|
780
|
+
#
|
|
781
|
+
HeadState = Struct.new(:state, :name)
|
|
782
|
+
|
|
783
|
+
# The current branch state which is the state of `HEAD`
|
|
784
|
+
#
|
|
785
|
+
# @return [HeadState] the state and name of the current branch
|
|
786
|
+
#
|
|
787
|
+
def current_branch_state
|
|
788
|
+
branch_name = command('branch', '--show-current')
|
|
789
|
+
return HeadState.new(:detached, 'HEAD') if branch_name.empty?
|
|
790
|
+
|
|
791
|
+
state = get_branch_state(branch_name)
|
|
792
|
+
HeadState.new(state, branch_name)
|
|
450
793
|
end
|
|
451
794
|
|
|
452
795
|
def branch_current
|
|
453
|
-
|
|
796
|
+
branch_name = command('branch', '--show-current')
|
|
797
|
+
branch_name.empty? ? 'HEAD' : branch_name
|
|
454
798
|
end
|
|
455
799
|
|
|
456
|
-
def branch_contains(commit, branch_name=
|
|
457
|
-
command(
|
|
800
|
+
def branch_contains(commit, branch_name = '')
|
|
801
|
+
command('branch', branch_name, '--contains', commit)
|
|
458
802
|
end
|
|
459
803
|
|
|
804
|
+
GREP_OPTION_MAP = [
|
|
805
|
+
{ keys: [:ignore_case], flag: '-i', type: :boolean },
|
|
806
|
+
{ keys: [:invert_match], flag: '-v', type: :boolean },
|
|
807
|
+
{ keys: [:extended_regexp], flag: '-E', type: :boolean },
|
|
808
|
+
# For validation only, as these are handled manually
|
|
809
|
+
{ keys: [:object], type: :validate_only },
|
|
810
|
+
{ keys: [:path_limiter], type: :validate_only }
|
|
811
|
+
].freeze
|
|
812
|
+
|
|
460
813
|
# returns hash
|
|
461
814
|
# [tree-ish] = [[line_no, match], [line_no, match2]]
|
|
462
815
|
# [tree-ish] = [[line_no, match], [line_no, match2]]
|
|
463
816
|
def grep(string, opts = {})
|
|
464
817
|
opts[:object] ||= 'HEAD'
|
|
818
|
+
ArgsBuilder.validate!(opts, GREP_OPTION_MAP)
|
|
465
819
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
grep_opts << '-v' if opts[:invert_match]
|
|
469
|
-
grep_opts << '-E' if opts[:extended_regexp]
|
|
470
|
-
grep_opts << '-e'
|
|
471
|
-
grep_opts << string
|
|
472
|
-
grep_opts << opts[:object] if opts[:object].is_a?(String)
|
|
473
|
-
grep_opts.push('--', opts[:path_limiter]) if opts[:path_limiter].is_a?(String)
|
|
474
|
-
grep_opts.push('--', *opts[:path_limiter]) if opts[:path_limiter].is_a?(Array)
|
|
820
|
+
boolean_flags = build_args(opts, GREP_OPTION_MAP)
|
|
821
|
+
args = ['-n', *boolean_flags, '-e', string, opts[:object]]
|
|
475
822
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if m = /(.*?)\:(\d+)\:(.*)/.match(line)
|
|
479
|
-
hsh[m[1]] ||= []
|
|
480
|
-
hsh[m[1]] << [m[2].to_i, m[3]]
|
|
481
|
-
end
|
|
823
|
+
if (limiter = opts[:path_limiter])
|
|
824
|
+
args.push('--', *Array(limiter))
|
|
482
825
|
end
|
|
483
|
-
|
|
826
|
+
|
|
827
|
+
lines = execute_grep_command(args)
|
|
828
|
+
parse_grep_output(lines)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
# Validate that the given arguments cannot be mistaken for a command-line option
|
|
832
|
+
#
|
|
833
|
+
# @param arg_name [String] the name of the arguments to mention in the error message
|
|
834
|
+
# @param args [Array<String, nil>] the arguments to validate
|
|
835
|
+
#
|
|
836
|
+
# @raise [ArgumentError] if any of the parameters are a string starting with a hyphen
|
|
837
|
+
# @return [void]
|
|
838
|
+
#
|
|
839
|
+
def assert_args_are_not_options(arg_name, *args)
|
|
840
|
+
invalid_args = args.select { |arg| arg&.start_with?('-') }
|
|
841
|
+
return unless invalid_args.any?
|
|
842
|
+
|
|
843
|
+
raise ArgumentError, "Invalid #{arg_name}: '#{invalid_args.join("', '")}'"
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
# Normalizes path specifications for Git commands
|
|
847
|
+
#
|
|
848
|
+
# Converts a single path or array of paths into a consistent array format
|
|
849
|
+
# suitable for appending to Git command arguments after '--'. Empty strings
|
|
850
|
+
# are filtered out after conversion.
|
|
851
|
+
#
|
|
852
|
+
# @param pathspecs [String, Pathname, Array<String, Pathname>, nil] path(s) to normalize
|
|
853
|
+
# @param arg_name [String] name of the argument for error messages
|
|
854
|
+
# @return [Array<String>, nil] normalized array of path strings, or nil if empty/nil input
|
|
855
|
+
# @raise [ArgumentError] if any path is not a String or Pathname
|
|
856
|
+
#
|
|
857
|
+
def normalize_pathspecs(pathspecs, arg_name)
|
|
858
|
+
return nil unless pathspecs
|
|
859
|
+
|
|
860
|
+
normalized = Array(pathspecs)
|
|
861
|
+
validate_pathspec_types(normalized, arg_name)
|
|
862
|
+
|
|
863
|
+
normalized = normalized.map(&:to_s).reject(&:empty?)
|
|
864
|
+
return nil if normalized.empty?
|
|
865
|
+
|
|
866
|
+
normalized
|
|
484
867
|
end
|
|
485
868
|
|
|
869
|
+
# Validates that all pathspecs are String or Pathname objects
|
|
870
|
+
#
|
|
871
|
+
# @param pathspecs [Array] the pathspecs to validate
|
|
872
|
+
# @param arg_name [String] name of the argument for error messages
|
|
873
|
+
# @raise [ArgumentError] if any path is not a String or Pathname
|
|
874
|
+
#
|
|
875
|
+
def validate_pathspec_types(pathspecs, arg_name)
|
|
876
|
+
return if pathspecs.all? { |path| path.is_a?(String) || path.is_a?(Pathname) }
|
|
877
|
+
|
|
878
|
+
raise ArgumentError, "Invalid #{arg_name}: must be a String, Pathname, or Array of Strings/Pathnames"
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Handle deprecated :path option in favor of :path_limiter
|
|
882
|
+
def handle_deprecated_path_option(opts)
|
|
883
|
+
if opts.key?(:path_limiter)
|
|
884
|
+
opts[:path_limiter]
|
|
885
|
+
elsif opts.key?(:path)
|
|
886
|
+
Git::Deprecation.warn(
|
|
887
|
+
'Git::Lib#diff_path_status :path option is deprecated. Use :path_limiter instead.'
|
|
888
|
+
)
|
|
889
|
+
opts[:path]
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
DIFF_FULL_OPTION_MAP = [
|
|
894
|
+
{ type: :static, flag: '-p' },
|
|
895
|
+
{ keys: [:path_limiter], type: :validate_only }
|
|
896
|
+
].freeze
|
|
897
|
+
|
|
486
898
|
def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
899
|
+
assert_args_are_not_options('commit or commit range', obj1, obj2)
|
|
900
|
+
ArgsBuilder.validate!(opts, DIFF_FULL_OPTION_MAP)
|
|
901
|
+
|
|
902
|
+
args = build_args(opts, DIFF_FULL_OPTION_MAP)
|
|
903
|
+
args.push(obj1, obj2).compact!
|
|
904
|
+
|
|
905
|
+
if (pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter'))
|
|
906
|
+
args.push('--', *pathspecs)
|
|
907
|
+
end
|
|
491
908
|
|
|
492
|
-
command('diff', *
|
|
909
|
+
command('diff', *args)
|
|
493
910
|
end
|
|
494
911
|
|
|
912
|
+
DIFF_STATS_OPTION_MAP = [
|
|
913
|
+
{ type: :static, flag: '--numstat' },
|
|
914
|
+
{ keys: [:path_limiter], type: :validate_only }
|
|
915
|
+
].freeze
|
|
916
|
+
|
|
495
917
|
def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
(insertions, deletions, filename) = file.split("\t")
|
|
505
|
-
hsh[:total][:insertions] += insertions.to_i
|
|
506
|
-
hsh[:total][:deletions] += deletions.to_i
|
|
507
|
-
hsh[:total][:lines] = (hsh[:total][:deletions] + hsh[:total][:insertions])
|
|
508
|
-
hsh[:total][:files] += 1
|
|
509
|
-
hsh[:files][filename] = {:insertions => insertions.to_i, :deletions => deletions.to_i}
|
|
918
|
+
assert_args_are_not_options('commit or commit range', obj1, obj2)
|
|
919
|
+
ArgsBuilder.validate!(opts, DIFF_STATS_OPTION_MAP)
|
|
920
|
+
|
|
921
|
+
args = build_args(opts, DIFF_STATS_OPTION_MAP)
|
|
922
|
+
args.push(obj1, obj2).compact!
|
|
923
|
+
|
|
924
|
+
if (pathspecs = normalize_pathspecs(opts[:path_limiter], 'path limiter'))
|
|
925
|
+
args.push('--', *pathspecs)
|
|
510
926
|
end
|
|
511
927
|
|
|
512
|
-
|
|
928
|
+
output_lines = command_lines('diff', *args)
|
|
929
|
+
parse_diff_stats_output(output_lines)
|
|
513
930
|
end
|
|
514
931
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
932
|
+
DIFF_PATH_STATUS_OPTION_MAP = [
|
|
933
|
+
{ type: :static, flag: '--name-status' },
|
|
934
|
+
{ keys: [:path_limiter], type: :validate_only },
|
|
935
|
+
{ keys: [:path], type: :validate_only }
|
|
936
|
+
].freeze
|
|
519
937
|
|
|
520
|
-
|
|
938
|
+
def diff_path_status(reference1 = nil, reference2 = nil, opts = {})
|
|
939
|
+
assert_args_are_not_options('commit or commit range', reference1, reference2)
|
|
940
|
+
ArgsBuilder.validate!(opts, DIFF_PATH_STATUS_OPTION_MAP)
|
|
521
941
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
942
|
+
args = build_args(opts, DIFF_PATH_STATUS_OPTION_MAP)
|
|
943
|
+
args.push(reference1, reference2).compact!
|
|
944
|
+
|
|
945
|
+
path_limiter = handle_deprecated_path_option(opts)
|
|
946
|
+
if (pathspecs = normalize_pathspecs(path_limiter, 'path limiter'))
|
|
947
|
+
args.push('--', *pathspecs)
|
|
526
948
|
end
|
|
949
|
+
|
|
950
|
+
parse_diff_path_status(args)
|
|
527
951
|
end
|
|
528
952
|
|
|
529
953
|
# compares the index and the working directory
|
|
@@ -536,48 +960,82 @@ module Git
|
|
|
536
960
|
diff_as_hash('diff-index', treeish)
|
|
537
961
|
end
|
|
538
962
|
|
|
539
|
-
|
|
963
|
+
# List all files that are in the index
|
|
964
|
+
#
|
|
965
|
+
# @param location [String] the location to list the files from
|
|
966
|
+
#
|
|
967
|
+
# @return [Hash<String, Hash>] a hash of files in the index
|
|
968
|
+
# * key: file [String] the file path
|
|
969
|
+
# * value: file_info [Hash] the file information containing the following keys:
|
|
970
|
+
# * :path [String] the file path
|
|
971
|
+
# * :mode_index [String] the file mode
|
|
972
|
+
# * :sha_index [String] the file sha
|
|
973
|
+
# * :stage [String] the file stage
|
|
974
|
+
#
|
|
975
|
+
def ls_files(location = nil)
|
|
540
976
|
location ||= '.'
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
977
|
+
{}.tap do |files|
|
|
978
|
+
command_lines('ls-files', '--stage', location).each do |line|
|
|
979
|
+
(info, file) = split_status_line(line)
|
|
980
|
+
(mode, sha, stage) = info.split
|
|
981
|
+
files[file] = {
|
|
982
|
+
path: file, mode_index: mode, sha_index: sha, stage: stage
|
|
983
|
+
}
|
|
547
984
|
end
|
|
548
|
-
hsh[file] = {:path => file, :mode_index => mode, :sha_index => sha, :stage => stage}
|
|
549
985
|
end
|
|
550
|
-
hsh
|
|
551
986
|
end
|
|
552
987
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
988
|
+
# Unescape a path if it is quoted
|
|
989
|
+
#
|
|
990
|
+
# Git commands that output paths (e.g. ls-files, diff), will escape unusual
|
|
991
|
+
# characters.
|
|
992
|
+
#
|
|
993
|
+
# @example
|
|
994
|
+
# lib.unescape_if_quoted('"quoted_file_\\342\\230\\240"') # => 'quoted_file_☠'
|
|
995
|
+
# lib.unescape_if_quoted('unquoted_file') # => 'unquoted_file'
|
|
996
|
+
#
|
|
997
|
+
# @param path [String] the path to unescape if quoted
|
|
998
|
+
#
|
|
999
|
+
# @return [String] the unescaped path if quoted otherwise the original path
|
|
1000
|
+
#
|
|
1001
|
+
# @api private
|
|
1002
|
+
#
|
|
1003
|
+
def unescape_quoted_path(path)
|
|
1004
|
+
if path.start_with?('"') && path.end_with?('"')
|
|
1005
|
+
Git::EscapedPath.new(path[1..-2]).unescape
|
|
1006
|
+
else
|
|
1007
|
+
path
|
|
567
1008
|
end
|
|
568
1009
|
end
|
|
569
1010
|
|
|
1011
|
+
LS_REMOTE_OPTION_MAP = [
|
|
1012
|
+
{ keys: [:refs], flag: '--refs', type: :boolean }
|
|
1013
|
+
].freeze
|
|
1014
|
+
|
|
1015
|
+
def ls_remote(location = nil, opts = {})
|
|
1016
|
+
ArgsBuilder.validate!(opts, LS_REMOTE_OPTION_MAP)
|
|
1017
|
+
|
|
1018
|
+
flags = build_args(opts, LS_REMOTE_OPTION_MAP)
|
|
1019
|
+
positional_arg = location || '.'
|
|
1020
|
+
|
|
1021
|
+
output_lines = command_lines('ls-remote', *flags, positional_arg)
|
|
1022
|
+
parse_ls_remote_output(output_lines)
|
|
1023
|
+
end
|
|
1024
|
+
|
|
570
1025
|
def ignored_files
|
|
571
|
-
command_lines('ls-files', '--others', '-i', '--exclude-standard')
|
|
1026
|
+
command_lines('ls-files', '--others', '-i', '--exclude-standard').map { |f| unescape_quoted_path(f) }
|
|
572
1027
|
end
|
|
573
1028
|
|
|
1029
|
+
def untracked_files
|
|
1030
|
+
command_lines('ls-files', '--others', '--exclude-standard', chdir: @git_work_dir).map do |f|
|
|
1031
|
+
unescape_quoted_path(f)
|
|
1032
|
+
end
|
|
1033
|
+
end
|
|
574
1034
|
|
|
575
1035
|
def config_remote(name)
|
|
576
1036
|
hsh = {}
|
|
577
1037
|
config_list.each do |key, value|
|
|
578
|
-
if /remote.#{name}/.match(key)
|
|
579
|
-
hsh[key.gsub("remote.#{name}.", '')] = value
|
|
580
|
-
end
|
|
1038
|
+
hsh[key.gsub("remote.#{name}.", '')] = value if /remote.#{name}/.match(key)
|
|
581
1039
|
end
|
|
582
1040
|
hsh
|
|
583
1041
|
end
|
|
@@ -616,7 +1074,7 @@ module Git
|
|
|
616
1074
|
# @param [String|NilClass] objectish the target object reference (nil == HEAD)
|
|
617
1075
|
# @param [String|NilClass] path the path of the file to be shown
|
|
618
1076
|
# @return [String] the object information
|
|
619
|
-
def show(objectish=nil, path=nil)
|
|
1077
|
+
def show(objectish = nil, path = nil)
|
|
620
1078
|
arr_opts = []
|
|
621
1079
|
|
|
622
1080
|
arr_opts << (path ? "#{objectish}:#{path}" : objectish)
|
|
@@ -626,55 +1084,97 @@ module Git
|
|
|
626
1084
|
|
|
627
1085
|
## WRITE COMMANDS ##
|
|
628
1086
|
|
|
1087
|
+
CONFIG_SET_OPTION_MAP = [
|
|
1088
|
+
{ keys: [:file], flag: '--file', type: :valued_space }
|
|
1089
|
+
].freeze
|
|
1090
|
+
|
|
629
1091
|
def config_set(name, value, options = {})
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
command('config', '--file', options[:file], name, value)
|
|
634
|
-
end
|
|
1092
|
+
ArgsBuilder.validate!(options, CONFIG_SET_OPTION_MAP)
|
|
1093
|
+
flags = build_args(options, CONFIG_SET_OPTION_MAP)
|
|
1094
|
+
command('config', *flags, name, value)
|
|
635
1095
|
end
|
|
636
1096
|
|
|
637
1097
|
def global_config_set(name, value)
|
|
638
1098
|
command('config', '--global', name, value)
|
|
639
1099
|
end
|
|
640
1100
|
|
|
641
|
-
|
|
1101
|
+
ADD_OPTION_MAP = [
|
|
1102
|
+
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1103
|
+
{ keys: [:force], flag: '--force', type: :boolean }
|
|
1104
|
+
].freeze
|
|
1105
|
+
|
|
1106
|
+
# Update the index from the current worktree to prepare the for the next commit
|
|
642
1107
|
#
|
|
643
|
-
#
|
|
644
|
-
#
|
|
645
|
-
#
|
|
1108
|
+
# @example
|
|
1109
|
+
# lib.add('path/to/file')
|
|
1110
|
+
# lib.add(['path/to/file1','path/to/file2'])
|
|
1111
|
+
# lib.add(:all => true)
|
|
646
1112
|
#
|
|
647
|
-
#
|
|
648
|
-
# :all => true
|
|
649
|
-
# :force => true
|
|
650
|
-
#
|
|
651
|
-
# @param [String,Array] paths files paths to be added to the repository
|
|
1113
|
+
# @param [String, Array<String>] paths files to be added to the repository (relative to the worktree root)
|
|
652
1114
|
# @param [Hash] options
|
|
653
|
-
|
|
654
|
-
|
|
1115
|
+
#
|
|
1116
|
+
# @option options [Boolean] :all Add, modify, and remove index entries to match the worktree
|
|
1117
|
+
# @option options [Boolean] :force Allow adding otherwise ignored files
|
|
1118
|
+
#
|
|
1119
|
+
def add(paths = '.', options = {})
|
|
1120
|
+
args = build_args(options, ADD_OPTION_MAP)
|
|
655
1121
|
|
|
656
|
-
|
|
657
|
-
|
|
1122
|
+
args << '--'
|
|
1123
|
+
args.concat(Array(paths))
|
|
658
1124
|
|
|
659
|
-
|
|
1125
|
+
command('add', *args)
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
RM_OPTION_MAP = [
|
|
1129
|
+
{ type: :static, flag: '-f' },
|
|
1130
|
+
{ keys: [:recursive], flag: '-r', type: :boolean },
|
|
1131
|
+
{ keys: [:cached], flag: '--cached', type: :boolean }
|
|
1132
|
+
].freeze
|
|
660
1133
|
|
|
661
|
-
|
|
1134
|
+
def rm(path = '.', opts = {})
|
|
1135
|
+
args = build_args(opts, RM_OPTION_MAP)
|
|
662
1136
|
|
|
663
|
-
|
|
1137
|
+
args << '--'
|
|
1138
|
+
args.concat(Array(path))
|
|
664
1139
|
|
|
665
|
-
command('
|
|
1140
|
+
command('rm', *args)
|
|
666
1141
|
end
|
|
667
1142
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
1143
|
+
# Returns true if the repository is empty (meaning it has no commits)
|
|
1144
|
+
#
|
|
1145
|
+
# @return [Boolean]
|
|
1146
|
+
#
|
|
1147
|
+
def empty?
|
|
1148
|
+
command('rev-parse', '--verify', 'HEAD')
|
|
1149
|
+
false
|
|
1150
|
+
rescue Git::FailedError => e
|
|
1151
|
+
raise unless e.result.status.exitstatus == 128 &&
|
|
1152
|
+
e.result.stderr == 'fatal: Needed a single revision'
|
|
674
1153
|
|
|
675
|
-
|
|
1154
|
+
true
|
|
676
1155
|
end
|
|
677
1156
|
|
|
1157
|
+
COMMIT_OPTION_MAP = [
|
|
1158
|
+
{ keys: %i[add_all all], flag: '--all', type: :boolean },
|
|
1159
|
+
{ keys: [:allow_empty], flag: '--allow-empty', type: :boolean },
|
|
1160
|
+
{ keys: [:no_verify], flag: '--no-verify', type: :boolean },
|
|
1161
|
+
{ keys: [:allow_empty_message], flag: '--allow-empty-message', type: :boolean },
|
|
1162
|
+
{ keys: [:author], flag: '--author', type: :valued_equals },
|
|
1163
|
+
{ keys: [:message], flag: '--message', type: :valued_equals },
|
|
1164
|
+
{ keys: [:no_gpg_sign], flag: '--no-gpg-sign', type: :boolean },
|
|
1165
|
+
{ keys: [:date], flag: '--date', type: :valued_equals, validator: ->(v) { v.is_a?(String) } },
|
|
1166
|
+
{ keys: [:amend], type: :custom, builder: ->(value) { ['--amend', '--no-edit'] if value } },
|
|
1167
|
+
{
|
|
1168
|
+
keys: [:gpg_sign],
|
|
1169
|
+
type: :custom,
|
|
1170
|
+
builder: lambda { |value|
|
|
1171
|
+
if value
|
|
1172
|
+
value == true ? '--gpg-sign' : "--gpg-sign=#{value}"
|
|
1173
|
+
end
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
].freeze
|
|
1177
|
+
|
|
678
1178
|
# Takes the commit message with the options and executes the commit command
|
|
679
1179
|
#
|
|
680
1180
|
# accepts options:
|
|
@@ -690,59 +1190,52 @@ module Git
|
|
|
690
1190
|
#
|
|
691
1191
|
# @param [String] message the commit message to be used
|
|
692
1192
|
# @param [Hash] opts the commit options to be used
|
|
1193
|
+
|
|
693
1194
|
def commit(message, opts = {})
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
arr_opts << '--allow-empty' if opts[:allow_empty]
|
|
699
|
-
arr_opts << "--author=#{opts[:author]}" if opts[:author]
|
|
700
|
-
arr_opts << "--date=#{opts[:date]}" if opts[:date].is_a? String
|
|
701
|
-
arr_opts << '--no-verify' if opts[:no_verify]
|
|
702
|
-
arr_opts << '--allow-empty-message' if opts[:allow_empty_message]
|
|
703
|
-
|
|
704
|
-
if opts[:gpg_sign] && opts[:no_gpg_sign]
|
|
705
|
-
raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign'
|
|
706
|
-
elsif opts[:gpg_sign]
|
|
707
|
-
arr_opts <<
|
|
708
|
-
if opts[:gpg_sign] == true
|
|
709
|
-
'--gpg-sign'
|
|
710
|
-
else
|
|
711
|
-
"--gpg-sign=#{opts[:gpg_sign]}"
|
|
712
|
-
end
|
|
713
|
-
elsif opts[:no_gpg_sign]
|
|
714
|
-
arr_opts << '--no-gpg-sign'
|
|
715
|
-
end
|
|
1195
|
+
opts[:message] = message if message # Handle message arg for backward compatibility
|
|
1196
|
+
|
|
1197
|
+
# Perform cross-option validation before building args
|
|
1198
|
+
raise ArgumentError, 'cannot specify :gpg_sign and :no_gpg_sign' if opts[:gpg_sign] && opts[:no_gpg_sign]
|
|
716
1199
|
|
|
717
|
-
|
|
1200
|
+
ArgsBuilder.validate!(opts, COMMIT_OPTION_MAP)
|
|
1201
|
+
|
|
1202
|
+
args = build_args(opts, COMMIT_OPTION_MAP)
|
|
1203
|
+
command('commit', *args)
|
|
718
1204
|
end
|
|
1205
|
+
RESET_OPTION_MAP = [
|
|
1206
|
+
{ keys: [:hard], flag: '--hard', type: :boolean }
|
|
1207
|
+
].freeze
|
|
719
1208
|
|
|
720
1209
|
def reset(commit, opts = {})
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
command('reset', *arr_opts)
|
|
1210
|
+
args = build_args(opts, RESET_OPTION_MAP)
|
|
1211
|
+
args << commit if commit
|
|
1212
|
+
command('reset', *args)
|
|
725
1213
|
end
|
|
726
1214
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1215
|
+
CLEAN_OPTION_MAP = [
|
|
1216
|
+
{ keys: [:force], flag: '--force', type: :boolean },
|
|
1217
|
+
{ keys: [:ff], flag: '-ff', type: :boolean },
|
|
1218
|
+
{ keys: [:d], flag: '-d', type: :boolean },
|
|
1219
|
+
{ keys: [:x], flag: '-x', type: :boolean }
|
|
1220
|
+
].freeze
|
|
733
1221
|
|
|
734
|
-
|
|
1222
|
+
def clean(opts = {})
|
|
1223
|
+
args = build_args(opts, CLEAN_OPTION_MAP)
|
|
1224
|
+
command('clean', *args)
|
|
735
1225
|
end
|
|
736
1226
|
|
|
1227
|
+
REVERT_OPTION_MAP = [
|
|
1228
|
+
{ keys: [:no_edit], flag: '--no-edit', type: :boolean }
|
|
1229
|
+
].freeze
|
|
1230
|
+
|
|
737
1231
|
def revert(commitish, opts = {})
|
|
738
1232
|
# Forcing --no-edit as default since it's not an interactive session.
|
|
739
|
-
opts = {:
|
|
1233
|
+
opts = { no_edit: true }.merge(opts)
|
|
740
1234
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
arr_opts << commitish
|
|
1235
|
+
args = build_args(opts, REVERT_OPTION_MAP)
|
|
1236
|
+
args << commitish
|
|
744
1237
|
|
|
745
|
-
command('revert', *
|
|
1238
|
+
command('revert', *args)
|
|
746
1239
|
end
|
|
747
1240
|
|
|
748
1241
|
def apply(patch_file)
|
|
@@ -758,17 +1251,9 @@ module Git
|
|
|
758
1251
|
end
|
|
759
1252
|
|
|
760
1253
|
def stashes_all
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if File.exist?(filename)
|
|
764
|
-
File.open(filename) do |f|
|
|
765
|
-
f.each_with_index do |line, i|
|
|
766
|
-
m = line.match(/:(.*)$/)
|
|
767
|
-
arr << [i, m[1].strip]
|
|
768
|
-
end
|
|
769
|
-
end
|
|
1254
|
+
stash_log_lines.each_with_index.map do |line, index|
|
|
1255
|
+
parse_stash_log_line(line, index)
|
|
770
1256
|
end
|
|
771
|
-
arr
|
|
772
1257
|
end
|
|
773
1258
|
|
|
774
1259
|
def stash_save(message)
|
|
@@ -800,6 +1285,12 @@ module Git
|
|
|
800
1285
|
command('branch', '-D', branch)
|
|
801
1286
|
end
|
|
802
1287
|
|
|
1288
|
+
CHECKOUT_OPTION_MAP = [
|
|
1289
|
+
{ keys: %i[force f], flag: '--force', type: :boolean },
|
|
1290
|
+
{ keys: %i[new_branch b], type: :validate_only },
|
|
1291
|
+
{ keys: [:start_point], type: :validate_only }
|
|
1292
|
+
].freeze
|
|
1293
|
+
|
|
803
1294
|
# Runs checkout command to checkout or create branch
|
|
804
1295
|
#
|
|
805
1296
|
# accepts options:
|
|
@@ -810,18 +1301,16 @@ module Git
|
|
|
810
1301
|
# @param [String] branch
|
|
811
1302
|
# @param [Hash] opts
|
|
812
1303
|
def checkout(branch = nil, opts = {})
|
|
813
|
-
if branch.is_a?(Hash) && opts
|
|
1304
|
+
if branch.is_a?(Hash) && opts.empty?
|
|
814
1305
|
opts = branch
|
|
815
1306
|
branch = nil
|
|
816
1307
|
end
|
|
1308
|
+
ArgsBuilder.validate!(opts, CHECKOUT_OPTION_MAP)
|
|
817
1309
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
arr_opts << '--force' if opts[:force] || opts[:f]
|
|
821
|
-
arr_opts << branch if branch
|
|
822
|
-
arr_opts << opts[:start_point] if opts[:start_point] && arr_opts.include?('-b')
|
|
1310
|
+
flags = build_args(opts, CHECKOUT_OPTION_MAP)
|
|
1311
|
+
positional_args = build_checkout_positional_args(branch, opts)
|
|
823
1312
|
|
|
824
|
-
command('checkout', *
|
|
1313
|
+
command('checkout', *flags, *positional_args)
|
|
825
1314
|
end
|
|
826
1315
|
|
|
827
1316
|
def checkout_file(version, file)
|
|
@@ -831,62 +1320,88 @@ module Git
|
|
|
831
1320
|
command('checkout', *arr_opts)
|
|
832
1321
|
end
|
|
833
1322
|
|
|
1323
|
+
MERGE_OPTION_MAP = [
|
|
1324
|
+
{ keys: [:no_commit], flag: '--no-commit', type: :boolean },
|
|
1325
|
+
{ keys: [:no_ff], flag: '--no-ff', type: :boolean },
|
|
1326
|
+
{ keys: [:m], flag: '-m', type: :valued_space }
|
|
1327
|
+
].freeze
|
|
1328
|
+
|
|
834
1329
|
def merge(branch, message = nil, opts = {})
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1330
|
+
# For backward compatibility, treat the message arg as the :m option.
|
|
1331
|
+
opts[:m] = message if message
|
|
1332
|
+
ArgsBuilder.validate!(opts, MERGE_OPTION_MAP)
|
|
1333
|
+
|
|
1334
|
+
args = build_args(opts, MERGE_OPTION_MAP)
|
|
1335
|
+
args.concat(Array(branch))
|
|
1336
|
+
|
|
1337
|
+
command('merge', *args)
|
|
841
1338
|
end
|
|
842
1339
|
|
|
1340
|
+
MERGE_BASE_OPTION_MAP = [
|
|
1341
|
+
{ keys: [:octopus], flag: '--octopus', type: :boolean },
|
|
1342
|
+
{ keys: [:independent], flag: '--independent', type: :boolean },
|
|
1343
|
+
{ keys: [:fork_point], flag: '--fork-point', type: :boolean },
|
|
1344
|
+
{ keys: [:all], flag: '--all', type: :boolean }
|
|
1345
|
+
].freeze
|
|
1346
|
+
|
|
843
1347
|
def merge_base(*args)
|
|
844
1348
|
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1349
|
+
ArgsBuilder.validate!(opts, MERGE_BASE_OPTION_MAP)
|
|
845
1350
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
arg_opts << '--octopus' if opts[:octopus]
|
|
849
|
-
arg_opts << '--independent' if opts[:independent]
|
|
850
|
-
arg_opts << '--fork-point' if opts[:fork_point]
|
|
851
|
-
arg_opts << '--all' if opts[:all]
|
|
1351
|
+
flags = build_args(opts, MERGE_BASE_OPTION_MAP)
|
|
1352
|
+
command_args = flags + args
|
|
852
1353
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
command('merge-base', *arg_opts).lines.map(&:strip)
|
|
1354
|
+
command('merge-base', *command_args).lines.map(&:strip)
|
|
856
1355
|
end
|
|
857
1356
|
|
|
858
1357
|
def unmerged
|
|
859
1358
|
unmerged = []
|
|
860
|
-
command_lines('diff',
|
|
861
|
-
unmerged <<
|
|
1359
|
+
command_lines('diff', '--cached').each do |line|
|
|
1360
|
+
unmerged << ::Regexp.last_match(1) if line =~ /^\* Unmerged path (.*)/
|
|
862
1361
|
end
|
|
863
1362
|
unmerged
|
|
864
1363
|
end
|
|
865
1364
|
|
|
866
1365
|
def conflicts # :yields: file, your, their
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
command('show', ":3:#{f}", redirect: "> #{escape their}")
|
|
877
|
-
yield(f, your, their)
|
|
1366
|
+
unmerged.each do |file_path|
|
|
1367
|
+
Tempfile.create(['YOUR-', File.basename(file_path)]) do |your_file|
|
|
1368
|
+
write_staged_content(file_path, 2, your_file).flush
|
|
1369
|
+
|
|
1370
|
+
Tempfile.create(['THEIR-', File.basename(file_path)]) do |their_file|
|
|
1371
|
+
write_staged_content(file_path, 3, their_file).flush
|
|
1372
|
+
yield(file_path, your_file.path, their_file.path)
|
|
1373
|
+
end
|
|
1374
|
+
end
|
|
878
1375
|
end
|
|
879
1376
|
end
|
|
880
1377
|
|
|
1378
|
+
REMOTE_ADD_OPTION_MAP = [
|
|
1379
|
+
{ keys: %i[with_fetch fetch], flag: '-f', type: :boolean },
|
|
1380
|
+
{ keys: [:track], flag: '-t', type: :valued_space }
|
|
1381
|
+
].freeze
|
|
1382
|
+
|
|
881
1383
|
def remote_add(name, url, opts = {})
|
|
882
|
-
|
|
883
|
-
arr_opts << '-f' if opts[:with_fetch] || opts[:fetch]
|
|
884
|
-
arr_opts << '-t' << opts[:track] if opts[:track]
|
|
885
|
-
arr_opts << '--'
|
|
886
|
-
arr_opts << name
|
|
887
|
-
arr_opts << url
|
|
1384
|
+
ArgsBuilder.validate!(opts, REMOTE_ADD_OPTION_MAP)
|
|
888
1385
|
|
|
889
|
-
|
|
1386
|
+
flags = build_args(opts, REMOTE_ADD_OPTION_MAP)
|
|
1387
|
+
positional_args = ['--', name, url]
|
|
1388
|
+
command_args = ['add'] + flags + positional_args
|
|
1389
|
+
|
|
1390
|
+
command('remote', *command_args)
|
|
1391
|
+
end
|
|
1392
|
+
|
|
1393
|
+
REMOTE_SET_BRANCHES_OPTION_MAP = [
|
|
1394
|
+
{ keys: [:add], flag: '--add', type: :boolean }
|
|
1395
|
+
].freeze
|
|
1396
|
+
|
|
1397
|
+
def remote_set_branches(name, branches, opts = {})
|
|
1398
|
+
ArgsBuilder.validate!(opts, REMOTE_SET_BRANCHES_OPTION_MAP)
|
|
1399
|
+
|
|
1400
|
+
flags = build_args(opts, REMOTE_SET_BRANCHES_OPTION_MAP)
|
|
1401
|
+
branch_args = Array(branches).flatten
|
|
1402
|
+
command_args = ['set-branches'] + flags + [name] + branch_args
|
|
1403
|
+
|
|
1404
|
+
command('remote', *command_args)
|
|
890
1405
|
end
|
|
891
1406
|
|
|
892
1407
|
def remote_set_url(name, url)
|
|
@@ -909,99 +1424,103 @@ module Git
|
|
|
909
1424
|
command_lines('tag')
|
|
910
1425
|
end
|
|
911
1426
|
|
|
912
|
-
|
|
913
|
-
|
|
1427
|
+
TAG_OPTION_MAP = [
|
|
1428
|
+
{ keys: %i[force f], flag: '-f', type: :boolean },
|
|
1429
|
+
{ keys: %i[annotate a], flag: '-a', type: :boolean },
|
|
1430
|
+
{ keys: %i[sign s], flag: '-s', type: :boolean },
|
|
1431
|
+
{ keys: %i[delete d], flag: '-d', type: :boolean },
|
|
1432
|
+
{ keys: %i[message m], flag: '-m', type: :valued_space }
|
|
1433
|
+
].freeze
|
|
914
1434
|
|
|
915
|
-
|
|
1435
|
+
def tag(name, *args)
|
|
1436
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
|
1437
|
+
target = args.first
|
|
916
1438
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
end
|
|
1439
|
+
validate_tag_options!(opts)
|
|
1440
|
+
ArgsBuilder.validate!(opts, TAG_OPTION_MAP)
|
|
920
1441
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
arr_opts << '-f' if opts[:force] || opts[:f]
|
|
924
|
-
arr_opts << '-a' if opts[:a] || opts[:annotate]
|
|
925
|
-
arr_opts << '-s' if opts[:s] || opts[:sign]
|
|
926
|
-
arr_opts << '-d' if opts[:d] || opts[:delete]
|
|
927
|
-
arr_opts << name
|
|
928
|
-
arr_opts << target if target
|
|
1442
|
+
flags = build_args(opts, TAG_OPTION_MAP)
|
|
1443
|
+
positional_args = [name, target].compact
|
|
929
1444
|
|
|
930
|
-
|
|
931
|
-
arr_opts << '-m' << (opts[:m] || opts[:message])
|
|
932
|
-
end
|
|
933
|
-
|
|
934
|
-
command('tag', *arr_opts)
|
|
1445
|
+
command('tag', *flags, *positional_args)
|
|
935
1446
|
end
|
|
936
1447
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
arr_opts << remote if remote
|
|
949
|
-
arr_opts << opts[:ref] if opts[:ref]
|
|
950
|
-
|
|
951
|
-
command('fetch', *arr_opts)
|
|
952
|
-
end
|
|
1448
|
+
FETCH_OPTION_MAP = [
|
|
1449
|
+
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1450
|
+
{ keys: %i[tags t], flag: '--tags', type: :boolean },
|
|
1451
|
+
{ keys: %i[prune p], flag: '--prune', type: :boolean },
|
|
1452
|
+
{ keys: %i[prune-tags P], flag: '--prune-tags', type: :boolean },
|
|
1453
|
+
{ keys: %i[force f], flag: '--force', type: :boolean },
|
|
1454
|
+
{ keys: %i[update-head-ok u], flag: '--update-head-ok', type: :boolean },
|
|
1455
|
+
{ keys: [:unshallow], flag: '--unshallow', type: :boolean },
|
|
1456
|
+
{ keys: [:depth], flag: '--depth', type: :valued_space },
|
|
1457
|
+
{ keys: [:ref], type: :validate_only }
|
|
1458
|
+
].freeze
|
|
953
1459
|
|
|
954
|
-
def
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
branch = nil
|
|
958
|
-
end
|
|
1460
|
+
def fetch(remote, opts)
|
|
1461
|
+
ArgsBuilder.validate!(opts, FETCH_OPTION_MAP)
|
|
1462
|
+
args = build_args(opts, FETCH_OPTION_MAP)
|
|
959
1463
|
|
|
960
|
-
if
|
|
961
|
-
|
|
962
|
-
remote
|
|
1464
|
+
if remote || opts[:ref]
|
|
1465
|
+
args << '--'
|
|
1466
|
+
args << remote if remote
|
|
1467
|
+
args << opts[:ref] if opts[:ref]
|
|
963
1468
|
end
|
|
964
1469
|
|
|
965
|
-
|
|
1470
|
+
command('fetch', *args, merge: true)
|
|
1471
|
+
end
|
|
966
1472
|
|
|
967
|
-
|
|
968
|
-
|
|
1473
|
+
PUSH_OPTION_MAP = [
|
|
1474
|
+
{ keys: [:mirror], flag: '--mirror', type: :boolean },
|
|
1475
|
+
{ keys: [:delete], flag: '--delete', type: :boolean },
|
|
1476
|
+
{ keys: %i[force f], flag: '--force', type: :boolean },
|
|
1477
|
+
{ keys: [:push_option], flag: '--push-option', type: :repeatable_valued_space },
|
|
1478
|
+
{ keys: [:all], type: :validate_only }, # For validation purposes
|
|
1479
|
+
{ keys: [:tags], type: :validate_only } # From the `push` method's logic
|
|
1480
|
+
].freeze
|
|
969
1481
|
|
|
970
|
-
|
|
1482
|
+
def push(remote = nil, branch = nil, opts = nil)
|
|
1483
|
+
remote, branch, opts = normalize_push_args(remote, branch, opts)
|
|
1484
|
+
ArgsBuilder.validate!(opts, PUSH_OPTION_MAP)
|
|
971
1485
|
|
|
972
|
-
|
|
973
|
-
arr_opts << '--mirror' if opts[:mirror]
|
|
974
|
-
arr_opts << '--delete' if opts[:delete]
|
|
975
|
-
arr_opts << '--force' if opts[:force] || opts[:f]
|
|
976
|
-
arr_opts << '--all' if opts[:all] && remote
|
|
1486
|
+
raise ArgumentError, 'remote is required if branch is specified' if !remote && branch
|
|
977
1487
|
|
|
978
|
-
|
|
979
|
-
arr_opts << remote if remote
|
|
980
|
-
arr_opts_with_branch = arr_opts.dup
|
|
981
|
-
arr_opts_with_branch << branch if branch
|
|
1488
|
+
args = build_push_args(remote, branch, opts)
|
|
982
1489
|
|
|
983
1490
|
if opts[:mirror]
|
|
984
|
-
|
|
1491
|
+
command('push', *args)
|
|
985
1492
|
else
|
|
986
|
-
|
|
987
|
-
|
|
1493
|
+
command('push', *args)
|
|
1494
|
+
command('push', '--tags', *(args - [branch].compact)) if opts[:tags]
|
|
988
1495
|
end
|
|
989
1496
|
end
|
|
990
1497
|
|
|
991
|
-
|
|
992
|
-
|
|
1498
|
+
PULL_OPTION_MAP = [
|
|
1499
|
+
{ keys: [:allow_unrelated_histories], flag: '--allow-unrelated-histories', type: :boolean }
|
|
1500
|
+
].freeze
|
|
993
1501
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1502
|
+
def pull(remote = nil, branch = nil, opts = {})
|
|
1503
|
+
raise ArgumentError, 'You must specify a remote if a branch is specified' if remote.nil? && !branch.nil?
|
|
1504
|
+
|
|
1505
|
+
ArgsBuilder.validate!(opts, PULL_OPTION_MAP)
|
|
1506
|
+
|
|
1507
|
+
flags = build_args(opts, PULL_OPTION_MAP)
|
|
1508
|
+
positional_args = [remote, branch].compact
|
|
1509
|
+
|
|
1510
|
+
command('pull', *flags, *positional_args)
|
|
998
1511
|
end
|
|
999
1512
|
|
|
1000
1513
|
def tag_sha(tag_name)
|
|
1001
1514
|
head = File.join(@git_dir, 'refs', 'tags', tag_name)
|
|
1002
1515
|
return File.read(head).chomp if File.exist?(head)
|
|
1003
1516
|
|
|
1004
|
-
|
|
1517
|
+
begin
|
|
1518
|
+
command('show-ref', '--tags', '-s', tag_name)
|
|
1519
|
+
rescue Git::FailedError => e
|
|
1520
|
+
raise unless e.result.status.exitstatus == 1 && e.result.stderr == ''
|
|
1521
|
+
|
|
1522
|
+
''
|
|
1523
|
+
end
|
|
1005
1524
|
end
|
|
1006
1525
|
|
|
1007
1526
|
def repack
|
|
@@ -1012,89 +1531,117 @@ module Git
|
|
|
1012
1531
|
command('gc', '--prune', '--aggressive', '--auto')
|
|
1013
1532
|
end
|
|
1014
1533
|
|
|
1015
|
-
|
|
1534
|
+
FSCK_OPTION_MAP = [
|
|
1535
|
+
{ flag: '--no-progress', type: :static },
|
|
1536
|
+
{ keys: [:unreachable], flag: '--unreachable', type: :boolean },
|
|
1537
|
+
{ keys: [:strict], flag: '--strict', type: :boolean },
|
|
1538
|
+
{ keys: [:connectivity_only], flag: '--connectivity-only', type: :boolean },
|
|
1539
|
+
{ keys: [:root], flag: '--root', type: :boolean },
|
|
1540
|
+
{ keys: [:tags], flag: '--tags', type: :boolean },
|
|
1541
|
+
{ keys: [:cache], flag: '--cache', type: :boolean },
|
|
1542
|
+
{ keys: [:no_reflogs], flag: '--no-reflogs', type: :boolean },
|
|
1543
|
+
{ keys: [:lost_found], flag: '--lost-found', type: :boolean },
|
|
1544
|
+
{ keys: [:dangling], flag: '--dangling', type: :boolean_negatable },
|
|
1545
|
+
{ keys: [:full], flag: '--full', type: :boolean_negatable },
|
|
1546
|
+
{ keys: [:name_objects], flag: '--name-objects', type: :boolean_negatable },
|
|
1547
|
+
{ keys: [:references], flag: '--references', type: :boolean_negatable }
|
|
1548
|
+
].freeze
|
|
1549
|
+
|
|
1550
|
+
def fsck(*objects, **opts)
|
|
1551
|
+
args = ArgsBuilder.build(opts, FSCK_OPTION_MAP)
|
|
1552
|
+
args.concat(objects) unless objects.empty?
|
|
1553
|
+
# fsck returns non-zero exit status when issues are found:
|
|
1554
|
+
# 1 = errors found, 2 = missing objects, 4 = warnings
|
|
1555
|
+
# We still want to parse the output in these cases
|
|
1556
|
+
output = begin
|
|
1557
|
+
command('fsck', *args)
|
|
1558
|
+
rescue Git::FailedError => e
|
|
1559
|
+
raise unless [1, 2, 4].include?(e.result.status.exitstatus)
|
|
1560
|
+
|
|
1561
|
+
e.result.stdout
|
|
1562
|
+
end
|
|
1563
|
+
parse_fsck_output(output)
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
READ_TREE_OPTION_MAP = [
|
|
1567
|
+
{ keys: [:prefix], flag: '--prefix', type: :valued_equals }
|
|
1568
|
+
].freeze
|
|
1569
|
+
|
|
1016
1570
|
def read_tree(treeish, opts = {})
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
command('read-tree', *arr_opts)
|
|
1571
|
+
ArgsBuilder.validate!(opts, READ_TREE_OPTION_MAP)
|
|
1572
|
+
flags = build_args(opts, READ_TREE_OPTION_MAP)
|
|
1573
|
+
command('read-tree', *flags, treeish)
|
|
1021
1574
|
end
|
|
1022
1575
|
|
|
1023
1576
|
def write_tree
|
|
1024
1577
|
command('write-tree')
|
|
1025
1578
|
end
|
|
1026
1579
|
|
|
1580
|
+
COMMIT_TREE_OPTION_MAP = [
|
|
1581
|
+
{ keys: %i[parent parents], flag: '-p', type: :repeatable_valued_space },
|
|
1582
|
+
{ keys: [:message], flag: '-m', type: :valued_space }
|
|
1583
|
+
].freeze
|
|
1584
|
+
|
|
1027
1585
|
def commit_tree(tree, opts = {})
|
|
1028
1586
|
opts[:message] ||= "commit tree #{tree}"
|
|
1029
|
-
|
|
1030
|
-
t.write(opts[:message])
|
|
1031
|
-
t.close
|
|
1587
|
+
ArgsBuilder.validate!(opts, COMMIT_TREE_OPTION_MAP)
|
|
1032
1588
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
arr_opts << '-p' << opts[:parent] if opts[:parent]
|
|
1036
|
-
arr_opts += Array(opts[:parents]).map { |p| ['-p', p] }.flatten if opts[:parents]
|
|
1037
|
-
command('commit-tree', *arr_opts, redirect: "< #{escape t.path}")
|
|
1589
|
+
flags = build_args(opts, COMMIT_TREE_OPTION_MAP)
|
|
1590
|
+
command('commit-tree', tree, *flags)
|
|
1038
1591
|
end
|
|
1039
1592
|
|
|
1040
1593
|
def update_ref(ref, commit)
|
|
1041
1594
|
command('update-ref', ref, commit)
|
|
1042
1595
|
end
|
|
1043
1596
|
|
|
1597
|
+
CHECKOUT_INDEX_OPTION_MAP = [
|
|
1598
|
+
{ keys: [:prefix], flag: '--prefix', type: :valued_equals },
|
|
1599
|
+
{ keys: [:force], flag: '--force', type: :boolean },
|
|
1600
|
+
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1601
|
+
{ keys: [:path_limiter], type: :validate_only }
|
|
1602
|
+
].freeze
|
|
1603
|
+
|
|
1044
1604
|
def checkout_index(opts = {})
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1605
|
+
ArgsBuilder.validate!(opts, CHECKOUT_INDEX_OPTION_MAP)
|
|
1606
|
+
args = build_args(opts, CHECKOUT_INDEX_OPTION_MAP)
|
|
1607
|
+
|
|
1608
|
+
if (path = opts[:path_limiter]) && path.is_a?(String)
|
|
1609
|
+
args.push('--', path)
|
|
1610
|
+
end
|
|
1050
1611
|
|
|
1051
|
-
command('checkout-index', *
|
|
1612
|
+
command('checkout-index', *args)
|
|
1052
1613
|
end
|
|
1053
1614
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1615
|
+
ARCHIVE_OPTION_MAP = [
|
|
1616
|
+
{ keys: [:prefix], flag: '--prefix', type: :valued_equals },
|
|
1617
|
+
{ keys: [:remote], flag: '--remote', type: :valued_equals },
|
|
1618
|
+
# These options are used by helpers or handled manually
|
|
1619
|
+
{ keys: [:path], type: :validate_only },
|
|
1620
|
+
{ keys: [:format], type: :validate_only },
|
|
1621
|
+
{ keys: [:add_gzip], type: :validate_only }
|
|
1622
|
+
].freeze
|
|
1623
|
+
|
|
1061
1624
|
def archive(sha, file = nil, opts = {})
|
|
1062
|
-
opts
|
|
1625
|
+
ArgsBuilder.validate!(opts, ARCHIVE_OPTION_MAP)
|
|
1626
|
+
file ||= temp_file_name
|
|
1627
|
+
format, gzip = parse_archive_format_options(opts)
|
|
1063
1628
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1629
|
+
args = build_args(opts, ARCHIVE_OPTION_MAP)
|
|
1630
|
+
args.unshift("--format=#{format}")
|
|
1631
|
+
args << sha
|
|
1632
|
+
args.push('--', opts[:path]) if opts[:path]
|
|
1068
1633
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
file = tempfile.path
|
|
1072
|
-
# delete it now, before we write to it, so that Ruby doesn't delete it
|
|
1073
|
-
# when it finalizes the Tempfile.
|
|
1074
|
-
tempfile.close!
|
|
1075
|
-
end
|
|
1634
|
+
File.open(file, 'wb') { |f| command('archive', *args, out: f) }
|
|
1635
|
+
apply_gzip(file) if gzip
|
|
1076
1636
|
|
|
1077
|
-
|
|
1078
|
-
arr_opts << "--format=#{opts[:format]}" if opts[:format]
|
|
1079
|
-
arr_opts << "--prefix=#{opts[:prefix]}" if opts[:prefix]
|
|
1080
|
-
arr_opts << "--remote=#{opts[:remote]}" if opts[:remote]
|
|
1081
|
-
arr_opts << sha
|
|
1082
|
-
arr_opts << '--' << opts[:path] if opts[:path]
|
|
1083
|
-
command('archive', *arr_opts, redirect: " > #{escape file}")
|
|
1084
|
-
if opts[:add_gzip]
|
|
1085
|
-
file_content = File.read(file)
|
|
1086
|
-
Zlib::GzipWriter.open(file) do |gz|
|
|
1087
|
-
gz.write(file_content)
|
|
1088
|
-
end
|
|
1089
|
-
end
|
|
1090
|
-
return file
|
|
1637
|
+
file
|
|
1091
1638
|
end
|
|
1092
1639
|
|
|
1093
1640
|
# returns the current version of git, as an Array of Fixnums.
|
|
1094
1641
|
def current_command_version
|
|
1095
1642
|
output = command('version')
|
|
1096
1643
|
version = output[/\d+(\.\d+)+/]
|
|
1097
|
-
version_parts = version.split('.').collect
|
|
1644
|
+
version_parts = version.split('.').collect(&:to_i)
|
|
1098
1645
|
version_parts.fill(0, version_parts.length...3)
|
|
1099
1646
|
end
|
|
1100
1647
|
|
|
@@ -1115,117 +1662,616 @@ module Git
|
|
|
1115
1662
|
end
|
|
1116
1663
|
|
|
1117
1664
|
def required_command_version
|
|
1118
|
-
[
|
|
1665
|
+
[2, 28]
|
|
1119
1666
|
end
|
|
1120
1667
|
|
|
1121
1668
|
def meets_required_version?
|
|
1122
|
-
(
|
|
1669
|
+
(current_command_version <=> required_command_version) >= 0
|
|
1123
1670
|
end
|
|
1124
1671
|
|
|
1125
|
-
def self.warn_if_old_command(lib)
|
|
1672
|
+
def self.warn_if_old_command(lib) # rubocop:disable Naming/PredicateMethod
|
|
1673
|
+
Git::Deprecation.warn('Git::Lib#warn_if_old_command is deprecated. Use meets_required_version?.')
|
|
1674
|
+
|
|
1126
1675
|
return true if @version_checked
|
|
1676
|
+
|
|
1127
1677
|
@version_checked = true
|
|
1128
1678
|
unless lib.meets_required_version?
|
|
1129
|
-
|
|
1679
|
+
warn "[WARNING] The git gem requires git #{lib.required_command_version.join('.')} or later, " \
|
|
1680
|
+
"but only found #{lib.current_command_version.join('.')}. You should probably upgrade."
|
|
1130
1681
|
end
|
|
1131
1682
|
true
|
|
1132
1683
|
end
|
|
1133
1684
|
|
|
1685
|
+
COMMAND_ARG_DEFAULTS = {
|
|
1686
|
+
out: nil,
|
|
1687
|
+
err: nil,
|
|
1688
|
+
normalize: true,
|
|
1689
|
+
chomp: true,
|
|
1690
|
+
merge: false,
|
|
1691
|
+
chdir: nil,
|
|
1692
|
+
timeout: nil # Don't set to Git.config.timeout here since it is mutable
|
|
1693
|
+
}.freeze
|
|
1694
|
+
|
|
1695
|
+
STATIC_GLOBAL_OPTS = %w[
|
|
1696
|
+
-c core.quotePath=true
|
|
1697
|
+
-c color.ui=false
|
|
1698
|
+
-c color.advice=false
|
|
1699
|
+
-c color.diff=false
|
|
1700
|
+
-c color.grep=false
|
|
1701
|
+
-c color.push=false
|
|
1702
|
+
-c color.remote=false
|
|
1703
|
+
-c color.showBranch=false
|
|
1704
|
+
-c color.status=false
|
|
1705
|
+
-c color.transport=false
|
|
1706
|
+
].freeze
|
|
1707
|
+
|
|
1708
|
+
LOG_OPTION_MAP = [
|
|
1709
|
+
{ type: :static, flag: '--no-color' },
|
|
1710
|
+
{ keys: [:all], flag: '--all', type: :boolean },
|
|
1711
|
+
{ keys: [:cherry], flag: '--cherry', type: :boolean },
|
|
1712
|
+
{ keys: [:since], flag: '--since', type: :valued_equals },
|
|
1713
|
+
{ keys: [:until], flag: '--until', type: :valued_equals },
|
|
1714
|
+
{ keys: [:grep], flag: '--grep', type: :valued_equals },
|
|
1715
|
+
{ keys: [:author], flag: '--author', type: :valued_equals },
|
|
1716
|
+
{ keys: [:count], flag: '--max-count', type: :valued_equals },
|
|
1717
|
+
{ keys: [:between], type: :custom, builder: ->(value) { "#{value[0]}..#{value[1]}" if value } }
|
|
1718
|
+
].freeze
|
|
1719
|
+
|
|
1720
|
+
FSCK_OBJECT_PATTERN = /\A(dangling|missing|unreachable) (\w+) ([0-9a-f]{40})(?: \((.+)\))?\z/
|
|
1721
|
+
FSCK_WARNING_PATTERN = /\Awarning in (\w+) ([0-9a-f]{40}): (.+)\z/
|
|
1722
|
+
FSCK_ROOT_PATTERN = /\Aroot ([0-9a-f]{40})\z/
|
|
1723
|
+
FSCK_TAGGED_PATTERN = /\Atagged (\w+) ([0-9a-f]{40}) \((.+)\) in ([0-9a-f]{40})\z/
|
|
1724
|
+
|
|
1725
|
+
private_constant :FSCK_OBJECT_PATTERN, :FSCK_WARNING_PATTERN, :FSCK_ROOT_PATTERN, :FSCK_TAGGED_PATTERN
|
|
1726
|
+
|
|
1134
1727
|
private
|
|
1135
1728
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1729
|
+
def parse_fsck_output(output)
|
|
1730
|
+
result = { dangling: [], missing: [], unreachable: [], warnings: [], root: [], tagged: [] }
|
|
1731
|
+
output.each_line { |line| parse_fsck_line(line.strip, result) }
|
|
1732
|
+
Git::FsckResult.new(**result)
|
|
1733
|
+
end
|
|
1140
1734
|
|
|
1141
|
-
def
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1735
|
+
def parse_fsck_line(line, result)
|
|
1736
|
+
parse_fsck_object_line(line, result) ||
|
|
1737
|
+
parse_fsck_warning_line(line, result) ||
|
|
1738
|
+
parse_fsck_root_line(line, result) ||
|
|
1739
|
+
parse_fsck_tagged_line(line, result)
|
|
1740
|
+
end
|
|
1741
|
+
|
|
1742
|
+
def parse_fsck_object_line(line, result)
|
|
1743
|
+
return unless (match = FSCK_OBJECT_PATTERN.match(line))
|
|
1744
|
+
|
|
1745
|
+
result[match[1].to_sym] << Git::FsckObject.new(type: match[2].to_sym, sha: match[3], name: match[4])
|
|
1746
|
+
end
|
|
1747
|
+
|
|
1748
|
+
def parse_fsck_warning_line(line, result)
|
|
1749
|
+
return unless (match = FSCK_WARNING_PATTERN.match(line))
|
|
1750
|
+
|
|
1751
|
+
result[:warnings] << Git::FsckObject.new(type: match[1].to_sym, sha: match[2], message: match[3])
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
def parse_fsck_root_line(line, result)
|
|
1755
|
+
return unless (match = FSCK_ROOT_PATTERN.match(line))
|
|
1756
|
+
|
|
1757
|
+
result[:root] << Git::FsckObject.new(type: :commit, sha: match[1])
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
def parse_fsck_tagged_line(line, result)
|
|
1761
|
+
return unless (match = FSCK_TAGGED_PATTERN.match(line))
|
|
1762
|
+
|
|
1763
|
+
result[:tagged] << Git::FsckObject.new(type: match[1].to_sym, sha: match[2], name: match[3])
|
|
1149
1764
|
end
|
|
1150
1765
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
@git_system_env_variables[env_variable_name] = ENV[env_variable_name]
|
|
1766
|
+
def parse_diff_path_status(args)
|
|
1767
|
+
command_lines('diff', *args).each_with_object({}) do |line, memo|
|
|
1768
|
+
status, path = split_status_line(line)
|
|
1769
|
+
memo[path] = status
|
|
1156
1770
|
end
|
|
1157
1771
|
end
|
|
1158
1772
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1773
|
+
def build_checkout_positional_args(branch, opts)
|
|
1774
|
+
args = []
|
|
1775
|
+
if opts[:new_branch] || opts[:b]
|
|
1776
|
+
args.push('-b', branch)
|
|
1777
|
+
args << opts[:start_point] if opts[:start_point]
|
|
1778
|
+
elsif branch
|
|
1779
|
+
args << branch
|
|
1163
1780
|
end
|
|
1781
|
+
args
|
|
1164
1782
|
end
|
|
1165
1783
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
ENV['GIT_DIR'] = @git_dir
|
|
1169
|
-
ENV['GIT_WORK_TREE'] = @git_work_dir
|
|
1170
|
-
ENV['GIT_INDEX_FILE'] = @git_index_file
|
|
1171
|
-
ENV['GIT_SSH'] = Git::Base.config.git_ssh
|
|
1784
|
+
def build_args(opts, option_map)
|
|
1785
|
+
Git::ArgsBuilder.new(opts, option_map).build
|
|
1172
1786
|
end
|
|
1173
1787
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1788
|
+
def initialize_from_base(base_object)
|
|
1789
|
+
@git_dir = base_object.repo.to_s
|
|
1790
|
+
@git_index_file = base_object.index&.to_s
|
|
1791
|
+
@git_work_dir = base_object.dir&.to_s
|
|
1792
|
+
@git_ssh = base_object.git_ssh
|
|
1793
|
+
end
|
|
1794
|
+
|
|
1795
|
+
def initialize_from_hash(base_hash)
|
|
1796
|
+
@git_dir = base_hash[:repository]
|
|
1797
|
+
@git_index_file = base_hash[:index]
|
|
1798
|
+
@git_work_dir = base_hash[:working_directory]
|
|
1799
|
+
@git_ssh = base_hash.key?(:git_ssh) ? base_hash[:git_ssh] : :use_global_config
|
|
1800
|
+
end
|
|
1801
|
+
|
|
1802
|
+
def return_base_opts_from_clone(clone_dir, opts)
|
|
1803
|
+
base_opts = {}
|
|
1804
|
+
base_opts[:repository] = clone_dir if opts[:bare] || opts[:mirror]
|
|
1805
|
+
base_opts[:working_directory] = clone_dir unless opts[:bare] || opts[:mirror]
|
|
1806
|
+
base_opts[:log] = opts[:log] if opts[:log]
|
|
1807
|
+
base_opts[:git_ssh] = opts[:git_ssh] if opts.key?(:git_ssh)
|
|
1808
|
+
base_opts
|
|
1809
|
+
end
|
|
1810
|
+
|
|
1811
|
+
def process_commit_headers(data)
|
|
1812
|
+
headers = { 'parent' => [] } # Pre-initialize for multiple parents
|
|
1813
|
+
each_cat_file_header(data) do |key, value|
|
|
1814
|
+
if key == 'parent'
|
|
1815
|
+
headers['parent'] << value
|
|
1816
|
+
else
|
|
1817
|
+
headers[key] = value
|
|
1818
|
+
end
|
|
1183
1819
|
end
|
|
1184
|
-
|
|
1185
|
-
restore_git_system_env_variables()
|
|
1820
|
+
headers
|
|
1186
1821
|
end
|
|
1187
1822
|
|
|
1188
|
-
def
|
|
1189
|
-
|
|
1823
|
+
def parse_branch_line(line, index, all_lines)
|
|
1824
|
+
match_data = match_branch_line(line, index, all_lines)
|
|
1190
1825
|
|
|
1191
|
-
|
|
1826
|
+
return nil if match_data[:not_a_branch] || match_data[:detached_ref]
|
|
1192
1827
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
global_opts << "--work-tree=#{@git_work_dir}" if !@git_work_dir.nil?
|
|
1196
|
-
global_opts << '-c' << 'core.quotePath=true'
|
|
1197
|
-
global_opts << '-c' << 'color.ui=false'
|
|
1828
|
+
format_branch_data(match_data)
|
|
1829
|
+
end
|
|
1198
1830
|
|
|
1199
|
-
|
|
1831
|
+
def match_branch_line(line, index, all_lines)
|
|
1832
|
+
match_data = line.match(BRANCH_LINE_REGEXP)
|
|
1833
|
+
raise Git::UnexpectedResultError, unexpected_branch_line_error(all_lines, line, index) unless match_data
|
|
1200
1834
|
|
|
1201
|
-
|
|
1835
|
+
match_data
|
|
1836
|
+
end
|
|
1202
1837
|
|
|
1203
|
-
|
|
1838
|
+
def format_branch_data(match_data)
|
|
1839
|
+
[
|
|
1840
|
+
match_data[:refname],
|
|
1841
|
+
!match_data[:current].nil?,
|
|
1842
|
+
!match_data[:worktree].nil?,
|
|
1843
|
+
match_data[:symref]
|
|
1844
|
+
]
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
def unexpected_branch_line_error(lines, line, index)
|
|
1848
|
+
<<~ERROR
|
|
1849
|
+
Unexpected line in output from `git branch -a`, line #{index + 1}
|
|
1204
1850
|
|
|
1205
|
-
|
|
1851
|
+
Full output:
|
|
1852
|
+
#{lines.join("\n ")}
|
|
1206
1853
|
|
|
1207
|
-
|
|
1854
|
+
Line #{index + 1}:
|
|
1855
|
+
"#{line}"
|
|
1856
|
+
ERROR
|
|
1857
|
+
end
|
|
1208
1858
|
|
|
1209
|
-
|
|
1859
|
+
def get_branch_state(branch_name)
|
|
1860
|
+
command('rev-parse', '--verify', '--quiet', branch_name)
|
|
1861
|
+
:active
|
|
1862
|
+
rescue Git::FailedError => e
|
|
1863
|
+
# An exit status of 1 with empty stderr from `rev-parse --verify`
|
|
1864
|
+
# indicates a ref that exists but does not yet point to a commit.
|
|
1865
|
+
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
|
|
1210
1866
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1867
|
+
:unborn
|
|
1868
|
+
end
|
|
1869
|
+
|
|
1870
|
+
def execute_grep_command(args)
|
|
1871
|
+
command_lines('grep', *args)
|
|
1872
|
+
rescue Git::FailedError => e
|
|
1873
|
+
# `git grep` returns 1 when no lines are selected.
|
|
1874
|
+
raise unless e.result.status.exitstatus == 1 && e.result.stderr.empty?
|
|
1875
|
+
|
|
1876
|
+
[] # Return an empty array for "no matches found"
|
|
1877
|
+
end
|
|
1878
|
+
|
|
1879
|
+
def parse_grep_output(lines)
|
|
1880
|
+
lines.each_with_object(Hash.new { |h, k| h[k] = [] }) do |line, hsh|
|
|
1881
|
+
match = line.match(/\A(.*?):(\d+):(.*)/)
|
|
1882
|
+
next unless match
|
|
1883
|
+
|
|
1884
|
+
_full, filename, line_num, text = match.to_a
|
|
1885
|
+
hsh[filename] << [line_num.to_i, text]
|
|
1886
|
+
end
|
|
1887
|
+
end
|
|
1888
|
+
|
|
1889
|
+
def parse_diff_stats_output(lines)
|
|
1890
|
+
file_stats = parse_stat_lines(lines)
|
|
1891
|
+
build_final_stats_hash(file_stats)
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
def parse_stat_lines(lines)
|
|
1895
|
+
lines.map do |line|
|
|
1896
|
+
insertions_s, deletions_s, filename = split_status_line(line)
|
|
1897
|
+
{
|
|
1898
|
+
filename: filename,
|
|
1899
|
+
insertions: insertions_s.to_i,
|
|
1900
|
+
deletions: deletions_s.to_i
|
|
1901
|
+
}
|
|
1902
|
+
end
|
|
1903
|
+
end
|
|
1904
|
+
|
|
1905
|
+
def split_status_line(line)
|
|
1906
|
+
parts = line.split("\t")
|
|
1907
|
+
parts[-1] = unescape_quoted_path(parts[-1]) if parts.any?
|
|
1908
|
+
parts
|
|
1909
|
+
end
|
|
1910
|
+
|
|
1911
|
+
def build_final_stats_hash(file_stats)
|
|
1912
|
+
{
|
|
1913
|
+
total: build_total_stats(file_stats),
|
|
1914
|
+
files: build_files_hash(file_stats)
|
|
1915
|
+
}
|
|
1916
|
+
end
|
|
1917
|
+
|
|
1918
|
+
def build_total_stats(file_stats)
|
|
1919
|
+
insertions = file_stats.sum { |s| s[:insertions] }
|
|
1920
|
+
deletions = file_stats.sum { |s| s[:deletions] }
|
|
1921
|
+
{
|
|
1922
|
+
insertions: insertions,
|
|
1923
|
+
deletions: deletions,
|
|
1924
|
+
lines: insertions + deletions,
|
|
1925
|
+
files: file_stats.size
|
|
1926
|
+
}
|
|
1927
|
+
end
|
|
1928
|
+
|
|
1929
|
+
def build_files_hash(file_stats)
|
|
1930
|
+
file_stats.to_h { |s| [s[:filename], s.slice(:insertions, :deletions)] }
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1933
|
+
def parse_ls_remote_output(lines)
|
|
1934
|
+
lines.each_with_object(Hash.new { |h, k| h[k] = {} }) do |line, hsh|
|
|
1935
|
+
type, name, value = parse_ls_remote_line(line)
|
|
1936
|
+
if name
|
|
1937
|
+
hsh[type][name] = value
|
|
1938
|
+
else # Handles the HEAD entry, which has no name
|
|
1939
|
+
hsh[type].update(value)
|
|
1214
1940
|
end
|
|
1215
|
-
command_thread.join
|
|
1216
1941
|
end
|
|
1942
|
+
end
|
|
1943
|
+
|
|
1944
|
+
def parse_ls_remote_line(line)
|
|
1945
|
+
sha, info = line.split("\t", 2)
|
|
1946
|
+
ref, type, name = info.split('/', 3)
|
|
1947
|
+
|
|
1948
|
+
type ||= 'head'
|
|
1949
|
+
type = 'branches' if type == 'heads'
|
|
1950
|
+
|
|
1951
|
+
value = { ref: ref, sha: sha }
|
|
1952
|
+
|
|
1953
|
+
[type, name, value]
|
|
1954
|
+
end
|
|
1955
|
+
|
|
1956
|
+
def stash_log_lines
|
|
1957
|
+
path = File.join(@git_dir, 'logs/refs/stash')
|
|
1958
|
+
return [] unless File.exist?(path)
|
|
1959
|
+
|
|
1960
|
+
File.readlines(path, chomp: true)
|
|
1961
|
+
end
|
|
1962
|
+
|
|
1963
|
+
def parse_stash_log_line(line, index)
|
|
1964
|
+
full_message = line.split("\t", 2).last
|
|
1965
|
+
match_data = full_message.match(/^[^:]+:(.*)$/)
|
|
1966
|
+
message = match_data ? match_data[1] : full_message
|
|
1967
|
+
|
|
1968
|
+
[index, message.strip]
|
|
1969
|
+
end
|
|
1970
|
+
|
|
1971
|
+
# Writes the staged content of a conflicted file to an IO stream
|
|
1972
|
+
#
|
|
1973
|
+
# @param path [String] the path to the file in the index
|
|
1974
|
+
#
|
|
1975
|
+
# @param stage [Integer] the stage of the file to show (e.g., 2 for 'ours', 3 for 'theirs')
|
|
1976
|
+
#
|
|
1977
|
+
# @param out_io [IO] the IO object to write the staged content to
|
|
1978
|
+
#
|
|
1979
|
+
# @return [IO] the IO object that was written to
|
|
1980
|
+
#
|
|
1981
|
+
def write_staged_content(path, stage, out_io)
|
|
1982
|
+
command('show', ":#{stage}:#{path}", out: out_io)
|
|
1983
|
+
out_io
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
def validate_tag_options!(opts)
|
|
1987
|
+
is_annotated = opts[:a] || opts[:annotate]
|
|
1988
|
+
has_message = opts[:m] || opts[:message]
|
|
1989
|
+
|
|
1990
|
+
return unless is_annotated && !has_message
|
|
1991
|
+
|
|
1992
|
+
raise ArgumentError, 'Cannot create an annotated tag without a message.'
|
|
1993
|
+
end
|
|
1994
|
+
|
|
1995
|
+
def normalize_push_args(remote, branch, opts)
|
|
1996
|
+
if branch.is_a?(Hash)
|
|
1997
|
+
opts = branch
|
|
1998
|
+
branch = nil
|
|
1999
|
+
elsif remote.is_a?(Hash)
|
|
2000
|
+
opts = remote
|
|
2001
|
+
remote = nil
|
|
2002
|
+
end
|
|
2003
|
+
|
|
2004
|
+
opts ||= {}
|
|
2005
|
+
# Backwards compatibility for `push(remote, branch, true)`
|
|
2006
|
+
opts = { tags: opts } if [true, false].include?(opts)
|
|
2007
|
+
[remote, branch, opts]
|
|
2008
|
+
end
|
|
2009
|
+
|
|
2010
|
+
def build_push_args(remote, branch, opts)
|
|
2011
|
+
# Build the simple flags using the ArgsBuilder
|
|
2012
|
+
args = build_args(opts, PUSH_OPTION_MAP)
|
|
2013
|
+
|
|
2014
|
+
# Manually handle the flag with external dependencies and positional args
|
|
2015
|
+
args << '--all' if opts[:all] && remote
|
|
2016
|
+
args << remote if remote
|
|
2017
|
+
args << branch if branch
|
|
2018
|
+
args
|
|
2019
|
+
end
|
|
2020
|
+
|
|
2021
|
+
def temp_file_name
|
|
2022
|
+
tempfile = Tempfile.new('archive')
|
|
2023
|
+
file = tempfile.path
|
|
2024
|
+
tempfile.close! # Prevents Ruby from deleting the file on garbage collection
|
|
2025
|
+
file
|
|
2026
|
+
end
|
|
2027
|
+
|
|
2028
|
+
def parse_archive_format_options(opts)
|
|
2029
|
+
format = opts[:format] || 'zip'
|
|
2030
|
+
gzip = opts[:add_gzip] == true || format == 'tgz'
|
|
2031
|
+
format = 'tar' if format == 'tgz'
|
|
2032
|
+
[format, gzip]
|
|
2033
|
+
end
|
|
2034
|
+
|
|
2035
|
+
def apply_gzip(file)
|
|
2036
|
+
file_content = File.read(file)
|
|
2037
|
+
Zlib::GzipWriter.open(file) { |gz| gz.write(file_content) }
|
|
2038
|
+
end
|
|
2039
|
+
|
|
2040
|
+
def command_lines(cmd, *opts, chdir: nil)
|
|
2041
|
+
cmd_op = command(cmd, *opts, chdir: chdir)
|
|
2042
|
+
op = if cmd_op.encoding.name == 'UTF-8'
|
|
2043
|
+
cmd_op
|
|
2044
|
+
else
|
|
2045
|
+
cmd_op.encode('UTF-8', 'binary', invalid: :replace, undef: :replace)
|
|
2046
|
+
end
|
|
2047
|
+
op.split("\n")
|
|
2048
|
+
end
|
|
2049
|
+
|
|
2050
|
+
# Returns a hash of environment variable overrides for git commands
|
|
2051
|
+
#
|
|
2052
|
+
# This method builds a hash of environment variables that control git's behavior,
|
|
2053
|
+
# such as the git directory, working tree, and index file locations.
|
|
2054
|
+
#
|
|
2055
|
+
# @param additional_overrides [Hash] additional environment variables to set or unset
|
|
2056
|
+
#
|
|
2057
|
+
# Keys should be environment variable names (String) and values should be either:
|
|
2058
|
+
# * A String value to set the environment variable
|
|
2059
|
+
# * `nil` to unset the environment variable
|
|
2060
|
+
#
|
|
2061
|
+
# Per Process.spawn semantics, setting a key to `nil` will unset that environment
|
|
2062
|
+
# variable, removing it from the environment passed to the git command.
|
|
2063
|
+
#
|
|
2064
|
+
# @return [Hash<String, String|nil>] environment variable overrides
|
|
2065
|
+
#
|
|
2066
|
+
# @example Basic usage with default environment variables
|
|
2067
|
+
# env_overrides
|
|
2068
|
+
# # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree', ... }
|
|
2069
|
+
#
|
|
2070
|
+
# @example Adding a custom environment variable
|
|
2071
|
+
# env_overrides('GIT_TRACE' => '1')
|
|
2072
|
+
# # => { 'GIT_DIR' => '/path/to/.git', ..., 'GIT_TRACE' => '1' }
|
|
2073
|
+
#
|
|
2074
|
+
# @example Unsetting an environment variable (used by worktree_command_line)
|
|
2075
|
+
# env_overrides('GIT_INDEX_FILE' => nil)
|
|
2076
|
+
# # => { 'GIT_DIR' => '/path/to/.git', 'GIT_WORK_TREE' => '/path/to/worktree',
|
|
2077
|
+
# # 'GIT_INDEX_FILE' => nil, 'GIT_SSH' => <git_ssh_value>, 'LC_ALL' => <pinned_locale> }
|
|
2078
|
+
# # When passed to Process.spawn, GIT_INDEX_FILE will be unset in the environment
|
|
2079
|
+
#
|
|
2080
|
+
# @see https://ruby-doc.org/core/Process.html#method-c-spawn Process.spawn
|
|
2081
|
+
#
|
|
2082
|
+
# @api private
|
|
2083
|
+
#
|
|
2084
|
+
def env_overrides(**additional_overrides)
|
|
2085
|
+
{
|
|
2086
|
+
'GIT_DIR' => @git_dir,
|
|
2087
|
+
'GIT_WORK_TREE' => @git_work_dir,
|
|
2088
|
+
'GIT_INDEX_FILE' => @git_index_file,
|
|
2089
|
+
'GIT_SSH' => resolved_git_ssh,
|
|
2090
|
+
# Pin the locale so git's behavior does not depend on the user's environment.
|
|
2091
|
+
# Added for issue #753, where a German user's `git branch` output
|
|
2092
|
+
# ("* (HEAD losgelöst bei origin/25.1)") broke branch parsing.
|
|
2093
|
+
#
|
|
2094
|
+
# The pin has two halves, and both are load-bearing here:
|
|
2095
|
+
#
|
|
2096
|
+
# - Messages: keeps git's human-readable text in English. `BRANCH_LINE_REGEXP`
|
|
2097
|
+
# matches the literal English strings "(HEAD detached at ...)" and
|
|
2098
|
+
# "(not a branch)" in `git branch -a` output, which is the exact issue #753
|
|
2099
|
+
# failure mode.
|
|
2100
|
+
# - Ctype: makes git's regex engine match *characters* rather than *bytes*.
|
|
2101
|
+
# Under a C ctype, `grep` and `log --grep` silently return wrong answers —
|
|
2102
|
+
# with exit status zero — for any pattern whose metacharacters span
|
|
2103
|
+
# non-ASCII text.
|
|
2104
|
+
#
|
|
2105
|
+
# So do not "simplify" this to `C`: that yields English messages and a broken
|
|
2106
|
+
# ctype, which is the worst of both. A pinned locale the host does not have
|
|
2107
|
+
# degrades to that same C ctype, which is why each branch below has to name a
|
|
2108
|
+
# locale that actually exists on the platform it applies to:
|
|
2109
|
+
#
|
|
2110
|
+
# - Non-Darwin: `C.UTF-8`. Stock Debian, Ubuntu, and RHEL images generate no
|
|
2111
|
+
# `en_US.UTF-8`, so pinning it there produced exactly the silent breakage
|
|
2112
|
+
# above. (musl ignores the locale name for ctype, so Alpine is UTF-8 either
|
|
2113
|
+
# way.)
|
|
2114
|
+
# - Darwin: `en_US.UTF-8`, which every macOS release ships. `C.UTF-8` did not
|
|
2115
|
+
# arrive until macOS 15, so macOS 11–14 — including Intel Macs that are
|
|
2116
|
+
# hardware-capped below 15 — would break under it. Delete this branch once
|
|
2117
|
+
# macOS 14 and earlier are out of support.
|
|
2118
|
+
#
|
|
2119
|
+
# Windows takes the non-Darwin branch, where the value makes no difference:
|
|
2120
|
+
# Git for Windows folds case and runs PCRE in UTF mode under every value, and
|
|
2121
|
+
# matches bytes for `.` and POSIX classes under every value — measured on git
|
|
2122
|
+
# 2.55.0 against `en_US.UTF-8`, `C.UTF-8`, `C`, and no pin at all.
|
|
2123
|
+
#
|
|
2124
|
+
# RHEL 7 (glibc 2.17) has neither locale and gets a C ctype whatever is pinned.
|
|
2125
|
+
# It is EOL, and there is deliberately no public override for this value.
|
|
2126
|
+
'LC_ALL' => darwin_platform? ? 'en_US.UTF-8' : 'C.UTF-8'
|
|
2127
|
+
}.merge(additional_overrides)
|
|
2128
|
+
end
|
|
2129
|
+
|
|
2130
|
+
# Whether this process is running on macOS
|
|
2131
|
+
#
|
|
2132
|
+
# Checks `RUBY_DESCRIPTION` as well as `RUBY_PLATFORM` because JRuby reports
|
|
2133
|
+
# `RUBY_PLATFORM` as `"java"` on every host and records the real platform only in
|
|
2134
|
+
# `RUBY_DESCRIPTION` (as, for example, `"... [arm64-darwin]"`). Testing
|
|
2135
|
+
# `RUBY_PLATFORM` alone would put JRuby on macOS onto the non-Darwin branch, which is
|
|
2136
|
+
# the one platform pairing that branch must not be given. This is the same detection
|
|
2137
|
+
# the test suite uses for Windows, and for the same reason.
|
|
2138
|
+
#
|
|
2139
|
+
# Test the platform plainly rather than sniffing the Darwin version: `RUBY_PLATFORM`
|
|
2140
|
+
# records the version Ruby was *built* against, so a Ruby built on macOS 14 still
|
|
2141
|
+
# reports `darwin23` when run on macOS 15.
|
|
2142
|
+
#
|
|
2143
|
+
# @return [Boolean] true if this process is running on macOS
|
|
2144
|
+
#
|
|
2145
|
+
# @api private
|
|
2146
|
+
#
|
|
2147
|
+
def darwin_platform?
|
|
2148
|
+
RUBY_PLATFORM.include?('darwin') || RUBY_DESCRIPTION.include?('darwin')
|
|
2149
|
+
end
|
|
1217
2150
|
|
|
1218
|
-
|
|
1219
|
-
|
|
2151
|
+
# Resolve the git_ssh value to use for this instance
|
|
2152
|
+
#
|
|
2153
|
+
# @return [String, nil] the resolved git_ssh value
|
|
2154
|
+
#
|
|
2155
|
+
# Returns the global config value if @git_ssh is the sentinel :use_global_config,
|
|
2156
|
+
# otherwise returns @git_ssh (which may be nil or a string)
|
|
2157
|
+
#
|
|
2158
|
+
# @api private
|
|
2159
|
+
#
|
|
2160
|
+
def resolved_git_ssh
|
|
2161
|
+
return Git::Base.config.git_ssh if @git_ssh == :use_global_config
|
|
1220
2162
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
2163
|
+
@git_ssh
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
def global_opts
|
|
2167
|
+
[].tap do |global_opts|
|
|
2168
|
+
global_opts << "--git-dir=#{@git_dir}" unless @git_dir.nil?
|
|
2169
|
+
global_opts << "--work-tree=#{@git_work_dir}" unless @git_work_dir.nil?
|
|
2170
|
+
global_opts.concat(STATIC_GLOBAL_OPTS)
|
|
1224
2171
|
end
|
|
2172
|
+
end
|
|
2173
|
+
|
|
2174
|
+
def command_line
|
|
2175
|
+
@command_line ||=
|
|
2176
|
+
Git::CommandLine.new(env_overrides, Git::Base.config.binary_path, global_opts, @logger)
|
|
2177
|
+
end
|
|
2178
|
+
|
|
2179
|
+
# Returns a command line instance without GIT_INDEX_FILE for worktree commands
|
|
2180
|
+
#
|
|
2181
|
+
# Git worktrees manage their own index files and setting GIT_INDEX_FILE
|
|
2182
|
+
# causes corruption of both the main worktree and new worktree indexes.
|
|
2183
|
+
#
|
|
2184
|
+
# @return [Git::CommandLine]
|
|
2185
|
+
# @api private
|
|
2186
|
+
#
|
|
2187
|
+
def worktree_command_line
|
|
2188
|
+
@worktree_command_line ||=
|
|
2189
|
+
Git::CommandLine.new(env_overrides('GIT_INDEX_FILE' => nil), Git::Base.config.binary_path, global_opts,
|
|
2190
|
+
@logger)
|
|
2191
|
+
end
|
|
1225
2192
|
|
|
1226
|
-
|
|
2193
|
+
# @overload worktree_command(*args, **options_hash)
|
|
2194
|
+
# Runs a git worktree command and returns the output
|
|
2195
|
+
#
|
|
2196
|
+
# This method is similar to #command but uses a command line instance
|
|
2197
|
+
# that excludes GIT_INDEX_FILE from the environment to prevent index corruption.
|
|
2198
|
+
#
|
|
2199
|
+
# @param args [Array<String>] the command arguments
|
|
2200
|
+
# @param options_hash [Hash] the options to pass to the command
|
|
2201
|
+
#
|
|
2202
|
+
# @return [String] the command's stdout
|
|
2203
|
+
#
|
|
2204
|
+
# @see #command
|
|
2205
|
+
#
|
|
2206
|
+
# @api private
|
|
2207
|
+
#
|
|
2208
|
+
def worktree_command(*, **options_hash)
|
|
2209
|
+
options_hash = COMMAND_ARG_DEFAULTS.merge(options_hash)
|
|
2210
|
+
options_hash[:timeout] ||= Git.config.timeout
|
|
1227
2211
|
|
|
1228
|
-
|
|
2212
|
+
extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
|
|
2213
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2214
|
+
|
|
2215
|
+
result = worktree_command_line.run(*, **options_hash)
|
|
2216
|
+
result.stdout
|
|
2217
|
+
end
|
|
2218
|
+
|
|
2219
|
+
# Runs a git command and returns the output
|
|
2220
|
+
#
|
|
2221
|
+
# Additional args are passed to the command line. They should exclude the 'git'
|
|
2222
|
+
# command itself and global options. Remember to splat the the arguments if given
|
|
2223
|
+
# as an array.
|
|
2224
|
+
#
|
|
2225
|
+
# For example, to run `git log --pretty=oneline`, you would create the array
|
|
2226
|
+
# `args = ['log', '--pretty=oneline']` and call `command(*args)`.
|
|
2227
|
+
#
|
|
2228
|
+
# @param options_hash [Hash] the options to pass to the command
|
|
2229
|
+
# @option options_hash [IO, String, #write, nil] :out the destination for captured stdout
|
|
2230
|
+
# @option options_hash [IO, String, #write, nil] :err the destination for captured stderr
|
|
2231
|
+
# @option options_hash [Boolean] :normalize true to normalize the output encoding to UTF-8
|
|
2232
|
+
# @option options_hash [Boolean] :chomp true to remove trailing newlines from the output
|
|
2233
|
+
# @option options_hash [Boolean] :merge true to merge stdout and stderr into a single output
|
|
2234
|
+
# @option options_hash [String, nil] :chdir the directory to run the command in
|
|
2235
|
+
# @option options_hash [Numeric, nil] :timeout the maximum seconds to wait for the command to complete
|
|
2236
|
+
#
|
|
2237
|
+
# If timeout is nil, the global timeout from {Git::Config} is used.
|
|
2238
|
+
#
|
|
2239
|
+
# If timeout is zero, the timeout will not be enforced.
|
|
2240
|
+
#
|
|
2241
|
+
# If the command times out, it is killed via a `SIGKILL` signal and `Git::TimeoutError` is raised.
|
|
2242
|
+
#
|
|
2243
|
+
# If the command does not respond to SIGKILL, it will hang this method.
|
|
2244
|
+
#
|
|
2245
|
+
# @see Git::CommandLine#run
|
|
2246
|
+
#
|
|
2247
|
+
# @return [String] the command's stdout (or merged stdout and stderr if `merge`
|
|
2248
|
+
# is true)
|
|
2249
|
+
#
|
|
2250
|
+
# @raise [ArgumentError] if an unknown option is passed
|
|
2251
|
+
#
|
|
2252
|
+
# @raise [Git::FailedError] if the command failed
|
|
2253
|
+
#
|
|
2254
|
+
# @raise [Git::SignaledError] if the command was signaled
|
|
2255
|
+
#
|
|
2256
|
+
# @raise [Git::TimeoutError] if the command times out
|
|
2257
|
+
#
|
|
2258
|
+
# @raise [Git::ProcessIOError] if an exception was raised while collecting subprocess output
|
|
2259
|
+
#
|
|
2260
|
+
# The exception's `result` attribute is a {Git::CommandLineResult} which will
|
|
2261
|
+
# contain the result of the command including the exit status, stdout, and
|
|
2262
|
+
# stderr.
|
|
2263
|
+
#
|
|
2264
|
+
# @api private
|
|
2265
|
+
#
|
|
2266
|
+
def command(*, **options_hash)
|
|
2267
|
+
options_hash = COMMAND_ARG_DEFAULTS.merge(options_hash)
|
|
2268
|
+
options_hash[:timeout] ||= Git.config.timeout
|
|
2269
|
+
|
|
2270
|
+
extra_options = options_hash.keys - COMMAND_ARG_DEFAULTS.keys
|
|
2271
|
+
raise ArgumentError, "Unknown options: #{extra_options.join(', ')}" if extra_options.any?
|
|
2272
|
+
|
|
2273
|
+
result = command_line.run(*, **options_hash)
|
|
2274
|
+
result.stdout
|
|
1229
2275
|
end
|
|
1230
2276
|
|
|
1231
2277
|
# Takes the diff command line output (as Array) and parse it into a Hash
|
|
@@ -1233,23 +2279,18 @@ module Git
|
|
|
1233
2279
|
# @param [String] diff_command the diff commadn to be used
|
|
1234
2280
|
# @param [Array] opts the diff options to be used
|
|
1235
2281
|
# @return [Hash] the diff as Hash
|
|
1236
|
-
def diff_as_hash(diff_command, opts=[])
|
|
2282
|
+
def diff_as_hash(diff_command, opts = [])
|
|
1237
2283
|
# update index before diffing to avoid spurious diffs
|
|
1238
2284
|
command('status')
|
|
1239
|
-
command_lines(diff_command, *opts).
|
|
1240
|
-
info, file = line
|
|
2285
|
+
command_lines(diff_command, *opts).each_with_object({}) do |line, memo|
|
|
2286
|
+
info, file = split_status_line(line)
|
|
1241
2287
|
mode_src, mode_dest, sha_src, sha_dest, type = info.split
|
|
1242
2288
|
|
|
1243
2289
|
memo[file] = {
|
|
1244
|
-
:
|
|
1245
|
-
:
|
|
1246
|
-
:
|
|
1247
|
-
:sha_repo => sha_src,
|
|
1248
|
-
:sha_index => sha_dest,
|
|
1249
|
-
:type => type
|
|
2290
|
+
mode_index: mode_dest, mode_repo: mode_src.to_s[1, 7],
|
|
2291
|
+
path: file, sha_repo: sha_src, sha_index: sha_dest,
|
|
2292
|
+
type: type
|
|
1250
2293
|
}
|
|
1251
|
-
|
|
1252
|
-
memo
|
|
1253
2294
|
end
|
|
1254
2295
|
end
|
|
1255
2296
|
|
|
@@ -1258,23 +2299,11 @@ module Git
|
|
|
1258
2299
|
# @param [Hash] opts the given options
|
|
1259
2300
|
# @return [Array] the set of common options that the log command will use
|
|
1260
2301
|
def log_common_options(opts)
|
|
1261
|
-
arr_opts = []
|
|
1262
|
-
|
|
1263
2302
|
if opts[:count] && !opts[:count].is_a?(Integer)
|
|
1264
2303
|
raise ArgumentError, "The log count option must be an Integer but was #{opts[:count].inspect}"
|
|
1265
2304
|
end
|
|
1266
2305
|
|
|
1267
|
-
|
|
1268
|
-
arr_opts << "--all" if opts[:all]
|
|
1269
|
-
arr_opts << "--no-color"
|
|
1270
|
-
arr_opts << "--cherry" if opts[:cherry]
|
|
1271
|
-
arr_opts << "--since=#{opts[:since]}" if opts[:since].is_a? String
|
|
1272
|
-
arr_opts << "--until=#{opts[:until]}" if opts[:until].is_a? String
|
|
1273
|
-
arr_opts << "--grep=#{opts[:grep]}" if opts[:grep].is_a? String
|
|
1274
|
-
arr_opts << "--author=#{opts[:author]}" if opts[:author].is_a? String
|
|
1275
|
-
arr_opts << "#{opts[:between][0].to_s}..#{opts[:between][1].to_s}" if (opts[:between] && opts[:between].size == 2)
|
|
1276
|
-
|
|
1277
|
-
arr_opts
|
|
2306
|
+
build_args(opts, LOG_OPTION_MAP)
|
|
1278
2307
|
end
|
|
1279
2308
|
|
|
1280
2309
|
# Retrurns an array holding path options for the log commands
|
|
@@ -1292,37 +2321,13 @@ module Git
|
|
|
1292
2321
|
arr_opts
|
|
1293
2322
|
end
|
|
1294
2323
|
|
|
1295
|
-
def
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
opts = {}
|
|
1301
|
-
opts[:chdir] = File.expand_path(chdir) if chdir
|
|
1302
|
-
|
|
1303
|
-
Open3.popen2(git_cmd, opts) do |stdin, stdout, wait_thr|
|
|
1304
|
-
[block.call(stdout), wait_thr.value]
|
|
1305
|
-
end
|
|
1306
|
-
end
|
|
1307
|
-
|
|
1308
|
-
def escape(s)
|
|
1309
|
-
windows_platform? ? escape_for_windows(s) : escape_for_sh(s)
|
|
1310
|
-
end
|
|
1311
|
-
|
|
1312
|
-
def escape_for_sh(s)
|
|
1313
|
-
"'#{s && s.to_s.gsub('\'','\'"\'"\'')}'"
|
|
1314
|
-
end
|
|
1315
|
-
|
|
1316
|
-
def escape_for_windows(s)
|
|
1317
|
-
# Escape existing double quotes in s and then wrap the result with double quotes
|
|
1318
|
-
escaped_string = s.to_s.gsub('"','\\"')
|
|
1319
|
-
%Q{"#{escaped_string}"}
|
|
1320
|
-
end
|
|
2324
|
+
def log_or_empty_on_unborn
|
|
2325
|
+
yield
|
|
2326
|
+
rescue Git::FailedError => e
|
|
2327
|
+
raise unless e.result.status.exitstatus == 128 &&
|
|
2328
|
+
e.result.stderr =~ /does not have any commits yet/
|
|
1321
2329
|
|
|
1322
|
-
|
|
1323
|
-
# Check if on Windows via RUBY_PLATFORM (CRuby) and RUBY_DESCRIPTION (JRuby)
|
|
1324
|
-
win_platform_regex = /mingw|mswin/
|
|
1325
|
-
RUBY_PLATFORM =~ win_platform_regex || RUBY_DESCRIPTION =~ win_platform_regex
|
|
2330
|
+
[]
|
|
1326
2331
|
end
|
|
1327
2332
|
end
|
|
1328
2333
|
end
|