gitgo 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/History +44 -0
- data/License.txt +22 -0
- data/README +45 -0
- data/bin/gitgo +4 -0
- data/lib/gitgo.rb +1 -0
- data/lib/gitgo/app.rb +63 -0
- data/lib/gitgo/controller.rb +89 -0
- data/lib/gitgo/controllers/code.rb +198 -0
- data/lib/gitgo/controllers/issue.rb +76 -0
- data/lib/gitgo/controllers/repo.rb +186 -0
- data/lib/gitgo/controllers/wiki.rb +19 -0
- data/lib/gitgo/document.rb +680 -0
- data/lib/gitgo/document/invalid_document_error.rb +34 -0
- data/lib/gitgo/documents/comment.rb +20 -0
- data/lib/gitgo/documents/issue.rb +56 -0
- data/lib/gitgo/git.rb +941 -0
- data/lib/gitgo/git/tree.rb +315 -0
- data/lib/gitgo/git/utils.rb +59 -0
- data/lib/gitgo/helper.rb +3 -0
- data/lib/gitgo/helper/doc.rb +28 -0
- data/lib/gitgo/helper/form.rb +88 -0
- data/lib/gitgo/helper/format.rb +200 -0
- data/lib/gitgo/helper/html.rb +19 -0
- data/lib/gitgo/helper/utils.rb +85 -0
- data/lib/gitgo/index.rb +421 -0
- data/lib/gitgo/index/idx_file.rb +119 -0
- data/lib/gitgo/index/sha_file.rb +135 -0
- data/lib/gitgo/patches/grit.rb +47 -0
- data/lib/gitgo/repo.rb +626 -0
- data/lib/gitgo/repo/graph.rb +333 -0
- data/lib/gitgo/repo/node.rb +122 -0
- data/lib/gitgo/rest.rb +87 -0
- data/lib/gitgo/server.rb +114 -0
- data/lib/gitgo/version.rb +8 -0
- data/public/css/gitgo.css +24 -0
- data/public/javascript/gitgo.js +148 -0
- data/public/javascript/jquery-1.4.2.min.js +154 -0
- data/views/app/index.erb +4 -0
- data/views/app/timeline.erb +27 -0
- data/views/app/welcome.erb +13 -0
- data/views/code/_comment.erb +10 -0
- data/views/code/_comment_form.erb +14 -0
- data/views/code/_comments.erb +5 -0
- data/views/code/_commit.erb +25 -0
- data/views/code/_grepnav.erb +5 -0
- data/views/code/_treenav.erb +3 -0
- data/views/code/blob.erb +6 -0
- data/views/code/commit_grep.erb +35 -0
- data/views/code/commits.erb +11 -0
- data/views/code/diff.erb +10 -0
- data/views/code/grep.erb +32 -0
- data/views/code/index.erb +17 -0
- data/views/code/obj/blob.erb +4 -0
- data/views/code/obj/commit.erb +25 -0
- data/views/code/obj/tag.erb +25 -0
- data/views/code/obj/tree.erb +9 -0
- data/views/code/tree.erb +9 -0
- data/views/error.erb +19 -0
- data/views/issue/_issue.erb +15 -0
- data/views/issue/_issue_form.erb +39 -0
- data/views/issue/edit.erb +11 -0
- data/views/issue/index.erb +28 -0
- data/views/issue/new.erb +5 -0
- data/views/issue/show.erb +27 -0
- data/views/layout.erb +34 -0
- data/views/not_found.erb +1 -0
- data/views/repo/fsck.erb +29 -0
- data/views/repo/help.textile +5 -0
- data/views/repo/help/faq.textile +19 -0
- data/views/repo/help/howto.textile +31 -0
- data/views/repo/help/trouble.textile +28 -0
- data/views/repo/idx.erb +29 -0
- data/views/repo/index.erb +72 -0
- data/views/repo/status.erb +16 -0
- data/views/wiki/index.erb +3 -0
- 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
|
data/lib/gitgo/index.rb
ADDED
@@ -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
|