git-fastclone 1.3.2 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d5ae1cf32d0c0f13534a9e7d986cb4d1845cec980085b27332cf366829b0ccf
4
- data.tar.gz: 909d9f98c4d09d9f52b70539749639b3fc57464f6b622ac9620d24509e690476
3
+ metadata.gz: ed5f3f84cbab65351479f22b659b912521699f542c898010527f25b7c3786c84
4
+ data.tar.gz: 5f02716656bf0962d9a808ae368078f0e151e6bdfd3abba5b25503504f64194b
5
5
  SHA512:
6
- metadata.gz: 8f20b952bdfe79605012ae2b365dfb01b3621239f75e2e9e40aef76d807c9a2eb1af5b876dbd34032311efeea0ae1a9990c18295c3256ea8d7a622f99b1e1032
7
- data.tar.gz: e9fa4c99ce664a1c1780b76a5a952cdef57b1d43ddf92ea870bba0c29a76fce2e2b0281b6698af48c5a60c1446427c66614fb226cb159f3870b614e06b2d897b
6
+ metadata.gz: a9219567c52c31d5219027adb6227c88970dd839fa8dd2f50258fc205adc3e4f9372d44d6f734242849865d7885f6b8f5a3fadfef05f77c88e24d8d85016a849
7
+ data.tar.gz: c136b5240dfc87480457c924772944282f518f8c77d49473f41724449ae0566d8cc5476e6f698aa86de2b18f1f77a8995d0f5f3e8778546630e390d6ed2d2530
data/README.md CHANGED
@@ -74,6 +74,8 @@ keep the code as readable as possible.
74
74
  Before accepting any pull requests, we need you to sign an [Individual Contributor Agreement][2]
75
75
  (Google form).
76
76
 
77
+ Once landed, please reach out to any owner listed in https://rubygems.org/gems/git-fastclone and ask them to help publish the new version.
78
+
77
79
 
78
80
  Acknowledgements
79
81
  ----------------
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version string for git-fastclone
4
4
  module GitFastCloneVersion
5
- VERSION = '1.3.2'
5
+ VERSION = '1.4.0'
6
6
  end
data/lib/git-fastclone.rb CHANGED
@@ -16,9 +16,8 @@
16
16
 
17
17
  require 'optparse'
18
18
  require 'fileutils'
19
- require 'logger'
20
- require 'terrapin'
21
19
  require 'timeout'
20
+ require_relative 'runner_execution'
22
21
 
23
22
  # Contains helper module UrlHelper and execution class GitFastClone::Runner
24
23
  module GitFastClone
@@ -68,13 +67,14 @@ module GitFastClone
68
67
  require 'colorize'
69
68
 
70
69
  include GitFastClone::UrlHelper
70
+ include RunnerExecution
71
71
 
72
72
  DEFAULT_REFERENCE_REPO_DIR = '/var/tmp/git-fastclone/reference'
73
73
 
74
74
  DEFAULT_GIT_ALLOW_PROTOCOL = 'file:git:http:https:ssh'
75
75
 
76
76
  attr_accessor :reference_dir, :prefetch_submodules, :reference_updated, :reference_mutex,
77
- :options, :logger, :abs_clone_path, :using_local_repo, :verbose, :color,
77
+ :options, :abs_clone_path, :using_local_repo, :verbose, :color,
78
78
  :flock_timeout_secs
79
79
 
80
80
  def initialize
@@ -94,8 +94,6 @@ module GitFastClone
94
94
 
95
95
  self.options = {}
96
96
 
97
- self.logger = nil # Only set in verbose mode
98
-
99
97
  self.abs_clone_path = Dir.pwd
100
98
 
101
99
  self.using_local_repo = false
@@ -119,8 +117,7 @@ module GitFastClone
119
117
  end
120
118
 
121
119
  puts "Cloning #{path_from_git_url(url)} to #{File.join(abs_clone_path, path)}"
122
- Terrapin::CommandLine.environment['GIT_ALLOW_PROTOCOL'] =
123
- ENV['GIT_ALLOW_PROTOCOL'] || DEFAULT_GIT_ALLOW_PROTOCOL
120
+ ENV['GIT_ALLOW_PROTOCOL'] ||= DEFAULT_GIT_ALLOW_PROTOCOL
124
121
  clone(url, options[:branch], path, options[:config])
125
122
  end
126
123
 
@@ -137,11 +134,6 @@ module GitFastClone
137
134
 
138
135
  opts.on('-v', '--verbose', 'Verbose mode') do
139
136
  self.verbose = true
