~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-08-26 02:31:37 UTC
  • Revision ID: mbp@sourcefrog.net-20050826023137-eb4b101cc92f9792
- ignore tags files

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
 
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
 
 
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
 
28
import bzrlib.errors
 
29
from bzrlib.textui import show_status
 
30
from bzrlib.revision import Revision
 
31
from bzrlib.xml import unpack_xml
 
32
from bzrlib.delta import compare_trees
 
33
from bzrlib.tree import EmptyTree, RevisionTree
 
34
import bzrlib.ui
 
35
 
 
36
 
34
37
 
35
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
39
## TODO: Maybe include checks for common corruption of newlines, etc?
37
40
 
38
41
 
 
42
# TODO: Some operations like log might retrieve the same revisions
 
43
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
44
# cache in memory to make this faster.
 
45
 
 
46
# TODO: please move the revision-string syntax stuff out of the branch
 
47
# object; it's clutter
 
48
 
39
49
 
40
50
def find_branch(f, **args):
41
51
    if f and (f.startswith('http://') or f.startswith('https://')):
45
55
        return Branch(f, **args)
46
56
 
47
57
 
 
58
def find_cached_branch(f, cache_root, **args):
 
59
    from remotebranch import RemoteBranch
 
60
    br = find_branch(f, **args)
 
61
    def cacheify(br, store_name):
 
62
        from meta_store import CachedStore
 
63
        cache_path = os.path.join(cache_root, store_name)
 
64
        os.mkdir(cache_path)
 
65
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
66
        setattr(br, store_name, new_store)
 
67
 
 
68
    if isinstance(br, RemoteBranch):
 
69
        cacheify(br, 'inventory_store')
 
70
        cacheify(br, 'text_store')
 
71
        cacheify(br, 'revision_store')
 
72
    return br
 
73
 
48
74
 
49
75
def _relpath(base, path):
50
76
    """Return path relative to base, or raise exception.
82
108
    It is not necessary that f exists.
83
109
 
84
110
    Basically we keep looking up until we find the control directory or
85
 
    run into the root."""
 
111
    run into the root.  If there isn't one, raises NotBranchError.
 
112
    """
86
113
    if f == None:
87
114
        f = os.getcwd()
88
115
    elif hasattr(os.path, 'realpath'):
101
128
        head, tail = os.path.split(f)
102
129
        if head == f:
103
130
            # reached the root, whatever that may be
104
 
            raise BzrError('%r is not in a branch' % orig_f)
 
131
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
105
132
        f = head
106
 
    
 
133
 
 
134
 
 
135
 
 
136
# XXX: move into bzrlib.errors; subclass BzrError    
107
137
class DivergedBranches(Exception):
108
138
    def __init__(self, branch1, branch2):
109
139
        self.branch1 = branch1
111
141
        Exception.__init__(self, "These branches have diverged.")
112
142
 
113
143
 
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
144
######################################################################
123
145
# branch objects
124
146
 
143
165
    _lock_count = None
144
166
    _lock = None
145
167
    
 
168
    # Map some sort of prefix into a namespace
 
169
    # stuff like "revno:10", "revid:", etc.
 
170
    # This should match a prefix with a function which accepts
 
171
    REVISION_NAMESPACES = {}
 
172
 
146
173
    def __init__(self, base, init=False, find_root=True):
147
174
        """Create new branch object at a particular location.
148
175
 
158
185
        In the test suite, creation of new trees is tested using the
159
186
        `ScratchBranch` class.
