~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:
23
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
24
     splitpath, \
25
25
     sha_file, appendpath, file_kind
26
 
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
27
 
import bzrlib.errors
 
26
 
 
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
 
28
     DivergedBranches, NotBranchError
28
29
from bzrlib.textui import show_status
29
30
from bzrlib.revision import Revision
30
 
from bzrlib.xml import unpack_xml
31
31
from bzrlib.delta import compare_trees
32
32
from bzrlib.tree import EmptyTree, RevisionTree
33
 
        
 
33
import bzrlib.xml
 
34
import bzrlib.ui
 
35
 
 
36
 
 
37
 
34
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
35
39
## TODO: Maybe include checks for common corruption of newlines, etc?
36
40
 
39
43
# repeatedly to calculate deltas.  We could perhaps have a weakref
40
44
# cache in memory to make this faster.
41
45
 
42
 
 
43
 
def find_branch(f, **args):
44
 
    if f and (f.startswith('http://') or f.startswith('https://')):
45
 
        import remotebranch 
46
 
        return remotebranch.RemoteBranch(f, **args)
47
 
    else:
48
 
        return Branch(f, **args)
49
 
 
50
 
 
51
 
def find_cached_branch(f, cache_root, **args):
52
 
    from remotebranch import RemoteBranch
53
 
    br = find_branch(f, **args)
54
 
    def cacheify(br, store_name):
55
 
        from meta_store import CachedStore
56
 
        cache_path = os.path.join(cache_root, store_name)
57
 
        os.mkdir(cache_path)
58
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
59
 
        setattr(br, store_name, new_store)
60
 
 
61
 
    if isinstance(br, RemoteBranch):
62
 
        cacheify(br, 'inventory_store')
63
 
        cacheify(br, 'text_store')
64
 
        cacheify(br, 'revision_store')
65
 
    return br
66
 
 
 
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')
67
50
 
68
51
def _relpath(base, path):
69
52
    """Return path relative to base, or raise exception.
87
70
        if tail:
88
71
            s.insert(0, tail)
89
72
    else:
90
 
        from errors import NotBranchError
91
73
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
92
74
 
93
75
    return os.sep.join(s)
101
83
    It is not necessary that f exists.
102
84
 
103
85
    Basically we keep looking up until we find the control directory or
104
 
    run into the root."""
 
86
    run into the root.  If there isn't one, raises NotBranchError.
 
87
    """
105
88
    if f == None:
106
89
        f = os.getcwd()
107
90
    elif hasattr(os.path, 'realpath'):
120
103
        head, tail = os.path.split(f)
121
104
        if head == f:
122
105
            # reached the root, whatever that may be
123
 
            raise BzrError('%r is not in a branch' % orig_f)
 
106
            raise NotBranchError('%s is not in a branch' % orig_f)
124
107
        f = head
125
 
    
126
 
class DivergedBranches(Exception):
127
 
    def __init__(self, branch1, branch2):
128
 
        self.branch1 = branch1
129
 
        self.branch2 = branch2
130
 
        Exception.__init__(self, "These branches have diverged.")
 
108
 
 
109
 
131
110
 
132
111
 
133
112
######################################################################
137
116
    """Branch holding a history of revisions.
138
117
 
139
118
    base
140
 
        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.
141
166
 
142
167
    _lock_mode
143
168
        None, or 'r' or 'w'
149
174
    _lock
150
175
        Lock object from bzrlib.lock.
151
176
    """
152
 
    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.
153
181
    _lock_mode = None
154
182
    _lock_count = None
155
183
    _lock = None
156
 
    
157
 
    # Map some sort of prefix into a namespace
158
 
    # stuff like "revno:10", "revid:", etc.
159
 
    # This should match a prefix with a function which accepts
160
 
    REVISION_NAMESPACES = {}
161
184
 
162
185
    def __init__(self, base, init=False, find_root=True):
163
186
        """Create new branch object at a particular location.
164
187
 
165
 
        base -- Base directory for the branch.
 
