~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-09-12 13:48:32 UTC
  • mfrom: (1185.3.4)
  • mto: (1185.1.16)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: abentley@panoramicfeedback.com-20050912134832-c23db11dc63170b6
Merged from mpool

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
 
18
import sys
 
19
import os
19
20
 
20
21
import bzrlib
21
22
from bzrlib.trace import mutter, note
22
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     splitpath, \
23
25
     sha_file, appendpath, file_kind
24
 
from bzrlib.errors import BzrError
 
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.delta import compare_trees
 
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
import bzrlib.xml
 
34
import bzrlib.ui
 
35
 
 
36
 
25
37
 
26
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
39
## TODO: Maybe include checks for common corruption of newlines, etc?
28
40
 
29
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
 
30
49
 
31
50
def find_branch(f, **args):
32
51
    if f and (f.startswith('http://') or f.startswith('https://')):
89
108
    It is not necessary that f exists.
90
109
 
91
110
    Basically we keep looking up until we find the control directory or
92
 
    run into the root."""
 
111
    run into the root.  If there isn't one, raises NotBranchError.
 
112
    """
93
113
    if f == None:
94
114
        f = os.getcwd()
95
115
    elif hasattr(os.path, 'realpath'):
108
128
        head, tail = os.path.split(f)
109
129
        if head == f:
110
130
            # reached the root, whatever that may be
111
 
            raise BzrError('%r is not in a branch' % orig_f)
 
131
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
112
132
        f = head
113
 
    
 
133
 
 
134
 
 
135
 
 
136
# XXX: move into bzrlib.errors; subclass BzrError    
114
137
class DivergedBranches(Exception):
115
138
    def __init__(self, branch1, branch2):
116
139
        self.branch1 = branch1
118
141
        Exception.__init__(self, "These branches have diverged.")
119
142
 
120
143
 
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
 
 
129
144
######################################################################
130
145
# branch objects
131
146
 
204
219
            self._lock.unlock()
205
220
 
206
221
 
207
 
 
208
222
    def lock_write(self):
209
223
        if self._lock_mode:
210
224
            if self._lock_mode != 'w':
220
234
            self._lock_count = 1
221
235
 
222
236
 
223
 
 
224
237
    def lock_read(self):
225
238
        if self._lock_mode:
226
239
            assert self._lock_mode in ('r', 'w'), \
233
246
            self._lock_mode = 'r'
234
247
            self._lock_count = 1
235
248
                        
236
 
 
237
 
            
238
249
    def unlock(self):
239
250
        if not self._lock_mode:
240
251
            from errors import LockError
247
258
            self._lock = None
248
259
            self._lock_mode = self._lock_count = None
249
260
 
250
 
 
251
261
    def abspath(self, name):
252
262
        """Return absolute filename for something in the branch"""
253
263
        return os.path.join(self.base, name)
254
264
 
255
 
 
256
265
    def relpath(self, path):
257
266
        """Return path relative to this branch of something inside it.
258
267
 
259
268
        Raises an error if path is not in this branch."""
260
269
        return _relpath(self.base, path)
261
270
 
262
 
 
263
271
    def controlfilename(self, file_or_path):
264
272
        """Return location relative to branch."""
265
273
        if isinstance(file_or_path, basestring):
292
300
        else:
293
301
            raise BzrError("invalid controlfile mode %r" % mode)
294
302
 
295
 
 
296
 
 
297
303
    def _make_control(self):
298
304
        from bzrlib.inventory import Inventory
299
 
        from bzrlib.xml import pack_xml
300
305
        
301
306
        os.mkdir(self.controlfilename([]))
