~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-08-30 06:10:39 UTC
  • Revision ID: mbp@sourcefrog.net-20050830061039-1d0347fb236c39ad
- clean up some code in revision.py

- move all exceptions to bzrlib.errors

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
import sys
19
19
import os
20
 
import errno
21
 
from warnings import warn
22
 
 
23
20
 
24
21
import bzrlib
25
22
from bzrlib.trace import mutter, note
26
 
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
27
 
                            rename, splitpath, sha_file, appendpath, 
28
 
                            file_kind)
29
 
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
30
 
                           NoSuchRevision, HistoryMissing, NotBranchError,
31
 
                           DivergedBranches, LockError, UnlistableStore,
32
 
                           UnlistableBranch)
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     splitpath, \
 
25
     sha_file, appendpath, file_kind
 
26
 
 
27
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
 
28
import bzrlib.errors
33
29
from bzrlib.textui import show_status
34
 
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
 
30
from bzrlib.revision import Revision
 
31
from bzrlib.xml import unpack_xml
35
32
from bzrlib.delta import compare_trees
36
33
from bzrlib.tree import EmptyTree, RevisionTree
37
 
from bzrlib.inventory import Inventory
38
 
from bzrlib.weavestore import WeaveStore
39
 
from bzrlib.store import copy_all, ImmutableStore
40
 
import bzrlib.xml5
41
34
import bzrlib.ui
42
35
 
43
36
 
44
 
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
45
 
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
37
 
 
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
46
39
## TODO: Maybe include checks for common corruption of newlines, etc?
47
40
 
48
41
 
49
42
# TODO: Some operations like log might retrieve the same revisions
50
43
# repeatedly to calculate deltas.  We could perhaps have a weakref
51
 
# cache in memory to make this faster.  In general anything can be
52
 
# cached in memory between lock and unlock operations.
53
 
 
54
 
def find_branch(*ignored, **ignored_too):
55
 
    # XXX: leave this here for about one release, then remove it
56
 
    raise NotImplementedError('find_branch() is not supported anymore, '
57
 
                              'please use one of the new branch constructors')
 
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
 
 
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
 
58
74
 
59
75
def _relpath(base, path):
60
76
    """Return path relative to base, or raise exception.
78
94
        if tail:
79
95
            s.insert(0, tail)
80
96
    else:
 
97
        from errors import NotBranchError
81
98
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
82
99
 
83
100
    return os.sep.join(s)
111
128
        head, tail = os.path.split(f)
112
129
        if head == f:
113
130
            # reached the root, whatever that may be
114
 
            raise NotBranchError('%s is not in a branch' % orig_f)
 
131
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
115
132
        f = head
116
133
 
117
134
 
118
135
 
 
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
 
119
143
 
120
144
######################################################################
121
145
# branch objects
124
148
    """Branch holding a history of revisions.
125
149
 
126
150
    base
127
 
        Base directory/url of the branch.
128
 
    """
129
 
    base = None
130
 
 
131
 
    def __init__(self, *ignored, **ignored_too):
132
 
        raise NotImplementedError('The Branch class is abstract')
133
 
 
134
 
    @staticmethod
135
 
    def open_downlevel(base):
136
 
        """Open a branch which may be of an old format.
137
 
        
138
 
        Only local branches are supported."""
139
 
        return LocalBranch(base, find_root=False, relax_version_check=True)
140
 
        
141
 
    @staticmethod
142
 
    def open(base):
143
 
        """Open an existing branch, rooted at 'base' (url)"""
144
 
        if base and (base.startswith('http://') or base.startswith('https://')):
145
 
            from bzrlib.remotebranch import RemoteBranch
146
 
            return RemoteBranch(base, find_root=False)
147
 
        else:
148
 
            return LocalBranch(base, find_root=False)
149
 
 
150
 
    @staticmethod
151
 
    def open_containing(url):
152
 
        """Open an existing branch which contains url.
153
 
        
154
 
        This probes for a branch at url, and searches upwards from there.
155
 
        """
156
 
        if url and (url.startswith('http://') or url.startswith('https://')):
157
 
            from bzrlib.remotebranch import RemoteBranch
158
 
            return RemoteBranch(url)
159
 
        else:
160
 
            return LocalBranch(url)
161
 
 
162
 
    @staticmethod
163
 
    def initialize(base):
164
 
        """Create a new branch, rooted at 'base' (url)"""
165
 
        if base and (base.startswith('http://') or base.startswith('https://')):
166
 
            from bzrlib.remotebranch import RemoteBranch
167
 
            return RemoteBranch(base, init=True)
168
 
        else:
169
 
            return LocalBranch(base, init=True)
170
 
 
171
 
    def setup_caching(self, cache_root):
172
 
        """Subclasses that care about caching should override this, and set
173
 
        up cached stores located under cache_root.
