~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-07-04 11:57:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050704115718-b532986c0714e7a7
- don't write precursor field in new revision xml
- make parents more primary; remove more precursor code
- test commit of revision with parents

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys
19
 
import os
20
 
from cStringIO import StringIO
 
18
import sys, os
21
19
 
22
20
import bzrlib
23
21
from bzrlib.trace import mutter, note
24
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
 
     splitpath, \
 
22
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
26
23
     sha_file, appendpath, file_kind
27
 
 
28
 
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
29
 
                           NoSuchRevision, HistoryMissing, NotBranchError,
30
 
                           LockError)
31
 
from bzrlib.textui import show_status
32
 
from bzrlib.revision import Revision, validate_revision_id
33
 
from bzrlib.delta import compare_trees
34
 
from bzrlib.tree import EmptyTree, RevisionTree
35
 
from bzrlib.inventory import Inventory
36
 
from bzrlib.weavestore import WeaveStore
37
 
from bzrlib.store import ImmutableStore
38
 
import bzrlib.xml5
39
 
import bzrlib.ui
40
 
 
41
 
 
42
 
INVENTORY_FILEID = '__inventory'
43
 
ANCESTRY_FILEID = '__ancestry'
44
 
 
45
 
 
46
 
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
47
 
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
24
from bzrlib.errors import BzrError
 
25
 
 
26
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
48
27
## TODO: Maybe include checks for common corruption of newlines, etc?
49
28
 
50
29
 
51
 
# TODO: Some operations like log might retrieve the same revisions
52
 
# repeatedly to calculate deltas.  We could perhaps have a weakref
53
 
# cache in memory to make this faster.  In general anything can be
54
 
# cached in memory between lock and unlock operations.
55
 
 
56
 
# TODO: please move the revision-string syntax stuff out of the branch
57
 
# object; it's clutter
58
 
 
59
30
 
60
31
def find_branch(f, **args):
61
32
    if f and (f.startswith('http://') or f.startswith('https://')):
104
75
        if tail:
105
76
            s.insert(0, tail)
106
77
    else:
 
78
        from errors import NotBranchError
107
79
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
108
80
 
109
81
    return os.sep.join(s)
117
89
    It is not necessary that f exists.
118
90
 
119
91
    Basically we keep looking up until we find the control directory or
120
 
    run into the root.  If there isn't one, raises NotBranchError.
121
 
    """
 
92
    run into the root."""
122
93
    if f == None:
123
94
        f = os.getcwd()
124
95
    elif hasattr(os.path, 'realpath'):
137
108
        head, tail = os.path.split(f)
138
109
        if head == f:
139
110
            # reached the root, whatever that may be
140
 
            raise NotBranchError('%s is not in a branch' % orig_f)
 
111
            raise BzrError('%r is not in a branch' % orig_f)
141
112
        f = head
142
 
 
143
 
 
144
 
 
145
 
# XXX: move into bzrlib.errors; subclass BzrError    
 
113
    
146
114
class DivergedBranches(Exception):
147
115
    def __init__(self, branch1, branch2):
148
116
        self.branch1 = branch1
150
118
        Exception.__init__(self, "These branches have diverged.")
151
119
 
152
120
 
 
121
class NoSuchRevision(BzrError):
 
122
    def __init__(self, branch, revision):
 
123
        self.branch = branch
 
124
        self.revision = revision
 
125
        msg = "Branch %s has no revision %d" % (branch, revision)
 
126
        BzrError.__init__(self, msg)
 
127
 
 
128
 
153
129
######################################################################
154
130
# branch objects
155
131
 
173
149
    _lock_mode = None
174
150
    _lock_count = None
175
151
    _lock = None
176
 
    _inventory_weave = None
177
152
    
178
 
    # Map some sort of prefix into a namespace
179
 
    # stuff like "revno:10", "revid:", etc.
180
 
    # This should match a prefix with a function which accepts
181
 
    REVISION_NAMESPACES = {}
182
 
 
183
 
    def __init__(self, base, init=False, find_root=True,
184
 
                 relax_version_check=False):
 
153
    def __init__(self, base, init=False, find_root=True):
185
154
        """Create new branch object at a particular location.
186
155
 
187
156
        base -- Base directory for the branch.
193
162
        find_root -- If true and init is false, find the root of the
194
163
             existing branch containing base.
195
164
 
196
 
        relax_version_check -- If true, the usual check for the branch
197
 
            version is not applied.  This is intended only for
198
 
            upgrade/recovery type use; it's not guaranteed that
199
 
            all operations will work on old format branches.
200
 
 
201
165
        In the test suite, creation of new trees is tested using the
202
166
        `ScratchBranch` class.
203
167
        """
 
168
        from bzrlib.store import ImmutableStore
204
169
        if init:
205
170
            self.base = os.path.realpath(base)
206
171
            self._make_control()
209
174
        else:
210
175
            self.base = os.path.realpath(base)
211
176
            if not isdir(self.controlfilename('.')):
212
 
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
213
 
                                     ['use "bzr init" to initialize a '
214
 
                                      'new working tree'])
215
 
        
216
 
        self._check_format(relax_version_check)
217
 
        if self._branch_format == 4:
218
 
            self.inventory_store = \
219
 
                ImmutableStore(self.controlfilename('inventory-store'))
220
 
            self.text_store = \
221
 
                ImmutableStore(self.controlfilename('text-store'))
222
 
        self.weave_store = WeaveStore(self.controlfilename('weaves'))
223
 
        self.revision_store = \
224
 
            ImmutableStore(self.controlfilename('revision-store'))
 