188
        base -- Base directory for the branch. May be a file:// url.
166
189
        
167
190
        init -- If True, create new control files in a previously
168
191
             unversioned directory.  If False, the branch must already
181
204
        elif find_root:
182
205
            self.base = find_branch_root(base)
183
206
        else:
 
207
            if base.startswith("file://"):
 
208
                base = base[7:]
184
209
            self.base = os.path.realpath(base)
185
210
            if not isdir(self.controlfilename('.')):
186
 
                from errors import NotBranchError
187
211
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
188
212
                                     ['use "bzr init" to initialize a new working tree',
189
213
                                      'current bzr can only operate from top-of-tree'])
203
227
 
204
228
    def __del__(self):
205
229
        if self._lock_mode or self._lock:
206
 
            from warnings import warn
 
230
            from bzrlib.warnings import warn
207
231
            warn("branch %r was not explicitly unlocked" % self)
208
232
            self._lock.unlock()
209
233
 
210
 
 
211
 
 
212
234
    def lock_write(self):
213
235
        if self._lock_mode:
214
236
            if self._lock_mode != 'w':
215
 
                from errors import LockError
 
237
                from bzrlib.errors import LockError
216
238
                raise LockError("can't upgrade to a write lock from %r" %
217
239
                                self._lock_mode)
218
240
            self._lock_count += 1
224
246
            self._lock_count = 1
225
247
 
226
248
 
227
 
 
228
249
    def lock_read(self):
229
250
        if self._lock_mode:
230
251
            assert self._lock_mode in ('r', 'w'), \
237
258
            self._lock_mode = 'r'
238
259
            self._lock_count = 1
239
260
                        
240
 
 
241
 
            
242
261
    def unlock(self):
243
262
        if not self._lock_mode:
244
 
            from errors import LockError
 
263
            from bzrlib.errors import LockError
245
264
            raise LockError('branch %r is not locked' % (self))
246
265
 
247
266
        if self._lock_count > 1:
251
270
            self._lock = None
252
271
            self._lock_mode = self._lock_count = None
253
272
 
254
 
 
255
273
    def abspath(self, name):
256
274
        """Return absolute filename for something in the branch"""
257
275
        return os.path.join(self.base, name)
258
276
 
259
 
 
260
277
    def relpath(self, path):
261
278
        """Return path relative to this branch of something inside it.
262
279
 
263
280
        Raises an error if path is not in this branch."""
264
281
        return _relpath(self.base, path)
265
282
 
266
 
 
267
283
    def controlfilename(self, file_or_path):
268
284
        """Return location relative to branch."""
269
285
        if isinstance(file_or_path, basestring):
296
312
        else:
297
313
            raise BzrError("invalid controlfile mode %r" % mode)
298
314
 
299
 
 
300
 
 
301
315
    def _make_control(self):
302
316
        from bzrlib.inventory import Inventory
303
 
        from bzrlib.xml import pack_xml
304
317
        
305
318
        os.mkdir(self.controlfilename([]))