140
- self.logger = Logger.new($stdout)
141
- logger.formatter = proc do |_severity, _datetime, _progname, msg|
142
- "#{msg}\n"
143
- end
144
- Terrapin::CommandLine.logger = logger
145
137
  end
146
138
 
147
139
  opts.on('-c', '--color', 'Display colored output') do
@@ -154,7 +146,7 @@ module GitFastClone
154
146
 
155
147
  opts.on('--lock-timeout N', 'Timeout in seconds to acquire a lock on any reference repo.
156
148
  Default is 0 which waits indefinitely.') do |timeout_secs|
157
- self.flock_timeout_secs = timeout_secs
149
+ self.flock_timeout_secs = timeout_secs.to_i
158
150
  end
159
151
  end.parse!
160
152
  end
@@ -217,19 +209,16 @@ module GitFastClone
217
209
  with_git_mirror(url) do |mirror, attempt_number|
218
210
  clear_clone_dest_if_needed(attempt_number, clone_dest)
219
211
 
220
- clone_command = '--quiet --reference :mirror :url :path'
221
- clone_command += ' --config :config' unless config.nil?
222
- Terrapin::CommandLine.new('git clone', clone_command)
223
- .run(mirror: mirror.to_s,
224
- url: url.to_s,
225
- path: clone_dest,
226
- config: config.to_s)
212
+ clone_commands = ['git', 'clone', verbose ? '--verbose' : '--quiet']
213
+ clone_commands << '--reference' << mirror.to_s << url.to_s << clone_dest
214
+ clone_commands << '--config' << config.to_s unless config.nil?
215
+ fail_pipe_on_error(clone_commands, quiet: !verbose)
227
216
  end
228
217
 
229
218
  # Only checkout if we're changing branches to a non-default branch
230
219
  if rev
231
220
  Dir.chdir(File.join(abs_clone_path, src_dir)) do
232
- Terrapin::CommandLine.new('git checkout', '--quiet :rev').run(rev: rev.to_s)
221
+ fail_pipe_on_error(['git', 'checkout', '--quiet', rev.to_s], quiet: !verbose)
233
222
  end
234
223
  end
235
224
 
@@ -252,9 +241,12 @@ module GitFastClone
252
241
 
253
242
  threads = []
254
243
  submodule_url_list = []
244
+ output = ''
245
+ Dir.chdir(File.join(abs_clone_path, pwd).to_s) do
246
+ output = fail_on_error('git', 'submodule', 'init', quiet: !verbose)
247
+ end
255
248
 
256
- Terrapin::CommandLine.new('cd', ':path; git submodule init 2>&1')
257
- .run(path: File.join(abs_clone_path, pwd)).split("\n").each do |line|
249
+ output.split("\n").each do |line|
258
250
  submodule_path, submodule_url = parse_update_info(line)
259
251
  submodule_url_list << submodule_url
260
252
 
@@ -268,10 +260,12 @@ module GitFastClone
268
260
  def thread_update_submodule(submodule_url, submodule_path, threads, pwd)
269
261
  threads << Thread.new do
270
262
  with_git_mirror(submodule_url) do |mirror, _|
271
- Terrapin::CommandLine.new('cd', ':dir; git submodule update --quiet --reference :mirror :path')
272
- .run(dir: File.join(abs_clone_path, pwd).to_s,
273
- mirror: mirror.to_s,
274
- path: submodule_path.to_s)
263
+ Dir.chdir(File.join(abs_clone_path, pwd).to_s) do
264
+ fail_pipe_on_error(
265
+ ['git', 'submodule', verbose ? nil : '--quiet', 'update', '--reference', mirror.to_s,
266
+ submodule_path.to_s].compact, quiet: !verbose
267
+ )
268
+ end
275
269
  end
276
270
 
277
271
  update_submodules(File.join(pwd, submodule_path), submodule_url)
@@ -343,43 +337,54 @@ module GitFastClone
343
337
  # that this repo has been updated on this run of fastclone
344
338
  def store_updated_repo(url, mirror, repo_name, fail_hard)
345
339
  unless Dir.exist?(mirror)
346
- Terrapin::CommandLine.new('git clone', '--mirror :url :mirror')
347
- .run(url: url.to_s, mirror: mirror.to_s)
340
+ fail_pipe_on_error(
341
+ ['git', 'clone', verbose ? '--verbose' : '--quiet', '--mirror', url.to_s,
342
+ mirror.to_s], quiet: !verbose
343
+ )
348
344
  end
349
345
 
350
- Terrapin::CommandLine.new('cd', ':path; git remote update --prune').run(path: mirror)
351
-
346
+ Dir.chdir(mirror) do
347
+ cmd = ['git', 'remote', verbose ? '--verbose' : nil, 'update', '--prune'].compact
348
+ if verbose
349
+ fail_pipe_on_error(cmd, quiet: !verbose)
350
+ else
351
+ # Because above operation might spit out a lot to stderr, we use this to swallow them
352
+ # and only display if the operation return non 0 exit code
353
+ fail_on_error(*cmd, quiet: !verbose)
354
+ end
355
+ end
352
356
  reference_updated[repo_name] = true
353
- rescue Terrapin::ExitStatusError => e
357
+ rescue RunnerExecutionRuntimeError => e
354
358
  # To avoid corruption of the cache, if we failed to update or check out we remove
355
359
  # the cache directory entirely. This may cause the current clone to fail, but if the
356
360
  # underlying error from git is transient it will not affect future clones.
357
- FileUtils.remove_entry_secure(mirror, force: true)
361
+ clear_cache(mirror, url)
358
362
  raise e if fail_hard
359
363
  end
360
364
 
361
365
  def retriable_error?(error)
362
366
  error_strings = [
363
- 'fatal: missing blob object',
364
- 'fatal: remote did not send all necessary objects',
365
- /fatal: packed object [a-z0-9]+ \(stored in .*?\) is corrupt/,
366
- /fatal: pack has \d+ unresolved delta/,
367
- 'error: unable to read sha1 file of ',
368
- 'fatal: did not receive expected object',
367
+ /^fatal: missing blob object/,
368
+ /^fatal: remote did not send all necessary objects/,
369
+ /^fatal: packed object [a-z0-9]+ \(stored in .*?\) is corrupt/,
370
+ /^fatal: pack has \d+ unresolved delta/,
371
+ /^error: unable to read sha1 file of /,
372
+ /^fatal: did not receive expected object/,
369
373
  /^fatal: unable to read tree [a-z0-9]+\n^warning: Clone succeeded, but checkout failed/
370
374
  ]
371
- error.to_s =~ /^STDERR:\n.*^#{Regexp.union(error_strings)}/m
375
+ error.to_s =~ /.*#{Regexp.union(error_strings)}/m
372
376
  end
373
377
 
374
378
  def print_formatted_error(error)
375
379
  indented_error = error.to_s.split("\n").map { |s| "> #{s}\n" }.join
376
- puts "Encountered a retriable error:\n#{indented_error}\n\nRemoving the fastclone cache."
380
+ puts "[INFO] Encountered a retriable error:\n#{indented_error}\n"
377
381
  end
378
382
 
379
383
  # To avoid corruption of the cache, if we failed to update or check out we remove
380
384
  # the cache directory entirely. This may cause the current clone to fail, but if the
381
385
  # underlying error from git is transient it will not affect future clones.
382
386
  def clear_cache(dir, url)
387
+ puts "[WARN] Removing the fastclone cache at #{dir}"
383
388
  FileUtils.remove_entry_secure(dir, force: true)
384
389
  reference_updated.delete(reference_repo_name(url))
385
390
  end
@@ -405,9 +410,9 @@ module GitFastClone
405
410
  with_reference_repo_lock(url) do
406
411
  yield dir, attempt_number
407
412
  end
408
- rescue Terrapin::ExitStatusError => e
409
- if retriable_error?(e)
410
- print_formatted_error(e)
413
+ rescue RunnerExecutionRuntimeError => e
414
+ if retriable_error?(e.output)
415
+ print_formatted_error(e.output)
411
416
  clear_cache(dir, url)
412
417
 
413
418
  if attempt_number < retries_allowed
@@ -416,7 +421,7 @@ module GitFastClone
416
421
  end
417
422
  end
418
423
 
419
- raise
424
+ raise e
420
425
  end
421
426
 
422
427
  def usage
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable all
3
+
4
+ require 'open3'
5
+ require 'logger'
6
+
7
+ # Execution primitives that force explicit error handling and never call the shell.
8
+ # Cargo-culted from internal BuildExecution code on top of public version: https://github.com/square/build_execution
9
+ module RunnerExecution
10
+ class RunnerExecutionRuntimeError < RuntimeError
11
+ attr_reader :status, :exitstatus, :command, :output
12
+
13
+ def initialize(status, command, output = nil)
14
+ @status = status
15
+ @exitstatus = status.exitstatus
16
+ @command = command
17
+ @output = output
18
+
19
+ super "#{status.inspect}\n#{command.inspect}"
20
+ end
21
+ end
22
+
23
+ # Wrapper around open3.pipeline_r which fails on error.
24
+ # and stops users from invoking the shell by accident.
25
+ def fail_pipe_on_error(*cmd_list, quiet: false, **opts)
26
+ print_command('Running Pipeline:', cmd_list) unless quiet
27
+
28
+ env = opts.delete(:env) { {} }
29
+ raise ArgumentError, "The :env option must be a hash, not #{env.inspect}" unless env.is_a?(Hash)
30
+
31
+ cmd_list.map! { |cmd| shell_safe(cmd).unshift(env) }
32
+
33
+ output, *status_list = Open3.pipeline_r(*cmd_list, opts) do |out, wait_threads|
34
+ out_reader = Thread.new do
35
+ if quiet
36
+ output = out.read
37
+ else
38
+ # Output from pipeline should go to stdout and also get returned for
39
+ # processing if necessary.
40
+ output = tee(out, STDOUT)
41
+ end
42
+ out.close
43
+ output
44
+ end
45
+ [out_reader.value] + wait_threads.map(&:value)
46
+ end
47
+ exit_on_status(output, cmd_list, status_list, quiet: quiet)
48
+ end
49
+ module_function :fail_pipe_on_error
50
+
51
+ # Runs a command that fails on error.
52
+ # Uses popen2e wrapper. Handles bad statuses with potential for retries.
53
+ def fail_on_error(*cmd, stdin_data: nil, binmode: false, quiet: false, **opts)
54
+ print_command('Running Shell Safe Command:', [cmd]) unless quiet
55
+ shell_safe_cmd = shell_safe(cmd)
56
+ retry_times = opts[:retry] || 0
57
+ opts.delete(:retry)
58
+
59
+ while retry_times >= 0
60
+ output, status = popen2e_wrapper(*shell_safe_cmd, stdin_data: stdin_data, binmode: binmode,
61
+ quiet: quiet, **opts)
62
+
63
+ break unless status.exitstatus != 0
64
+
65
+ logger.debug("Command failed with exit status #{status.exitstatus}, retrying #{retry_times} more time(s).") if retry_times > 0
66
+ retry_times -= 1
67
+ end
68
+
69
+ # Get out with the status, good or bad.
70
+ exit_on_status(output, [shell_safe_cmd], [status], quiet: quiet)
71
+ end
72
+ module_function :fail_on_error
73
+
74
+ # Wrapper around open3.popen2e
75
+ #
76
+ # We emulate open3.capture2e with the following changes in behavior:
77
+ # 1) The command is printed to stdout before execution.
78
+ # 2) Attempts to use the shell implicitly are blocked.
79
+ # 3) Nonzero return codes result in the process exiting.
80
+ # 4) Combined stdout/stderr goes to callers stdout
81
+ # (continuously streamed) and is returned as a string
82
+ #
83
+ # If you're looking for more process/stream control read the spawn
84
+ # documentation, and pass options directly here
85
+ def popen2e_wrapper(*shell_safe_cmd, stdin_data: nil, binmode: false,
86
+ quiet: false, **opts)
87
+
88
+ env = opts.delete(:env) { {} }
89
+ raise ArgumentError, "The :env option must be a hash, not #{env.inspect}" if !env.is_a?(Hash)
90
+
91
+ # Most of this is copied from Open3.capture2e in ruby/lib/open3.rb
92
+ _output, _status = Open3.popen2e(env, *shell_safe_cmd, opts) do |i, oe, t|
93
+ if binmode
94
+ i.binmode
95
+ oe.binmode
96
+ end
97
+
98
+ outerr_reader = Thread.new do
99
+ if quiet
100
+ oe.read
101
+ else
102
+ # Instead of oe.read, we redirect. Output from command goes to stdout
103
+ # and also is returned for processing if necessary.
104
+ tee(oe, STDOUT)
105
+ end
106
+ end
107
+
108
+ if stdin_data
109
+ begin
110
+ i.write stdin_data
111
+ rescue Errno::EPIPE
112
+ end
113
+ end
114
+
115
+ i.close
116
+ [outerr_reader.value, t.value]
117
+ end
118
+ end
119
+ module_function :popen2e_wrapper
120
+
121
+ # Look at a cmd list intended for spawn.
122
+ # determine if spawn will call the shell implicitly, fail in that case.
123
+ def shell_safe(cmd)
124
+ # Take the first string and change it to a list of [executable,argv0]
125
+ # This syntax for calling popen2e (and eventually spawn) avoids
126
+ # the shell in all cases
127
+ shell_safe_cmd = Array.new(cmd)
128
+ if shell_safe_cmd[0].class == String
129
+ shell_safe_cmd[0] = [shell_safe_cmd[0], shell_safe_cmd[0]]
130
+ end
131
+ shell_safe_cmd
132
+ end
133
+ module_function :shell_safe
134
+
135
+ def debug_print_cmd_list(cmd_list)
136
+ # Take a list of command argument lists like you'd sent to open3.pipeline or
137
+ # fail_on_error_pipe and print out a string that would do the same thing when
138
+ # entered at the shell.
139
+ #
140
+ # This is a converter from our internal representation of commands to a subset
141
+ # of bash that can be executed directly.
142
+ #
143
+ # Note this has problems if you specify env or opts
144
+ # TODO: make this remove those command parts
145
+ "\"" +
146
+ cmd_list.map do |cmd|
147
+ cmd.map do |arg|
148
+ arg.gsub("\"", "\\\"") # Escape all double quotes in command arguments
149
+ end.join("\" \"") # Fully quote all command parts, beginning and end.
150
+ end.join("\" | \"") + "\"" # Pipe commands to one another.
151
+ end
152
+ module_function :debug_print_cmd_list
153
+
154
+ # Prints a formatted string with command
155
+ def print_command(message, cmd)
156
+ logger.debug("#{message} #{debug_print_cmd_list(cmd)}\n")
157
+ end
158
+ module_function :print_command
159
+
160
+ # Takes in an input stream and an output stream
161
+ # Redirects data from one to the other until the input stream closes.
162
+ # Returns all data that passed through on return.
163
+ def tee(in_stream, out_stream)
164
+ alldata = ''
165
+ loop do
166
+ begin
167
+ data = in_stream.read_nonblock(4096)
168
+ alldata += data
169
+ out_stream.write(data)
170
+ out_stream.flush
171
+ rescue IO::WaitReadable
172
+ IO.select([in_stream])
173
+ retry
174
+ rescue IOError
175
+ break
176
+ end
177
+ end
178
+ alldata
179
+ end
180
+ module_function :tee
181
+
182
+ # If any of the statuses are bad, exits with the
183
+ # return code of the first one.
184
+ #
185
+ # Otherwise returns first argument (output)
186
+ def exit_on_status(output, cmd_list, status_list, quiet: false)
187
+ status_list.each_index do |index|
188
+ status = status_list[index]
189
+ cmd = cmd_list[index]
190
+ check_status(cmd, status, output: output, quiet: quiet)
191
+ end
192
+
193
+ output
194
+ end
195
+ module_function :exit_on_status
196
+
197
+ def check_status(cmd, status, output: nil, quiet: false)
198
+ return if status.exited? && status.exitstatus == 0
199
+
200
+ # If we exited nonzero or abnormally, print debugging info and explode.
201
+ if status.exited?
202
+ logger.debug("Process Exited normally. Exit status:#{status.exitstatus}") unless quiet
203
+ else
204
+ # This should only get executed if we're stopped or signaled
205
+ logger.debug("Process exited abnormally:\nProcessStatus: #{status.inspect}\n" \
206
+ "Raw POSIX Status: #{status.to_i}\n") unless quiet
207
+ end
208
+
209
+ raise RunnerExecutionRuntimeError.new(status, cmd, output)
210
+ end
211
+ module_function :check_status
212
+
213
+ DEFAULT_LOGGER = Logger.new(STDOUT)
214
+ private_constant :DEFAULT_LOGGER
215
+
216
+ def logger
217
+ DEFAULT_LOGGER
218
+ end
219
+ module_function :logger
220
+ end
221
+ # rubocop:enable all
@@ -36,6 +36,7 @@ describe GitFastClone::Runner do
36
36
 
37
37
  before do
38
38
  stub_const('ARGV', ['ssh://git@git.com/git-fastclone.git', 'test_reference_dir'])
39
+ allow($stdout).to receive(:puts)
39
40
  end
40
41
 
41
42
  let(:yielded) { [] }
@@ -50,7 +51,6 @@ describe GitFastClone::Runner do
50
51
  expect(subject.reference_mutex).to eq({})
51
52
  expect(subject.reference_updated).to eq({})
52
53
  expect(subject.options).to eq({})
53
- expect(subject.logger).to eq(nil)
54
54
  end
55
55
  end
56
56
 
@@ -87,10 +87,9 @@ describe GitFastClone::Runner do
87
87
  end
88
88
 
89
89
  describe '.clone' do
90
- let(:terrapin_commandline_double) { double('new_terrapin_commandline') }
90
+ let(:runner_execution_double) { double('runner_execution') }
91
91
  before(:each) do
92
- allow(terrapin_commandline_double).to receive(:run) {}
93
- expect(Time).to receive(:now).twice { 0 }
92
+ allow(runner_execution_double).to receive(:fail_pipe_on_error) {}
94
93
  allow(Dir).to receive(:pwd) { '/pwd' }
95
94
  allow(Dir).to receive(:chdir).and_yield
96
95
  allow(subject).to receive(:with_git_mirror).and_yield('/cache', 0)
@@ -98,36 +97,37 @@ describe GitFastClone::Runner do
98
97
  end
99
98
 
100
99
  it 'should clone correctly' do
101
- expect(Terrapin::CommandLine).to receive(:new).with(
102
- 'git clone',
103
- '--quiet --reference :mirror :url :path'
104
- ) { terrapin_commandline_double }
105
- expect(Terrapin::CommandLine).to receive(:new).with(
106
- 'git checkout',
107
- '--quiet :rev'
108
- ) { terrapin_commandline_double }
109
- expect(terrapin_commandline_double).to receive(:run).with(
110
- mirror: '/cache',
111
- url: placeholder_arg,
112
- path: '/pwd/.',
113
- config: ''
114
- )
115
- expect(terrapin_commandline_double).to receive(:run).with(rev: placeholder_arg)
100
+ expect(subject).to receive(:fail_pipe_on_error).with(
101
+ ['git', 'checkout', '--quiet', 'PH'],
102
+ { quiet: true }
103
+ ) { runner_execution_double }
104
+ expect(subject).to receive(:fail_pipe_on_error).with(
105
+ ['git', 'clone', '--quiet', '--reference', '/cache', 'PH', '/pwd/.'],
106
+ { quiet: true }
107
+ ) { runner_execution_double }
108
+
109
+ subject.clone(placeholder_arg, placeholder_arg, '.', nil)
110
+ end
111
+
112
+ it 'should clone correctly with verbose mode on' do
113
+ subject.verbose = true
114
+ expect(subject).to receive(:fail_pipe_on_error).with(
115
+ ['git', 'checkout', '--quiet', 'PH'],
116
+ { quiet: false }
117
+ ) { runner_execution_double }
118
+ expect(subject).to receive(:fail_pipe_on_error).with(
119
+ ['git', 'clone', '--verbose', '--reference', '/cache', 'PH', '/pwd/.'],
120
+ { quiet: false }
121
+ ) { runner_execution_double }
116
122
 
117
123
  subject.clone(placeholder_arg, placeholder_arg, '.', nil)
118
124
  end
119
125
 
120
126
  it 'should clone correctly with custom configs' do
121
- expect(Terrapin::CommandLine).to receive(:new).with(
122
- 'git clone',
123
- '--quiet --reference :mirror :url :path --config :config'
124
- ) { terrapin_commandline_double }
125
- expect(terrapin_commandline_double).to receive(:run).with(
126
- mirror: '/cache',
127
- url: placeholder_arg,
128
- path: '/pwd/.',
129
- config: 'config'
130
- )
127
+ expect(subject).to receive(:fail_pipe_on_error).with(
128
+ ['git', 'clone', '--quiet', '--reference', '/cache', 'PH', '/pwd/.', '--config', 'config'],
129
+ { quiet: true }
130
+ ) { runner_execution_double }
131
131
 
132
132
  subject.clone(placeholder_arg, nil, '.', 'config')
133
133
  end
@@ -294,36 +294,35 @@ describe GitFastClone::Runner do
294
294
 
295
295
  describe '.store_updated_repo' do
296
296
  context 'when fail_hard is true' do
297
- it 'should raise a Terrapin error' do
298
- terrapin_commandline_double = double('new_terrapin_commandline')
299
- allow(terrapin_commandline_double).to receive(:run) { raise Terrapin::ExitStatusError }
300
- allow(Terrapin::CommandLine).to receive(:new) { terrapin_commandline_double }
297
+ it 'should raise a Runtime error and clear cache' do
298
+ status = double('status')
299
+ allow(status).to receive(:exitstatus).and_return(1)
300
+ ex = RunnerExecution::RunnerExecutionRuntimeError.new(status, 'cmd')
301
+ allow(subject).to receive(:fail_pipe_on_error) { raise ex }
301
302
  expect(FileUtils).to receive(:remove_entry_secure).with(placeholder_arg, force: true)
302
303
  expect do
303
304
  subject.store_updated_repo(placeholder_arg, placeholder_arg, placeholder_arg, true)
304
- end.to raise_error(Terrapin::ExitStatusError)
305
+ end.to raise_error(ex)
305
306
  end
306
307
  end
307
308
 
308
309
  context 'when fail_hard is false' do
309
- it 'should not raise a terrapin error' do
310
- terrapin_commandline_double = double('new_terrapin_commandline')
311
- allow(terrapin_commandline_double).to receive(:run) { raise Terrapin::ExitStatusError }
312
- allow(Terrapin::CommandLine).to receive(:new) { terrapin_commandline_double }
310
+ it 'should not raise a Runtime error but clear cache' do
311
+ status = double('status')
312
+ allow(status).to receive(:exitstatus).and_return(1)
313
+ ex = RunnerExecution::RunnerExecutionRuntimeError.new(status, 'cmd')
314
+ allow(subject).to receive(:fail_pipe_on_error) { raise ex }
313
315
  expect(FileUtils).to receive(:remove_entry_secure).with(placeholder_arg, force: true)
314
-
315
316
  expect do
316
317
  subject.store_updated_repo(placeholder_arg, placeholder_arg, placeholder_arg, false)
317
- end.not_to raise_error
318
+ end.to_not raise_error
318
319
  end
319
320
  end
320
321
 
321
322
  let(:placeholder_hash) { {} }
322
323
 
323
324
  it 'should correctly update the hash' do
324
- terrapin_commandline_double = double('new_terrapin_commandline')
325
- allow(terrapin_commandline_double).to receive(:run) {}
326
- allow(Terrapin::CommandLine).to receive(:new) { terrapin_commandline_double }
325
+ allow(subject).to receive(:fail_pipe_on_error)
327
326
  allow(Dir).to receive(:chdir) {}
328
327
 
329
328
  subject.reference_updated = placeholder_hash
@@ -335,10 +334,6 @@ describe GitFastClone::Runner do
335
334
  describe '.with_git_mirror' do
336
335
  def retriable_error
337
336
  %(
338
- STDOUT:
339
-
340
- STDERR:
341
-
342
337
  fatal: bad object ee35b1e14e7c3a53dcc14d82606e5b872f6a05a7
343
338
  fatal: remote did not send all necessary objects
344
339
  ).strip.split("\n").map(&:strip).join("\n")
@@ -351,7 +346,11 @@ describe GitFastClone::Runner do
351
346
  ->(url) { url }
352
347
  else
353
348
  # Simulate failed error response
354
- ->(_url) { raise Terrapin::ExitStatusError, response }
349
+ lambda { |_url|
350
+ status = double('status')
351
+ allow(status).to receive(:exitstatus).and_return(1)
352
+ raise RunnerExecution::RunnerExecutionRuntimeError.new(status, 'cmd', response)
353
+ }
355
354
  end
356
355
  end
357
356
 
@@ -366,19 +365,22 @@ describe GitFastClone::Runner do
366
365
  end
367
366
 
368
367
  let(:expected_commands) { [] }
369
- let(:expected_commands_args) { [] }
370
368
 
371
369
  before(:each) do
372
- expect(expected_commands.length).to eq(expected_commands_args.length)
373
- allow(Terrapin::CommandLine).to receive(:new) do |*command|
370
+ allow(subject).to receive(:fail_pipe_on_error) { |*params|
371
+ command = params[0]
374
372
  expect(expected_commands.length).to be > 0
375
373
  expected_command = expected_commands.shift
376
- expected_args = expected_commands_args.shift
377
374
  expect(command).to eq(expected_command)
378
- stub = double(Terrapin::CommandLine)
379
- expect(stub).to receive(:run).with(expected_args)
380
- stub
381
- end
375
+ }
376
+ allow(subject).to receive(:fail_on_error) { |*params|
377
+ # last one is an argument `quiet:`
378
+ command = params.first(params.size - 1)
379
+ expect(expected_commands.length).to be > 0
380
+ expected_command = expected_commands.shift
381
+ expect(command).to eq(expected_command)
382
+ }
383
+ allow(Dir).to receive(:chdir).and_yield
382
384
 
383
385
  allow(subject).to receive(:print_formatted_error) {}
384
386
  allow(subject).to receive(:reference_repo_dir).and_return(test_reference_repo_dir)
@@ -389,39 +391,46 @@ describe GitFastClone::Runner do
389
391
  expect(expected_commands).to be_empty
390
392
  end
391
393
 
392
- def clone_cmds
394
+ def clone_cmds(verbose: false)
393
395
  [
394
- ['git clone', '--mirror :url :mirror'],
395
- ['cd', ':path; git remote update --prune']
396
- ]
397
- end
398
-
399
- def clone_args
400
- [
401
- {
402
- mirror: test_reference_repo_dir,
403
- url: test_url_valid
404
- },
405
- {
406
- path: test_reference_repo_dir
407
- }
396
+ ['git', 'clone', verbose ? '--verbose' : '--quiet', '--mirror', test_url_valid,
397
+ test_reference_repo_dir],
398
+ ['git', 'remote', verbose ? '--verbose' : nil, 'update', '--prune'].compact
408
399
  ]
409
400
  end
410
401
 
411
402
  context 'expecting 1 clone attempt' do
412
- let(:expected_commands) { clone_cmds }
413
- let(:expected_commands_args) { clone_args }
403
+ context 'with verbose mode on' do
404
+ before { subject.verbose = true }
405
+ let(:expected_commands) { clone_cmds(verbose: true) }
406
+
407
+ it 'should succeed with a successful clone' do
408
+ expect(subject).not_to receive(:clear_cache)
409
+ try_with_git_mirror([true], [[test_reference_repo_dir, 0]])
410
+ end
414
411
 
415
- it 'should succeed with a successful clone' do
416
- expect(subject).not_to receive(:clear_cache)
417
- try_with_git_mirror([true], [[test_reference_repo_dir, 0]])
412
+ it 'should fail after a non-retryable clone error' do
413
+ expect(subject).not_to receive(:clear_cache)
414
+ expect do
415
+ try_with_git_mirror(['Some unexpected error message'], [])
416
+ end.to raise_error(RunnerExecution::RunnerExecutionRuntimeError)
417
+ end
418
418
  end
419
419
 
420
- it 'should fail after a non-retryable clone error' do
421
- expect(subject).not_to receive(:clear_cache)
422
- expect do
423
- try_with_git_mirror(['Some unexpected error message'], [])
424
- end.to raise_error(Terrapin::ExitStatusError)
420
+ context 'with verbose mode off' do
421
+ let(:expected_commands) { clone_cmds }
422
+
423
+ it 'should succeed with a successful clone' do
424
+ expect(subject).not_to receive(:clear_cache)
425
+ try_with_git_mirror([true], [[test_reference_repo_dir, 0]])
426
+ end
427
+
428
+ it 'should fail after a non-retryable clone error' do
429
+ expect(subject).not_to receive(:clear_cache)
430
+ expect do
431
+ try_with_git_mirror(['Some unexpected error message'], [])
432
+ end.to raise_error(RunnerExecution::RunnerExecutionRuntimeError)
433
+ end
425
434
  end
426
435
  end
427
436
 
@@ -438,14 +447,14 @@ describe GitFastClone::Runner do
438
447
  expect(subject).to receive(:clear_cache).twice.and_call_original
439
448
  expect do
440
449
  try_with_git_mirror([retriable_error, retriable_error], [])
441
- end.to raise_error(Terrapin::ExitStatusError)
450
+ end.to raise_error(RunnerExecution::RunnerExecutionRuntimeError)
442
451
  end
443
452
  end
444
453
  end
445
454
 
446
455
  describe '.retriable_error?' do
447
456
  def format_error(error)
448
- error_wrapper = "STDOUT:\n\nSTDERR:\n#{error}"
457
+ error_wrapper = error.to_s
449
458
  error_wrapper.strip.lines.map(&:strip).join("\n")
450
459
  end
451
460
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: git-fastclone
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Tauraso
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2023-01-07 00:00:00.000000000 Z
12
+ date: 2023-03-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: colorize
@@ -25,20 +25,6 @@ dependencies:
25
25
  - - ">="
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
- - !ruby/object:Gem::Dependency
29
- name: terrapin
30
- requirement: !ruby/object:Gem::Requirement
31
- requirements:
32
- - - "~>"
33
- - !ruby/object:Gem::Version
34
- version: 0.6.0
35
- type: :runtime
36
- prerelease: false
37
- version_requirements: !ruby/object:Gem::Requirement
38
- requirements:
39
- - - "~>"
40
- - !ruby/object:Gem::Version
41
- version: 0.6.0
42
28
  description: A git command that uses reference repositories and threading to quicklyand
43
29
  recursively clone repositories with many nested submodules
44
30
  email:
@@ -55,6 +41,7 @@ files:
55
41
  - bin/git-fastclone
56
42
  - lib/git-fastclone.rb
57
43
  - lib/git-fastclone/version.rb
44
+ - lib/runner_execution.rb
58
45
  - spec/git_fastclone_runner_spec.rb
59
46
  - spec/git_fastclone_url_helper_spec.rb
60
47
  - spec/spec_helper.rb