174
 
        """
175
 
 
176
 
 
177
 
class LocalBranch(Branch):
178
 
    """A branch stored in the actual filesystem.
179
 
 
180
 
    Note that it's "local" in the context of the filesystem; it doesn't
181
 
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
182
 
    it's writable, and can be accessed via the normal filesystem API.
 
151
        Base directory of the branch.
183
152
 
184
153
    _lock_mode
185
154
        None, or 'r' or 'w'
191
160
    _lock
192
161
        Lock object from bzrlib.lock.
193
162
    """
194
 
    # We actually expect this class to be somewhat short-lived; part of its
195
 
    # purpose is to try to isolate what bits of the branch logic are tied to
196
 
    # filesystem access, so that in a later step, we can extricate them to
197
 
    # a separarte ("storage") class.
 
163
    base = None
198
164
    _lock_mode = None
199
165
    _lock_count = None
200
166
    _lock = None
201
 
    _inventory_weave = None
202
167
    
203
168
    # Map some sort of prefix into a namespace
204
169
    # stuff like "revno:10", "revid:", etc.
205
170
    # This should match a prefix with a function which accepts
206
171
    REVISION_NAMESPACES = {}
207
172
 
208
 
    def push_stores(self, branch_to):
209
 
        """Copy the content of this branches store to branch_to."""
210
 
        if (self._branch_format != branch_to._branch_format
211
 
            or self._branch_format != 4):
212
 
            from bzrlib.fetch import greedy_fetch
213
 
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
214
 
                   self, self._branch_format, branch_to, branch_to._branch_format)
215
 
            greedy_fetch(to_branch=branch_to, from_branch=self,
216
 
                         revision=self.last_revision())
217
 
            return
218
 
 
219
 
        store_pairs = ((self.text_store,      branch_to.text_store),
220
 
                       (self.inventory_store, branch_to.inventory_store),
221
 
                       (self.revision_store,  branch_to.revision_store))
222
 
        try:
223
 
            for from_store, to_store in store_pairs: 
224
 
                copy_all(from_store, to_store)
225
 
        except UnlistableStore:
226
 
            raise UnlistableBranch(from_store)
227
 
 
228
 
    def __init__(self, base, init=False, find_root=True,
229
 
                 relax_version_check=False):
 
173
    def __init__(self, base, init=False, find_root=True):
230
174
        """Create new branch object at a particular location.
231
175
 
232
 
        base -- Base directory for the branch. May be a file:// url.
 
176
        base -- Base directory for the branch.
233
177
        
234
178
        init -- If True, create new control files in a previously
235
179
             unversioned directory.  If False, the branch must already
238
182
        find_root -- If true and init is false, find the root of the
239
183
             existing branch containing base.
240
184
 
241
 
        relax_version_check -- If true, the usual check for the branch
242
 
            version is not applied.  This is intended only for
243
 
            upgrade/recovery type use; it's not guaranteed that
244
 
            all operations will work on old format branches.
245
 
 
246
185
        In the test suite, creation of new trees is tested using the
247
186
        `ScratchBranch` class.
