~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-09-19 02:52:24 UTC
  • mto: (1185.1.29)
  • mto: This revision was merged to the branch mainline in revision 1390.
  • Revision ID: aaron.bentley@utoronto.ca-20050919025224-1cc3c70640086e09
TODO re tests

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
     splitpath, \
25
25
     sha_file, appendpath, file_kind
26
26
 
27
 
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
28
 
import bzrlib.errors
 
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
 
28
     DivergedBranches, NotBranchError
29
29
from bzrlib.textui import show_status
30
30
from bzrlib.revision import Revision
31
 
from bzrlib.xml import unpack_xml
32
31
from bzrlib.delta import compare_trees
33
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
import bzrlib.xml
34
34
import bzrlib.ui
35
35
 
36
36
 
43
43
# repeatedly to calculate deltas.  We could perhaps have a weakref
44
44
# cache in memory to make this faster.
45
45
 
46
 
# TODO: please move the revision-string syntax stuff out of the branch
47
 
# object; it's clutter
48
 
 
49
 
 
50
 
def find_branch(f, **args):
51
 
    if f and (f.startswith('http://') or f.startswith('https://')):
52
 
        import remotebranch 
53
 
        return remotebranch.RemoteBranch(f, **args)
54
 
    else:
55
 
        return Branch(f, **args)
56
 
 
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
 
 
 
46
def find_branch(*ignored, **ignored_too):
 
47
    # XXX: leave this here for about one release, then remove it
 
48
    raise NotImplementedError('find_branch() is not supported anymore, '
 
49
                              'please use one of the new branch constructors')
74
50
 
75
51
def _relpath(base, path):
76
52
    """Return path relative to base, or raise exception.
94
70
        if tail:
95
71
            s.insert(0, tail)
96
72
    else:
97
 
        from errors import NotBranchError
98
73
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
99
74
 
100
75
    return os.sep.join(s)
128
103
        head, tail = os.path.split(f)
129
104
        if head == f:
130
105
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
106
            raise NotBranchError('%s is not in a branch' % orig_f)
132
107
        f = head
133
108
 
134
109
 
135
110
 
136
 
# XXX: move into bzrlib.errors; subclass BzrError    
137
 
class DivergedBranches(Exception):
138
 
    def __init__(self, branch1, branch2):
139
 
        self.branch1 = branch1
140
 
        self.branch2 = branch2
141
 
        Exception.__init__(self, "These branches have diverged.")
142
 
 
143
111
 
144
112
######################################################################
145
113
# branch objects
148
116
    """Branch holding a history of revisions.
149
117
 
150
118
    base
151
 
        Base directory of the branch.
 
119
        Base directory/url of the branch.
 
120
    """
 
121
    base = None
 
122
 
 
123
    def __init__(self, *ignored, **ignored_too):
 
124
        raise NotImplementedError('The Branch class is abstract')
 
125
 
 
126
    @staticmethod
 
127
    def open(base):
 
128
        """Open an existing branch, rooted at 'base' (url)"""
 
129
        if base and (base.startswith('http://') or base.startswith('https://')):
 
130
            from bzrlib.remotebranch import RemoteBranch
 
131
            return RemoteBranch(base, find_root=False)
 
132
        else:
 
133
            return LocalBranch(base, find_root=False)
 
134
 
 
135
    @staticmethod
 
136
    def open_containing(url):
 
137
        """Open an existing branch, containing url (search upwards for the root)
 
138
        """
 
139
        if url and (url.startswith('http://') or url.startswith('https://')):
 
140
            from bzrlib.remotebranch import RemoteBranch
 
141
            return RemoteBranch(url)
 
142
        else:
 
143
            return LocalBranch(url)
 
144
 
 
145
    @staticmethod
 
146
    def initialize(base):
 
147
        """Create a new branch, rooted at 'base' (url)"""
 
148
        if base and (base.startswith('http://') or base.startswith('https://')):
 
149
            from bzrlib.remotebranch import RemoteBranch
 
150
            return RemoteBranch(base, init=True)
 
151
        else:
 
152
            return LocalBranch(base, init=True)
 
153
 
 
154
    def setup_caching(self, cache_root):
 
155
        """Subclasses that care about caching should override this, and set
 
156
        up cached stores located under cache_root.
 
