~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-08-10 21:43:27 UTC
  • mto: (1092.1.41) (1185.3.4) (974.1.47)
  • mto: This revision was merged to the branch mainline in revision 1110.
  • Revision ID: abentley@panoramicfeedback.com-20050810214327-4e8c22e4cba24527
Eliminated ThreeWayInventory

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
 
import traceback, socket, fnmatch, difflib, time
20
 
from binascii import hexlify
 
18
import sys, os
21
19
 
22
20
import bzrlib
23
 
from inventory import Inventory
24
 
from trace import mutter, note
25
 
from tree import Tree, EmptyTree, RevisionTree
26
 
from inventory import InventoryEntry, Inventory
27
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
 
     joinpath, sha_string, file_kind, local_time_offset, appendpath
30
 
from store import ImmutableStore
31
 
from revision import Revision
32
 
from errors import BzrError
33
 
from textui import show_status
 
21
from bzrlib.trace import mutter, note
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
 
23
     sha_file, appendpath, file_kind
 
24
from bzrlib.errors import BzrError
34
25
 
35
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
27
## TODO: Maybe include checks for common corruption of newlines, etc?
45
36
        return Branch(f, **args)
46
37
 
47
38
 
 
39
def find_cached_branch(f, cache_root, **args):
 
40
    from remotebranch import RemoteBranch
 
41
    br = find_branch(f, **args)
 
42
    def cacheify(br, store_name):
 
43
        from meta_store import CachedStore
 
44
        cache_path = os.path.join(cache_root, store_name)
 
45
        os.mkdir(cache_path)
 
46
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
47
        setattr(br, store_name, new_store)
 
48
 
 
49
    if isinstance(br, RemoteBranch):
 
50
        cacheify(br, 'inventory_store')
 
51
        cacheify(br, 'text_store')
 
52
        cacheify(br, 'revision_store')
 
53
    return br
 
54
 
48
55
 
49
56
def _relpath(base, path):
50
57
    """Return path relative to base, or raise exception.
110
117
        self.branch2 = branch2
111
118
        Exception.__init__(self, "These branches have diverged.")
112
119
 
 
120
 
 
121
class NoSuchRevision(BzrError):
 
122
    def __init__(self, branch, revision):
 
123
        self.branch = branch
 
124
        self.revision = revision
 
125
        msg = "Branch %s has no revision %d" % (branch, revision)
 
126
        BzrError.__init__(self, msg)
 
127
 
 
128
 
113
129
######################################################################
114
130
# branch objects
115
131
 
134
150
    _lock_count = None
135
151
    _lock = None
136
152
    
 
153
    # Map some sort of prefix into a namespace
 
154
    # stuff like "revno:10", "revid:", etc.
 
155
    # This should match a prefix with a function which accepts
 
156
    REVISION_NAMESPACES = {}
 
157
 
137
158
    def __init__(self, base, init=False, find_root=True):
138
159
        """Create new branch object at a particular location.
139
160
 
149
170
        In the test suite, creation of new trees is tested using the
150
171
        `ScratchBranch` class.