248
187
        """
 
188
        from bzrlib.store import ImmutableStore
249
189
        if init:
250
190
            self.base = os.path.realpath(base)
251
191
            self._make_control()
252
192
        elif find_root:
253
193
            self.base = find_branch_root(base)
254
194
        else:
255
 
            if base.startswith("file://"):
256
 
                base = base[7:]
257
195
            self.base = os.path.realpath(base)
258
196
            if not isdir(self.controlfilename('.')):
259
 
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
260
 
                                     ['use "bzr init" to initialize a '
261
 
                                      'new working tree'])
262
 
        self._check_format(relax_version_check)
263
 
        cfn = self.controlfilename
264
 
        if self._branch_format == 4:
265
 
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
266
 
            self.text_store = ImmutableStore(cfn('text-store'))
267
 
        elif self._branch_format == 5:
268
 
            self.control_weaves = WeaveStore(cfn([]))
269
 
            self.weave_store = WeaveStore(cfn('weaves'))
270
 
            if init:
271
 
                # FIXME: Unify with make_control_files
272
 
                self.control_weaves.put_empty_weave('inventory')
273
 
                self.control_weaves.put_empty_weave('ancestry')
274
 
        self.revision_store = ImmutableStore(cfn('revision-store'))
 
197
                from errors import NotBranchError
 
198
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
199
                                     ['use "bzr init" to initialize a new working tree',
 
200
                                      'current bzr can only operate from top-of-tree'])
 
201
        self._check_format()
 
202
 
 
203
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
204
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
205
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
275
206
 
276
207
 
277
208
    def __str__(self):
283
214
 
284
215
    def __del__(self):
285
216
        if self._lock_mode or self._lock:
286
 
            # XXX: This should show something every time, and be suitable for
287
 
            # headless operation and embedding
 
217
            from warnings import warn
288
218
            warn("branch %r was not explicitly unlocked" % self)
289
219
            self._lock.unlock()
290
220
 
 
221
 
291
222
    def lock_write(self):
292
223
        if self._lock_mode:
293
224
            if self._lock_mode != 'w':
 
225
                from errors import LockError
294
226
                raise LockError("can't upgrade to a write lock from %r" %
295
227
                                self._lock_mode)
296
228
            self._lock_count += 1
316
248
                        
317
249
    def unlock(self):
318
250
        if not self._lock_mode:
 
251
            from errors import LockError
319
252
            raise LockError('branch %r is not locked' % (self))
320
253
 
321
254
        if self._lock_count > 1:
368
301
            raise BzrError("invalid controlfile mode %r" % mode)
369
302
 
370
303
    def _make_control(self):
 
304
        from bzrlib.inventory import Inventory
 
305
        from bzrlib.xml import pack_xml
 
306
        
371
307
        os.mkdir(self.controlfilename([]))
372
308
        self.controlfile('README', 'w').write(
373
309
            "This is a Bazaar-NG control directory.\n"
374
310
            "Do not change any files in this directory.\n")
375
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
376
 
        for d in ('text-store', 'revision-store',
377
 
                  'weaves'):
 
311
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
312
        for d in ('text-store', 'inventory-store', 'revision-store'):
378
313
            os.mkdir(self.controlfilename(d))
379
 
        for f in ('revision-history',
380
 
                  'branch-name',
 
314
        for f in ('revision-history', 'merged-patches',
 
315
                  'pending-merged-patches', 'branch-name',
381
316
                  'branch-lock',
382
317
                  'pending-merges'):
383
318
            self.controlfile(f, 'w').write('')
386
321
        # if we want per-tree root ids then this is the place to set
387
322
        # them; they're not needed for now and so ommitted for
388
323
        # simplicity.
389
 
        f = self.controlfile('inventory','w')
390
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
391
 
 
392
 
 
393
 
    def _check_format(self, relax_version_check):
 
324
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
325
 
 
326
    def _check_format(self):
394
327
        """Check this branch format is supported.
395
328
 
396
 
        The format level is stored, as an integer, in
397
 
        self._branch_format for code that needs to check it later.
 
329
        The current tool only supports the current unstable format.
398
330
 
399
331
        In the future, we might need different in-memory Branch
400
332
        classes to support downlevel branches.  But not yet.
401
333
        """
402
 
        try:
403
 
            fmt = self.controlfile('branch-format', 'r').read()
404
 
        except IOError, e:
405
 
            if e.errno == errno.ENOENT:
406
 
                raise NotBranchError(self.base)
407
 
            else:
408
 
                raise
409
 
 
410
 
        if fmt == BZR_BRANCH_FORMAT_5:
411
 
            self._branch_format = 5
412
 
        elif fmt == BZR_BRANCH_FORMAT_4:
413
 
            self._branch_format = 4
414
 
 
415
 
        if (not relax_version_check
416
 
            and self._branch_format != 5):
 
334
        # This ignores newlines so that we can open branches created
 
335
        # on Windows from Linux and so on.  I think it might be better
 
336
        # to always make all internal files in unix format.
 
337
        fmt = self.controlfile('branch-format', 'r').read()
 
338
        fmt.replace('\r\n', '')
 
339
        if fmt != BZR_BRANCH_FORMAT:
417
340
            raise BzrError('sorry, branch format %r not supported' % fmt,
418
341
                           ['use a different bzr version',
419
342
                            'or remove the .bzr directory and "bzr init" again'])
437
360
 
438
361
    def read_working_inventory(self):
439
362
        """Read the working inventory."""
 
363
        from bzrlib.inventory import Inventory
 
364
        from bzrlib.xml import unpack_xml
 
365
        from time import time
 
366
        before = time()
440
367
        self.lock_read()
441
368
        try:
442
369
            # ElementTree does its own conversion from UTF-8, so open in
443
370
            # binary.
444
 
            f = self.controlfile('inventory', 'rb')
445
 
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
371
            inv = unpack_xml(Inventory,
 
372
                             self.controlfile('inventory', 'rb'))
 
373
            mutter("loaded inventory of %d items in %f"
 
374
                   % (len(inv), time() - before))
 
375
            return inv
446
376
        finally:
447
377
            self.unlock()
448
378
            
454
384
        will be committed to the next revision.
455
385
        """
456
386
        from bzrlib.atomicfile import AtomicFile
 
387
        from bzrlib.xml import pack_xml
457
388
        
458
389
        self.lock_write()
459
390
        try:
460
391
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
461
392
            try:
462
 
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
 
393
                pack_xml(inv, f)
463
394
                f.commit()
464
395
            finally:
465
396
                f.close()
546
477
        """Print `file` to stdout."""
547
478
        self.lock_read()
548
479
        try:
549
 
            tree = self.revision_tree(self.get_rev_id(revno))
 
480
            tree = self.revision_tree(self.lookup_revision(revno))
550
481
            # use inventory as it was in that revision
551
482
            file_id = tree.inventory.path2id(file)
552
483
            if not file_id:
650
581
            f.close()
651
582
 
652
583
 
653
 
    def has_revision(self, revision_id):
654
 
        """True if this branch has a copy of the revision.
655
 
 
656
 
        This does not necessarily imply the revision is merge
657
 
        or on the mainline."""
658
 
        return (revision_id is None
659
 
                or revision_id in self.revision_store)
660
 
 
661
 
 
662
 
    def get_revision_xml_file(self, revision_id):
 
584
    def get_revision_xml(self, revision_id):
663
585
        """Return XML file object for revision object."""
664
586
        if not revision_id or not isinstance(revision_id, basestring):
665
587
            raise InvalidRevisionId(revision_id)
668
590
        try:
669
591
            try:
670
592
                return self.revision_store[revision_id]
671
 
            except (IndexError, KeyError):
 
593
            except IndexError:
672
594
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
673
595
        finally:
674
596
            self.unlock()
675
597
 
676
598
 
677
 
    def get_revision_xml(self, revision_id):
678
 
        return self.get_revision_xml_file(revision_id).read()
679
 
 
680
 
 
681
599
    def get_revision(self, revision_id):
682
600
        """Return the Revision object for a named revision"""
683
 
        xml_file = self.get_revision_xml_file(revision_id)
 
601
        xml_file = self.get_revision_xml(revision_id)
684
602
 
685
603
        try:
686
 
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
604
            r = unpack_xml(Revision, xml_file)
687
605
        except SyntaxError, e:
688
606
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
689
607
                                         [revision_id,
714
632
 
715
633
        return compare_trees(old_tree, new_tree)
716
634
 
 
635
        
717
636
 
718
637
    def get_revision_sha1(self, revision_id):
719
638
        """Hash the stored value of a revision, and return it."""
720
 
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
721
 
 
722
 
 
723
 
    def _get_ancestry_weave(self):
724
 
        return self.control_weaves.get_weave('ancestry')
725
 
        
726
 
 
727
 
    def get_ancestry(self, revision_id):
728
 
        """Return a list of revision-ids integrated by a revision.
729
 
        """
730
 
        # strip newlines
731
 
        if revision_id is None:
732
 
            return [None]
733
 
        w = self._get_ancestry_weave()
734
 
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
735
 
 
736
 
 
737
 
    def get_inventory_weave(self):
738
 
        return self.control_weaves.get_weave('inventory')
739
 
 
740
 
 
741
 
    def get_inventory(self, revision_id):
742
 
        """Get Inventory object by hash."""
743
 
        xml = self.get_inventory_xml(revision_id)
744
 
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
745
 
 
746
 
 
747
 
    def get_inventory_xml(self, revision_id):
 
639
        # In the future, revision entries will be signed. At that
 
640
        # point, it is probably best *not* to include the signature
 
641
        # in the revision hash. Because that lets you re-sign
 
642
        # the revision, (add signatures/remove signatures) and still
 
643
        # have all hash pointers stay consistent.
 
644
        # But for now, just hash the contents.
 
645
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
646
 
 
647
 
 
648
    def get_inventory(self, inventory_id):
 
649
        """Get Inventory object by hash.
 
650
 
 
651
        TODO: Perhaps for this and similar methods, take a revision
 
652
               parameter which can be either an integer revno or a
 
653
               string hash."""
 
654
        from bzrlib.inventory import Inventory
 
655
        from bzrlib.xml import unpack_xml
 
656
 
 
657
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
 
658
 
 
659
 
 
660
    def get_inventory_xml(self, inventory_id):
748
661
        """Get inventory XML as a file object."""
749
 
        try:
750
 
            assert isinstance(revision_id, basestring), type(revision_id)
751
 
            iw = self.get_inventory_weave()
752
 
            return iw.get_text(iw.lookup(revision_id))
753
 
        except IndexError:
754
 
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
755
 
 
756
 
 
757
 
    def get_inventory_sha1(self, revision_id):
 
662
        return self.inventory_store[inventory_id]
 
663
            
 
664
 
 
665
    def get_inventory_sha1(self, inventory_id):
758
666
        """Return the sha1 hash of the inventory entry
759
667
        """
760
 
        return self.get_revision(revision_id).inventory_sha1
 
668
        return sha_file(self.get_inventory_xml(inventory_id))
761
669
 
762
670
 
763
671
    def get_revision_inventory(self, revision_id):
764
672
        """Return inventory of a past revision."""
765
 
        # TODO: Unify this with get_inventory()
766
 
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
673
        # bzr 0.0.6 imposes the constraint that the inventory_id
767
674
        # must be the same as its revision, so this is trivial.
768
675
        if revision_id == None:
 
676
            from bzrlib.inventory import Inventory
769
677
            return Inventory(self.get_root_id())
770
678
        else:
771
679
            return self.get_inventory(revision_id)
772
680
 
773
681
 
774
682
    def revision_history(self):
775
 
        """Return sequence of revision hashes on to this branch."""
 
683
        """Return sequence of revision hashes on to this branch.
 
684
 
 
685
        >>> ScratchBranch().revision_history()
 
686
        []
 
687
        """
776
688
        self.lock_read()
777
689
        try:
778
690
            return [l.rstrip('\r\n') for l in
783
695
 
784
696
    def common_ancestor(self, other, self_revno=None, other_revno=None):
785
697
        """
786
 
        >>> from bzrlib.commit import commit
 
698
        >>> import commit
787
699
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
788
700
        >>> sb.common_ancestor(sb) == (None, None)
789
701
        True
790
 
        >>> commit(sb, "Committing first revision", verbose=False)
 
702
        >>> commit.commit(sb, "Committing first revision", verbose=False)
791
703
        >>> sb.common_ancestor(sb)[0]
792
704
        1
793
705
        >>> clone = sb.clone()
794
 
        >>> commit(sb, "Committing second revision", verbose=False)
 
706
        >>> commit.commit(sb, "Committing second revision", verbose=False)
795
707
        >>> sb.common_ancestor(sb)[0]
796
708
        2
797
709
        >>> sb.common_ancestor(clone)[0]
798
710
        1
799
 
        >>> commit(clone, "Committing divergent second revision", 
 
711
        >>> commit.commit(clone, "Committing divergent second revision", 
800
712
        ...               verbose=False)
801
713
        >>> sb.common_ancestor(clone)[0]
802
714
        1
835
747
        return len(self.revision_history())
836
748
 
837
749
 
838
 
    def last_revision(self):
 
750
    def last_patch(self):
839
751
        """Return last patch hash, or None if no history.
840
752
        """
841
753
        ph = self.revision_history()
846
758
 
847
759
 
848
760
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
849
 
        """Return a list of new revisions that would perfectly fit.
850
 
        
 
761
        """
851
762
        If self and other have not diverged, return a list of the revisions
852
763
        present in other, but missing from self.
853
764
 
873
784
        Traceback (most recent call last):
874
785
        DivergedBranches: These branches have diverged.
875
786
        """
876
 
        # FIXME: If the branches have diverged, but the latest
877
 
        # revision in this branch is completely merged into the other,
878
 
        # then we should still be able to pull.
879
787
        self_history = self.revision_history()
880
788
        self_len = len(self_history)
881
789
        other_history = other.revision_history()
887
795
 
888
796
        if stop_revision is None:
889
797
            stop_revision = other_len
890
 
        else:
891
 
            assert isinstance(stop_revision, int)
892
 
            if stop_revision > other_len:
893
 
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
798
        elif stop_revision > other_len:
 
799
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
800
        
894
801
        return other_history[self_len:stop_revision]
895
802
 
 
803
 
896
804
    def update_revisions(self, other, stop_revision=None):
897
 
        """Pull in new perfect-fit revisions."""
 
805
        """Pull in all new revisions from other branch.
 
806
        """
898
807
        from bzrlib.fetch import greedy_fetch
899
 
        from bzrlib.revision import get_intervening_revisions
900
 
        if stop_revision is None:
901
 
            stop_revision = other.last_revision()
902
 
        greedy_fetch(to_branch=self, from_branch=other,
903
 
                     revision=stop_revision)
904
 
        pullable_revs = self.missing_revisions(
905
 
            other, other.revision_id_to_revno(stop_revision))
906
 
        if pullable_revs:
907
 
            greedy_fetch(to_branch=self,
908
 
                         from_branch=other,
909
 
                         revision=pullable_revs[-1])
910
 
            self.append_revision(*pullable_revs)
911
 
    
 
808
 
 
809
        pb = bzrlib.ui.ui_factory.progress_bar()
 
810
        pb.update('comparing histories')
 
811
 
 
812
        revision_ids = self.missing_revisions(other, stop_revision)
 
813
 
 
814
        if len(revision_ids) > 0:
 
815
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
816
        else:
 
817
            count = 0
 
818
        self.append_revision(*revision_ids)
 
819
        ## note("Added %d revisions." % count)
 
820
        pb.clear()
 
821
 
 
822
    def install_revisions(self, other, revision_ids, pb):
 
823
        if hasattr(other.revision_store, "prefetch"):
 
824
            other.revision_store.prefetch(revision_ids)
 
825
        if hasattr(other.inventory_store, "prefetch"):
 
826
            inventory_ids = [other.get_revision(r).inventory_id
 
827
                             for r in revision_ids]
 
828
            other.inventory_store.prefetch(inventory_ids)
 
829
 
 
830
        if pb is None:
 
831
            pb = bzrlib.ui.ui_factory.progress_bar()
 
832
                
 
833
        revisions = []
 
834
        needed_texts = set()
 
835
        i = 0
 
836
 
 
837
        failures = set()
 
838
        for i, rev_id in enumerate(revision_ids):
 
839
            pb.update('fetching revision', i+1, len(revision_ids))
 
840
            try:
 
841
                rev = other.get_revision(rev_id)
 
842
            except bzrlib.errors.NoSuchRevision:
 
843
                failures.add(rev_id)
 
844
                continue
 
845
 
 
846
            revisions.append(rev)
 
847
            inv = other.get_inventory(str(rev.inventory_id))
 
848
            for key, entry in inv.iter_entries():
 
849
                if entry.text_id is None:
 
850
                    continue
 
851
                if entry.text_id not in self.text_store:
 
852
                    needed_texts.add(entry.text_id)
 
853
 
 
854
        pb.clear()
 
855
                    
 
856
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
857
                                                    needed_texts)
 
858
        #print "Added %d texts." % count 
 
859
        inventory_ids = [ f.inventory_id for f in revisions ]
 
860
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
861
                                                         inventory_ids)
 
