~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-08-05 19:01:37 UTC
  • Revision ID: mbp@sourcefrog.net-20050805190137-96f1d7c1ec9459ab
- fix construction of NoSuchRevision

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
 
19
import os
21
20
 
22
21
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_file, 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
34
 
 
 
22
from bzrlib.trace import mutter, note
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     splitpath, \
 
25
     sha_file, appendpath, file_kind
 
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
 
27
import bzrlib.errors
 
28
from bzrlib.textui import show_status
 
29
from bzrlib.revision import Revision
 
30
from bzrlib.xml import unpack_xml
 
31
from bzrlib.delta import compare_trees
 
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
        
35
34
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
35
## TODO: Maybe include checks for common corruption of newlines, etc?
37
36
 
38
37
 
 
38
# TODO: Some operations like log might retrieve the same revisions
 
39
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
40
# cache in memory to make this faster.
 
41
 
39
42
 
40
43
def find_branch(f, **args):
41
44
    if f and (f.startswith('http://') or f.startswith('https://')):
45
48
        return Branch(f, **args)
46
49
 
47
50
 
 
51
def find_cached_branch(f, cache_root, **args):
 
52
    from remotebranch import RemoteBranch
 
53
    br = find_branch(f, **args)
 
54
    def cacheify(br, store_name):
 
55
        from meta_store import CachedStore
 
56
        cache_path = os.path.join(cache_root, store_name)
 
57
        os.mkdir(cache_path)
 
58
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
59
        setattr(br, store_name, new_store)
 
60
 
 
61
    if isinstance(br, RemoteBranch):
 
62
        cacheify(br, 'inventory_store')
 
63
        cacheify(br, 'text_store')
 
64
        cacheify(br, 'revision_store')
 
65
    return br
 
66
 
48
67
 
49
68
def _relpath(base, path):
50
69
    """Return path relative to base, or raise exception.
111
130
        Exception.__init__(self, "These branches have diverged.")
112
131
 
113
132
 
114
 
class NoSuchRevision(BzrError):
115
 
    def __init__(self, branch, revision):
116
 
        self.branch = branch
117
 
        self.revision = revision
118
 
        msg = "Branch %s has no revision %d" % (branch, revision)
119
 
        BzrError.__init__(self, msg)
120
 
 
121
 
 
122
133
######################################################################
123
134
# branch objects
124
135
 
143
154
    _lock_count = None
144
155
    _lock = None
145
156
    
 
157
    # Map some sort of prefix into a namespace
 
158
    # stuff like "revno:10", "revid:", etc.
 
159
    # This should match a prefix with a function which accepts
 
160
    REVISION_NAMESPACES = {}
 
161
 
146
162
    def __init__(self, base, init=False, find_root=True):
147
163
        """Create new branch object at a particular location.
148
164
 
158
174
        In the test suite, creation of new trees is tested using the
159
175
        `ScratchBranch` class.