151
172
        """
 
173
        from bzrlib.store import ImmutableStore
152
174
        if init:
153
175
            self.base = os.path.realpath(base)
154
176
            self._make_control()
240
262
 
241
263
    def controlfilename(self, file_or_path):
242
264
        """Return location relative to branch."""
243
 
        if isinstance(file_or_path, types.StringTypes):
 
265
        if isinstance(file_or_path, basestring):
244
266
            file_or_path = [file_or_path]
245
267
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
246
268
 
273
295
 
274
296
 
275
297
    def _make_control(self):
 
298
        from bzrlib.inventory import Inventory
 
299
        from bzrlib.xml import pack_xml
 
300
        
276
301
        os.mkdir(self.controlfilename([]))
277
302
        self.controlfile('README', 'w').write(
278
303
            "This is a Bazaar-NG control directory.\n"
279
 
            "Do not change any files in this directory.")
 
304
            "Do not change any files in this directory.\n")
280
305
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
281
306
        for d in ('text-store', 'inventory-store', 'revision-store'):
282
307
            os.mkdir(self.controlfilename(d))
283
308
        for f in ('revision-history', 'merged-patches',
284
309
                  'pending-merged-patches', 'branch-name',
285
 
                  'branch-lock'):
 
310
                  'branch-lock',
 
311
                  'pending-merges'):
286
312
            self.controlfile(f, 'w').write('')
287
313
        mutter('created control directory in ' + self.base)
288
 
        Inventory().write_xml(self.controlfile('inventory','w'))
 
314
 
 
315
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
289
316
 
290
317
 
291
318
    def _check_format(self):
306
333
                           ['use a different bzr version',
307
334
                            'or remove the .bzr directory and "bzr init" again'])
308
335
 
 
336
    def get_root_id(self):
 
337
        """Return the id of this branches root"""
 
338
        inv = self.read_working_inventory()
 
339
        return inv.root.file_id
309
340
 
 
341
    def set_root_id(self, file_id):
 
342
        inv = self.read_working_inventory()
 
343
        orig_root_id = inv.root.file_id
 
344
        del inv._byid[inv.root.file_id]
 
345
        inv.root.file_id = file_id
 
346
        inv._byid[inv.root.file_id] = inv.root
 
347
        for fid in inv:
 
348
            entry = inv[fid]
 
349
            if entry.parent_id in (None, orig_root_id):
 
350
                entry.parent_id = inv.root.file_id
 
351
        self._write_inventory(inv)
310
352
 
311
353
    def read_working_inventory(self):
312
354
        """Read the working inventory."""
313
 
        before = time.time()
314
 
        # ElementTree does its own conversion from UTF-8, so open in
315
 
        # binary.
 
355
        from bzrlib.inventory import Inventory
 
356
        from bzrlib.xml import unpack_xml
 
357
        from time import time
 
358
        before = time()
316
359
        self.lock_read()
317
360
        try:
318
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
361
            # ElementTree does its own conversion from UTF-8, so open in
 
362
            # binary.
 
363
            inv = unpack_xml(Inventory,
 
364
                             self.controlfile('inventory', 'rb'))
319
365
            mutter("loaded inventory of %d items in %f"
320
 
                   % (len(inv), time.time() - before))
 
366
                   % (len(inv), time() - before))
321
367
            return inv
322
368
        finally:
323
369
            self.unlock()
329
375
        That is to say, the inventory describing changes underway, that
330
376
        will be committed to the next revision.
331
377
        """
332
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
333
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
334
 
        tmpfname = self.controlfilename('inventory.tmp')
335
 
        tmpf = file(tmpfname, 'wb')
336
 
        inv.write_xml(tmpf)
337
 
        tmpf.close()
338
 
        inv_fname = self.controlfilename('inventory')
339
 
        if sys.platform == 'win32':
340
 
            os.remove(inv_fname)
341
 
        os.rename(tmpfname, inv_fname)
 
378
        from bzrlib.atomicfile import AtomicFile
 
379
        from bzrlib.xml import pack_xml
 
380
        
 
381
        self.lock_write()
 
382
        try:
 
383
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
384
            try:
 
385
                pack_xml(inv, f)
 
386
                f.commit()
 
387
            finally:
 
388
                f.close()
 
389
        finally:
 
390
            self.unlock()
 
391
        
342
392
        mutter('wrote working inventory')
343
393
            
344
394
 
372
422
              add all non-ignored children.  Perhaps do that in a
373
423
              higher-level method.
374
424
        """
 
425
        from bzrlib.textui import show_status
375
426
        # TODO: Re-adding a file that is removed in the working copy
376
427
        # should probably put it back with the previous ID.
377
 
        if isinstance(files, types.StringTypes):
378
 
            assert(ids is None or isinstance(ids, types.StringTypes))
 
428
        if isinstance(files, basestring):
 
429
            assert(ids is None or isinstance(ids, basestring))
379
430
            files = [files]
380
431
            if ids is not None:
381
432
                ids = [ids]
413
464
                inv.add_path(f, kind=kind, file_id=file_id)
414
465
 
415
466
                if verbose:
416
 
                    show_status('A', kind, quotefn(f))
 
467
                    print 'added', quotefn(f)
417
468
 
418
469
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
419
470
 
430
481
            # use inventory as it was in that revision
431
482
            file_id = tree.inventory.path2id(file)
432
483
            if not file_id:
433
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
484
                raise BzrError("%r is not present in revision %s" % (file, revno))
434
485
            tree.print_file(file_id)
435
486
        finally:
436
487
            self.unlock()
450
501
        is the opposite of add.  Removing it is consistent with most
451
502
        other tools.  Maybe an option.
452
503
        """
 