306
319
        self.controlfile('README', 'w').write(
316
329
            self.controlfile(f, 'w').write('')
317
330
        mutter('created control directory in ' + self.base)
318
331
 
319
 
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
 
332
        # if we want per-tree root ids then this is the place to set
 
333
        # them; they're not needed for now and so ommitted for
 
334
        # simplicity.
 
335
        f = self.controlfile('inventory','w')
 
336
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
320
337
 
321
338
 
322
339
    def _check_format(self):
331
348
        # on Windows from Linux and so on.  I think it might be better
332
349
        # to always make all internal files in unix format.
333
350
        fmt = self.controlfile('branch-format', 'r').read()
334
 
        fmt.replace('\r\n', '')
 
351
        fmt = fmt.replace('\r\n', '\n')
335
352
        if fmt != BZR_BRANCH_FORMAT:
336
353
            raise BzrError('sorry, branch format %r not supported' % fmt,
337
354
                           ['use a different bzr version',
357
374
    def read_working_inventory(self):
358
375
        """Read the working inventory."""
359
376
        from bzrlib.inventory import Inventory
360
 
        from bzrlib.xml import unpack_xml
361
 
        from time import time
362
 
        before = time()
363
377
        self.lock_read()
364
378
        try:
365
379
            # ElementTree does its own conversion from UTF-8, so open in
366
380
            # binary.
367
 
            inv = unpack_xml(Inventory,
368
 
                             self.controlfile('inventory', 'rb'))
369
 
            mutter("loaded inventory of %d items in %f"
370
 
                   % (len(inv), time() - before))
371
 
            return inv
 
381
            f = self.controlfile('inventory', 'rb')
 
382
            return bzrlib.xml.serializer_v4.read_inventory(f)
372
383
        finally:
373
384
            self.unlock()
374
385
            
380
391
        will be committed to the next revision.
381
392
        """
382
393
        from bzrlib.atomicfile import AtomicFile
383
 
        from bzrlib.xml import pack_xml
384
394
        
385
395
        self.lock_write()
386
396
        try:
387
397
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
388
398
            try:
389
 
                pack_xml(inv, f)
 
399
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
390
400
                f.commit()
391
401
            finally:
392
402
                f.close()
400
410
                         """Inventory for the working copy.""")
401
411
 
402
412
 
403
 
    def add(self, files, verbose=False, ids=None):
 
413
    def add(self, files, ids=None):
404
414
        """Make files versioned.
405
415
 
406
 
        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.
407
418
 
408
419
        This puts the files in the Added state, so that they will be
409
420
        recorded by the next commit.
419
430
        TODO: Perhaps have an option to add the ids even if the files do
420
431
              not (yet) exist.
421
432
 
422
 
        TODO: Perhaps return the ids of the files?  But then again it
423
 
              is easy to retrieve them if they're needed.
424
 
 
425
 
        TODO: Adding a directory should optionally recurse down and
426
 
              add all non-ignored children.  Perhaps do that in a
427
 
              higher-level method.
 
433
        TODO: Perhaps yield the ids and paths as they're added.
428
434
        """
429
435
        # TODO: Re-adding a file that is removed in the working copy
430
436
        # should probably put it back with the previous ID.
466
472
                    file_id = gen_file_id(f)
467
473
                inv.add_path(f, kind=kind, file_id=file_id)
468
474
 
469
 
                if verbose:
470
 
                    print 'added', quotefn(f)
471
 
 
472
475
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
473
476
 
474
477
            self._write_inventory(inv)
480
483
        """Print `file` to stdout."""
481
484
        self.lock_read()
482
485
        try:
483
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
486
            tree = self.revision_tree(self.get_rev_id(revno))
484
487
            # use inventory as it was in that revision
485
488
            file_id = tree.inventory.path2id(file)
486
489
            if not file_id:
584
587
            f.close()
585
588
 
586
589
 
587
 
    def get_revision_xml(self, revision_id):
 
590
    def get_revision_xml_file(self, revision_id):
588
591
        """Return XML file object for revision object."""
589
592
        if not revision_id or not isinstance(revision_id, basestring):
590
593
            raise InvalidRevisionId(revision_id)
593
596
        try:
594
597
            try:
595
598
                return self.revision_store[revision_id]
596
 
            except IndexError:
597
 
                raise bzrlib.errors.NoSuchRevision(revision_id)
 
599
            except (IndexError, KeyError):
 
600
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
598
601
        finally:
599
602
            self.unlock()
600
603
 
601
604
 
 
605
    #deprecated
 
606
    get_revision_xml = get_revision_xml_file
 
607
 
 
608
 
602
609
    def get_revision(self, revision_id):
603
610
        """Return the Revision object for a named revision"""
604
 
        xml_file = self.get_revision_xml(revision_id)
 
611
        xml_file = self.get_revision_xml_file(revision_id)
605
612
 
606
613
        try:
607
 
            r = unpack_xml(Revision, xml_file)
 
614
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
608
615
        except SyntaxError, e:
609
616
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
610
617
                                         [revision_id,
655
662
               parameter which can be either an integer revno or a
656
663
               string hash."""
657
664
        from bzrlib.inventory import Inventory
658
 
        from bzrlib.xml import unpack_xml
659
 
 
660
 
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
665
 
 
666
        f = self.get_inventory_xml_file(inventory_id)
 
667
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
668
 
 
669
 
 
670
    def get_inventory_xml(self, inventory_id):
 
671
        """Get inventory XML as a file object."""
 
672
        return self.inventory_store[inventory_id]
 
673
 
 
674
    get_inventory_xml_file = get_inventory_xml
661
675
            
662
676
 
663
677
    def get_inventory_sha1(self, inventory_id):
664
678
        """Return the sha1 hash of the inventory entry
665
679
        """
666
 
        return sha_file(self.inventory_store[inventory_id])
 
680
        return sha_file(self.get_inventory_xml(inventory_id))
667
681
 
668
682
 
669
683
    def get_revision_inventory(self, revision_id):
693
707
 
694
708
    def common_ancestor(self, other, self_revno=None, other_revno=None):
695
709
        """
696
 
        >>> import commit
 
710
        >>> from bzrlib.commit import commit
697
711
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
698
712
        >>> sb.common_ancestor(sb) == (None, None)
699
713
        True
700
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
714
        >>> commit(sb, "Committing first revision", verbose=False)
701
715
        >>> sb.common_ancestor(sb)[0]
702
716
        1
703
717
        >>> clone = sb.clone()
704
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
718
        >>> commit(sb, "Committing second revision", verbose=False)
705
719
        >>> sb.common_ancestor(sb)[0]
706
720
        2
707
721
        >>> sb.common_ancestor(clone)[0]
708
722
        1
709
 
        >>> commit.commit(clone, "Committing divergent second revision", 
 
723
        >>> commit(clone, "Committing divergent second revision", 
710
724
        ...               verbose=False)
711
725
        >>> sb.common_ancestor(clone)[0]
712
726
        1
755
769
            return None
756
770
 
757
771
 
758
 
    def missing_revisions(self, other, stop_revision=None):
 
772
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
759
773
        """
760
774
        If self and other have not diverged, return a list of the revisions
761
775
        present in other, but missing from self.
794
808
        if stop_revision is None:
795
809
            stop_revision = other_len
796
810
        elif stop_revision > other_len:
797
 
            raise NoSuchRevision(self, stop_revision)
 
811
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
798
812
        
799
813
        return other_history[self_len:stop_revision]
800
814
 
801
815
 
802
816
    def update_revisions(self, other, stop_revision=None):
803
817
        """Pull in all new revisions from other branch.
804
 
        
805
 
        >>> from bzrlib.commit import commit
806
 
        >>> bzrlib.trace.silent = True
807
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
808
 
        >>> br1.add('foo')
809
 
        >>> br1.add('bar')
810
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
811
 
        >>> br2 = ScratchBranch()
812
 
        >>> br2.update_revisions(br1)
813
 
        Added 2 texts.
814
 
        Added 1 inventories.
815
 
        Added 1 revisions.
816
 
        >>> br2.revision_history()
817
 
        [u'REVISION-ID-1']
818
 
        >>> br2.update_revisions(br1)
819
 
        Added 0 texts.
820
 
        Added 0 inventories.
821
 
        Added 0 revisions.
822
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
823
 
        True
824
818
        """
825
 
        from bzrlib.progress import ProgressBar
826
 
 
827
 
        pb = ProgressBar()
828
 
 
 
819
        from bzrlib.fetch import greedy_fetch
 
820
        from bzrlib.revision import get_intervening_revisions
 
821
 
 
822
        pb = bzrlib.ui.ui_factory.progress_bar()
829
823
        pb.update('comparing histories')
830
 
        revision_ids = self.missing_revisions(other, stop_revision)
831
 
 
 
824
        if stop_revision is None:
 
825
            other_revision = other.last_patch()
 
826
        else:
 
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
 
 
839
        self.append_revision(*revision_ids)
 
840
        pb.clear()
 
841
 
 
842
    def install_revisions(self, other, revision_ids, pb):
832
843
        if hasattr(other.revision_store, "prefetch"):
833
844
            other.revision_store.prefetch(revision_ids)
834
845
        if hasattr(other.inventory_store, "prefetch"):
835
 
            inventory_ids = [other.get_revision(r).inventory_id
836
 
                             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
837
853
            other.inventory_store.prefetch(inventory_ids)
 
854
 
 
855
        if pb is None:
 
856
            pb = bzrlib.ui.ui_factory.progress_bar()
838
857
                
839
858
        revisions = []
840
859
        needed_texts = set()
841
860
        i = 0
842
 
        for rev_id in revision_ids:
843
 
            i += 1
844
 
            pb.update('fetching revision', i, len(revision_ids))
845
 
            rev = other.get_revision(rev_id)
 
861
 
 
862
        failures = set()
 
863
        for i, rev_id in enumerate(revision_ids):
 
864
            pb.update('fetching revision', i+1, len(revision_ids))
 
865
            try:
 
866
                rev = other.get_revision(rev_id)
 
867
            except bzrlib.errors.NoSuchRevision:
 
868
                failures.add(rev_id)
 
869
                continue
 
870
 
846
871
            revisions.append(rev)
847
872
            inv = other.get_inventory(str(rev.inventory_id))
848
873
            for key, entry in inv.iter_entries():
853
878
 
854
879
        pb.clear()
855
880
                    
856
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
857
 
        print "Added %d texts." % count 
 
881
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
882
                                                    needed_texts)
 
883
        #print "Added %d texts." % count 
858
884
        inventory_ids = [ f.inventory_id for f in revisions ]
859
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
860
 
                                                inventory_ids)
861
 
        print "Added %d inventories." % count 
 
885
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
886
                                                         inventory_ids)
 
887
        #print "Added %d inventories." % count 
862
888
        revision_ids = [ f.revision_id for f in revisions]
863
 
        count = self.revision_store.copy_multi(other.revision_store, 
864
 
                                               revision_ids)
865
 
        for revision_id in revision_ids:
866
 
            self.append_revision(revision_id)
867
 
        print "Added %d revisions." % count
868
 
                    
869
 
        
 
889
 
 
890
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
891
                                                          revision_ids,
 
892
                                                          permit_failure=True)
 
