Class: Gitgo::Document

Inherits:
Object
  • Object
show all
Defined in:
lib/gitgo/document.rb,
lib/gitgo/document/invalid_document_error.rb

Overview

Document represents the data model of Gitgo, and provides high(er)-level access to documents stored in a Repo. Content and data consistency constraints are enforced on Document and not on Repo. As such, Document should be the only way casual users enter data into a Repo.

Usage

For the most part Document behaves like a standard ORM model. The primary gotcha revolves around setting documents into the git repository and exists to prevent the creation of unnecessary git objects.

Unlike you would expect, two method calls are required to store a document:

a = Document.new(:content => 'a')
a.save
a.create

The save method sets the document data into the git repo as a blob and records the blob sha as a unique identifier for the document. The create method is what indicates the document is the head of a new document graph. Simply calling save is not enough (indeed the result of save is a hanging blob that can be gc’d by git).

The link and update methods are used instead of create to associate new documents into an existing graph. For example:

b = Document.new(:content => 'b')
b.save
a.link(b)

Calling create prevents a document from being linked into another graph and vice-versa; the intent is that a given document only belongs to one document graph. This constraint is only enforced at the Document level and represents the main reason why using repo directy is a no-no.

Additionally, as in the command-line git workflow, newly added documents are not actually committed to a repo until commit is called.

Defined Under Namespace

Classes: InvalidDocumentError

Constant Summary collapse

AUTHOR =
/\A.*?<.*?>\z/
DATE =
/\A\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d-\d\d:\d\d\z/
SHA =
Git::SHA

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(attrs = {}, repo = nil, sha = nil) ⇒ Document

Returns a new instance of Document.



315
316
317
318
319
# File 'lib/gitgo/document.rb', line 315

def initialize(attrs={}, repo=nil, sha=nil)
  @repo = repo || Repo.current
  @attrs = attrs
  reset(sha)
end

Class Attribute Details

.typesObject (readonly)

Returns a hash registry mapping a type string to a Document class. Document itself is registered as the nil type. Types also includes reverse mappings for a Document class to it’s type string.



50
51
52
# File 'lib/gitgo/document.rb', line 50

def types
  @types
end

.validatorsObject (readonly)

A hash of (key, validator) pairs mapping attribute keys to a validation method. Not all attributes will have a validator whereas some attributes share the same validation method.



55
56
57
# File 'lib/gitgo/document.rb', line 55

def validators
  @validators
end

Instance Attribute Details

#attrsObject (readonly)

A hash of the document attributes, corresponding to what is stored in the repo.



301
302
303
# File 'lib/gitgo/document.rb', line 301

def attrs
  @attrs
end

#repoObject (readonly)

The repo this document belongs to.



297
298
299
# File 'lib/gitgo/document.rb', line 297

def repo
  @repo
end

#shaObject (readonly)

The document sha, unset until the document is saved.



304
305
306
# File 'lib/gitgo/document.rb', line 304

def sha
  @sha
end

Class Method Details

.[](sha) ⇒ Object

Reads the specified document from the repo cache and casts it into an instance as per cast. Returns nil if the document doesn’t exist.



110
111
112
113
114
115
# File 'lib/gitgo/document.rb', line 110

def [](sha)
  sha = repo.resolve(sha)
  attrs = repo[sha]
  
  attrs ? cast(attrs, sha) : nil
end

.cast(attrs, sha) ⇒ Object

Casts the attributes hash into a document instance. The document class is determined by resolving the ‘type’ attribute against the types registry.



120
121
122
123
124
# File 'lib/gitgo/document.rb', line 120

def cast(attrs, sha)
  type = attrs['type']
  klass = types[type] or raise "unknown type: #{type}"
  klass.new(attrs, repo, sha)
end

.create(attrs = {}) ⇒ Object

Creates a new document with the attrs. The document is saved, created, and indexed before being returned.



86
87
88
89
90
91
# File 'lib/gitgo/document.rb', line 86

def create(attrs={})
  update_index
  doc = save(attrs)
  doc.create
  doc
end

.delete(sha) ⇒ Object



178
179
180
181
182
# File 'lib/gitgo/document.rb', line 178

def delete(sha)
  doc = self[sha]
  doc.delete
  doc
end

.find(all = {}, any = nil) ⇒ Object

Finds all documents matching the any and all criteria. The any and all inputs are hashes of index values used to filter all possible documents. They consist of (key, value) or (key, [values]) pairs. At least one of pair must match in the any case. All pairs must match in the all case. Specify nil for either array to prevent filtering using that criteria.