504
        from bzrlib.textui import show_status
453
505
        ## TODO: Normalize names
454
506
        ## TODO: Remove nested loops; better scalability
455
 
        if isinstance(files, types.StringTypes):
 
507
        if isinstance(files, basestring):
456
508
            files = [files]
457
509
 
458
510
        self.lock_write()
483
535
 
484
536
    # FIXME: this doesn't need to be a branch method
485
537
    def set_inventory(self, new_inventory_list):
486
 
        inv = Inventory()
 
538
        from bzrlib.inventory import Inventory, InventoryEntry
 
539
        inv = Inventory(self.get_root_id())
487
540
        for path, file_id, parent, kind in new_inventory_list:
488
541
            name = os.path.basename(path)
489
542
            if name == "":
511
564
        return self.working_tree().unknowns()
512
565
 
513
566
 
514
 
    def append_revision(self, revision_id):
515
 
        mutter("add {%s} to revision-history" % revision_id)
 
567
    def append_revision(self, *revision_ids):
 
568
        from bzrlib.atomicfile import AtomicFile
 
569
 
 
570
        for revision_id in revision_ids:
 
571
            mutter("add {%s} to revision-history" % revision_id)
 
572
 
516
573
        rev_history = self.revision_history()
517
 
 
518
 
        tmprhname = self.controlfilename('revision-history.tmp')
519
 
        rhname = self.controlfilename('revision-history')
520
 
        
521
 
        f = file(tmprhname, 'wt')
522
 
        rev_history.append(revision_id)
523
 
        f.write('\n'.join(rev_history))
524
 
        f.write('\n')
525
 
        f.close()
526
 
 
527
 
        if sys.platform == 'win32':
528
 
            os.remove(rhname)
529
 
        os.rename(tmprhname, rhname)
530
 
        
 
574
        rev_history.extend(revision_ids)
 
575
 
 
576
        f = AtomicFile(self.controlfilename('revision-history'))
 
577
        try:
 
578
            for rev_id in rev_history:
 
579
                print >>f, rev_id
 
580
            f.commit()
 
581
        finally:
 
582
            f.close()
531
583
 
532
584
 
533
585
    def get_revision(self, revision_id):
534
586
        """Return the Revision object for a named revision"""
535
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
587
        from bzrlib.revision import Revision
 
588
        from bzrlib.xml import unpack_xml
 
589
 
 
590
        self.lock_read()
 
591
        try:
 
592
            if not revision_id or not isinstance(revision_id, basestring):
 
593
                raise ValueError('invalid revision-id: %r' % revision_id)
 
594
            r = unpack_xml(Revision, self.revision_store[revision_id])
 
595
        finally:
 
596
            self.unlock()
 
597
            
536
598
        assert r.revision_id == revision_id
537
599
        return r
 
600
        
 
601
 
 
602
    def get_revision_sha1(self, revision_id):
 
603
        """Hash the stored value of a revision, and return it."""
 
604
        # In the future, revision entries will be signed. At that
 
605
        # point, it is probably best *not* to include the signature
 
606
        # in the revision hash. Because that lets you re-sign
 
607
        # the revision, (add signatures/remove signatures) and still
 
608
        # have all hash pointers stay consistent.
 
609
        # But for now, just hash the contents.
 
610
        return sha_file(self.revision_store[revision_id])
538
611
 
539
612
 
540
613
    def get_inventory(self, inventory_id):
543
616
        TODO: Perhaps for this and similar methods, take a revision
544
617
               parameter which can be either an integer revno or a
545
618
               string hash."""
546
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
547
 
        return i
 
619
        from bzrlib.inventory import Inventory
 
620
        from bzrlib.xml import unpack_xml
 
621
 
 
622
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
623
            
 
624
 
 
625
    def get_inventory_sha1(self, inventory_id):
 
626
        """Return the sha1 hash of the inventory entry
 
