~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

- merge improved merge base selection from aaron
aaron.bentley@utoronto.ca-20050912025534-43d7275dd948e4ad

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
 
 
813
        try:
 
814
            revision_ids = self.missing_revisions(other, stop_revision)
 
815
        except DivergedBranches, e:
 
816
            try:
 
817
                if stop_revision is None:
 
818
                    end_revision = other.last_patch()
 
819
                revision_ids = get_intervening_revisions(self.last_patch(), 
 
820
                                                         end_revision, other)
 
821
                assert self.last_patch() not in revision_ids
 
822
            except bzrlib.errors.NotAncestor:
 
823
                raise e
 
824
 
 
825
        if len(revision_ids) > 0:
 
826
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
827
        else:
 
828
            count = 0
 
829
        self.append_revision(*revision_ids)
 
830
        ## note("Added %d revisions." % count)
 
831
        pb.clear()
 
832
 
 
833
    def install_revisions(self, other, revision_ids, pb):
815
834
        if hasattr(other.revision_store, "prefetch"):
816
835
            other.revision_store.prefetch(revision_ids)
817
836
        if hasattr(other.inventory_store, "prefetch"):
818
837
            inventory_ids = [other.get_revision(r).inventory_id
819
838
                             for r in revision_ids]
820
839
            other.inventory_store.prefetch(inventory_ids)
 
840
 
 
841
        if pb is None:
 
842
            pb = bzrlib.ui.ui_factory.progress_bar()
821
843
                
822
844
        revisions = []
823
845
        needed_texts = set()
824
846
        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)
 
847
 
 
848
        failures = set()
 
849
        for i, rev_id in enumerate(revision_ids):
 
850
            pb.update('fetching revision', i+1, len(revision_ids))
 
851
            try:
 
852
                rev = other.get_revision(rev_id)
 
853
            except bzrlib.errors.NoSuchRevision:
 
854
                failures.add(rev_id)
 
855
                continue
 
856
 
829
857
            revisions.append(rev)
830
858
            inv = other.get_inventory(str(rev.inventory_id))
831
859
            for key, entry in inv.iter_entries():
836
864
 
837
865
        pb.clear()
838
866
                    
839
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
840
 
        print "Added %d texts." % count 
 
867
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
868
                                                    needed_texts)
 
869
        #print "Added %d texts." % count 
841
870
        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 
 
871
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
872
                                                         inventory_ids)
 
873
        #print "Added %d inventories." % count 
845
874
        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
 
        
 
875
 
 
876
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
877
                                                          revision_ids,
 
878
                                                          permit_failure=True)
 
879
        assert len(cp_fail) == 0 
 
880
        return count, failures
 
881
       
 
882
 
853
883
    def commit(self, *args, **kw):
854
884
        from bzrlib.commit import commit
855
885
        commit(self, *args, **kw)
857
887
 
858
888
    def lookup_revision(self, revision):
859
889
        """Return the revision identifier for a given revision information."""
860
 
        revno, info = self.get_revision_info(revision)
 
890
        revno, info = self._get_revision_info(revision)
861
891
        return info
862
892
 
 
893
 
 
894
    def revision_id_to_revno(self, revision_id):
 
895
        """Given a revision id, return its revno"""
 
896
        history = self.revision_history()
 
897
        try:
 
898
            return history.index(revision_id) + 1
 
899
        except ValueError:
 
900
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
901
 
 
902
 
863
903
    def get_revision_info(self, revision):
864
904
        """Return (revno, revision id) for revision identifier.
865
905
 
868
908
        revision can also be a string, in which case it is parsed for something like
869
909
            'date:' or 'revid:' etc.
870
910
        """
 
911
        revno, rev_id = self._get_revision_info(revision)
 
912
        if revno is None:
 
913
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
914
        return revno, rev_id
 
915
 
 
916
    def get_rev_id(self, revno, history=None):
 
917
        """Find the revision id of the specified revno."""
 
918
        if revno == 0:
 
919
            return None
 
920
        if history is None:
 
921
            history = self.revision_history()
 
922
        elif revno <= 0 or revno > len(history):
 
923
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
924
        return history[revno - 1]
 
925
 
 
926
    def _get_revision_info(self, revision):
 
927
        """Return (revno, revision id) for revision specifier.
 
928
 
 
929
        revision can be an integer, in which case it is assumed to be revno
 
930
        (though this will translate negative values into positive ones)
 
931
        revision can also be a string, in which case it is parsed for something
 
932
        like 'date:' or 'revid:' etc.
 
933
 
 
934
        A revid is always returned.  If it is None, the specifier referred to
 
935
        the null revision.  If the revid does not occur in the revision
 
936
        history, revno will be None.
 
937
        """
 
938
        
871
939
        if revision is None:
872
940
            return 0, None
873
941
        revno = None
877
945
            pass
878
946
        revs = self.revision_history()
879
947
        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
