~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-09-28 05:25:54 UTC
  • mfrom: (1185.1.42)
  • mto: (1092.2.18)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050928052554-beb985505f77ea6a
update symlink branch to integration

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys, os
 
18
import sys
 
19
import os
19
20
 
20
21
import bzrlib
21
22
from bzrlib.trace import mutter, note
22
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
23
 
     sha_file, appendpath, file_kind
24
 
from bzrlib.errors import BzrError
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     rename, splitpath, sha_file, appendpath, file_kind
 
25
 
 
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId, \
 
27
     DivergedBranches, NotBranchError
 
28
from bzrlib.textui import show_status
 
29
from bzrlib.revision import Revision
 
30
from bzrlib.delta import compare_trees
 
31
from bzrlib.tree import EmptyTree, RevisionTree
 
32
import bzrlib.xml
 
33
import bzrlib.ui
 
34
 
 
35
 
25
36
 
26
37
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
27
38
## TODO: Maybe include checks for common corruption of newlines, etc?
28
39
 
29
40
 
30
 
 
31
 
def find_branch(f, **args):
32
 
    if f and (f.startswith('http://') or f.startswith('https://')):
33
 
        import remotebranch 
34
 
        return remotebranch.RemoteBranch(f, **args)
35
 
    else:
36
 
        return Branch(f, **args)
37
 
 
38
 
 
39
 
def find_cached_branch(f, cache_root, **args):
40
 
    from remotebranch import RemoteBranch
41
 
    br = find_branch(f, **args)
42
 
    def cacheify(br, store_name):
43
 
        from meta_store import CachedStore
44
 
        cache_path = os.path.join(cache_root, store_name)
45
 
        os.mkdir(cache_path)
46
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
47
 
        setattr(br, store_name, new_store)
48
 
 
49
 
    if isinstance(br, RemoteBranch):
50
 
        cacheify(br, 'inventory_store')
51
 
        cacheify(br, 'text_store')
52
 
        cacheify(br, 'revision_store')
53
 
    return br
54
 
 
 
41
# TODO: Some operations like log might retrieve the same revisions
 
42
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
43
# cache in memory to make this faster.
 
44
 
 
45
def find_branch(*ignored, **ignored_too):
 
46
    # XXX: leave this here for about one release, then remove it
 
47
    raise NotImplementedError('find_branch() is not supported anymore, '
 
48
                              'please use one of the new branch constructors')
55
49
 
56
50
def _relpath(base, path):
57
51
    """Return path relative to base, or raise exception.
75
69
        if tail:
76
70
            s.insert(0, tail)
77
71
    else:
78
 
        from errors import NotBranchError
79
72
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
80
73
 
81
74
    return os.sep.join(s)
89
82
    It is not necessary that f exists.
90
83
 
91
84
    Basically we keep looking up until we find the control directory or
92
 
    run into the root."""
 
85
    run into the root.  If there isn't one, raises NotBranchError.
 
86
    """
93
87
    if f == None:
94
88
        f = os.getcwd()
95
 
    elif hasattr(os.path, 'realpath'):
96
 
        f = os.path.realpath(f)
97
89
    else:
98
 
        f = os.path.abspath(f)
99
 
    if not os.path.exists(f):
 
90
        f = bzrlib.osutils.normalizepath(f)
 
91
    if not bzrlib.osutils.lexists(f):
100
92
        raise BzrError('%r does not exist' % f)
101
 
        
102
93
 
103
94
    orig_f = f
104
95
 
108
99
        head, tail = os.path.split(f)
109
100
        if head == f:
110
101
            # reached the root, whatever that may be
111
 
            raise BzrError('%r is not in a branch' % orig_f)
 
102
            raise NotBranchError('%s is not in a branch' % orig_f)
112
103
        f = head
113
 
    
114
 
class DivergedBranches(Exception):
115
 
    def __init__(self, branch1, branch2):
116
 
        self.branch1 = branch1
117
 
        self.branch2 = branch2
118
 
        Exception.__init__(self, "These branches have diverged.")
119
 
 
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)
 
104
 
 
105
 
127
106
 
128
107
 
129
108
######################################################################
133
112
    """Branch holding a history of revisions.
134
113
 
135
114
    base
136
 
        Base directory of the branch.
 
115
        Base directory/url of the branch.
 