893
        assert len(cp_fail) == 0 
 
894
        return count, failures
 
895
       
 
896
 
870
897
    def commit(self, *args, **kw):
871
898
        from bzrlib.commit import commit
872
899
        commit(self, *args, **kw)
873
900
        
874
901
 
875
 
    def lookup_revision(self, revision):
876
 
        """Return the revision identifier for a given revision information."""
877
 
        revno, info = self.get_revision_info(revision)
878
 
        return info
879
 
 
880
 
    def get_revision_info(self, revision):
881
 
        """Return (revno, revision id) for revision identifier.
882
 
 
883
 
        revision can be an integer, in which case it is assumed to be revno (though
884
 
            this will translate negative values into positive ones)
885
 
        revision can also be a string, in which case it is parsed for something like
886
 
            'date:' or 'revid:' etc.
887
 
        """
888
 
        if revision is None:
889
 
            return 0, None
890
 
        revno = None
891
 
        try:# Convert to int if possible
892
 
            revision = int(revision)
893
 
        except ValueError:
894
 
            pass
895
 
        revs = self.revision_history()
896
 
        if isinstance(revision, int):
897
 
            if revision == 0:
898
 
                return 0, None
899
 
            # Mabye we should do this first, but we don't need it if revision == 0
900
 
            if revision < 0:
901
 
                revno = len(revs) + revision + 1
902
 
            else:
903
 
                revno = revision
904
 
        elif isinstance(revision, basestring):
905
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
906
 
                if revision.startswith(prefix):
907
 
                    revno = func(self, revs, revision)
908
 
                    break
909
 
            else:
910
 
                raise BzrError('No namespace registered for string: %r' % revision)
911
 
 
912
 
        if revno is None or revno <= 0 or revno > len(revs):
913
 
            raise BzrError("no such revision %s" % revision)
914
 
        return revno, revs[revno-1]
915
 
 
916
 
    def _namespace_revno(self, revs, revision):
917
 
        """Lookup a revision by revision number"""
918
 
        assert revision.startswith('revno:')
919
 
        try:
920
 
            return int(revision[6:])
921
 
        except ValueError:
922
 
            return None
923
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
924
 
 
925
 
    def _namespace_revid(self, revs, revision):
926
 
        assert revision.startswith('revid:')
927
 
        try:
928
 
            return revs.index(revision[6:]) + 1
929
 
        except ValueError:
930
 
            return None
931
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
932
 
 
933
 
    def _namespace_last(self, revs, revision):
934
 
        assert revision.startswith('last:')
935
 
        try:
936
 
            offset = int(revision[5:])
937
 
        except ValueError:
938
 
            return None
939
 
        else:
940
 
            if offset <= 0:
941
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
942
 
            return len(revs) - offset + 1
943
 
    REVISION_NAMESPACES['last:'] = _namespace_last
944
 
 
945
 
    def _namespace_tag(self, revs, revision):
946
 
        assert revision.startswith('tag:')
947
 
        raise BzrError('tag: namespace registered, but not implemented.')
948
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
949
 
 
950
 
    def _namespace_date(self, revs, revision):
951
 
        assert revision.startswith('date:')
952
 
        import datetime
953
 
        # Spec for date revisions:
954
 
        #   date:value
955
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
956
 
        #   it can also start with a '+/-/='. '+' says match the first
957
 
        #   entry after the given date. '-' is match the first entry before the date
958
 
        #   '=' is match the first entry after, but still on the given date.
959
 
        #
960
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
961
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
962
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
963
 
        #       May 13th, 2005 at 0:00
964
 
        #
965
 
        #   So the proper way of saying 'give me all entries for today' is:
966
 
        #       -r {date:+today}:{date:-tomorrow}
967
 
        #   The default is '=' when not supplied
968
 
        val = revision[5:]
969
 
        match_style = '='
970
 
        if val[:1] in ('+', '-', '='):
971
 
            match_style = val[:1]
972
 
            val = val[1:]
973
 
 
974
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
975
 
        if val.lower() == 'yesterday':
976
 
            dt = today - datetime.timedelta(days=1)
977
 
        elif val.lower() == 'today':
978
 
            dt = today
979
 
        elif val.lower() == 'tomorrow':
980
 
            dt = today + datetime.timedelta(days=1)
981
 
        else:
982
 
            import re
983
 
            # This should be done outside the function to avoid recompiling it.
984
 
            _date_re = re.compile(
985
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
986
 
                    r'(,|T)?\s*'
987
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
988
 
                )
989
 
            m = _date_re.match(val)
990
 
            if not m or (not m.group('date') and not m.group('time')):
991
 
                raise BzrError('Invalid revision date %r' % revision)
992
 
 
993
 
            if m.group('date'):
994
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
995
 
            else:
996
 
                year, month, day = today.year, today.month, today.day
997
 
            if m.group('time'):
998
 
                hour = int(m.group('hour'))
999
 
                minute = int(m.group('minute'))
1000
 
                if m.group('second'):
1001
 
                    second = int(m.group('second'))
1002
 
                else:
1003
 
                    second = 0
1004
 
            else:
1005
 
                hour, minute, second = 0,0,0
1006
 
 
1007
 
            dt = datetime.datetime(year=year, month=month, day=day,
1008
 
                    hour=hour, minute=minute, second=second)
1009
 
        first = dt