862
        #print "Added %d inventories." % count 
 
863
        revision_ids = [ f.revision_id for f in revisions]
 
864
 
 
865
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
866
                                                          revision_ids,
 
867
                                                          permit_failure=True)
 
868
        assert len(cp_fail) == 0 
 
869
        return count, failures
 
870
       
912
871
 
913
872
    def commit(self, *args, **kw):
914
 
        from bzrlib.commit import Commit
915
 
        Commit().commit(self, *args, **kw)
916
 
    
 
873
        from bzrlib.commit import commit
 
874
        commit(self, *args, **kw)
 
875
        
 
876
 
 
877
    def lookup_revision(self, revision):
 
878
        """Return the revision identifier for a given revision information."""
 
879
        revno, info = self.get_revision_info(revision)
 
880
        return info
 
881
 
 
882
 
917
883
    def revision_id_to_revno(self, revision_id):
918
884
        """Given a revision id, return its revno"""
919
 
        if revision_id is None:
920
 
            return 0
921
885
        history = self.revision_history()
922
886
        try:
923
887
            return history.index(revision_id) + 1
924
888
        except ValueError:
925
889
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
926
890
 
927
 
    def get_rev_id(self, revno, history=None):
928
 
        """Find the revision id of the specified revno."""
929
 
        if revno == 0:
930
 
            return None
931
 
        if history is None:
932
 
            history = self.revision_history()
933
 
        elif revno <= 0 or revno > len(history):
934
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
935
 
        return history[revno - 1]
 
891
 
 
892
    def get_revision_info(self, revision):
 
893
        """Return (revno, revision id) for revision identifier.
 
894
 
 
895
        revision can be an integer, in which case it is assumed to be revno (though
 
896
            this will translate negative values into positive ones)
 
897
        revision can also be a string, in which case it is parsed for something like
 
898
            'date:' or 'revid:' etc.
 
899
        """
 
900
        if revision is None:
 
901
            return 0, None
 
902
        revno = None
 
903
        try:# Convert to int if possible
 
904
            revision = int(revision)
 
905
        except ValueError:
 
906
            pass
 
907
        revs = self.revision_history()
 
908
        if isinstance(revision, int):
 
909
            if revision == 0:
 
910
                return 0, None
 
911
            # Mabye we should do this first, but we don't need it if revision == 0
 
912
            if revision < 0:
 
913
                revno = len(revs) + revision + 1
 
914
            else:
 
915
                revno = revision
 
916
        elif isinstance(revision, basestring):
 
917
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
918
                if revision.startswith(prefix):
 
919
                    revno = func(self, revs, revision)
 
920
                    break
 
921
            else:
 
922
                raise BzrError('No namespace registered for string: %r' % revision)
 
923
 
 
924
        if revno is None or revno <= 0 or revno > len(revs):
 
925
            raise BzrError("no such revision %s" % revision)
 
926
        return revno, revs[revno-1]
 
927
 
 
928
    def _namespace_revno(self, revs, revision):
 
929
        """Lookup a revision by revision number"""
 
930
        assert revision.startswith('revno:')
 
931
        try:
 
932
            return int(revision[6:])
 
933
        except ValueError:
 
934
            return None
 
935
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
936
 
 
937
    def _namespace_revid(self, revs, revision):
 
938
        assert revision.startswith('revid:')
 
939
        try:
 
940
            return revs.index(revision[6:]) + 1
 
941
        except ValueError:
 
942
            return None
 
943
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
944
 
 
945
    def _namespace_last(self, revs, revision):
 
946
        assert revision.startswith('last:')
 
947
        try:
 
