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