160
187
        """
 
188
        from bzrlib.store import ImmutableStore
161
189
        if init:
162
190
            self.base = os.path.realpath(base)
163
191
            self._make_control()
249
277
 
250
278
    def controlfilename(self, file_or_path):
251
279
        """Return location relative to branch."""
252
 
        if isinstance(file_or_path, types.StringTypes):
 
280
        if isinstance(file_or_path, basestring):
253
281
            file_or_path = [file_or_path]
254
282
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
255
283
 
282
310
 
283
311
 
284
312
    def _make_control(self):
 
313
        from bzrlib.inventory import Inventory
 
314
        from bzrlib.xml import pack_xml
 
315
        
285
316
        os.mkdir(self.controlfilename([]))
286
317
        self.controlfile('README', 'w').write(
287
318
            "This is a Bazaar-NG control directory.\n"
291
322
            os.mkdir(self.controlfilename(d))
292
323
        for f in ('revision-history', 'merged-patches',
293
324
                  'pending-merged-patches', 'branch-name',
294
 
                  'branch-lock'):
 
325
                  'branch-lock',
 
326
                  'pending-merges'):
295
327
            self.controlfile(f, 'w').write('')
296
328
        mutter('created control directory in ' + self.base)
297
 
        Inventory().write_xml(self.controlfile('inventory','w'))
 
329
 
 
330
        # if we want per-tree root ids then this is the place to set
 
331
        # them; they're not needed for now and so ommitted for
 
332
        # simplicity.
 
333
        pack_xml(Inventory(), self.controlfile('inventory','w'))
298
334
 
299
335
 
300
336
    def _check_format(self):
315
351
                           ['use a different bzr version',
316
352
                            'or remove the .bzr directory and "bzr init" again'])
317
353
 
 
354
    def get_root_id(self):
 
355
        """Return the id of this branches root"""
 
356
        inv = self.read_working_inventory()
 
357
        return inv.root.file_id
318
358
 
 
359
    def set_root_id(self, file_id):
 
360
        inv = self.read_working_inventory()
 
361
        orig_root_id = inv.root.file_id
 
362
        del inv._byid[inv.root.file_id]
 
363
        inv.root.file_id = file_id
 
364
        inv._byid[inv.root.file_id] = inv.root
 
365
        for fid in inv:
 
366
            entry = inv[fid]
 
367
            if entry.parent_id in (None, orig_root_id):
 
368
                entry.parent_id = inv.root.file_id
 
369
        self._write_inventory(inv)
319
370
 
320
371
    def read_working_inventory(self):
321
372
        """Read the working inventory."""
322
 
        before = time.time()
323
 
        # ElementTree does its own conversion from UTF-8, so open in
324
 
        # binary.
 
373
        from bzrlib.inventory import Inventory
 
374
        from bzrlib.xml import unpack_xml
 
375
        from time import time
 
376
        before = time()
325
377
        self.lock_read()
326
378
        try:
327
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
379
            # ElementTree does its own conversion from UTF-8, so open in
 
380
            # binary.
 
381
            inv = unpack_xml(Inventory,
 
382
                             self.controlfile('inventory', 'rb'))
328
383
            mutter("loaded inventory of %d items in %f"
329
 
                   % (len(inv), time.time() - before))
 
384
                   % (len(inv), time() - before))
330
385
            return inv
331
386
        finally:
332
387
            self.unlock()
338
393
        That is to say, the inventory describing changes underway, that
339
394
        will be committed to the next revision.
340
395
        """
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)
 
396
        from bzrlib.atomicfile import AtomicFile
 
397
        from bzrlib.xml import pack_xml
 
398
        
 
399
        self.lock_write()
 
400
        try:
 
401
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
402
            try:
 
403
                pack_xml(inv, f)
 
404
                f.commit()
 
405
            finally:
 
406
                f.close()
 
407
        finally:
 
408
            self.unlock()
 
409
        
351
410
        mutter('wrote working inventory')
352
411
            
353
412
 
