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,76 @@
|
|
1
|
+
require 'gitgo/controller'
|
2
|
+
require 'gitgo/documents/issue'
|
3
|
+
|
4
|
+
module Gitgo
|
5
|
+
module Controllers
|
6
|
+
class Issue < Controller
|
7
|
+
include Rest
|
8
|
+
|
9
|
+
set :views, File.expand_path("views/issue", ROOT)
|
10
|
+
|
11
|
+
get('/issue') { index }
|
12
|
+
get('/issue/new') { preview }
|
13
|
+
get('/issue/:sha') {|sha| show(sha) }
|
14
|
+
get('/issue/:sha/edit') {|sha| edit(sha) }
|
15
|
+
|
16
|
+
post('/issue') { create }
|
17
|
+
post('/issue/:sha') {|sha|
|
18
|
+
_method = request[:_method]
|
19
|
+
case _method
|
20
|
+
when /\Aupdate\z/i then update(sha)
|
21
|
+
when /\Adelete\z/i then destroy(sha)
|
22
|
+
else create(sha)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
put('/issue/:sha') {|sha| update(sha) }
|
27
|
+
delete('/issue/:sha') {|sha| destroy(sha) }
|
28
|
+
|
29
|
+
#
|
30
|
+
# actions
|
31
|
+
#
|
32
|
+
|
33
|
+
Issue = Documents::Issue
|
34
|
+
|
35
|
+
def index
|
36
|
+
all = request['all']
|
37
|
+
any = request['any']
|
38
|
+
|
39
|
+
if tags = request['tags']
|
40
|
+
tags = [tags] unless tags.kind_of?(Array)
|
41
|
+
((all ||= {})['tags'] ||= []).concat(tags)
|
42
|
+
end
|
43
|
+
|
44
|
+
issues = Issue.find(all, any)
|
45
|
+
|
46
|
+
# sort results
|
47
|
+
sort = request['sort'] || 'date'
|
48
|
+
reverse = request['reverse'] == 'true'
|
49
|
+
|
50
|
+
issues.sort! {|a, b| a[sort] <=> b[sort] }
|
51
|
+
issues.reverse! if reverse
|
52
|
+
|
53
|
+
erb :index, :locals => {
|
54
|
+
:docs => issues,
|
55
|
+
:any => any || {},
|
56
|
+
:all => all || {},
|
57
|
+
:sort => sort,
|
58
|
+
:reverse => reverse,
|
59
|
+
:active_sha => session_head
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def tags
|
64
|
+
repo.index.values('tags')
|
65
|
+
end
|
66
|
+
|
67
|
+
def model
|
68
|
+
Issue
|
69
|
+
end
|
70
|
+
|
71
|
+
def attrs
|
72
|
+
request['doc'] || {'tags' => ['open'], 'at' => session_head}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'gitgo/controller'
|
2
|
+
|
3
|
+
module Gitgo
|
4
|
+
module Controllers
|
5
|
+
class Repo < Controller
|
6
|
+
set :views, File.expand_path('views/repo', ROOT)
|
7
|
+
|
8
|
+
get('/repo') { index }
|
9
|
+
get('/repo/index') { show_idx }
|
10
|
+
get('/repo/index/:key') {|key| show_idx(key) }
|
11
|
+
get('/repo/index/:k/:v') {|key, value| show_idx(key, value) }
|
12
|
+
get('/repo/status') { repo_status }
|
13
|
+
get('/repo/fsck') { fsck }
|
14
|
+
get('/repo/*') {|path| template(path) }
|
15
|
+
|
16
|
+
post('/repo/track') { track }
|
17
|
+
post('/repo/commit') { commit }
|
18
|
+
post('/repo/update') { update }
|
19
|
+
post('/repo/reindex') { reset }
|
20
|
+
post('/repo/reset') { reset }
|
21
|
+
post('/repo/prune') { prune }
|
22
|
+
post('/repo/gc') { gc }
|
23
|
+
post('/repo/setup') { setup }
|
24
|
+
|
25
|
+
def git
|
26
|
+
@git ||= repo.git
|
27
|
+
end
|
28
|
+
|
29
|
+
def grit
|
30
|
+
@grit ||= git.grit
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# actions
|
35
|
+
#
|
36
|
+
|
37
|
+
def index
|
38
|
+
erb :index, :locals => {
|
39
|
+
:path => repo.path,
|
40
|
+
:branch => repo.branch,
|
41
|
+
:commit => repo.head.nil? ? nil : grit.commit(repo.head),
|
42
|
+
:upstream_branch => repo.upstream_branch,
|
43
|
+
:refs => repo.refs,
|
44
|
+
:active_sha => session_head,
|
45
|
+
:active_commit => session_head ? grit.commit(session_head) : nil,
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def template(path)
|
50
|
+
begin
|
51
|
+
textile path.to_sym
|
52
|
+
rescue(Errno::ENOENT)
|
53
|
+
$!.message.include?(path) ? not_found : raise
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def show_idx(key=nil, value=nil)
|
58
|
+
index = repo.index
|
59
|
+
|
60
|
+
erb :idx, :locals => {
|
61
|
+
:current_key => key,
|
62
|
+
:index_keys => index.keys.sort,
|
63
|
+
:current_value => value,
|
64
|
+
:index_values => key ? index.values(key).sort : [],
|
65
|
+
:shas => key && value ? index[key][value].collect {|idx| index.list[idx] } : index.list
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
# (note status is taken as a method by Sinatra)
|
70
|
+
def repo_status
|
71
|
+
erb :status, :locals => {
|
72
|
+
:branch => git.branch,
|
73
|
+
:status => git.status(true)
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def fsck
|
78
|
+
erb :fsck, :locals => {
|
79
|
+
:branch => git.branch,
|
80
|
+
:head => session_head,
|
81
|
+
:issues => git.fsck.split("\n"),
|
82
|
+
:stats => git.stats
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
def commit
|
87
|
+
repo.commit request['message']
|
88
|
+
redirect url('/repo/status')
|
89
|
+
end
|
90
|
+
|
91
|
+
def update
|
92
|
+
unless repo.status.empty?
|
93
|
+
raise 'local changes; cannot update'
|
94
|
+
end
|
95
|
+
|
96
|
+
upstream_branch = request['upstream_branch'] || git.upstream_branch
|
97
|
+
unless upstream_branch.nil? || upstream_branch.empty?
|
98
|
+
|
99
|
+
# Note that push and pull cannot be cleanly supported as separate
|
100
|
+
# updates because pull can easily fail without a preceding pull. Since
|
101
|
+
# there is no good way to detect that failure, see issue 7f7e85, the
|
102
|
+
# next best option is to ensure a pull if doing a push.
|
103
|
+
git.pull(upstream_branch)
|
104
|
+
Document.update_index
|
105
|
+
|
106
|
+
if request['sync'] == 'true'
|
107
|
+
git.push(upstream_branch)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
redirect url('/repo')
|
112
|
+
end
|
113
|
+
|
114
|
+
def track
|
115
|
+
tracking_branch = request['tracking_branch']
|
116
|
+
git.track(tracking_branch.empty? ? nil : tracking_branch)
|
117
|
+
|
118
|
+
redirect url('/repo')
|
119
|
+
end
|
120
|
+
|
121
|
+
def reset
|
122
|
+
repo.index.clear
|
123
|
+
|
124
|
+
if full = request['full']
|
125
|
+
git.reset(full == 'true')
|
126
|
+
end
|
127
|
+
|
128
|
+
Document.update_index
|
129
|
+
redirect env['HTTP_REFERER'] || url('/repo')
|
130
|
+
end
|
131
|
+
|
132
|
+
def prune
|
133
|
+
git.prune
|
134
|
+
redirect url('/repo/fsck')
|
135
|
+
end
|
136
|
+
|
137
|
+
def gc
|
138
|
+
git.gc
|
139
|
+
redirect url('/repo/fsck')
|
140
|
+
end
|
141
|
+
|
142
|
+
def setup
|
143
|
+
gitgo = request['gitgo'] || {}
|
144
|
+
if branch = gitgo['branch']
|
145
|
+
repo.checkout(branch)
|
146
|
+
end
|
147
|
+
|
148
|
+
if upstream_branch = request['upstream_branch']
|
149
|
+
repo.setup(upstream_branch)
|
150
|
+
end
|
151
|
+
|
152
|
+
session = request['session'] || {}
|
153
|
+
if head = session['head']
|
154
|
+
self.session_head = head.strip.empty? ? nil : head
|
155
|
+
end
|
156
|
+
|
157
|
+
redirect request['redirect'] || env['HTTP_REFERER'] || url('/repo')
|
158
|
+
end
|
159
|
+
|
160
|
+
# Renders template as erb, then formats using RedCloth.
|
161
|
+
def textile(template, options={}, locals={})
|
162
|
+
require_warn('RedCloth') unless defined?(::RedCloth)
|
163
|
+
|
164
|
+
# extract generic options
|
165
|
+
layout = options.delete(:layout)
|
166
|
+
layout = :layout if layout.nil? || layout == true
|
167
|
+
views = options.delete(:views) || self.class.views || "./views"
|
168
|
+
locals = options.delete(:locals) || locals || {}
|
169
|
+
|
170
|
+
# render template
|
171
|
+
data, options[:filename], options[:line] = lookup_template(:textile, template, views)
|
172
|
+
output = format.textile render_erb(template, data, options, locals)
|
173
|
+
|
174
|
+
# render layout
|
175
|
+
if layout
|
176
|
+
data, options[:filename], options[:line] = lookup_layout(:erb, layout, views)
|
177
|
+
if data
|
178
|
+
output = render_erb(layout, data, options, locals) { output }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
output
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'gitgo/controller'
|
2
|
+
|
3
|
+
module Gitgo
|
4
|
+
module Controllers
|
5
|
+
class Wiki < Controller
|
6
|
+
set :views, File.expand_path("views/wiki", ROOT)
|
7
|
+
|
8
|
+
get("/wiki") { index }
|
9
|
+
|
10
|
+
#
|
11
|
+
# actions
|
12
|
+
#
|
13
|
+
|
14
|
+
def index
|
15
|
+
erb :index
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,680 @@
|
|
1
|
+
require 'gitgo/repo'
|
2
|
+
require 'gitgo/document/invalid_document_error'
|
3
|
+
|
4
|
+
module Gitgo
|
5
|
+
|
6
|
+
# Document represents the data model of Gitgo, and provides high(er)-level
|
7
|
+
# access to documents stored in a Repo. Content and data consistency
|
8
|
+
# constraints are enforced on Document and not on Repo. As such, Document
|
9
|
+
# should be the only way casual users enter data into a Repo.
|
10
|
+
#
|
11
|
+
# == Usage
|
12
|
+
#
|
13
|
+
# For the most part Document behaves like a standard ORM model. The primary
|
14
|
+
# gotcha revolves around setting documents into the git repository and
|
15
|
+
# exists to prevent the creation of unnecessary git objects.
|
16
|
+
#
|
17
|
+
# Unlike you would expect, two method calls are required to store a
|
18
|
+
# document:
|
19
|
+
#
|
20
|
+
# a = Document.new(:content => 'a')
|
21
|
+
# a.save
|
22
|
+
# a.create
|
23
|
+
#
|
24
|
+
# The save method sets the document data into the git repo as a blob and
|
25
|
+
# records the blob sha as a unique identifier for the document. The create
|
26
|
+
# method is what indicates the document is the head of a new document graph.
|
27
|
+
# Simply calling save is not enough (indeed the result of save is a hanging
|
28
|
+
# blob that can be gc'd by git).
|
29
|
+
#
|
30
|
+
# The link and update methods are used instead of create to associate new
|
31
|
+
# documents into an existing graph. For example:
|
32
|
+
#
|
33
|
+
# b = Document.new(:content => 'b')
|
34
|
+
# b.save
|
35
|
+
# a.link(b)
|
36
|
+
#
|
37
|
+
# Calling create prevents a document from being linked into another graph
|
38
|
+
# and vice-versa; the intent is that a given document only belongs to one
|
39
|
+
# document graph. This constraint is only enforced at the Document level
|
40
|
+
# and represents the main reason why using repo directy is a no-no.
|
41
|
+
#
|
42
|
+
# Additionally, as in the command-line git workflow, newly added documents
|
43
|
+
# are not actually committed to a repo until commit is called.
|
44
|
+
class Document
|
45
|
+
class << self
|
46
|
+
|
47
|
+
# Returns a hash registry mapping a type string to a Document class.
|
48
|
+
# Document itself is registered as the nil type. Types also includes
|
49
|
+
# reverse mappings for a Document class to it's type string.
|
50
|
+
attr_reader :types
|
51
|
+
|
52
|
+
# A hash of (key, validator) pairs mapping attribute keys to a
|
53
|
+
# validation method. Not all attributes will have a validator whereas
|
54
|
+
# some attributes share the same validation method.
|
55
|
+
attr_reader :validators
|
56
|
+
|
57
|
+
def inherited(base) # :nodoc:
|
58
|
+
base.instance_variable_set(:@validators, validators.dup)
|
59
|
+
base.instance_variable_set(:@types, types)
|
60
|
+
base.register_as base.to_s.split('::').last.downcase
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the Repo currently in scope (see Repo.current)
|
64
|
+
def repo
|
65
|
+
Repo.current
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the type string for self.
|
69
|
+
def type
|
70
|
+
types[self]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Creates a new document with the attributes and saves. Saved documents
|
74
|
+
# are not automatically associated with a document graph and must be
|
75
|
+
# associated with one via create/update/link to be permanently stored in
|
76
|
+
# the repo.
|
77
|
+
def save(attrs={})
|
78
|
+
doc = new(attrs, repo)
|
79
|
+
doc.save
|
80
|
+
doc.reindex
|
81
|
+
doc
|
82
|
+
end
|
83
|
+
|
84
|
+
# Creates a new document with the attrs. The document is saved,
|
85
|
+
# created, and indexed before being returned.
|
86
|
+
def create(attrs={})
|
87
|
+
update_index
|
88
|
+
doc = save(attrs)
|
89
|
+
doc.create
|
90
|
+
doc
|
91
|
+
end
|
92
|
+
|
93
|
+
# Reads the specified document and casts it into an instance as per
|
94
|
+
# cast. Returns nil if the document doesn't exist.
|
95
|
+
#
|
96
|
+
# == Usage Note
|
97
|
+
#
|
98
|
+
# Read will re-read the document directly from the git repository every
|
99
|
+
# time it is called. For better performance, use the AGET method which
|
100
|
+
# performs the same read but uses the Repo cache if possible.
|
101
|
+
def read(sha)
|
102
|
+
sha = repo.resolve(sha)
|
103
|
+
attrs = repo.read(sha)
|
104
|
+
|
105
|
+
attrs ? cast(attrs, sha) : nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# Reads the specified document from the repo cache and casts it into an
|
109
|
+
# instance as per cast. Returns nil if the document doesn't exist.
|
110
|
+
def [](sha)
|
111
|
+
sha = repo.resolve(sha)
|
112
|
+
attrs = repo[sha]
|
113
|
+
|
114
|
+
attrs ? cast(attrs, sha) : nil
|
115
|
+
end
|
116
|
+
|
117
|
+
# Casts the attributes hash into a document instance. The document
|
118
|
+
# class is determined by resolving the 'type' attribute against the
|
119
|
+
# types registry.
|
120
|
+
def cast(attrs, sha)
|
121
|
+
type = attrs['type']
|
122
|
+
klass = types[type] or raise "unknown type: #{type}"
|
123
|
+
klass.new(attrs, repo, sha)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Updates and indexes the old document with new attributes. The new
|
127
|
+
# attributes are merged with the current doc attributes. Returns the
|
128
|
+
# new document.
|
129
|
+
#
|
130
|
+
# The new document can be used to update other documents, if necessary,
|
131
|
+
# as when resolving forks in an update graph:
|
132
|
+
#
|
133
|
+
# a = Document.create(:content => 'a')
|
134
|
+
# b = Document.update(a, :content => 'b')
|
135
|
+
# c = Document.update(a, :content => 'c')
|
136
|
+
#
|
137
|
+
# d = Document.update(b, :content => 'd')
|
138
|
+
# c.update(d)
|
139
|
+
#
|
140
|
+
# a.reset
|
141
|
+
# a.node.versions.uniq # => [d.sha]
|
142
|
+
#
|
143
|
+
def update(old_doc, attrs={})
|
144
|
+
update_index
|
145
|
+
|
146
|
+
unless old_doc.kind_of?(Document)
|
147
|
+
old_doc = Document[old_doc]
|
148
|
+
end
|
149
|
+
|
150
|
+
new_doc = old_doc.merge(attrs)
|
151
|
+
new_doc.save
|
152
|
+
new_doc.reindex
|
153
|
+
|
154
|
+
old_doc.update(new_doc)
|
155
|
+
new_doc
|
156
|
+
end
|
157
|
+
|
158
|
+
# Finds all documents matching the any and all criteria. The any and
|
159
|
+
# all inputs are hashes of index values used to filter all possible
|
160
|
+
# documents. They consist of (key, value) or (key, [values]) pairs. At
|
161
|
+
# least one of pair must match in the any case. All pairs must match in
|
162
|
+
# the all case. Specify nil for either array to prevent filtering using
|
163
|
+
# that criteria.
|
164
|
+
#
|
165
|
+
# See basis for more detail regarding the scope of 'all documents' that
|
166
|
+
# can be found via find.
|
167
|
+
def find(all={}, any=nil)
|
168
|
+
update_index
|
169
|
+
|
170
|
+
repo.index.select(
|
171
|
+
:basis => basis,
|
172
|
+
:all => all,
|
173
|
+
:any => any,
|
174
|
+
:shas => true
|
175
|
+
).collect! {|sha| self[sha] }
|
176
|
+
end
|
177
|
+
|
178
|
+
def delete(sha)
|
179
|
+
doc = self[sha]
|
180
|
+
doc.delete
|
181
|
+
doc
|
182
|
+
end
|
183
|
+
|
184
|
+
# Performs a partial update of the document index. All documents added
|
185
|
+
# between the index-head and the repo-head are updated using this
|
186
|
+
# method.
|
187
|
+
#
|
188
|
+
# Specify reindex to clobber and completely rebuild the index.
|
189
|
+
def update_index(reindex=false)
|
190
|
+
index = repo.index
|
191
|
+
index.clear if reindex
|
192
|
+
git_head, index_head = repo.git.head, index.head
|
193
|
+
|
194
|
+
# if the index is up-to-date save the work of doing diff
|
195
|
+
if git_head.nil? || git_head == index_head
|
196
|
+
return []
|
197
|
+
end
|
198
|
+
|
199
|
+
shas = repo.diff(index_head, git_head)
|
200
|
+
shas.each do |source|
|
201
|
+
doc = self[source]
|
202
|
+
doc.reindex
|
203
|
+
|
204
|
+
repo.each_assoc(source) do |target, type|
|
205
|
+
index.assoc(source, target, type)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
index.write(git_head)
|
210
|
+
shas
|
211
|
+
end
|
212
|
+
|
213
|
+
protected
|
214
|
+
|
215
|
+
# Returns the basis for finds, ie the set of documents that get filtered
|
216
|
+
# by the find method.
|
217
|
+
#
|
218
|
+
# If type is specified for self, then only documents of type will be
|
219
|
+
# available (ie Issue.find will only find documents of type 'issue').
|
220
|
+
# Document itself will filter all documents with an email; which should
|
221
|
+
# typically represent all possible documents.
|
222
|
+
def basis
|
223
|
+
type ? repo.index['type'][type] : repo.index.all('email')
|
224
|
+
end
|
225
|
+
|
226
|
+
# Registers self as the specified type. The previous registered type is
|
227
|
+
# overridden.
|
228
|
+
def register_as(type)
|
229
|
+
types.delete_if {|key, value| key == self || value == self }
|
230
|
+
types[type] = self
|
231
|
+
types[self] = type
|
232
|
+
end
|
233
|
+
|
234
|
+
# Turns on attribute definition for the duration of the block. If
|
235
|
+
# attribute definition is on, then the standard attribute declarations
|
236
|
+
# (attr_reader, attr_writer, attr_accessor) will create accessors for
|
237
|
+
# the attrs hash rather than instance variables.
|
238
|
+
#
|
239
|
+
# Moreover, blocks given to attr_writer/attr_accessor will be used to
|
240
|
+
# define a validator for the accessor.
|
241
|
+
def define_attributes(&block)
|
242
|
+
begin
|
243
|
+
@define_attributes = true
|
244
|
+
instance_eval(&block)
|
245
|
+
ensure
|
246
|
+
@define_attributes = false
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def attr_reader(*keys) # :nodoc:
|
251
|
+
return super unless @define_attributes
|
252
|
+
keys.each do |key|
|
253
|
+
key = key.to_s
|
254
|
+
define_method(key) { attrs[key] }
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def attr_writer(*keys, &block) # :nodoc:
|
259
|
+
return super unless @define_attributes
|
260
|
+
keys.each do |key|
|
261
|
+
key = key.to_s
|
262
|
+
define_method("#{key}=") {|value| attrs[key] = value }
|
263
|
+
validate(key, &block) if block_given?
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def attr_accessor(*keys, &block) # :nodoc:
|
268
|
+
return super unless @define_attributes
|
269
|
+
attr_reader(*keys)
|
270
|
+
attr_writer(*keys, &block)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Registers the validator method to validate the specified attribute. If
|
274
|
+
# a block is given, it will be used to define the validator as a
|
275
|
+
# protected instance method (otherwise you need to define the validator
|
276
|
+
# method manually).
|
277
|
+
def validate(key, validator="validate_#{key}", &block)
|
278
|
+
validators[key.to_s] = validator.to_sym
|
279
|
+
|
280
|
+
if block_given?
|
281
|
+
define_method(validator, &block)
|
282
|
+
protected validator
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
AUTHOR = /\A.*?<.*?>\z/
|
288
|
+
DATE = /\A\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d-\d\d:\d\d\z/
|
289
|
+
SHA = Git::SHA
|
290
|
+
|
291
|
+
@define_attributes = false
|
292
|
+
@validators = {}
|
293
|
+
@types = {}
|
294
|
+
register_as(nil)
|
295
|
+
|
296
|
+
# The repo this document belongs to.
|
297
|
+
attr_reader :repo
|
298
|
+
|
299
|
+
# A hash of the document attributes, corresponding to what is stored in
|
300
|
+
# the repo.
|
301
|
+
attr_reader :attrs
|
302
|
+
|
303
|
+
# The document sha, unset until the document is saved.
|
304
|
+
attr_reader :sha
|
305
|
+
|
306
|
+
validate(:author) {|author| validate_format(author, AUTHOR) }
|
307
|
+
validate(:date) {|date| validate_format(date, DATE) }
|
308
|
+
|
309
|
+
define_attributes do
|
310
|
+
attr_accessor(:at) {|at| validate_format_or_nil(at, SHA) }
|
311
|
+
attr_writer(:tags) {|tags| validate_array_or_nil(tags) }
|
312
|
+
attr_accessor(:type)
|
313
|
+
end
|
314
|
+
|
315
|
+
def initialize(attrs={}, repo=nil, sha=nil)
|
316
|
+
@repo = repo || Repo.current
|
317
|
+
@attrs = attrs
|
318
|
+
reset(sha)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Returns the repo index.
|
322
|
+
def index
|
323
|
+
repo.index
|
324
|
+
end
|
325
|
+
|
326
|
+
def idx
|
327
|
+
sha ? index.idx(sha) : nil
|
328
|
+
end
|
329
|
+
|
330
|
+
def graph_head_idx
|
331
|
+
index.graph_head_idx(idx)
|
332
|
+
end
|
333
|
+
|
334
|
+
def graph_head?
|
335
|
+
graph_head_idx == idx
|
336
|
+
end
|
337
|
+
|
338
|
+
def graph_head
|
339
|
+
idx = graph_head_idx
|
340
|
+
idx ? index.list[idx] : nil
|
341
|
+
end
|
342
|
+
|
343
|
+
def graph
|
344
|
+
@graph ||= repo.graph(graph_head)
|
345
|
+
end
|
346
|
+
|
347
|
+
def node
|
348
|
+
graph[sha]
|
349
|
+
end
|
350
|
+
|
351
|
+
# Gets the specified attribute.
|
352
|
+
def [](key)
|
353
|
+
attrs[key]
|
354
|
+
end
|
355
|
+
|
356
|
+
# Sets the specified attribute.
|
357
|
+
def []=(key, value)
|
358
|
+
attrs[key] = value
|
359
|
+
end
|
360
|
+
|
361
|
+
def author=(author)
|
362
|
+
if author.kind_of?(Grit::Actor)
|
363
|
+
email = author.email
|
364
|
+
author = blank?(email) ? author.name : "#{author.name} <#{email}>".lstrip
|
365
|
+
end
|
366
|
+
self['author'] = author
|
367
|
+
end
|
368
|
+
|
369
|
+
def author(cast=true)
|
370
|
+
author = attrs['author']
|
371
|
+
if cast && author.kind_of?(String)
|
372
|
+
author = Grit::Actor.from_string(author)
|
373
|
+
end
|
374
|
+
author
|
375
|
+
end
|
376
|
+
|
377
|
+
def date=(date)
|
378
|
+
if date.respond_to?(:iso8601)
|
379
|
+
date = date.iso8601
|
380
|
+
end
|
381
|
+
self['date'] = date
|
382
|
+
end
|
383
|
+
|
384
|
+
def date(cast=true)
|
385
|
+
date = attrs['date']
|
386
|
+
if cast && date.kind_of?(String)
|
387
|
+
date = Time.parse(date)
|
388
|
+
end
|
389
|
+
date
|
390
|
+
end
|
391
|
+
|
392
|
+
def active?(commit=nil)
|
393
|
+
return true if at.nil? || commit.nil?
|
394
|
+
repo.rev_list(commit).include?(at)
|
395
|
+
end
|
396
|
+
|
397
|
+
def tags
|
398
|
+
self['tags'] ||= []
|
399
|
+
end
|
400
|
+
|
401
|
+
def summary
|
402
|
+
sha
|
403
|
+
end
|
404
|
+
|
405
|
+
def merge(attrs)
|
406
|
+
dup.merge!(attrs)
|
407
|
+
end
|
408
|
+
|
409
|
+
def merge!(attrs)
|
410
|
+
self.attrs.merge!(attrs)
|
411
|
+
self
|
412
|
+
end
|
413
|
+
|
414
|
+
def normalize
|
415
|
+
dup.normalize!
|
416
|
+
end
|
417
|
+
|
418
|
+
def normalize!
|
419
|
+
self.author ||= repo.git.author
|
420
|
+
self.date ||= Time.now
|
421
|
+
|
422
|
+
if at = attrs['at']
|
423
|
+
attrs['at'] = repo.resolve(at)
|
424
|
+
end
|
425
|
+
|
426
|
+
if tags = attrs['tags']
|
427
|
+
tags = arrayify(tags)
|
428
|
+
tags.delete_if {|tag| tag.to_s.empty? }
|
429
|
+
attrs['tags'] = tags
|
430
|
+
end
|
431
|
+
|
432
|
+
unless type = attrs['type']
|
433
|
+
default_type = self.class.type
|
434
|
+
attrs['type'] = default_type if default_type
|
435
|
+
end
|
436
|
+
|
437
|
+
self
|
438
|
+
end
|
439
|
+
|
440
|
+
def errors
|
441
|
+
errors = {}
|
442
|
+
self.class.validators.each_pair do |key, validator|
|
443
|
+
begin
|
444
|
+
send(validator, attrs[key])
|
445
|
+
rescue
|
446
|
+
errors[key] = $!
|
447
|
+
end
|
448
|
+
end
|
449
|
+
errors
|
450
|
+
end
|
451
|
+
|
452
|
+
def validate(normalize=true)
|
453
|
+
normalize! if normalize
|
454
|
+
|
455
|
+
errors = self.errors
|
456
|
+
unless errors.empty?
|
457
|
+
raise InvalidDocumentError.new(self, errors)
|
458
|
+
end
|
459
|
+
self
|
460
|
+
end
|
461
|
+
|
462
|
+
def indexes
|
463
|
+
indexes = []
|
464
|
+
each_index {|key, value| indexes << [key, value] }
|
465
|
+
indexes
|
466
|
+
end
|
467
|
+
|
468
|
+
def each_index
|
469
|
+
if author = attrs['author']
|
470
|
+
email = Grit::Actor.from_string(author).email
|
471
|
+
yield('email', blank?(email) ? 'unknown' : email)
|
472
|
+
end
|
473
|
+
|
474
|
+
if date = attrs['date']
|
475
|
+
# reformats iso8601 as YYYYMMDD
|
476
|
+
yield('date', "#{date[0,4]}#{date[5,2]}#{date[8,2]}")
|
477
|
+
end
|
478
|
+
|
479
|
+
if at = attrs['at']
|
480
|
+
yield('at', at)
|
481
|
+
end
|
482
|
+
|
483
|
+
if tags = attrs['tags']
|
484
|
+
tags.each do |tag|
|
485
|
+
yield('tags', tag)
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
if type = attrs['type']
|
490
|
+
yield('type', type)
|
491
|
+
end
|
492
|
+
|
493
|
+
self
|
494
|
+
end
|
495
|
+
|
496
|
+
# Validates and saves attrs into the repo, then resets self with the
|
497
|
+
# resulting sha. Returns self.
|
498
|
+
def save
|
499
|
+
validate
|
500
|
+
reset repo.save(attrs)
|
501
|
+
end
|
502
|
+
|
503
|
+
# Returns true if sha is set.
|
504
|
+
def saved?
|
505
|
+
sha.nil? ? false : true
|
506
|
+
end
|
507
|
+
|
508
|
+
# Stores self as a new graph head. Returns self.
|
509
|
+
def create
|
510
|
+
unless saved?
|
511
|
+
raise "cannot create unless saved"
|
512
|
+
end
|
513
|
+
|
514
|
+
index.create(sha)
|
515
|
+
repo.create(sha)
|
516
|
+
self
|
517
|
+
end
|
518
|
+
|
519
|
+
# Updates self with the new document. Returns self.
|
520
|
+
def update(new_doc)
|
521
|
+
unless saved?
|
522
|
+
raise "cannot update unless saved"
|
523
|
+
end
|
524
|
+
|
525
|
+
unless new_doc.saved?
|
526
|
+
raise "cannot update with an unsaved document: #{new_doc.inspect}"
|
527
|
+
end
|
528
|
+
|
529
|
+
new_sha = new_doc.sha
|
530
|
+
if repo.assoc_type(sha, new_sha) == :link
|
531
|
+
raise "cannot update with a child of self: #{sha} -> #{new_sha}"
|
532
|
+
end
|
533
|
+
|
534
|
+
index.update(sha, new_sha)
|
535
|
+
repo.update(sha, new_sha)
|
536
|
+
|
537
|
+
new_doc.reset
|
538
|
+
reset
|
539
|
+
end
|
540
|
+
|
541
|
+
def update_to(*old_docs)
|
542
|
+
originals = old_docs.collect {|old_doc| old_doc.node.original }.uniq
|
543
|
+
|
544
|
+
unless originals.length == 1
|
545
|
+
old_docs.collect! {|old_doc| old_doc.sha }
|
546
|
+
raise "cannot update unrelated documents: #{old_docs.inspect}"
|
547
|
+
end
|
548
|
+
|
549
|
+
old_docs.each do |old_doc|
|
550
|
+
old_doc.update(self)
|
551
|
+
end
|
552
|
+
self
|
553
|
+
end
|
554
|
+
|
555
|
+
# Links the child document to self. Returns self.
|
556
|
+
def link(child)
|
557
|
+
unless saved?
|
558
|
+
raise "cannot link unless saved"
|
559
|
+
end
|
560
|
+
|
561
|
+
unless child.saved?
|
562
|
+
raise "cannot link to an unsaved document: #{child.inspect}"
|
563
|
+
end
|
564
|
+
|
565
|
+
child_sha = child.sha
|
566
|
+
if repo.assoc_type(sha, child_sha) == :update
|
567
|
+
raise "cannot link to an update of self: #{sha} -> #{child_sha}"
|
568
|
+
end
|
569
|
+
|
570
|
+
index.link(sha, child_sha)
|
571
|
+
repo.link(sha, child_sha)
|
572
|
+
|
573
|
+
child.reset
|
574
|
+
reset
|
575
|
+
end
|
576
|
+
|
577
|
+
def link_to(*parents)
|
578
|
+
graph_heads = parents.collect {|parent| parent.graph_head }.uniq
|
579
|
+
|
580
|
+
unless graph_heads.length == 1
|
581
|
+
parents.collect! {|parent| parent.sha }
|
582
|
+
raise "cannot link to unrelated documents: #{parents.inspect}"
|
583
|
+
end
|
584
|
+
|
585
|
+
parents.each do |parent|
|
586
|
+
parent.link(self)
|
587
|
+
end
|
588
|
+
self
|
589
|
+
end
|
590
|
+
|
591
|
+
# Deletes self. Delete raises an error if unsaved. Returns self.
|
592
|
+
def delete
|
593
|
+
unless saved?
|
594
|
+
raise "cannot delete unless saved"
|
595
|
+
end
|
596
|
+
|
597
|
+
index.delete(sha)
|
598
|
+
repo.delete(sha)
|
599
|
+
self
|
600
|
+
end
|
601
|
+
|
602
|
+
def reindex
|
603
|
+
raise "cannot reindex unless saved" unless saved?
|
604
|
+
|
605
|
+
idx = self.idx
|
606
|
+
each_index do |key, value|
|
607
|
+
index[key][value] << idx
|
608
|
+
end
|
609
|
+
|
610
|
+
self
|
611
|
+
end
|
612
|
+
|
613
|
+
def reset(new_sha=sha)
|
614
|
+
@sha = new_sha
|
615
|
+
@graph = nil
|
616
|
+
self
|
617
|
+
end
|
618
|
+
|
619
|
+
def commit!
|
620
|
+
repo.commit!
|
621
|
+
self
|
622
|
+
end
|
623
|
+
|
624
|
+
def initialize_copy(orig)
|
625
|
+
super
|
626
|
+
reset(nil)
|
627
|
+
@attrs = orig.attrs.dup
|
628
|
+
end
|
629
|
+
|
630
|
+
def inspect
|
631
|
+
"#<#{self.class}:#{object_id} sha=#{sha.inspect}>"
|
632
|
+
end
|
633
|
+
|
634
|
+
# This is a thin equality -- use with caution.
|
635
|
+
def ==(another)
|
636
|
+
saved? && another.kind_of?(Document) ? sha == another.sha : super
|
637
|
+
end
|
638
|
+
|
639
|
+
protected
|
640
|
+
|
641
|
+
def arrayify(obj)
|
642
|
+
case obj
|
643
|
+
when Array then obj
|
644
|
+
when nil then []
|
645
|
+
when String then obj.strip.empty? ? [] : [obj]
|
646
|
+
else [obj]
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
def blank?(obj)
|
651
|
+
obj.nil? || obj.to_s.strip.empty?
|
652
|
+
end
|
653
|
+
|
654
|
+
def validate_not_blank(str)
|
655
|
+
if blank?(str)
|
656
|
+
raise 'nothing specified'
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
def validate_format(value, format)
|
661
|
+
if value.nil?
|
662
|
+
raise 'missing'
|
663
|
+
end
|
664
|
+
|
665
|
+
unless value =~ format
|
666
|
+
raise 'misformatted'
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
def validate_format_or_nil(value, format)
|
671
|
+
value.nil? || validate_format(value, format)
|
672
|
+
end
|
673
|
+
|
674
|
+
def validate_array_or_nil(value)
|
675
|
+
unless value.nil? || value.kind_of?(Array)
|
676
|
+
raise 'not an array'
|
677
|
+
end
|
678
|
+
end
|
679
|
+
end
|
680
|
+
end
|