177
                from errors import NotBranchError
 
178
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
179
                                     ['use "bzr init" to initialize a new working tree',
 
180
                                      'current bzr can only operate from top-of-tree'])
 
181
        self._check_format()
 
182
 
 
183
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
184
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
185
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
225
186
 
226
187
 
227
188
    def __str__(self):
238
199
            self._lock.unlock()
239
200
 
240
201
 
 
202
 
241
203
    def lock_write(self):
242
204
        if self._lock_mode:
243
205
            if self._lock_mode != 'w':
 
206
                from errors import LockError
244
207
                raise LockError("can't upgrade to a write lock from %r" %
245
208
                                self._lock_mode)
246
209
            self._lock_count += 1
252
215
            self._lock_count = 1
253
216
 
254
217
 
 
218
 
255
219
    def lock_read(self):
256
220
        if self._lock_mode:
257
221
            assert self._lock_mode in ('r', 'w'), \
264
228
            self._lock_mode = 'r'
265
229
            self._lock_count = 1
266
230
                        
 
231
 
 
232
            
267
233
    def unlock(self):
268
234
        if not self._lock_mode:
 
235
            from errors import LockError
269
236
            raise LockError('branch %r is not locked' % (self))
270
237
 
271
238
        if self._lock_count > 1:
275
242
            self._lock = None
276
243
            self._lock_mode = self._lock_count = None
277
244
 
 
245
 
278
246
    def abspath(self, name):
279
247
        """Return absolute filename for something in the branch"""
280
248
        return os.path.join(self.base, name)
281
249
 
 
250
 
282
251
    def relpath(self, path):
283
252
        """Return path relative to this branch of something inside it.
284
253
 
285
254
        Raises an error if path is not in this branch."""
286
255
        return _relpath(self.base, path)
287
256
 
 
257
 
288
258
    def controlfilename(self, file_or_path):
289
259
        """Return location relative to branch."""
290
260
        if isinstance(file_or_path, basestring):
317
287
        else:
318
288
            raise BzrError("invalid controlfile mode %r" % mode)
319
289
 
 
290
 
 
291
 
320
292
    def _make_control(self):
 
293
        from bzrlib.inventory import Inventory
 
294
        from bzrlib.xml import pack_xml
 
295
        
321
296
        os.mkdir(self.controlfilename([]))
322
297
        self.controlfile('README', 'w').write(
323
298
            "This is a Bazaar-NG control directory.\n"
324
299
            "Do not change any files in this directory.\n")
325
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
326
 
        for d in ('text-store', 'revision-store',
327
 
                  'weaves'):
 
300
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
301
        for d in ('text-store', 'inventory-store', 'revision-store'):
328
302
            os.mkdir(self.controlfilename(d))
329
303
        for f in ('revision-history', 'merged-patches',
330
304
                  'pending-merged-patches', 'branch-name',
333
307
            self.controlfile(f, 'w').write('')
334
308
        mutter('created control directory in ' + self.base)
335
309
 
336
 
        # if we want per-tree root ids then this is the place to set
337
 
        # them; they're not needed for now and so ommitted for
338
 
        # simplicity.
339
 
        f = self.controlfile('inventory','w')
340
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
341
 
        
342
 
 
343
 
 
344
 
    def _check_format(self, relax_version_check):
 
310
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
311
 
 
312
 
 
313
    def _check_format(self):
345
314
        """Check this branch format is supported.
346
315
 
347
 
        The format level is stored, as an integer, in
348
 
        self._branch_format for code that needs to check it later.
 
316
        The current tool only supports the current unstable format.
349
317
 
350
318
        In the future, we might need different in-memory Branch
351
319
        classes to support downlevel branches.  But not yet.
352
320
        """
 
321
        # This ignores newlines so that we can open branches created
 
322
        # on Windows from Linux and so on.  I think it might be better
 
323
        # to always make all internal files in unix format.
353
324
        fmt = self.controlfile('branch-format', 'r').read()
354
 
        if fmt == BZR_BRANCH_FORMAT_5:
355
 
            self._branch_format = 5
356
 
        elif fmt == BZR_BRANCH_FORMAT_4:
357
 
            self._branch_format = 4
358
 
 
359
 
        if (not relax_version_check
360
 
            and self._branch_format != 5):
361
 
            raise BzrError('sorry, branch format "%s" not supported; ' 
362
 
                           'use a different bzr version, '
363
 
                           'or run "bzr upgrade"'
364
 
                           % fmt.rstrip('\n\r'))
365
 
        
366
 
 
367
 
    def get_root_id(self):
368
 
        """Return the id of this branches root"""
369
 
        inv = self.read_working_inventory()
370
 
        return inv.root.file_id
371
 
 
372
 
    def set_root_id(self, file_id):
373
 
        inv = self.read_working_inventory()
374
 
        orig_root_id = inv.root.file_id
375
 
        del inv._byid[inv.root.file_id]
376
 
        inv.root.file_id = file_id
377
 
        inv._byid[inv.root.file_id] = inv.root
378
 
        for fid in inv:
379
 
            entry = inv[fid]
380
 
            if entry.parent_id in (None, orig_root_id):
381
 
                entry.parent_id = inv.root.file_id
382
 
        self._write_inventory(inv)
 
325
        fmt.replace('\r\n', '')
 
326
        if fmt != BZR_BRANCH_FORMAT:
 
327
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
328
                           ['use a different bzr version',
 
329
                            'or remove the .bzr directory and "bzr init" again'])
 
330
 
 
331
 
383
332
 