383
442
        """
384
443
        # TODO: Re-adding a file that is removed in the working copy
385
444
        # 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))
 
445
        if isinstance(files, basestring):
 
446
            assert(ids is None or isinstance(ids, basestring))
388
447
            files = [files]
389
448
            if ids is not None:
390
449
                ids = [ids]
422
481
                inv.add_path(f, kind=kind, file_id=file_id)
423
482
 
424
483
                if verbose:
425
 
                    show_status('A', kind, quotefn(f))
 
484
                    print 'added', quotefn(f)
426
485
 
427
486
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
428
487
 
439
498
            # use inventory as it was in that revision
440
499
            file_id = tree.inventory.path2id(file)
441
500
            if not file_id:
442
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
501
                raise BzrError("%r is not present in revision %s" % (file, revno))
443
502
            tree.print_file(file_id)
444
503
        finally:
445
504
            self.unlock()
461
520
        """
462
521
        ## TODO: Normalize names
463
522
        ## TODO: Remove nested loops; better scalability
464
 
        if isinstance(files, types.StringTypes):
 
523
        if isinstance(files, basestring):
465
524
            files = [files]
466
525
 
467
526
        self.lock_write()
492
551
 
493
552
    # FIXME: this doesn't need to be a branch method
494
553
    def set_inventory(self, new_inventory_list):
495
 
        inv = Inventory()
 
554
        from bzrlib.inventory import Inventory, InventoryEntry
 
555
        inv = Inventory(self.get_root_id())
496
556
        for path, file_id, parent, kind in new_inventory_list:
497
557
            name = os.path.basename(path)
498
558
            if name == "":
520
580
        return self.working_tree().unknowns()
521
581
 
522
582
 
523
 
    def append_revision(self, revision_id):
524
 
        mutter("add {%s} to revision-history" % revision_id)
 
583
    def append_revision(self, *revision_ids):
 
584
        from bzrlib.atomicfile import AtomicFile
 
585
 
 
586
        for revision_id in revision_ids:
 
587
            mutter("add {%s} to revision-history" % revision_id)
 
588
 
525
589
        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
 
        
 
590
        rev_history.extend(revision_ids)
 
591
 
 
592
        f = AtomicFile(self.controlfilename('revision-history'))
 
593
        try:
 
594
            for rev_id in rev_history:
 
595
                print >>f, rev_id
 
596
            f.commit()
 
597
        finally:
 
598
            f.close()
 
599
 
 
600
 
 
601
    def get_revision_xml(self, revision_id):
 
602
        """Return XML file object for revision object."""
 
603
        if not revision_id or not isinstance(revision_id, basestring):
 
604
            raise InvalidRevisionId(revision_id)
 
605
 
 
606
        self.lock_read()
 
607
        try:
 
608
            try:
 
609
                return self.revision_store[revision_id]
 
610
            except IndexError:
 
611
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
612
        finally:
 
613
            self.unlock()
540
614
 
541
615
 
542
616
    def get_revision(self, revision_id):
543
617
        """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])
 
618
        xml_file = self.get_revision_xml(revision_id)
 
619
 
 
620
        try:
 
621
            r = unpack_xml(Revision, xml_file)
 
622
        except SyntaxError, e:
 
623
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
624
                                         [revision_id,
 
625
                                          str(e)])
 
626
            
547
627
        assert r.revision_id == revision_id
548
628
        return r
549
629
 
 
630
 
 
631
    def get_revision_delta(self, revno):
 
632
        """Return the delta for one revision.
 
633
 
 
634
        The delta is relative to its mainline predecessor, or the
 
635
        empty tree for revision 1.
 