948
            if revision < 0:
884
949
                revno = len(revs) + revision + 1
885
950
            else:
886
951
                revno = revision
 
952
            rev_id = self.get_rev_id(revno, revs)
887
953
        elif isinstance(revision, basestring):
888
954
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
889
955
                if revision.startswith(prefix):
890
 
                    revno = func(self, revs, revision)
 
956
                    result = func(self, revs, revision)
 
957
                    if len(result) > 1:
 
958
                        revno, rev_id = result
 
959
                    else:
 
960
                        revno = result[0]
 
961
                        rev_id = self.get_rev_id(revno, revs)
891
962
                    break
892
963
            else:
893
 
                raise BzrError('No namespace registered for string: %r' % revision)
 
964
                raise BzrError('No namespace registered for string: %r' %
 
965
                               revision)
 
966
        else:
 
967
            raise TypeError('Unhandled revision type %s' % revision)
894
968
 
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]
 
969
        if revno is None:
 
970
            if rev_id is None:
 
971
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
972
        return revno, rev_id
898
973
 
899
974
    def _namespace_revno(self, revs, revision):
900
975
        """Lookup a revision by revision number"""
901
976
        assert revision.startswith('revno:')
902
977
        try:
903
 
            return int(revision[6:])
 
978
            return (int(revision[6:]),)
904
979
        except ValueError:
905
980
            return None
906
981
    REVISION_NAMESPACES['revno:'] = _namespace_revno
907
982
 
908
983
    def _namespace_revid(self, revs, revision):
909
984
        assert revision.startswith('revid:')
 
985
        rev_id = revision[len('revid:'):]
910
986
        try:
911
 
            return revs.index(revision[6:]) + 1
 
987
            return revs.index(rev_id) + 1, rev_id
912
988
        except ValueError:
913
 
            return None
 
989
            return None, rev_id
914
990
    REVISION_NAMESPACES['revid:'] = _namespace_revid
915
991
 
916
992
    def _namespace_last(self, revs, revision):
918
994
        try:
919
995
            offset = int(revision[5:])
920
996
        except ValueError:
921
 
            return None
 
997
            return (None,)
922
998
        else:
923
999
            if offset <= 0:
924
1000
                raise BzrError('You must supply a positive value for --revision last:XXX')
925
 
            return len(revs) - offset + 1
 
1001
            return (len(revs) - offset + 1,)
926
1002
    REVISION_NAMESPACES['last:'] = _namespace_last
927
1003
 
928
1004
    def _namespace_tag(self, revs, revision):
1003
1079
                # TODO: Handle timezone.
1004
1080
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1005
1081
                if first >= dt and (last is None or dt >= last):
1006
 
                    return i+1
 
1082
                    return (i+1,)
1007
1083
        else:
1008
1084
            for i in range(len(revs)):
1009
1085
                r = self.get_revision(revs[i])
1010
1086
                # TODO: Handle timezone.
1011
1087
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1012
1088
                if first <= dt and (last is None or dt <= last):
1013
 
                    return i+1
 
1089
                    return (i+1,)
1014
1090
    REVISION_NAMESPACES['date:'] = _namespace_date
1015
1091
 
1016
1092
    def revision_tree(self, revision_id):
1018
1094
 
1019
1095
        `revision_id` may be None for the null revision, in which case
1020
1096
        an `EmptyTree` is returned."""
1021
 
        from bzrlib.tree import EmptyTree, RevisionTree
1022
1097
        # TODO: refactor this to use an existing revision object
1023
1098
        # so we don't need to read it in twice.
1024
1099
        if revision_id == None:
1025
 
            return EmptyTree(self.get_root_id())
 
1100
            return EmptyTree()
1026
1101
        else:
1027
1102
            inv = self.get_revision_inventory(revision_id)
1028
1103
            return RevisionTree(self.text_store, inv)
1039
1114
 
1040
1115
        If there are no revisions yet, return an `EmptyTree`.
1041
1116
        """
1042
 
        from bzrlib.tree import EmptyTree, RevisionTree
1043
1117
        r = self.last_patch()
1044
1118
        if r == None:
1045
 
            return EmptyTree(self.get_root_id())
 
1119
            return EmptyTree()
1046
1120
        else:
1047
1121
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1048
1122
 
1083
1157
 
1084
1158
            inv.rename(file_id, to_dir_id, to_tail)
1085
1159
 
1086
 
            print "%s => %s" % (from_rel, to_rel)
1087
 
 
1088
1160
            from_abs = self.abspath(from_rel)
1089
1161
            to_abs = self.abspath(to_rel)
1090
1162
            try:
1109
1181
 
1110
1182
        Note that to_name is only the last component of the new name;
1111
1183
        this doesn't change the directory.
 
1184
 
 
1185
        This returns a list of (from_path, to_path) pairs for each
 
1186
        entry that is moved.