160
176
        """
 
177
        from bzrlib.store import ImmutableStore
161
178
        if init:
162
179
            self.base = os.path.realpath(base)
163
180
            self._make_control()
249
266
 
250
267
    def controlfilename(self, file_or_path):
251
268
        """Return location relative to branch."""
252
 
        if isinstance(file_or_path, types.StringTypes):
 
269
        if isinstance(file_or_path, basestring):
253
270
            file_or_path = [file_or_path]
254
271
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
255
272
 
282
299
 
283
300
 
284
301
    def _make_control(self):
 
302
        from bzrlib.inventory import Inventory
 
303
        from bzrlib.xml import pack_xml
 
304
        
285
305
        os.mkdir(self.controlfilename([]))
286
306
        self.controlfile('README', 'w').write(
287
307
            "This is a Bazaar-NG control directory.\n"
291
311
            os.mkdir(self.controlfilename(d))
292
312
        for f in ('revision-history', 'merged-patches',
293
313
                  'pending-merged-patches', 'branch-name',
294
 
                  'branch-lock'):
 
314
                  'branch-lock',
 
315
                  'pending-merges'):
295
316
            self.controlfile(f, 'w').write('')
296
317
        mutter('created control directory in ' + self.base)
297
 
        Inventory().write_xml(self.controlfile('inventory','w'))
 
318
 
 
319
        # if we want per-tree root ids then this is the place to set
 
320
        # them; they're not needed for now and so ommitted for
 
321
        # simplicity.
 
322
        pack_xml(Inventory(), self.controlfile('inventory','w'))
298
323
 
299
324
 
300
325
    def _check_format(self):
315
340
                           ['use a different bzr version',
316
341
                            'or remove the .bzr directory and "bzr init" again'])
317
342
 
 
343
    def get_root_id(self):
 
344
        """Return the id of this branches root"""
 
345
        inv = self.read_working_inventory()
 
346
        return inv.root.file_id
318
347
 
 
348
    def set_root_id(self, file_id):
 
349
        inv = self.read_working_inventory()
 
350
        orig_root_id = inv.root.file_id
 
351
        del inv._byid[inv.root.file_id]
 
352
        inv.root.file_id = file_id
 
353
        inv._byid[inv.root.file_id] = inv.root
 
354
        for fid in inv:
 
355
            entry = inv[fid]
 
356
            if entry.parent_id in (None, orig_root_id):
 
357
                entry.parent_id = inv.root.file_id
 
358
        self._write_inventory(inv)
319
359
 
320
360
    def read_working_inventory(self):
321
361
        """Read the working inventory."""
322
 
        before = time.time()
323
 
        # ElementTree does its own conversion from UTF-8, so open in
324
 
        # binary.
 
362
        from bzrlib.inventory import Inventory
 
363
        from bzrlib.xml import unpack_xml
 
364
        from time import time
 
365
        before = time()
325
366
        self.lock_read()
326
367
        try:
327
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
368
            # ElementTree does its own conversion from UTF-8, so open in
 
369
            # binary.
 
370
            inv = unpack_xml(Inventory,
 
371
                             self.controlfile('inventory', 'rb'))
328
372
            mutter("loaded inventory of %d items in %f"
329
 
                   % (len(inv), time.time() - before))
 
373
                   % (len(inv), time() - before))
330
374
            return inv
331
375
        finally:
332
376
            self.unlock()
338
382
        That is to say, the inventory describing changes underway, that
339
383
        will be committed to the next revision.
340
384
        """
341
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
342
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
343
 
        tmpfname = self.controlfilename('inventory.tmp')
344
 
        tmpf = file(tmpfname, 'wb')
345
 
        inv.write_xml(tmpf)
346
 
        tmpf.close()
347
 
        inv_fname = self.controlfilename('inventory')
348
 
        if sys.platform == 'win32':
349
 
            os.remove(inv_fname)
350
 
        os.rename(tmpfname, inv_fname)
 
385
        from bzrlib.atomicfile import AtomicFile
 
386
        from bzrlib.xml import pack_xml
 
387
        
 
388
        self.lock_write()
 
389
        try:
 
390
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
391
            try:
 
392
                pack_xml(inv, f)
 
393
                f.commit()
 
394
            finally:
 
395
                f.close()
 
396
        finally:
 
397
            self.unlock()
 
398
        
351
399
        mutter('wrote working inventory')
352
400
            
353
401
 
383
431
        """
384
432
        # TODO: Re-adding a file that is removed in the working copy
385
433
        # should probably put it back with the previous ID.
386
 
        if isinstance(files, types.StringTypes):
387
 
            assert(ids is None or isinstance(ids, types.StringTypes))
 
434
        if isinstance(files, basestring):
 
435
            assert(ids is None or isinstance(ids, basestring))
388
436
            files = [files]
389
437
            if ids is not None:
390
438
                ids = [ids]
422
470
                inv.add_path(f, kind=kind, file_id=file_id)
423
471
 
424
472
                if verbose:
425
 
                    show_status('A', kind, quotefn(f))
 
473
                    print 'added', quotefn(f)
426
474
 
427
475
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
428
476
 
439
487
            # use inventory as it was in that revision
440
488
            file_id = tree.inventory.path2id(file)
441
489
            if not file_id:
442
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
490
                raise BzrError("%r is not present in revision %s" % (file, revno))
443
491
            tree.print_file(file_id)
444
492
        finally:
445
493
            self.unlock()
461
509
        """