See basis for more detail regarding the scope of ‘all documents’ that can be found via find.



167
168
169
170
171
172
173
174
175
176
# File 'lib/gitgo/document.rb', line 167

def find(all={}, any=nil)
  update_index
  
  repo.index.select(
    :basis => basis, 
    :all => all, 
    :any => any, 
    :shas => true
  ).collect! {|sha| self[sha] }
end

.inherited(base) ⇒ Object

:nodoc:



57
58
59
60
61
# File 'lib/gitgo/document.rb', line 57

def inherited(base) # :nodoc:
  base.instance_variable_set(:@validators, validators.dup)
  base.instance_variable_set(:@types, types)
  base.register_as base.to_s.split('::').last.downcase
end

.read(sha) ⇒ Object

Reads the specified document and casts it into an instance as per cast. Returns nil if the document doesn’t exist.

Usage Note

Read will re-read the document directly from the git repository every time it is called. For better performance, use the AGET method which performs the same read but uses the Repo cache if possible.



101
102
103
104
105
106
# File 'lib/gitgo/document.rb', line 101

def read(sha)
  sha = repo.resolve(sha)
  attrs = repo.read(sha)
  
  attrs ? cast(attrs, sha) : nil
end

.repoObject

Returns the Repo currently in scope (see Repo.current)



64
65
66
# File 'lib/gitgo/document.rb', line 64

def repo
  Repo.current
end

.save(attrs = {}) ⇒ Object

Creates a new document with the attributes and saves. Saved documents are not automatically associated with a document graph and must be associated with one via create/update/link to be permanently stored in the repo.



77
78
79
80
81
82
# File 'lib/gitgo/document.rb', line 77

def save(attrs={})
  doc = new(attrs, repo)
  doc.save
  doc.reindex
  doc
end

.typeObject

Returns the type string for self.



69
70
71
# File 'lib/gitgo/document.rb', line 69

def type
  types[self]
end

.update(old_doc, attrs = {}) ⇒ Object

Updates and indexes the old document with new attributes. The new attributes are merged with the current doc attributes. Returns the new document.

The new document can be used to update other documents, if necessary, as when resolving forks in an update graph:

a = Document.create(:content => 'a')
b = Document.update(a, :content => 'b')
c = Document.update(a, :content => 'c')

d = Document.update(b, :content => 'd')
c.update(d)

a.reset
a.node.versions.uniq    # => [d.sha]


143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/gitgo/document.rb', line 143

def update(old_doc, attrs={})
  update_index
  
  unless old_doc.kind_of?(Document)
    old_doc = Document[old_doc]
  end
  
  new_doc = old_doc.merge(attrs)
  new_doc.save
  new_doc.reindex
  
  old_doc.update(new_doc)
  new_doc
end

.update_index(reindex = false) ⇒ Object

Performs a partial update of the document index. All documents added between the index-head and the repo-head are updated using this method.

Specify reindex to clobber and completely rebuild the index.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/gitgo/document.rb', line 189

def update_index(reindex=false)
  index = repo.index
  index.clear if reindex
  git_head, index_head = repo.git.head, index.head
  
  # if the index is up-to-date save the work of doing diff
  if git_head.nil? || git_head == index_head
    return []
  end
  
  shas = repo.diff(index_head, git_head)
  shas.each do |source|
    doc = self[source]
    doc.reindex
    
    repo.each_assoc(source) do |target, type|
      index.assoc(source, target, type)
    end
  end
  
  index.write(git_head)
  shas
end

Instance Method Details

#==(another) ⇒ Object

This is a thin equality – use with caution.



635
636
637
# File 'lib/gitgo/document.rb', line 635

def ==(another)
  saved? && another.kind_of?(Document) ? sha == another.sha : super
end

#[](key) ⇒ Object

Gets the specified attribute.



352
353
354
# File 'lib/gitgo/document.rb', line 352

def [](key)
  attrs[key]
end

#[]=(key, value) ⇒ Object

Sets the specified attribute.



357
358
359
# File 'lib/gitgo/document.rb', line 357

def []=(key, value)
  attrs[key] = value
end

#active?(commit = nil) ⇒ Boolean

Returns:

  • (Boolean)


392
393
394
395
# File 'lib/gitgo/document.rb', line 392

def active?(commit=nil)
  return true if at.nil? || commit.nil?
  repo.rev_list(commit).include?(at)
end

#author(cast = true) ⇒ Object



369
370
371
372
373
374
375
# File 'lib/gitgo/document.rb', line 369