948
            offset = int(revision[5:])
 
949
        except ValueError:
 
950
            return None
 
951
        else:
 
952
            if offset <= 0:
 
953
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
954
            return len(revs) - offset + 1
 
955
    REVISION_NAMESPACES['last:'] = _namespace_last
 
956
 
 
957
    def _namespace_tag(self, revs, revision):
 
958
        assert revision.startswith('tag:')
 
959
        raise BzrError('tag: namespace registered, but not implemented.')
 
960
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
961
 
 
962
    def _namespace_date(self, revs, revision):
 
963
        assert revision.startswith('date:')
 
964
        import datetime
 
965
        # Spec for date revisions:
 
966
        #   date:value
 
967
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
968
        #   it can also start with a '+/-/='. '+' says match the first
 
969
        #   entry after the given date. '-' is match the first entry before the date
 
970
        #   '=' is match the first entry after, but still on the given date.
 
971
        #
 
972
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
973
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
974
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
975
        #       May 13th, 2005 at 0:00
 
976
        #
 
977
        #   So the proper way of saying 'give me all entries for today' is:
 
978
        #       -r {date:+today}:{date:-tomorrow}
 
979
        #   The default is '=' when not supplied
 
980
        val = revision[5:]
 
981
        match_style = '='
 
982
        if val[:1] in ('+', '-', '='):
 
983
            match_style = val[:1]
 
984
            val = val[1:]
 
985
 
 
986
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
987
        if val.lower() == 'yesterday':
 
988
            dt = today - datetime.timedelta(days=1)
 
989
        elif val.lower() == 'today':
 
990
            dt = today
 
991
        elif val.lower() == 'tomorrow':
 
992
            dt = today + datetime.timedelta(days=1)
 
993
        else:
 
994
            import re
 
995
            # This should be done outside the function to avoid recompiling it.
 
996
            _date_re = re.compile(
 
997
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
998
                    r'(,|T)?\s*'
 
999
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1000
                )
 
1001
            m = _date_re.match(val)
 
1002
            if not m or (not m.group('date') and not m.group('time')):
 
1003
                raise BzrError('Invalid revision date %r' % revision)
 
1004
 
 
1005
            if m.group('date'):
 
1006
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1007
            else:
 
1008
                year, month, day = today.year, today.month, today.day
 
1009
            if m.group('time'):
 
1010
                hour = int(m.group('hour'))
 
1011
                minute = int(m.group('minute'))
 
1012
                if m.group('second'):
 
1013
                    second = int(m.group('second'))
 
1014
                else:
 
1015
                    second = 0
 
1016
            else:
 
1017
                hour, minute, second = 0,0,0
 
1018
 
 
1019
            dt = datetime.datetime(year=year, month=month, day=day,
 
1020
                    hour=hour, minute=minute, second=second)
 
1021
        first = dt
 
1022
        last = None
 
1023
        reversed = False
 
1024
        if match_style == '-':
 
1025
            reversed = True
 
1026
        elif match_style == '=':
 
1027
            last = dt + datetime.timedelta(days=1)
 
1028
 
 
1029
        if reversed:
 
1030
            for i in range(len(revs)-1, -1, -1):
 
1031
                r = self.get_revision(revs[i])
 
1032
                # TODO: Handle timezone.
 
1033
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1034
                if first >= dt and (last is None or dt >= last):
 
1035
                    return i+1
 
1036
        else:
 
1037
            for i in range(len(revs)):
 
1038
                r = self.get_revision(revs[i])
 
1039
                # TODO: Handle timezone.
 
1040
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1041
                if first <= dt and (last is None or dt <= last):
 
1042
                    return i+1
 
1043
    REVISION_NAMESPACES['date:'] = _namespace_date
936
1044
 
937
1045
    def revision_tree(self, revision_id):
938
1046
        """Return Tree for a revision on this branch.
945
1053
            return EmptyTree()
946
1054
        else:
947
1055
            inv = self.get_revision_inventory(revision_id)
948
 
            return RevisionTree(self.weave_store, inv, revision_id)
 
1056
            return RevisionTree(self.text_store, inv)
949
1057
 
950
1058
 
951
1059
    def working_tree(self):
952
1060
        """Return a `Tree` for the working copy."""
953
 
        from bzrlib.workingtree import WorkingTree
 
1061
        from workingtree import WorkingTree
954
1062
        return WorkingTree(self.base, self.read_working_inventory())
955
1063
 
956
1064
 
959
1067
 
960
1068
        If there are no revisions yet, return an `EmptyTree`.
961
1069
        """
962
 
        return self.revision_tree(self.last_revision())
 
1070
        r = self.last_patch()
 
1071
        if r == None:
 
1072
            return EmptyTree()
 
1073
        else:
 
1074
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
1075
 
963
1076
 
964
1077
 
965
1078
    def rename_one(self, from_rel, to_rel):