462
510
        ## TODO: Normalize names
463
511
        ## TODO: Remove nested loops; better scalability
464
 
        if isinstance(files, types.StringTypes):
 
512
        if isinstance(files, basestring):
465
513
            files = [files]
466
514
 
467
515
        self.lock_write()
492
540
 
493
541
    # FIXME: this doesn't need to be a branch method
494
542
    def set_inventory(self, new_inventory_list):
495
 
        inv = Inventory()
 
543
        from bzrlib.inventory import Inventory, InventoryEntry
 
544
        inv = Inventory(self.get_root_id())
496
545
        for path, file_id, parent, kind in new_inventory_list:
497
546
            name = os.path.basename(path)
498
547
            if name == "":
520
569
        return self.working_tree().unknowns()
521
570
 
522
571
 
523
 
    def append_revision(self, revision_id):
524
 
        mutter("add {%s} to revision-history" % revision_id)
 
572
    def append_revision(self, *revision_ids):
 
573
        from bzrlib.atomicfile import AtomicFile
 
574
 
 
575
        for revision_id in revision_ids:
 
576
            mutter("add {%s} to revision-history" % revision_id)
 
577
 
525
578
        rev_history = self.revision_history()
526
 
 
527
 
        tmprhname = self.controlfilename('revision-history.tmp')
528
 
        rhname = self.controlfilename('revision-history')
529
 
        
530
 
        f = file(tmprhname, 'wt')
531
 
        rev_history.append(revision_id)
532
 
        f.write('\n'.join(rev_history))
533
 
        f.write('\n')
534
 
        f.close()
535
 
 
536
 
        if sys.platform == 'win32':
537
 
            os.remove(rhname)
538
 
        os.rename(tmprhname, rhname)
539
 
        
 
579
        rev_history.extend(revision_ids)
 
580
 
 
581
        f = AtomicFile(self.controlfilename('revision-history'))
 
582
        try:
 
583
            for rev_id in rev_history:
 
584
                print >>f, rev_id
 
585
            f.commit()
 
586
        finally:
 
587
            f.close()
 
588
 
 
589
 
 
590
    def get_revision_xml(self, revision_id):
 
591
        """Return XML file object for revision object."""
 
592
        if not revision_id or not isinstance(revision_id, basestring):
 
593
            raise InvalidRevisionId(revision_id)
 
594
 
 
595
        self.lock_read()
 
596
        try:
 
597
            try:
 
598
                return self.revision_store[revision_id]
 
599
            except IndexError:
 
600
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
601
        finally:
 
602
            self.unlock()
540
603
 
541
604
 
542
605
    def get_revision(self, revision_id):
543
606
        """Return the Revision object for a named revision"""
544
 
        if not revision_id or not isinstance(revision_id, basestring):
545
 
            raise ValueError('invalid revision-id: %r' % revision_id)
546
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
607
        xml_file = self.get_revision_xml(revision_id)
 
608
 
 
609
        try:
 
610
            r = unpack_xml(Revision, xml_file)
 
611
        except SyntaxError, e:
 
612
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
613
                                         [revision_id,
 
614
                                          str(e)])
 
615
            
547
616
        assert r.revision_id == revision_id
548
617
        return r
549
618
 
 
619
 
 
620
    def get_revision_delta(self, revno):
 
621
        """Return the delta for one revision.
 
622
 
 
623
        The delta is relative to its mainline predecessor, or the
 
624
        empty tree for revision 1.
 
625
        """
 
626
        assert isinstance(revno, int)
 
627
        rh = self.revision_history()
 
628
        if not (1 <= revno <= len(rh)):
 
629
            raise InvalidRevisionNumber(revno)
 
630
 
 
631
        # revno is 1-based; list is 0-based
 