def author(cast=true)
  author = attrs['author']
  if cast && author.kind_of?(String)
    author = Grit::Actor.from_string(author)
  end
  author
end

#author=(author) ⇒ Object



361
362
363
364
365
366
367
# File 'lib/gitgo/document.rb', line 361

def author=(author)
  if author.kind_of?(Grit::Actor)
    email = author.email
    author = blank?(email) ? author.name : "#{author.name} <#{email}>".lstrip
  end
  self['author'] = author
end

#commit!Object



619
620
621
622
# File 'lib/gitgo/document.rb', line 619

def commit!
  repo.commit!
  self
end

#createObject

Stores self as a new graph head. Returns self.



509
510
511
512
513
514
515
516
517
# File 'lib/gitgo/document.rb', line 509

def create
  unless saved?
    raise "cannot create unless saved"
  end
  
  index.create(sha)
  repo.create(sha)
  self
end

#date(cast = true) ⇒ Object



384
385
386
387
388
389
390
# File 'lib/gitgo/document.rb', line 384

def date(cast=true)
  date = attrs['date']
  if cast && date.kind_of?(String)
    date = Time.parse(date)
  end
  date
end

#date=(date) ⇒ Object



377
378
379
380
381
382
# File 'lib/gitgo/document.rb', line 377

def date=(date)
  if date.respond_to?(:iso8601)
    date = date.iso8601
  end
  self['date'] = date
end

#deleteObject

Deletes self. Delete raises an error if unsaved. Returns self.



592
593
594
595
596
597
598
599
600
# File 'lib/gitgo/document.rb', line 592

def delete
  unless saved?
    raise "cannot delete unless saved"
  end
  
  index.delete(sha)
  repo.delete(sha)
  self
end

#each_indexObject



468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/gitgo/document.rb', line 468

def each_index
  if author = attrs['author']
    email = Grit::Actor.from_string(author).email
    yield('email', blank?(email) ? 'unknown' : email)
  end
  
  if date = attrs['date']
    # reformats iso8601 as YYYYMMDD
    yield('date', "#{date[0,4]}#{date[5,2]}#{date[8,2]}")
  end
  
  if at = attrs['at']
    yield('at', at)
  end
  
  if tags = attrs['tags']
    tags.each do |tag|
      yield('tags', tag)
    end
  end
  
  if type = attrs['type']
    yield('type', type)
  end
  
  self
end

#errorsObject



440
441
442
443
444
445
446
447
448
449
450
# File 'lib/gitgo/document.rb', line 440

def errors
  errors = {}
  self.class.validators.each_pair do |key, validator|
    begin
      send(validator, attrs[key])
    rescue
      errors[key] = $!
    end
  end
  errors
end

#graphObject



343
344
345
# File 'lib/gitgo/document.rb', line 343

def graph
  @graph ||= repo.graph(graph_head)
end

#graph_headObject



338
339
340
341
# File 'lib/gitgo/document.rb', line 338

def graph_head
  idx = graph_head_idx
  idx ? index.list[idx] : nil
end

#graph_head?Boolean

Returns:

  • (Boolean)


334
335
336
# File 'lib/gitgo/document.rb', line 334

def graph_head?
  graph_head_idx == idx
end

#graph_head_idxObject



330
331
332
# File 'lib/gitgo/document.rb', line 330

def graph_head_idx
  index.graph_head_idx(idx)
end

#idxObject



326
327
328
# File 'lib/gitgo/document.rb', line 326

def idx
  sha ? index.idx(sha) : nil
end

#indexObject

Returns the repo index.



322
323
324
# File 'lib/gitgo/document.rb', line 322

def index
  repo.index
end

#indexesObject



462
463
464
465
466
# File 'lib/gitgo/document.rb', line 462

def indexes
  indexes = []
  each_index {|key, value| indexes << [key, value] }
  indexes
end

#initialize_copy(orig) ⇒ Object



624
625
626
627
628
# File 'lib/gitgo/document.rb', line 624

def initialize_copy(orig)
  super
  reset(nil)
  @attrs = orig.attrs.dup
end

#inspectObject



630
631
632
# File 'lib/gitgo/document.rb', line 630

def inspect
  "#<#{self.class}:#{object_id} sha=#{sha.inspect}>"
end

Links the child document to self. Returns self.



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/gitgo/document.rb', line 556