157
        """
 
158
 
 
159
 
 
160
class LocalBranch(Branch):
 
161
    """A branch stored in the actual filesystem.
 
162
 
 
163
    Note that it's "local" in the context of the filesystem; it doesn't
 
164
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
165
    it's writable, and can be accessed via the normal filesystem API.
152
166
 
153
167
    _lock_mode
154
168
        None, or 'r' or 'w'
160
174
    _lock
161
175
        Lock object from bzrlib.lock.
162
176
    """
163
 
    base = None
 
177
    # We actually expect this class to be somewhat short-lived; part of its
 
178
    # purpose is to try to isolate what bits of the branch logic are tied to
 
179
    # filesystem access, so that in a later step, we can extricate them to
 
180
    # a separarte ("storage") class.
164
181
    _lock_mode = None
165
182
    _lock_count = None
166
183
    _lock = None
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
184
 
173
185
    def __init__(self, base, init=False, find_root=True):
174
186
        """Create new branch object at a particular location.
175
187
 
176
 
        base -- Base directory for the branch.
 
188
        base -- Base directory for the branch. May be a file:// url.
177
189
        
178
190
        init -- If True, create new control files in a previously
179
191
             unversioned directory.  If False, the branch must already
192
204
        elif find_root:
193
205
            self.base = find_branch_root(base)
194
206
        else:
 
207
            if base.startswith("file://"):
 
208
                base = base[7:]
195
209
            self.base = os.path.realpath(base)
196
210
            if not isdir(self.controlfilename('.')):
197
 
                from errors import NotBranchError
198
211
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
199
212
                                     ['use "bzr init" to initialize a new working tree',
200
213
                                      'current bzr can only operate from top-of-tree'])
214
227
 
215
228
    def __del__(self):
216
229
        if self._lock_mode or self._lock:
217
 
            from warnings import warn
 
230
            from bzrlib.warnings import warn
218
231
            warn("branch %r was not explicitly unlocked" % self)
219
232
            self._lock.unlock()
220
233
 
221
 
 
222
 
 
223
234
    def lock_write(self):
224
235
        if self._lock_mode:
225
236
            if self._lock_mode != 'w':
226
 
                from errors import LockError
 
237
                from bzrlib.errors import LockError
227
238
                raise LockError("can't upgrade to a write lock from %r" %
228
239
                                self._lock_mode)
229
240
            self._lock_count += 1
235
246
            self._lock_count = 1
236
247
 
237
248
 
238
 
 
239
249
    def lock_read(self):
240
250
        if self._lock_mode:
241
251
            assert self._lock_mode in ('r', 'w'), \
248
258
            self._lock_mode = 'r'
249
259
            self._lock_count = 1
250
260
                        
251
 
 
252
 
            
253
261
    def unlock(self):
254
262
        if not self._lock_mode:
255
 
            from errors import LockError
 
263
            from bzrlib.errors import LockError
256
264
            raise LockError('branch %r is not locked' % (self))
257
265
 
258
266
        if self._lock_count > 1:
262
270
            self._lock = None
263
271
            self._lock_mode = self._lock_count = None
264
272
 
265
 
 
266
273
    def abspath(self, name):
267
274
        """Return absolute filename for something in the branch"""
268
275
        return os.path.join(self.base, name)
269
276
 
270
 
 
271
277
    def relpath(self, path):
272
278
        """Return path relative to this branch of something inside it.
273
279
 
274
280
        Raises an error if path is not in this branch."""
275
281
        return _relpath(self.base, path)
276
282
 
277
 
 
278
283
    def controlfilename(self, file_or_path):
279
284
        """Return location relative to branch."""
280
285
        if isinstance(file_or_path, basestring):
307
312
        else:
308
313
            raise BzrError("invalid controlfile mode %r" % mode)
309
314
 
310
 
 
311
 
 
312
315
    def _make_control(self):
313
316
        from bzrlib.inventory import Inventory
314
 
        from bzrlib.xml import pack_xml
315
317
        
316
318
        os.mkdir(self.controlfilename([]))
