grit 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,38 @@
1
+ == 2.4.0 / 2011-01-06
2
+ * Major Enhancements
3
+ * Add support for parsing git notes.
4
+ * Add `git cat-file --batch` support with Grit::Repo#batch.
5
+ * Grit::Process is a custom written external command invocation heavily
6
+ optimized for running git commands quickly and efficiently.
7
+ * Grit::Git#native takes an :input option for piping data into git
8
+ commands
9
+ * Grit::Git#native takes an :env option for setting the git child
10
+ process's
11
+ environment without futsing with the parent's environment.
12
+ * Grit::Git#native takes an :chdir option for setting the current working
13
+ directory (PWD) of the git child process.
14
+ * Grit::Git#native takes an :raise => true option that causes an exception
15
+ to be raised when the git child process exits non-zero.
16
+ * Minor Enhancements
17
+ * Grit::Index#commit supports custom committer/author names and dates.
18
+ * Performance enhancements with internal command output buffering.
19
+ * Reduce fork/execs needed to execute a smoke command from between 3-4
20
+ to 1.
21
+ * Git child processes are now properly parented under the grit Ruby
22
+ process instead of being dropped under init.
23
+ * Bug Fixes
24
+ * Zero-Padding issue in Grit::Index was fixed.
25
+ * Fix issue where Repo#diff skips the first diff (#42)
26
+ * Fix Repo.init_bare for repo names not ending in .git (#40)
27
+ * Fix a variety of process hangs when git stderr output or data written
28
+ to stdin exceeded PIPE_BUF bytes.
29
+
30
+ == 2.3.2 / 2011-01-06
31
+ * Erroneously released. SemVer violation and misc release screwups.
32
+
33
+ == 2.3.1
34
+ * Skipped for unknown reasons.
35
+
1
36
  == 2.3.0 / 2010-09-29
2
37
  * Minor Enhancements
3
38
  * Add Grit::Repo.init.
@@ -4,8 +4,8 @@ Gem::Specification.new do |s|
4
4
  s.rubygems_version = '1.3.5'
5
5
 
6
6
  s.name = 'grit'
7
- s.version = '2.3.0'
8
- s.date = '2010-09-29'
7
+ s.version = '2.4.0'
8
+ s.date = '2011-01-06'
9
9
  s.rubyforge_project = 'grit'
10
10
 
11
11
  s.summary = "Ruby Git bindings."
@@ -59,9 +59,10 @@ Gem::Specification.new do |s|
59
59
  lib/grit/git-ruby/repository.rb
60
60
  lib/grit/git.rb
61
61
  lib/grit/index.rb
62
+ lib/grit/jruby.rb
62
63
  lib/grit/lazy.rb
63
64
  lib/grit/merge.rb
64
- lib/grit/open3_detach.rb
65
+ lib/grit/process.rb
65
66
  lib/grit/ref.rb
66
67
  lib/grit/repo.rb
67
68
  lib/grit/ruby1.9.rb
@@ -9,27 +9,26 @@ require 'timeout'
9
9
  require 'logger'
10
10
  require 'digest/sha1'
11
11
 
12
- if defined? RUBY_ENGINE && RUBY_ENGINE == 'jruby'
13
- require 'open3'
14
- elsif RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
15
- require 'win32/open3'
16
- else
17
- require 'grit/open3_detach'
18
- end
19
-
20
12
  # third party
21
- require 'rubygems'
13
+
22
14
  begin
23
- gem "mime-types", ">=0"
24
15
  require 'mime/types'
25
- rescue Gem::LoadError => e
26
- puts "WARNING: Gem LoadError: #{e.message}"
16
+ require 'rubygems'
17
+ rescue LoadError
18
+ require 'rubygems'
19
+ begin
20
+ gem "mime-types", ">=0"
21
+ require 'mime/types'
22
+ rescue Gem::LoadError => e
23
+ puts "WARNING: Gem LoadError: #{e.message}"
24
+ end
27
25
  end
28
26
 
29
27
  # ruby 1.9 compatibility
30
28
  require 'grit/ruby1.9'
31
29
 
32
30
  # internal requires
31
+ require 'grit/process'
33
32
  require 'grit/lazy'
34
33
  require 'grit/errors'
35
34
  require 'grit/git-ruby'
@@ -50,8 +49,13 @@ require 'grit/submodule'
50
49
  require 'grit/blame'
51
50
  require 'grit/merge'
52
51
 
52
+ # platform specific requires
53
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE =~ /jruby/
54
+ require 'grit/jruby'
55
+ end
56
+
53
57
  module Grit
54
- VERSION = '2.3.0'
58
+ VERSION = '2.4.0'
55
59
 
56
60
  class << self
57
61
  # Set +debug+ to true to log all git calls and responses
@@ -11,12 +11,10 @@ module Grit
11
11
  alias_method :to_s, :name
12
12
 
13
13
  # Create an Actor from a string.
14
- # +str+ is the string, which is expected to be in regular git format
15
14
  #
16
- # Format
17
- # John Doe <jdoe@example.com>
15
+ # str - The String in this format: 'John Doe <jdoe@example.com>'
18
16
  #
19
- # Returns Actor
17
+ # Returns Git::Actor.
20
18
  def self.from_string(str)
21
19
  case str
22
20
  when /<.+>/
@@ -27,6 +25,24 @@ module Grit
27
25
  end
28
26
  end
29
27
 
28
+ # Outputs an actor string for Git commits.
29
+ #
30
+ # actor = Actor.new('bob', 'bob@email.com')
31
+ # actor.output(time) # => "bob <bob@email.com> UNIX_TIME +0700"
32
+ #
33
+ # time - The Time the commit was authored or committed.
34
+ #
35
+ # Returns a String.
36
+ def output(time)
37
+ out = @name.to_s.dup
38
+ if @email
39
+ out << " <#{@email}>"
40
+ end
41
+ hours = (time.utc_offset.to_f / 3600).to_i # 60 * 60, seconds to hours
42
+ rem = time.utc_offset.abs % 3600
43
+ out << " #{time.to_i} #{hours >= 0 ? :+ : :-}#{hours.abs.to_s.rjust(2, '0')}#{rem.to_s.rjust(2, '0')}"
44
+ end
45
+
30
46
  # Pretty object inspection
31
47
  def inspect
32
48
  %Q{#<Grit::Actor "#{@name} <#{@email}>">}
@@ -26,16 +26,21 @@ module Grit
26
26
  if line[0, 1] == "\t"
27
27
  lines << line[1, line.size]
28
28
  elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
29
- if !commits[m[1]]
30
- commits[m[1]] = @repo.commit(m[1])
31
- end
32
- info[m[3].to_i] = [commits[m[1]], m[2].to_i]
29
+ commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
30
+ commits[commit_id] = nil if !commits.key?(commit_id)
31
+ info[lineno] = [commit_id, old_lineno]
33
32
  end
34
33
  end
35
34
 
35
+ # load all commits in single call
36
+ @repo.batch(*commits.keys).each do |commit|
37
+ commits[commit.id] = commit
38
+ end
39
+
36
40
  # get it together
37
- info.sort.each do |lineno, commit|
38
- final << BlameLine.new(lineno, commit[1], commit[0], lines[lineno - 1])
41
+ info.sort.each do |lineno, (commit_id, old_lineno)|
42
+ commit = commits[commit_id]
43
+ final << BlameLine.new(lineno, old_lineno, commit, lines[lineno - 1])
39
44
  end
40
45
 
41
46
  @lines = final
@@ -58,4 +63,4 @@ module Grit
58
63
 
59
64
  end # Blame
60
65
 
61
- end # Grit
66
+ end # Grit
@@ -1,6 +1,8 @@
1
1
  module Grit
2
2
 
3
3
  class Commit
4
+ extend Lazy
5
+
4
6
  attr_reader :id
5
7
  attr_reader :repo
6
8
  lazy_reader :parents
@@ -13,6 +15,31 @@ module Grit
13
15
  lazy_reader :short_message
14
16
  lazy_reader :author_string
15
17
 
18
+ # Parses output from the `git-cat-file --batch'.
19
+ #
20
+ # repo - Grit::Repo instance.
21
+ # sha - String SHA of the Commit.
22
+ # size - Fixnum size of the object.
23
+ # object - Parsed String output from `git cat-file --batch`.
24
+ #
25
+ # Returns an Array of Grit::Commit objects.
26
+ def self.parse_batch(repo, sha, size, object)
27
+ info, message = object.split("\n\n", 2)
28
+
29
+ lines = info.split("\n")
30
+ tree = lines.shift.split(' ', 2).last
31
+ parents = []
32
+ parents << lines.shift[7..-1] while lines.first[0, 6] == 'parent'
33
+ author, authored_date = Grit::Commit.actor(lines.shift)
34
+ committer, committed_date = Grit::Commit.actor(lines.shift)
35
+
36
+ Grit::Commit.new(
37
+ repo, sha, parents, tree,
38
+ author, authored_date,
39
+ committer, committed_date,
40
+ message.to_s.split("\n"))
41
+ end
42
+
16
43
  # Instantiate a new Commit
17
44
  # +id+ is the id of the commit
18
45
  # +parents+ is an array of commit ids (will be converted into Commit instances)
@@ -167,7 +194,7 @@ module Grit
167
194
 
168
195
  def show
169
196
  if parents.size > 1
170
- diff = @repo.git.native("diff #{parents[0].id}...#{parents[1].id}", {:full_index => true})
197
+ diff = @repo.git.native(:diff, {:full_index => true}, "#{parents[0].id}...#{parents[1].id}")
171
198
  else
172
199
  diff = @repo.git.show({:full_index => true, :pretty => 'raw'}, @id)
173
200
  end
@@ -214,6 +241,17 @@ module Grit
214
241
  @repo.git.format_patch({'1' => true, :stdout => true}, to_s)
215
242
  end
216
243
 
244
+ def notes
245
+ ret = {}
246
+ notes = Note.find_all(@repo)
247
+ notes.each do |note|
248
+ if n = note.commit.tree/(self.id)
249
+ ret[note.name] = n.data
250
+ end
251
+ end
252
+ ret
253
+ end
254
+
217
255
  # Pretty object inspection
218
256
  def inspect
219
257
  %Q{#<Grit::Commit "#{@id}">}
@@ -317,7 +317,7 @@ module Grit
317
317
  if indata.size == 0
318
318
  raise PackFormatError, 'error reading pack data'
319
319
  end
320
- outdata += zstr.inflate(indata)
320
+ outdata << zstr.inflate(indata)
321
321
  end
322
322
  if outdata.size > destsize
323
323
  raise PackFormatError, 'error reading pack data'
@@ -351,9 +351,9 @@ module Grit
351
351
  cp_size |= delta.getord(pos += 1) << 16 if c & 0x40 != 0
352
352
  cp_size = 0x10000 if cp_size == 0
353
353
  pos += 1
354
- dest += base[cp_off,cp_size]
354
+ dest << base[cp_off,cp_size]
355
355
  elsif c != 0
356
- dest += delta[pos,c]
356
+ dest << delta[pos,c]
357
357
  pos += c
358
358
  else
359
359
  raise PackFormatError, 'invalid delta data'
@@ -701,21 +701,35 @@ module Grit
701
701
  @loose
702
702
  end
703
703
 
704
- def load_alternate_loose(path)
705
- # load alternate loose, too
704
+ def each_alternate_path(path)
706
705
  alt = File.join(path, 'info/alternates')
707
- if File.exists?(alt)
708
- File.readlines(alt).each do |line|
709
- next if @loaded.include?(line.chomp)
710
- if line[0, 2] == '..'
711
- line = File.expand_path(File.join(@git_dir, line))
712
- end
713
- load_loose(line.chomp)
714
- load_alternate_loose(line.chomp)
706
+ return if !File.exists?(alt)
707
+
708
+ File.readlines(alt).each do |line|
709
+ path = line.chomp
710
+ if path[0, 2] == '..'
711
+ yield File.expand_path(File.join(@git_dir, 'objects', path))
712
+
713
+ # XXX this is here for backward compatibility with grit < 2.3.0
714
+ # relative alternate objects paths are expanded relative to the
715
+ # objects directory, not the git repository directory.
716
+ yield File.expand_path(File.join(@git_dir, path))
717
+ else
718
+ yield path
715
719
  end
716
720
  end
717
721
  end
718
722
 
723
+ def load_alternate_loose(path)
724
+ # load alternate loose, too
725
+ each_alternate_path path do |path|
726
+ next if @loaded.include?(path)
727
+ next if !File.exist?(path)
728
+ load_loose(path)
729
+ load_alternate_loose(path)
730
+ end
731
+ end
732
+
719
733
  def load_loose(path)
720
734
  @loaded << path
721
735
  return if !File.exists?(path)
@@ -732,17 +746,11 @@ module Grit
732
746
  end
733
747
 
734
748
  def load_alternate_packs(path)
735
- alt = File.join(path, 'info/alternates')
736
- if File.exists?(alt)
737
- File.readlines(alt).each do |line|
738
- if line[0, 2] == '..'
739
- line = File.expand_path(File.join(@git_dir, line))
740
- end
741
- full_pack = File.join(line.chomp, 'pack')
742
- next if @loaded_packs.include?(full_pack)
743
- load_packs(full_pack)
744
- load_alternate_packs(File.join(line.chomp))
745
- end
749
+ each_alternate_path path do |path|
750
+ full_pack = File.join(path, 'pack')
751
+ next if @loaded_packs.include?(full_pack)
752
+ load_packs(full_pack)
753
+ load_alternate_packs(path)
746
754
  end
747
755
  end
748
756
 
@@ -3,7 +3,8 @@ module Grit
3
3
 
4
4
  class Git
5
5
  class GitTimeout < RuntimeError
6
- attr_reader :command, :bytes_read
6
+ attr_accessor :command
7
+ attr_accessor :bytes_read
7
8
 
8
9
  def initialize(command = nil, bytes_read = nil)
9
10
  @command = command
@@ -11,6 +12,25 @@ module Grit
11
12
  end
12
13
  end
13
14
 
15
+ # Raised when a native git command exits with non-zero.
16
+ class CommandFailed < StandardError
17
+ # The full git command that failed as a String.
18
+ attr_reader :command
19
+
20
+ # The integer exit status.
21
+ attr_reader :exitstatus
22
+
23
+ # Everything output on the command's stderr as a String.
24
+ attr_reader :err
25
+
26
+ def initialize(command, exitstatus, err='')
27
+ @command = command
28
+ @exitstatus = exitstatus
29
+ @err = err
30
+ super "Command exited with #{exitstatus}: #{command}"
31
+ end
32
+ end
33
+
14
34
  undef_method :clone
15
35
 
16
36
  include GitRuby
@@ -34,14 +54,16 @@ module Grit
34
54
  end
35
55
 
36
56
  class << self
37
- attr_accessor :git_binary, :git_timeout, :git_max_size
57
+ attr_accessor :git_timeout, :git_max_size
58
+ def git_binary
59
+ @git_binary ||=
60
+ ENV['PATH'].split(':').
61
+ map { |p| File.join(p, 'git') }.
62
+ find { |p| File.exist?(p) }
63
+ end
64
+ attr_writer :git_binary
38
65
  end
39
66
 
40
- if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
41
- self.git_binary = "git" # using search path
42
- else
43
- self.git_binary = "/usr/bin/env git"
44
- end
45
67
  self.git_timeout = 10
46
68
  self.git_max_size = 5242880 # 5.megabytes
47
69
 
@@ -206,35 +228,157 @@ module Grit
206
228
  # RAW CALLS WITH ENV SETTINGS END
207
229
 
208
230
 
209
-
210
- # Run the given git command with the specified arguments and return
211
- # the result as a String
212
- # +cmd+ is the command
213
- # +options+ is a hash of Ruby style options
214
- # +args+ is the list of arguments (to be joined by spaces)
231
+ # Execute a git command, bypassing any library implementation.
232
+ #
233
+ # cmd - The name of the git command as a Symbol. Underscores are
234
+ # converted to dashes as in :rev_parse => 'rev-parse'.
235
+ # options - Command line option arguments passed to the git command.
236
+ # Single char keys are converted to short options (:a => -a).
237
+ # Multi-char keys are converted to long options (:arg => '--arg').
238
+ # Underscores in keys are converted to dashes. These special options
239
+ # are used to control command execution and are not passed in command
240
+ # invocation:
241
+ # :timeout - Maximum amount of time the command can run for before
242
+ # being aborted. When true, use Grit::Git.git_timeout; when numeric,
243
+ # use that number of seconds; when false or 0, disable timeout.
244
+ # :base - Set false to avoid passing the --git-dir argument when
245
+ # invoking the git command.
246
+ # :env - Hash of environment variable key/values that are set on the
247
+ # child process.
248
+ # :raise - When set true, commands that exit with a non-zero status
249
+ # raise a CommandFailed exception. This option is available only on
250
+ # platforms that support fork(2).
251
+ # args - Non-option arguments passed on the command line.
252
+ #
253
+ # Optionally yields to the block an IO object attached to the child
254
+ # process's STDIN.
215
255
  #
216
256
  # Examples
257
+ # git.native(:rev_list, {:max_count => 10, :header => true}, "master")
258
+ #
259
+ # Returns a String with all output written to the child process's stdout.
260
+ # Raises Grit::Git::GitTimeout when the timeout is exceeded or when more
261
+ # than Grit::Git.git_max_size bytes are output.
262
+ # Raises Grit::Git::CommandFailed when the :raise option is set true and the
263
+ # git command exits with a non-zero exit status. The CommandFailed's #command,
264
+ # #exitstatus, and #err attributes can be used to retrieve additional
265
+ # detail about the error.
266
+ def native(cmd, options = {}, *args, &block)
267
+ args = args.first if args.size == 1 && args[0].is_a?(Array)
268
+ args.map! { |a| a.to_s.strip }
269
+ args.reject! { |a| a.empty? }
270
+
271
+ # special option arguments
272
+ env = options.delete(:env) || {}
273
+ raise_errors = options.delete(:raise)
274
+
275
+ # fall back to using a shell when the last argument looks like it wants to
276
+ # start a pipeline for compatibility with previous versions of grit.
277
+ return run(prefix, cmd, '', options, args) if args[-1].to_s[0] == ?|
278
+
279
+ # more options
280
+ input = options.delete(:input)
281
+ timeout = options.delete(:timeout); timeout = true if timeout.nil?
282
+ base = options.delete(:base); base = true if base.nil?
283
+ chdir = options.delete(:chdir)
284
+
285
+ # build up the git process argv
286
+ argv = []
287
+ argv << Git.git_binary
288
+ argv << "--git-dir=#{git_dir}" if base
289
+ argv << cmd.to_s.tr('_', '-')
290
+ argv.concat(options_to_argv(options))
291
+ argv.concat(args)
292
+
293
+ # run it and deal with fallout
294
+ Grit.log(argv.join(' ')) if Grit.debug
295
+
296
+ process =
297
+ Grit::Process.new(argv, env,
298
+ :input => input,
299
+ :chdir => chdir,
300
+ :timeout => (Grit::Git.git_timeout if timeout == true),
301
+ :max => (Grit::Git.git_max_size if timeout == true)
302
+ )
303
+ status = process.status
304
+ Grit.log(process.out) if Grit.debug
305
+ Grit.log(process.err) if Grit.debug
306
+ if raise_errors && !status.success?
307
+ raise CommandFailed.new(argv.join(' '), status.exitstatus, process.err)
308
+ else
309
+ process.out
310
+ end
311
+ rescue Grit::Process::TimeoutExceeded, Grit::Process::MaximumOutputExceeded
312
+ raise GitTimeout, argv.join(' ')
313
+ end
314
+
315
+ # Methods not defined by a library implementation execute the git command
316
+ # using #native, passing the method name as the git command name.
317
+ #
318
+ # Examples:
217
319
  # git.rev_list({:max_count => 10, :header => true}, "master")
320
+ def method_missing(cmd, options={}, *args, &block)
321
+ native(cmd, options, *args, &block)
322
+ end
323
+
324
+ # Transform a ruby-style options hash to command-line arguments sutiable for
325
+ # use with Kernel::exec. No shell escaping is performed.
218
326
  #
219
- # Returns String
220
- def method_missing(cmd, options = {}, *args)
221
- run('', cmd, '', options, args)
327
+ # Returns an Array of String option arguments.
328
+ def options_to_argv(options)
329
+ argv = []
330
+ options.each do |key, val|
331
+ if key.to_s.size == 1
332
+ if val == true
333
+ argv << "-#{key}"
334
+ elsif val == false
335
+ # ignore
336
+ else
337
+ argv << "-#{key}"
338
+ argv << val.to_s
339
+ end
340
+ else
341
+ if val == true
342
+ argv << "--#{key.to_s.tr('_', '-')}"
343
+ elsif val == false
344
+ # ignore
345
+ else
346
+ argv << "--#{key.to_s.tr('_', '-')}=#{val}"
347
+ end
348
+ end
349
+ end
350
+ argv
222
351
  end
223
352
 
224
- # Bypass any pure Ruby implementations and go straight to the native Git command
353
+ # Simple wrapper around Timeout::timeout.
354
+ #
355
+ # seconds - Float number of seconds before a Timeout::Error is raised. When
356
+ # true, the Grit::Git.git_timeout value is used. When the timeout is less
357
+ # than or equal to 0, no timeout is established.
225
358
  #
226
- # Returns String
227
- def native(cmd, options = {}, *args)
228
- method_missing(cmd, options, *args)
359
+ # Raises Timeout::Error when the timeout has elapsed.
360
+ def timeout_after(seconds)
361
+ seconds = self.class.git_timeout if seconds == true
362
+ if seconds && seconds > 0
363
+ Timeout.timeout(seconds) { yield }
364
+ else
365
+ yield
366
+ end
229
367
  end
230
368
 
231
- def run(prefix, cmd, postfix, options, args)
369
+ # DEPRECATED OPEN3-BASED COMMAND EXECUTION
370
+
371
+ def run(prefix, cmd, postfix, options, args, &block)
232
372
  timeout = options.delete(:timeout) rescue nil
233
373
  timeout = true if timeout.nil?
234
374
 
235
375
  base = options.delete(:base) rescue nil
236
376
  base = true if base.nil?
237
377
 
378
+ if input = options.delete(:input)
379
+ block = lambda { |stdin| stdin.write(input) }
380
+ end
381
+
238
382
  opt_args = transform_options(options)
239
383
 
240
384
  if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin/
@@ -246,50 +390,29 @@ module Grit
246
390
  gitdir = base ? "--git-dir='#{self.git_dir}'" : ""
247
391
  call = "#{prefix}#{Git.git_binary} #{gitdir} #{cmd.to_s.gsub(/_/, '-')} #{(opt_args + ext_args).join(' ')}#{e(postfix)}"
248
392
  end
393
+
249
394
  Grit.log(call) if Grit.debug
250
- response, err = timeout ? sh(call) : wild_sh(call)
395
+ response, err = timeout ? sh(call, &block) : wild_sh(call, &block)
251
396
  Grit.log(response) if Grit.debug
252
397
  Grit.log(err) if Grit.debug
253
398
  response
254
399
  end
255
400
 
256
- def sh(command)
257
- ret, err = '', ''
258
- Open3.popen3(command) do |_, stdout, stderr|
259
- Timeout.timeout(self.class.git_timeout) do
260
- while tmp = stdout.read(1024)
261
- ret += tmp
262
- if (@bytes_read += tmp.size) > self.class.git_max_size
263
- bytes = @bytes_read
264
- @bytes_read = 0
265
- raise GitTimeout.new(command, bytes)
266
- end
267
- end
268
- end
269
-
270
- while tmp = stderr.read(1024)
271
- err += tmp
272
- end
273
- end
274
- [ret, err]
275
- rescue Timeout::Error, Grit::Git::GitTimeout
276
- bytes = @bytes_read
277
- @bytes_read = 0
278
- raise GitTimeout.new(command, bytes)
279
- end
280
-
281
- def wild_sh(command)
282
- ret, err = '', ''
283
- Open3.popen3(command) do |_, stdout, stderr|
284
- while tmp = stdout.read(1024)
285
- ret += tmp
286
- end
401
+ def sh(command, &block)
402
+ process =
403
+ Grit::Process.new(
404
+ command, {},
405
+ :timeout => Git.git_timeout,
406
+ :max => Git.git_max_size
407
+ )
408
+ [process.out, process.err]
409
+ rescue Grit::Process::TimeoutExceeded, Grit::Process::MaximumOutputExceeded
410
+ raise GitTimeout, command
411
+ end
287
412
 
288
- while tmp = stderr.read(1024)
289
- err += tmp
290
- end
291
- end
292
- [ret, err]
413
+ def wild_sh(command, &block)
414
+ process = Grit::Process.new(command)
415
+ [process.out, process.err]
293
416
  end
294
417
 
295
418
  # Transform Ruby style options into git command line options
@@ -63,7 +63,27 @@ module Grit
63
63
  self.current_tree = self.repo.tree(tree)
64
64
  end
65
65
 
66
- # Public: Commit the contents of the index
66
+ # Public: Commit the contents of the index. This method supports two
67
+ # formats for arguments:
68
+ #
69
+ # message - The String commit message.
70
+ # options - An optional Hash of index options.
71
+ # :parents - Array of String commit SHA1s or Grit::Commit
72
+ # objects to attach this commit to to form a
73
+ # new head (default: nil).
74
+ # :actor - The Grit::Actor details of the user making
75
+ # the commit (default: nil).
76
+ # :last_tree - The String SHA1 of a tree to compare with
77
+ # in order to avoid making empty commits
78
+ # (default: nil).
79
+ # :head - The String branch name to write this head to
80
+ # (default: "master").
81
+ # :committed_date - The Time that the commit was made.
82
+ # (Default: Time.now)
83
+ # :authored_date - The Time that the commit was authored.
84
+ # (Default: committed_date)
85
+ #
86
+ # The legacy argument style looks like:
67
87
  #
68
88
  # message - The String commit message.
69
89
  # parents - Array of String commit SHA1s or Grit::Commit objects to
@@ -77,6 +97,20 @@ module Grit
77
97
  #
78
98
  # Returns a String of the SHA1 of the new commit.
79
99
  def commit(message, parents = nil, actor = nil, last_tree = nil, head = 'master')
100
+ if parents.is_a?(Hash)
101
+ actor = parents[:actor]
102
+ committer = parents[:committer]
103
+ author = parents[:author]
104
+ last_tree = parents[:last_tree]
105
+ head = parents[:head]
106
+ committed_date = parents[:committed_date]
107
+ authored_date = parents[:authored_date]
108
+ parents = parents[:parents]
109
+ end
110
+
111
+ committer ||= actor
112
+ author ||= committer
113
+
80
114
  tree_sha1 = write_tree(self.tree, self.current_tree)
81
115
 
82
116
  # don't write identical commits
@@ -88,18 +122,16 @@ module Grit
88
122
  contents << ['parent', p].join(' ')
89
123
  end if parents
90
124
 
91
- if actor
92
- name = actor.name
93
- email = actor.email
94
- else
125
+ committer ||= begin
95
126
  config = Config.new(self.repo)
96
- name = config['user.name']
97
- email = config['user.email']
127
+ Actor.new(config['user.name'], config['user.email'])
98
128
  end
129
+ author ||= committer
130
+ committed_date ||= Time.now
131
+ authored_date ||= committed_date
99
132
 
100
- author_string = "#{name} <#{email}> #{Time.now.to_i} -0700" # !! TODO : gotta fix this
101
- contents << ['author', author_string].join(' ')
102
- contents << ['committer', author_string].join(' ')
133
+ contents << ['author', author.output(authored_date)].join(' ')
134
+ contents << ['committer', committer.output(committed_date)].join(' ')
103
135
  contents << ''
104
136
  contents << message
105
137
 
@@ -125,7 +157,8 @@ module Grit
125
157
  sha = [obj.id].pack("H*")
126
158
  k = obj.name
127
159
  k += '/' if (obj.class == Grit::Tree)
128
- tree_contents[k] = "%s %s\0%s" % [obj.mode.to_s, obj.name, sha]
160
+ tmode = obj.mode.to_i.to_s ## remove zero-padding
161
+ tree_contents[k] = "%s %s\0%s" % [tmode, obj.name, sha]
129
162
  end if now_tree
130
163
 
131
164
  # overwrite with new tree contents
@@ -0,0 +1,45 @@
1
+ require 'grit/process'
2
+
3
+ module Grit
4
+ # Override the Grit::Process class's popen4 and waitpid methods to work around
5
+ # various quirks in JRuby.
6
+ class Process
7
+ # Use JRuby's built in IO.popen4 but emulate the special spawn env
8
+ # and options arguments as best we can.
9
+ def popen4(*argv)
10
+ env = (argv.shift if argv[0].is_a?(Hash)) || {}
11
+ opt = (argv.pop if argv[-1].is_a?(Hash)) || {}
12
+
13
+ # emulate :chdir option
14
+ if opt[:chdir]
15
+ previous_dir = Dir.pwd
16
+ Dir.chdir(opt[:chdir])
17
+ else
18
+ previous_dir = nil
19
+ end
20
+
21
+ # emulate :env option
22
+ if env.size > 0
23
+ previous_env = ENV
24
+ ENV.merge!(env)
25
+ else
26
+ previous_env = nil
27
+ end
28
+
29
+ pid, stdin, stdout, stderr = IO.popen4(*argv)
30
+ ensure
31
+ ENV.replace(previous_env) if previous_env
32
+ Dir.chdir(previous_dir) if previous_dir
33
+ end
34
+
35
+ # JRuby always raises ECHILD on pids returned from its IO.popen4 method
36
+ # for some reason. Return a fake Process::Status object.
37
+ FakeStatus = Struct.new(:pid, :exitstatus, :success?, :fake?)
38
+ def waitpid(pid)
39
+ ::Process::waitpid(pid)
40
+ $?
41
+ rescue Errno::ECHILD
42
+ FakeStatus.new(pid, 0, true, true)
43
+ end
44
+ end
45
+ end
@@ -16,6 +16,10 @@
16
16
  # => 2
17
17
  #
18
18
  module Lazy
19
+ def self.extended(klass)
20
+ klass.send(:attr_writer, :lazy_source)
21
+ end
22
+
19
23
  def lazy_reader(*args)
20
24
  args.each do |arg|
21
25
  ivar = "@#{arg}"
@@ -28,6 +32,4 @@ module Lazy
28
32
  end
29
33
  end
30
34
  end
31
- end
32
-
33
- Object.extend Lazy unless Object.ancestors.include? Lazy
35
+ end
@@ -0,0 +1,294 @@
1
+ module Grit
2
+ # Grit::Process includes logic for executing child processes and
3
+ # reading/writing from their standard input, output, and error streams.
4
+ #
5
+ # Create an run a process to completion:
6
+ #
7
+ # >> process = Grit::Process.new(['git', '--help'])
8
+ #
9
+ # Retrieve stdout or stderr output:
10
+ #
11
+ # >> process.out
12
+ # => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
13
+ # >> process.err
14
+ # => ""
15
+ #
16
+ # Check process exit status information:
17
+ #
18
+ # >> process.status
19
+ # => #<Process::Status: pid=80718,exited(0)>
20
+ #
21
+ # Grit::Process is designed to take all input in a single string and
22
+ # provides all output as single strings. It is therefore not well suited
23
+ # to streaming large quantities of data in and out of commands.
24
+ #
25
+ # Q: Why not use popen3 or hand-roll fork/exec code?
26
+ #
27
+ # - It's more efficient than popen3 and provides meaningful process
28
+ # hierarchies because it performs a single fork/exec. (popen3 double forks
29
+ # to avoid needing to collect the exit status and also calls
30
+ # Process::detach which creates a Ruby Thread!!!!).
31
+ #
32
+ # - It's more portable than hand rolled pipe, fork, exec code because
33
+ # fork(2) and exec(2) aren't available on all platforms. In those cases,
34
+ # Grit::Process falls back to using whatever janky substitutes the platform
35
+ # provides.
36
+ #
37
+ # - It handles all max pipe buffer hang cases, which is non trivial to
38
+ # implement correctly and must be accounted for with either popen3 or
39
+ # hand rolled fork/exec code.
40
+ class Process
41
+ # Create and execute a new process.
42
+ #
43
+ # argv - Array of [command, arg1, ...] strings to use as the new
44
+ # process's argv. When argv is a String, the shell is used
45
+ # to interpret the command.
46
+ # env - The new process's environment variables. This is merged with
47
+ # the current environment as if by ENV.merge(env).
48
+ # options - Additional options:
49
+ # :input => str to write str to the process's stdin.
50
+ # :timeout => int number of seconds before we given up.
51
+ # :max => total number of output bytes
52
+ # A subset of Process:spawn options are also supported on all
53
+ # platforms:
54
+ # :chdir => str to start the process in different working dir.
55
+ #
56
+ # Returns a new Process instance that has already executed to completion.
57
+ # The out, err, and status attributes are immediately available.
58
+ def initialize(argv, env={}, options={})
59
+ @argv = argv
60
+ @env = env
61
+
62
+ @options = options.dup
63
+ @input = @options.delete(:input)
64
+ @timeout = @options.delete(:timeout)
65
+ @max = @options.delete(:max)
66
+ @options.delete(:chdir) if @options[:chdir].nil?
67
+
68
+ exec!
69
+ end
70
+
71
+ # All data written to the child process's stdout stream as a String.
72
+ attr_reader :out
73
+
74
+ # All data written to the child process's stderr stream as a String.
75
+ attr_reader :err
76
+
77
+ # A Process::Status object with information on how the child exited.
78
+ attr_reader :status
79
+
80
+ # Total command execution time (wall-clock time)
81
+ attr_reader :runtime
82
+
83
+ # Determine if the process did exit with a zero exit status.
84
+ def success?
85
+ @status && @status.success?
86
+ end
87
+
88
+ private
89
+ # Execute command, write input, and read output. This is called
90
+ # immediately when a new instance of this object is initialized.
91
+ def exec!
92
+ # when argv is a string, use /bin/sh to interpret command
93
+ argv = @argv
94
+ argv = ['/bin/sh', '-c', argv.to_str] if argv.respond_to?(:to_str)
95
+
96
+ # spawn the process and hook up the pipes
97
+ pid, stdin, stdout, stderr = popen4(@env, *(argv + [@options]))
98
+
99
+ # async read from all streams into buffers
100
+ @out, @err = read_and_write(@input, stdin, stdout, stderr, @timeout, @max)
101
+
102
+ # grab exit status
103
+ @status = waitpid(pid)
104
+ rescue Object => boom
105
+ [stdin, stdout, stderr].each { |fd| fd.close rescue nil }
106
+ if @status.nil?
107
+ ::Process.kill('TERM', pid) rescue nil
108
+ @status = waitpid(pid) rescue nil
109
+ end
110
+ raise
111
+ end
112
+
113
+ # Exception raised when the total number of bytes output on the command's
114
+ # stderr and stdout streams exceeds the maximum output size (:max option).
115
+ class MaximumOutputExceeded < StandardError
116
+ end
117
+
118
+ # Exception raised when timeout is exceeded.
119
+ class TimeoutExceeded < StandardError
120
+ end
121
+
122
+ # Maximum buffer size for reading
123
+ BUFSIZE = (32 * 1024)
124
+
125
+ # Start a select loop writing any input on the child's stdin and reading
126
+ # any output from the child's stdout or stderr.
127
+ #
128
+ # input - String input to write on stdin. May be nil.
129
+ # stdin - The write side IO object for the child's stdin stream.
130
+ # stdout - The read side IO object for the child's stdout stream.
131
+ # stderr - The read side IO object for the child's stderr stream.
132
+ # timeout - An optional Numeric specifying the total number of seconds
133
+ # the read/write operations should occur for.
134
+ #
135
+ # Returns an [out, err] tuple where both elements are strings with all
136
+ # data written to the stdout and stderr streams, respectively.
137
+ # Raises TimeoutExceeded when all data has not been read / written within
138
+ # the duration specified in the timeout argument.
139
+ # Raises MaximumOutputExceeded when the total number of bytes output
140
+ # exceeds the amount specified by the max argument.
141
+ def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil)
142
+ input ||= ''
143
+ max = nil if max && max <= 0
144
+ out, err = '', ''
145
+ offset = 0
146
+
147
+ timeout = nil if timeout && timeout <= 0.0
148
+ @runtime = 0.0
149
+ start = Time.now
150
+
151
+ writers = [stdin]
152
+ readers = [stdout, stderr]
153
+ t = timeout
154
+ while readers.any? || writers.any?
155
+ ready = IO.select(readers, writers, readers + writers, t)
156
+ raise TimeoutExceeded if ready.nil?
157
+
158
+ # write to stdin stream
159
+ ready[1].each do |fd|
160
+ begin
161
+ boom = nil
162
+ size = fd.write_nonblock(input)
163
+ input = input[size, input.size]
164
+ rescue Errno::EPIPE => boom
165
+ rescue Errno::EAGAIN, Errno::EINTR
166
+ end
167
+ if boom || input.size == 0
168
+ stdin.close
169
+ writers.delete(stdin)
170
+ end
171
+ end
172
+
173
+ # read from stdout and stderr streams
174
+ ready[0].each do |fd|
175
+ buf = (fd == stdout) ? out : err
176
+ begin
177
+ buf << fd.readpartial(BUFSIZE)
178
+ rescue Errno::EAGAIN, Errno::EINTR
179
+ rescue EOFError
180
+ readers.delete(fd)
181
+ fd.close
182
+ end
183
+ end
184
+
185
+ # keep tabs on the total amount of time we've spent here
186
+ @runtime = Time.now - start
187
+ if timeout
188
+ t = timeout - @runtime
189
+ raise TimeoutExceeded if t < 0.0
190
+ end
191
+
192
+ # maybe we've hit our max output
193
+ if max && ready[0].any? && (out.size + err.size) > max
194
+ raise MaximumOutputExceeded
195
+ end
196
+ end
197
+
198
+ [out, err]
199
+ end
200
+
201
+ # Spawn a child process, perform IO redirection and environment prep, and
202
+ # return the running process's pid.
203
+ #
204
+ # This method implements a limited subset of Ruby 1.9's Process::spawn.
205
+ # The idea is that we can just use that when available, since most platforms
206
+ # will eventually build in special (and hopefully good) support for it.
207
+ #
208
+ # env - Hash of { name => val } environment variables set in the child
209
+ # process.
210
+ # argv - New process's argv as an Array. When this value is a string,
211
+ # the command may be run through the system /bin/sh or
212
+ # options - Supports a subset of Process::spawn options, including:
213
+ # :chdir => str to change the directory to str in the child
214
+ # FD => :close to close a file descriptor in the child
215
+ # :in => FD to redirect child's stdin to FD
216
+ # :out => FD to redirect child's stdout to FD
217
+ # :err => FD to redirect child's stderr to FD
218
+ #
219
+ # Returns the pid of the new process as an integer. The process exit status
220
+ # must be obtained using Process::waitpid.
221
+ def spawn(env, *argv)
222
+ options = (argv.pop if argv[-1].kind_of?(Hash)) || {}
223
+ fork do
224
+ # { fd => :close } in options means close that fd
225
+ options.each { |k,v| k.close if v == :close && !k.closed? }
226
+
227
+ # reopen stdin, stdout, and stderr on provided fds
228
+ STDIN.reopen(options[:in])
229
+ STDOUT.reopen(options[:out])
230
+ STDERR.reopen(options[:err])
231
+
232
+ # setup child environment
233
+ env.each { |k, v| ENV[k] = v }
234
+
235
+ # { :chdir => '/' } in options means change into that dir
236
+ ::Dir.chdir(options[:chdir]) if options[:chdir]
237
+
238
+ # do the deed
239
+ ::Kernel::exec(*argv)
240
+ exit! 1
241
+ end
242
+ end
243
+
244
+ # Start a process with spawn options and return
245
+ # popen4([env], command, arg1, arg2, [opt])
246
+ #
247
+ # env - The child process's environment as a Hash.
248
+ # command - The command and zero or more arguments.
249
+ # options - An options hash.
250
+ #
251
+ # See Ruby 1.9 IO.popen and Process::spawn docs for more info:
252
+ # http://www.ruby-doc.org/core-1.9/classes/IO.html#M001640
253
+ #
254
+ # Returns a [pid, stdin, stderr, stdout] tuple where pid is the child
255
+ # process's pid, stdin is a writeable IO object, and stdout + stderr are
256
+ # readable IO objects.
257
+ def popen4(*argv)
258
+ # create some pipes (see pipe(2) manual -- the ruby docs suck)
259
+ ird, iwr = IO.pipe
260
+ ord, owr = IO.pipe
261
+ erd, ewr = IO.pipe
262
+
263
+ # spawn the child process with either end of pipes hooked together
264
+ opts =
265
+ ((argv.pop if argv[-1].is_a?(Hash)) || {}).merge(
266
+ # redirect fds # close other sides
267
+ :in => ird, iwr => :close,
268
+ :out => owr, ord => :close,
269
+ :err => ewr, erd => :close
270
+ )
271
+ pid = spawn(*(argv + [opts]))
272
+
273
+ [pid, iwr, ord, erd]
274
+ ensure
275
+ # we're in the parent, close child-side fds
276
+ [ird, owr, ewr].each { |fd| fd.close }
277
+ end
278
+
279
+ # Wait for the child process to exit
280
+ #
281
+ # Returns the Process::Status object obtained by reaping the process.
282
+ def waitpid(pid)
283
+ ::Process::waitpid(pid)
284
+ $?
285
+ end
286
+
287
+ # Use native Process::spawn implementation on Ruby 1.9.
288
+ if ::Process.respond_to?(:spawn)
289
+ def spawn(*argv)
290
+ ::Process.spawn(*argv)
291
+ end
292
+ end
293
+ end
294
+ end
@@ -73,4 +73,6 @@ module Grit
73
73
 
74
74
  class Remote < Ref; end
75
75
 
76
+ class Note < Ref; end
77
+
76
78
  end # Grit
@@ -2,6 +2,9 @@ module Grit
2
2
 
3
3
  class Repo
4
4
  DAEMON_EXPORT_FILE = 'git-daemon-export-ok'
5
+ BATCH_PARSERS = {
6
+ 'commit' => ::Grit::Commit
7
+ }
5
8
 
6
9
  # Public: The String path of the Git repo.
7
10
  attr_accessor :path
@@ -55,7 +58,7 @@ module Grit
55
58
 
56
59
  # Public: Initialize a git repository (create it on the filesystem). By
57
60
  # default, the newly created repository will contain a working directory.
58
- # If you would like to create a bare repo, use Gollum::Repo.init_bare.
61
+ # If you would like to create a bare repo, use Grit::Repo.init_bare.
59
62
  #
60
63
  # path - The String full path to the repo. Traditionally ends with
61
64
  # "/<name>.git".
@@ -96,6 +99,7 @@ module Grit
96
99
  git = Git.new(path)
97
100
  git.fs_mkdir('..')
98
101
  git.init(git_options)
102
+ repo_options = {:is_bare => true}.merge(repo_options)
99
103
  self.new(path, repo_options)
100
104
  end
101
105
 
@@ -155,6 +159,40 @@ module Grit
155
159
  Repo.new(self.path)
156
160
  end
157
161
 
162
+ # Public: Return the full Git objects from the given SHAs. Only Commit
163
+ # objects are parsed for now.
164
+ #
165
+ # *shas - Array of String SHAs.
166
+ #
167
+ # Returns an Array of Grit objects (Grit::Commit).
168
+ def batch(*shas)
169
+ shas.flatten!
170
+ text = git.native(:cat_file, {:batch => true, :input => (shas * "\n")})
171
+ parse_batch(text)
172
+ end
173
+
174
+ # Parses `git cat-file --batch` output, returning an array of Grit objects.
175
+ #
176
+ # text - Raw String output.
177
+ #
178
+ # Returns an Array of Grit objects (Grit::Commit).
179
+ def parse_batch(text)
180
+ io = StringIO.new(text)
181
+ objects = []
182
+ while line = io.gets
183
+ sha, type, size = line.split(" ", 3)
184
+ parser = BATCH_PARSERS[type]
185
+ if type == 'missing' || !parser
186
+ objects << nil
187
+ next
188
+ end
189
+
190
+ object = io.read(size.to_i + 1)
191
+ objects << parser.parse_batch(self, sha, size, object)
192
+ end
193
+ objects
194
+ end
195
+
158
196
  # The project's description. Taken verbatim from GIT_REPO/description
159
197
  #
160
198
  # Returns String
@@ -415,7 +453,8 @@ module Grit
415
453
  else
416
454
  # NO PARENTS:
417
455
  cmd = "-r -t #{commit_sha}"
418
- revs = self.git.method_missing('ls-tree', {:timeout => false}, "-r -t #{commit_sha}").split("\n").map{ |a| a.split("\t").first.split(' ')[2] }
456
+ revs = self.git.native(:ls_tree, {:timeout => false, :r => true, :t => true}, commit_sha).
457
+ split("\n").map{ |a| a.split("\t").first.split(' ')[2] }
419
458
  end
420
459
  revs << self.commit(commit_sha).tree.id
421
460
  Grit.no_quote = false
@@ -461,7 +500,7 @@ module Grit
461
500
  diff = self.git.native('diff', {}, a, b, '--', *paths)
462
501
 
463
502
  if diff =~ /diff --git a/
464
- diff = diff.sub(/.+?(diff --git a)/m, '\1')
503
+ diff = diff.sub(/.*?(diff --git a)/m, '\1')
465
504
  else
466
505
  diff = ''
467
506
  end
@@ -558,11 +597,9 @@ module Grit
558
597
  # Returns Array[String] (pathnames of alternates)
559
598
  def alternates
560
599
  alternates_path = "objects/info/alternates"
561
- if self.git.fs_exist?(alternates_path)
562
- self.git.fs_read(alternates_path).strip.split("\n")
563
- else
564
- []
565
- end
600
+ self.git.fs_read(alternates_path).strip.split("\n")
601
+ rescue Errno::ENOENT
602
+ []
566
603
  end
567
604
 
568
605
  # Sets the alternates
@@ -1,6 +1,8 @@
1
1
  module Grit
2
2
 
3
3
  class Tag < Ref
4
+ extend Lazy
5
+
4
6
  lazy_reader :message
5
7
  lazy_reader :tagger
6
8
  lazy_reader :tag_date
@@ -1,6 +1,8 @@
1
1
  module Grit
2
2
 
3
3
  class Tree
4
+ extend Lazy
5
+
4
6
  lazy_reader :contents
5
7
  attr_reader :id
6
8
  attr_reader :mode
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grit
3
3
  version: !ruby/object:Gem::Version
4
- hash: 3
4
+ hash: 31
5
5
  prerelease: false
6
6
  segments:
7
7
  - 2
8
- - 3
8
+ - 4
9
9
  - 0
10
- version: 2.3.0
10
+ version: 2.4.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Tom Preston-Werner
@@ -16,7 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2010-09-29 00:00:00 -07:00
19
+ date: 2011-01-06 00:00:00 -08:00
20
20
  default_executable:
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency
@@ -105,9 +105,10 @@ files:
105
105
  - lib/grit/git-ruby/repository.rb
106
106
  - lib/grit/git.rb
107
107
  - lib/grit/index.rb
108
+ - lib/grit/jruby.rb
108
109
  - lib/grit/lazy.rb
109
110
  - lib/grit/merge.rb
110
- - lib/grit/open3_detach.rb
111
+ - lib/grit/process.rb
111
112
  - lib/grit/ref.rb
112
113
  - lib/grit/repo.rb
113
114
  - lib/grit/ruby1.9.rb
@@ -1,46 +0,0 @@
1
- module Open3
2
- extend self
3
-
4
- def popen3(*cmd)
5
- pw = IO::pipe # pipe[0] for read, pipe[1] for write
6
- pr = IO::pipe
7
- pe = IO::pipe
8
-
9
- pid = fork{
10
- # child
11
- fork{
12
- # grandchild
13
- pw[1].close
14
- STDIN.reopen(pw[0])
15
- pw[0].close
16
-
17
- pr[0].close
18
- STDOUT.reopen(pr[1])
19
- pr[1].close
20
-
21
- pe[0].close
22
- STDERR.reopen(pe[1])
23
- pe[1].close
24
-
25
- exec(*cmd)
26
- }
27
- exit!(0)
28
- }
29
-
30
- pw[0].close
31
- pr[1].close
32
- pe[1].close
33
- Process.waitpid(pid)
34
- pi = [pw[1], pr[0], pe[0]]
35
- pw[1].sync = true
36
- if defined? yield
37
- begin
38
- return yield(*pi)
39
- ensure
40
- Process.detach(pid) if pid
41
- pi.each { |p| p.close unless p.closed? }
42
- end
43
- end
44
- pi
45
- end
46
- end