1112
1187
        """
 
1188
        result = []
1113
1189
        self.lock_write()
1114
1190
        try:
1115
1191
            ## TODO: Option to move IDs only
1150
1226
            for f in from_paths:
1151
1227
                name_tail = splitpath(f)[-1]
1152
1228
                dest_path = appendpath(to_name, name_tail)
1153
 
                print "%s => %s" % (f, dest_path)
 
1229
                result.append((f, dest_path))
1154
1230
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1155
1231
                try:
1156
1232
                    os.rename(self.abspath(f), self.abspath(dest_path))
1162
1238
        finally:
1163
1239
            self.unlock()
1164
1240
 
 
1241
        return result
 
1242
 
1165
1243
 
1166
1244
    def revert(self, filenames, old_tree=None, backups=True):
1167
1245
        """Restore selected files to the versions from a previous tree.
1249
1327
            self.unlock()
1250
1328
 
1251
1329
 
 
1330
    def get_parent(self):
 
1331
        """Return the parent location of the branch.
 
1332
 
 
1333
        This is the default location for push/pull/missing.  The usual
 
1334
        pattern is that the user can override it by specifying a
 
1335
        location.
 
1336
        """
 
1337
        import errno
 
1338
        _locs = ['parent', 'pull', 'x-pull']
 
1339
        for l in _locs:
 
1340
            try:
 
1341
                return self.controlfile(l, 'r').read().strip('\n')
 
1342
            except IOError, e:
 
1343
                if e.errno != errno.ENOENT:
 
1344
                    raise
 
1345
        return None
 
1346
 
 
1347
 
 
1348
    def set_parent(self, url):
 
1349
        # TODO: Maybe delete old location files?
 
1350
        from bzrlib.atomicfile import AtomicFile
 
1351
        self.lock_write()
 
1352
        try:
 
1353
            f = AtomicFile(self.controlfilename('parent'))
 
1354
            try:
 
1355
                f.write(url + '\n')
 
1356
                f.commit()
 
1357
            finally:
 
1358
                f.close()
 
1359
        finally:
 
1360
            self.unlock()
 
1361
 
 
1362
    def check_revno(self, revno):
 
1363
        """\
 
1364
        Check whether a revno corresponds to any revision.
 
1365
        Zero (the NULL revision) is considered valid.
 
1366
        """
 
1367
        if revno != 0:
 
1368
            self.check_real_revno(revno)
 
1369
            
 
1370
    def check_real_revno(self, revno):
 
1371
        """\
 
1372
        Check whether a revno corresponds to a real revision.
 
1373
        Zero (the NULL revision) is considered invalid
 
1374
        """
 
1375
        if revno < 1 or revno > self.revno():
 
1376
            raise InvalidRevisionNumber(revno)
 
1377
        
 
1378
        
 
1379
 
1252
1380
 
1253
1381
class ScratchBranch(Branch):
1254
1382
    """Special test class: a branch that cleans up after itself.
1296
1424
        os.rmdir(base)
1297
1425
        copytree(self.base, base, symlinks=True)
1298
1426
        return ScratchBranch(base=base)
 
1427
 
 
1428
 
1299
1429
        
1300
1430
    def __del__(self):
1301
1431
        self.destroy()
1371
1501
    """Return a new tree-root file id."""
1372
1502
    return gen_file_id('TREE_ROOT')
1373
1503
 
 
1504
 
 
1505
def pull_loc(branch):
 
1506
    # TODO: Should perhaps just make attribute be 'base' in
 
1507
    # RemoteBranch and Branch?
 
1508
    if hasattr(branch, "baseurl"):
 
1509
        return branch.baseurl
 
1510
    else:
 
1511
        return branch.base
 
1512
 
 
1513
 
 
1514
def copy_branch(branch_from, to_location, revision=None):
 
1515
    """Copy branch_from into the existing directory to_location.
 
1516
 
 
1517
    revision
 
1518
        If not None, only revisions up to this point will be copied.
 
1519
        The head of the new branch will be that revision.
 
1520
 
 
1521
    to_location
 
1522
        The name of a local directory that exists but is empty.
 
1523
    """
 
1524
    from bzrlib.merge import merge
 
1525
    from bzrlib.branch import Branch
 
1526
 
 
1527
    assert isinstance(branch_from, Branch)
 
1528
    assert isinstance(to_location, basestring)
 
1529
    
 
1530
    br_to = Branch(to_location, init=True)
 
1531
    br_to.set_root_id(branch_from.get_root_id())
 
1532
    if revision is None:
 
1533
        revno = branch_from.revno()
 
1534
    else:
 
1535
        revno, rev_id = branch_from.get_revision_info(revision)
 
1536
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1537
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1538
          check_clean=False, ignore_zero=True)
 
1539
    
 
1540
    from_location = pull_loc(branch_from)
 
1541
    br_to.set_parent(pull_loc(branch_from))
 
1542