317
319
        self.controlfile('README', 'w').write(
330
332
        # if we want per-tree root ids then this is the place to set
331
333
        # them; they're not needed for now and so ommitted for
332
334
        # simplicity.
333
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
335
        f = self.controlfile('inventory','w')
 
336
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
334
337
 
335
338
 
336
339
    def _check_format(self):
345
348
        # on Windows from Linux and so on.  I think it might be better
346
349
        # to always make all internal files in unix format.
347
350
        fmt = self.controlfile('branch-format', 'r').read()
348
 
        fmt.replace('\r\n', '')
 
351
        fmt = fmt.replace('\r\n', '\n')
349
352
        if fmt != BZR_BRANCH_FORMAT:
350
353
            raise BzrError('sorry, branch format %r not supported' % fmt,
351
354
                           ['use a different bzr version',
371
374
    def read_working_inventory(self):
372
375
        """Read the working inventory."""
373
376
        from bzrlib.inventory import Inventory
374
 
        from bzrlib.xml import unpack_xml
375
 
        from time import time
376
 
        before = time()
377
377
        self.lock_read()
378
378
        try:
379
379
            # ElementTree does its own conversion from UTF-8, so open in
380
380
            # binary.
381
 
            inv = unpack_xml(Inventory,
382
 
                             self.controlfile('inventory', 'rb'))
383
 
            mutter("loaded inventory of %d items in %f"
384
 
                   % (len(inv), time() - before))
385
 
            return inv
 
381
            f = self.controlfile('inventory', 'rb')
 
382
            return bzrlib.xml.serializer_v4.read_inventory(f)
386
383
        finally:
387
384
            self.unlock()
388
385
            
394
391
        will be committed to the next revision.
395
392
        """
396
393
        from bzrlib.atomicfile import AtomicFile
397
 
        from bzrlib.xml import pack_xml
398
394
        
399
395
        self.lock_write()
400
396
        try:
401
397
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
402
398
            try:
403
 
                pack_xml(inv, f)
 
399
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
404
400
                f.commit()
405
401
            finally:
406
402
                f.close()
414
410
                         """Inventory for the working copy.""")
415
411
 
416
412
 
417
 
    def add(self, files, verbose=False, ids=None):
 
413
    def add(self, files, ids=None):
418
414
        """Make files versioned.
419
415
 
420
 
        Note that the command line normally calls smart_add instead.
 
416
        Note that the command line normally calls smart_add instead,
 
417
        which can automatically recurse.
421
418
 
422
419
        This puts the files in the Added state, so that they will be
423
420
        recorded by the next commit.
433
430
        TODO: Perhaps have an option to add the ids even if the files do
434
431
              not (yet) exist.
435
432
 
436
 
        TODO: Perhaps return the ids of the files?  But then again it
437
 
              is easy to retrieve them if they're needed.
438
 
 
439
 
        TODO: Adding a directory should optionally recurse down and
440
 
              add all non-ignored children.  Perhaps do that in a
441
 
              higher-level method.
 
433
        TODO: Perhaps yield the ids and paths as they're added.
442
434
        """
443
435
        # TODO: Re-adding a file that is removed in the working copy
444
436
        # should probably put it back with the previous ID.
480
472
                    file_id = gen_file_id(f)
481
473
                inv.add_path(f, kind=kind, file_id=file_id)
482
474
 
483
 
                if verbose:
484
 
                    print 'added', quotefn(f)
485
 
 
486
475
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
487
476
 
488
477
            self._write_inventory(inv)
494
483
        """Print `file` to stdout."""
495
484
        self.lock_read()
496
485
        try:
497
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
486
            tree = self.revision_tree(self.get_rev_id(revno))
498
487
            # use inventory as it was in that revision
499
488
            file_id = tree.inventory.path2id(file)
500
489
            if not file_id:
598
587
            f.close()
599
588
 
600
589
 
601
 
    def get_revision_xml(self, revision_id):
 
590
    def get_revision_xml_file(self, revision_id):
602
591
        """Return XML file object for revision object."""
603
592
        if not revision_id or not isinstance(revision_id, basestring):
604
593
            raise InvalidRevisionId(revision_id)
607
596
        try:
608
597
            try:
609
598
                return self.revision_store[revision_id]
610
 
            except IndexError:
 
599
            except (IndexError, KeyError):
611
600
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
612
601
        finally:
613
602
            self.unlock()
614
603
 
615
604
 
 
605
    #deprecated
 
606
    get_revision_xml = get_revision_xml_file
 
607
 
 
608
 
616
609
    def get_revision(self, revision_id):
617
610
        """Return the Revision object for a named revision"""
618
 
        xml_file = self.get_revision_xml(revision_id)
 
611
        xml_file = self.get_revision_xml_file(revision_id)
619
612
 
620
613
        try:
621
 
            r = unpack_xml(Revision, xml_file)
 
614
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
622
615
        except SyntaxError, e:
623
616
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
624
617
                                         [revision_id,
669
662
               parameter which can be either an integer revno or a
670
663
               string hash."""
671
664
        from bzrlib.inventory import Inventory
672
 
        from bzrlib.xml import unpack_xml
673
665
 
674
 
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
 
666
        f = self.get_inventory_xml_file(inventory_id)
 
667
        return bzrlib.xml.serializer_v4.read_inventory(f)
675
668
 
676
669
 
677
670
    def get_inventory_xml(self, inventory_id):
678
671
        """Get inventory XML as a file object."""
679
672
        return self.inventory_store[inventory_id]
 
673
 
 
674
    get_inventory_xml_file = get_inventory_xml
680
675
            
681
676
 
682
677
    def get_inventory_sha1(self, inventory_id):
712
707
 
713
708
    def common_ancestor(self, other, self_revno=None, other_revno=None):
714
709
        """
715
 
        >>> import commit
 
710
        >>> from bzrlib.commit import commit
716
711
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
717
712
        >>> sb.common_ancestor(sb) == (None, None)
718
713
        True
719
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
714
        >>> commit(sb, "Committing first revision", verbose=False)
720
715
        >>> sb.common_ancestor(sb)[0]
721
716
        1
722
717
        >>> clone = sb.clone()
723
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
718
        >>> commit(sb, "Committing second revision", verbose=False)
724
719
        >>> sb.common_ancestor(sb)[0]
725
720
        2
726
721
        >>> sb.common_ancestor(clone)[0]
727
722
        1
728
 
        >>> commit.commit(clone, "Committing divergent second revision", 
 
723
        >>> commit(clone, "Committing divergent second revision", 
729
724
        ...               verbose=False)
730
725
        >>> sb.common_ancestor(clone)[0]
731
726
        1
822
817
        """Pull in all new revisions from other branch.
823
818
        """
824
819
        from bzrlib.fetch import greedy_fetch
 
820
        from bzrlib.revision import get_intervening_revisions
825
821
 
826
822
        pb = bzrlib.ui.ui_factory.progress_bar()
827
823
        pb.update('comparing histories')
828
 
 
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]
 
824
        if stop_revision is None:
 
825
            other_revision = other.last_patch()
833
826
        else:
834
 
            count = 0
 
827
            other_revision = other.get_rev_id(stop_revision)
 
828
        count = greedy_fetch(self, other, other_revision, pb)[0]
 
829
        try:
 
830
            revision_ids = self.missing_revisions(other, stop_revision)
 
831
        except DivergedBranches, e:
 
832
            try:
 
833
                revision_ids = get_intervening_revisions(self.last_patch(), 
 
834
                                                         other_revision, self)
 
835
                assert self.last_patch() not in revision_ids
 
836
            except bzrlib.errors.NotAncestor:
 
837
                raise e
 
838
 
835
839
        self.append_revision(*revision_ids)
836
 
        ## note("Added %d revisions." % count)
 
840
        pb.clear()
837
841
 
838
 
        
839
842
    def install_revisions(self, other, revision_ids, pb):
840
843
        if hasattr(other.revision_store, "prefetch"):
841
844
            other.revision_store.prefetch(revision_ids)
842
845
        if hasattr(other.inventory_store, "prefetch"):
843
 
            inventory_ids = [other.get_revision(r).inventory_id
844
 
                             for r in revision_ids]
 
846
            inventory_ids = []
 
847
            for rev_id in revision_ids:
 
848
                try:
 
849
                    revision = other.get_revision(rev_id).inventory_id
 
850
                    inventory_ids.append(revision)
 
851
                except bzrlib.errors.NoSuchRevision:
 
852
                    pass
845
853
            other.inventory_store.prefetch(inventory_ids)
846
854
 
847
855
        if pb is None:
872
880
                    
873
881
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
874
882
                                                    needed_texts)
875
 
        print "Added %d texts." % count 
 
883
        #print "Added %d texts." % count 
876
884
        inventory_ids = [ f.inventory_id for f in revisions ]
877
885
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
878
886
                                                         inventory_ids)
879
 
        print "Added %d inventories." % count 
 
887
        #print "Added %d inventories." % count 
880
888
        revision_ids = [ f.revision_id for f in revisions]
881
889
 
882
890
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
891
899
        commit(self, *args, **kw)
892
900
        
893
901
 
894
 
    def lookup_revision(self, revision):
895
 
        """Return the revision identifier for a given revision information."""
896
 
        revno, info = self.get_revision_info(revision)
897
 
        return info
898
 
 
899
902
 
900
903
    def revision_id_to_revno(self, revision_id):
901
904
        """Given a revision id, return its revno"""
906
909
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
907
910
 
908
911
 
909
 
    def get_revision_info(self, revision):
910
 
        """Return (revno, revision id) for revision identifier.
911
 
 
912
 
        revision can be an integer, in which case it is assumed to be revno (though
913
 
            this will translate negative values into positive ones)
914
 
        revision can also be a string, in which case it is parsed for something like
915
 
            'date:' or 'revid:' etc.
916
 
        """
917
 
        if revision is None:
918
 
            return 0, None
919
 
        revno = None
920
 
        try:# Convert to int if possible
921
 
            revision = int(revision)
922
 
        except ValueError:
923
 
            pass
924
 
        revs = self.revision_history()
925
 
        if isinstance(revision, int):
926
 
            if revision == 0:
927
 
                return 0, None
928
 
            # Mabye we should do this first, but we don't need it if revision == 0
929
 
            if revision < 0:
930
 
                revno = len(revs) + revision + 1
931
 
            else:
932
 
                revno = revision
933
 
        elif isinstance(revision, basestring):
934
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
935
 
                if revision.startswith(prefix):
936
 
                    revno = func(self, revs, revision)
937
 
                    break
938
 
            else:
939
 
                raise BzrError('No namespace registered for string: %r' % revision)
940
 
 
941
 
        if revno is None or revno <= 0 or revno > len(revs):
942
 
            raise BzrError("no such revision %s" % revision)
943
 
        return revno, revs[revno-1]
944
 
 
945
 
    def _namespace_revno(self, revs, revision):
946
 
        """Lookup a revision by revision number"""
947
 
        assert revision.startswith('revno:')
948
 
        try:
949
 
            return int(revision[6:])
950
 
        except ValueError:
951
 
            return None
952
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
953
 
 
954
 
    def _namespace_revid(self, revs, revision):
955
 
        assert revision.startswith('revid:')
956
 
        try:
957
 
            return revs.index(revision[6:]) + 1
958
 
        except ValueError:
959
 
            return None
960
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
961
 
 
962
 
    def _namespace_last(self, revs, revision):
963
 
        assert revision.startswith('last:')
964
 
        try:
965
 
            offset = int(revision[5:])
966
 
        except ValueError:
967
 
            return None
968
 
        else:
969
 
            if offset <= 0:
970
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
971
 
            return len(revs) - offset + 1
972
 
    REVISION_NAMESPACES['last:'] = _namespace_last
973
 
 
974
 
    def _namespace_tag(self, revs, revision):
975
 
        assert revision.startswith('tag:')
976
 
        raise BzrError('tag: namespace registered, but not implemented.')
977
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
978
 
 
979
 
    def _namespace_date(self, revs, revision):
980
 
        assert revision.startswith('date:')
981
 
        import datetime
982
 
        # Spec for date revisions:
983
 
        #   date:value
984
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
985
 
        #   it can also start with a '+/-/='. '+' says match the first
986
 
        #   entry after the given date. '-' is match the first entry before the date
987
 
        #   '=' is match the first entry after, but still on the given date.
988
 
        #
989
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
990
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
991
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
992
 
        #       May 13th, 2005 at 0:00
993
 
        #
994
 
        #   So the proper way of saying 'give me all entries for today' is:
995
 
        #       -r {date:+today}:{date:-tomorrow}
996
 
        #   The default is '=' when not supplied
997
 
        val = revision[5:]
998
 
        match_style = '='
999
 
        if val[:1] in ('+', '-', '='):
1000
 
            match_style = val[:1]
1001
 
            val = val[1:]
1002
 
 
1003
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1004
 
        if val.lower() == 'yesterday':
1005
 
            dt = today - datetime.timedelta(days=1)
1006
 
        elif val.lower() == 'today':
1007
 
            dt = today
1008
 
        elif val.lower() == 'tomorrow':
1009
 
            dt = today + datetime.timedelta(days=1)
1010
 
        else:
1011
 
            import re
1012
 
            # This should be done outside the function to avoid recompiling it.
1013
 
            _date_re = re.compile(
1014
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1015
 
                    r'(,|T)?\s*'
1016
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1017
 
                )
1018
 
            m = _date_re.match(val)
1019
 
            if not m or (not m.group('date') and not m.group('time')):
1020
 
                raise BzrError('Invalid revision date %r' % revision)
1021
 
 
1022
 
            if m.group('date'):
1023
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1024
 
            else:
1025
 
                year, month, day = today.year, today.month, today.day
1026
 
            if m.group('time'):
1027
 
                hour = int(m.group('hour'))
1028
 
                minute = int(m.group('minute'))
1029
 
                if m.group('second'):
1030
 
                    second = int(m.group('second'))
1031
 
                else:
1032
 
                    second = 0
1033
 
            else:
1034
 
                hour, minute, second = 0,0,0
1035
 
 
1036
 
            dt = datetime.datetime(year=year, month=month, day=day,
1037
 
                    hour=hour, minute=minute, second=second)
1038
 
        first = dt
1039
 
        last = None
1040
 
        reversed = False
1041
 
        if match_style == '-':
1042
 
            reversed = True
1043
 
        elif match_style == '=':
1044
 
            last = dt + datetime.timedelta(days=1)
1045
 
 
1046
 
        if reversed:
1047
 
            for i in range(len(revs)-1, -1, -1):
1048
 
                r = self.get_revision(revs[i])
1049
 
                # TODO: Handle timezone.
1050
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1051
 
                if first >= dt and (last is None or dt >= last):
1052
 
                    return i+1
1053
 
        else:
1054
 
            for i in range(len(revs)):
1055
 
                r = self.get_revision(revs[i])
1056
 
                # TODO: Handle timezone.
1057
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1058
 
                if first <= dt and (last is None or dt <= last):
1059
 
                    return i+1
1060
 
    REVISION_NAMESPACES['date:'] = _namespace_date
 
912
    def get_rev_id(self, revno, history=None):
 
913
        """Find the revision id of the specified revno."""
 
914
        if revno == 0:
 
915
            return None
 
916
        if history is None:
 
917
            history = self.revision_history()
 
918
        elif revno <= 0 or revno > len(history):
 
919
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
920
        return history[revno - 1]
 
921
 
1061
922
 
1062
923
    def revision_tree(self, revision_id):
1063
924
        """Return Tree for a revision on this branch.
1075
936
 
1076
937
    def working_tree(self):
1077
938
        """Return a `Tree` for the working copy."""
1078
 
        from workingtree import WorkingTree
 
939
        from bzrlib.workingtree import WorkingTree
1079
940
        return WorkingTree(self.base, self.read_working_inventory())
1080
941
 
1081
942
 
1127
988
 
1128
989
            inv.rename(file_id, to_dir_id, to_tail)
1129
990
 
1130
 
            print "%s => %s" % (from_rel, to_rel)
1131
 
 
1132
991
            from_abs = self.abspath(from_rel)
1133
992
            to_abs = self.abspath(to_rel)
1134
993
            try:
1153
1012
 
1154
1013
        Note that to_name is only the last component of the new name;
1155
1014
        this doesn't change the directory.
 
1015
 
 
1016
        This returns a list of (from_path, to_path) pairs for each
 
1017
        entry that is moved.
1156
1018
        """
 
1019
        result = []
1157
1020
        self.lock_write()
1158
1021
        try:
1159
1022
            ## TODO: Option to move IDs only
1194
1057
            for f in from_paths:
1195
1058
                name_tail = splitpath(f)[-1]
1196
1059
                dest_path = appendpath(to_name, name_tail)
1197
 
                print "%s => %s" % (f, dest_path)
 
1060
                result.append((f, dest_path))
1198
1061
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1199
1062
                try:
1200
1063
                    os.rename(self.abspath(f), self.abspath(dest_path))
1206
1069
        finally:
1207
1070
            self.unlock()
1208
1071
 
 
1072
        return result
 
1073
 
1209
1074
 
1210
1075
    def revert(self, filenames, old_tree=None, backups=True):
1211
1076
        """Restore selected files to the versions from a previous tree.
1293
1158
            self.unlock()
1294
1159
 
1295
1160
 
1296
 
 
1297
 
class ScratchBranch(Branch):
 
1161
    def get_parent(self):
 
1162
        """Return the parent location of the branch.
 
1163
 
 
1164
        This is the default location for push/pull/missing.  The usual
 
1165
        pattern is that the user can override it by specifying a
 
1166
        location.
 
1167
        """
 
1168
        import errno
 
1169
        _locs = ['parent', 'pull', 'x-pull']
 
1170
        for l in _locs:
 
1171
            try:
 
1172
                return self.controlfile(l, 'r').read().strip('\n')
 
1173
            except IOError, e:
 
1174
                if e.errno != errno.ENOENT:
 
1175
                    raise
 
1176
        return None
 
1177
 
 
1178
 
 
1179
    def set_parent(self, url):
 
1180
        # TODO: Maybe delete old location files?
 
1181
        from bzrlib.atomicfile import AtomicFile
 
1182
        self.lock_write()
 
1183
        try:
 
1184
            f = AtomicFile(self.controlfilename('parent'))
 
1185
            try:
 
1186
                f.write(url + '\n')
 
1187
                f.commit()
 
1188
            finally:
 
1189
                f.close()
 
1190
        finally:
 
1191
            self.unlock()
 
1192
 
 
1193
    def check_revno(self, revno):
 
1194
        """\
 
1195
        Check whether a revno corresponds to any revision.
 
1196
        Zero (the NULL revision) is considered valid.
 
1197
        """
 
1198
        if revno != 0:
 
1199
            self.check_real_revno(revno)
 
1200
            
 
1201
    def check_real_revno(self, revno):
 
1202
        """\
 
1203
        Check whether a revno corresponds to a real revision.
 
1204
        Zero (the NULL revision) is considered invalid
 
1205
        """
 
1206
        if revno < 1 or revno > self.revno():
 
1207
            raise InvalidRevisionNumber(revno)
 
1208
        
 
1209
        
 
1210
        
 
1211
 
 
1212
 
 
1213
class ScratchBranch(LocalBranch):
1298
1214
    """Special test class: a branch that cleans up after itself.
1299
1215
 
1300
1216
    >>> b = ScratchBranch()
1317
1233
        if base is None:
1318
1234
            base = mkdtemp()
1319
1235
            init = True
1320
 
        Branch.__init__(self, base, init=init)
 
1236
        LocalBranch.__init__(self, base, init=init)
1321
1237
        for d in dirs:
1322
1238
            os.mkdir(self.abspath(d))
1323
1239
            
1340
1256
        os.rmdir(base)
1341
1257
        copytree(self.base, base, symlinks=True)
1342
1258
        return ScratchBranch(base=base)
 
1259
 
 
1260
 
1343
1261
        
1344
1262
    def __del__(self):
1345
1263
        self.destroy()
1415
1333
    """Return a new tree-root file id."""
1416
1334
    return gen_file_id('TREE_ROOT')
1417
1335
 
 
1336
 
 
1337
def copy_branch(branch_from, to_location, revision=None):
 
1338
    """Copy branch_from into the existing directory to_location.
 
1339
 
 
1340
    revision
 
1341
        If not None, only revisions up to this point will be copied.
 
1342
        The head of the new branch will be that revision.
 
1343
 
 
1344
    to_location
 
1345
        The name of a local directory that exists but is empty.
 
1346
    """
 
1347
    from bzrlib.merge import merge
 
1348
    from bzrlib.revisionspec import RevisionSpec
 
1349
 
 
1350
    assert isinstance(branch_from, Branch)
 
1351
    assert isinstance(to_location, basestring)
 
1352
    
 
1353
    br_to = Branch.initialize(to_location)
 
1354
    br_to.set_root_id(branch_from.get_root_id())
 
1355
    if revision is None:
 
1356
        revno = branch_from.revno()
 
1357
    else:
 
1358
        revno, rev_id = RevisionSpec(revision).in_history(branch_from)
 
1359
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1360
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1361
          check_clean=False, ignore_zero=True)
 
1362
    br_to.set_parent(branch_from.base)
 
1363
    return br_to