636
        """
 
637
        assert isinstance(revno, int)
 
638
        rh = self.revision_history()
 
639
        if not (1 <= revno <= len(rh)):
 
640
            raise InvalidRevisionNumber(revno)
 
641
 
 
642
        # revno is 1-based; list is 0-based
 
643
 
 
644
        new_tree = self.revision_tree(rh[revno-1])
 
645
        if revno == 1:
 
646
            old_tree = EmptyTree()
 
647
        else:
 
648
            old_tree = self.revision_tree(rh[revno-2])
 
649
 
 
650
        return compare_trees(old_tree, new_tree)
 
651
 
 
652
        
 
653
 
550
654
    def get_revision_sha1(self, revision_id):
551
655
        """Hash the stored value of a revision, and return it."""
552
656
        # In the future, revision entries will be signed. At that
555
659
        # the revision, (add signatures/remove signatures) and still
556
660
        # have all hash pointers stay consistent.
557
661
        # But for now, just hash the contents.
558
 
        return sha_file(self.revision_store[revision_id])
 
662
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
559
663
 
560
664
 
561
665
    def get_inventory(self, inventory_id):
564
668
        TODO: Perhaps for this and similar methods, take a revision
565
669
               parameter which can be either an integer revno or a
566
670
               string hash."""
567
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
568
 
        return i
 
671
        from bzrlib.inventory import Inventory
 
672
        from bzrlib.xml import unpack_xml
 
673
 
 
674
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
 
675
 
 
676
 
 
677
    def get_inventory_xml(self, inventory_id):
 
678
        """Get inventory XML as a file object."""
 
679
        return self.inventory_store[inventory_id]
 
680
            
569
681
 
570
682
    def get_inventory_sha1(self, inventory_id):
571
683
        """Return the sha1 hash of the inventory entry
572
684
        """
573
 
        return sha_file(self.inventory_store[inventory_id])
 
685
        return sha_file(self.get_inventory_xml(inventory_id))
574
686
 
575
687
 
576
688
    def get_revision_inventory(self, revision_id):
577
689
        """Return inventory of a past revision."""
 
690
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
691
        # must be the same as its revision, so this is trivial.
578
692
        if revision_id == None:
579
 
            return Inventory()
 
693
            from bzrlib.inventory import Inventory
 
694
            return Inventory(self.get_root_id())
580
695
        else:
581
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
696
            return self.get_inventory(revision_id)
582
697
 
583
698
 
584
699
    def revision_history(self):
639
754
                return r+1, my_history[r]
640
755
        return None, None
641
756
 
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
757
 
664
758
    def revno(self):
665
759
        """Return current revision number for this branch.
680
774
            return None
681
775
 
682
776
 
683
 
    def missing_revisions(self, other, stop_revision=None):
 
777
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
684
778
        """
685
779
        If self and other have not diverged, return a list of the revisions
686
780
        present in other, but missing from self.
719
813
        if stop_revision is None:
720
814
            stop_revision = other_len
721
815
        elif stop_revision > other_len:
722
 
            raise NoSuchRevision(self, stop_revision)
 
816
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
723
817
        
724
818
        return other_history[self_len:stop_revision]
725
819
 
726
820
 
727
821
    def update_revisions(self, other, stop_revision=None):
728
822
        """Pull in all new revisions from other branch.
729
 
        
730
 
        >>> from bzrlib.commit import commit
731
 
        >>> bzrlib.trace.silent = True
732
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
733
 
        >>> br1.add('foo')
734
 
        >>> br1.add('bar')
735
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
736
 
        >>> br2 = ScratchBranch()
737
 
        >>> br2.update_revisions(br1)
738
 
        Added 2 texts.
739
 
        Added 1 inventories.
740
 
        Added 1 revisions.
741
 
        >>> br2.revision_history()
742
 
        [u'REVISION-ID-1']
743
 
        >>> br2.update_revisions(br1)
744
 
        Added 0 texts.
745
 
        Added 0 inventories.
746
 
        Added 0 revisions.
747
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
748
 
        True
749
823
        """
750
 
        from bzrlib.progress import ProgressBar
751
 
 
752
 
        pb = ProgressBar()
753
 
 
 
824
        from bzrlib.fetch import greedy_fetch
 
825
 
 
826
        pb = bzrlib.ui.ui_factory.progress_bar()
754
827
        pb.update('comparing histories')
 
828
 