116
    """
 
117
    base = None
 
118
 
 
119
    def __init__(self, *ignored, **ignored_too):
 
120
        raise NotImplementedError('The Branch class is abstract')
 
121
 
 
122
    @staticmethod
 
123
    def open(base):
 
124
        """Open an existing branch, rooted at 'base' (url)"""
 
125
        if base and (base.startswith('http://') or base.startswith('https://')):
 
126
            from bzrlib.remotebranch import RemoteBranch
 
127
            return RemoteBranch(base, find_root=False)
 
128
        else:
 
129
            return LocalBranch(base, find_root=False)
 
130
 
 
131
    @staticmethod
 
132
    def open_containing(url):
 
133
        """Open an existing branch which contains url.
 
134
        
 
135
        This probes for a branch at url, and searches upwards from there.
 
136
        """
 
137
        if url and (url.startswith('http://') or url.startswith('https://')):
 
138
            from bzrlib.remotebranch import RemoteBranch
 
139
            return RemoteBranch(url)
 
140
        else:
 
141
            return LocalBranch(url)
 
142
 
 
143
    @staticmethod
 
144
    def initialize(base):
 
145
        """Create a new branch, rooted at 'base' (url)"""
 
146
        if base and (base.startswith('http://') or base.startswith('https://')):
 
147
            from bzrlib.remotebranch import RemoteBranch
 
148
            return RemoteBranch(base, init=True)
 
149
        else:
 
150
            return LocalBranch(base, init=True)
 
151
 
 
152
    def setup_caching(self, cache_root):
 
153
        """Subclasses that care about caching should override this, and set
 
154
        up cached stores located under cache_root.
 
155
        """
 
156
 
 
157
 
 
158
class LocalBranch(Branch):
 
159
    """A branch stored in the actual filesystem.
 
160
 
 
161
    Note that it's "local" in the context of the filesystem; it doesn't
 
162
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
163
    it's writable, and can be accessed via the normal filesystem API.
137
164
 
138
165
    _lock_mode
139
166
        None, or 'r' or 'w'
145
172
    _lock
146
173
        Lock object from bzrlib.lock.
147
174
    """
148
 
    base = None
 
175
    # We actually expect this class to be somewhat short-lived; part of its
 
176
    # purpose is to try to isolate what bits of the branch logic are tied to
 
177
    # filesystem access, so that in a later step, we can extricate them to
 
178
    # a separarte ("storage") class.
149
179
    _lock_mode = None
150
180
    _lock_count = None
151
181
    _lock = None
152
 
    
 
182
 
153
183
    def __init__(self, base, init=False, find_root=True):
154
184
        """Create new branch object at a particular location.
155
185
 
156
 
        base -- Base directory for the branch.
 
186
        base -- Base directory for the branch. May be a file:// url.
157
187
        
158
188
        init -- If True, create new control files in a previously
159
189
             unversioned directory.  If False, the branch must already
172
202
        elif find_root:
173
203
            self.base = find_branch_root(base)
174
204
        else:
 
205
            if base.startswith("file://"):
 
206
                base = base[7:]
175
207
            self.base = os.path.realpath(base)
176
208
            if not isdir(self.controlfilename('.')):
177
 
                from errors import NotBranchError
178
209
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
179
210
                                     ['use "bzr init" to initialize a new working tree',
180
211
                                      'current bzr can only operate from top-of-tree'])
194
225
 
195
226
    def __del__(self):
196
227
        if self._lock_mode or self._lock:
197
 
            from warnings import warn
 
228
            from bzrlib.warnings import warn
198
229
            warn("branch %r was not explicitly unlocked" % self)
199
230
            self._lock.unlock()
200
231
 
201
 
 
202
 
 
203
232
    def lock_write(self):
204
233
        if self._lock_mode:
205
234
            if self._lock_mode != 'w':
206
 
                from errors import LockError
 
235
                from bzrlib.errors import LockError
207
236
                raise LockError("can't upgrade to a write lock from %r" %
208
237
                                self._lock_mode)
209
238
            self._lock_count += 1
215
244
            self._lock_count = 1
216
245
 
217
246
 
218
 
 
219
247
    def lock_read(self):
220
248
        if self._lock_mode:
221
249
            assert self._lock_mode in ('r', 'w'), \
228
256
            self._lock_mode = 'r'
229
257
            self._lock_count = 1
230
258
                        
231
 
 
232
 
            
233
259
    def unlock(self):
234
260
        if not self._lock_mode:
235
 
            from errors import LockError
 
261
            from bzrlib.errors import LockError
236
262
            raise LockError('branch %r is not locked' % (self))
237
263
 
238
264
        if self._lock_count > 1:
242
268
            self._lock = None
243
269
            self._lock_mode = self._lock_count = None
244
270
 
245
 
 
246
271
    def abspath(self, name):
247
272
        """Return absolute filename for something in the branch"""