1000
1113
            from_abs = self.abspath(from_rel)
1001
1114
            to_abs = self.abspath(to_rel)
1002
1115
            try:
1003
 
                rename(from_abs, to_abs)
 
1116
                os.rename(from_abs, to_abs)
1004
1117
            except OSError, e:
1005
1118
                raise BzrError("failed to rename %r to %r: %s"
1006
1119
                        % (from_abs, to_abs, e[1]),
1069
1182
                result.append((f, dest_path))
1070
1183
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1071
1184
                try:
1072
 
                    rename(self.abspath(f), self.abspath(dest_path))
 
1185
                    os.rename(self.abspath(f), self.abspath(dest_path))
1073
1186
                except OSError, e:
1074
1187
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1075
1188
                            ["rename rolled back"])
1141
1254
 
1142
1255
 
1143
1256
    def add_pending_merge(self, revision_id):
 
1257
        from bzrlib.revision import validate_revision_id
 
1258
 
1144
1259
        validate_revision_id(revision_id)
1145
 
        # TODO: Perhaps should check at this point that the
1146
 
        # history of the revision is actually present?
 
1260
 
1147
1261
        p = self.pending_merges()
1148
1262
        if revision_id in p:
1149
1263
            return
1198
1312
        finally:
1199
1313
            self.unlock()
1200
1314
 
1201
 
    def check_revno(self, revno):
1202
 
        """\
1203
 
        Check whether a revno corresponds to any revision.
1204
 
        Zero (the NULL revision) is considered valid.
1205
 
        """
1206
 
        if revno != 0:
1207
 
            self.check_real_revno(revno)
1208
 
            
1209
 
    def check_real_revno(self, revno):
1210
 
        """\
1211
 
        Check whether a revno corresponds to a real revision.
1212
 
        Zero (the NULL revision) is considered invalid
1213
 
        """
1214
 
        if revno < 1 or revno > self.revno():
1215
 
            raise InvalidRevisionNumber(revno)
1216
 
        
1217
 
        
1218
 
        
1219
 
 
1220
 
 
1221
 
class ScratchBranch(LocalBranch):
 
1315
        
 
1316
 
 
1317
 
 
1318
class ScratchBranch(Branch):
1222
1319
    """Special test class: a branch that cleans up after itself.
1223
1320
 
1224
1321
    >>> b = ScratchBranch()
1241
1338
        if base is None:
1242
1339
            base = mkdtemp()
1243
1340
            init = True
1244
 
        LocalBranch.__init__(self, base, init=init)
 
1341
        Branch.__init__(self, base, init=init)
1245
1342
        for d in dirs:
1246
1343
            os.mkdir(self.abspath(d))
1247
1344
            
1253
1350
        """
1254
1351
        >>> orig = ScratchBranch(files=["file1", "file2"])
1255
1352
        >>> clone = orig.clone()
1256
 
        >>> if os.name != 'nt':
1257
 
        ...   os.path.samefile(orig.base, clone.base)
1258
 
        ... else:
1259
 
        ...   orig.base == clone.base
1260
 
        ...
 
1353
        >>> os.path.samefile(orig.base, clone.base)
1261
1354
        False
1262
1355
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1263
1356
        True
1346
1439
    return gen_file_id('TREE_ROOT')
1347
1440
 
1348
1441
 
 
1442
def pull_loc(branch):
 
1443
    # TODO: Should perhaps just make attribute be 'base' in
 
1444
    # RemoteBranch and Branch?
 
1445
    if hasattr(branch, "baseurl"):
 
1446
        return branch.baseurl
 
1447
    else:
 
1448
        return branch.base
 
1449
 
 
1450
 
 
1451
def copy_branch(branch_from, to_location, revision=None):
 
1452
    """Copy branch_from into the existing directory to_location.
 
1453
 
 
1454
    revision
 
1455
        If not None, only revisions up to this point will be copied.
 
1456
        The head of the new branch will be that revision.
 
1457
 
 
1458
    to_location
 
1459
        The name of a local directory that exists but is empty.
 
1460
    """
 
1461
    from bzrlib.merge import merge
 
1462
    from bzrlib.branch import Branch
 
1463
 
 
1464
    assert isinstance(branch_from, Branch)
 
1465
    assert isinstance(to_location, basestring)
 
1466
    
 
1467
    br_to = Branch(to_location, init=True)
 
1468
    br_to.set_root_id(branch_from.get_root_id())
 
1469
    if revision is None:
 
1470
        revno = branch_from.revno()
 
1471
    else:
 
1472
        revno, rev_id = branch_from.get_revision_info(revision)
 
1473
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1474
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1475
          check_clean=False, ignore_zero=True)
 
1476
    
 
1477
    from_location = pull_loc(branch_from)
 
1478
    br_to.set_parent(pull_loc(branch_from))
 
1479