755
829
        revision_ids = self.missing_revisions(other, stop_revision)
 
830
 
 
831
        if len(revision_ids) > 0:
 
832
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
833
        else:
 
834
            count = 0
 
835
        self.append_revision(*revision_ids)
 
836
        ## note("Added %d revisions." % count)
 
837
        pb.clear()
 
838
 
 
839
        
 
840
        
 
841
    def install_revisions(self, other, revision_ids, pb):
 
842
        if hasattr(other.revision_store, "prefetch"):
 
843
            other.revision_store.prefetch(revision_ids)
 
844
        if hasattr(other.inventory_store, "prefetch"):
 
845
            inventory_ids = [other.get_revision(r).inventory_id
 
846
                             for r in revision_ids]
 
847
            other.inventory_store.prefetch(inventory_ids)
 
848
 
 
849
        if pb is None:
 
850
            pb = bzrlib.ui.ui_factory.progress_bar()
 
851
                
756
852
        revisions = []
757
 
        needed_texts = sets.Set()
 
853
        needed_texts = set()
758
854
        i = 0
759
 
        for rev_id in revision_ids:
760
 
            i += 1
761
 
            pb.update('fetching revision', i, len(revision_ids))
762
 
            rev = other.get_revision(rev_id)
 
855
 
 
856
        failures = set()
 
857
        for i, rev_id in enumerate(revision_ids):
 
858
            pb.update('fetching revision', i+1, len(revision_ids))
 
859
            try:
 
860
                rev = other.get_revision(rev_id)
 
861
            except bzrlib.errors.NoSuchRevision:
 
862
                failures.add(rev_id)
 
863
                continue
 
864
 
763
865
            revisions.append(rev)
764
866
            inv = other.get_inventory(str(rev.inventory_id))
765
867
            for key, entry in inv.iter_entries():
770
872
 
771
873
        pb.clear()
772
874
                    
773
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
774
 
        print "Added %d texts." % count 
 
875
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
876
                                                    needed_texts)
 
877
        #print "Added %d texts." % count 
775
878
        inventory_ids = [ f.inventory_id for f in revisions ]
776
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
777
 
                                                inventory_ids)
778
 
        print "Added %d inventories." % count 
 
879
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
880
                                                         inventory_ids)
 
881
        #print "Added %d inventories." % count 
779
882
        revision_ids = [ f.revision_id for f in revisions]
780
 
        count = self.revision_store.copy_multi(other.revision_store, 
781
 
                                               revision_ids)
782
 
        for revision_id in revision_ids:
783
 
            self.append_revision(revision_id)
784
 
        print "Added %d revisions." % count
785
 
                    
786
 
        
 
883
 
 
884
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
885
                                                          revision_ids,
 
886
                                                          permit_failure=True)
 
887
        assert len(cp_fail) == 0 
 
888
        return count, failures
 
889
       
 
890
 
787
891
    def commit(self, *args, **kw):
788
 
        """Deprecated"""
789
892
        from bzrlib.commit import commit
790
893
        commit(self, *args, **kw)
791
894
        
792
895
 
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
 
 
 
896
    def lookup_revision(self, revision):
 
897
        """Return the revision identifier for a given revision information."""
 
898
        revno, info = self.get_revision_info(revision)
 
899
        return info
 
900
 
 
901
 
 
902
    def revision_id_to_revno(self, revision_id):
 
903
        """Given a revision id, return its revno"""
 
904
        history = self.revision_history()
 
905
        try:
 
906
            return history.index(revision_id) + 1
 
907
        except ValueError:
 
908
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
909
 
 
910
 
 
911
    def get_revision_info(self, revision):
 
912
        """Return (revno, revision id) for revision identifier.
 
913
 
 
914
        revision can be an integer, in which case it is assumed to be revno (though
 
915
            this will translate negative values into positive ones)
 
916
        revision can also be a string, in which case it is parsed for something like
 
917
            'date:' or 'revid:' etc.
 
918
        """
 