def link(child)
  unless saved?
    raise "cannot link unless saved"
  end
  
  unless child.saved?
    raise "cannot link to an unsaved document: #{child.inspect}" 
  end
  
  child_sha = child.sha
  if repo.assoc_type(sha, child_sha) == :update
    raise "cannot link to an update of self: #{sha} -> #{child_sha}"
  end
  
  index.link(sha, child_sha)
  repo.link(sha, child_sha)
  
  child.reset
  reset
end


577
578
579
580
581
582
583
584
585
586
587
588
589
# File 'lib/gitgo/document.rb', line 577

def link_to(*parents)
  graph_heads = parents.collect {|parent| parent.graph_head }.uniq
  
  unless graph_heads.length == 1
    parents.collect! {|parent| parent.sha }
    raise "cannot link to unrelated documents: #{parents.inspect}"
  end
  
  parents.each do |parent|
    parent.link(self)
  end
  self
end

#merge(attrs) ⇒ Object



405
406
407
# File 'lib/gitgo/document.rb', line 405

def merge(attrs)
  dup.merge!(attrs)
end

#merge!(attrs) ⇒ Object



409
410
411
412
# File 'lib/gitgo/document.rb', line 409

def merge!(attrs)
  self.attrs.merge!(attrs)
  self
end

#nodeObject



347
348
349
# File 'lib/gitgo/document.rb', line 347

def node
  graph[sha]
end

#normalizeObject



414
415
416
# File 'lib/gitgo/document.rb', line 414

def normalize
  dup.normalize!
end

#normalize!Object



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/gitgo/document.rb', line 418

def normalize!
  self.author ||= repo.git.author
  self.date   ||= Time.now
  
  if at = attrs['at']
    attrs['at'] = repo.resolve(at)
  end
  
  if tags = attrs['tags']
    tags = arrayify(tags)
    tags.delete_if {|tag| tag.to_s.empty? }
    attrs['tags'] = tags
  end
  
  unless type = attrs['type']
    default_type = self.class.type
    attrs['type'] = default_type if default_type
  end
  
  self
end

#reindexObject



602
603
604
605
606
607
608
609
610
611
# File 'lib/gitgo/document.rb', line 602

def reindex
  raise "cannot reindex unless saved" unless saved?
  
  idx = self.idx
  each_index do |key, value|
    index[key][value] << idx
  end
  
  self
end

#reset(new_sha = sha) ⇒ Object



613
614
615
616
617
# File 'lib/gitgo/document.rb', line 613

def reset(new_sha=sha)
  @sha = new_sha
  @graph = nil
  self
end

#saveObject

Validates and saves attrs into the repo, then resets self with the resulting sha. Returns self.



498
499
500
501
# File 'lib/gitgo/document.rb', line 498

def save
  validate
  reset repo.save(attrs)
end

#saved?Boolean

Returns true if sha is set.

Returns:

  • (Boolean)


504
505
506
# File 'lib/gitgo/document.rb', line 504

def saved?
  sha.nil? ? false : true
end

#summaryObject



401
402
403
# File 'lib/gitgo/document.rb', line 401

def summary
  sha
end

#tagsObject



397
398
399
# File 'lib/gitgo/document.rb', line 397

def tags
  self['tags'] ||= []
end

#update(new_doc) ⇒ Object

Updates self with the new document. Returns self.



520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/gitgo/document.rb', line 520

def update(new_doc)
  unless saved?
    raise "cannot update unless saved"
  end
  
  unless new_doc.saved?
    raise "cannot update with an unsaved document: #{new_doc.inspect}" 
  end
  
  new_sha = new_doc.sha
  if repo.assoc_type(sha, new_sha) == :link
    raise "cannot update with a child of self: #{sha} -> #{new_sha}"
  end
  
  index.update(sha, new_sha)
  repo.update(sha, new_sha)
  
  new_doc.reset
  reset
end

#update_to(*old_docs) ⇒ Object



541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/gitgo/document.rb', line 541

def update_to(*old_docs)
  originals = old_docs.collect {|old_doc| old_doc.node.original }.uniq
  
  unless originals.length == 1
    old_docs.collect! {|old_doc| old_doc.sha }
    raise "cannot update unrelated documents: #{old_docs.inspect}"
  end
  
  old_docs.each do |old_doc|
    old_doc.update(self)
  end
  self
end

#validate(normalize = true) ⇒ Object



452
453
454
455
456
457
458
459
460
# File 'lib/gitgo/document.rb', line 452

def validate(normalize=true)
  normalize! if normalize
  
  errors = self.errors
  unless errors.empty?
    raise InvalidDocumentError.new(self, errors)
  end
  self
end