302
307
        self.controlfile('README', 'w').write(
312
317
            self.controlfile(f, 'w').write('')
313
318
        mutter('created control directory in ' + self.base)
314
319
 
315
 
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
 
320
        # if we want per-tree root ids then this is the place to set
 
321
        # them; they're not needed for now and so ommitted for
 
322
        # simplicity.
 
323
        f = self.controlfile('inventory','w')
 
324
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
316
325
 
317
326
 
318
327
    def _check_format(self):
327
336
        # on Windows from Linux and so on.  I think it might be better
328
337
        # to always make all internal files in unix format.
329
338
        fmt = self.controlfile('branch-format', 'r').read()
330
 
        fmt.replace('\r\n', '')
 
339
        fmt = fmt.replace('\r\n', '\n')
331
340
        if fmt != BZR_BRANCH_FORMAT:
332
341
            raise BzrError('sorry, branch format %r not supported' % fmt,
333
342
                           ['use a different bzr version',
353
362
    def read_working_inventory(self):
354
363
        """Read the working inventory."""
355
364
        from bzrlib.inventory import Inventory
356
 
        from bzrlib.xml import unpack_xml
357
 
        from time import time
358
 
        before = time()
359
365
        self.lock_read()
360
366
        try:
361
367
            # ElementTree does its own conversion from UTF-8, so open in
362
368
            # binary.
363
 
            inv = unpack_xml(Inventory,
364
 
                             self.controlfile('inventory', 'rb'))
365
 
            mutter("loaded inventory of %d items in %f"
366
 
                   % (len(inv), time() - before))
367
 
            return inv
 
369
            f = self.controlfile('inventory', 'rb')
 
370
            return bzrlib.xml.serializer_v4.read_inventory(f)
368
371
        finally:
369
372
            self.unlock()
370
373
            
376
379
        will be committed to the next revision.
377
380
        """
378
381
        from bzrlib.atomicfile import AtomicFile
379
 
        from bzrlib.xml import pack_xml
380
382
        
381
383
        self.lock_write()
382
384
        try:
383
385
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
384
386
            try:
385
 
                pack_xml(inv, f)
 
387
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
386
388
                f.commit()
387
389
            finally:
388
390
                f.close()
396
398
                         """Inventory for the working copy.""")
397
399
 
398
400
 
399
 
    def add(self, files, verbose=False, ids=None):
 
401
    def add(self, files, ids=None):
400
402
        """Make files versioned.
401
403
 
402
 
        Note that the command line normally calls smart_add instead.
 
404
        Note that the command line normally calls smart_add instead,
 
405
        which can automatically recurse.
403
406
 
404
407
        This puts the files in the Added state, so that they will be
405
408
        recorded by the next commit.
415
418
        TODO: Perhaps have an option to add the ids even if the files do
416
419
              not (yet) exist.
417
420
 
418
 
        TODO: Perhaps return the ids of the files?  But then again it
419
 
              is easy to retrieve them if they're needed.
420
 
 
421
 
        TODO: Adding a directory should optionally recurse down and
422
 
              add all non-ignored children.  Perhaps do that in a
423
 
              higher-level method.
 
421
        TODO: Perhaps yield the ids and paths as they're added.
424
422
        """
425
 
        from bzrlib.textui import show_status
426
423
        # TODO: Re-adding a file that is removed in the working copy
427
424
        # should probably put it back with the previous ID.
428
425
        if isinstance(files, basestring):
463
460
                    file_id = gen_file_id(f)
464
461
                inv.add_path(f, kind=kind, file_id=file_id)
465
462
 
466
 
                if verbose:
467
 
                    print 'added', quotefn(f)
468
 
 
469
463
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
470
464
 
471
465
            self._write_inventory(inv)
501
495
        is the opposite of add.  Removing it is consistent with most
502
496
        other tools.  Maybe an option.
503
497
        """
504
 
        from bzrlib.textui import show_status
505
498
        ## TODO: Normalize names
506
499
        ## TODO: Remove nested loops; better scalability
507
500
        if isinstance(files, basestring):
582
575
            f.close()
583
576
 
584
577
 
 
578
    def get_revision_xml_file(self, revision_id):
 
579
        """Return XML file object for revision object."""
 
580
        if not revision_id or not isinstance(revision_id, basestring):
 
581
            raise InvalidRevisionId(revision_id)
 
582
 
 
583
        self.lock_read()
 
584
        try:
 
585
            try:
 
586
                return self.revision_store[revision_id]
 
587
            except IndexError:
 
588
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
589
        finally:
 
590
            self.unlock()
 
591
 
 
592
 
 
593
    #deprecated
 
594
    get_revision_xml = get_revision_xml_file
 
595
 
 
596
 
585
597
    def get_revision(self, revision_id):
586
598
        """Return the Revision object for a named revision"""
587
 
        from bzrlib.revision import Revision
588
 
        from bzrlib.xml import unpack_xml
 
599
        xml_file = self.get_revision_xml_file(revision_id)
589
600
 
590
 
        self.lock_read()
591
601
        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()
 
602
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
603
        except SyntaxError, e:
 
604
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
605
                                         [revision_id,
 
606
                                          str(e)])
597
607
            
598
608
        assert r.revision_id == revision_id
599
609
        return r
 
610
 
 
611
 
 
612
    def get_revision_delta(self, revno):
 
613
        """Return the delta for one revision.
 
614
 
 
615
        The delta is relative to its mainline predecessor, or the
 
616
        empty tree for revision 1.
 
617
        """
 
618
        assert isinstance(revno, int)
 
619
        rh = self.revision_history()
 
620
        if not (1 <= revno <= len(rh)):
 
621
            raise InvalidRevisionNumber(revno)
 
622
 
 
623
        # revno is 1-based; list is 0-based
 
624
 
 
625
        new_tree = self.revision_tree(rh[revno-1])
 
626
        if revno == 1:
 
627
            old_tree = EmptyTree()
 
628
        else:
 
629
            old_tree = self.revision_tree(rh[revno-2])
 
630
 
 
631
        return compare_trees(old_tree, new_tree)
 
632
 
600
633
        
601
634
 
602
635
    def get_revision_sha1(self, revision_id):
607
640
        # the revision, (add signatures/remove signatures) and still
608
641
        # have all hash pointers stay consistent.
609
642
        # But for now, just hash the contents.
610
 
        return sha_file(self.revision_store[revision_id])
 
643
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
611
644
 
612
645
 
613
646
    def get_inventory(self, inventory_id):
617
650
               parameter which can be either an integer revno or a
618
651
               string hash."""
619
652
        from bzrlib.inventory import Inventory
620
 
        from bzrlib.xml import unpack_xml
621
 
 
622
 
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
653
 
 
654
        f = self.get_inventory_xml_file(inventory_id)
 
655
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
656
 
 
657
 
 
658
    def get_inventory_xml(self, inventory_id):
 
659
        """Get inventory XML as a file object."""
 
660
        return self.inventory_store[inventory_id]
 
661
 
 
662
    get_inventory_xml_file = get_inventory_xml
623
663
            
624
664
 
625
665
    def get_inventory_sha1(self, inventory_id):
626
666
        """Return the sha1 hash of the inventory entry
627
667
        """
628
 
        return sha_file(self.inventory_store[inventory_id])
 
668
        return sha_file(self.get_inventory_xml(inventory_id))
629
669
 
630
670
 
631
671
    def get_revision_inventory(self, revision_id):
697
737
                return r+1, my_history[r]
698
738
        return None, None
699
739
 
700
 
    def enum_history(self, direction):
701
 
        """Return (revno, revision_id) for history of branch.
702
 
 
703
 
        direction
704
 
            'forward' is from earliest to latest
705
 
            'reverse' is from latest to earliest
706
 
        """
707
 
        rh = self.revision_history()
708
 
        if direction == 'forward':
709
 
            i = 1
710
 
            for rid in rh:
711
 
                yield i, rid
712
 
                i += 1
713
 
        elif direction == 'reverse':
714
 
            i = len(rh)
715
 
            while i > 0:
716
 
                yield i, rh[i-1]
717
 
                i -= 1
718
 
        else:
719
 
            raise ValueError('invalid history direction', direction)
720
 
 
721
740
 
722
741
    def revno(self):
723
742
        """Return current revision number for this branch.
738
757
            return None
739
758
 
740
759
 
741
 
    def missing_revisions(self, other, stop_revision=None):
 
760
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
742
761
        """
743
762
        If self and other have not diverged, return a list of the revisions
744
763
        present in other, but missing from self.
777
796
        if stop_revision is None:
778
797
            stop_revision = other_len
779
798
        elif stop_revision > other_len:
780
 
            raise NoSuchRevision(self, stop_revision)
 
799
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
781
800
        
782
801
        return other_history[self_len:stop_revision]
783
802
 
784
803
 
785
804
    def update_revisions(self, other, stop_revision=None):
786
805
        """Pull in all new revisions from other branch.
787
 
        
788
 
        >>> from bzrlib.commit import commit
789
 
        >>> bzrlib.trace.silent = True
790
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
791
 
        >>> br1.add('foo')
792
 
        >>> br1.add('bar')
793
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
794
 
        >>> br2 = ScratchBranch()
795
 
        >>> br2.update_revisions(br1)
796
 
        Added 2 texts.
797
 
        Added 1 inventories.
798
 
        Added 1 revisions.
799
 
        >>> br2.revision_history()
800
 
        [u'REVISION-ID-1']
801
 
        >>> br2.update_revisions(br1)
802
 
        Added 0 texts.
803
 
        Added 0 inventories.
804
 
        Added 0 revisions.
805
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
806
 
        True
807
806
        """
808
 
        from bzrlib.progress import ProgressBar
809
 
 
810
 
        pb = ProgressBar()
811
 
 
 
807
        from bzrlib.fetch import greedy_fetch
 
808
        from bzrlib.revision import get_intervening_revisions
 
809
 
 
810
        pb = bzrlib.ui.ui_factory.progress_bar()
812
811
        pb.update('comparing histories')
813
 
        revision_ids = self.missing_revisions(other, stop_revision)
814
 
 
 
812
        if stop_revision is None:
 
813
            other_revision = other.last_patch()
 
814
        else:
 
815
            other_revision = other.lookup_revision(stop_revision)
 
816
        count = greedy_fetch(self, other, other_revision, pb)[0]
 
817
        try:
 
818
            revision_ids = self.missing_revisions(other, stop_revision)
 
819
        except DivergedBranches, e:
 
820
            try:
 
821
                revision_ids = get_intervening_revisions(self.last_patch(), 
 
822
                                                         other_revision, self)
 
823
                assert self.last_patch() not in revision_ids
 
824
            except bzrlib.errors.NotAncestor:
 
825
                raise e
 
826
 
 
827
        self.append_revision(*revision_ids)
 
828
        pb.clear()
 
829
 
 
830
    def install_revisions(self, other, revision_ids, pb):
815
831
        if hasattr(other.revision_store, "prefetch"):
816
832
            other.revision_store.prefetch(revision_ids)
817
833
        if hasattr(other.inventory_store, "prefetch"):
818
834
            inventory_ids = [other.get_revision(r).inventory_id
819
835
                             for r in revision_ids]
820
836
            other.inventory_store.prefetch(inventory_ids)
 
837
 
 
838
        if pb is None:
 
839
            pb = bzrlib.ui.ui_factory.progress_bar()
821
840
                
822
841
        revisions = []
823
842
        needed_texts = set()
824
843
        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)
 
844
 
 
845
        failures = set()
 
846
        for i, rev_id in enumerate(revision_ids):
 
847
            pb.update('fetching revision', i+1, len(revision_ids))
 
848
            try:
 
849
                rev = other.get_revision(rev_id)
 
850
            except bzrlib.errors.NoSuchRevision:
 
851
                failures.add(rev_id)
 
852
                continue
 
853
 
829
854
            revisions.append(rev)
830
855
            inv = other.get_inventory(str(rev.inventory_id))
831
856
            for key, entry in inv.iter_entries():
836
861
 
837
862
        pb.clear()
838
863
                    
839
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
840
 
        print "Added %d texts." % count 
 
864
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
865
                                                    needed_texts)
 
866
        #print "Added %d texts." % count 
841
867
        inventory_ids = [ f.inventory_id for f in revisions ]
842
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
843
 
                                                inventory_ids)
844
 
        print "Added %d inventories." % count 
 
868
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
869
                                                         inventory_ids)
 
870
        #print "Added %d inventories." % count 
845
871
        revision_ids = [ f.revision_id for f in revisions]
846
 
        count = self.revision_store.copy_multi(other.revision_store, 
847
 
                                               revision_ids)
848
 
        for revision_id in revision_ids:
849
 
            self.append_revision(revision_id)
850
 
        print "Added %d revisions." % count
851
 
                    
852
 
        
 
872
 
 
873
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
874
                                                          revision_ids,
 
875
                                                          permit_failure=True)
 
876
        assert len(cp_fail) == 0 
 
877
        return count, failures
 
878
       
 
879
 
853
880
    def commit(self, *args, **kw):
854
881
        from bzrlib.commit import commit
855
882
        commit(self, *args, **kw)
857
884
 
858
885
    def lookup_revision(self, revision):
859
886
        """Return the revision identifier for a given revision information."""
860
 
        revno, info = self.get_revision_info(revision)
 
887
        revno, info = self._get_revision_info(revision)
861
888
        return info
862
889
 
 
890
 
 
891
    def revision_id_to_revno(self, revision_id):
 
892
        """Given a revision id, return its revno"""
 
893
        history = self.revision_history()
 
894
        try:
 
895
            return history.index(revision_id) + 1
 
896
        except ValueError:
 
897
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
898
 
 
899
 
863
900
    def get_revision_info(self, revision):
864
901
        """Return (revno, revision id) for revision identifier.
865
902
 
868
905
        revision can also be a string, in which case it is parsed for something like
869
906
            'date:' or 'revid:' etc.
870
907
        """
 
908
        revno, rev_id = self._get_revision_info(revision)
 
909
        if revno is None:
 
910
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
911
        return revno, rev_id
 
912
 
 
913
    def get_rev_id(self, revno, history=None):
 
914
        """Find the revision id of the specified revno."""
 
915
        if revno == 0:
 
916
            return None
 
917
        if history is None:
 
918
            history = self.revision_history()
 
919
        elif revno <= 0 or revno > len(history):
 
920
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
921
        return history[revno - 1]
 
922
 
 
923
    def _get_revision_info(self, revision):
 
924
        """Return (revno, revision id) for revision specifier.
 
925
 
 
926
        revision can be an integer, in which case it is assumed to be revno
 
927
        (though this will translate negative values into positive ones)
 
928
        revision can also be a string, in which case it is parsed for something
 
929
        like 'date:' or 'revid:' etc.
 
930
 
 
931
        A revid is always returned.  If it is None, the specifier referred to
 
932
        the null revision.  If the revid does not occur in the revision
 
933
        history, revno will be None.
 
934
        """
 
935
        
871
936
        if revision is None:
872
937
            return 0, None
873
938
        revno = None
877
942
            pass
878
943
        revs = self.revision_history()
879
944
        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
945
            if revision < 0:
884
946
                revno = len(revs) + revision + 1
885
947
            else:
886
948
                revno = revision
 
949
            rev_id = self.get_rev_id(revno, revs)
887
950
        elif isinstance(revision, basestring):
888
951
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
889
952
                if revision.startswith(prefix):
890
 
                    revno = func(self, revs, revision)
 
953
                    result = func(self, revs, revision)
 
954
                    if len(result) > 1:
 
955
                        revno, rev_id = result
 
956
                    else:
 
957
                        revno = result[0]
 
958
                        rev_id = self.get_rev_id(revno, revs)
891
959
                    break
892
960
            else:
893
 
                raise BzrError('No namespace registered for string: %r' % revision)
 
961
                raise BzrError('No namespace registered for string: %r' %
 
962
                               revision)
 
963
        else:
 
964
            raise TypeError('Unhandled revision type %s' % revision)
894
965
 
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]
 
966
        if revno is None:
 
967
            if rev_id is None:
 
968
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
969
        return revno, rev_id
898
970
 
899
971
    def _namespace_revno(self, revs, revision):
900
972
        """Lookup a revision by revision number"""
901
973
        assert revision.startswith('revno:')
902
974
        try:
903
 
            return int(revision[6:])
 
975
            return (int(revision[6:]),)
904
976
        except ValueError:
905
977
            return None
906
978
    REVISION_NAMESPACES['revno:'] = _namespace_revno
907
979
 
908
980
    def _namespace_revid(self, revs, revision):
909
981
        assert revision.startswith('revid:')
 
982
        rev_id = revision[len('revid:'):]
910
983
        try:
911
 
            return revs.index(revision[6:]) + 1
 
984
            return revs.index(rev_id) + 1, rev_id
912
985
        except ValueError:
913
 
            return None
 
986
            return None, rev_id
914
987
    REVISION_NAMESPACES['revid:'] = _namespace_revid
915
988
 
916
989
    def _namespace_last(self, revs, revision):
918
991
        try:
919
992
            offset = int(revision[5:])
920
993
        except ValueError:
921
 
            return None
 
994
            return (None,)
922
995
        else:
923
996
            if offset <= 0:
924
997
                raise BzrError('You must supply a positive value for --revision last:XXX')
925
 
            return len(revs) - offset + 1
 
998
            return (len(revs) - offset + 1,)
926
999
    REVISION_NAMESPACES['last:'] = _namespace_last
927
1000
 
928
1001
    def _namespace_tag(self, revs, revision):
1003
1076
                # TODO: Handle timezone.
1004
1077
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1005
1078
                if first >= dt and (last is None or dt >= last):
1006
 
                    return i+1
 
1079
                    return (i+1,)
1007
1080
        else:
1008
1081
            for i in range(len(revs)):
1009
1082
                r = self.get_revision(revs[i])
1010
1083
                # TODO: Handle timezone.
1011
1084
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1012
1085
                if first <= dt and (last is None or dt <= last):
1013
 
                    return i+1
 
1086
                    return (i+1,)
1014
1087
    REVISION_NAMESPACES['date:'] = _namespace_date
1015
1088
 
1016
1089
    def revision_tree(self, revision_id):
1018
1091
 
1019
1092
        `revision_id` may be None for the null revision, in which case
1020
1093
        an `EmptyTree` is returned."""
1021
 
        from bzrlib.tree import EmptyTree, RevisionTree
1022
1094
        # TODO: refactor this to use an existing revision object
1023
1095
        # so we don't need to read it in twice.
1024
1096
        if revision_id == None:
1025
 
            return EmptyTree(self.get_root_id())
 
1097
            return EmptyTree()
1026
1098
        else:
1027
1099
            inv = self.get_revision_inventory(revision_id)
1028
1100
            return RevisionTree(self.text_store, inv)
1039
1111
 
1040
1112
        If there are no revisions yet, return an `EmptyTree`.
1041
1113
        """
1042
 
        from bzrlib.tree import EmptyTree, RevisionTree
1043
1114
        r = self.last_patch()
1044
1115
        if r == None:
1045
 
            return EmptyTree(self.get_root_id())
 
1116
            return EmptyTree()
1046
1117
        else:
1047
1118
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1048
1119
 
1083
1154
 
1084
1155
            inv.rename(file_id, to_dir_id, to_tail)
1085
1156
 
1086
 
            print "%s => %s" % (from_rel, to_rel)
1087
 
 
1088
1157
            from_abs = self.abspath(from_rel)
1089
1158
            to_abs = self.abspath(to_rel)
1090
1159
            try:
1109
1178
 
1110
1179
        Note that to_name is only the last component of the new name;
1111
1180
        this doesn't change the directory.
 
1181
 
 
1182
        This returns a list of (from_path, to_path) pairs for each
 
1183
        entry that is moved.
1112
1184
        """
 
1185
        result = []
1113
1186
        self.lock_write()
1114
1187
        try:
1115
1188
            ## TODO: Option to move IDs only
1150
1223
            for f in from_paths:
1151
1224
                name_tail = splitpath(f)[-1]
1152
1225
                dest_path = appendpath(to_name, name_tail)
1153
 
                print "%s => %s" % (f, dest_path)
 
1226
                result.append((f, dest_path))
1154
1227
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1155
1228
                try:
1156
1229
                    os.rename(self.abspath(f), self.abspath(dest_path))
1162
1235
        finally:
1163
1236
            self.unlock()
1164
1237
 
 
1238
        return result
 
1239
 
1165
1240
 
1166
1241
    def revert(self, filenames, old_tree=None, backups=True):
1167
1242
        """Restore selected files to the versions from a previous tree.
1249
1324
            self.unlock()
1250
1325
 
1251
1326
 
 
1327
    def get_parent(self):
 
1328
        """Return the parent location of the branch.
 
1329
 
 
1330
        This is the default location for push/pull/missing.  The usual
 
1331
        pattern is that the user can override it by specifying a
 
1332
        location.
 
1333
        """
 
1334
        import errno
 
1335
        _locs = ['parent', 'pull', 'x-pull']
 
1336
        for l in _locs:
 
1337
            try:
 
1338
                return self.controlfile(l, 'r').read().strip('\n')
 
1339
            except IOError, e:
 
1340
                if e.errno != errno.ENOENT:
 
1341
                    raise
 
1342
        return None
 
1343
 
 
1344
 
 
1345
    def set_parent(self, url):
 
1346
        # TODO: Maybe delete old location files?
 
1347
        from bzrlib.atomicfile import AtomicFile
 
1348
        self.lock_write()
 
1349
        try:
 
1350
            f = AtomicFile(self.controlfilename('parent'))
 
1351
            try:
 
1352
                f.write(url + '\n')
 
1353
                f.commit()
 
1354
            finally:
 
1355
                f.close()
 
1356
        finally:
 
1357
            self.unlock()
 
1358
 
 
1359
    def check_revno(self, revno):
 
1360
        """\
 
1361
        Check whether a revno corresponds to any revision.
 
1362
        Zero (the NULL revision) is considered valid.
 
1363
        """
 
1364
        if revno != 0:
 
1365
            self.check_real_revno(revno)
 
1366
            
 
1367
    def check_real_revno(self, revno):
 
1368
        """\
 
1369
        Check whether a revno corresponds to a real revision.
 
1370
        Zero (the NULL revision) is considered invalid
 
1371
        """
 
1372
        if revno < 1 or revno > self.revno():
 
1373
            raise InvalidRevisionNumber(revno)
 
1374
        
 
1375
        
 
1376
 
1252
1377
 
1253
1378
class ScratchBranch(Branch):
1254
1379
    """Special test class: a branch that cleans up after itself.
1296
1421
        os.rmdir(base)
1297
1422
        copytree(self.base, base, symlinks=True)
1298
1423
        return ScratchBranch(base=base)
 
1424
 
 
1425
 
1299
1426
        
1300
1427
    def __del__(self):
1301
1428
        self.destroy()
1371
1498
    """Return a new tree-root file id."""
1372
1499
    return gen_file_id('TREE_ROOT')
1373
1500
 
 
1501
 
 
1502
def pull_loc(branch):
 
1503
    # TODO: Should perhaps just make attribute be 'base' in
 
1504
    # RemoteBranch and Branch?
 
1505
    if hasattr(branch, "baseurl"):
 
1506
        return branch.baseurl
 
1507
    else:
 
1508
        return branch.base
 
1509
 
 
1510
 
 
1511
def copy_branch(branch_from, to_location, revision=None):
 
1512
    """Copy branch_from into the existing directory to_location.
 
1513
 
 
1514
    revision
 
1515
        If not None, only revisions up to this point will be copied.
 
1516
        The head of the new branch will be that revision.
 
1517
 
 
1518
    to_location
 
1519
        The name of a local directory that exists but is empty.
 
1520
    """
 
1521
    from bzrlib.merge import merge
 
1522
    from bzrlib.branch import Branch
 
1523
 
 
1524
    assert isinstance(branch_from, Branch)
 
1525
    assert isinstance(to_location, basestring)
 
1526
    
 
1527
    br_to = Branch(to_location, init=True)
 
1528
    br_to.set_root_id(branch_from.get_root_id())
 
1529
    if revision is None:
 
1530
        revno = branch_from.revno()
 
1531
    else:
 
1532
        revno, rev_id = branch_from.get_revision_info(revision)
 
1533
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1534
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1535
          check_clean=False, ignore_zero=True)
 
1536
    
 
1537
    from_location = pull_loc(branch_from)
 
1538
    br_to.set_parent(pull_loc(branch_from))
 
1539