gitgo 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/History +44 -0
  2. data/License.txt +22 -0
  3. data/README +45 -0
  4. data/bin/gitgo +4 -0
  5. data/lib/gitgo.rb +1 -0
  6. data/lib/gitgo/app.rb +63 -0
  7. data/lib/gitgo/controller.rb +89 -0
  8. data/lib/gitgo/controllers/code.rb +198 -0
  9. data/lib/gitgo/controllers/issue.rb +76 -0
  10. data/lib/gitgo/controllers/repo.rb +186 -0
  11. data/lib/gitgo/controllers/wiki.rb +19 -0
  12. data/lib/gitgo/document.rb +680 -0
  13. data/lib/gitgo/document/invalid_document_error.rb +34 -0
  14. data/lib/gitgo/documents/comment.rb +20 -0
  15. data/lib/gitgo/documents/issue.rb +56 -0
  16. data/lib/gitgo/git.rb +941 -0
  17. data/lib/gitgo/git/tree.rb +315 -0
  18. data/lib/gitgo/git/utils.rb +59 -0
  19. data/lib/gitgo/helper.rb +3 -0
  20. data/lib/gitgo/helper/doc.rb +28 -0
  21. data/lib/gitgo/helper/form.rb +88 -0
  22. data/lib/gitgo/helper/format.rb +200 -0
  23. data/lib/gitgo/helper/html.rb +19 -0
  24. data/lib/gitgo/helper/utils.rb +85 -0
  25. data/lib/gitgo/index.rb +421 -0
  26. data/lib/gitgo/index/idx_file.rb +119 -0
  27. data/lib/gitgo/index/sha_file.rb +135 -0
  28. data/lib/gitgo/patches/grit.rb +47 -0
  29. data/lib/gitgo/repo.rb +626 -0
  30. data/lib/gitgo/repo/graph.rb +333 -0
  31. data/lib/gitgo/repo/node.rb +122 -0
  32. data/lib/gitgo/rest.rb +87 -0
  33. data/lib/gitgo/server.rb +114 -0
  34. data/lib/gitgo/version.rb +8 -0
  35. data/public/css/gitgo.css +24 -0
  36. data/public/javascript/gitgo.js +148 -0
  37. data/public/javascript/jquery-1.4.2.min.js +154 -0
  38. data/views/app/index.erb +4 -0
  39. data/views/app/timeline.erb +27 -0
  40. data/views/app/welcome.erb +13 -0
  41. data/views/code/_comment.erb +10 -0
  42. data/views/code/_comment_form.erb +14 -0
  43. data/views/code/_comments.erb +5 -0
  44. data/views/code/_commit.erb +25 -0
  45. data/views/code/_grepnav.erb +5 -0
  46. data/views/code/_treenav.erb +3 -0
  47. data/views/code/blob.erb +6 -0
  48. data/views/code/commit_grep.erb +35 -0
  49. data/views/code/commits.erb +11 -0
  50. data/views/code/diff.erb +10 -0
  51. data/views/code/grep.erb +32 -0
  52. data/views/code/index.erb +17 -0
  53. data/views/code/obj/blob.erb +4 -0
  54. data/views/code/obj/commit.erb +25 -0
  55. data/views/code/obj/tag.erb +25 -0
  56. data/views/code/obj/tree.erb +9 -0
  57. data/views/code/tree.erb +9 -0
  58. data/views/error.erb +19 -0
  59. data/views/issue/_issue.erb +15 -0
  60. data/views/issue/_issue_form.erb +39 -0
  61. data/views/issue/edit.erb +11 -0
  62. data/views/issue/index.erb +28 -0
  63. data/views/issue/new.erb +5 -0
  64. data/views/issue/show.erb +27 -0
  65. data/views/layout.erb +34 -0
  66. data/views/not_found.erb +1 -0
  67. data/views/repo/fsck.erb +29 -0
  68. data/views/repo/help.textile +5 -0
  69. data/views/repo/help/faq.textile +19 -0
  70. data/views/repo/help/howto.textile +31 -0
  71. data/views/repo/help/trouble.textile +28 -0
  72. data/views/repo/idx.erb +29 -0
  73. data/views/repo/index.erb +72 -0
  74. data/views/repo/status.erb +16 -0
  75. data/views/wiki/index.erb +3 -0
  76. metadata +253 -0
