gitgo 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/History +44 -0
  2. data/License.txt +22 -0
  3. data/README +45 -0
  4. data/bin/gitgo +4 -0
  5. data/lib/gitgo.rb +1 -0
  6. data/lib/gitgo/app.rb +63 -0
  7. data/lib/gitgo/controller.rb +89 -0
  8. data/lib/gitgo/controllers/code.rb +198 -0
  9. data/lib/gitgo/controllers/issue.rb +76 -0
  10. data/lib/gitgo/controllers/repo.rb +186 -0
  11. data/lib/gitgo/controllers/wiki.rb +19 -0
  12. data/lib/gitgo/document.rb +680 -0
  13. data/lib/gitgo/document/invalid_document_error.rb +34 -0
  14. data/lib/gitgo/documents/comment.rb +20 -0
  15. data/lib/gitgo/documents/issue.rb +56 -0
  16. data/lib/gitgo/git.rb +941 -0
  17. data/lib/gitgo/git/tree.rb +315 -0
  18. data/lib/gitgo/git/utils.rb +59 -0
  19. data/lib/gitgo/helper.rb +3 -0
  20. data/lib/gitgo/helper/doc.rb +28 -0
  21. data/lib/gitgo/helper/form.rb +88 -0
  22. data/lib/gitgo/helper/format.rb +200 -0
  23. data/lib/gitgo/helper/html.rb +19 -0
  24. data/lib/gitgo/helper/utils.rb +85 -0
  25. data/lib/gitgo/index.rb +421 -0
  26. data/lib/gitgo/index/idx_file.rb +119 -0
  27. data/lib/gitgo/index/sha_file.rb +135 -0
  28. data/lib/gitgo/patches/grit.rb +47 -0
  29. data/lib/gitgo/repo.rb +626 -0
  30. data/lib/gitgo/repo/graph.rb +333 -0
  31. data/lib/gitgo/repo/node.rb +122 -0
  32. data/lib/gitgo/rest.rb +87 -0
  33. data/lib/gitgo/server.rb +114 -0
  34. data/lib/gitgo/version.rb +8 -0
  35. data/public/css/gitgo.css +24 -0
  36. data/public/javascript/gitgo.js +148 -0
  37. data/public/javascript/jquery-1.4.2.min.js +154 -0
  38. data/views/app/index.erb +4 -0
  39. data/views/app/timeline.erb +27 -0
  40. data/views/app/welcome.erb +13 -0
  41. data/views/code/_comment.erb +10 -0
  42. data/views/code/_comment_form.erb +14 -0
  43. data/views/code/_comments.erb +5 -0
  44. data/views/code/_commit.erb +25 -0
  45. data/views/code/_grepnav.erb +5 -0
  46. data/views/code/_treenav.erb +3 -0
  47. data/views/code/blob.erb +6 -0
  48. data/views/code/commit_grep.erb +35 -0
  49. data/views/code/commits.erb +11 -0
  50. data/views/code/diff.erb +10 -0
  51. data/views/code/grep.erb +32 -0
  52. data/views/code/index.erb +17 -0
  53. data/views/code/obj/blob.erb +4 -0
  54. data/views/code/obj/commit.erb +25 -0
  55. data/views/code/obj/tag.erb +25 -0
  56. data/views/code/obj/tree.erb +9 -0
  57. data/views/code/tree.erb +9 -0
  58. data/views/error.erb +19 -0
  59. data/views/issue/_issue.erb +15 -0
  60. data/views/issue/_issue_form.erb +39 -0
  61. data/views/issue/edit.erb +11 -0
  62. data/views/issue/index.erb +28 -0
  63. data/views/issue/new.erb +5 -0
  64. data/views/issue/show.erb +27 -0
  65. data/views/layout.erb +34 -0
  66. data/views/not_found.erb +1 -0
  67. data/views/repo/fsck.erb +29 -0
  68. data/views/repo/help.textile +5 -0
  69. data/views/repo/help/faq.textile +19 -0
  70. data/views/repo/help/howto.textile +31 -0
  71. data/views/repo/help/trouble.textile +28 -0
  72. data/views/repo/idx.erb +29 -0
  73. data/views/repo/index.erb +72 -0
  74. data/views/repo/status.erb +16 -0
  75. data/views/wiki/index.erb +3 -0
  76. metadata +253 -0
@@ -0,0 +1,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