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