632
 
 
633
        new_tree = self.revision_tree(rh[revno-1])
 
634
        if revno == 1:
 
635
            old_tree = EmptyTree()
 
636
        else:
 
637
            old_tree = self.revision_tree(rh[revno-2])
 
638
 
 
639
        return compare_trees(old_tree, new_tree)
 
640
 
 
641
        
 
642
 
550
643
    def get_revision_sha1(self, revision_id):
551
644
        """Hash the stored value of a revision, and return it."""
552
645
        # In the future, revision entries will be signed. At that
555
648
        # the revision, (add signatures/remove signatures) and still
556
649
        # have all hash pointers stay consistent.
557
650
        # But for now, just hash the contents.
558
 
        return sha_file(self.revision_store[revision_id])
 
651
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
559
652
 
560
653
 
561
654
    def get_inventory(self, inventory_id):
564
657
        TODO: Perhaps for this and similar methods, take a revision
565
658
               parameter which can be either an integer revno or a
566
659
               string hash."""
567
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
568
 
        return i
 
660
        from bzrlib.inventory import Inventory
 
661
        from bzrlib.xml import unpack_xml
 
662
 
 
663
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
664
            
569
665
 
570
666
    def get_inventory_sha1(self, inventory_id):
571
667
        """Return the sha1 hash of the inventory entry
575
671
 
576
672
    def get_revision_inventory(self, revision_id):
577
673
        """Return inventory of a past revision."""
 
674
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
675
        # must be the same as its revision, so this is trivial.
578
676
        if revision_id == None:
579
 
            return Inventory()
 
677
            from bzrlib.inventory import Inventory
 
678
            return Inventory(self.get_root_id())
580
679
        else:
581
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
680
            return self.get_inventory(revision_id)
582
681
 
583
682
 
584
683
    def revision_history(self):
639
738
                return r+1, my_history[r]
640
739
        return None, None
641
740
 
642
 
    def enum_history(self, direction):
643
 
        """Return (revno, revision_id) for history of branch.
644
 
 
645
 
        direction
646
 
            'forward' is from earliest to latest
647
 
            'reverse' is from latest to earliest
648
 
        """
649
 
        rh = self.revision_history()
650
 
        if direction == 'forward':
651
 
            i = 1
652
 
            for rid in rh:
653
 
                yield i, rid
654
 
                i += 1
655
 
        elif direction == 'reverse':
656
 
            i = len(rh)
657
 
            while i > 0:
658
 
                yield i, rh[i-1]
659
 
                i -= 1
660
 
        else:
661
 
            raise ValueError('invalid history direction', direction)
662
 
 
663
741
 
664
742
    def revno(self):
665
743
        """Return current revision number for this branch.
753
831
 
754
832
        pb.update('comparing histories')
755
833
        revision_ids = self.missing_revisions(other, stop_revision)
 
834
 
 
835
        if hasattr(other.revision_store, "prefetch"):
 
836
            other.revision_store.prefetch(revision_ids)
 
837
        if hasattr(other.inventory_store, "prefetch"):
 
838
            inventory_ids = [other.get_revision(r).inventory_id
 
839
                             for r in revision_ids]
 
840
            other.inventory_store.prefetch(inventory_ids)
 
841
                
756
842
        revisions = []
757
 
        needed_texts = sets.Set()
 
843
        needed_texts = set()
758
844
        i = 0
759
845
        for rev_id in revision_ids:
760
846
            i += 1
785
871
                    
786
872
        
787
873
    def commit(self, *args, **kw):
788
 
        """Deprecated"""
789
874
        from bzrlib.commit import commit
790
875
        commit(self, *args, **kw)
791
876
        
792
877
 
793
 
    def lookup_revision(self, revno):
794
 
        """Return revision hash for revision number."""
795
 
        if revno == 0:
796
 
            return None
797
 
 
798
 
        try:
799
 
            # list is 0-based; revisions are 1-based
800
 
            return self.revision_history()[revno-1]
801
 
        except IndexError:
802
 
            raise BzrError("no such revision %s" % revno)
803
 
 
 
878
    def lookup_revision(self, revision):
 
879
        """Return the revision identifier for a given revision information."""
 
880
        revno, info = self.get_revision_info(revision)
 
881
        return info
 
882
 
 
883
    def get_revision_info(self, revision):
 
884
        """Return (revno, revision id) for revision identifier.
 