627
        """
 
628
        return sha_file(self.inventory_store[inventory_id])
548
629
 
549
630
 
550
631
    def get_revision_inventory(self, revision_id):
551
632
        """Return inventory of a past revision."""
 
633
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
634
        # must be the same as its revision, so this is trivial.
552
635
        if revision_id == None:
553
 
            return Inventory()
 
636
            from bzrlib.inventory import Inventory
 
637
            return Inventory(self.get_root_id())
554
638
        else:
555
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
639
            return self.get_inventory(revision_id)
556
640
 
557
641
 
558
642
    def revision_history(self):
654
738
            return None
655
739
 
656
740
 
657
 
    def missing_revisions(self, other):
 
741
    def missing_revisions(self, other, stop_revision=None):
658
742
        """
659
743
        If self and other have not diverged, return a list of the revisions
660
744
        present in other, but missing from self.
689
773
        if common_index >= 0 and \
690
774
            self_history[common_index] != other_history[common_index]:
691
775
            raise DivergedBranches(self, other)
692
 
        if self_len < other_len:
693
 
            return other_history[self_len:]
694
 
        return []
695
 
 
696
 
 
697
 
    def update_revisions(self, other):
698
 
        """If self and other have not diverged, ensure self has all the
699
 
        revisions in other
700
 
 
 
776
 
 
777
        if stop_revision is None:
 
778
            stop_revision = other_len
 
779
        elif stop_revision > other_len:
 
780
            raise NoSuchRevision(self, stop_revision)
 
781
        
 
782
        return other_history[self_len:stop_revision]
 
783
 
 
784
 
 
785
    def update_revisions(self, other, stop_revision=None):
 
786
        """Pull in all new revisions from other branch.
 
787
        
701
788
        >>> from bzrlib.commit import commit
702
789
        >>> bzrlib.trace.silent = True
703
790
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
718
805
        >>> br1.text_store.total_size() == br2.text_store.total_size()
719
806
        True
720
807
        """
721
 
        revision_ids = self.missing_revisions(other)
722
 
        revisions = [other.get_revision(f) for f in revision_ids]
723
 
        needed_texts = sets.Set()
724
 
        for rev in revisions:
 
808
        from bzrlib.progress import ProgressBar
 
809
 
 
810
        pb = ProgressBar()
 
811
 
 
812
        pb.update('comparing histories')
 
813
        revision_ids = self.missing_revisions(other, stop_revision)
 
814
 
 
815
        if hasattr(other.revision_store, "prefetch"):
 
816
            other.revision_store.prefetch(revision_ids)
 
817
        if hasattr(other.inventory_store, "prefetch"):
 
818
            inventory_ids = [other.get_revision(r).inventory_id
 
819
                             for r in revision_ids]
 
820
            other.inventory_store.prefetch(inventory_ids)
 
821
                
 
822
        revisions = []
 
823
        needed_texts = set()
 
824
        i = 0
 
825
        for rev_id in revision_ids:
 
826
            i += 1
 
827
            pb.update('fetching revision', i, len(revision_ids))
 
828
            rev = other.get_revision(rev_id)
 
829
            revisions.append(rev)
725
830
            inv = other.get_inventory(str(rev.inventory_id))
726
831
            for key, entry in inv.iter_entries():
727
832
                if entry.text_id is None:
728
833
                    continue
729
834
                if entry.text_id not in self.text_store:
730
835
                    needed_texts.add(entry.text_id)
 
836
 
 
837
        pb.clear()
 
838
                    
731
839
        count = self.text_store.copy_multi(other.text_store, needed_texts)
732
840
        print "Added %d texts." % count 
733
841
        inventory_ids = [ f.inventory_id for f in revisions ]
743
851
                    
744
852
        
745
853
    def commit(self, *args, **kw):
746
 
        """Deprecated"""
747
854
        from bzrlib.commit import commit
748
855
        commit(self, *args, **kw)
749
856
        
750
857
 
751
 
    def lookup_revision(self, revno):
752
 
        """Return revision hash for revision number."""
753
 
        if revno == 0:
754
 
            return None
755
 
 
756
 
        try:
757
 
            # list is 0-based; revisions are 1-based
758
 
            return self.revision_history()[revno-1]
759
 
        except IndexError:
760
 
            raise BzrError("no such revision %s" % revno)
761
 
 
 
858
    def lookup_revision(self, revision):
 
859
        """Return the revision identifier for a given revision information."""
 
860
        revno, info = self.get_revision_info(revision)
 
861
        return info
 
862
 
 
863
    def get_revision_info(self, revision):
 
864
        """Return (revno, revision id) for revision identifier.
 