248
273
        return os.path.join(self.base, name)
249
274
 
250
 
 
251
275
    def relpath(self, path):
252
276
        """Return path relative to this branch of something inside it.
253
277
 
254
278
        Raises an error if path is not in this branch."""
255
279
        return _relpath(self.base, path)
256
280
 
257
 
 
258
281
    def controlfilename(self, file_or_path):
259
282
        """Return location relative to branch."""
260
283
        if isinstance(file_or_path, basestring):
287
310
        else:
288
311
            raise BzrError("invalid controlfile mode %r" % mode)
289
312
 
290
 
 
291
 
 
292
313
    def _make_control(self):
293
314
        from bzrlib.inventory import Inventory
294
 
        from bzrlib.xml import pack_xml
295
315
        
296
316
        os.mkdir(self.controlfilename([]))
297
317
        self.controlfile('README', 'w').write(
307
327
            self.controlfile(f, 'w').write('')
308
328
        mutter('created control directory in ' + self.base)
309
329
 
310
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
330
        # if we want per-tree root ids then this is the place to set
 
331
        # them; they're not needed for now and so ommitted for
 
332
        # simplicity.
 
333
        f = self.controlfile('inventory','w')
 
334
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
311
335
 
312
336
 
313
337
    def _check_format(self):
322
346
        # on Windows from Linux and so on.  I think it might be better
323
347
        # to always make all internal files in unix format.
324
348
        fmt = self.controlfile('branch-format', 'r').read()
325
 
        fmt.replace('\r\n', '')
 
349
        fmt = fmt.replace('\r\n', '\n')
326
350
        if fmt != BZR_BRANCH_FORMAT:
327
351
            raise BzrError('sorry, branch format %r not supported' % fmt,
328
352
                           ['use a different bzr version',
329
353
                            'or remove the .bzr directory and "bzr init" again'])
330
354
 
 
355
    def get_root_id(self):
 
356
        """Return the id of this branches root"""
 
357
        inv = self.read_working_inventory()
 
358
        return inv.root.file_id
331
359
 
 
360
    def set_root_id(self, file_id):
 
361
        inv = self.read_working_inventory()
 
362
        orig_root_id = inv.root.file_id
 
363
        del inv._byid[inv.root.file_id]
 
364
        inv.root.file_id = file_id
 
365
        inv._byid[inv.root.file_id] = inv.root
 
366
        for fid in inv:
 
367
            entry = inv[fid]
 
368
            if entry.parent_id in (None, orig_root_id):
 
369
                entry.parent_id = inv.root.file_id
 
370
        self._write_inventory(inv)
332
371
 
333
372
    def read_working_inventory(self):
334
373
        """Read the working inventory."""
335
374
        from bzrlib.inventory import Inventory
336
 
        from bzrlib.xml import unpack_xml
337
 
        from time import time
338
 
        before = time()
339
375
        self.lock_read()
340
376
        try:
341
377
            # ElementTree does its own conversion from UTF-8, so open in
342
378
            # binary.
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
 
379
            f = self.controlfile('inventory', 'rb')
 
380
            return bzrlib.xml.serializer_v4.read_inventory(f)
348
381
        finally:
349
382
            self.unlock()
350
383
            
356
389
        will be committed to the next revision.
357
390
        """
358
391
        from bzrlib.atomicfile import AtomicFile
359
 
        from bzrlib.xml import pack_xml
360
392
        
361
393
        self.lock_write()
362
394
        try:
363
395
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
364
396
            try:
365
 
                pack_xml(inv, f)
 
397
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
366
398
                f.commit()
367
399
            finally:
368
400
                f.close()
376
408
                         """Inventory for the working copy.""")
377
409
 
378
410
 
379
 
    def add(self, files, verbose=False, ids=None):
 
411
    def add(self, files, ids=None):
380
412
        """Make files versioned.
381
413
 
382
 
        Note that the command line normally calls smart_add instead.
 
414
        Note that the command line normally calls smart_add instead,
 
415
        which can automatically recurse.
383
416
 
384
417
        This puts the files in the Added state, so that they will be
385
418
        recorded by the next commit.
395
428
        TODO: Perhaps have an option to add the ids even if the files do
396
429
              not (yet) exist.
397
430
 
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.
 
431
        TODO: Perhaps yield the ids and paths as they're added.
404
432
        """
405
 
        from bzrlib.textui import show_status
406
433
        # TODO: Re-adding a file that is removed in the working copy
407
434
        # should probably put it back with the previous ID.
408
435
        if isinstance(files, basestring):
434
461
                    kind = file_kind(fullpath)
435
462
                except OSError:
436
463
                    # maybe something better?
437
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
464
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
438
465
 
439
 
                if kind != 'file' and kind != 'directory':
440
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
466
                if kind not in ('file', 'directory', 'symlink'):
 
467
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
441
468
 
442
469
                if file_id is None:
443
470
                    file_id = gen_file_id(f)
444
471
                inv.add_path(f, kind=kind, file_id=file_id)
445
472
 
446
 
                if verbose:
447
 
                    print 'added', quotefn(f)
448
 
 
449
473
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
450
474
 
451
475
            self._write_inventory(inv)
457
481
        """Print `file` to stdout."""
458
482
        self.lock_read()
459
483
        try:
460
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
484
            tree = self.revision_tree(self.get_rev_id(revno))
461
485
            # use inventory as it was in that revision
462
486
            file_id = tree.inventory.path2id(file)
463
487
            if not file_id:
464
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
488
                raise BzrError("%r is not present in revision %s" % (file, revno))
465
489
            tree.print_file(file_id)
466
490
        finally:
467
491
            self.unlock()
481
505
        is the opposite of add.  Removing it is consistent with most
482
506
        other tools.  Maybe an option.
483
507
        """
484
 
        from bzrlib.textui import show_status
485
508
        ## TODO: Normalize names
486
509
        ## TODO: Remove nested loops; better scalability
487
510
        if isinstance(files, basestring):
512
535
        finally:
513
536
            self.unlock()
514
537
 
515
 
 
516
538
    # FIXME: this doesn't need to be a branch method
517
539
    def set_inventory(self, new_inventory_list):
518
540
        from bzrlib.inventory import Inventory, InventoryEntry
519
 
        inv = Inventory()
 
541
        inv = Inventory(self.get_root_id())
520
542
        for path, file_id, parent, kind in new_inventory_list:
521
543
            name = os.path.basename(path)
522
544
            if name == "":
524
546
            inv.add(InventoryEntry(file_id, name, kind, parent))
525
547
        self._write_inventory(inv)
526
548
 
527
 
 
528
549
    def unknowns(self):
529
550
        """Return all unknown files.
530
551
 
544
565
        return self.working_tree().unknowns()
545
566
 
546
567
 
547
 
    def append_revision(self, revision_id):
 
568
    def append_revision(self, *revision_ids):
548
569
        from bzrlib.atomicfile import AtomicFile
549
570
 
550
 
        mutter("add {%s} to revision-history" % revision_id)
551
 
        rev_history = self.revision_history() + [revision_id]
 
571
        for revision_id in revision_ids:
 
572
            mutter("add {%s} to revision-history" % revision_id)
 
573
 
 
574
        rev_history = self.revision_history()
 
575
        rev_history.extend(revision_ids)
552
576
 
553
577
        f = AtomicFile(self.controlfilename('revision-history'))
554
578
        try:
558
582
        finally:
559
583
            f.close()
560
584
 
 
585
    def get_revision_xml_file(self, revision_id):
 
586
        """Return XML file object for revision object."""
 
587
        if not revision_id or not isinstance(revision_id, basestring):
 
588
            raise InvalidRevisionId(revision_id)
 
589
 
 
590
        self.lock_read()
 
591
        try:
 
592
            try:
 
593
                return self.revision_store[revision_id]
 
594
            except (IndexError, KeyError):
 
595
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
596
        finally:
 
597
            self.unlock()
 
598
 
 
599
    #deprecated
 
600
    get_revision_xml = get_revision_xml_file
 
601
 
 
602
    #deprecated
 
603
    get_revision_xml = get_revision_xml_file
 
604
 
561
605
 
562
606
    def get_revision(self, revision_id):
563
607
        """Return the Revision object for a named revision"""
564
 
        from bzrlib.revision import Revision
565
 
        from bzrlib.xml import unpack_xml
 
608
        xml_file = self.get_revision_xml_file(revision_id)
566
609
 
567
 
        self.lock_read()
568
610
        try:
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()
 
611
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
612
        except SyntaxError, e:
 
613
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
614
                                         [revision_id,
 
615
                                          str(e)])
574
616
            
575
617
        assert r.revision_id == revision_id
576
618
        return r
577
 
        
 
619
 
 
620
    def get_revision_delta(self, revno):
 
621
        """Return the delta for one revision.
 
622
 
 
623
        The delta is relative to its mainline predecessor, or the
 
624
        empty tree for revision 1.
 
625
        """
 
626
        assert isinstance(revno, int)
 
627
        rh = self.revision_history()
 
628
        if not (1 <= revno <= len(rh)):
 
629
            raise InvalidRevisionNumber(revno)
 
630
 
 
631
        # revno is 1-based; list is 0-based
 
632
 
 
633
        new_tree = self.revision_tree(rh[revno-1])
 
634
        if revno == 1:
 
635
            old_tree = EmptyTree()
 
636
        else:
 
637
            old_tree = self.revision_tree(rh[revno-2])
 
638
 
 
639
        return compare_trees(old_tree, new_tree)
578
640
 
579
641
    def get_revision_sha1(self, revision_id):
580
642
        """Hash the stored value of a revision, and return it."""
584
646
        # the revision, (add signatures/remove signatures) and still
585
647
        # have all hash pointers stay consistent.
586
648
        # But for now, just hash the contents.
587
 
        return sha_file(self.revision_store[revision_id])
588
 
 
 
649
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
589
650
 
590
651
    def get_inventory(self, inventory_id):
591
652
        """Get Inventory object by hash.
594
655
               parameter which can be either an integer revno or a
595
656
               string hash."""
596
657
        from bzrlib.inventory import Inventory
597
 
        from bzrlib.xml import unpack_xml
598
 
 
599
 
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
658
        f = self.get_inventory_xml_file(inventory_id)
 
659
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
660
 
 
661
    def get_inventory_xml(self, inventory_id):
 
662
        """Get inventory XML as a file object."""
 
663
        return self.inventory_store[inventory_id]
 
664
 
 
665
    get_inventory_xml_file = get_inventory_xml
600
666
            
601
 
 
602
667
    def get_inventory_sha1(self, inventory_id):
603
668
        """Return the sha1 hash of the inventory entry
604
669
        """
605
 
        return sha_file(self.inventory_store[inventory_id])
606
 
 
 
670
        return sha_file(self.get_inventory_xml(inventory_id))
607
671
 
608
672
    def get_revision_inventory(self, revision_id):
609
673
        """Return inventory of a past revision."""
611
675
        # must be the same as its revision, so this is trivial.
612
676
        if revision_id == None:
613
677
            from bzrlib.inventory import Inventory
614
 
            return Inventory()
 
678
            return Inventory(self.get_root_id())
615
679
        else:
616
680
            return self.get_inventory(revision_id)
617
681
 
618
 
 
619
682
    def revision_history(self):
620
683
        """Return sequence of revision hashes on to this branch.
621
684
 
629
692
        finally:
630
693
            self.unlock()
631
694
 
632
 
 
633
695
    def common_ancestor(self, other, self_revno=None, other_revno=None):
634
696
        """
635
 
        >>> import commit
 
697
        >>> from bzrlib.commit import commit
636
698
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
637
699
        >>> sb.common_ancestor(sb) == (None, None)
638
700
        True
639
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
701
        >>> commit(sb, "Committing first revision", verbose=False)
640
702
        >>> sb.common_ancestor(sb)[0]
641
703
        1
642
704
        >>> clone = sb.clone()
643
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
705
        >>> commit(sb, "Committing second revision", verbose=False)
644
706
        >>> sb.common_ancestor(sb)[0]
645
707
        2
646
708
        >>> sb.common_ancestor(clone)[0]
647
709
        1
648
 
        >>> commit.commit(clone, "Committing divergent second revision", 
 
710
        >>> commit(clone, "Committing divergent second revision", 
649
711
        ...               verbose=False)
650
712
        >>> sb.common_ancestor(clone)[0]
651
713
        1
674
736
                return r+1, my_history[r]
675
737
        return None, None
676
738
 
677
 
    def enum_history(self, direction):
678
 
        """Return (revno, revision_id) for history of branch.
679
 
 
680
 
        direction
681
 
            'forward' is from earliest to latest
682
 
            'reverse' is from latest to earliest
683
 
        """
684
 
        rh = self.revision_history()
685
 
        if direction == 'forward':
686
 
            i = 1
687
 
            for rid in rh:
688
 
                yield i, rid
689
 
                i += 1
690
 
        elif direction == 'reverse':
691
 
            i = len(rh)
692
 
            while i > 0:
693
 
                yield i, rh[i-1]
694
 
                i -= 1
695
 
        else:
696
 
            raise ValueError('invalid history direction', direction)
697
 
 
698
739
 
699
740
    def revno(self):
700
741
        """Return current revision number for this branch.
715
756
            return None
716
757
 
717
758
 
718
 
    def missing_revisions(self, other, stop_revision=None):
 
759
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
719
760
        """
720
761
        If self and other have not diverged, return a list of the revisions
721
762
        present in other, but missing from self.
754
795
        if stop_revision is None:
755
796
            stop_revision = other_len
756
797
        elif stop_revision > other_len:
757
 
            raise NoSuchRevision(self, stop_revision)
 
798
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
758
799
        
759
800
        return other_history[self_len:stop_revision]
760
801
 
761
802
 
762
803
    def update_revisions(self, other, stop_revision=None):
763
804
        """Pull in all new revisions from other branch.
764
 
        
765
 
        >>> from bzrlib.commit import commit
766
 
        >>> bzrlib.trace.silent = True
767
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
768
 
        >>> br1.add('foo')
769
 
        >>> br1.add('bar')
770
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
771
 
        >>> br2 = ScratchBranch()
772
 
        >>> br2.update_revisions(br1)
773
 
        Added 2 texts.
774
 
        Added 1 inventories.
775
 
        Added 1 revisions.
776
 
        >>> br2.revision_history()
777
 
        [u'REVISION-ID-1']
778
 
        >>> br2.update_revisions(br1)
779
 
        Added 0 texts.
780
 
        Added 0 inventories.
781
 
        Added 0 revisions.
782
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
783
 
        True
784
805
        """
785
 
        from bzrlib.progress import ProgressBar
786
 
        try:
787
 
            set
788
 
        except NameError:
789
 
            from sets import Set as set
790
 
 
791
 
        pb = ProgressBar()
792
 
 
 
806
        from bzrlib.fetch import greedy_fetch
 
807
        from bzrlib.revision import get_intervening_revisions
 
808
 
 
809
        pb = bzrlib.ui.ui_factory.progress_bar()
793
810
        pb.update('comparing histories')
794
 
        revision_ids = self.missing_revisions(other, stop_revision)
795
 
 
 
811
        if stop_revision is None:
 
812
            other_revision = other.last_patch()
 
813
        else:
 
814
            other_revision = other.get_rev_id(stop_revision)
 
815
        count = greedy_fetch(self, other, other_revision, pb)[0]
 
816
        try:
 
817
            revision_ids = self.missing_revisions(other, stop_revision)
 
818
        except DivergedBranches, e:
 
819
            try:
 
820
                revision_ids = get_intervening_revisions(self.last_patch(), 
 
821
                                                         other_revision, self)
 
822
                assert self.last_patch() not in revision_ids
 
823
            except bzrlib.errors.NotAncestor:
 
824
                raise e
 
825
 
 
826
        self.append_revision(*revision_ids)
 
827
        pb.clear()
 
828
 
 
829
    def install_revisions(self, other, revision_ids, pb):
796
830
        if hasattr(other.revision_store, "prefetch"):
797
831
            other.revision_store.prefetch(revision_ids)
798
832
        if hasattr(other.inventory_store, "prefetch"):
799
 
            inventory_ids = [other.get_revision(r).inventory_id
800
 
                             for r in revision_ids]
 
833
            inventory_ids = []
 
834
            for rev_id in revision_ids:
 
835
                try:
 
836
                    revision = other.get_revision(rev_id).inventory_id
 
837
                    inventory_ids.append(revision)
 
838
                except bzrlib.errors.NoSuchRevision:
 
839
                    pass
801
840
            other.inventory_store.prefetch(inventory_ids)
 
841
 
 
842
        if pb is None:
 
843
            pb = bzrlib.ui.ui_factory.progress_bar()
802
844
                
803
845
        revisions = []
804
846
        needed_texts = set()
805
847
        i = 0
806
 
        for rev_id in revision_ids:
807
 
            i += 1
808
 
            pb.update('fetching revision', i, len(revision_ids))
809
 
            rev = other.get_revision(rev_id)
 
848
 
 
849
        failures = set()
 
850
        for i, rev_id in enumerate(revision_ids):
 
851
            pb.update('fetching revision', i+1, len(revision_ids))
 
852
            try:
 
853
                rev = other.get_revision(rev_id)
 
854
            except bzrlib.errors.NoSuchRevision:
 
855
                failures.add(rev_id)
 
856
                continue
 
857
 
810
858
            revisions.append(rev)
811
859
            inv = other.get_inventory(str(rev.inventory_id))
812
860
            for key, entry in inv.iter_entries():
817
865
 
818
866
        pb.clear()
819
867
                    
820
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
821
 
        print "Added %d texts." % count 
 
868
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
869
                                                    needed_texts)
 
870
        #print "Added %d texts." % count 
822
871
        inventory_ids = [ f.inventory_id for f in revisions ]
823
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
824
 
                                                inventory_ids)
825
 
        print "Added %d inventories." % count 
 
872
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
873
                                                         inventory_ids)
 
874
        #print "Added %d inventories." % count 
826
875
        revision_ids = [ f.revision_id for f in revisions]
827
 
        count = self.revision_store.copy_multi(other.revision_store, 
828
 
                                               revision_ids)
829
 
        for revision_id in revision_ids:
830
 
            self.append_revision(revision_id)
831
 
        print "Added %d revisions." % count
832
 
                    
833
 
        
 
876
 
 
877
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
878
                                                          revision_ids,
 
879
                                                          permit_failure=True)
 
880
        assert len(cp_fail) == 0 
 
881
        return count, failures
 
882
       
 
883
 
834
884
    def commit(self, *args, **kw):
835
885
        from bzrlib.commit import commit
836
886
        commit(self, *args, **kw)
837
887
        
 
888
    def revision_id_to_revno(self, revision_id):
 
889
        """Given a revision id, return its revno"""
 
890
        history = self.revision_history()
 
891
        try:
 
892
            return history.index(revision_id) + 1
 
893
        except ValueError:
 
894
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
838
895
 
839
 
    def lookup_revision(self, revno):
840
 
        """Return revision hash for revision number."""
 
896
    def get_rev_id(self, revno, history=None):
 
897
        """Find the revision id of the specified revno."""
841
898
        if revno == 0:
842
899
            return None
843
 
 
844
 
        try:
845
 
            # list is 0-based; revisions are 1-based
846
 
            return self.revision_history()[revno-1]
847
 
        except IndexError:
848
 
            raise BzrError("no such revision %s" % revno)
849
 
 
 
900
        if history is None:
 
901
            history = self.revision_history()
 
902
        elif revno <= 0 or revno > len(history):
 
903
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
904
        return history[revno - 1]
850
905
 
851
906
    def revision_tree(self, revision_id):
852
907
        """Return Tree for a revision on this branch.
853
908
 
854
909
        `revision_id` may be None for the null revision, in which case
855
910
        an `EmptyTree` is returned."""
856
 
        from bzrlib.tree import EmptyTree, RevisionTree
857
911
        # TODO: refactor this to use an existing revision object
858
912
        # so we don't need to read it in twice.
859
913
        if revision_id == None:
865
919
 
866
920
    def working_tree(self):
867
921
        """Return a `Tree` for the working copy."""
868
 
        from workingtree import WorkingTree
 
922
        from bzrlib.workingtree import WorkingTree
869
923
        return WorkingTree(self.base, self.read_working_inventory())
870
924
 
871
925
 
874
928
 
875
929
        If there are no revisions yet, return an `EmptyTree`.
876
930
        """
877
 
        from bzrlib.tree import EmptyTree, RevisionTree
878
931
        r = self.last_patch()
879
932
        if r == None:
880
933
            return EmptyTree()
918
971
 
919
972
            inv.rename(file_id, to_dir_id, to_tail)
920
973
 
921
 
            print "%s => %s" % (from_rel, to_rel)
922
 
 
923
974
            from_abs = self.abspath(from_rel)
924
975
            to_abs = self.abspath(to_rel)
925
976
            try:
926
 
                os.rename(from_abs, to_abs)
 
977
                rename(from_abs, to_abs)
927
978
            except OSError, e:
928
979
                raise BzrError("failed to rename %r to %r: %s"
929
980
                        % (from_abs, to_abs, e[1]),
944
995
 
945
996
        Note that to_name is only the last component of the new name;
946
997
        this doesn't change the directory.
 
998
 
 
999
        This returns a list of (from_path, to_path) pairs for each
 
1000
        entry that is moved.
947
1001
        """
 
1002
        result = []
948
1003
        self.lock_write()
949
1004
        try:
950
1005
            ## TODO: Option to move IDs only
985
1040
            for f in from_paths:
986
1041
                name_tail = splitpath(f)[-1]
987
1042
                dest_path = appendpath(to_name, name_tail)
988
 
                print "%s => %s" % (f, dest_path)
 
1043
                result.append((f, dest_path))
989
1044
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
990
1045
                try:
991
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1046
                    rename(self.abspath(f), self.abspath(dest_path))
992
1047
                except OSError, e:
993
1048
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
994
1049
                            ["rename rolled back"])
997
1052
        finally:
998
1053
            self.unlock()
999
1054
 
 
1055
        return result
 
1056
 
1000
1057
 
1001
1058
    def revert(self, filenames, old_tree=None, backups=True):
1002
1059
        """Restore selected files to the versions from a previous tree.
1084
1141
            self.unlock()
1085
1142
 
1086
1143
 
1087
 
 
1088
 
class ScratchBranch(Branch):
 
1144
    def get_parent(self):
 
1145
        """Return the parent location of the branch.
 
1146
 
 
1147
        This is the default location for push/pull/missing.  The usual
 
1148
        pattern is that the user can override it by specifying a
 
1149
        location.
 
1150
        """
 
1151
        import errno
 
1152
        _locs = ['parent', 'pull', 'x-pull']
 
1153
        for l in _locs:
 
1154
            try:
 
1155
                return self.controlfile(l, 'r').read().strip('\n')
 
1156
            except IOError, e:
 
1157
                if e.errno != errno.ENOENT:
 
1158
                    raise
 
1159
        return None
 
1160
 
 
1161
 
 
1162
    def set_parent(self, url):
 
1163
        # TODO: Maybe delete old location files?
 
1164
        from bzrlib.atomicfile import AtomicFile
 
1165
        self.lock_write()
 
1166
        try:
 
1167
            f = AtomicFile(self.controlfilename('parent'))
 
1168
            try:
 
1169
                f.write(url + '\n')
 
1170
                f.commit()
 
1171
            finally:
 
1172
                f.close()
 
1173
        finally:
 
1174
            self.unlock()
 
1175
 
 
1176
    def check_revno(self, revno):
 
1177
        """\
 
1178
        Check whether a revno corresponds to any revision.
 
1179
        Zero (the NULL revision) is considered valid.
 
1180
        """
 
1181
        if revno != 0:
 
1182
            self.check_real_revno(revno)
 
1183
            
 
1184
    def check_real_revno(self, revno):
 
1185
        """\
 
1186
        Check whether a revno corresponds to a real revision.
 
1187
        Zero (the NULL revision) is considered invalid
 
1188
        """
 
1189
        if revno < 1 or revno > self.revno():
 
1190
            raise InvalidRevisionNumber(revno)
 
1191
        
 
1192
        
 
1193
        
 
1194
 
 
1195
 
 
1196
class ScratchBranch(LocalBranch):
1089
1197
    """Special test class: a branch that cleans up after itself.
1090
1198
 
1091
1199
    >>> b = ScratchBranch()
1108
1216
        if base is None:
1109
1217
            base = mkdtemp()
1110
1218
            init = True
1111
 
        Branch.__init__(self, base, init=init)
 
1219
        LocalBranch.__init__(self, base, init=init)
1112
1220
        for d in dirs:
1113
1221
            os.mkdir(self.abspath(d))
1114
1222
            
1120
1228
        """
1121
1229
        >>> orig = ScratchBranch(files=["file1", "file2"])
1122
1230
        >>> clone = orig.clone()
1123
 
        >>> os.path.samefile(orig.base, clone.base)
 
1231
        >>> if os.name != 'nt':
 
1232
        ...   os.path.samefile(orig.base, clone.base)
 
1233
        ... else:
 
1234
        ...   orig.base == clone.base
 
1235
        ...
1124
1236
        False
1125
1237
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1126
1238
        True
1131
1243
        os.rmdir(base)
1132
1244
        copytree(self.base, base, symlinks=True)
1133
1245
        return ScratchBranch(base=base)
 
1246
 
 
1247
 
1134
1248
        
1135
1249
    def __del__(self):
1136
1250
        self.destroy()
1200
1314
 
1201
1315
    s = hexlify(rand_bytes(8))
1202
1316
    return '-'.join((name, compact_date(time()), s))
 
1317
 
 
1318
 
 
1319
def gen_root_id():
 
1320
    """Return a new tree-root file id."""
 
1321
    return gen_file_id('TREE_ROOT')
 
1322
 
 
1323
 
 
1324
def copy_branch(branch_from, to_location, revno=None):
 
1325
    """Copy branch_from into the existing directory to_location.
 
1326
 
 
1327
    revision
 
1328
        If not None, only revisions up to this point will be copied.
 
1329
        The head of the new branch will be that revision.
 
1330
 
 
1331
    to_location
 
1332
        The name of a local directory that exists but is empty.
 
1333
    """
 
1334
    from bzrlib.merge import merge
 
1335
 
 
1336
    assert isinstance(branch_from, Branch)
 
1337
    assert isinstance(to_location, basestring)
 
1338
    
 
1339
    br_to = Branch.initialize(to_location)
 
1340
    br_to.set_root_id(branch_from.get_root_id())
 
1341
    if revno is None:
 
1342
        revno = branch_from.revno()
 
1343
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1344
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1345
          check_clean=False, ignore_zero=True)
 
1346
    br_to.set_parent(branch_from.base)
 
1347
    return br_to