georgi-git_store 0.2.4 → 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/README.md +30 -112
- data/git_store.gemspec +5 -1
- data/lib/git_store.rb +137 -118
- data/lib/git_store/blob.rb +9 -49
- data/lib/git_store/commit.rb +66 -0
- data/lib/git_store/diff.rb +76 -0
- data/lib/git_store/handlers.rb +4 -25
- data/lib/git_store/tree.rb +89 -117
- data/test/benchmark.rb +2 -2
- data/test/commit_spec.rb +82 -0
- data/test/git_store_spec.rb +170 -141
- data/test/tree_spec.rb +92 -0
- metadata +5 -1
data/lib/git_store/blob.rb
CHANGED
@@ -4,60 +4,20 @@ class GitStore
|
|
4
4
|
# deserialized data object.
|
5
5
|
class Blob
|
6
6
|
|
7
|
-
attr_accessor :store, :id, :
|
7
|
+
attr_accessor :store, :id, :data, :mode, :object
|
8
8
|
|
9
|
-
# Initialize a Blob
|
10
|
-
def initialize(store)
|
9
|
+
# Initialize a Blob
|
10
|
+
def initialize(store, id = nil, data = nil)
|
11
11
|
@store = store
|
12
|
-
@
|
13
|
-
|
14
|
-
|
15
|
-
# Set all attributes at once.
|
16
|
-
def set(id, mode = nil, path = nil, data = nil, object = nil)
|
17
|
-
@id, @mode, @path, @data, @object = id, mode, path, data, object
|
18
|
-
end
|
19
|
-
|
20
|
-
# Returns the extension of the filename.
|
21
|
-
def extname
|
22
|
-
File.extname(path)[1..-1]
|
23
|
-
end
|
24
|
-
|
25
|
-
# Returns the handler for serializing the blob data.
|
26
|
-
def handler
|
27
|
-
Handler[extname]
|
28
|
-
end
|
29
|
-
|
30
|
-
# Returns true if data is new or hash value is different from current id.
|
31
|
-
def modified?
|
32
|
-
id.nil? || @modified
|
33
|
-
end
|
34
|
-
|
35
|
-
# Returns the data object.
|
36
|
-
def object
|
37
|
-
@object ||= handler.read(path, data)
|
38
|
-
end
|
39
|
-
|
40
|
-
# Set the data object.
|
41
|
-
def object=(value)
|
42
|
-
@modified = true
|
43
|
-
@object = value
|
44
|
-
@data = handler.respond_to?(:write) ? handler.write(path, value) : value
|
45
|
-
end
|
46
|
-
|
47
|
-
def load_from_disk
|
48
|
-
@object = nil
|
49
|
-
@data = open("#{store.path}/#{path}", 'rb') { |f| f.read }
|
12
|
+
@id = id || store.id_for('blob', data)
|
13
|
+
@data = data
|
14
|
+
@mode = "100644"
|
50
15
|
end
|
51
16
|
|
52
17
|
# Write the data to the git object store
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
@id = store.put_object(data, 'blob')
|
57
|
-
else
|
58
|
-
@id
|
59
|
-
end
|
60
|
-
end
|
18
|
+
def write
|
19
|
+
@id = store.put_object('blob', data)
|
20
|
+
end
|
61
21
|
|
62
22
|
end
|
63
23
|
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class GitStore
|
2
|
+
|
3
|
+
class Commit
|
4
|
+
attr_accessor :store, :id, :data, :author, :committer, :tree, :parent, :message, :headers
|
5
|
+
attr_reader :author_name, :author_email, :author_time
|
6
|
+
attr_reader :committer_name, :committer_email, :committer_time
|
7
|
+
|
8
|
+
def initialize(store, id = nil, data = nil)
|
9
|
+
@store = store
|
10
|
+
@id = id
|
11
|
+
@parent = []
|
12
|
+
|
13
|
+
parse(data) if data
|
14
|
+
|
15
|
+
@author_name, @author_email, @author_time = parse_user(author) if author
|
16
|
+
@committer_name, @commiter_email, @committer_time = parse_user(committer) if committer
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_user(user)
|
20
|
+
if match = user.match(/(.*)<(.*)> (\d+) ([+-]\d+)/)
|
21
|
+
[ match[1].chomp,
|
22
|
+
match[2].chomp,
|
23
|
+
Time.at(match[3].to_i)]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse(data)
|
28
|
+
headers, @message = data.split(/\n\n/, 2)
|
29
|
+
|
30
|
+
headers.split(/\n/).each do |header|
|
31
|
+
key, value = header.split(/ /, 2)
|
32
|
+
if key == 'parent'
|
33
|
+
@parent << value
|
34
|
+
else
|
35
|
+
instance_variable_set "@#{key}", value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def diff(commit, path = nil)
|
43
|
+
commit = commit.id if Commit === commit
|
44
|
+
Diff.exec(store, "git diff --full-index #{commit} #{id} -- #{path}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def diffs(path = nil)
|
48
|
+
diff(parent.first, path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def write
|
52
|
+
@id = store.put_object('commit', dump)
|
53
|
+
end
|
54
|
+
|
55
|
+
def dump
|
56
|
+
[ "tree #@tree",
|
57
|
+
@parent.map { |parent| "parent #{parent}" },
|
58
|
+
"author #@author",
|
59
|
+
"committer #@committer",
|
60
|
+
'',
|
61
|
+
@message ].flatten.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class GitStore
|
2
|
+
|
3
|
+
# adapted from Grit
|
4
|
+
class Diff
|
5
|
+
attr_reader :store
|
6
|
+
attr_reader :a_path, :b_path
|
7
|
+
attr_reader :a_blob, :b_blob
|
8
|
+
attr_reader :a_mode, :b_mode
|
9
|
+
attr_reader :new_file, :deleted_file
|
10
|
+
attr_reader :diff
|
11
|
+
|
12
|
+
def initialize(store, a_path, b_path, a_blob, b_blob, a_mode, b_mode, new_file, deleted_file, diff)
|
13
|
+
@store = store
|
14
|
+
@a_path = a_path
|
15
|
+
@b_path = b_path
|
16
|
+
@a_blob = a_blob =~ /^0{40}$/ ? nil : store.get(a_blob)
|
17
|
+
@b_blob = b_blob =~ /^0{40}$/ ? nil : store.get(b_blob)
|
18
|
+
@a_mode = a_mode
|
19
|
+
@b_mode = b_mode
|
20
|
+
@new_file = new_file
|
21
|
+
@deleted_file = deleted_file
|
22
|
+
@diff = diff
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.exec(store, cmd)
|
26
|
+
list(store, IO.popen(cmd) { |io| io.read })
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.list(store, text)
|
30
|
+
lines = text.split("\n")
|
31
|
+
|
32
|
+
diffs = []
|
33
|
+
|
34
|
+
while !lines.empty?
|
35
|
+
m, a_path, b_path = *lines.shift.match(%r{^diff --git a/(.+?) b/(.+)$})
|
36
|
+
|
37
|
+
if lines.first =~ /^old mode/
|
38
|
+
m, a_mode = *lines.shift.match(/^old mode (\d+)/)
|
39
|
+
m, b_mode = *lines.shift.match(/^new mode (\d+)/)
|
40
|
+
end
|
41
|
+
|
42
|
+
if lines.empty? || lines.first =~ /^diff --git/
|
43
|
+
diffs << Diff.new(store, a_path, b_path, nil, nil, a_mode, b_mode, false, false, nil)
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
new_file = false
|
48
|
+
deleted_file = false
|
49
|
+
|
50
|
+
if lines.first =~ /^new file/
|
51
|
+
m, b_mode = lines.shift.match(/^new file mode (.+)$/)
|
52
|
+
a_mode = nil
|
53
|
+
new_file = true
|
54
|
+
elsif lines.first =~ /^deleted file/
|
55
|
+
m, a_mode = lines.shift.match(/^deleted file mode (.+)$/)
|
56
|
+
b_mode = nil
|
57
|
+
deleted_file = true
|
58
|
+
end
|
59
|
+
|
60
|
+
m, a_blob, b_blob, b_mode = *lines.shift.match(%r{^index ([0-9A-Fa-f]+)\.\.([0-9A-Fa-f]+) ?(.+)?$})
|
61
|
+
b_mode.strip! if b_mode
|
62
|
+
|
63
|
+
diff_lines = []
|
64
|
+
while lines.first && lines.first !~ /^diff/
|
65
|
+
diff_lines << lines.shift
|
66
|
+
end
|
67
|
+
diff = diff_lines.join("\n")
|
68
|
+
|
69
|
+
diffs << Diff.new(store, a_path, b_path, a_blob, b_blob, a_mode, b_mode, new_file, deleted_file, diff)
|
70
|
+
end
|
71
|
+
|
72
|
+
diffs
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
data/lib/git_store/handlers.rb
CHANGED
@@ -15,43 +15,22 @@ end
|
|
15
15
|
class GitStore
|
16
16
|
|
17
17
|
class DefaultHandler
|
18
|
-
def read(
|
18
|
+
def read(data)
|
19
19
|
data
|
20
20
|
end
|
21
21
|
|
22
|
-
def write(
|
22
|
+
def write(data)
|
23
23
|
data.to_s
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
27
|
class YAMLHandler
|
28
|
-
def read(
|
28
|
+
def read(data)
|
29
29
|
YAML.load(data)
|
30
30
|
end
|
31
31
|
|
32
|
-
def write(
|
32
|
+
def write(data)
|
33
33
|
data.to_yaml
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
class RubyHandler
|
38
|
-
def read(path, data)
|
39
|
-
Object.module_eval(data)
|
40
34
|
end
|
41
35
|
end
|
42
|
-
|
43
|
-
class ERBHandler
|
44
|
-
def read(path, data)
|
45
|
-
ERB.new(data)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
Handler = {
|
50
|
-
'yml' => YAMLHandler.new,
|
51
|
-
'rhtml' => ERBHandler.new,
|
52
|
-
'rxml' => ERBHandler.new,
|
53
|
-
'rb' => RubyHandler.new
|
54
|
-
}
|
55
|
-
|
56
|
-
Handler.default = DefaultHandler.new
|
57
36
|
end
|
data/lib/git_store/tree.rb
CHANGED
@@ -1,42 +1,23 @@
|
|
1
1
|
class GitStore
|
2
2
|
|
3
3
|
class Tree
|
4
|
-
TYPE_CLASS = {
|
5
|
-
'tree' => Tree,
|
6
|
-
'blob' => Blob
|
7
|
-
}
|
8
|
-
|
9
4
|
include Enumerable
|
10
5
|
|
11
|
-
attr_reader :store
|
12
|
-
attr_accessor :id, :
|
6
|
+
attr_reader :store, :table
|
7
|
+
attr_accessor :id, :data, :mode
|
13
8
|
|
14
|
-
# Initialize a tree
|
15
|
-
def initialize(store)
|
16
|
-
@store = store
|
17
|
-
@
|
18
|
-
@path = ''
|
9
|
+
# Initialize a tree
|
10
|
+
def initialize(store, id = nil, data = nil)
|
11
|
+
@store = store
|
12
|
+
@id = id
|
19
13
|
@table = {}
|
20
|
-
|
21
|
-
|
22
|
-
# Set all attributes at once.
|
23
|
-
def set(id, mode = '040000', path = nil, data = nil)
|
24
|
-
@id, @mode, @path, @data = id, mode, path, data
|
25
|
-
end
|
26
|
-
|
27
|
-
# Does this tree exist in the repository?
|
28
|
-
def created?
|
29
|
-
not @id.nil?
|
14
|
+
@mode = "040000"
|
15
|
+
parse(data) if data
|
30
16
|
end
|
31
17
|
|
32
18
|
# Has this tree been modified?
|
33
19
|
def modified?
|
34
|
-
@modified
|
35
|
-
end
|
36
|
-
|
37
|
-
# Path of a child element with specified name.
|
38
|
-
def child_path(name)
|
39
|
-
path.empty? ? name : "#{path}/#{name}"
|
20
|
+
@modified or @table.values.any? { |entry| Tree === entry and entry.modified? }
|
40
21
|
end
|
41
22
|
|
42
23
|
# Find or create a subtree with specified name.
|
@@ -44,157 +25,148 @@ class GitStore
|
|
44
25
|
get(name) or put(name, Tree.new(store))
|
45
26
|
end
|
46
27
|
|
47
|
-
# Load this tree from a real directory instead of a repository.
|
48
|
-
def load_from_disk
|
49
|
-
dir = File.join(store.path, self.path)
|
50
|
-
entries = Dir.entries(dir) - ['.', '..']
|
51
|
-
|
52
|
-
@table = entries.inject({}) do |hash, name|
|
53
|
-
if name[-1, 1] != '~' && name[0, 1] != '.'
|
54
|
-
path = "#{dir}/#{name}"
|
55
|
-
stat = File.stat(path)
|
56
|
-
mode = '%o' % stat.mode
|
57
|
-
klass = stat.directory? ? Tree : Blob
|
58
|
-
|
59
|
-
child = table[name] ||= klass.new(store)
|
60
|
-
child.set(nil, mode, child_path(name), data)
|
61
|
-
child.load_from_disk
|
62
|
-
|
63
|
-
hash[name] = child
|
64
|
-
end
|
65
|
-
hash
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
28
|
# Read the contents of a raw git object.
|
70
|
-
|
71
|
-
|
72
|
-
def read_contents(data)
|
73
|
-
contents = []
|
29
|
+
def parse(data)
|
30
|
+
@table.clear
|
74
31
|
|
75
32
|
while data.size > 0
|
76
33
|
mode, data = data.split(" ", 2)
|
77
34
|
name, data = data.split("\0", 2)
|
78
35
|
id = data.slice!(0, 20).unpack("H*").first
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
contents
|
83
|
-
end
|
84
|
-
|
85
|
-
# Load this tree from a git repository.
|
86
|
-
def load_from_store
|
87
|
-
@table = read_contents(data).inject({}) do |hash, (mode, name, id)|
|
88
|
-
content, type = store.get_object(id)
|
89
|
-
|
90
|
-
child = table[name] || TYPE_CLASS[type].new(store)
|
91
|
-
child.set(id, mode, child_path(name), content)
|
92
|
-
child.load_from_store if Tree === child
|
93
|
-
|
94
|
-
hash[name] = child
|
95
|
-
hash
|
36
|
+
|
37
|
+
@table[name] = store.get(id)
|
96
38
|
end
|
97
39
|
end
|
98
|
-
|
40
|
+
|
99
41
|
# Write this tree back to the git repository.
|
100
42
|
#
|
101
43
|
# Returns the object id of the tree.
|
102
|
-
def
|
44
|
+
def write
|
103
45
|
return id if not modified?
|
104
46
|
|
105
|
-
contents = table.map do |name, entry|
|
106
|
-
entry.
|
107
|
-
"%s %s\0%s" % [entry.mode, name, [entry.id].pack("H*")]
|
47
|
+
contents = @table.map do |name, entry|
|
48
|
+
"#{ entry.mode } #{ name }\0#{ [entry.write].pack("H*") }"
|
108
49
|
end
|
109
50
|
|
110
51
|
@modified = false
|
111
|
-
@id = store.put_object(contents.join
|
52
|
+
@id = store.put_object('tree', contents.join)
|
112
53
|
end
|
113
|
-
|
54
|
+
|
114
55
|
# Read entry with specified name.
|
115
56
|
def get(name)
|
116
|
-
|
117
|
-
entry = table[name]
|
57
|
+
entry = @table[name]
|
118
58
|
|
119
59
|
case entry
|
120
|
-
when Blob
|
121
|
-
|
60
|
+
when Blob
|
61
|
+
entry.object ||= handler_for(name).read(entry.data)
|
62
|
+
|
63
|
+
when Tree
|
64
|
+
entry
|
122
65
|
end
|
123
66
|
end
|
124
67
|
|
68
|
+
def handler_for(name)
|
69
|
+
store.handler_for(name)
|
70
|
+
end
|
71
|
+
|
125
72
|
# Write entry with specified name.
|
126
73
|
def put(name, value)
|
127
74
|
@modified = true
|
128
|
-
name = name.to_s
|
129
75
|
|
130
76
|
if value.is_a?(Tree)
|
131
|
-
|
132
|
-
table[name] = value
|
77
|
+
@table[name] = value
|
133
78
|
else
|
134
|
-
|
135
|
-
blob = Blob.new(store) if not blob.is_a?(Blob)
|
136
|
-
blob.path = child_path(name)
|
137
|
-
blob.object = value
|
138
|
-
table[name] = blob
|
79
|
+
@table[name] = Blob.new(store, nil, handler_for(name).write(value))
|
139
80
|
end
|
140
|
-
|
81
|
+
|
141
82
|
value
|
142
83
|
end
|
143
84
|
|
144
85
|
# Remove entry with specified name.
|
145
86
|
def remove(name)
|
146
87
|
@modified = true
|
147
|
-
table.delete(name.to_s)
|
88
|
+
@table.delete(name.to_s)
|
148
89
|
end
|
149
90
|
|
150
91
|
# Does this key exist in the table?
|
151
92
|
def has_key?(name)
|
152
|
-
table.has_key?(name)
|
93
|
+
@table.has_key?(name.to_s)
|
153
94
|
end
|
154
95
|
|
96
|
+
def normalize_path(path)
|
97
|
+
(path[0, 1] == '/' ? path[1..-1] : path).split('/')
|
98
|
+
end
|
99
|
+
|
155
100
|
# Read a value on specified path.
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
args.inject(self) { |tree, key| tree.get(key) or return nil }
|
101
|
+
def [](path)
|
102
|
+
normalize_path(path).inject(self) do |tree, key|
|
103
|
+
tree.get(key) or return nil
|
104
|
+
end
|
161
105
|
end
|
162
106
|
|
163
107
|
# Write a value on specified path.
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
value
|
168
|
-
args = args.first.to_s.split('/') if args.size == 1
|
169
|
-
tree = args[0..-2].to_a.inject(self) { |tree, name| tree.tree(name) }
|
170
|
-
tree.put(args.last, value)
|
108
|
+
def []=(path, value)
|
109
|
+
list = normalize_path(path)
|
110
|
+
tree = list[0..-2].to_a.inject(self) { |tree, name| tree.tree(name) }
|
111
|
+
tree.put(list.last, value)
|
171
112
|
end
|
172
113
|
|
173
114
|
# Delete a value on specified path.
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
tree = args[0..-2].to_a.inject(self) do |tree, key|
|
115
|
+
def delete(path)
|
116
|
+
list = normalize_path(path)
|
117
|
+
|
118
|
+
tree = list[0..-2].to_a.inject(self) do |tree, key|
|
179
119
|
tree.get(key) or return
|
180
120
|
end
|
181
|
-
|
121
|
+
|
122
|
+
tree.remove(list.last)
|
182
123
|
end
|
183
124
|
|
184
125
|
# Iterate over all objects found in this subtree.
|
185
|
-
def each(&block)
|
186
|
-
table.sort.each do |name, entry|
|
126
|
+
def each(path = [], &block)
|
127
|
+
@table.sort.each do |name, entry|
|
128
|
+
child_path = path + [name]
|
129
|
+
case entry
|
130
|
+
when Blob
|
131
|
+
entry.object ||= handler_for(name).read(entry.data)
|
132
|
+
yield child_path.join("/"), entry.object
|
133
|
+
|
134
|
+
when Tree
|
135
|
+
entry.each(child_path, &block)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def each_blob(path = [], &block)
|
141
|
+
@table.sort.each do |name, entry|
|
142
|
+
child_path = path + [name]
|
143
|
+
|
187
144
|
case entry
|
188
|
-
when Blob
|
189
|
-
|
145
|
+
when Blob
|
146
|
+
yield child_path.join("/"), entry
|
147
|
+
|
148
|
+
when Tree
|
149
|
+
entry.each_blob(child_path, &block)
|
190
150
|
end
|
191
151
|
end
|
192
152
|
end
|
193
153
|
|
154
|
+
def paths
|
155
|
+
map { |path, data| path }
|
156
|
+
end
|
157
|
+
|
158
|
+
def values
|
159
|
+
map { |path, data| data }
|
160
|
+
end
|
161
|
+
|
194
162
|
# Convert this tree into a hash object.
|
195
163
|
def to_hash
|
196
|
-
table.inject({}) do |hash, (name, entry)|
|
197
|
-
|
164
|
+
@table.inject({}) do |hash, (name, entry)|
|
165
|
+
if entry.is_a?(Tree)
|
166
|
+
hash[name] = entry.to_hash
|
167
|
+
else
|
168
|
+
hash[name] = entry.object ||= handler_for(name).read(entry.data)
|
169
|
+
end
|
198
170
|
hash
|
199
171
|
end
|
200
172
|
end
|