865
 
 
866
        revision can be an integer, in which case it is assumed to be revno (though
 
867
            this will translate negative values into positive ones)
 
868
        revision can also be a string, in which case it is parsed for something like
 
869
            'date:' or 'revid:' etc.
 
870
        """
 
871
        if revision is None:
 
872
            return 0, None
 
873
        revno = None
 
874
        try:# Convert to int if possible
 
875
            revision = int(revision)
 
876
        except ValueError:
 
877
            pass
 
878
        revs = self.revision_history()
 
879
        if isinstance(revision, int):
 
880
            if revision == 0:
 
881
                return 0, None
 
882
            # Mabye we should do this first, but we don't need it if revision == 0
 
883
            if revision < 0:
 
884
                revno = len(revs) + revision + 1
 
885
            else:
 
886
                revno = revision
 
887
        elif isinstance(revision, basestring):
 
888
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
889
                if revision.startswith(prefix):
 
890
                    revno = func(self, revs, revision)
 
891
                    break
 
892
            else:
 
893
                raise BzrError('No namespace registered for string: %r' % revision)
 
894
 
 
895
        if revno is None or revno <= 0 or revno > len(revs):
 
896
            raise BzrError("no such revision %s" % revision)
 
897
        return revno, revs[revno-1]
 
898
 
 
899
    def _namespace_revno(self, revs, revision):
 
900
        """Lookup a revision by revision number"""
 
901
        assert revision.startswith('revno:')
 
902
        try:
 
903
            return int(revision[6:])
 
904
        except ValueError:
 
905
            return None
 
906
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
907
 
 
908
    def _namespace_revid(self, revs, revision):
 
909
        assert revision.startswith('revid:')
 
910
        try:
 
911
            return revs.index(revision[6:]) + 1
 
912
        except ValueError:
 
913
            return None
 
914
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
915
 
 
916
    def _namespace_last(self, revs, revision):
 
917
        assert revision.startswith('last:')
 
918
        try:
 
919
            offset = int(revision[5:])
 
920
        except ValueError:
 
921
            return None
 
922
        else:
 
923
            if offset <= 0:
 
924
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
925
            return len(revs) - offset + 1
 
926
    REVISION_NAMESPACES['last:'] = _namespace_last
 
927
 
 
928
    def _namespace_tag(self, revs, revision):
 
929
        assert revision.startswith('tag:')
 
930
        raise BzrError('tag: namespace registered, but not implemented.')
 
931
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
932
 
 
933
    def _namespace_date(self, revs, revision):
 
934
        assert revision.startswith('date:')
 
935
        import datetime
 
936
        # Spec for date revisions:
 
937
        #   date:value
 
938
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
939
        #   it can also start with a '+/-/='. '+' says match the first
 
940
        #   entry after the given date. '-' is match the first entry before the date
 
941
        #   '=' is match the first entry after, but still on the given date.
 
942
        #
 
943
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
944
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
945
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
946
        #       May 13th, 2005 at 0:00
 
947
        #
 
948
        #   So the proper way of saying 'give me all entries for today' is:
 
949
        #       -r {date:+today}:{date:-tomorrow}
 
950
        #   The default is '=' when not supplied
 
951
        val = revision[5:]
 
952
        match_style = '='
 
953
        if val[:1] in ('+', '-', '='):
 
954
            match_style = val[:1]
 
955
            val = val[1:]
 
956
 
 
957
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
958
        if val.lower() == 'yesterday':
 
959
            dt = today - datetime.timedelta(days=1)
 
960
        elif val.lower() == 'today':
 
961
            dt = today
 
962
        elif val.lower() == 'tomorrow':
 
963
            dt = today + datetime.timedelta(days=1)
 
964
        else:
 
965
            import re
 
966
            # This should be done outside the function to avoid recompiling it.
 
967
            _date_re = re.compile(
 
968
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
969
                    r'(,|T)?\s*'
 
970
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
971
                )
 
972
            m = _date_re.match(val)
 
973
            if not m or (not m.group('date') and not m.group('time')):
 
974
                raise BzrError('Invalid revision date %r' % revision)
 
975
 
 
976
            if m.group('date'):
 
977
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
978
            else:
 
979
                year, month, day = today.year, today.month, today.day
 
980
            if m.group('time'):
 
981
                hour = int(m.group('hour'))
 
982
                minute = int(m.group('minute'))
 
983
                if m.group('second'):
 
984
                    second = int(m.group('second'))
 
985
                else:
 
986
                    second = 0
 
987
            else:
 
988
                hour, minute, second = 0,0,0
 
989
 
 
990
            dt = datetime.datetime(year=year, month=month, day=day,
 
991
                    hour=hour, minute=minute, second=second)
 
992
        first = dt
 
993
        last = None
 
994
        reversed = False
 
995
        if match_style == '-':
 
996
            reversed = True
 
997
        elif match_style == '=':
 
998
            last = dt + datetime.timedelta(days=1)
 
999
 
 
1000
        if reversed:
 
1001
            for i in range(len(revs)-1, -1, -1):
 
1002
                r = self.get_revision(revs[i])
 
1003
                # TODO: Handle timezone.
 
1004
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1005
                if first >= dt and (last is None or dt >= last):
 
1006
                    return i+1
 
1007
        else:
 
1008
            for i in range(len(revs)):
 
1009
                r = self.get_revision(revs[i])
 
1010
                # TODO: Handle timezone.
 
1011
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1012
                if first <= dt and (last is None or dt <= last):
 
1013
                    return i+1
 
1014
    REVISION_NAMESPACES['date:'] = _namespace_date
762
1015
 
763
1016
    def revision_tree(self, revision_id):
764
1017
        """Return Tree for a revision on this branch.
