git-trip 0.0.3
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.
- data.tar.gz.sig +0 -0
- data/History.txt +21 -0
- data/Manifest.txt +89 -0
- data/README.txt +60 -0
- data/Rakefile +31 -0
- data/bin/git-trip +0 -0
- data/doc/USAGE.txt +54 -0
- data/lib/core_ext/hash.rb +15 -0
- data/lib/git-trip.rb +23 -0
- data/lib/git-trip/errors.rb +22 -0
- data/lib/git-trip/gitter.rb +10 -0
- data/lib/git-trip/gitter/base.rb +18 -0
- data/lib/git-trip/gitter/dir.rb +32 -0
- data/lib/git-trip/gitter/uri.rb +40 -0
- data/lib/git-trip/paint_mode.rb +52 -0
- data/lib/git-trip/painter.rb +167 -0
- data/spec/core_ext/hash_spec.rb +25 -0
- data/spec/git-trip/errors_spec.rb +43 -0
- data/spec/git-trip/gitter/base_spec.rb +15 -0
- data/spec/git-trip/gitter/dir_spec.rb +37 -0
- data/spec/git-trip/gitter/uri_spec.rb +25 -0
- data/spec/git-trip/gitter_spec.rb +11 -0
- data/spec/git-trip/paint_mode_spec.rb +56 -0
- data/spec/git-trip/painter_spec.rb +173 -0
- data/spec/git_trip_spec.rb +23 -0
- data/spec/rcov.opts +1 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +15 -0
- data/tasks/ditz.rake +42 -0
- data/tasks/docs.rake +68 -0
- data/tasks/gittrip.rake +63 -0
- data/tasks/rspec.rake +22 -0
- data/tasks/site.rake +48 -0
- data/tasks/util.rake +44 -0
- data/vendor/grit/History.txt +6 -0
- data/vendor/grit/Manifest.txt +53 -0
- data/vendor/grit/README.txt +213 -0
- data/vendor/grit/Rakefile +29 -0
- data/vendor/grit/grit.gemspec +16 -0
- data/vendor/grit/lib/grit.rb +37 -0
- data/vendor/grit/lib/grit/actor.rb +36 -0
- data/vendor/grit/lib/grit/blob.rb +117 -0
- data/vendor/grit/lib/grit/commit.rb +208 -0
- data/vendor/grit/lib/grit/config.rb +44 -0
- data/vendor/grit/lib/grit/diff.rb +70 -0
- data/vendor/grit/lib/grit/errors.rb +7 -0
- data/vendor/grit/lib/grit/git.rb +116 -0
- data/vendor/grit/lib/grit/index.rb +77 -0
- data/vendor/grit/lib/grit/lazy.rb +31 -0
- data/vendor/grit/lib/grit/ref.rb +110 -0
- data/vendor/grit/lib/grit/repo.rb +318 -0
- data/vendor/grit/lib/grit/tree.rb +99 -0
- data/vendor/grit/test/fixtures/blame +131 -0
- data/vendor/grit/test/fixtures/cat_file_blob +1 -0
- data/vendor/grit/test/fixtures/cat_file_blob_size +1 -0
- data/vendor/grit/test/fixtures/diff_2 +54 -0
- data/vendor/grit/test/fixtures/diff_2f +19 -0
- data/vendor/grit/test/fixtures/diff_f +15 -0
- data/vendor/grit/test/fixtures/diff_i +201 -0
- data/vendor/grit/test/fixtures/diff_mode_only +1152 -0
- data/vendor/grit/test/fixtures/diff_new_mode +17 -0
- data/vendor/grit/test/fixtures/diff_p +610 -0
- data/vendor/grit/test/fixtures/for_each_ref +0 -0
- data/vendor/grit/test/fixtures/for_each_ref_remotes +0 -0
- data/vendor/grit/test/fixtures/for_each_ref_tags +0 -0
- data/vendor/grit/test/fixtures/ls_tree_a +7 -0
- data/vendor/grit/test/fixtures/ls_tree_b +2 -0
- data/vendor/grit/test/fixtures/ls_tree_commit +3 -0
- data/vendor/grit/test/fixtures/rev_list +26 -0
- data/vendor/grit/test/fixtures/rev_list_count +655 -0
- data/vendor/grit/test/fixtures/rev_list_single +7 -0
- data/vendor/grit/test/fixtures/rev_parse +1 -0
- data/vendor/grit/test/fixtures/show_empty_commit +6 -0
- data/vendor/grit/test/fixtures/simple_config +2 -0
- data/vendor/grit/test/helper.rb +17 -0
- data/vendor/grit/test/profile.rb +21 -0
- data/vendor/grit/test/suite.rb +6 -0
- data/vendor/grit/test/test_actor.rb +35 -0
- data/vendor/grit/test/test_blob.rb +74 -0
- data/vendor/grit/test/test_commit.rb +182 -0
- data/vendor/grit/test/test_config.rb +58 -0
- data/vendor/grit/test/test_diff.rb +18 -0
- data/vendor/grit/test/test_git.rb +52 -0
- data/vendor/grit/test/test_head.rb +22 -0
- data/vendor/grit/test/test_real.rb +19 -0
- data/vendor/grit/test/test_reality.rb +17 -0
- data/vendor/grit/test/test_remote.rb +15 -0
- data/vendor/grit/test/test_repo.rb +278 -0
- data/vendor/grit/test/test_tag.rb +29 -0
- data/vendor/grit/test/test_tree.rb +91 -0
- metadata +179 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module Grit
|
2
|
+
|
3
|
+
class Config
|
4
|
+
def initialize(repo)
|
5
|
+
@repo = repo
|
6
|
+
end
|
7
|
+
|
8
|
+
def []=(key, value)
|
9
|
+
@repo.git.config({}, key, value)
|
10
|
+
@data = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
data[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(key, default = nil)
|
18
|
+
data[key] || default || raise(IndexError.new("key not found"))
|
19
|
+
end
|
20
|
+
|
21
|
+
def keys
|
22
|
+
data.keys
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
def data
|
27
|
+
@data ||= load_config
|
28
|
+
end
|
29
|
+
|
30
|
+
def load_config
|
31
|
+
hash = {}
|
32
|
+
config_lines.map do |line|
|
33
|
+
key, value = line.split(/=/, 2)
|
34
|
+
hash[key] = value
|
35
|
+
end
|
36
|
+
hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def config_lines
|
40
|
+
@repo.git.config(:list => true).split(/\n/)
|
41
|
+
end
|
42
|
+
end # Config
|
43
|
+
|
44
|
+
end # Grit
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Grit
|
2
|
+
|
3
|
+
class Diff
|
4
|
+
attr_reader :a_path, :b_path
|
5
|
+
attr_reader :a_commit, :b_commit
|
6
|
+
attr_reader :a_mode, :b_mode
|
7
|
+
attr_reader :new_file, :deleted_file
|
8
|
+
attr_reader :diff
|
9
|
+
|
10
|
+
def initialize(repo, a_path, b_path, a_commit, b_commit, a_mode, b_mode, new_file, deleted_file, diff)
|
11
|
+
@repo = repo
|
12
|
+
@a_path = a_path
|
13
|
+
@b_path = b_path
|
14
|
+
@a_commit = a_commit =~ /^0{40}$/ ? nil : Commit.create(repo, :id => a_commit)
|
15
|
+
@b_commit = b_commit =~ /^0{40}$/ ? nil : Commit.create(repo, :id => b_commit)
|
16
|
+
@a_mode = a_mode
|
17
|
+
@b_mode = b_mode
|
18
|
+
@new_file = new_file
|
19
|
+
@deleted_file = deleted_file
|
20
|
+
@diff = diff
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.list_from_string(repo, text)
|
24
|
+
lines = text.split("\n")
|
25
|
+
|
26
|
+
diffs = []
|
27
|
+
|
28
|
+
while !lines.empty?
|
29
|
+
m, a_path, b_path = *lines.shift.match(%r{^diff --git a/(.+?) b/(.+)$})
|
30
|
+
|
31
|
+
if lines.first =~ /^old mode/
|
32
|
+
m, a_mode = *lines.shift.match(/^old mode (\d+)/)
|
33
|
+
m, b_mode = *lines.shift.match(/^new mode (\d+)/)
|
34
|
+
end
|
35
|
+
|
36
|
+
if lines.empty? || lines.first =~ /^diff --git/
|
37
|
+
diffs << Diff.new(repo, a_path, b_path, nil, nil, a_mode, b_mode, false, false, nil)
|
38
|
+
next
|
39
|
+
end
|
40
|
+
|
41
|
+
new_file = false
|
42
|
+
deleted_file = false
|
43
|
+
|
44
|
+
if lines.first =~ /^new file/
|
45
|
+
m, b_mode = lines.shift.match(/^new file mode (.+)$/)
|
46
|
+
a_mode = nil
|
47
|
+
new_file = true
|
48
|
+
elsif lines.first =~ /^deleted file/
|
49
|
+
m, a_mode = lines.shift.match(/^deleted file mode (.+)$/)
|
50
|
+
b_mode = nil
|
51
|
+
deleted_file = true
|
52
|
+
end
|
53
|
+
|
54
|
+
m, a_commit, b_commit, b_mode = *lines.shift.match(%r{^index ([0-9A-Fa-f]+)\.\.([0-9A-Fa-f]+) ?(.+)?$})
|
55
|
+
b_mode.strip! if b_mode
|
56
|
+
|
57
|
+
diff_lines = []
|
58
|
+
while lines.first && lines.first !~ /^diff/
|
59
|
+
diff_lines << lines.shift
|
60
|
+
end
|
61
|
+
diff = diff_lines.join("\n")
|
62
|
+
|
63
|
+
diffs << Diff.new(repo, a_path, b_path, a_commit, b_commit, a_mode, b_mode, new_file, deleted_file, diff)
|
64
|
+
end
|
65
|
+
|
66
|
+
diffs
|
67
|
+
end
|
68
|
+
end # Diff
|
69
|
+
|
70
|
+
end # Grit
|
@@ -0,0 +1,116 @@
|
|
1
|
+
trap("CHLD") do
|
2
|
+
begin
|
3
|
+
Process.wait(-1, Process::WNOHANG)
|
4
|
+
rescue Object
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module Grit
|
9
|
+
|
10
|
+
class Git
|
11
|
+
class GitTimeout < RuntimeError
|
12
|
+
attr_reader :command, :bytes_read
|
13
|
+
|
14
|
+
def initialize(command = nil, bytes_read = nil)
|
15
|
+
@command = command
|
16
|
+
@bytes_read = bytes_read
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
undef_method :clone
|
21
|
+
|
22
|
+
class << self
|
23
|
+
attr_accessor :git_binary, :git_timeout
|
24
|
+
end
|
25
|
+
|
26
|
+
self.git_binary = "/usr/bin/env git"
|
27
|
+
self.git_timeout = 5
|
28
|
+
|
29
|
+
attr_accessor :git_dir, :bytes_read
|
30
|
+
|
31
|
+
def initialize(git_dir)
|
32
|
+
self.git_dir = git_dir
|
33
|
+
self.bytes_read = 0
|
34
|
+
end
|
35
|
+
|
36
|
+
# Run the given git command with the specified arguments and return
|
37
|
+
# the result as a String
|
38
|
+
# +cmd+ is the command
|
39
|
+
# +options+ is a hash of Ruby style options
|
40
|
+
# +args+ is the list of arguments (to be joined by spaces)
|
41
|
+
#
|
42
|
+
# Examples
|
43
|
+
# git.rev_list({:max_count => 10, :header => true}, "master")
|
44
|
+
#
|
45
|
+
# Returns String
|
46
|
+
def method_missing(cmd, options = {}, *args)
|
47
|
+
run('', cmd, '', options, args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def run(prefix, cmd, postfix, options, args)
|
51
|
+
timeout = options.delete(:timeout)
|
52
|
+
timeout = true if timeout.nil?
|
53
|
+
|
54
|
+
opt_args = transform_options(options)
|
55
|
+
ext_args = args.map { |a| a == '--' ? a : "'#{a}'" }
|
56
|
+
|
57
|
+
call = "#{prefix}#{Git.git_binary} --git-dir='#{self.git_dir}' #{cmd.to_s.gsub(/_/, '-')} #{(opt_args + ext_args).join(' ')}#{postfix}"
|
58
|
+
puts call if Grit.debug
|
59
|
+
response = timeout ? sh(call) : wild_sh(call)
|
60
|
+
puts response if Grit.debug
|
61
|
+
response
|
62
|
+
end
|
63
|
+
|
64
|
+
def sh(command)
|
65
|
+
pid, _, io, _ = Open4.popen4(command)
|
66
|
+
ret = Timeout.timeout(self.class.git_timeout) { io.read }
|
67
|
+
@bytes_read += ret.size
|
68
|
+
|
69
|
+
if @bytes_read > 5242880 # 5.megabytes
|
70
|
+
bytes = @bytes_read
|
71
|
+
@bytes_read = 0
|
72
|
+
raise GitTimeout.new(command, bytes)
|
73
|
+
end
|
74
|
+
|
75
|
+
ret
|
76
|
+
rescue Object => e
|
77
|
+
Process.kill('KILL', pid) rescue nil
|
78
|
+
bytes = @bytes_read
|
79
|
+
@bytes_read = 0
|
80
|
+
raise GitTimeout.new(command, bytes)
|
81
|
+
end
|
82
|
+
|
83
|
+
def wild_sh(command)
|
84
|
+
pid, _, io, _ = Open4.popen4(command)
|
85
|
+
io.read
|
86
|
+
end
|
87
|
+
|
88
|
+
# Transform Ruby style options into git command line options
|
89
|
+
# +options+ is a hash of Ruby style options
|
90
|
+
#
|
91
|
+
# Returns String[]
|
92
|
+
# e.g. ["--max-count=10", "--header"]
|
93
|
+
def transform_options(options)
|
94
|
+
args = []
|
95
|
+
options.keys.each do |opt|
|
96
|
+
if opt.to_s.size == 1
|
97
|
+
if options[opt] == true
|
98
|
+
args << "-#{opt}"
|
99
|
+
else
|
100
|
+
val = options.delete(opt)
|
101
|
+
args << "-#{opt.to_s} '#{val}'"
|
102
|
+
end
|
103
|
+
else
|
104
|
+
if options[opt] == true
|
105
|
+
args << "--#{opt.to_s.gsub(/_/, '-')}"
|
106
|
+
else
|
107
|
+
val = options.delete(opt)
|
108
|
+
args << "--#{opt.to_s.gsub(/_/, '-')}='#{val}'"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
args
|
113
|
+
end
|
114
|
+
end # Git
|
115
|
+
|
116
|
+
end # Grit
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Grit
|
2
|
+
|
3
|
+
class Index
|
4
|
+
attr_accessor :repo, :tree
|
5
|
+
|
6
|
+
def initialize(repo)
|
7
|
+
self.repo = repo
|
8
|
+
self.tree = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a file to the index
|
12
|
+
# +path+ is the path (including filename)
|
13
|
+
# +data+ is the binary contents of the file
|
14
|
+
#
|
15
|
+
# Returns nothing
|
16
|
+
def add(file_path, data)
|
17
|
+
path = file_path.split('/')
|
18
|
+
filename = path.pop
|
19
|
+
|
20
|
+
current = self.tree
|
21
|
+
|
22
|
+
path.each do |dir|
|
23
|
+
current[dir] ||= {}
|
24
|
+
node = current[dir]
|
25
|
+
current = node
|
26
|
+
end
|
27
|
+
|
28
|
+
current[filename] = data
|
29
|
+
end
|
30
|
+
|
31
|
+
# Commit the contents of the index
|
32
|
+
# +message+ is the commit message
|
33
|
+
#
|
34
|
+
# Returns a String of the SHA1 of the commit
|
35
|
+
def commit(message)
|
36
|
+
tree_sha1 = write_tree(self.tree)
|
37
|
+
|
38
|
+
message = message.gsub("'", "\\'")
|
39
|
+
commit_sha1 = self.repo.git.run("echo '#{message}' | ", :commit_tree, '', {}, [tree_sha1])
|
40
|
+
|
41
|
+
# self.repo.git.update_ref({}, 'HEAD', commit_sha1)
|
42
|
+
File.open(File.join(self.repo.path, 'refs', 'heads', 'master'), 'w') do |f|
|
43
|
+
f.write(commit_sha1)
|
44
|
+
end
|
45
|
+
|
46
|
+
commit_sha1
|
47
|
+
end
|
48
|
+
|
49
|
+
# Recursively write a tree to the index
|
50
|
+
# +tree+ is the tree
|
51
|
+
#
|
52
|
+
# Returns the SHA1 String of the tree
|
53
|
+
def write_tree(tree)
|
54
|
+
lstree = []
|
55
|
+
tree.each do |k, v|
|
56
|
+
case v
|
57
|
+
when String:
|
58
|
+
lstree << "100644 blob #{write_blob(v)}\t#{k}"
|
59
|
+
when Hash:
|
60
|
+
lstree << "040000 tree #{write_tree(v)}\t#{k}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
lstree_string = lstree.join("\n").gsub("'", "\\'")
|
65
|
+
self.repo.git.run("echo '#{lstree_string}' | ", :mktree, '', {}, []).chomp
|
66
|
+
end
|
67
|
+
|
68
|
+
# Write the blob to the index
|
69
|
+
# +data+ is the data to write
|
70
|
+
#
|
71
|
+
# Returns the SHA1 String of the blob
|
72
|
+
def write_blob(data)
|
73
|
+
self.repo.git.run("echo '#{data}' | ", :hash_object, '', {:w => true, :stdin => true}, []).chomp
|
74
|
+
end
|
75
|
+
end # Index
|
76
|
+
|
77
|
+
end # Grit
|
@@ -0,0 +1,31 @@
|
|
1
|
+
##
|
2
|
+
# Allows attributes to be declared as lazy, meaning that they won't be
|
3
|
+
# computed until they are asked for.
|
4
|
+
#
|
5
|
+
# Works by delegating each lazy_reader to a cached lazy_source method.
|
6
|
+
#
|
7
|
+
# class Person
|
8
|
+
# lazy_reader :eyes
|
9
|
+
#
|
10
|
+
# def lazy_source
|
11
|
+
# OpenStruct.new(:eyes => 2)
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# >> Person.new.eyes
|
16
|
+
# => 2
|
17
|
+
#
|
18
|
+
module Lazy
|
19
|
+
def lazy_reader(*args)
|
20
|
+
args.each do |arg|
|
21
|
+
ivar = "@#{arg}"
|
22
|
+
define_method(arg) do
|
23
|
+
val = instance_variable_get(ivar)
|
24
|
+
return val if val
|
25
|
+
instance_variable_set(ivar, (@lazy_source ||= lazy_source).send(arg))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Object.extend Lazy unless Object.ancestors.include? Lazy
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module Grit
|
2
|
+
|
3
|
+
class Ref
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# Find all Refs
|
8
|
+
# +repo+ is the Repo
|
9
|
+
# +options+ is a Hash of options
|
10
|
+
#
|
11
|
+
# Returns Grit::Ref[] (baked)
|
12
|
+
def find_all(repo, options = {})
|
13
|
+
default_options = {:sort => "committerdate",
|
14
|
+
:format => "%(refname)%00%(objectname)"}
|
15
|
+
|
16
|
+
actual_options = default_options.merge(options)
|
17
|
+
|
18
|
+
output = repo.git.for_each_ref(actual_options, prefix)
|
19
|
+
|
20
|
+
self.list_from_string(repo, output)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Parse out ref information into an array of baked refs objects
|
24
|
+
# +repo+ is the Repo
|
25
|
+
# +text+ is the text output from the git command
|
26
|
+
#
|
27
|
+
# Returns Grit::Ref[] (baked)
|
28
|
+
def list_from_string(repo, text)
|
29
|
+
refs = []
|
30
|
+
|
31
|
+
text.split("\n").each do |line|
|
32
|
+
refs << self.from_string(repo, line)
|
33
|
+
end
|
34
|
+
|
35
|
+
refs.sort { | x, y | x.name <=> y.name }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a new Ref instance from the given string.
|
39
|
+
# +repo+ is the Repo
|
40
|
+
# +line+ is the formatted head information
|
41
|
+
#
|
42
|
+
# Format
|
43
|
+
# name: [a-zA-Z_/]+
|
44
|
+
# <null byte>
|
45
|
+
# id: [0-9A-Fa-f]{40}
|
46
|
+
#
|
47
|
+
# Returns Grit::Ref (baked)
|
48
|
+
def from_string(repo, line)
|
49
|
+
full_name, id = line.split("\0")
|
50
|
+
name = full_name.sub("#{prefix}/", '')
|
51
|
+
commit = Commit.create(repo, :id => id)
|
52
|
+
self.new(name, commit)
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def prefix
|
58
|
+
"refs/#{name.to_s.gsub(/^.*::/, '').downcase}s"
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :name
|
64
|
+
attr_reader :commit
|
65
|
+
|
66
|
+
# Instantiate a new Head
|
67
|
+
# +name+ is the name of the head
|
68
|
+
# +commit+ is the Commit that the head points to
|
69
|
+
#
|
70
|
+
# Returns Grit::Head (baked)
|
71
|
+
def initialize(name, commit)
|
72
|
+
@name = name
|
73
|
+
@commit = commit
|
74
|
+
end
|
75
|
+
|
76
|
+
# Pretty object inspection
|
77
|
+
def inspect
|
78
|
+
%Q{#<#{self.class.name} "#{@name}">}
|
79
|
+
end
|
80
|
+
end # Ref
|
81
|
+
|
82
|
+
# A Head is a named reference to a Commit. Every Head instance contains a name
|
83
|
+
# and a Commit object.
|
84
|
+
#
|
85
|
+
# r = Grit::Repo.new("/path/to/repo")
|
86
|
+
# h = r.heads.first
|
87
|
+
# h.name # => "master"
|
88
|
+
# h.commit # => #<Grit::Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455">
|
89
|
+
# h.commit.id # => "1c09f116cbc2cb4100fb6935bb162daa4723f455"
|
90
|
+
class Head < Ref
|
91
|
+
|
92
|
+
# Get the HEAD revision of the repo.
|
93
|
+
# +repo+ is the Repo
|
94
|
+
# +options+ is a Hash of options
|
95
|
+
#
|
96
|
+
# Returns Grit::Head (baked)
|
97
|
+
def self.current(repo, options = {})
|
98
|
+
head = File.open(File.join(repo.path, 'HEAD')).read.chomp
|
99
|
+
if /ref: refs\/heads\/(.*)/.match(head)
|
100
|
+
self.new($1, repo.git.rev_parse(options, 'HEAD'))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end # Head
|
105
|
+
|
106
|
+
class Tag < Ref ; end
|
107
|
+
|
108
|
+
class Remote < Ref ; end
|
109
|
+
|
110
|
+
end # Grit
|