384
333
    def read_working_inventory(self):
385
334
        """Read the working inventory."""
 
335
        from bzrlib.inventory import Inventory
 
336
        from bzrlib.xml import unpack_xml
 
337
        from time import time
 
338
        before = time()
386
339
        self.lock_read()
387
340
        try:
388
341
            # ElementTree does its own conversion from UTF-8, so open in
389
342
            # binary.
390
 
            f = self.controlfile('inventory', 'rb')
391
 
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
343
            inv = unpack_xml(Inventory,
 
344
                                  self.controlfile('inventory', 'rb'))
 
345
            mutter("loaded inventory of %d items in %f"
 
346
                   % (len(inv), time() - before))
 
347
            return inv
392
348
        finally:
393
349
            self.unlock()
394
350
            
400
356
        will be committed to the next revision.
401
357
        """
402
358
        from bzrlib.atomicfile import AtomicFile
 
359
        from bzrlib.xml import pack_xml
403
360
        
404
361
        self.lock_write()
405
362
        try:
406
363
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
407
364
            try:
408
 
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
 
365
                pack_xml(inv, f)
409
366
                f.commit()
410
367
            finally:
411
368
                f.close()
419
376
                         """Inventory for the working copy.""")
420
377
 
421
378
 
422
 
    def add(self, files, ids=None):
 
379
    def add(self, files, verbose=False, ids=None):
423
380
        """Make files versioned.
424
381
 
425
 
        Note that the command line normally calls smart_add instead,
426
 
        which can automatically recurse.
 
382
        Note that the command line normally calls smart_add instead.
427
383
 
428
384
        This puts the files in the Added state, so that they will be
429
385
        recorded by the next commit.
439
395
        TODO: Perhaps have an option to add the ids even if the files do
440
396
              not (yet) exist.
441
397
 
442
 
        TODO: Perhaps yield the ids and paths as they're added.
 
398
        TODO: Perhaps return the ids of the files?  But then again it
 
399
              is easy to retrieve them if they're needed.
 
400
 
 
401
        TODO: Adding a directory should optionally recurse down and
 
402
              add all non-ignored children.  Perhaps do that in a
 
403
              higher-level method.
443
404
        """
 
405
        from bzrlib.textui import show_status
444
406
        # TODO: Re-adding a file that is removed in the working copy
445
407
        # should probably put it back with the previous ID.
446
408
        if isinstance(files, basestring):
481
443
                    file_id = gen_file_id(f)
482
444
                inv.add_path(f, kind=kind, file_id=file_id)
483
445
 
 
446
                if verbose:
 
447
                    print 'added', quotefn(f)
 
448
 
484
449
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
485
450
 
486
451
            self._write_inventory(inv)
496
461
            # use inventory as it was in that revision
497
462
            file_id = tree.inventory.path2id(file)
498
463
            if not file_id:
499
 
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
464
                raise BzrError("%r is not present in revision %d" % (file, revno))
500
465
            tree.print_file(file_id)
501
466
        finally:
502
467
            self.unlock()
516
481
        is the opposite of add.  Removing it is consistent with most
517
482
        other tools.  Maybe an option.
518
483
        """
 
484
        from bzrlib.textui import show_status
519
485
        ## TODO: Normalize names
520
486
        ## TODO: Remove nested loops; better scalability
521
487
        if isinstance(files, basestring):
550
516
    # FIXME: this doesn't need to be a branch method
551
517
    def set_inventory(self, new_inventory_list):
552
518
        from bzrlib.inventory import Inventory, InventoryEntry
553
 
        inv = Inventory(self.get_root_id())
 
519
        inv = Inventory()
554
520
        for path, file_id, parent, kind in new_inventory_list:
555
521
            name = os.path.basename(path)
556
522
            if name == "":
578
544
        return self.working_tree().unknowns()
579
545
 
580
546
 
581
 
    def append_revision(self, *revision_ids):
 
547
    def append_revision(self, revision_id):
582
548
        from bzrlib.atomicfile import AtomicFile
583
549
 
584
 
        for revision_id in revision_ids:
585
 
            mutter("add {%s} to revision-history" % revision_id)
586
 
 
587
 
        rev_history = self.revision_history()
588
 
        rev_history.extend(revision_ids)
 
550
        mutter("add {%s} to revision-history" % revision_id)
 
551
        rev_history = self.revision_history() + [revision_id]
589
552
 
590
553
        f = AtomicFile(self.controlfilename('revision-history'))
591
554
        try:
596
559
            f.close()
597
560
 
598
561
 
599
 
    def has_revision(self, revision_id):
600
 
        """True if this branch has a copy of the revision.
601
 
 
602
 
        This does not necessarily imply the revision is merge
603
 
        or on the mainline."""
604
 
        return revision_id in self.revision_store
605
 
 
606
 
 
607
 
    def get_revision_xml_file(self, revision_id):
608
 
        """Return XML file object for revision object."""
609
 
        if not revision_id or not isinstance(revision_id, basestring):
610
 
            raise InvalidRevisionId(revision_id)
611
 
 
612
 
        self.lock_read()
613
 
        try:
614
 
            try:
615
 
                return self.revision_store[revision_id]
616
 
            except IndexError:
617
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
618
 
        finally:
619
 
            self.unlock()
620
 
 
621
 
 
622
 
    def get_revision_xml(self, revision_id):
623
 
        return self.get_revision_xml_file(revision_id).read()
624
 
 
625
 
 
626
562
    def get_revision(self, revision_id):
627
563
        """Return the Revision object for a named revision"""
628
 
        xml_file = self.get_revision_xml_file(revision_id)
 