765
1018
 
766
1019
        `revision_id` may be None for the null revision, in which case
767
1020
        an `EmptyTree` is returned."""
 
1021
        from bzrlib.tree import EmptyTree, RevisionTree
768
1022
        # TODO: refactor this to use an existing revision object
769
1023
        # so we don't need to read it in twice.
770
1024
        if revision_id == None:
771
 
            return EmptyTree()
 
1025
            return EmptyTree(self.get_root_id())
772
1026
        else:
773
1027
            inv = self.get_revision_inventory(revision_id)
774
1028
            return RevisionTree(self.text_store, inv)
785
1039
 
786
1040
        If there are no revisions yet, return an `EmptyTree`.
787
1041
        """
 
1042
        from bzrlib.tree import EmptyTree, RevisionTree
788
1043
        r = self.last_patch()
789
1044
        if r == None:
790
 
            return EmptyTree()
 
1045
            return EmptyTree(self.get_root_id())
791
1046
        else:
792
1047
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
793
1048
 
908
1163
            self.unlock()
909
1164
 
910
1165
 
 
1166
    def revert(self, filenames, old_tree=None, backups=True):
 
1167
        """Restore selected files to the versions from a previous tree.
 
1168
 
 
1169
        backups
 
1170
            If true (default) backups are made of files before
 
1171
            they're renamed.
 
1172
        """
 
1173
        from bzrlib.errors import NotVersionedError, BzrError
 
1174
        from bzrlib.atomicfile import AtomicFile
 
1175
        from bzrlib.osutils import backup_file
 
1176
        
 
1177
        inv = self.read_working_inventory()
 
1178
        if old_tree is None:
 
1179
            old_tree = self.basis_tree()
 
1180
        old_inv = old_tree.inventory
 
1181
 
 
1182
        nids = []
 
1183
        for fn in filenames:
 
1184
            file_id = inv.path2id(fn)
 
1185
            if not file_id:
 
1186
                raise NotVersionedError("not a versioned file", fn)
 
1187
            if not old_inv.has_id(file_id):
 
1188
                raise BzrError("file not present in old tree", fn, file_id)
 
1189
            nids.append((fn, file_id))
 
1190
            
 
1191
        # TODO: Rename back if it was previously at a different location
 
1192
 
 
1193
        # TODO: If given a directory, restore the entire contents from
 
1194
        # the previous version.
 
1195
 
 
1196
        # TODO: Make a backup to a temporary file.
 
1197
 
 
1198
        # TODO: If the file previously didn't exist, delete it?
 
1199
        for fn, file_id in nids:
 
1200
            backup_file(fn)
 
1201
            
 
1202
            f = AtomicFile(fn, 'wb')
 