885
 
 
886
        revision can be an integer, in which case it is assumed to be revno (though
 
887
            this will translate negative values into positive ones)
 
888
        revision can also be a string, in which case it is parsed for something like
 
889
            'date:' or 'revid:' etc.
 
890
        """
 
891
        if revision is None:
 
892
            return 0, None
 
893
        revno = None
 
894
        try:# Convert to int if possible
 
895
            revision = int(revision)
 
896
        except ValueError:
 
897
            pass
 
898
        revs = self.revision_history()
 
899
        if isinstance(revision, int):
 
900
            if revision == 0:
 
901
                return 0, None
 
902
            # Mabye we should do this first, but we don't need it if revision == 0
 
903
            if revision < 0:
 
904
                revno = len(revs) + revision + 1
 
905
            else:
 
906
                revno = revision
 
907
        elif isinstance(revision, basestring):
 
908
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
909
                if revision.startswith(prefix):
 
910
                    revno = func(self, revs, revision)
 
911
                    break
 
912
            else:
 
913
                raise BzrError('No namespace registered for string: %r' % revision)
 
914
 
 
915
        if revno is None or revno <= 0 or revno > len(revs):
 
916
            raise BzrError("no such revision %s" % revision)
 
917
        return revno, revs[revno-1]
 
918
 
 
919
    def _namespace_revno(self, revs, revision):
 
920
        """Lookup a revision by revision number"""
 
921
        assert revision.startswith('revno:')
 
922
        try:
 
923
            return int(revision[6:])
 
924
        except ValueError:
 
925
            return None
 
926
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
927
 
 
928
    def _namespace_revid(self, revs, revision):
 
929
        assert revision.startswith('revid:')
 
930
        try:
 
931
            return revs.index(revision[6:]) + 1
 
932
        except ValueError:
 
933
            return None
 
934
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
935
 
 
936
    def _namespace_last(self, revs, revision):
 
937
        assert revision.startswith('last:')
 
938
        try:
 
939
            offset = int(revision[5:])
 
940
        except ValueError:
 
941
            return None
 
942
        else:
 
943
            if offset <= 0:
 
944
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
945
            return len(revs) - offset + 1
 
946
    REVISION_NAMESPACES['last:'] = _namespace_last
 
947
 
 
948
    def _namespace_tag(self, revs, revision):
 
949
        assert revision.startswith('tag:')
 
950
        raise BzrError('tag: namespace registered, but not implemented.')
 
951
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
952
 
 
953
    def _namespace_date(self, revs, revision):
 
954
        assert revision.startswith('date:')
 
955
        import datetime
 
956
        # Spec for date revisions:
 
957
        #   date:value
 
958
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
959
        #   it can also start with a '+/-/='. '+' says match the first
 
960
        #   entry after the given date. '-' is match the first entry before the date
 
961
        #   '=' is match the first entry after, but still on the given date.
 
962
        #
 
963
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
964
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
965
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
966
        #       May 13th, 2005 at 0:00
 
967
        #
 
968
        #   So the proper way of saying 'give me all entries for today' is:
 
969
        #       -r {date:+today}:{date:-tomorrow}
 
970
        #   The default is '=' when not supplied
 
971
        val = revision[5:]
 
972
        match_style = '='
 
973
        if val[:1] in ('+', '-', '='):
 
974
            match_style = val[:1]
 
975
            val = val[1:]
 
976
 
 
977
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
978
        if val.lower() == 'yesterday':
 
979
            dt = today - datetime.timedelta(days=1)
 
980
        elif val.lower() == 'today':
 
981
            dt = today
 
982
        elif val.lower() == 'tomorrow':
 
983
            dt = today + datetime.timedelta(days=1)
 
984
        else:
 
985
            import re
 
986
            # This should be done outside the function to avoid recompiling it.
 
987
            _date_re = re.compile(
 
988
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
989
                    r'(,|T)?\s*'
 
990
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
991
                )
 
992
            m = _date_re.match(val)
 
993
            if not m or (not m.group('date') and not m.group('time')):
 
994
                raise BzrError('Invalid revision date %r' % revision)
 
995
 
 
996
            if m.group('date'):
 
997
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
998
            else:
 
999
                year, month, day = today.year, today.month, today.day
 
1000
            if m.group('time'):
 
1001
                hour = int(m.group('hour'))
 
1002
                minute = int(m.group('minute'))
 
1003
                if m.group('second'):
 
1004
                    second = int(m.group('second'))
 
1005
                else:
 
1006
                    second = 0
 
1007
            else:
 
1008
                hour, minute, second = 0,0,0
 
1009
 
 
1010
            dt = datetime.datetime(year=year, month=month, day=day,
 
1011
                    hour=hour, minute=minute, second=second)
 
1012
        first = dt
 
1013
        last = None
 
1014
        reversed = False
 
1015
        if match_style == '-':
 
1016
            reversed = True
 
1017
        elif match_style == '=':
 
1018
            last = dt + datetime.timedelta(days=1)
 
1019
 
 
1020
        if reversed:
 
1021
            for i in range(len(revs)-1, -1, -1):
 
1022
                r = self.get_revision(revs[i])
 
1023
                # TODO: Handle timezone.
 
1024
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1025
                if first >= dt and (last is None or dt >= last):
 
1026
                    return i+1
 
1027
        else:
 
1028
            for i in range(len(revs)):
 
1029
                r = self.get_revision(revs[i])
 
1030
                # TODO: Handle timezone.
 
1031
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1032
                if first <= dt and (last is None or dt <= last):
 
1033
                    return i+1
 
1034
    REVISION_NAMESPACES['date:'] = _namespace_date
804
1035
 
805
1036
    def revision_tree(self, revision_id):
806
1037
        """Return Tree for a revision on this branch.