1010
 
        last = None
1011
 
        reversed = False
1012
 
        if match_style == '-':
1013
 
            reversed = True
1014
 
        elif match_style == '=':
1015
 
            last = dt + datetime.timedelta(days=1)
1016
 
 
1017
 
        if reversed:
1018
 
            for i in range(len(revs)-1, -1, -1):
1019
 
                r = self.get_revision(revs[i])
1020
 
                # TODO: Handle timezone.
1021
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1022
 
                if first >= dt and (last is None or dt >= last):
1023
 
                    return i+1
1024
 
        else:
1025
 
            for i in range(len(revs)):
1026
 
                r = self.get_revision(revs[i])
1027
 
                # TODO: Handle timezone.
1028
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1029
 
                if first <= dt and (last is None or dt <= last):
1030
 
                    return i+1
1031
 
    REVISION_NAMESPACES['date:'] = _namespace_date
 
902
 
 
903
    def revision_id_to_revno(self, revision_id):
 
904
        """Given a revision id, return its revno"""
 
905
        history = self.revision_history()
 
906
        try:
 
907
            return history.index(revision_id) + 1
 
908
        except ValueError:
 
909
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
910
 
 
911
 
 
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
 
1032
922
 
1033
923
    def revision_tree(self, revision_id):
1034
924
        """Return Tree for a revision on this branch.
1046
936
 
1047
937
    def working_tree(self):
1048
938
        """Return a `Tree` for the working copy."""
1049
 
        from workingtree import WorkingTree
 
939
        from bzrlib.workingtree import WorkingTree
1050
940
        return WorkingTree(self.base, self.read_working_inventory())
1051
941
 
1052
942
 
1098
988
 
1099
989
            inv.rename(file_id, to_dir_id, to_tail)
1100
990
 
1101
 
            print "%s => %s" % (from_rel, to_rel)
1102
 
 
1103
991
            from_abs = self.abspath(from_rel)
1104
992
            to_abs = self.abspath(to_rel)
1105
993
            try:
1124
1012
 
1125
1013
        Note that to_name is only the last component of the new name;
1126
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.
1127
1018
        """
 
1019
        result = []
1128
1020
        self.lock_write()
1129
1021
        try:
1130
1022
            ## TODO: Option to move IDs only
1165
1057
            for f in from_paths:
1166
1058
                name_tail = splitpath(f)[-1]
1167
1059
                dest_path = appendpath(to_name, name_tail)
1168
 
                print "%s => %s" % (f, dest_path)
 
1060
                result.append((f, dest_path))
1169
1061
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1170
1062
                try:
1171
1063
                    os.rename(self.abspath(f), self.abspath(dest_path))
1177
1069
        finally:
1178
1070
            self.unlock()
1179
1071
 
 
1072
        return result
 
1073
 
1180
1074
 
1181
1075
    def revert(self, filenames, old_tree=None, backups=True):
1182
1076
        """Restore selected files to the versions from a previous tree.
1264
1158
            self.unlock()
1265
1159
 
1266
1160
 
1267
 
 
1268
 
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):
1269
1214
    """Special test class: a branch that cleans up after itself.
1270
1215
 
1271
1216
    >>> b = ScratchBranch()
1288
1233
        if base is None:
1289
1234
            base = mkdtemp()
1290
1235
            init = True
1291
 
        Branch.__init__(self, base, init=init)
 
1236
        LocalBranch.__init__(self, base, init=init)
1292
1237
        for d in dirs:
1293
1238
            os.mkdir(self.abspath(d))
1294
1239
            
1311
1256
        os.rmdir(base)
1312
1257
        copytree(self.base, base, symlinks=True)
1313
1258
        return ScratchBranch(base=base)
 
1259
 
 
1260
 
1314
1261
        
1315
1262
    def __del__(self):
1316
1263
        self.destroy()
1386
1333
    """Return a new tree-root file id."""
1387
1334
    return gen_file_id('TREE_ROOT')
1388
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