gitgo 0.3.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.
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