950
1181
            self.unlock()
951
1182
 
952
1183
 
 
1184
    def revert(self, filenames, old_tree=None, backups=True):
 
1185
        """Restore selected files to the versions from a previous tree.
 
1186
 
 
1187
        backups
 
1188
            If true (default) backups are made of files before
 
1189
            they're renamed.
 
1190
        """
 
1191
        from bzrlib.errors import NotVersionedError, BzrError
 
1192
        from bzrlib.atomicfile import AtomicFile
 
1193
        from bzrlib.osutils import backup_file
 
1194
        
 
1195
        inv = self.read_working_inventory()
 
1196
        if old_tree is None:
 
1197
            old_tree = self.basis_tree()
 
1198
        old_inv = old_tree.inventory
 
1199
 
 
1200
        nids = []
 
1201
        for fn in filenames:
 
1202
            file_id = inv.path2id(fn)
 
1203
            if not file_id:
 
1204
                raise NotVersionedError("not a versioned file", fn)
 
1205
            if not old_inv.has_id(file_id):
 
1206
                raise BzrError("file not present in old tree", fn, file_id)
 
1207
            nids.append((fn, file_id))
 
1208
            
 
1209
        # TODO: Rename back if it was previously at a different location
 
1210
 
 
1211
        # TODO: If given a directory, restore the entire contents from
 
1212
        # the previous version.
 
1213
 
 
1214
        # TODO: Make a backup to a temporary file.
 
1215
 
 
1216
        # TODO: If the file previously didn't exist, delete it?
 
1217
        for fn, file_id in nids:
 
1218
            backup_file(fn)
 
1219
            
 
1220
            f = AtomicFile(fn, 'wb')
 
1221
            try:
 
1222
                f.write(old_tree.get_file(file_id).read())
 
1223
                f.commit()
 
1224
            finally:
 
1225
                f.close()
 
1226
 
 
1227
 
 
1228
    def pending_merges(self):
 