@@ -0,0 +1,200 @@
1
+ require 'rack/utils'
2
+ require 'redcloth'
3
+ require 'gitgo/helper/utils'
4
+
5
+ module Gitgo
6
+ module Helper
7
+ class Format
8
+ include Rack::Utils
9
+ include Helper::Utils
10
+
11
+ attr_reader :controller
12
+
13
+ def initialize(controller)
14
+ @controller = controller
15
+ end
16
+
17
+ def url(*paths)
18
+ controller.url(paths)
19
+ end
20
+
21
+ def refs
22
+ @refs ||= controller.repo.git.grit.refs
23
+ end
24
+
25
+ def ref_name(sha)
26
+ reference = refs.find {|ref| ref.commit.id == sha}
27
+ reference ? reference.name : sha
28
+ end
29
+
30
+ #
31
+ # general formatters
32
+ #
33
+
34
+ def text(str)
35
+ str = escape_html(str)
36
+ str.gsub!(/[A-Fa-f\d]{40}/) {|sha| sha_a(sha) }
37
+ str
38
+ end
39
+
40
+ def sha(sha)
41
+ escape_html(sha)
42
+ end
43
+
44
+ def textile(str)
45
+ ::RedCloth.new(str).to_html
46
+ end
47
+
48
+ #
49
+ # links
50
+ #
51
+
52
+ def sha_a(sha)
53
+ "<a class=\"sha\" href=\"#{url('obj', sha)}\" title=\"#{sha}\">#{sha}</a>"
54
+ end
55
+
56
+ def path_a(type, treeish, path)
57
+ "<a class=\"#{type}\" href=\"#{url(type, treeish, *path)}\">#{escape_html(path.pop || treeish)}</a>"
58
+ end
59
+
60
+ def full_path_a(type, treeish, path)
61
+ "<a class=\"#{type}\" href=\"#{url(type, treeish, *path)}\">#{escape_html File.join(path)}</a>"
62
+ end
63
+
64
+ def commit_a(treeish)
65
+ "<a class=\"commit\" href=\"#{url('commit', treeish)}\">#{escape_html treeish}</a>"
66
+ end
67
+
68
+ def tree_a(treeish, *path)
69
+ path_a('tree', treeish, path)
70
+ end
71
+
72
+ def blob_a(treeish, *path)
73
+ path_a('blob', treeish, path)
74
+ end
75
+
76
+ def history_a(treeish)
77
+ "<a class=\"history\" href=\"#{url('commits', treeish)}\" title=\"#{escape_html treeish}\">history</a>"
78
+ end
79
+
80
+ def issue_a(doc)
81
+ target = doc.graph_head? ? doc.graph_head : "#{doc.graph_head}##{doc.sha}"
82
+ "<a id=\"#{doc.sha}\" href=\"#{url('issue', target)}\">#{titles(doc.graph_titles)}</a>"
83
+ end
84
+
85
+ def doc_a(doc)
86
+ target = doc.graph_head? ? doc.graph_head : "#{doc.graph_head}##{doc.sha}"
87
+ "<a id=\"#{doc.sha}\" href=\"#{url(doc.type, target)}\">#{escape_html doc.summary}</a>"
88
+ end
89
+
90
+ def index_key_a(key)
91
+ "<a href=\"#{url('repo', 'index', key)}\">#{escape_html key}</a>"
92
+ end
93
+
94
+ def index_value_a(key, value)
95
+ "<a href=\"#{url('repo', 'index', key, value)}\">#{escape_html value}</a>"
96
+ end
97
+
98
+ def each_path(treeish, path)
99
+ paths = path.split("/")
100
+ base = paths.pop
101
+ paths.unshift(treeish)
102
+
103
+ object_path = ['tree']
104
+ paths.collect! do |path|
105
+ object_path << path
106
+ yield "<a href=\"#{url(*object_path)}\">#{escape_html path}</a>"
107
+ end
108
+
109
+ yield(base) if base
110
+ paths
111
+ end
112
+
113
+ #
114
+ # documents
115
+ #
116
+
117
+ def tree(hash, io=[], &block)
118
+ dup = {}
119
+ hash.each_pair do |key, value|
120
+ dup[key] = value.dup
121
+ end
122
+
123
+ tree!(dup, io, &block)
124
+ end
125
+
126
+ def tree!(hash, io=[], &block)
127
+ nodes = flatten(hash)[nil]
128
+ nodes = collapse(nodes)
129
+ nodes.shift
130
+
131
+ render(nodes, io, &block)
132
+ end
133
+
134
+ def title(title)
135
+ escape_html(title)
136
+ end
137
+
138
+ def titles(titles)
139
+ titles << "(nameless)" if titles.empty?
140
+ escape_html titles.join(', ')
141
+ end
142
+
143
+ def content(str)
144
+ textile text(str)
145
+ end
146
+
147
+ def author(author)
148
+ return nil if author.nil?
149
+ "#{escape_html(author.name)} (<a href=\"#{url('timeline')}?#{build_query(:author => author.email)}\">#{escape_html author.email}</a>)"
150
+ end
151
+
152
+ def date(date)
153
+ return nil if date.nil?
154
+ "<abbr title=\"#{date.iso8601}\">#{date.strftime('%Y/%m/%d %H:%M %p')}</abbr>"
155
+ end
156
+
157
+ def at(at)
158
+ at.nil? || at.empty? ? '(none)' : sha_a(at)
159
+ end
160
+
161
+ def origin(origin)
162
+ sha(origin)
163
+ end
164
+
165
+ def tags(tags)
166
+ tags << '(unclassified)' if tags.empty?
167
+ escape_html tags.join(', ')
168
+ end
169
+
170
+ def graph(graph)
171
+ graph.each do |sha, slot, index, current_slots, transitions|
172
+ next unless sha
173
+ yield(sha, "#{slot}:#{index}:#{current_slots.join(',')}:#{transitions.join(',')}")
174
+ end
175
+ end
176
+
177
+ #
178
+ # repo
179
+ #
180
+
181
+ def path(path)
182
+ escape_html(path)
183
+ end
184
+
185
+ def branch(branch)
186
+ escape_html(branch)
187
+ end
188
+
189
+ def each_diff_a(status)
190
+ status.keys.sort.each do |path|
191
+ change, a, b = status[path]
192
+ a_mode, a_sha = a
193
+ b_mode, b_sha = b
194
+
195
+ yield "<a class=\"#{change}\" href=\"#{url('obj', b_sha.to_s)}\" title=\"#{a_sha || '-'} to #{b_sha || '-'}\">#{path}</a>"
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,19 @@
1
+ module Gitgo
2
+ module Helper
3
+ module Html
4
+ module_function
5
+
6
+ def check(true_or_false)
7
+ true_or_false ? ' checked="checked"' : nil
8
+ end
9
+
10
+ def select(true_or_false)
11
+ true_or_false ? ' selected="selected"' : nil
12
+ end
13
+
14
+ def disable(true_or_false)
15
+ true_or_false ? ' disabled="disabled"' : nil
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,85 @@
1
+ module Gitgo
2
+ module Helper
3
+ module Utils
4
+ module_function
5
+
6
+ # Flattens a hash of (parent, [children]) pairs. For example:
7
+ #
8
+ # tree = {
9
+ # "a" => ["b"],
10
+ # "b" => ["c", "d"],
11
+ # "c" => [],
12
+ # "d" => ["e"],
13
+ # "e" => []
14
+ # }
15
+ #
16
+ # flatten(tree)
17
+ # # => {
18
+ # # "a" => ["a", ["b", ["c"], ["d", ["e"]]]],
19
+ # # "b" => ["b", ["c"], ["d", ["e"]]],
20
+ # # "c" => ["c"],
21
+ # # "d" => ["d", ["e"]],
22
+ # # "e" => ["e"]
23
+ # # }
24
+ #
25
+ # Note that the flattened hash re-uses the array values, such that
26
+ # modifiying the "b" value will propagate to the "a" value.
27
+ def flatten(tree)
28
+ tree.each_pair do |parent, children|
29
+ next unless children
30
+
31
+ children.collect! {|child| tree[child] }
32
+ children.unshift(parent)
33
+ end
34
+ tree
35
+ end
36
+
37
+ # Collapses an nested array hierarchy such that nesting is only
38
+ # preserved for existing, and not just potential, branches:
39
+ #
40
+ # collapse(["a", ["b", ["c"]]]) # => ["a", "b", "c"]
41
+ # collapse(["a", ["b", ["c"], ["d", ["e"]]]]) # => ["a", "b", ["c"], ["d", "e"]]
42
+ #
43
+ def collapse(array, result=[])
44
+ result << array.at(0)
45
+
46
+ if (length = array.length) == 2
47
+ collapse(array.at(1), result)
48
+ else
49
+ 1.upto(length-1) do |i|
50
+ result << collapse(array.at(i))
51
+ end
52
+ end
53
+
54
+ result
55
+ end
56
+
57
+ def render(nodes, io=[], list_open='<ul>', list_close='</ul>', item_open='<li>', item_close='</li>', indent='', newline="\n", &block)
58
+ io << indent
59
+ io << list_open
60
+ io << newline
61
+
62
+ nodes.each do |node|
63
+ io << indent
64
+ io << item_open
65
+
66
+ if node.kind_of?(Array)
67
+ io << newline
68
+ render(node, io, list_open, list_close, item_open, item_close, indent + ' ', newline, &block)
69
+ io << newline
70
+ io << indent
71
+ else
72
+ io << (block_given? ? yield(node) : node)
73
+ end
74
+
75
+ io << item_close
76
+ io << newline
77
+ end
78
+
79
+ io << indent
80
+ io << list_close
81
+ io
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,421 @@
1
+ require 'enumerator'
2
+ require 'gitgo/index/idx_file'
3
+ require 'gitgo/index/sha_file'
4
+
5
+ module Gitgo
6
+
7
+ # Index provides an index of documents used to expedite searches. Index
8
+ # structures it's data into a branch-specific directory structure:
9
+ #
10
+ # .git/gitgo/refs/[branch]/index
11
+ # |- filter
12
+ # | `- key
13
+ # | `- value
14
+ # |
15
+ # |- head
16
+ # |- list
17
+ # `- graph_map
18
+ #
19
+ # The files contain the following data (in conceptual order):
20
+ #
21
+ # head The user commit sha at which the last reindex occured.
22
+ # list A list of H* packed shas representing all of the documents
23
+ # accessible by the gitgo branch. The index (idx) of the sha in
24
+ # list serves as an identifier for the sha in map and filters.
25
+ # map A list of L* packed idx pairs mapping a document to its graph
26
+ # head.
27
+ # [value] A list of L* packed idx that match the key-value pair. These
28
+ # lists act as filters in searches.
29
+ #
30
+ # The packing format for each of the index files was chosen for performance;
31
+ # both to minimize the footprint of the file and optimize the usage of the
32
+ # file data.
33
+ #--
34
+ # Index also maintains a cache of temporary files that auto-expire after a
35
+ # certain period of time. The temporary files contain H* packed shas and
36
+ # represent the results of various queries, such as rev-lists.
37
+ #++
38
+ # == Usage
39
+ #
40
+ # Index files are used primarily to select documents based on various
41
+ # filters. For example, to select the shas for all comments tagged as
42
+ # 'important' you would do this:
43
+ #
44
+ # index = Index.new('path')
45
+ #
46
+ # comment = index['type']['comment']
47
+ # important = index['tag']['important']
48
+ # selected = comment & important
49
+ #
50
+ # heads = selected.collect {|id| idx.map[id] }
51
+ # shas = heads.collect {|id| idx.list[id] }.uniq
52
+ #
53
+ # The array operations are very quick because the filters are composed of
54
+ # integers, as is the map. The final step resolves the integers to shas,
55
+ # but this too is simply an array lookup. The select method encapsulates
56
+ # this logic:
57
+ #
58
+ # shas = index.select(
59
+ # :all => {'type' => 'comment', 'tag' => 'important'},
60
+ # :map => true,
61
+ # :shas => true
62
+ # )
63
+ #
64
+ # Importantly, any of the index files can contain duplication without
65
+ # affecting the results of select; this allows new documents to be quickly
66
+ # added into a filter or appended to list/map. As needed or convenient, the
67
+ # index can compact itself and remove duplication.
68
+ #
69
+ class Index
70
+
71
+ # A file containing the ref at which the last index was performed; used to
72
+ # determine when a reindex is required relative to some other ref.
73
+ HEAD = 'head'
74
+
75
+ # An idx file mapping shas to their graph heads
76
+ MAP = 'map'
77
+
78
+ # A sha file listing indexed shas; the index of the sha is used as an
79
+ # identifer for the sha in all idx files.
80
+ LIST = 'list'
81
+
82
+ # The filter directory
83
+ FILTER = 'filter'
84
+
85
+ # The head file for self
86
+ attr_reader :head_file
87
+
88
+ # The map file for self
89
+ attr_reader :map_file
90
+
91
+ # The list file for self
92
+ attr_reader :list_file
93
+
94
+ # Returns an in-memory, self-filling cache of idx files
95
+ attr_reader :cache
96
+
97
+ def initialize(path)
98
+ @path = path
99
+ @head_file = File.expand_path(HEAD, path)
100
+ @map_file = File.expand_path(MAP, path)
101
+ @list_file = File.expand_path(LIST, path)
102
+
103
+ @cache = Hash.new do |key_hash, key|
104
+ key_hash[key] = Hash.new do |value_hash, value|
105
+ value_hash[value] = begin
106
+ index = self.path(FILTER, key, value)
107
+ File.file?(index) ? IdxFile.read(index) : []
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # Returns the sha in the head_file, if it exists, and nil otherwise.
114
+ def head
115
+ File.exists?(head_file) ? File.open(head_file) {|io| io.read(40) } : nil
116
+ end
117
+
118
+ # Returns the contents of map_file, as a hash.
119
+ def map
120
+ @map ||= begin
121
+ entries = File.exists?(map_file) ? IdxFile.read(map_file) : []
122
+ Hash[*entries]
123
+ end
124
+ end
125
+
126
+ # Returns the contents of list_file, as an array.
127
+ def list
128
+ @list ||= (File.exists?(list_file) ? ShaFile.read(list_file) : [])
129
+ end
130
+
131
+ # Returns the segments joined to the path used to initialize self.
132
+ def path(*segments)
133
+ File.join(@path, *segments)
134
+ end
135
+
136
+ # Returns cache[key], a self-filling hash of filter values. Be careful
137
+ # not to modify index[k][v] as it is the actual cache storage.
138
+ def [](key)
139
+ cache[key]
140
+ end
141
+
142
+ # Returns the idx for sha, as specified in list. If the sha is not in
143
+ # list then it is appended to list.
144
+ def idx(sha)
145
+ case
146
+ when list[-1] == sha
147
+ list.length - 1
148
+ when idx = list.index(sha)
149
+ idx
150
+ else
151
+ idx = list.length
152
+ list << sha
153
+ idx
154
+ end
155
+ end
156
+
157
+ # Return the graph head idx for the specified idx.
158
+ def graph_head_idx(idx)
159
+ deconvolute(idx, map)
160
+ end
161
+
162
+ def assoc(source, target, type)
163
+ source_idx = idx(source)
164
+ target_idx = idx(target)
165
+ map[target_idx] = source_idx
166
+ tail_filter << source_idx unless type == :create
167
+ delete_filter << source_idx if type == :delete
168
+ self
169
+ end
170
+
171
+ def create(source)
172
+ source_idx = idx(source)
173
+
174
+ head_idx = graph_head_idx(source_idx)
175
+ unless head_idx.nil? || head_idx == source_idx
176
+ raise "create graph fail: #{source} (already associated with graph #{list[head_idx]})"
177
+ end
178
+
179
+ map[source_idx] = source_idx
180
+ self
181
+ end
182
+
183
+ def link(source, target)
184
+ associate :link, source, target
185
+ end
186
+
187
+ def update(source, target)
188
+ associate :update, source, target
189
+ end
190
+
191
+ def delete(source)
192
+ source_idx = idx(source)
193
+ tail_filter << source_idx
194
+ delete_filter << source_idx
195
+ self
196
+ end
197
+
198
+ # Returns a list of possible index keys.
199
+ def keys
200
+ keys = cache.keys
201
+
202
+ Dir.glob(self.path(FILTER, '*')).select do |path|
203
+ File.directory?(path)
204
+ end.each do |path|
205
+ keys << File.basename(path)
206
+ end
207
+
208
+ keys.uniq!
209
+ keys
210
+ end
211
+
212
+ # Returns a list of possible values for the specified index key.
213
+ def values(key)
214
+ values = cache[key].keys
215
+
216
+ base = path(FILTER, key)
217
+ start = base.length + 1
218
+ Dir.glob("#{base}/**/*").each do |value_path|
219
+ values << value_path[start, value_path.length-start]
220
+ end
221
+
222
+ values.uniq!
223
+ values
224
+ end
225
+
226
+ def all(*keys)
227
+ results = []
228
+ keys.collect do |key|
229
+ values(key).each do |value|
230
+ results.concat(cache[key][value])
231
+ end
232
+ end
233
+ results.uniq!
234
+ results
235
+ end
236
+
237
+ def each_idx(key, values)
238
+ unless values.respond_to?(:each)
239
+ values = [values]
240
+ end
241
+
242
+ values.each do |value|
243
+ cache[key][value].each do |idx|
244
+ yield idx
245
+ end
246
+ end
247
+ end
248
+
249
+ def each_sha(key, values, filter=delete_filter)
250
+ each_idx(key, values) do |idx|
251
+ next if filter.include?(idx)
252
+ yield list[idx]
253
+ end
254
+ end
255
+
256
+ def join(key, *values)
257
+ values.collect {|value| cache[key][value] }.flatten
258
+ end
259
+
260
+ def select(options={})
261
+ basis = options[:basis] || (0..list.length).to_a
262
+
263
+ if all = options[:all]
264
+ each_pair(all) do |key, value|
265
+ basis = basis & cache[key][value]
266
+ break if basis.empty?
267
+ end
268
+ end
269
+
270
+ if any = options[:any]
271
+ matches = []
272
+ each_pair(any) do |key, value|
273
+ matches.concat cache[key][value]
274
+ end
275
+ basis = basis & matches
276
+ end
277
+
278
+ if options[:map]
279
+ basis.collect! {|idx| map[idx] }
280
+ end
281
+
282
+ if options[:shas]
283
+ basis.collect! {|idx| list[idx] }
284
+ end
285
+
286
+ basis.uniq!
287
+ basis
288
+ end
289
+
290
+ def compact
291
+ # reindex shas in list, and create an idx map for updating idxs
292
+ old_list = {}
293
+ list.each {|sha| old_list[old_list.length] = sha }
294
+
295
+ list.uniq!
296
+
297
+ new_list = {}
298
+ list.each {|sha| new_list[sha] = new_list.length}
299
+
300
+ idx_map = {}
301
+ old_list.each_pair {|idx, sha| idx_map[idx] = new_list[sha]}
302
+
303
+ # update/deconvolute mapped idx values
304
+ new_map = {}
305
+ map.each_pair {|idx, head_idx| new_map[idx_map[idx]] = idx_map[head_idx] }
306
+ new_map.keys.each {|idx| new_map[idx] = deconvolute(idx, new_map) || idx }
307
+
308
+ @map = new_map
309
+
310
+ # update filter values
311
+ @cache.values.each do |value_hash|
312
+ value_hash.values.each do |idxs|
313
+ idxs.collect! {|idx| idx_map[idx] }.uniq!
314
+ end
315
+ end
316
+
317
+ self
318
+ end
319
+
320
+ # Writes cached changes.
321
+ def write(sha=nil)
322
+ @cache.each_pair do |key, value_hash|
323
+ value_hash.each_pair do |value, idx|
324
+ IdxFile.write(path(FILTER, key, value), idx)
325
+ end
326
+ end
327
+
328
+ FileUtils.mkdir_p(path) unless File.exists?(path)
329
+ File.open(head_file, "w") {|io| io.write(sha) } if sha
330
+ ShaFile.write(list_file, list.join)
331
+ IdxFile.write(map_file, map.to_a.flatten)
332
+
333
+ self
334
+ end
335
+
336
+ # Clears the cache.
337
+ def reset
338
+ @list = nil
339
+ @map = nil
340
+ @cache.clear
341
+ self
342
+ end
343
+
344
+ # Clears all index files, and the cache.
345
+ def clear
346
+ if File.exists?(path)
347
+ FileUtils.rm_r(path)
348
+ end
349
+ reset
350
+ end
351
+
352
+ private
353
+
354
+ # walks up the map to find the graph head for idx. returns nil if idx is not
355
+ # associated with a graph
356
+ def deconvolute(idx, map, visited=nil) # :nodoc:
357
+ head_idx = map[idx]
358
+
359
+ if visited.nil?
360
+ if head_idx.nil?
361
+ return nil
362
+ else
363
+ visited = []
364
+ end
365
+ end
366
+
367
+ circular = visited.include?(idx)
368
+ visited << idx
369
+
370
+ if circular
371
+ raise "cannot deconvolute cyclic graph: #{visited.inspect}"
372
+ end
373
+
374
+ head_idx == idx ? idx : deconvolute(head_idx, map, visited)
375
+ end
376
+
377
+ def delete_filter
378
+ cache['filter']['delete']
379
+ end
380
+
381
+ def tail_filter
382
+ cache['filter']['tail']
383
+ end
384
+
385
+ def associate(type, source, target) # :nodoc:
386
+ if source == target
387
+ raise "#{type} fail: #{source} -> #{target} (cannot #{type} with self)"
388
+ end
389
+
390
+ source_idx = idx(source)
391
+ target_idx = idx(target)
392
+
393
+ source_head_idx = graph_head_idx(source_idx)
394
+ if source_head_idx.nil?
395
+ raise "#{type} fail: #{source} -> #{target} (source is not associated with a graph)"
396
+ end
397
+
398
+ target_head_idx = graph_head_idx(target_idx)
399
+ unless target_head_idx.nil? || target_head_idx == source_head_idx
400
+ raise "#{type} fail: #{source} -> #{target} (different graph heads #{list[source_head_idx]}/#{list[target_head_idx]})"
401
+ end
402
+
403
+ map[target_idx] = source_head_idx
404
+ tail_filter << source_idx
405
+
406
+ self
407
+ end
408
+
409
+ def each_pair(pairs) # :nodoc:
410
+ pairs.each_pair do |key, values|
411
+ unless values.kind_of?(Array)
412
+ values = [values]
413
+ end
414
+
415
+ values.each do |value|
416
+ yield(key, value)
417
+ end
418
+ end
419
+ end
420
+ end
421
+ end