919
        if revision is None:
 
920
            return 0, None
 
921
        revno = None
 
922
        try:# Convert to int if possible
 
923
            revision = int(revision)
 
924
        except ValueError:
 
925
            pass
 
926
        revs = self.revision_history()
 
927
        if isinstance(revision, int):
 
928
            if revision == 0:
 
929
                return 0, None
 
930
            # Mabye we should do this first, but we don't need it if revision == 0
 
931
            if revision < 0:
 
932
                revno = len(revs) + revision + 1
 
933
            else:
 
934
                revno = revision
 
935
        elif isinstance(revision, basestring):
 
936
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
937
                if revision.startswith(prefix):
 
938
                    revno = func(self, revs, revision)
 
939
                    break
 
940
            else:
 
941
                raise BzrError('No namespace registered for string: %r' % revision)
 
942
 
 
943
        if revno is None or revno <= 0 or revno > len(revs):
 
944
            raise BzrError("no such revision %s" % revision)
 
945
        return revno, revs[revno-1]
 
946
 
 
947
    def _namespace_revno(self, revs, revision):
 
948
        """Lookup a revision by revision number"""
 
949
        assert revision.startswith('revno:')
 
950
        try:
 
951
            return int(revision[6:])
 
952
        except ValueError:
 
953
            return None
 
954
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
955
 
 
956
    def _namespace_revid(self, revs, revision):
 
957
        assert revision.startswith('revid:')
 
958
        try:
 
959
            return revs.index(revision[6:]) + 1
 
960
        except ValueError:
 
961
            return None
 
962
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
963
 
 
964
    def _namespace_last(self, revs, revision):
 
965
        assert revision.startswith('last:')
 
966
        try:
 
967
            offset = int(revision[5:])
 
968
        except ValueError:
 
969
            return None
 
970
        else:
 
971
            if offset <= 0:
 
972
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
973
            return len(revs) - offset + 1
 
974
    REVISION_NAMESPACES['last:'] = _namespace_last
 
975
 
 
976
    def _namespace_tag(self, revs, revision):
 
977
        assert revision.startswith('tag:')
 
978
        raise BzrError('tag: namespace registered, but not implemented.')
 
979
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
980
 
 
981
    def _namespace_date(self, revs, revision):
 
982
        assert revision.startswith('date:')
 
983
        import datetime
 
984
        # Spec for date revisions:
 
985
        #   date:value
 
986
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
987
        #   it can also start with a '+/-/='. '+' says match the first
 
988
        #   entry after the given date. '-' is match the first entry before the date
 
989
        #   '=' is match the first entry after, but still on the given date.
 
990
        #
 
991
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
992
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
993
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
994
        #       May 13th, 2005 at 0:00
 
995
        #
 
996
        #   So the proper way of saying 'give me all entries for today' is:
 
997
        #       -r {date:+today}:{date:-tomorrow}
 
998
        #   The default is '=' when not supplied
 
999
        val = revision[5:]
 
1000
        match_style = '='
 
1001
        if val[:1] in ('+', '-', '='):
 
1002
            match_style = val[:1]
 
1003
            val = val[1:]
 
1004
 
 
1005
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
1006
        if val.lower() == 'yesterday':
 
1007
            dt = today - datetime.timedelta(days=1)
 
1008
        elif val.lower() == 'today':
 
1009
            dt = today
 
1010
        elif val.lower() == 'tomorrow':
 
1011
            dt = today + datetime.timedelta(days=1)
 
1012
        else:
 
1013
            import re
 
1014
            # This should be done outside the function to avoid recompiling it.
 
1015
            _date_re = re.compile(
 
1016
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
1017
                    r'(,|T)?\s*'
 
1018
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1019
                )
 
1020
            m = _date_re.match(val)
 
1021
            if not m or (not m.group('date') and not m.group('time')):
 
