Source

amp / lib / amp / repository / mercurial / repo_format / changeset.rb

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
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
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
#######################################################################
#                  Licensing Information                              #
#                                                                     #
#  The following code is a derivative work of the code from the       #
#  Mercurial project, which is licensed GPLv2. This code therefore    #
#  is also licensed under the terms of the GNU Public License,        #
#  verison 2.                                                         #
#                                                                     #
#  For information on the license of this code when distributed       #
#  with and used in conjunction with the other modules in the         #
#  Amp project, please see the root-level LICENSE file.               #
#                                                                     #
#  © Michael J. Edgar and Ari Brown, 2009-2010                        #
#                                                                     #
#######################################################################

module Amp
  module Mercurial
    
    ##
    # A Changeset is a simple way of accessing the repository within a certain
    # revision. For example, if the user specifies revision # 36, or revision
    # 3adf21, then we can look those up, and work within the repository at the
    # moment of that revision.
    class Changeset < Amp::Repositories::AbstractChangeset
      include Mercurial::RevlogSupport::Node
      
      attr_reader :repo
      alias_method :repository, :repo
      
      ##
      # Initializes a new changeset. We need a repository to work with, and also
      # a change_id. this change_id could be a revision index or a node_id for
      # the revision.
      #
      # @param [Repository] repo a repository to work with.
      # @param [Integer, String] change_id an ID or index to lookup to find this
      #   changeset.
      #
      def initialize(repo, change_id='')
        change_id = '.' if change_id == ''
        @repo = repo
        if change_id.kind_of? Integer
          @revision = change_id
          @node_id  = @repo.changelog.node_id_for_index change_id
        else
          @node_id = @repo.lookup change_id
          @revision = @repo.changelog.rev @node_id
        end
        @parents = nil
      end
      
      ##
      # Converts the revision to a number
      def to_i; @revision; end
      
      ##
      # Converts the revision to an easy-to-digest string
      def to_s(opts = {})
        if opts[:template]
          to_templated_s(opts)
        else
          @node_id[0..5].hexlify
        end
      end
      
      ##
      # Commits the given changeset to the repository.
      # 
      #  commit_changeset:
      #    foreach file in commit:
      #        commit_file file
      #    end
      #    add_manifest_entry
      #    add_changelog_entry
      # Is this changeset a working changeset?
      #
      # @param changeset the changeset to commit. Could be working dir, for
      #   example.
      # @param opts the options for committing the changeset.
      # @option opts [Boolean] :force (false) force the commit, even though
      #   nothing has changed.
      # @option opts [Boolean] :force_editor (false) force the user to open
      #   their editor, even though they provided a message already
      # @option opts [Boolean] :empty_ok (false) is it ok if they have no
      #   description of the commit?
      # @option opts [Boolean] :use_dirstate (true) use the DirState for this
      #   commit? Used if you're committing the working directory (typical)
      # @option opts [Boolean] :update_dirstate (true) should we update the
      #   DirState after the commit? Used if you're committing the working
      #   directory.
      # @return [String] the digest referring to this entry in the revlog
      def commit(opts = {:use_dirstate => true, :update_dirstate => true})
        valid = false # don't update the DirState if this is set!
        
        commit = ((modified || []) + (added || [])).sort
        remove = removed
        xtra = extra.dup
        branchname = xtra["branch"]
        text = description
        
        p1, p2 = parents.map {|p| p.node }
        c1 = repo.changelog.read(p1) # 1 parent's changeset as an array
        c2 = repo.changelog.read(p2) # 2nd parent's changeset as an array
        m1 = repo.manifest.read(c1[0]).dup # 1st parent's manifest
        m2 = repo.manifest.read(c2[0])     # 2nd parent's manifest
        
        if opts[:use_dirstate]
          oldname = c1[5]["branch"]
          tests = [ commit.empty?, remove.empty?, !opts[:force],
                    p2 == NULL_ID, branchname == oldname ]
          if tests.all?
            UI::status "nothing changed"
            return nil
          end
        end
        
        xp1 = p1.hexlify
        xp2 = p2 == NULL_ID ? "" : p2.hexlify
        
        Hook.run_hook :pre_commit
        journal = Amp::Mercurial::Journal.new(:opener => repo.store_opener)
  
        fresh    = {} # new = reserved haha i don't know why someone wrote "haha"
        changed  = []
        link_rev = repo.size
        
        (commit + (remove || [])).each {|file| UI::status file }
        
        # foreach file in commit:
        #     commit_file file
        # end
        commit.each do |file|
          versioned_file = self[file]
          fresh[file]    = versioned_file.commit :manifests     => [m1, m2],
                                                 :link_revision => link_rev,
                                                 :journal       => journal ,
                                                 :changed       => changed
          
          new_flags = versioned_file.flags
          
          # TODO
          # Clean this shit up
          if [ changed.empty? || changed.last != file, 
               m2[file] != fresh[file]
             ].all?
            changed << file if m1.flags[file] != new_flags
          end
          m1.flags[file] = new_flags
          
          repo.staging_area.normal file if opts[:use_dirstate]
        end
        
        #    add_manifest_entry
        man_entry, updated, added = *add_manifest_entry(:manifests  => [m1, m2],
                                                        :changesets => [c1, c2],
                                                        :journal    => journal ,
                                                        :link_rev   => link_rev,
                                                        :fresh      => fresh   ,
                                                        :remove     => remove  ,
                                                        :changed    => changed )

        #    get_commit_text
        text = get_commit_text text, :added   => added,   :updated => updated,
                                     :removed => removed, :user    => user   ,
                                     :empty_ok     => opts[:empty_ok]        ,
                                     :use_dirstate => opts[:use_dirstate]
        
        # atomically write to the changelog
        #    add_changelog_entry
        # for the unenlightened, rents = 'rents = parents
        new_rents = add_changelog_entry :manifest_entry => man_entry,
                                        :files   => (changed + removed),
                                        :text    => text,
                                        :journal => journal,
                                        :parents => [p1, p2],
                                        :user    => user,
                                        :date    => date,
                                        :extra   => xtra
      
        
        # Write the dirstate if it needs to be updated
        # basically just bring it up to speed
        if opts[:use_dirstate] || opts[:update_dirstate]
          repo.dirstate.parents = new_rents
          removed.each {|f| repo.dirstate.forget(f) } if opts[:use_dirstate]
          repo.staging_area.save
        end
      
        # The journal and dirstates are awesome. Leave them be.
        valid = true
        journal.close
        
        # if an error and we've gotten this far, then the journal is complete
        # and it deserves to stay (if an error is thrown and journal isn't nil,
        # the rescue will destroy it)
        journal = nil
        
        # Run any hooks
        Hook.run_hook :post_commit, :added => added, :modified => updated, :removed => removed, 
                                    :user  => user,  :date     => date,    :text    => text,
                                    :revision => repo.changelog.index_size
        return new_rents
      rescue StandardError => e
        if !valid
          repo.dirstate.invalidate!
        end
        if e.kind_of?(AbortError)
          UI::warn "Abort: #{e}"
        else
          UI::warn "Got exception while committing. #{e}"
          UI::warn e.backtrace.join("\n")
        end
        
        # the journal is a vestigial and incomplete file.
        # destroyzzzzzzzzzzz
        journal.delete if journal
      end
      
      ##
      # Add an entry to the changelog (the final receipt of the commit).
      # 
      # @param [Hash] opts
      # @return [String] the changelog id as to where the revision is in
      #   the changelog
      def add_changelog_entry(opts={})
        repo.changelog.delay_update 
        new_parents = repo.changelog.add opts[:manifest_entry],
                                         opts[:files],
                                         opts[:text],
                                         opts[:journal],
                                         opts[:parents][0],
                                         opts[:parents][1],
                                         opts[:user],
                                         opts[:date],
                                         opts[:extra]
  
        repo.changelog.write_pending
        repo.changelog.finalize opts[:journal]
        new_parents
      end
      
      ##
      # Get the commit text. Ask for it if none is given.
      # 
      # @param [String, NilClass] text (optional) the commit message
      # @param [Hash] opts
      def get_commit_text(text=nil, opts={})
        user = opts.delete :user
        
        unless opts[:empty_ok] || (text && !text.empty?)
          edit_text = to_templated_s :added   => added,   :updated       => modified,
                                     :removed => removed, :template_type => :commit
          text = UI::edit edit_text, user
        end
        
        lines = text.rstrip.split("\n").map {|r| r.rstrip }.reject {|l| l.empty? }
        raise abort("empty commit message") if lines.empty? && opts[:use_dirstate]
        lines.join("\n")
      end
      
      def add_manifest_entry(opts={})
        # changed, m1, m2, c1, c2, fresh, remove, journal, link_rev
        updated, added = [], []
        
        fresh   = opts[:fresh]
        remove  = opts[:remove]
        changed = opts[:changed]
        
        changesets = opts[:changesets]
        manifests  = opts[:manifests]
        
        changed.sort.each do |file|
          if manifests[0][file] || manifests[1][file]
            updated << file
          else
            added << file
          end
        end
        
        manifests[0].merge! fresh
        
        remove.sort!
        remove.reject! {|f| not manifests[0][f] }
        remove.each {|f| manifests[0].delete f }
        
        UI::debug "before adding manifest entry"
        
        # sorry for making this destructive
        # but it's clean and memory efficient
        # GHC's GC goes like 3 times per second, so STFU
        # I don't have that kind of luxury
        fresh.replace fresh.inject([]) {|a, (k, v)| v ? a << k : a }
        man_entry = repo.manifest.add manifests[0], opts[:journal],
                                      opts[:link_rev], changesets[0][0], changesets[1][0], [fresh, remove]
        [man_entry, updated, added]
      end
      
      ##
      # @return [Boolean] is the changeset representing the working directory?
      def working?
        false
      end
      
      ##
      # Gives an easier way to digest this changeset while reminding us it's a
      # changeset
      def inspect
        "#<Changeset #{to_s}>"
      end
      
      ##
      # Hash function for putting these bad boys in hashes
      # 
      # @return [Integer] a hash value.
      def hash
        return @revision.hash if @revision
        return object_id
      end
      
      ##
      # Compares 2 changesets so we can sort them and whatnot
      # 
      # @param [Changeset] other a changeset we will compare against
      # @return [Integer] -1, 0, or 1. Typical comparison.
      def <=>(other)
        return 0 if @revision.nil? || other.revision.nil?
        @revision <=> other.revision
      end
      
      ##
      # Are these two changesets equivalent?
      # 
      # @param  [Changeset] other a changeset we will compare against
      # @return [Boolean] whether they're equivalent
      def ==(other)
        (self <=> other) == 0
      end
      
      ##
      # Are we a null revision?
      # @return [Boolean] null?
      def nil?
       @revision != NULL_REV
      end
      alias_method :null?, :nil?
      
      # Gets the raw changeset data for this revision. This includes
      # the user who committed it, the description of the commit, and so on.
      # Returns this: [manifest, user, [time, timezone], files, desc, extra]
      def raw_changeset
        @repo.changelog.read(@node_id)
      end
      
      ##
      # Returns the {ManifestEntry} for this revision. This will give
      # us info on any file we want, including flags such as executable
      # or if it's a link. Sizes and so on are also included.
      #
      # @return [ManifestEntry] the manifest at this point in time
      def manifest_entry
        @manifest_entry ||= @repo.manifest.read(raw_changeset.manifest_node)
      end
      
      ##
      # Provides access to all the tracked files in the changeset. Needed
      # for API compatibility.
      #
      # @return [Array<String>] all the files tracked in this changeset.
      def all_files
        return manifest_entry.files
      end
      
      ##
      # Returns the change in the manifest at this revision. I don't entirely
      # know what this is yet.
      def manifest_delta
        @manifest_entry_delta ||= @repo.manifest.read_delta(raw_changeset.manifest_node)
      end
      
      ##
      # Returns the parents of this changeset as {Changeset}s.
      #
      # @return [[Changeset]] the parents of this changeset.
      def parents
        return @parents if @parents
        
        p = @repo.changelog.parent_indices_for_index @revision
        p = [p[0]] if p[1] == NULL_REV
        
        @parents = p.map {|x| Changeset.new(@repo, x) }
      end
      
      ##
      # Returns the children of this changeset as {Changeset}s.
      #
      # @return [Array<Changeset>] the children of this changeset.
      def children
        @repo.changelog.children(node_id).map do |node|
          Changeset.new(@repo, node)
        end
      end
      
      ##
      # Iterates over each entry in the manifest entry.
      # 
      # @return [Changeset] self, because that's how #each works
      def each(&block)
        manifest_entry.sort.each(&block)
        self
      end
      
      ##
      # Checks whether this changeset included a given file or not.
      # 
      # @param [String] file the file to lookup
      # @return [Boolean] whether the file is in this changeset's manifest
      def include?(file)
        manifest_entry[file] != nil
      end 
      
      ##
      # Gets the file with the given name, as a {VersionedFile}.
      # @param file the path to the file to retrieve
      # @return [VersionedFile] the file at this revision
      #
      def [](file)
        get_file(file)
      end
      
      ##
      # Returns the file's info, namely it's node_id and flags it may
      # have at this point in time, such as "x" for executable.
      # 
      # @param path the path to the file
      # @return [[String, String]] the [node_id, flags] pair for this file
      def file_info(path)
        if manifest_entry # have we loaded our manifest yet? if so, use that sucker
          result = [manifest_entry[path], manifest_entry.flags[path]]
          if result[0].nil?
            return [NULL_ID, '']
          else
            return result
          end
        end
        if manifest_delta || files[path] # check if it's in the delta... i dunno
          if manifest_delta[path]
            return [manifest_delta[path], manifest_delta.flags[path]]
          end
        end
        # Give us, just look it up the long way in the manifest. not fun. slow.
        node, flag = @repo.manifest.find(raw_changeset[0], path)
        if node.nil?
          return [NULL_ID, '']
        end
        return [node, flag]
      end
      
      ##
      # Gets the flags for the file at the given path at this revision.
      # @param path the path to the file in question
      # @return [String] the flags for the file, such as "x", "l", or "".
      #
      def flags(path)
        info = file_info(path)[1]
        return "" if info.nil?
        info
      end
      
      ##
      # Gets the node_id in the manifest_entry for the file at this path, for this
      # specific revision.
      # 
      # @param path the path to the file
      # @return [String] the node's ID in the manifest_entry, which we'll use every
      #   where we need a node_id.
      def file_node(path)
        file_info(path).first[0..19]
      end
      
      ##
      # Creates a versioned file for the file at the given path, for the frame
      # of reference of this revision.
      # @param path the path to the file
      # @param [String] file_id the node_id, to save us some computation
      # @param [FileLog] file_log the file_log to use, again to save us computation
      # @return [VersionedFile] the file at this revision.
      #
      def get_file(path, file_id = nil, file_log = nil)
        file_id = file_node(path) if file_id.nil?
        VersionedFile.new(@repo, path, :file_id => file_id, :changeset => self,
                                       :file_log => file_log)
      end
      #accessors
      # revision index
      def revision; @revision; end
      alias_method :rev, :revision
      # node_id
      def node_id; @node_id; end
      # @see node_id
      alias_method :node, :node_id
      # our node_id in sexy hexy
      def hex; @node_id.hexlify; end
      # the user who committed me!
      def user; raw_changeset.user; end
      # the date i was committed!
      def date; raw_changeset.time; end
      def easy_date; Time.at(raw_changeset.time.first); end
      # the files affected in this commit!
      def altered_files; raw_changeset.files; end
      # pre-API compatibility
      alias_method :files, :altered_files
      
      # the message with this commit
      def description; raw_changeset.description; end
      # What branch i was committed onto
      def branch 
        extra["branch"] 
      end
      # Any extra stuff I've got in me
      def extra; raw_changeset.extra; end
      # tags
      def tags; @repo.tags_for_node node; end
      
      def ancestor(other_changeset)
        node = @repo.changelog.ancestor(self.node, other_changeset.node)
        return Changeset.new(@repo, node)
      end
      
      def ancestors
        results = []
        @repo.changelog.ancestors(revision)
      end
    end
    
    ##
    # This is a special changeset that specifically works within the
    # working directory. We sort of have to combine the old revision
    # logs with the fact that files might be changed, and not in the
    # revision logs! oh, mercy!
    class WorkingDirectoryChangeset < Changeset
      
      def initialize(repo, opts={:text => ""})
        @repo = repo
        @revision = nil
        @parents = nil
        @node_id  = nil
        @text = opts[:text]
        require 'time' if opts[:date].kind_of?(String)
        @date = opts[:date].kind_of?(String) ? Time.parse(opts[:date]) : opts[:date]
        @user = opts[:user] if opts[:user]
        @parents = opts[:parents].map {|p| Changeset.new(@repo, p)} if opts[:parents]
        @status = opts[:changes] if opts[:changes]
        @manifest = nil
        @manifest_entry = nil
        @extra = opts[:extra] ? opts[:extra].dup : {}
        unless @extra["branch"]
          branch = @repo.dirstate.branch
          # encoding - to UTF-8
          @extra["branch"] = branch
        end
        @extra["branch"] = "default" if @extra["branch"] && @extra["branch"].empty?
        
      end
      
      ##
      # Is this changeset a working changeset?
      #
      # @return [Boolean] is the changeset representing the working directory?
      def working?
        true
      end
      
      ##
      # Converts to a string.
      # I'm my first parent, plus a little extra.
      # "I am my own grandpa"
      # 
      # @return [String]
      def to_s
        if (altered_files + status[:deleted]).any?
        then parents.first.to_s + "+"
        else parents.first.to_s
        end
      end
      
      
      ##
      # Do I include a given file? (not sure this is ever used yet)
      def include?(key)
        status = @repo.staging_area.file_status(key)
        ![:unknown, :removed].include?(status)
      end
      
      def all_files
        repo.staging_area.all_files
      end
      
      ##
      # Am I nil? never!
      def nil?; false; end
      
      ##
      # What is the status of the working directory? This little
      # method hides quite a bit of work!
      def status
        @status ||= @repo.status(:unknown => true)
      end
      
      ##
      # Who is the user working on me?
      def user 
        @user ||= @repo.config.username
      end
      
      ##
      # Well, I guess the working directory's date is... right now!
      def date
        @date ||= Time.new
      end
      
      ##
      # Who is the working directory's father? Is it Chef? Mr. Garrison?
      # the 1989 denver broncos?
      #
      # hahaha mike that's hilarious
      def parents
        @parents ||= begin
                       p = @repo.dirstate.parents
                       p = [p[0]] if p[1] == NULL_ID
                       p.map {|x| Changeset.new(@repo, x) }
                     end
      end
      
      ##
      # OK, so we've got the last revision's manifest entry, that part's simple and makes sense.
      # except now, we need to get the status of the working directory, and
      # add in all the other files, because they're in the "manifest entry" by being
      # in existence. Oh, and we need to remove any files from the parent's
      # manifest entry that don't exist anymore. Make sense?
      def manifest_entry
        return @manifest_entry if @manifest_entry
        
        # Start off with the last revision's manifest_entry, that's safe.
        man = parents()[0].manifest_entry.dup
        # Any copied files since the last revision?
        copied = @repo.dirstate.copy_map
        # Any modified, added, etc files since the last revision?
        modified, added, removed  = status[:modified], status[:added], status[:removed]
        deleted, unknown          = status[:deleted], status[:unknown]
        # Merge these discoveries in!
        {:a => added, :m => modified, :u => unknown}.each do |k, list|
          list.each do |file|
            copy_name = (copied[file] || file)
            man[file] = (man.flags[copy_name] || NULL_ID) + k.to_s
            man.flags[file] = @repo.dirstate.flags(file)
          end
        end
        
        # Delete files from the real manifest entry that don't exist.
        (deleted + removed).each do |file|
          man.delete file if man[file]
        end
        
        @manifest_entry = man
      end
      
      ##
      # Returns a {VersionedWorkingFile} to represent the file at the given
      # point in time. It represents a file in the working directory, which
      # obvious don't read from the history, but from the actual file in
      # question.
      # 
      # @param path the path to the file
      # @param file_log the log for the file to save some computation
      # @return [Amp::VersionedWorkingFile] the file object we can work with
      def get_file(path, file_log=nil)
        VersionedWorkingFile.new(@repo, path, :working_changeset => self, 
                                              :file_log => file_log)
      end
      
      ##
      # Gets the flags for the file at current state in time
      # 
      # @param [String] path the path to the file
      # @return [String] the flags, such as "x", "l", or ""
      def flags(path)
        if @manifest_entry ||= nil
          return manifest_entry.flags[path] || ""
        end
        pnode = parents[0].raw_changeset[0]
    
        orig = @repo.dirstate.copy_map[path] || path
        node, flag = @repo.manifest.find(pnode, orig)
        return @repo.dirstate.flags(@repo.working_join(path))
      end
      
      def useful_parents
        parents = @parents
        if parents[1].nil?
          if parents[0].revision >= @repo.size - 1
            parents = []
          else
            parents = [parents[0]]
          end
        end
        parents
      end
      
      ##
      # What's the hex hash of this revision? Just return nil, because we
      # haven't been committed yet.
      def hex
        nil # noop
      end
      
      ##
      # Recursively walk the directory tree, getting all files that +match+ says
      # are good.
      # 
      # @param [Amp::Match] match how to select the files in the tree
      # @param [Boolean] check_ignored (false) should we check for ignored files?
      # @return [Array<String>] an array of filenames in the tree that match +match+
      def walk(match, check_ignored = false)
        tree = @repo.staging_area.walk true, check_ignored, match
        tree.keys.sort
      end
      
      # If there's a description, ok then
      def description; @text; end
      # Files affected in this transaction: modified, added, removed.
      def altered_files; (modified + added + removed).sort; end
      # What files have changed?
      def modified; status[:modified] || []; end
      # What files have we added?
      def added; status[:added] || []; end
      # What files have been removed?
      def removed; status[:removed] || []; end
      # What files have been deleted (but not officially)?
      def deleted; status[:deleted] || []; end
      # What files are hanging out, but untracked?
      def unknown; status[:unknown] || []; end
      # What files are pristine since the last revision?
      def clean; status[:normal] || []; end
      # What branch are we in?
      def branch; @extra['branch']; end
      # Set the branch
      def branch=(name); @extra['branch'] = name; end
      # Any other extra data? i'd like to hear it
      def extra; @extra; end
      # No children. Returns the empty array.
      def children; []; end
      # The date, nicely handled
      def easy_date; date; end
      # The tags
      def tags; self.parents.first.tags; end
    end
  end
end