1203
            try:
 
1204
                f.write(old_tree.get_file(file_id).read())
 
1205
                f.commit()
 
1206
            finally:
 
1207
                f.close()
 
1208
 
 
1209
 
 
1210
    def pending_merges(self):
 
1211
        """Return a list of pending merges.
 
1212
 
 
1213
        These are revisions that have been merged into the working
 
1214
        directory but not yet committed.
 
1215
        """
 
1216
        cfn = self.controlfilename('pending-merges')
 
1217
        if not os.path.exists(cfn):
 
1218
            return []
 
1219
        p = []
 
1220
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1221
            p.append(l.rstrip('\n'))
 
1222
        return p
 
1223
 
 
1224
 
 
1225
    def add_pending_merge(self, revision_id):
 
1226
        from bzrlib.revision import validate_revision_id
 
1227
 
 
1228
        validate_revision_id(revision_id)
 
1229
 
 
1230
        p = self.pending_merges()
 
1231
        if revision_id in p:
 
1232
            return
 
1233
        p.append(revision_id)
 
1234
        self.set_pending_merges(p)
 
1235
 
 
1236
 
 
1237
    def set_pending_merges(self, rev_list):
 
1238
        from bzrlib.atomicfile import AtomicFile
 
1239
        self.lock_write()
 
1240
        try:
 
1241
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1242
            try:
 
1243
                for l in rev_list:
 
1244
                    print >>f, l
 
1245
                f.commit()
 
1246
            finally:
 
1247
                f.close()
 
1248
        finally:
 
1249
            self.unlock()
 
1250
 
 
1251
 
911
1252
 
912
1253
class ScratchBranch(Branch):
913
1254
    """Special test class: a branch that cleans up after itself.
927
1268
 
928
1269
        If any files are listed, they are created in the working copy.
929
1270
        """
 
1271
        from tempfile import mkdtemp
930
1272
        init = False
931
1273
        if base is None:
932
 
            base = tempfile.mkdtemp()
 
1274
            base = mkdtemp()
933
1275
            init = True
934
1276
        Branch.__init__(self, base, init=init)
935
1277
        for d in dirs:
948
1290
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
949
1291
        True
950
1292
        """
951
 
        base = tempfile.mkdtemp()
 
1293
        from shutil import copytree
 
1294
        from tempfile import mkdtemp
 
1295
        base = mkdtemp()
952
1296
        os.rmdir(base)
953
 
        shutil.copytree(self.base, base, symlinks=True)
 
1297
        copytree(self.base, base, symlinks=True)
954
1298
        return ScratchBranch(base=base)
955
1299
        
956
1300
    def __del__(self):
958
1302
 
959
1303
    def destroy(self):
960
1304
        """Destroy the test branch, removing the scratch directory."""
 
1305
        from shutil import rmtree
961
1306
        try:
962
1307
            if self.base:
963
1308
                mutter("delete ScratchBranch %s" % self.base)
964
 
                shutil.rmtree(self.base)
 
1309
                rmtree(self.base)
965
1310
        except OSError, e:
966
1311
            # Work around for shutil.rmtree failing on Windows when
967
1312
            # readonly files are encountered
969
1314
            for root, dirs, files in os.walk(self.base, topdown=False):
970
1315
                for name in files:
971
1316
                    os.chmod(os.path.join(root, name), 0700)
972
 
            shutil.rmtree(self.base)
 
1317
            rmtree(self.base)
973
1318
        self.base = None
974
1319
 
975
1320
    
1000
1345
    cope with just randomness because running uuidgen every time is
1001
1346
    slow."""
1002
1347
    import re
 
1348
    from binascii import hexlify
 
1349
    from time import time
1003
1350
 
1004
1351
    # get last component
1005
1352
    idx = name.rfind('/')
1017
1364
    name = re.sub(r'[^\w.]', '', name)
1018
1365
 
1019
1366
    s = hexlify(rand_bytes(8))
1020
 
    return '-'.join((name, compact_date(time.time()), s))
 
1367
    return '-'.join((name, compact_date(time()), s))
 
1368
 
 
1369
 
 
1370
def gen_root_id():
 
1371
    """Return a new tree-root file id."""
 
1372
    return gen_file_id('TREE_ROOT')
 
1373