1022
                raise BzrError('Invalid revision date %r' % revision)
 
1023
 
 
1024
            if m.group('date'):
 
1025
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1026
            else:
 
1027
                year, month, day = today.year, today.month, today.day
 
1028
            if m.group('time'):
 
1029
                hour = int(m.group('hour'))
 
1030
                minute = int(m.group('minute'))
 
1031
                if m.group('second'):
 
1032
                    second = int(m.group('second'))
 
1033
                else:
 
1034
                    second = 0
 
1035
            else:
 
1036
                hour, minute, second = 0,0,0
 
1037
 
 
1038
            dt = datetime.datetime(year=year, month=month, day=day,
 
1039
                    hour=hour, minute=minute, second=second)
 
1040
        first = dt
 
1041
        last = None
 
1042
        reversed = False
 
1043
        if match_style == '-':
 
1044
            reversed = True
 
1045
        elif match_style == '=':
 
1046
            last = dt + datetime.timedelta(days=1)
 
1047
 
 
1048
        if reversed:
 
1049
            for i in range(len(revs)-1, -1, -1):
 
1050
                r = self.get_revision(revs[i])
 
1051
                # TODO: Handle timezone.
 
1052
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1053
                if first >= dt and (last is None or dt >= last):
 
1054
                    return i+1
 
1055
        else:
 
1056
            for i in range(len(revs)):
 
1057
                r = self.get_revision(revs[i])
 
1058
                # TODO: Handle timezone.
 
1059
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1060
                if first <= dt and (last is None or dt <= last):
 
1061
                    return i+1
 
1062
    REVISION_NAMESPACES['date:'] = _namespace_date
804
1063
 
805
1064
    def revision_tree(self, revision_id):
806
1065
        """Return Tree for a revision on this branch.
950
1209
            self.unlock()
951
1210
 
952
1211
 
 
1212
    def revert(self, filenames, old_tree=None, backups=True):
 
1213
        """Restore selected files to the versions from a previous tree.
 
1214
 
 
1215
        backups
 
1216
            If true (default) backups are made of files before
 
1217
            they're renamed.
 
1218
        """
 
1219
        from bzrlib.errors import NotVersionedError, BzrError
 
1220
        from bzrlib.atomicfile import AtomicFile
 
1221
        from bzrlib.osutils import backup_file
 
1222
        
 
1223
        inv = self.read_working_inventory()
 
1224
        if old_tree is None:
 
1225
            old_tree = self.basis_tree()
 
1226
        old_inv = old_tree.inventory
 
1227
 
 
1228
        nids = []
 
1229
        for fn in filenames:
 
1230
            file_id = inv.path2id(fn)
 
1231
            if not file_id:
 
1232
                raise NotVersionedError("not a versioned file", fn)
 
1233
            if not old_inv.has_id(file_id):
 
1234
                raise BzrError("file not present in old tree", fn, file_id)
 
1235
            nids.append((fn, file_id))
 
1236
            
 
1237
        # TODO: Rename back if it was previously at a different location
 
1238
 
 
1239
        # TODO: If given a directory, restore the entire contents from
 
1240
        # the previous version.
 
1241
 
 
1242
        # TODO: Make a backup to a temporary file.
 
1243
 
 
1244
        # TODO: If the file previously didn't exist, delete it?
 
1245
        for fn, file_id in nids:
 
1246
            backup_file(fn)
 
1247
            
 
1248
            f = AtomicFile(fn, 'wb')
 
1249
            try:
 
1250
                f.write(old_tree.get_file(file_id).read())
 
1251
                f.commit()
 
1252
            finally:
 
1253
                f.close()
 
1254
 
 
1255
 
 
1256
    def pending_merges(self):
 
1257
        """Return a list of pending merges.
 
1258
 
 
1259
        These are revisions that have been merged into the working
 
1260
        directory but not yet committed.
 
