git-ds 0.9.2
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.rdoc +795 -0
- data/doc/Examples.rdoc +36 -0
- data/doc/examples/key_value/kv_get.rb +29 -0
- data/doc/examples/key_value/kv_init.rb +20 -0
- data/doc/examples/key_value/kv_list.rb +28 -0
- data/doc/examples/key_value/kv_remove.rb +29 -0
- data/doc/examples/key_value/kv_set.rb +39 -0
- data/doc/examples/key_value/model.rb +156 -0
- data/doc/examples/key_value/test.rb +50 -0
- data/doc/examples/test_suite/model.rb +503 -0
- data/doc/examples/test_suite/test.rb +173 -0
- data/doc/examples/test_suite/ts_add_bug.rb +65 -0
- data/doc/examples/test_suite/ts_add_module.rb +74 -0
- data/doc/examples/test_suite/ts_add_module_to_test.rb +78 -0
- data/doc/examples/test_suite/ts_add_test.rb +77 -0
- data/doc/examples/test_suite/ts_add_test_suite.rb +65 -0
- data/doc/examples/test_suite/ts_add_test_to_bug.rb +76 -0
- data/doc/examples/test_suite/ts_init.rb +20 -0
- data/doc/examples/test_suite/ts_list.rb +118 -0
- data/doc/examples/test_suite/ts_perform_test.rb +104 -0
- data/doc/examples/test_suite/ts_update_bugs.rb +58 -0
- data/doc/examples/user_group/model.rb +265 -0
- data/doc/examples/user_group/test.rb +64 -0
- data/doc/examples/user_group/ug_add_group.rb +39 -0
- data/doc/examples/user_group/ug_add_group_user.rb +36 -0
- data/doc/examples/user_group/ug_add_user.rb +39 -0
- data/doc/examples/user_group/ug_init.rb +20 -0
- data/doc/examples/user_group/ug_list.rb +32 -0
- data/lib/git-ds.rb +14 -0
- data/lib/git-ds/config.rb +53 -0
- data/lib/git-ds/database.rb +289 -0
- data/lib/git-ds/exec_cmd.rb +107 -0
- data/lib/git-ds/index.rb +205 -0
- data/lib/git-ds/model.rb +136 -0
- data/lib/git-ds/model/db_item.rb +42 -0
- data/lib/git-ds/model/fs_item.rb +51 -0
- data/lib/git-ds/model/item.rb +428 -0
- data/lib/git-ds/model/item_list.rb +97 -0
- data/lib/git-ds/model/item_proxy.rb +128 -0
- data/lib/git-ds/model/property.rb +144 -0
- data/lib/git-ds/model/root.rb +46 -0
- data/lib/git-ds/repo.rb +455 -0
- data/lib/git-ds/shared.rb +17 -0
- data/lib/git-ds/transaction.rb +77 -0
- data/tests/ut_database.rb +304 -0
- data/tests/ut_git_grit_equiv.rb +195 -0
- data/tests/ut_index.rb +203 -0
- data/tests/ut_model.rb +360 -0
- data/tests/ut_repo.rb +260 -0
- data/tests/ut_user_group_model.rb +316 -0
- metadata +142 -0
data/lib/git-ds/index.rb
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# :title: Git-DS::Index
|
3
|
+
=begin rdoc
|
4
|
+
Wrapper for Grit::Index
|
5
|
+
|
6
|
+
Copyright 2010 Thoughtgang <http://www.thoughtgang.org>
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'grit'
|
11
|
+
require 'fileutils'
|
12
|
+
|
13
|
+
require 'git-ds/shared'
|
14
|
+
|
15
|
+
module GitDS
|
16
|
+
|
17
|
+
# =============================================================================
|
18
|
+
=begin rdoc
|
19
|
+
A Git Index.
|
20
|
+
=end
|
21
|
+
class Index < Grit::Index
|
22
|
+
|
23
|
+
=begin rdoc
|
24
|
+
Write index to the object db, then read the object DB into the GIT staging
|
25
|
+
index. Returns SHA of new tree.
|
26
|
+
=end
|
27
|
+
def write
|
28
|
+
sha = local_write_tree(self.tree, self.current_tree)
|
29
|
+
sha
|
30
|
+
end
|
31
|
+
|
32
|
+
=begin rdoc
|
33
|
+
Re-implemented from Grit. Grit adds a trailing / to directory names, which
|
34
|
+
makes it impossible to delete Tree objects!
|
35
|
+
=end
|
36
|
+
def local_write_tree(tree, now_tree=nil)
|
37
|
+
tree_contents = {}
|
38
|
+
now_tree.contents.each do |obj|
|
39
|
+
sha = [obj.id].pack("H*")
|
40
|
+
k = obj.name
|
41
|
+
tree_contents[k] = "%s %s\0%s" % [obj.mode.to_s, obj.name, sha]
|
42
|
+
end if now_tree
|
43
|
+
|
44
|
+
tree.each do |k, v|
|
45
|
+
case v
|
46
|
+
when String
|
47
|
+
sha = write_blob(v)
|
48
|
+
sha = [sha].pack("H*")
|
49
|
+
str = "%s %s\0%s" % ['100644', k, sha]
|
50
|
+
tree_contents[k] = str
|
51
|
+
when Hash
|
52
|
+
ctree = now_tree/k if now_tree
|
53
|
+
sha = local_write_tree(v, ctree)
|
54
|
+
sha = [sha].pack("H*")
|
55
|
+
str = "%s %s\0%s" % ['40000', k, sha]
|
56
|
+
tree_contents[k] = str
|
57
|
+
when false
|
58
|
+
tree_contents.delete(k)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
tr = tree_contents.sort.map { |k, v| v }.join('')
|
63
|
+
self.repo.git.put_raw_object(tr, 'tree')
|
64
|
+
end
|
65
|
+
|
66
|
+
=begin rdoc
|
67
|
+
Add a DB entry at the virtual path 'path' with contents 'contents'
|
68
|
+
=end
|
69
|
+
alias :add_db :add
|
70
|
+
|
71
|
+
def add(path, data, on_fs=false)
|
72
|
+
super(path, data)
|
73
|
+
add_fs_item(path, data) if on_fs
|
74
|
+
end
|
75
|
+
|
76
|
+
=begin rdoc
|
77
|
+
Convenience function to add an on-filesystem object.
|
78
|
+
=end
|
79
|
+
def add_fs(path, data)
|
80
|
+
add(path, data, true)
|
81
|
+
end
|
82
|
+
|
83
|
+
=begin rdoc
|
84
|
+
Wrapper for Grit::Index#delete that removes the file if it exists on the
|
85
|
+
filesystem.
|
86
|
+
=end
|
87
|
+
def delete(path)
|
88
|
+
super
|
89
|
+
@repo.exec_in_git_dir {
|
90
|
+
::FileUtils.remove_entry(path) if ::File.exist?(path)
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
=begin rdoc
|
97
|
+
Add a DB entry at the filesystem path 'path' with contents 'contents'
|
98
|
+
=end
|
99
|
+
def add_fs_item( path, data )
|
100
|
+
fs_path = @repo.top_level + ::File::SEPARATOR + path
|
101
|
+
make_parent_dirs(fs_path)
|
102
|
+
|
103
|
+
# Create file in filesystem
|
104
|
+
@repo.exec_in_git_dir { ::File.open(fs_path, 'w') {|f| f.write(data)} }
|
105
|
+
end
|
106
|
+
|
107
|
+
=begin rdoc
|
108
|
+
Add parent directories as-needed to create 'path' on the filesystem.
|
109
|
+
=end
|
110
|
+
def make_parent_dirs(path)
|
111
|
+
tmp_path = ''
|
112
|
+
|
113
|
+
::File.dirname(path).split(::File::SEPARATOR).each do |dir|
|
114
|
+
next if dir.empty?
|
115
|
+
tmp_path << ::File::SEPARATOR << dir
|
116
|
+
Dir.mkdir(tmp_path) if not ::File.exist?(tmp_path)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
=begin rdoc
|
123
|
+
Index object for the Git staging index.
|
124
|
+
=end
|
125
|
+
class StageIndex < Index
|
126
|
+
|
127
|
+
attr_reader :sha
|
128
|
+
attr_reader :parent_commit
|
129
|
+
|
130
|
+
=begin rdoc
|
131
|
+
=end
|
132
|
+
def initialize(repo,treeish=nil)
|
133
|
+
super(repo)
|
134
|
+
@parent_commit = repo.commits(repo.current_branch, 1).first
|
135
|
+
treeish = (@parent_commit ? @parent_commit.tree.id : 'master') if \
|
136
|
+
not treeish
|
137
|
+
read_tree(treeish)
|
138
|
+
@sha = self.current_tree.id
|
139
|
+
end
|
140
|
+
|
141
|
+
=begin rdoc
|
142
|
+
=end
|
143
|
+
def commit(msg, author=nil)
|
144
|
+
last_tree = @parent_commit ? @parent_commit.tree.id : nil
|
145
|
+
parents = @parent_commit ? [@parent_commit] : []
|
146
|
+
# TODO : why does last_tree cause some commits to fail?
|
147
|
+
# test_transaction(TC_GitDatabaseTest)
|
148
|
+
# transaction commit has wrong message.
|
149
|
+
# <"SUCCESS"> expected but was <"auto-commit on transaction">
|
150
|
+
# Possible bug in Grit::last_tree?
|
151
|
+
#sha = super(msg, parents, author, last_tree, @repo.current_branch)
|
152
|
+
sha = super(msg, parents, author, nil, @repo.current_branch)
|
153
|
+
if sha
|
154
|
+
@parent_commit = @repo.commit(sha)
|
155
|
+
read_tree(@parent_commit.tree.id)
|
156
|
+
end
|
157
|
+
sha
|
158
|
+
end
|
159
|
+
|
160
|
+
=begin rdoc
|
161
|
+
Write tree object for index to object database.
|
162
|
+
=end
|
163
|
+
def write
|
164
|
+
@sha = super
|
165
|
+
end
|
166
|
+
|
167
|
+
=begin rdoc
|
168
|
+
Write, read tree. Done when a tree is requested.
|
169
|
+
=end
|
170
|
+
def build
|
171
|
+
return @sha if self.tree.empty?
|
172
|
+
self.read_tree(self.write)
|
173
|
+
end
|
174
|
+
|
175
|
+
def read_tree(sha)
|
176
|
+
super
|
177
|
+
end
|
178
|
+
|
179
|
+
=begin rdoc
|
180
|
+
Sync with staging index. This causes the Git index (used by command-line tools)
|
181
|
+
to be filled with the contents of this index.
|
182
|
+
|
183
|
+
This can be instead of a commit to ensure that command-line tools can access
|
184
|
+
the index contents.
|
185
|
+
=end
|
186
|
+
def sync
|
187
|
+
self.build
|
188
|
+
@repo.exec_in_git_dir { `git read-tree #{@sha}` }
|
189
|
+
end
|
190
|
+
|
191
|
+
=begin rdoc
|
192
|
+
Read staging index from disk and create a StagingIndex object for it.
|
193
|
+
|
194
|
+
This can be used to access index contents created by command-line tools.
|
195
|
+
=end
|
196
|
+
def self.read(repo)
|
197
|
+
sha = repo.exec_in_git_dir{`git write-tree`}.chomp
|
198
|
+
new(repo, sha)
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
end
|
205
|
+
|
data/lib/git-ds/model.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# :title: Git-DS::Model
|
3
|
+
=begin rdoc
|
4
|
+
|
5
|
+
Copyright 2010 Thoughtgang <http://www.thoughtgang.org>
|
6
|
+
=end
|
7
|
+
|
8
|
+
require 'git-ds/shared'
|
9
|
+
require 'git-ds/config'
|
10
|
+
require 'git-ds/model/property'
|
11
|
+
require 'git-ds/model/item'
|
12
|
+
require 'git-ds/model/db_item'
|
13
|
+
require 'git-ds/model/fs_item'
|
14
|
+
require 'git-ds/model/item_list'
|
15
|
+
require 'git-ds/model/item_proxy'
|
16
|
+
require 'git-ds/model/root'
|
17
|
+
|
18
|
+
# TODO: REFACTOR to act as delegate for database
|
19
|
+
|
20
|
+
# TODO: register ModelItemClass name, e.g. 'user', with model.
|
21
|
+
# then to instantiate, get item name from path, e.g.
|
22
|
+
# system/1/user/1 would be 'user'
|
23
|
+
# ...and use that to determine class to instantiate.
|
24
|
+
# TODO: query/find/grep (search of object contents or paths)
|
25
|
+
|
26
|
+
module GitDS
|
27
|
+
|
28
|
+
=begin rdoc
|
29
|
+
A data model.
|
30
|
+
=end
|
31
|
+
class Model
|
32
|
+
|
33
|
+
=begin rdoc
|
34
|
+
The Root item for the Model.
|
35
|
+
=end
|
36
|
+
attr_reader :root
|
37
|
+
|
38
|
+
=begin rdoc
|
39
|
+
The database connection for the model. This is expected to be a GitDS::Database
|
40
|
+
object.
|
41
|
+
=end
|
42
|
+
attr_reader :db
|
43
|
+
|
44
|
+
=begin rdoc
|
45
|
+
The name of the model. This is only used for storing configuration variables.
|
46
|
+
=end
|
47
|
+
attr_reader :name
|
48
|
+
|
49
|
+
def initialize(db, name='generic', root=nil)
|
50
|
+
@db = db
|
51
|
+
@root = root ? root : RootItem.new(self)
|
52
|
+
@name = name
|
53
|
+
end
|
54
|
+
|
55
|
+
=begin rdoc
|
56
|
+
Provides access to the Hash of Model-specific config variables.
|
57
|
+
=end
|
58
|
+
def config
|
59
|
+
@git_config ||= RepoConfig.new(@db, 'model-' + @name)
|
60
|
+
end
|
61
|
+
|
62
|
+
=begin rdoc
|
63
|
+
Returns true if Model contains path.
|
64
|
+
=end
|
65
|
+
def include?(path)
|
66
|
+
@db.include? path
|
67
|
+
end
|
68
|
+
|
69
|
+
alias :exist? :include?
|
70
|
+
|
71
|
+
=begin rdoc
|
72
|
+
List children (filenames) of path. Returns [] if path is not a directory.
|
73
|
+
=end
|
74
|
+
def list_children(path=root.path)
|
75
|
+
@db.list(path).keys.sort
|
76
|
+
end
|
77
|
+
|
78
|
+
=begin rdoc
|
79
|
+
Add an item to the object DB.
|
80
|
+
=end
|
81
|
+
# Might be better as add child?
|
82
|
+
def add_item(path, data)
|
83
|
+
# note: @db.add uses exec {} so there is no need to here.
|
84
|
+
@db.add(path, data)
|
85
|
+
end
|
86
|
+
|
87
|
+
=begin rdoc
|
88
|
+
Add an item to the object DB and the filesystem.
|
89
|
+
=end
|
90
|
+
def add_fs_item(path, data)
|
91
|
+
# note: @db.add uses exec {} so there is no need to here.
|
92
|
+
@db.add(path, data, true)
|
93
|
+
end
|
94
|
+
|
95
|
+
=begin rdoc
|
96
|
+
Return the contents of the BLOB at path.
|
97
|
+
=end
|
98
|
+
def get_item(path)
|
99
|
+
@db.object_data(path)
|
100
|
+
end
|
101
|
+
|
102
|
+
=begin rdoc
|
103
|
+
Delete an item from the object DB (and the filesystem, if it exists).
|
104
|
+
=end
|
105
|
+
def delete_item(path)
|
106
|
+
# note: @db.delete uses exec {} so there is no need to here.
|
107
|
+
@db.delete(path)
|
108
|
+
end
|
109
|
+
|
110
|
+
=begin rdoc
|
111
|
+
Execute block as a database ExecCmd.
|
112
|
+
=end
|
113
|
+
def exec(&block)
|
114
|
+
@db.exec(&block)
|
115
|
+
end
|
116
|
+
|
117
|
+
=begin rdoc
|
118
|
+
Execute block as a database transaction.
|
119
|
+
=end
|
120
|
+
def transaction(&block)
|
121
|
+
@db.transaction(&block)
|
122
|
+
end
|
123
|
+
|
124
|
+
=begin rdoc
|
125
|
+
Execute a transaction in a branch, then merge if it was successful.
|
126
|
+
|
127
|
+
See Database#branch_and_merge.
|
128
|
+
=end
|
129
|
+
def branched_transaction(name=@db.next_branch_tag(), &block)
|
130
|
+
raise 'Branched transactions cannot be nested' if @db.staging?
|
131
|
+
@db.branch_and_merge(name, &block)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# :title: Git-DS::DbModelItem
|
3
|
+
=begin rdoc
|
4
|
+
|
5
|
+
Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
|
6
|
+
=end
|
7
|
+
|
8
|
+
require 'git-ds/shared'
|
9
|
+
require 'git-ds/model/item'
|
10
|
+
|
11
|
+
module GitDS
|
12
|
+
|
13
|
+
# ----------------------------------------------------------------------
|
14
|
+
=begin rdoc
|
15
|
+
An in-DB ModelItem mixin. DbModelItems exist only in the database.
|
16
|
+
|
17
|
+
Note: this is an instance-method module. It should be included, not extended.
|
18
|
+
=end
|
19
|
+
module DbModelItemObject
|
20
|
+
include ModelItemObject
|
21
|
+
end
|
22
|
+
|
23
|
+
# ----------------------------------------------------------------------
|
24
|
+
=begin rdoc
|
25
|
+
Note: this is a class-method module. It should be extended in a class, not
|
26
|
+
included.
|
27
|
+
=end
|
28
|
+
module DbModelItemClass
|
29
|
+
include ModelItemClass
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# ----------------------------------------------------------------------
|
34
|
+
=begin rdoc
|
35
|
+
Base class for DB-only ModelItem objects. These do not appear in the filesystem.
|
36
|
+
=end
|
37
|
+
class ModelItem
|
38
|
+
extend DbModelItemClass
|
39
|
+
include DbModelItemObject
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# :title: Git-DS::FsModelItem
|
3
|
+
=begin rdoc
|
4
|
+
|
5
|
+
Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
|
6
|
+
=end
|
7
|
+
|
8
|
+
require 'git-ds/shared'
|
9
|
+
require 'git-ds/model/item'
|
10
|
+
|
11
|
+
module GitDS
|
12
|
+
|
13
|
+
# ----------------------------------------------------------------------
|
14
|
+
=begin rdoc
|
15
|
+
A filesystem ModelItem mixin. FsModelItems exist both on the filesystem and
|
16
|
+
in the database.
|
17
|
+
|
18
|
+
Note: this is an instance-method module. It should be included, not extended.
|
19
|
+
=end
|
20
|
+
module FsModelItemObject
|
21
|
+
include ModelItemObject
|
22
|
+
end
|
23
|
+
|
24
|
+
# ----------------------------------------------------------------------
|
25
|
+
=begin rdoc
|
26
|
+
Note: this is a class-method module. It should be extended in a class, not
|
27
|
+
included.
|
28
|
+
=end
|
29
|
+
module FsModelItemClass
|
30
|
+
include ModelItemClass
|
31
|
+
|
32
|
+
=begin rdoc
|
33
|
+
Define a property for this ModelItem class. The property will exist in the
|
34
|
+
DB and on the filesystem.
|
35
|
+
=end
|
36
|
+
def property(name, default=0, &block)
|
37
|
+
define_fs_property(name, default, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
# ----------------------------------------------------------------------
|
43
|
+
=begin rdoc
|
44
|
+
Base class for filesystem ModelItem objects.
|
45
|
+
=end
|
46
|
+
class FsModelItem
|
47
|
+
extend FsModelItemClass
|
48
|
+
include FsModelItemObject
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,428 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# :title: Git-DS::ModelItem
|
3
|
+
=begin rdoc
|
4
|
+
|
5
|
+
Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
|
6
|
+
|
7
|
+
Notes:
|
8
|
+
The children of an item will be one of the following:
|
9
|
+
* A Property (a named BLOB containing a value)
|
10
|
+
* A ModelItem (a subdirectory that defines a ModelItem instance)
|
11
|
+
* A ModelItem link (a named BLOB containing a path to a ModelItem instance)
|
12
|
+
Note that ModelItems and ModelItemLinks will be in a subdirectory named for
|
13
|
+
the ModelItem, even if there is only a single entry.
|
14
|
+
=end
|
15
|
+
|
16
|
+
require 'time' # for timestamp proprties
|
17
|
+
require 'git-ds/shared'
|
18
|
+
require 'git-ds/model/property'
|
19
|
+
|
20
|
+
module GitDS
|
21
|
+
|
22
|
+
class InvalidModelItemError < RuntimeError
|
23
|
+
end
|
24
|
+
|
25
|
+
# ----------------------------------------------------------------------
|
26
|
+
=begin rdoc
|
27
|
+
ModelItem class methods.
|
28
|
+
|
29
|
+
Note: this is a class-method module. It should be extended in a class, not
|
30
|
+
included.
|
31
|
+
=end
|
32
|
+
module ModelItemClass
|
33
|
+
|
34
|
+
=begin rdoc
|
35
|
+
The name of the ModelItemClass in the database.
|
36
|
+
|
37
|
+
To be overridden by a modelitem class.
|
38
|
+
=end
|
39
|
+
def name(name=nil)
|
40
|
+
@name ||= nil
|
41
|
+
raise 'ModelItemClass has no name defined' if (not name) && (not @name)
|
42
|
+
name ? @name = name : @name
|
43
|
+
end
|
44
|
+
|
45
|
+
=begin rdoc
|
46
|
+
Return the path to the ModelItem class owned by the specified parent.
|
47
|
+
|
48
|
+
For example, given the following database:
|
49
|
+
|
50
|
+
ClassA/a1/ClassB/b1
|
51
|
+
ClassA/a1/ClassB/b2
|
52
|
+
ClassA/a2/ClassB/b3
|
53
|
+
|
54
|
+
ClassA.path(@model.root) will return 'ClassA', ClassB.path(a1) will return
|
55
|
+
'ClassA/a1/ClassB', and ClassB.path(a2) will return 'ClassA/a2/ClassB'.
|
56
|
+
=end
|
57
|
+
def path(parent)
|
58
|
+
return name if not parent
|
59
|
+
build_path(parent.path)
|
60
|
+
end
|
61
|
+
|
62
|
+
=begin rdoc
|
63
|
+
Return the path to the ModelItem class inside the specified directory.
|
64
|
+
=end
|
65
|
+
def build_path(parent_path)
|
66
|
+
return name if (not parent_path) || parent_path.empty?
|
67
|
+
parent_path + ::File::SEPARATOR + name
|
68
|
+
end
|
69
|
+
|
70
|
+
=begin rdoc
|
71
|
+
Return the path to an instance of the object under parent_path.
|
72
|
+
=end
|
73
|
+
def instance_path(parent_path, ident)
|
74
|
+
path = build_path(parent_path)
|
75
|
+
path += ::File::SEPARATOR if not path.empty?
|
76
|
+
path += ident.to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
=begin rdoc
|
80
|
+
List all children of this ModelItem class.
|
81
|
+
|
82
|
+
This will list all instances of the class owned by the specified parent.
|
83
|
+
For example, given the following database:
|
84
|
+
|
85
|
+
ClassA/a1/ClassB/b1
|
86
|
+
ClassA/a1/ClassB/b2
|
87
|
+
ClassA/a2/ClassB/b3
|
88
|
+
|
89
|
+
ClassA.list(@model.root) will return [a1, a2], ClassB.list(a1) will return
|
90
|
+
[b1, b2], and ClassB.list(a2) will return [b3].
|
91
|
+
=end
|
92
|
+
def list(parent)
|
93
|
+
list_in_path parent.model, parent.path
|
94
|
+
end
|
95
|
+
|
96
|
+
=begin rdoc
|
97
|
+
List all children of the ModelItem class inside parent_path.
|
98
|
+
=end
|
99
|
+
def list_in_path(model, parent_path)
|
100
|
+
model.list_children build_path(parent_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
=begin rdoc
|
104
|
+
Generate an ident from a Hash of arguments.
|
105
|
+
|
106
|
+
To be overridden by a modelitem class.
|
107
|
+
=end
|
108
|
+
def ident(args)
|
109
|
+
args[ident_key()].to_s
|
110
|
+
end
|
111
|
+
|
112
|
+
=begin rdoc
|
113
|
+
The key containing the object ident in the args Hash passed to create.
|
114
|
+
|
115
|
+
This can be used to change the name of the ident key in the hash without
|
116
|
+
having to override the ident method.
|
117
|
+
=end
|
118
|
+
def ident_key
|
119
|
+
:ident
|
120
|
+
end
|
121
|
+
|
122
|
+
=begin rdoc
|
123
|
+
Create a new instance of the ModelItemClass owned by the specified parent.
|
124
|
+
|
125
|
+
This will create a subdirectory under ModelItemClass.path(parent); the name
|
126
|
+
of the directory is determined by ModelItemClass.ident.
|
127
|
+
|
128
|
+
For example, given the following database:
|
129
|
+
|
130
|
+
ClassA/a1/ClassB/b1
|
131
|
+
ClassA/a1/ClassB/b2
|
132
|
+
ClassA/a2/ClassB/b3
|
133
|
+
|
134
|
+
ModelItemClass.create(a2, { :ident => 'b4' } ) will create the directory
|
135
|
+
'ClassA/a2/ClassB/b4'.
|
136
|
+
|
137
|
+
The directory will then be filled by calling ModelItemClass.fill.
|
138
|
+
|
139
|
+
Note that this returns the path to the created item, not an instance.
|
140
|
+
=end
|
141
|
+
def create(parent, args={})
|
142
|
+
raise "Use Database.root instead of nil for parent" if not parent
|
143
|
+
raise "parent is not a ModelItem" if not parent.respond_to? :model
|
144
|
+
|
145
|
+
model = parent.model
|
146
|
+
create_in_path(parent.model, parent.path, args)
|
147
|
+
end
|
148
|
+
|
149
|
+
=begin rdoc
|
150
|
+
Create a new instance of the ModelItemClass in the specified directory.
|
151
|
+
|
152
|
+
This will create a subdirectory under parent_path; the name of the directory
|
153
|
+
is determined by ModelItemClass.ident.
|
154
|
+
|
155
|
+
For example, given the following database:
|
156
|
+
|
157
|
+
ClassA/a1/ClassB/b1
|
158
|
+
ClassA/a1/ClassB/b2
|
159
|
+
ClassA/a2/ClassB/b3
|
160
|
+
|
161
|
+
ModelItemClass.create(a2, { :ident => 'b4' } ) will create the directory
|
162
|
+
'ClassA/a2/ClassB/b4'.
|
163
|
+
|
164
|
+
The directory will then be filled by calling ModelItemClass.fill.
|
165
|
+
|
166
|
+
The creation of the objects in the model takes place within a DB transaction.
|
167
|
+
If this is called from within a transaction, it will use the existing staging
|
168
|
+
index; otherwise, it will create a new index and auto-commit on success.
|
169
|
+
|
170
|
+
Note that this returns the ident of the created item, not an instance.
|
171
|
+
=end
|
172
|
+
def create_in_path(model, parent_path, args)
|
173
|
+
id = ident(args)
|
174
|
+
item_path = build_path(parent_path) + ::File::SEPARATOR + id
|
175
|
+
|
176
|
+
# Ensure that nested calls (e.g. to create children) share index#write
|
177
|
+
cls = self
|
178
|
+
model.transaction {
|
179
|
+
propagate
|
180
|
+
cls.fill(model, item_path, args)
|
181
|
+
}
|
182
|
+
|
183
|
+
item_path
|
184
|
+
end
|
185
|
+
|
186
|
+
=begin rdoc
|
187
|
+
Create all subdirectories and files needed to represent a ModelItemClass
|
188
|
+
instance in the object repository.
|
189
|
+
|
190
|
+
item_path is the full path to the item in the model.
|
191
|
+
args is a hash of arguments used to construct the item.
|
192
|
+
|
193
|
+
Can be overridden by ModelItem classes and invoked via super.
|
194
|
+
=end
|
195
|
+
def fill(model, item_path, args)
|
196
|
+
fill_properties(model, item_path, args)
|
197
|
+
end
|
198
|
+
|
199
|
+
=begin rdoc
|
200
|
+
Fill all properties either with their value in 'args' or their default value.
|
201
|
+
|
202
|
+
Foreach key in properties,
|
203
|
+
if args.include?(key) && property.valid?(key, args[key])
|
204
|
+
set property to key
|
205
|
+
elsif properties[key].default
|
206
|
+
set property to default
|
207
|
+
else ignore
|
208
|
+
=end
|
209
|
+
def fill_properties(model, item_path, args)
|
210
|
+
hash = properties
|
211
|
+
hash.keys.each do |key|
|
212
|
+
prop = hash[key]
|
213
|
+
if args.include?(key)
|
214
|
+
prop.set(model, item_path, args[key])
|
215
|
+
elsif hash[key].default
|
216
|
+
prop.set(model, item_path, prop.default)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
=begin rdoc
|
222
|
+
Define a property for this ModelItem class.
|
223
|
+
=end
|
224
|
+
def define_db_property(name, default=0, &block)
|
225
|
+
add_property PropertyDefinition.new(name, default, false, &block)
|
226
|
+
end
|
227
|
+
|
228
|
+
=begin rdoc
|
229
|
+
Define an on-filesystem property for this ModelItem class.
|
230
|
+
=end
|
231
|
+
def define_fs_property(name, default=0, &block)
|
232
|
+
add_property PropertyDefinition.new(name, default, true, &block)
|
233
|
+
end
|
234
|
+
|
235
|
+
=begin rdoc
|
236
|
+
Define a Property for this ModelItem class. The property will be DB-only.
|
237
|
+
|
238
|
+
This can be overridden to change how properties are stored.
|
239
|
+
=end
|
240
|
+
def property(name, default=0, &block)
|
241
|
+
define_db_property(name, default, &block)
|
242
|
+
end
|
243
|
+
|
244
|
+
=begin rdoc
|
245
|
+
Define a property that is a link to a ModelItem object.
|
246
|
+
|
247
|
+
Note: this is a link to a single ModelItem class. For a list of links to
|
248
|
+
ModelItems, use a ModelItem List of ProxyModelItemClass objects.
|
249
|
+
=end
|
250
|
+
def link_property(name, cls, &block)
|
251
|
+
add_property ProxyProperty.new(name, cls, false, &block)
|
252
|
+
end
|
253
|
+
|
254
|
+
|
255
|
+
=begin rdoc
|
256
|
+
Hash of properties associated with this MOdelItem class.
|
257
|
+
=end
|
258
|
+
def properties
|
259
|
+
@properties ||= {}
|
260
|
+
end
|
261
|
+
|
262
|
+
private
|
263
|
+
|
264
|
+
=begin rdoc
|
265
|
+
Add a property to the properties Hash. This throws GitDS::DuplicatePropertyError
|
266
|
+
if a property with the same name already exists in the class.
|
267
|
+
=end
|
268
|
+
def add_property(p)
|
269
|
+
hash = properties
|
270
|
+
raise DuplicatePropertyError.new(p.name) if hash.include? p.name
|
271
|
+
hash[p.name] = p
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# ----------------------------------------------------------------------
|
276
|
+
=begin rdoc
|
277
|
+
Instance methods used by repo-backed objects.
|
278
|
+
|
279
|
+
Note: this is an instance-method module. It should be included, not extended.
|
280
|
+
=end
|
281
|
+
module ModelItemObject
|
282
|
+
|
283
|
+
=begin rdoc
|
284
|
+
The GitDS::Model that contains the object.
|
285
|
+
=end
|
286
|
+
attr_reader :model
|
287
|
+
|
288
|
+
def initialize(model, path)
|
289
|
+
@model = model
|
290
|
+
@path = path
|
291
|
+
@ident = ::File.basename(path)
|
292
|
+
end
|
293
|
+
|
294
|
+
=begin rdoc
|
295
|
+
Full path to this item in the repo.
|
296
|
+
=end
|
297
|
+
def path
|
298
|
+
ensure_valid
|
299
|
+
@path
|
300
|
+
end
|
301
|
+
|
302
|
+
=begin rdoc
|
303
|
+
Primary key (ident) for instance.
|
304
|
+
=end
|
305
|
+
def ident
|
306
|
+
ensure_valid
|
307
|
+
@ident
|
308
|
+
end
|
309
|
+
|
310
|
+
=begin rdoc
|
311
|
+
Return list of property names.
|
312
|
+
=end
|
313
|
+
def properties
|
314
|
+
self.class.properties.keys.sort
|
315
|
+
end
|
316
|
+
|
317
|
+
=begin rdoc
|
318
|
+
Return Hash of cached property values.
|
319
|
+
=end
|
320
|
+
def property_cache
|
321
|
+
ensure_valid
|
322
|
+
@property_cache ||= {}
|
323
|
+
end
|
324
|
+
|
325
|
+
=begin rdoc
|
326
|
+
Return the value of a specific property. If the proprty has not been set,
|
327
|
+
nil is returned.
|
328
|
+
|
329
|
+
ModelItem classes will generally write property accessors that wrap the
|
330
|
+
call to this method.
|
331
|
+
=end
|
332
|
+
def property(name)
|
333
|
+
ensure_valid
|
334
|
+
return property_cache[name] if property_cache.include? name
|
335
|
+
prop = self.class.properties[name]
|
336
|
+
raise "No such property #{name}" if not prop
|
337
|
+
property_cache[name] = prop.get(@model, @path)
|
338
|
+
end
|
339
|
+
|
340
|
+
=begin rdoc
|
341
|
+
Convenience method for reading Integer properties.
|
342
|
+
=end
|
343
|
+
def integer_property(name)
|
344
|
+
val = property(name)
|
345
|
+
val ? property_cache[name] = val.to_i : nil
|
346
|
+
end
|
347
|
+
|
348
|
+
=begin rdoc
|
349
|
+
Convenience method for reading Float properties.
|
350
|
+
=end
|
351
|
+
def float_property(name)
|
352
|
+
val = property(name)
|
353
|
+
val ? property_cache[name] = val.to_f : nil
|
354
|
+
end
|
355
|
+
|
356
|
+
=begin rdoc
|
357
|
+
Convenience method for reading Time (aka timestamp) properties.
|
358
|
+
=end
|
359
|
+
def ts_property(name)
|
360
|
+
val = property(name)
|
361
|
+
val && (! val.empty?) ? property_cache[name] = Time.parse(val) : nil
|
362
|
+
end
|
363
|
+
|
364
|
+
=begin rdoc
|
365
|
+
Convenience method for reading Boolean properties.
|
366
|
+
=end
|
367
|
+
def bool_property(name)
|
368
|
+
val = property(name)
|
369
|
+
(val && val == 'true')
|
370
|
+
end
|
371
|
+
|
372
|
+
=begin rdoc
|
373
|
+
Convenience method for reading Array properties.
|
374
|
+
|
375
|
+
Note that this returns an Array of Strings.
|
376
|
+
|
377
|
+
Note: the default delimiter is what Property uses to encode Array objects.
|
378
|
+
Classes which perform their own encoding can choose a different delimiter.
|
379
|
+
=end
|
380
|
+
def array_property(name, delim="\n")
|
381
|
+
val = property(name)
|
382
|
+
val ? (property_cache[name] = val.split(delim)) : nil
|
383
|
+
end
|
384
|
+
|
385
|
+
=begin rdoc
|
386
|
+
Set the value of a specific property.
|
387
|
+
|
388
|
+
ModelItem classes will generally write property accessors that wrap the
|
389
|
+
call to this method.
|
390
|
+
=end
|
391
|
+
def set_property(name, data)
|
392
|
+
ensure_valid
|
393
|
+
prop = self.class.properties[name]
|
394
|
+
raise "No such property #{name}" if not prop
|
395
|
+
property_cache[name] = prop.set(@model, @path, data)
|
396
|
+
end
|
397
|
+
|
398
|
+
=begin rdoc
|
399
|
+
=end
|
400
|
+
def delete
|
401
|
+
ensure_valid
|
402
|
+
@model.delete_item(@path)
|
403
|
+
# invalidate object
|
404
|
+
@path = nil
|
405
|
+
end
|
406
|
+
|
407
|
+
=begin rdoc
|
408
|
+
Return true if item is valid, false otherwise.
|
409
|
+
=end
|
410
|
+
def valid?
|
411
|
+
@path # an invalid item has a nil path
|
412
|
+
end
|
413
|
+
|
414
|
+
protected
|
415
|
+
|
416
|
+
=begin rdoc
|
417
|
+
Raises an InvalidModelItemError if item is not valid.
|
418
|
+
|
419
|
+
Note: accessors for non-property children should invoke this before
|
420
|
+
touching the object.
|
421
|
+
=end
|
422
|
+
def ensure_valid
|
423
|
+
raise InvalidModelItemError if not valid?
|
424
|
+
end
|
425
|
+
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|