564
        from bzrlib.revision import Revision
 
565
        from bzrlib.xml import unpack_xml
629
566
 
 
567
        self.lock_read()
630
568
        try:
631
 
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
632
 
        except SyntaxError, e:
633
 
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
634
 
                                         [revision_id,
635
 
                                          str(e)])
 
569
            if not revision_id or not isinstance(revision_id, basestring):
 
570
                raise ValueError('invalid revision-id: %r' % revision_id)
 
571
            r = unpack_xml(Revision, self.revision_store[revision_id])
 
572
        finally:
 
573
            self.unlock()
636
574
            
637
575
        assert r.revision_id == revision_id
638
576
        return r
639
 
 
640
 
 
641
 
    def get_revision_delta(self, revno):
642
 
        """Return the delta for one revision.
643
 
 
644
 
        The delta is relative to its mainline predecessor, or the
645
 
        empty tree for revision 1.
646
 
        """
647
 
        assert isinstance(revno, int)
648
 
        rh = self.revision_history()
649
 
        if not (1 <= revno <= len(rh)):
650
 
            raise InvalidRevisionNumber(revno)
651
 
 
652
 
        # revno is 1-based; list is 0-based
653
 
 
654
 
        new_tree = self.revision_tree(rh[revno-1])
655
 
        if revno == 1:
656
 
            old_tree = EmptyTree()
657
 
        else:
658
 
            old_tree = self.revision_tree(rh[revno-2])
659
 
 
660
 
        return compare_trees(old_tree, new_tree)
661
 
 
662
577
        
663
578
 
664
579
    def get_revision_sha1(self, revision_id):
665
580
        """Hash the stored value of a revision, and return it."""
666
 
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
667
 
 
668
 
 
669
 
    def get_ancestry(self, revision_id):
670
 
        """Return a list of revision-ids integrated by a revision.
671
 
        """
672
 
        w = self.weave_store.get_weave(ANCESTRY_FILEID)
673
 
        # strip newlines
674
 
        return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
675
 
 
676
 
 
677
 
    def get_inventory_weave(self):
678
 
        return self.weave_store.get_weave(INVENTORY_FILEID)
679
 
 
680
 
 
681
 
    def get_inventory(self, revision_id):
682
 
        """Get Inventory object by hash."""
683
 
        # FIXME: The text gets passed around a lot coming from the weave.
684
 
        f = StringIO(self.get_inventory_xml(revision_id))
685
 
        return bzrlib.xml5.serializer_v5.read_inventory(f)
686
 
 
687
 
 
688
 
    def get_inventory_xml(self, revision_id):
689
 
        """Get inventory XML as a file object."""
690
 
        try:
691
 
            assert isinstance(revision_id, basestring), type(revision_id)
692
 
            iw = self.get_inventory_weave()
693
 
            return iw.get_text(iw.lookup(revision_id))
694
 
        except IndexError:
695
 
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
696
 
 
697
 
 
698
 
    def get_inventory_sha1(self, revision_id):
 
581
        # In the future, revision entries will be signed. At that
 
582
        # point, it is probably best *not* to include the signature
 
583
        # in the revision hash. Because that lets you re-sign
 
584
        # the revision, (add signatures/remove signatures) and still
 
585
        # have all hash pointers stay consistent.
 
586
        # But for now, just hash the contents.
 
587
        return sha_file(self.revision_store[revision_id])
 
588
 
 
589
 
 
590
    def get_inventory(self, inventory_id):
 
591
        """Get Inventory object by hash.
 
592
 
 
593
        TODO: Perhaps for this and similar methods, take a revision
 
594
               parameter which can be either an integer revno or a
 
595
               string hash."""
 
596
        from bzrlib.inventory import Inventory
 
597
        from bzrlib.xml import unpack_xml
 
598
 
 
599
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
600
            
 
601
 
 
602
    def get_inventory_sha1(self, inventory_id):
699
603
        """Return the sha1 hash of the inventory entry
700
604
        """
701
 
        return self.get_revision(revision_id).inventory_sha1
 
605
        return sha_file(self.inventory_store[inventory_id])
702
606
 
703
607
 
704
608
    def get_revision_inventory(self, revision_id):
705
609
        """Return inventory of a past revision."""
706
 
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
707
 
        # must be the same as its revision, so this is trivial.
708
610
        if revision_id == None:
709
 
            return Inventory(self.get_root_id())
 
611
            from bzrlib.inventory import Inventory
 
612
            return Inventory()
710
613
        else:
711
 
            return self.get_inventory(revision_id)
 
614
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
712
615
 
713
616
 
714
617
    def revision_history(self):
715
 
        """Return sequence of revision hashes on to this branch."""
 
618
        """Return sequence of revision hashes on to this branch.
 
619
 
 
620
        >>> ScratchBranch().revision_history()
 
621
        []
 
622
        """
716
623
        self.lock_read()
717
624
        try:
718
625
            return [l.rstrip('\r\n') for l in
727
634
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
728
635
        >>> sb.common_ancestor(sb) == (None, None)
729
636
        True
730
 
        >>> commit.commit(sb, "Committing first revision")
 
637
        >>> commit.commit(sb, "Committing first revision", verbose=False)
731
638
        >>> sb.common_ancestor(sb)[0]
732
639
        1
733
640
        >>> clone = sb.clone()
734
 
        >>> commit.commit(sb, "Committing second revision")
 
641
        >>> commit.commit(sb, "Committing second revision", verbose=False)
735
642
        >>> sb.common_ancestor(sb)[0]
736
643
        2
737
644
        >>> sb.common_ancestor(clone)[0]
738
645
        1
739
 
        >>> commit.commit(clone, "Committing divergent second revision")
 
646
        >>> commit.commit(clone, "Committing divergent second revision", 
 
647
        ...               verbose=False)
740
648
        >>> sb.common_ancestor(clone)[0]
741
649
        1
742
650
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
764
672
                return r+1, my_history[r]
765
673
        return None, None
766
674
 
 
675
    def enum_history(self, direction):
 
676
        """Return (revno, revision_id) for history of branch.
 
677
 
 
678
        direction
 
679
            'forward' is from earliest to latest
 
680
            'reverse' is from latest to earliest
 
681
        """
 
682
        rh = self.revision_history()
 
683
        if direction == 'forward':
 
684
            i = 1
 
685
            for rid in rh:
 
686
                yield i, rid
 
687
                i += 1
 
688
        elif direction == 'reverse':
 
689
            i = len(rh)
 
690
            while i > 0:
 
691
                yield i, rh[i-1]
 
692
                i -= 1
 
693
        else:
 
694
            raise ValueError('invalid history direction', direction)
 
695
 
767
696
 
768
697
    def revno(self):
769
698
        """Return current revision number for this branch.
774
703
        return len(self.revision_history())
775
704
 
776
705
 
777
 
    def last_revision(self):
 
706
    def last_patch(self):
778
707
        """Return last patch hash, or None if no history.
779
708
        """
780
709
        ph = self.revision_history()
784
713
            return None
785
714
 
786
715
 
787
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
788
 
        """Return a list of new revisions that would perfectly fit.
789
 
        
 
716
    def missing_revisions(self, other, stop_revision=None):
 
717
        """
790
718
        If self and other have not diverged, return a list of the revisions
791
719
        present in other, but missing from self.
792
720
 
812
740
        Traceback (most recent call last):
813
741
        DivergedBranches: These branches have diverged.
814
742
        """
815
 
        # FIXME: If the branches have diverged, but the latest
816
 
        # revision in this branch is completely merged into the other,
817
 
        # then we should still be able to pull.
818
743
        self_history = self.revision_history()
819
744
        self_len = len(self_history)
820
745
        other_history = other.revision_history()
826
751
 
827
752
        if stop_revision is None:
828
753
            stop_revision = other_len
829
 
        else:
830
 
            assert isinstance(stop_revision, int)
831
 
            if stop_revision > other_len:
832
 
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
754
        elif stop_revision > other_len:
 
755
            raise NoSuchRevision(self, stop_revision)
833
756
        
834
757
        return other_history[self_len:stop_revision]
835
758
 
836
759
 
837
 
    def update_revisions(self, other, stop_revno=None):
838
 
        """Pull in new perfect-fit revisions.
 
760
    def update_revisions(self, other, stop_revision=None):
 
761
        """Pull in all new revisions from other branch.
 
762
        
 
763
        >>> from bzrlib.commit import commit
 
764
        >>> bzrlib.trace.silent = True
 
765
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
766
        >>> br1.add('foo')
 
767
        >>> br1.add('bar')
 
768
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
769
        >>> br2 = ScratchBranch()
 
770
        >>> br2.update_revisions(br1)
 
771
        Added 2 texts.
 
772
        Added 1 inventories.
 
773
        Added 1 revisions.
 
774
        >>> br2.revision_history()
 
775
        [u'REVISION-ID-1']
 
776
        >>> br2.update_revisions(br1)
 
777
        Added 0 texts.
 
778
        Added 0 inventories.
 
779
        Added 0 revisions.
 
780
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
781
        True
839
782
        """
840
 
        from bzrlib.fetch import greedy_fetch
841
 
 
842
 
        if stop_revno:
843
 
            stop_revision = other.lookup_revision(stop_revno)
844
 
        else:
845
 
            stop_revision = None
846
 
        greedy_fetch(to_branch=self, from_branch=other,
847
 
                     revision=stop_revision)
848
 
 
849
 
        pullable_revs = self.missing_revisions(other, stop_revision)
850
 
 
851
 
        if pullable_revs:
852
 
            greedy_fetch(to_branch=self,
853
 
                         from_branch=other,
854
 
                         revision=pullable_revs[-1])
855
 
            self.append_revision(*pullable_revs)
856
 
 
857
 
 
 
783
        from bzrlib.progress import ProgressBar
 
784
        try:
 
785
            set
 
786
        except NameError:
 
787
            from sets import Set as set
 
788
 
 
789
        pb = ProgressBar()
 
790
 
 
791
        pb.update('comparing histories')
 
792
        revision_ids = self.missing_revisions(other, stop_revision)
 
793
 
 
794
        if hasattr(other.revision_store, "prefetch"):
 
795
            other.revision_store.prefetch(revision_ids)
 
796
        if hasattr(other.inventory_store, "prefetch"):
 
797
            inventory_ids = [other.get_revision(r).inventory_id
 
798
                             for r in revision_ids]
 
799
            other.inventory_store.prefetch(inventory_ids)
 
800
                
 
801
        revisions = []
 
802
        needed_texts = set()
 
803
        i = 0
 
804
        for rev_id in revision_ids:
 
805
            i += 1
 
806
            pb.update('fetching revision', i, len(revision_ids))
 
807
            rev = other.get_revision(rev_id)
 
808
            revisions.append(rev)
 
809
            inv = other.get_inventory(str(rev.inventory_id))
 
810
            for key, entry in inv.iter_entries():
 
811
                if entry.text_id is None:
 
812
                    continue
 
813
                if entry.text_id not in self.text_store:
 
814
                    needed_texts.add(entry.text_id)
 
815
 
 
816
        pb.clear()
 
817
                    
 
818
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
819
        print "Added %d texts." % count 
 
820
        inventory_ids = [ f.inventory_id for f in revisions ]
 
821
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
822
                                                inventory_ids)
 
823
        print "Added %d inventories." % count 
 
824
        revision_ids = [ f.revision_id for f in revisions]
 
825
        count = self.revision_store.copy_multi(other.revision_store, 
 
826
                                               revision_ids)
 
827
        for revision_id in revision_ids:
 
828
            self.append_revision(revision_id)
 
829
        print "Added %d revisions." % count
 
830
                    
 
831
        
858
832
    def commit(self, *args, **kw):
859
 
        from bzrlib.commit import Commit
860
 
        Commit().commit(self, *args, **kw)
 
833
        from bzrlib.commit import commit
 
834
        commit(self, *args, **kw)
861
835
        
862
836
 
863
 
    def lookup_revision(self, revision):
864
 
        """Return the revision identifier for a given revision information."""
865
 
        revno, info = self._get_revision_info(revision)
866
 
        return info
867
 
 
868
 
 
869
 
    def revision_id_to_revno(self, revision_id):
870
 
        """Given a revision id, return its revno"""
871
 
        history = self.revision_history()
872
 
        try:
873
 
            return history.index(revision_id) + 1
874
 
        except ValueError:
875
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
876
 
 
877
 
 
878
 
    def get_revision_info(self, revision):
879
 
        """Return (revno, revision id) for revision identifier.
880
 
 
881
 
        revision can be an integer, in which case it is assumed to be revno (though
882
 
            this will translate negative values into positive ones)
883
 
        revision can also be a string, in which case it is parsed for something like
884
 
            'date:' or 'revid:' etc.
885
 
        """
886
 
        revno, rev_id = self._get_revision_info(revision)
887
 
        if revno is None:
888
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
889
 
        return revno, rev_id
890
 
 
891
 
    def get_rev_id(self, revno, history=None):
892
 
        """Find the revision id of the specified revno."""
 
837
    def lookup_revision(self, revno):
 
838
        """Return revision hash for revision number."""
893
839
        if revno == 0:
894
840
            return None
895
 
        if history is None:
896
 
            history = self.revision_history()
897
 
        elif revno <= 0 or revno > len(history):
898
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
899
 
        return history[revno - 1]
900
 
 
901
 
    def _get_revision_info(self, revision):
902
 
        """Return (revno, revision id) for revision specifier.
903
 
 
904
 
        revision can be an integer, in which case it is assumed to be revno
905
 
        (though this will translate negative values into positive ones)
906
 
        revision can also be a string, in which case it is parsed for something
907
 
        like 'date:' or 'revid:' etc.
908
 
 
909
 
        A revid is always returned.  If it is None, the specifier referred to
910
 
        the null revision.  If the revid does not occur in the revision
911
 
        history, revno will be None.
912
 
        """
913
 
        
914
 
        if revision is None:
915
 
            return 0, None
916
 
        revno = None
917
 
        try:# Convert to int if possible
918
 
            revision = int(revision)
919
 
        except ValueError:
920
 
            pass
921
 
        revs = self.revision_history()
922
 
        if isinstance(revision, int):
923
 
            if revision < 0:
924
 
                revno = len(revs) + revision + 1
925
 
            else:
926
 
                revno = revision
927
 
            rev_id = self.get_rev_id(revno, revs)
928
 
        elif isinstance(revision, basestring):
929
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
930
 
                if revision.startswith(prefix):
931
 
                    result = func(self, revs, revision)
932
 
                    if len(result) > 1:
933
 
                        revno, rev_id = result
934
 
                    else:
935
 
                        revno = result[0]
936
 
                        rev_id = self.get_rev_id(revno, revs)
937
 
                    break
938
 
            else:
939
 
                raise BzrError('No namespace registered for string: %r' %
940
 
                               revision)
941
 
        else:
942
 
            raise TypeError('Unhandled revision type %s' % revision)
943
 
 
944
 
        if revno is None:
945
 
            if rev_id is None:
946
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
947
 
        return revno, rev_id
948
 
 
949
 
    def _namespace_revno(self, revs, revision):
950
 
        """Lookup a revision by revision number"""
951
 
        assert revision.startswith('revno:')
952
 
        try:
953
 
            return (int(revision[6:]),)
954
 
        except ValueError:
955
 
            return None
956
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
957
 
 
958
 
    def _namespace_revid(self, revs, revision):
959
 
        assert revision.startswith('revid:')
960
 
        rev_id = revision[len('revid:'):]
961
 
        try:
962
 
            return revs.index(rev_id) + 1, rev_id
963
 
        except ValueError:
964
 
            return None, rev_id
965
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
966
 
 
967
 
    def _namespace_last(self, revs, revision):
968
 
        assert revision.startswith('last:')
969
 
        try:
970
 
            offset = int(revision[5:])
971
 
        except ValueError:
972
 
            return (None,)
973
 
        else:
974
 
            if offset <= 0:
975
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
976
 
            return (len(revs) - offset + 1,)
977
 
    REVISION_NAMESPACES['last:'] = _namespace_last
978
 
 
979
 
    def _namespace_tag(self, revs, revision):
980
 
        assert revision.startswith('tag:')
981
 
        raise BzrError('tag: namespace registered, but not implemented.')
982
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
983
 
 
984
 
    def _namespace_date(self, revs, revision):
985
 
        assert revision.startswith('date:')
986
 
        import datetime
987
 
        # Spec for date revisions:
988
 
        #   date:value
989
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
990
 
        #   it can also start with a '+/-/='. '+' says match the first
991
 
        #   entry after the given date. '-' is match the first entry before the date
992
 
        #   '=' is match the first entry after, but still on the given date.
993
 
        #
994
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
995
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
996
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
997
 
        #       May 13th, 2005 at 0:00
998
 
        #
999
 
        #   So the proper way of saying 'give me all entries for today' is:
1000
 
        #       -r {date:+today}:{date:-tomorrow}
1001
 
        #   The default is '=' when not supplied
1002
 
        val = revision[5:]
1003
 
        match_style = '='
1004
 
        if val[:1] in ('+', '-', '='):
1005
 
            match_style = val[:1]
1006
 
            val = val[1:]
1007
 
 
1008
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1009
 
        if val.lower() == 'yesterday':
1010
 
            dt = today - datetime.timedelta(days=1)
1011
 
        elif val.lower() == 'today':
1012
 
            dt = today
1013
 
        elif val.lower() == 'tomorrow':
1014
 
            dt = today + datetime.timedelta(days=1)
1015
 
        else:
1016
 
            import re
1017
 
            # This should be done outside the function to avoid recompiling it.
1018
 
            _date_re = re.compile(
1019
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1020
 
                    r'(,|T)?\s*'
1021
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1022
 
                )
1023
 
            m = _date_re.match(val)
1024
 
            if not m or (not m.group('date') and not m.group('time')):
1025
 
                raise BzrError('Invalid revision date %r' % revision)
1026
 
 
1027
 
            if m.group('date'):
1028
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1029
 
            else:
1030
 
                year, month, day = today.year, today.month, today.day
1031
 
            if m.group('time'):
1032
 
                hour = int(m.group('hour'))
1033
 
                minute = int(m.group('minute'))
1034
 
                if m.group('second'):
1035
 
                    second = int(m.group('second'))
1036
 
                else:
1037
 
                    second = 0
1038
 
            else:
1039
 
                hour, minute, second = 0,0,0
1040
 
 
1041
 
            dt = datetime.datetime(year=year, month=month, day=day,
1042
 
                    hour=hour, minute=minute, second=second)
1043
 
        first = dt
1044
 
        last = None
1045
 
        reversed = False
1046
 
        if match_style == '-':
1047
 
            reversed = True
1048
 
        elif match_style == '=':
1049
 
            last = dt + datetime.timedelta(days=1)
1050
 
 
1051
 
        if reversed:
1052
 
            for i in range(len(revs)-1, -1, -1):
1053
 
                r = self.get_revision(revs[i])
1054
 
                # TODO: Handle timezone.
1055
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1056
 
                if first >= dt and (last is None or dt >= last):
1057
 
                    return (i+1,)
1058
 
        else:
1059
 
            for i in range(len(revs)):
1060
 
                r = self.get_revision(revs[i])
1061
 
                # TODO: Handle timezone.
1062
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1063
 
                if first <= dt and (last is None or dt <= last):
1064
 
                    return (i+1,)
1065
 
    REVISION_NAMESPACES['date:'] = _namespace_date
 
841
 
 
842
        try:
 
843
            # list is 0-based; revisions are 1-based
 
844
            return self.revision_history()[revno-1]
 
845
        except IndexError:
 
846
            raise BzrError("no such revision %s" % revno)
 
847
 
1066
848
 
1067
849
    def revision_tree(self, revision_id):
1068
850
        """Return Tree for a revision on this branch.
1069
851
 
1070
852
        `revision_id` may be None for the null revision, in which case
1071
853
        an `EmptyTree` is returned."""
 
854
        from bzrlib.tree import EmptyTree, RevisionTree
1072
855
        # TODO: refactor this to use an existing revision object
1073
856
        # so we don't need to read it in twice.
1074
857
        if revision_id == None:
1075
858
            return EmptyTree()
1076
859
        else:
1077
860
            inv = self.get_revision_inventory(revision_id)
1078
 
            return RevisionTree(self.weave_store, inv, revision_id)
 
861
            return RevisionTree(self.text_store, inv)
1079
862
 
1080
863
 
1081
864
    def working_tree(self):
1089
872
 
1090
873
        If there are no revisions yet, return an `EmptyTree`.
1091
874
        """
1092
 
        return self.revision_tree(self.last_revision())
 
875
        from bzrlib.tree import EmptyTree, RevisionTree
 
876
        r = self.last_patch()
 
877
        if r == None:
 
878
            return EmptyTree()
 
879
        else:
 
880
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
881
 
1093
882
 
1094
883
 
1095
884
    def rename_one(self, from_rel, to_rel):
1127
916
 
1128
917
            inv.rename(file_id, to_dir_id, to_tail)
1129
918
 
 
919
            print "%s => %s" % (from_rel, to_rel)
 
920
 
1130
921
            from_abs = self.abspath(from_rel)
1131
922
            to_abs = self.abspath(to_rel)
1132
923
            try:
1151
942
 
1152
943
        Note that to_name is only the last component of the new name;
1153
944
        this doesn't change the directory.
1154
 
 
1155
 
        This returns a list of (from_path, to_path) pairs for each
1156
 
        entry that is moved.
1157
945
        """
1158
 
        result = []
1159
946
        self.lock_write()
1160
947
        try:
1161
948
            ## TODO: Option to move IDs only
1196
983
            for f in from_paths:
1197
984
                name_tail = splitpath(f)[-1]
1198
985
                dest_path = appendpath(to_name, name_tail)
1199
 
                result.append((f, dest_path))
 
986
                print "%s => %s" % (f, dest_path)
1200
987
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1201
988
                try:
1202
989
                    os.rename(self.abspath(f), self.abspath(dest_path))
1208
995
        finally:
1209
996
            self.unlock()
1210
997
 
1211
 
        return result
1212
 
 
1213
998
 
1214
999
    def revert(self, filenames, old_tree=None, backups=True):
1215
1000
        """Restore selected files to the versions from a previous tree.
1271
1056
 
1272
1057
 
1273
1058
    def add_pending_merge(self, revision_id):
 
1059
        from bzrlib.revision import validate_revision_id
 
1060
 
1274
1061
        validate_revision_id(revision_id)
1275
 
        # TODO: Perhaps should check at this point that the
1276
 
        # history of the revision is actually present?
 
1062
 
1277
1063
        p = self.pending_merges()
1278
1064
        if revision_id in p:
1279
1065
            return
1296
1082
            self.unlock()
1297
1083
 
1298
1084
 
1299
 
    def get_parent(self):
1300
 
        """Return the parent location of the branch.
1301
 
 
1302
 
        This is the default location for push/pull/missing.  The usual
1303
 
        pattern is that the user can override it by specifying a
1304
 
        location.
1305
 
        """
1306
 
        import errno
1307
 
        _locs = ['parent', 'pull', 'x-pull']
1308
 
        for l in _locs:
1309
 
            try:
1310
 
                return self.controlfile(l, 'r').read().strip('\n')
1311
 
            except IOError, e:
1312
 
                if e.errno != errno.ENOENT:
1313
 
                    raise
1314
 
        return None
1315
 
 
1316
 
 
1317
 
    def set_parent(self, url):
1318
 
        # TODO: Maybe delete old location files?
1319
 
        from bzrlib.atomicfile import AtomicFile
1320
 
        self.lock_write()
1321
 
        try:
1322
 
            f = AtomicFile(self.controlfilename('parent'))
1323
 
            try:
1324
 
                f.write(url + '\n')
1325
 
                f.commit()
1326
 
            finally:
1327
 
                f.close()
1328
 
        finally:
1329
 
            self.unlock()
1330
 
 
1331
 
    def check_revno(self, revno):
1332
 
        """\
1333
 
        Check whether a revno corresponds to any revision.
1334
 
        Zero (the NULL revision) is considered valid.
1335
 
        """
1336
 
        if revno != 0:
1337
 
            self.check_real_revno(revno)
1338
 
            
1339
 
    def check_real_revno(self, revno):
1340
 
        """\
1341
 
        Check whether a revno corresponds to a real revision.
1342
 
        Zero (the NULL revision) is considered invalid
1343
 
        """
1344
 
        if revno < 1 or revno > self.revno():
1345
 
            raise InvalidRevisionNumber(revno)
1346
 
        
1347
 
        
1348
 
 
1349
1085
 
1350
1086
class ScratchBranch(Branch):
1351
1087
    """Special test class: a branch that cleans up after itself.
1393
1129
        os.rmdir(base)
1394
1130
        copytree(self.base, base, symlinks=True)
1395
1131
        return ScratchBranch(base=base)
1396
 
 
1397
 
 
1398
1132
        
1399
1133
    def __del__(self):
1400
1134
        self.destroy()
1464
1198
 
1465
1199
    s = hexlify(rand_bytes(8))
1466
1200
    return '-'.join((name, compact_date(time()), s))
1467
 
 
1468
 
 
1469
 
def gen_root_id():
1470
 
    """Return a new tree-root file id."""
1471
 
    return gen_file_id('TREE_ROOT')
1472
 
 
1473
 
 
1474
 
def pull_loc(branch):
1475
 
    # TODO: Should perhaps just make attribute be 'base' in
1476
 
    # RemoteBranch and Branch?
1477
 
    if hasattr(branch, "baseurl"):
1478
 
        return branch.baseurl
1479
 
    else:
1480
 
        return branch.base
1481
 
 
1482
 
 
1483
 
def copy_branch(branch_from, to_location, revision=None):
1484
 
    """Copy branch_from into the existing directory to_location.
1485
 
 
1486
 
    revision
1487
 
        If not None, only revisions up to this point will be copied.
1488
 
        The head of the new branch will be that revision.  Can be a
1489
 
        revno or revid.
1490
 
 
1491
 
    to_location
1492
 
        The name of a local directory that exists but is empty.
1493
 
    """
1494
 
    # TODO: This could be done *much* more efficiently by just copying
1495
 
    # all the whole weaves and revisions, rather than getting one
1496
 
    # revision at a time.
1497
 
    from bzrlib.merge import merge
1498
 
    from bzrlib.branch import Branch
1499
 
 
1500
 
    assert isinstance(branch_from, Branch)
1501
 
    assert isinstance(to_location, basestring)
1502
 
    
1503
 
    br_to = Branch(to_location, init=True)
1504
 
    br_to.set_root_id(branch_from.get_root_id())
1505
 
    if revision is None:
1506
 
        revno = None
1507
 
    else:
1508
 
        revno, rev_id = branch_from.get_revision_info(revision)
1509
 
    br_to.update_revisions(branch_from, stop_revno=revno)
1510
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1511
 
          check_clean=False, ignore_zero=True)
1512
 
    
1513
 
    from_location = pull_loc(branch_from)
1514
 
    br_to.set_parent(pull_loc(branch_from))
1515