1229
        """Return a list of pending merges.
 
1230
 
 
1231
        These are revisions that have been merged into the working
 
1232
        directory but not yet committed.
 
1233
        """
 
1234
        cfn = self.controlfilename('pending-merges')
 
1235
        if not os.path.exists(cfn):
 
1236
            return []
 
1237
        p = []
 
1238
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1239
            p.append(l.rstrip('\n'))
 
1240
        return p
 
1241
 
 
1242
 
 
1243
    def add_pending_merge(self, revision_id):
 
1244
        from bzrlib.revision import validate_revision_id
 
1245
 
 
1246
        validate_revision_id(revision_id)
 
1247
 
 
1248
        p = self.pending_merges()
 
1249
        if revision_id in p:
 
1250
            return
 
1251
        p.append(revision_id)
 
1252
        self.set_pending_merges(p)
 
1253
 
 
1254
 
 
1255
    def set_pending_merges(self, rev_list):
 
1256
        from bzrlib.atomicfile import AtomicFile
 
1257
        self.lock_write()
 
1258
        try:
 
1259
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1260
            try:
 
1261
                for l in rev_list:
 
1262
                    print >>f, l
 
1263
                f.commit()
 
1264
            finally:
 
1265
                f.close()
 
1266
        finally:
 
1267
            self.unlock()
 
1268
 
 
1269
 
953
1270
 
954
1271
class ScratchBranch(Branch):
955
1272
    """Special test class: a branch that cleans up after itself.
969
1286
 
970
1287
        If any files are listed, they are created in the working copy.
971
1288
        """
 
1289
        from tempfile import mkdtemp
972
1290
        init = False
973
1291
        if base is None:
974
 
            base = tempfile.mkdtemp()
 
1292
            base = mkdtemp()
975
1293
            init = True
976
1294
        Branch.__init__(self, base, init=init)
977
1295
        for d in dirs:
990
1308
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
991
1309
        True
992
1310
        """
993
 
        base = tempfile.mkdtemp()
 
1311
        from shutil import copytree
 
1312
        from tempfile import mkdtemp
 
1313
        base = mkdtemp()
994
1314
        os.rmdir(base)
995
 
        shutil.copytree(self.base, base, symlinks=True)
 
1315
        copytree(self.base, base, symlinks=True)
996
1316
        return ScratchBranch(base=base)
997
1317
        
998
1318
    def __del__(self):
1000
1320
 
1001
1321
    def destroy(self):
1002
1322
        """Destroy the test branch, removing the scratch directory."""
 
1323
        from shutil import rmtree
1003
1324
        try:
1004
1325
            if self.base:
1005
1326
                mutter("delete ScratchBranch %s" % self.base)
1006
 
                shutil.rmtree(self.base)
 
1327
                rmtree(self.base)
1007
1328
        except OSError, e:
1008
1329
            # Work around for shutil.rmtree failing on Windows when
1009
1330
            # readonly files are encountered
1011
1332
            for root, dirs, files in os.walk(self.base, topdown=False):
1012
1333
                for name in files:
1013
1334
                    os.chmod(os.path.join(root, name), 0700)
1014
 
            shutil.rmtree(self.base)
 
1335
            rmtree(self.base)
1015
1336
        self.base = None
1016
1337
 
1017
1338
    
1042
1363
    cope with just randomness because running uuidgen every time is
1043
1364
    slow."""
1044
1365
    import re
 
1366
    from binascii import hexlify
 
1367
    from time import time
1045
1368
 
1046
1369
    # get last component
1047
1370
    idx = name.rfind('/')
1059
1382
    name = re.sub(r'[^\w.]', '', name)
1060
1383
 
1061
1384
    s = hexlify(rand_bytes(8))
1062
 
    return '-'.join((name, compact_date(time.time()), s))
 
1385
    return '-'.join((name, compact_date(time()), s))
 
1386
 
 
1387
 
 
1388
def gen_root_id():
 
1389
    """Return a new tree-root file id."""
 
1390
    return gen_file_id('TREE_ROOT')
 
1391