1261
        """
 
1262
        cfn = self.controlfilename('pending-merges')
 
1263
        if not os.path.exists(cfn):
 
1264
            return []
 
1265
        p = []
 
1266
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1267
            p.append(l.rstrip('\n'))
 
1268
        return p
 
1269
 
 
1270
 
 
1271
    def add_pending_merge(self, revision_id):
 
1272
        from bzrlib.revision import validate_revision_id
 
1273
 
 
1274
        validate_revision_id(revision_id)
 
1275
 
 
1276
        p = self.pending_merges()
 
1277
        if revision_id in p:
 
1278
            return
 
1279
        p.append(revision_id)
 
1280
        self.set_pending_merges(p)
 
1281
 
 
1282
 
 
1283
    def set_pending_merges(self, rev_list):
 
1284
        from bzrlib.atomicfile import AtomicFile
 
1285
        self.lock_write()
 
1286
        try:
 
1287
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1288
            try:
 
1289
                for l in rev_list:
 
1290
                    print >>f, l
 
1291
                f.commit()
 
1292
            finally:
 
1293
                f.close()
 
1294
        finally:
 
1295
            self.unlock()
 
1296
 
 
1297
 
953
1298
 
954
1299
class ScratchBranch(Branch):
955
1300
    """Special test class: a branch that cleans up after itself.
969
1314
 
970
1315
        If any files are listed, they are created in the working copy.
971
1316
        """
 
1317
        from tempfile import mkdtemp
972
1318
        init = False
973
1319
        if base is None:
974
 
            base = tempfile.mkdtemp()
 
1320
            base = mkdtemp()
975
1321
            init = True
976
1322
        Branch.__init__(self, base, init=init)
977
1323
        for d in dirs:
990
1336
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
991
1337
        True
992
1338
        """
993
 
        base = tempfile.mkdtemp()
 
1339
        from shutil import copytree
 
1340
        from tempfile import mkdtemp
 
1341
        base = mkdtemp()
994
1342
        os.rmdir(base)
995
 
        shutil.copytree(self.base, base, symlinks=True)
 
1343
        copytree(self.base, base, symlinks=True)
996
1344
        return ScratchBranch(base=base)
997
1345
        
998
1346
    def __del__(self):
1000
1348
 
1001
1349
    def destroy(self):
1002
1350
        """Destroy the test branch, removing the scratch directory."""
 
1351
        from shutil import rmtree
1003
1352
        try:
1004
1353
            if self.base:
1005
1354
                mutter("delete ScratchBranch %s" % self.base)
1006
 
                shutil.rmtree(self.base)
 
1355
                rmtree(self.base)
1007
1356
        except OSError, e:
1008
1357
            # Work around for shutil.rmtree failing on Windows when
1009
1358
            # readonly files are encountered
1011
1360
            for root, dirs, files in os.walk(self.base, topdown=False):
1012
1361
                for name in files:
1013
1362
                    os.chmod(os.path.join(root, name), 0700)
1014
 
            shutil.rmtree(self.base)
 
1363
            rmtree(self.base)
1015
1364
        self.base = None
1016
1365
 
1017
1366
    
1042
1391
    cope with just randomness because running uuidgen every time is
1043
1392
    slow."""
1044
1393
    import re
 
1394
    from binascii import hexlify
 
1395
    from time import time
1045
1396
 
1046
1397
    # get last component
1047
1398
    idx = name.rfind('/')
1059
1410
    name = re.sub(r'[^\w.]', '', name)
1060
1411
 
1061
1412
    s = hexlify(rand_bytes(8))
1062
 
    return '-'.join((name, compact_date(time.time()), s))
 
1413
    return '-'.join((name, compact_date(time()), s))
 
1414
 
 
1415
 
 
1416
def gen_root_id():
 
1417
    """Return a new tree-root file id."""
 
1418
    return gen_file_id('TREE_ROOT')
 
1419