~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-06-11 01:55:29 UTC
  • Revision ID: mbp@sourcefrog.net-20050611015529-26a9c988699de34f
- help formatting fix from ndim

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
 
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
 
19
import traceback, socket, fnmatch, difflib, time
 
20
from binascii import hexlify
20
21
 
21
22
import bzrlib
22
 
from bzrlib.trace import mutter, note
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
29
 
from bzrlib.textui import show_status
30
 
from bzrlib.revision import Revision
31
 
from bzrlib.xml import unpack_xml
32
 
from bzrlib.delta import compare_trees
33
 
from bzrlib.tree import EmptyTree, RevisionTree
34
 
import bzrlib.ui
35
 
 
36
 
 
 
23
from inventory import Inventory
 
24
from trace import mutter, note
 
25
from tree import Tree, EmptyTree, RevisionTree
 
26
from inventory import InventoryEntry, Inventory
 
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
 
28
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
 
29
     joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath
 
30
from store import ImmutableStore
 
31
from revision import Revision
 
32
from errors import BzrError
 
33
from textui import show_status
37
34
 
38
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
36
## TODO: Maybe include checks for common corruption of newlines, etc?
40
37
 
41
38
 
42
 
# TODO: Some operations like log might retrieve the same revisions
43
 
# repeatedly to calculate deltas.  We could perhaps have a weakref
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
39
 
50
40
def find_branch(f, **args):
51
41
    if f and (f.startswith('http://') or f.startswith('https://')):
55
45
        return Branch(f, **args)
56
46
 
57
47
 
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
 
 
74
48
 
75
49
def _relpath(base, path):
76
50
    """Return path relative to base, or raise exception.
108
82
    It is not necessary that f exists.
109
83
 
110
84
    Basically we keep looking up until we find the control directory or
111
 
    run into the root.  If there isn't one, raises NotBranchError.
112
 
    """
 
85
    run into the root."""
113
86
    if f == None:
114
87
        f = os.getcwd()
115
88
    elif hasattr(os.path, 'realpath'):
128
101
        head, tail = os.path.split(f)
129
102
        if head == f:
130
103
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
104
            raise BzrError('%r is not in a branch' % orig_f)
132
105
        f = head
133
 
 
134
 
 
135
 
 
136
 
# XXX: move into bzrlib.errors; subclass BzrError    
 
106
    
137
107
class DivergedBranches(Exception):
138
108
    def __init__(self, branch1, branch2):
139
109
        self.branch1 = branch1
140
110
        self.branch2 = branch2
141
111
        Exception.__init__(self, "These branches have diverged.")
142
112
 
143
 
 
144
113
######################################################################
145
114
# branch objects
146
115
 
165
134
    _lock_count = None
166
135
    _lock = None
167
136
    
168
 
    # Map some sort of prefix into a namespace
169
 
    # stuff like "revno:10", "revid:", etc.
170
 
    # This should match a prefix with a function which accepts
171
 
    REVISION_NAMESPACES = {}
172
 
 
173
137
    def __init__(self, base, init=False, find_root=True):
174
138
        """Create new branch object at a particular location.
175
139
 
185
149
        In the test suite, creation of new trees is tested using the
186
150
        `ScratchBranch` class.
187
151
        """
188
 
        from bzrlib.store import ImmutableStore
189
152
        if init:
190
153
            self.base = os.path.realpath(base)
191
154
            self._make_control()
219
182
            self._lock.unlock()
220
183
 
221
184
 
 
185
 
222
186
    def lock_write(self):
223
187
        if self._lock_mode:
224
188
            if self._lock_mode != 'w':
234
198
            self._lock_count = 1
235
199
 
236
200
 
 
201
 
237
202
    def lock_read(self):
238
203
        if self._lock_mode:
239
204
            assert self._lock_mode in ('r', 'w'), \
246
211
            self._lock_mode = 'r'
247
212
            self._lock_count = 1
248
213
                        
 
214
 
 
215
            
249
216
    def unlock(self):
250
217
        if not self._lock_mode:
251
218
            from errors import LockError
258
225
            self._lock = None
259
226
            self._lock_mode = self._lock_count = None
260
227
 
 
228
 
261
229
    def abspath(self, name):
262
230
        """Return absolute filename for something in the branch"""
263
231
        return os.path.join(self.base, name)
264
232
 
 
233
 
265
234
    def relpath(self, path):
266
235
        """Return path relative to this branch of something inside it.
267
236
 
268
237
        Raises an error if path is not in this branch."""
269
238
        return _relpath(self.base, path)
270
239
 
 
240
 
271
241
    def controlfilename(self, file_or_path):
272
242
        """Return location relative to branch."""
273
 
        if isinstance(file_or_path, basestring):
 
243
        if isinstance(file_or_path, types.StringTypes):
274
244
            file_or_path = [file_or_path]
275
245
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
276
246
 
300
270
        else:
301
271
            raise BzrError("invalid controlfile mode %r" % mode)
302
272
 
 
273
 
 
274
 
303
275
    def _make_control(self):
304
 
        from bzrlib.inventory import Inventory
305
 
        from bzrlib.xml import pack_xml
306
 
        
307
276
        os.mkdir(self.controlfilename([]))
308
277
        self.controlfile('README', 'w').write(
309
278
            "This is a Bazaar-NG control directory.\n"
310
 
            "Do not change any files in this directory.\n")
 
279
            "Do not change any files in this directory.")
311
280
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
312
281
        for d in ('text-store', 'inventory-store', 'revision-store'):
313
282
            os.mkdir(self.controlfilename(d))
314
283
        for f in ('revision-history', 'merged-patches',
315
284
                  'pending-merged-patches', 'branch-name',
316
 
                  'branch-lock',
317
 
                  'pending-merges'):
 
285
                  'branch-lock'):
318
286
            self.controlfile(f, 'w').write('')
319
287
        mutter('created control directory in ' + self.base)
 
288
        Inventory().write_xml(self.controlfile('inventory','w'))
320
289
 
321
 
        # if we want per-tree root ids then this is the place to set
322
 
        # them; they're not needed for now and so ommitted for
323
 
        # simplicity.
324
 
        pack_xml(Inventory(), self.controlfile('inventory','w'))
325
290
 
326
291
    def _check_format(self):
327
292
        """Check this branch format is supported.
341
306
                           ['use a different bzr version',
342
307
                            'or remove the .bzr directory and "bzr init" again'])
343
308
 
344
 
    def get_root_id(self):
345
 
        """Return the id of this branches root"""
346
 
        inv = self.read_working_inventory()
347
 
        return inv.root.file_id
348
309
 
349
 
    def set_root_id(self, file_id):
350
 
        inv = self.read_working_inventory()
351
 
        orig_root_id = inv.root.file_id
352
 
        del inv._byid[inv.root.file_id]
353
 
        inv.root.file_id = file_id
354
 
        inv._byid[inv.root.file_id] = inv.root
355
 
        for fid in inv:
356
 
            entry = inv[fid]
357
 
            if entry.parent_id in (None, orig_root_id):
358
 
                entry.parent_id = inv.root.file_id
359
 
        self._write_inventory(inv)
360
310
 
361
311
    def read_working_inventory(self):
362
312
        """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()
 
313
        before = time.time()
 
314
        # ElementTree does its own conversion from UTF-8, so open in
 
315
        # binary.
367
316
        self.lock_read()
368
317
        try:
369
 
            # ElementTree does its own conversion from UTF-8, so open in
370
 
            # binary.
371
 
            inv = unpack_xml(Inventory,
372
 
                             self.controlfile('inventory', 'rb'))
 
318
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
373
319
            mutter("loaded inventory of %d items in %f"
374
 
                   % (len(inv), time() - before))
 
320
                   % (len(inv), time.time() - before))
375
321
            return inv
376
322
        finally:
377
323
            self.unlock()
383
329
        That is to say, the inventory describing changes underway, that
384
330
        will be committed to the next revision.
385
331
        """
386
 
        from bzrlib.atomicfile import AtomicFile
387
 
        from bzrlib.xml import pack_xml
388
 
        
389
 
        self.lock_write()
390
 
        try:
391
 
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
392
 
            try:
393
 
                pack_xml(inv, f)
394
 
                f.commit()
395
 
            finally:
396
 
                f.close()
397
 
        finally:
398
 
            self.unlock()
399
 
        
 
332
        ## TODO: factor out to atomicfile?  is rename safe on windows?
 
333
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
 
334
        tmpfname = self.controlfilename('inventory.tmp')
 
335
        tmpf = file(tmpfname, 'wb')
 
336
        inv.write_xml(tmpf)
 
337
        tmpf.close()
 
338
        inv_fname = self.controlfilename('inventory')
 
339
        if sys.platform == 'win32':
 
340
            os.remove(inv_fname)
 
341
        os.rename(tmpfname, inv_fname)
400
342
        mutter('wrote working inventory')
401
343
            
402
344
 
432
374
        """
433
375
        # TODO: Re-adding a file that is removed in the working copy
434
376
        # should probably put it back with the previous ID.
435
 
        if isinstance(files, basestring):
436
 
            assert(ids is None or isinstance(ids, basestring))
 
377
        if isinstance(files, types.StringTypes):
 
378
            assert(ids is None or isinstance(ids, types.StringTypes))
437
379
            files = [files]
438
380
            if ids is not None:
439
381
                ids = [ids]
471
413
                inv.add_path(f, kind=kind, file_id=file_id)
472
414
 
473
415
                if verbose:
474
 
                    print 'added', quotefn(f)
 
416
                    show_status('A', kind, quotefn(f))
475
417
 
476
418
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
477
419
 
488
430
            # use inventory as it was in that revision
489
431
            file_id = tree.inventory.path2id(file)
490
432
            if not file_id:
491
 
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
433
                raise BzrError("%r is not present in revision %d" % (file, revno))
492
434
            tree.print_file(file_id)
493
435
        finally:
494
436
            self.unlock()
510
452
        """
511
453
        ## TODO: Normalize names
512
454
        ## TODO: Remove nested loops; better scalability
513
 
        if isinstance(files, basestring):
 
455
        if isinstance(files, types.StringTypes):
514
456
            files = [files]
515
457
 
516
458
        self.lock_write()
541
483
 
542
484
    # FIXME: this doesn't need to be a branch method
543
485
    def set_inventory(self, new_inventory_list):
544
 
        from bzrlib.inventory import Inventory, InventoryEntry
545
 
        inv = Inventory(self.get_root_id())
 
486
        inv = Inventory()
546
487
        for path, file_id, parent, kind in new_inventory_list:
547
488
            name = os.path.basename(path)
548
489
            if name == "":
570
511
        return self.working_tree().unknowns()
571
512
 
572
513
 
573
 
    def append_revision(self, *revision_ids):
574
 
        from bzrlib.atomicfile import AtomicFile
575
 
 
576
 
        for revision_id in revision_ids:
577
 
            mutter("add {%s} to revision-history" % revision_id)
578
 
 
 
514
    def append_revision(self, revision_id):
 
515
        mutter("add {%s} to revision-history" % revision_id)
579
516
        rev_history = self.revision_history()
580
 
        rev_history.extend(revision_ids)
581
 
 
582
 
        f = AtomicFile(self.controlfilename('revision-history'))
583
 
        try:
584
 
            for rev_id in rev_history:
585
 
                print >>f, rev_id
586
 
            f.commit()
587
 
        finally:
588
 
            f.close()
589
 
 
590
 
 
591
 
    def get_revision_xml(self, revision_id):
592
 
        """Return XML file object for revision object."""
593
 
        if not revision_id or not isinstance(revision_id, basestring):
594
 
            raise InvalidRevisionId(revision_id)
595
 
 
596
 
        self.lock_read()
597
 
        try:
598
 
            try:
599
 
                return self.revision_store[revision_id]
600
 
            except IndexError:
601
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
602
 
        finally:
603
 
            self.unlock()
 
517
 
 
518
        tmprhname = self.controlfilename('revision-history.tmp')
 
519
        rhname = self.controlfilename('revision-history')
 
520
        
 
521
        f = file(tmprhname, 'wt')
 
522
        rev_history.append(revision_id)
 
523
        f.write('\n'.join(rev_history))
 
524
        f.write('\n')
 
525
        f.close()
 
526
 
 
527
        if sys.platform == 'win32':
 
528
            os.remove(rhname)
 
529
        os.rename(tmprhname, rhname)
 
530
        
604
531
 
605
532
 
606
533
    def get_revision(self, revision_id):
607
534
        """Return the Revision object for a named revision"""
608
 
        xml_file = self.get_revision_xml(revision_id)
609
 
 
610
 
        try:
611
 
            r = unpack_xml(Revision, xml_file)
612
 
        except SyntaxError, e:
613
 
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
614
 
                                         [revision_id,
615
 
                                          str(e)])
616
 
            
 
535
        if not revision_id or not isinstance(revision_id, basestring):
 
536
            raise ValueError('invalid revision-id: %r' % revision_id)
 
537
        r = Revision.read_xml(self.revision_store[revision_id])
617
538
        assert r.revision_id == revision_id
618
539
        return r
619
540
 
620
 
 
621
 
    def get_revision_delta(self, revno):
622
 
        """Return the delta for one revision.
623
 
 
624
 
        The delta is relative to its mainline predecessor, or the
625
 
        empty tree for revision 1.
626
 
        """
627
 
        assert isinstance(revno, int)
628
 
        rh = self.revision_history()
629
 
        if not (1 <= revno <= len(rh)):
630
 
            raise InvalidRevisionNumber(revno)
631
 
 
632
 
        # revno is 1-based; list is 0-based
633
 
 
634
 
        new_tree = self.revision_tree(rh[revno-1])
635
 
        if revno == 1:
636
 
            old_tree = EmptyTree()
637
 
        else:
638
 
            old_tree = self.revision_tree(rh[revno-2])
639
 
 
640
 
        return compare_trees(old_tree, new_tree)
641
 
 
642
 
        
643
 
 
644
541
    def get_revision_sha1(self, revision_id):
645
542
        """Hash the stored value of a revision, and return it."""
646
543
        # In the future, revision entries will be signed. At that
649
546
        # the revision, (add signatures/remove signatures) and still
650
547
        # have all hash pointers stay consistent.
651
548
        # But for now, just hash the contents.
652
 
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
549
        return sha_file(self.revision_store[revision_id])
653
550
 
654
551
 
655
552
    def get_inventory(self, inventory_id):
658
555
        TODO: Perhaps for this and similar methods, take a revision
659
556
               parameter which can be either an integer revno or a
660
557
               string hash."""
661
 
        from bzrlib.inventory import Inventory
662
 
        from bzrlib.xml import unpack_xml
663
 
 
664
 
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
665
 
 
666
 
 
667
 
    def get_inventory_xml(self, inventory_id):
668
 
        """Get inventory XML as a file object."""
669
 
        return self.inventory_store[inventory_id]
670
 
            
 
558
        i = Inventory.read_xml(self.inventory_store[inventory_id])
 
559
        return i
671
560
 
672
561
    def get_inventory_sha1(self, inventory_id):
673
562
        """Return the sha1 hash of the inventory entry
674
563
        """
675
 
        return sha_file(self.get_inventory_xml(inventory_id))
 
564
        return sha_file(self.inventory_store[inventory_id])
676
565
 
677
566
 
678
567
    def get_revision_inventory(self, revision_id):
679
568
        """Return inventory of a past revision."""
680
 
        # bzr 0.0.6 imposes the constraint that the inventory_id
681
 
        # must be the same as its revision, so this is trivial.
682
569
        if revision_id == None:
683
 
            from bzrlib.inventory import Inventory
684
 
            return Inventory(self.get_root_id())
 
570
            return Inventory()
685
571
        else:
686
 
            return self.get_inventory(revision_id)
 
572
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
687
573
 
688
574
 
689
575
    def revision_history(self):
744
630
                return r+1, my_history[r]
745
631
        return None, None
746
632
 
 
633
    def enum_history(self, direction):
 
634
        """Return (revno, revision_id) for history of branch.
 
635
 
 
636
        direction
 
637
            'forward' is from earliest to latest
 
638
            'reverse' is from latest to earliest
 
639
        """
 
640
        rh = self.revision_history()
 
641
        if direction == 'forward':
 
642
            i = 1
 
643
            for rid in rh:
 
644
                yield i, rid
 
645
                i += 1
 
646
        elif direction == 'reverse':
 
647
            i = len(rh)
 
648
            while i > 0:
 
649
                yield i, rh[i-1]
 
650
                i -= 1
 
651
        else:
 
652
            raise ValueError('invalid history direction', direction)
 
653
 
747
654
 
748
655
    def revno(self):
749
656
        """Return current revision number for this branch.
764
671
            return None
765
672
 
766
673
 
767
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
674
    def missing_revisions(self, other):
768
675
        """
769
676
        If self and other have not diverged, return a list of the revisions
770
677
        present in other, but missing from self.
799
706
        if common_index >= 0 and \
800
707
            self_history[common_index] != other_history[common_index]:
801
708
            raise DivergedBranches(self, other)
802
 
 
803
 
        if stop_revision is None:
804
 
            stop_revision = other_len
805
 
        elif stop_revision > other_len:
806
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
807
 
        
808
 
        return other_history[self_len:stop_revision]
809
 
 
810
 
 
811
 
    def update_revisions(self, other, stop_revision=None):
 
709
        if self_len < other_len:
 
710
            return other_history[self_len:]
 
711
        return []
 
712
 
 
713
 
 
714
    def update_revisions(self, other):
812
715
        """Pull in all new revisions from other branch.
 
716
        
 
717
        >>> from bzrlib.commit import commit
 
718
        >>> bzrlib.trace.silent = True
 
719
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
720
        >>> br1.add('foo')
 
721
        >>> br1.add('bar')
 
722
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
723
        >>> br2 = ScratchBranch()
 
724
        >>> br2.update_revisions(br1)
 
725
        Added 2 texts.
 
726
        Added 1 inventories.
 
727
        Added 1 revisions.
 
728
        >>> br2.revision_history()
 
729
        [u'REVISION-ID-1']
 
730
        >>> br2.update_revisions(br1)
 
731
        Added 0 texts.
 
732
        Added 0 inventories.
 
733
        Added 0 revisions.
 
734
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
735
        True
813
736
        """
814
 
        from bzrlib.fetch import greedy_fetch
815
 
 
816
 
        pb = bzrlib.ui.ui_factory.progress_bar()
 
737
        from bzrlib.progress import ProgressBar
 
738
 
 
739
        pb = ProgressBar()
 
740
 
817
741
        pb.update('comparing histories')
818
 
 
819
 
        revision_ids = self.missing_revisions(other, stop_revision)
820
 
 
821
 
        if len(revision_ids) > 0:
822
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
823
 
        else:
824
 
            count = 0
825
 
        self.append_revision(*revision_ids)
826
 
        ## note("Added %d revisions." % count)
827
 
        pb.clear()
828
 
 
829
 
    def install_revisions(self, other, revision_ids, pb):
830
 
        if hasattr(other.revision_store, "prefetch"):
831
 
            other.revision_store.prefetch(revision_ids)
832
 
        if hasattr(other.inventory_store, "prefetch"):
833
 
            inventory_ids = [other.get_revision(r).inventory_id
834
 
                             for r in revision_ids]
835
 
            other.inventory_store.prefetch(inventory_ids)
836
 
 
837
 
        if pb is None:
838
 
            pb = bzrlib.ui.ui_factory.progress_bar()
839
 
                
 
742
        revision_ids = self.missing_revisions(other)
840
743
        revisions = []
841
 
        needed_texts = set()
 
744
        needed_texts = sets.Set()
842
745
        i = 0
843
 
 
844
 
        failures = set()
845
 
        for i, rev_id in enumerate(revision_ids):
846
 
            pb.update('fetching revision', i+1, len(revision_ids))
847
 
            try:
848
 
                rev = other.get_revision(rev_id)
849
 
            except bzrlib.errors.NoSuchRevision:
850
 
                failures.add(rev_id)
851
 
                continue
852
 
 
 
746
        for rev_id in revision_ids:
 
747
            i += 1
 
748
            pb.update('fetching revision', i, len(revision_ids))
 
749
            rev = other.get_revision(rev_id)
853
750
            revisions.append(rev)
854
751
            inv = other.get_inventory(str(rev.inventory_id))
855
752
            for key, entry in inv.iter_entries():
860
757
 
861
758
        pb.clear()
862
759
                    
863
 
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
864
 
                                                    needed_texts)
865
 
        #print "Added %d texts." % count 
 
760
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
761
        print "Added %d texts." % count 
866
762
        inventory_ids = [ f.inventory_id for f in revisions ]
867
 
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
868
 
                                                         inventory_ids)
869
 
        #print "Added %d inventories." % count 
 
763
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
764
                                                inventory_ids)
 
765
        print "Added %d inventories." % count 
870
766
        revision_ids = [ f.revision_id for f in revisions]
871
 
 
872
 
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
873
 
                                                          revision_ids,
874
 
                                                          permit_failure=True)
875
 
        assert len(cp_fail) == 0 
876
 
        return count, failures
877
 
       
878
 
 
 
767
        count = self.revision_store.copy_multi(other.revision_store, 
 
768
                                               revision_ids)
 
769
        for revision_id in revision_ids:
 
770
            self.append_revision(revision_id)
 
771
        print "Added %d revisions." % count
 
772
                    
 
773
        
879
774
    def commit(self, *args, **kw):
 
775
        """Deprecated"""
880
776
        from bzrlib.commit import commit
881
777
        commit(self, *args, **kw)
882
778
        
883
779
 
884
 
    def lookup_revision(self, revision):
885
 
        """Return the revision identifier for a given revision information."""
886
 
        revno, info = self._get_revision_info(revision)
887
 
        return info
888
 
 
889
 
 
890
 
    def revision_id_to_revno(self, revision_id):
891
 
        """Given a revision id, return its revno"""
892
 
        history = self.revision_history()
893
 
        try:
894
 
            return history.index(revision_id) + 1
895
 
        except ValueError:
896
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
897
 
 
898
 
 
899
 
    def get_revision_info(self, revision):
900
 
        """Return (revno, revision id) for revision identifier.
901
 
 
902
 
        revision can be an integer, in which case it is assumed to be revno (though
903
 
            this will translate negative values into positive ones)
904
 
        revision can also be a string, in which case it is parsed for something like
905
 
            'date:' or 'revid:' etc.
906
 
        """
907
 
        revno, rev_id = self._get_revision_info(revision)
908
 
        if revno is None:
909
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
910
 
        return revno, rev_id
911
 
 
912
 
    def get_rev_id(self, revno, history=None):
913
 
        """Find the revision id of the specified revno."""
 
780
    def lookup_revision(self, revno):
 
781
        """Return revision hash for revision number."""
914
782
        if revno == 0:
915
783
            return None
916
 
        if history is None:
917
 
            history = self.revision_history()
918
 
        elif revno <= 0 or revno > len(history):
919
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
920
 
        return history[revno - 1]
921
 
 
922
 
    def _get_revision_info(self, revision):
923
 
        """Return (revno, revision id) for revision specifier.
924
 
 
925
 
        revision can be an integer, in which case it is assumed to be revno
926
 
        (though this will translate negative values into positive ones)
927
 
        revision can also be a string, in which case it is parsed for something
928
 
        like 'date:' or 'revid:' etc.
929
 
 
930
 
        A revid is always returned.  If it is None, the specifier referred to
931
 
        the null revision.  If the revid does not occur in the revision
932
 
        history, revno will be None.
933
 
        """
934
 
        
935
 
        if revision is None:
936
 
            return 0, None
937
 
        revno = None
938
 
        try:# Convert to int if possible
939
 
            revision = int(revision)
940
 
        except ValueError:
941
 
            pass
942
 
        revs = self.revision_history()
943
 
        if isinstance(revision, int):
944
 
            if revision < 0:
945
 
                revno = len(revs) + revision + 1
946
 
            else:
947
 
                revno = revision
948
 
            rev_id = self.get_rev_id(revno, revs)
949
 
        elif isinstance(revision, basestring):
950
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
951
 
                if revision.startswith(prefix):
952
 
                    result = func(self, revs, revision)
953
 
                    if len(result) > 1:
954
 
                        revno, rev_id = result
955
 
                    else:
956
 
                        revno = result[0]
957
 
                        rev_id = self.get_rev_id(revno, revs)
958
 
                    break
959
 
            else:
960
 
                raise BzrError('No namespace registered for string: %r' %
961
 
                               revision)
962
 
        else:
963
 
            raise TypeError('Unhandled revision type %s' % revision)
964
 
 
965
 
        if revno is None:
966
 
            if rev_id is None:
967
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
968
 
        return revno, rev_id
969
 
 
970
 
    def _namespace_revno(self, revs, revision):
971
 
        """Lookup a revision by revision number"""
972
 
        assert revision.startswith('revno:')
973
 
        try:
974
 
            return (int(revision[6:]),)
975
 
        except ValueError:
976
 
            return None
977
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
978
 
 
979
 
    def _namespace_revid(self, revs, revision):
980
 
        assert revision.startswith('revid:')
981
 
        rev_id = revision[len('revid:'):]
982
 
        try:
983
 
            return revs.index(rev_id) + 1, rev_id
984
 
        except ValueError:
985
 
            return None, rev_id
986
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
987
 
 
988
 
    def _namespace_last(self, revs, revision):
989
 
        assert revision.startswith('last:')
990
 
        try:
991
 
            offset = int(revision[5:])
992
 
        except ValueError:
993
 
            return (None,)
994
 
        else:
995
 
            if offset <= 0:
996
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
997
 
            return (len(revs) - offset + 1,)
998
 
    REVISION_NAMESPACES['last:'] = _namespace_last
999
 
 
1000
 
    def _namespace_tag(self, revs, revision):
1001
 
        assert revision.startswith('tag:')
1002
 
        raise BzrError('tag: namespace registered, but not implemented.')
1003
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
1004
 
 
1005
 
    def _namespace_date(self, revs, revision):
1006
 
        assert revision.startswith('date:')
1007
 
        import datetime
1008
 
        # Spec for date revisions:
1009
 
        #   date:value
1010
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1011
 
        #   it can also start with a '+/-/='. '+' says match the first
1012
 
        #   entry after the given date. '-' is match the first entry before the date
1013
 
        #   '=' is match the first entry after, but still on the given date.
1014
 
        #
1015
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1016
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1017
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1018
 
        #       May 13th, 2005 at 0:00
1019
 
        #
1020
 
        #   So the proper way of saying 'give me all entries for today' is:
1021
 
        #       -r {date:+today}:{date:-tomorrow}
1022
 
        #   The default is '=' when not supplied
1023
 
        val = revision[5:]
1024
 
        match_style = '='
1025
 
        if val[:1] in ('+', '-', '='):
1026
 
            match_style = val[:1]
1027
 
            val = val[1:]
1028
 
 
1029
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1030
 
        if val.lower() == 'yesterday':
1031
 
            dt = today - datetime.timedelta(days=1)
1032
 
        elif val.lower() == 'today':
1033
 
            dt = today
1034
 
        elif val.lower() == 'tomorrow':
1035
 
            dt = today + datetime.timedelta(days=1)
1036
 
        else:
1037
 
            import re
1038
 
            # This should be done outside the function to avoid recompiling it.
1039
 
            _date_re = re.compile(
1040
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1041
 
                    r'(,|T)?\s*'
1042
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1043
 
                )
1044
 
            m = _date_re.match(val)
1045
 
            if not m or (not m.group('date') and not m.group('time')):
1046
 
                raise BzrError('Invalid revision date %r' % revision)
1047
 
 
1048
 
            if m.group('date'):
1049
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1050
 
            else:
1051
 
                year, month, day = today.year, today.month, today.day
1052
 
            if m.group('time'):
1053
 
                hour = int(m.group('hour'))
1054
 
                minute = int(m.group('minute'))
1055
 
                if m.group('second'):
1056
 
                    second = int(m.group('second'))
1057
 
                else:
1058
 
                    second = 0
1059
 
            else:
1060
 
                hour, minute, second = 0,0,0
1061
 
 
1062
 
            dt = datetime.datetime(year=year, month=month, day=day,
1063
 
                    hour=hour, minute=minute, second=second)
1064
 
        first = dt
1065
 
        last = None
1066
 
        reversed = False
1067
 
        if match_style == '-':
1068
 
            reversed = True
1069
 
        elif match_style == '=':
1070
 
            last = dt + datetime.timedelta(days=1)
1071
 
 
1072
 
        if reversed:
1073
 
            for i in range(len(revs)-1, -1, -1):
1074
 
                r = self.get_revision(revs[i])
1075
 
                # TODO: Handle timezone.
1076
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1077
 
                if first >= dt and (last is None or dt >= last):
1078
 
                    return (i+1,)
1079
 
        else:
1080
 
            for i in range(len(revs)):
1081
 
                r = self.get_revision(revs[i])
1082
 
                # TODO: Handle timezone.
1083
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1084
 
                if first <= dt and (last is None or dt <= last):
1085
 
                    return (i+1,)
1086
 
    REVISION_NAMESPACES['date:'] = _namespace_date
 
784
 
 
785
        try:
 
786
            # list is 0-based; revisions are 1-based
 
787
            return self.revision_history()[revno-1]
 
788
        except IndexError:
 
789
            raise BzrError("no such revision %s" % revno)
 
790
 
1087
791
 
1088
792
    def revision_tree(self, revision_id):
1089
793
        """Return Tree for a revision on this branch.
1233
937
            self.unlock()
1234
938
 
1235
939
 
1236
 
    def revert(self, filenames, old_tree=None, backups=True):
1237
 
        """Restore selected files to the versions from a previous tree.
1238
 
 
1239
 
        backups
1240
 
            If true (default) backups are made of files before
1241
 
            they're renamed.
1242
 
        """
1243
 
        from bzrlib.errors import NotVersionedError, BzrError
1244
 
        from bzrlib.atomicfile import AtomicFile
1245
 
        from bzrlib.osutils import backup_file
1246
 
        
1247
 
        inv = self.read_working_inventory()
1248
 
        if old_tree is None:
1249
 
            old_tree = self.basis_tree()
1250
 
        old_inv = old_tree.inventory
1251
 
 
1252
 
        nids = []
1253
 
        for fn in filenames:
1254
 
            file_id = inv.path2id(fn)
1255
 
            if not file_id:
1256
 
                raise NotVersionedError("not a versioned file", fn)
1257
 
            if not old_inv.has_id(file_id):
1258
 
                raise BzrError("file not present in old tree", fn, file_id)
1259
 
            nids.append((fn, file_id))
1260
 
            
1261
 
        # TODO: Rename back if it was previously at a different location
1262
 
 
1263
 
        # TODO: If given a directory, restore the entire contents from
1264
 
        # the previous version.
1265
 
 
1266
 
        # TODO: Make a backup to a temporary file.
1267
 
 
1268
 
        # TODO: If the file previously didn't exist, delete it?
1269
 
        for fn, file_id in nids:
1270
 
            backup_file(fn)
1271
 
            
1272
 
            f = AtomicFile(fn, 'wb')
1273
 
            try:
1274
 
                f.write(old_tree.get_file(file_id).read())
1275
 
                f.commit()
1276
 
            finally:
1277
 
                f.close()
1278
 
 
1279
 
 
1280
 
    def pending_merges(self):
1281
 
        """Return a list of pending merges.
1282
 
 
1283
 
        These are revisions that have been merged into the working
1284
 
        directory but not yet committed.
1285
 
        """
1286
 
        cfn = self.controlfilename('pending-merges')
1287
 
        if not os.path.exists(cfn):
1288
 
            return []
1289
 
        p = []
1290
 
        for l in self.controlfile('pending-merges', 'r').readlines():
1291
 
            p.append(l.rstrip('\n'))
1292
 
        return p
1293
 
 
1294
 
 
1295
 
    def add_pending_merge(self, revision_id):
1296
 
        from bzrlib.revision import validate_revision_id
1297
 
 
1298
 
        validate_revision_id(revision_id)
1299
 
 
1300
 
        p = self.pending_merges()
1301
 
        if revision_id in p:
1302
 
            return
1303
 
        p.append(revision_id)
1304
 
        self.set_pending_merges(p)
1305
 
 
1306
 
 
1307
 
    def set_pending_merges(self, rev_list):
1308
 
        from bzrlib.atomicfile import AtomicFile
1309
 
        self.lock_write()
1310
 
        try:
1311
 
            f = AtomicFile(self.controlfilename('pending-merges'))
1312
 
            try:
1313
 
                for l in rev_list:
1314
 
                    print >>f, l
1315
 
                f.commit()
1316
 
            finally:
1317
 
                f.close()
1318
 
        finally:
1319
 
            self.unlock()
1320
 
 
1321
 
 
1322
940
 
1323
941
class ScratchBranch(Branch):
1324
942
    """Special test class: a branch that cleans up after itself.
1338
956
 
1339
957
        If any files are listed, they are created in the working copy.
1340
958
        """
1341
 
        from tempfile import mkdtemp
1342
959
        init = False
1343
960
        if base is None:
1344
 
            base = mkdtemp()
 
961
            base = tempfile.mkdtemp()
1345
962
            init = True
1346
963
        Branch.__init__(self, base, init=init)
1347
964
        for d in dirs:
1360
977
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1361
978
        True
1362
979
        """
1363
 
        from shutil import copytree
1364
 
        from tempfile import mkdtemp
1365
 
        base = mkdtemp()
 
980
        base = tempfile.mkdtemp()
1366
981
        os.rmdir(base)
1367
 
        copytree(self.base, base, symlinks=True)
 
982
        shutil.copytree(self.base, base, symlinks=True)
1368
983
        return ScratchBranch(base=base)
1369
984
        
1370
985
    def __del__(self):
1372
987
 
1373
988
    def destroy(self):
1374
989
        """Destroy the test branch, removing the scratch directory."""
1375
 
        from shutil import rmtree
1376
990
        try:
1377
991
            if self.base:
1378
992
                mutter("delete ScratchBranch %s" % self.base)
1379
 
                rmtree(self.base)
 
993
                shutil.rmtree(self.base)
1380
994
        except OSError, e:
1381
995
            # Work around for shutil.rmtree failing on Windows when
1382
996
            # readonly files are encountered
1384
998
            for root, dirs, files in os.walk(self.base, topdown=False):
1385
999
                for name in files:
1386
1000
                    os.chmod(os.path.join(root, name), 0700)
1387
 
            rmtree(self.base)
 
1001
            shutil.rmtree(self.base)
1388
1002
        self.base = None
1389
1003
 
1390
1004
    
1415
1029
    cope with just randomness because running uuidgen every time is
1416
1030
    slow."""
1417
1031
    import re
1418
 
    from binascii import hexlify
1419
 
    from time import time
1420
1032
 
1421
1033
    # get last component
1422
1034
    idx = name.rfind('/')
1434
1046
    name = re.sub(r'[^\w.]', '', name)
1435
1047
 
1436
1048
    s = hexlify(rand_bytes(8))
1437
 
    return '-'.join((name, compact_date(time()), s))
1438
 
 
1439
 
 
1440
 
def gen_root_id():
1441
 
    """Return a new tree-root file id."""
1442
 
    return gen_file_id('TREE_ROOT')
1443
 
 
1444
 
 
1445
 
def pull_loc(branch):
1446
 
    # TODO: Should perhaps just make attribute be 'base' in
1447
 
    # RemoteBranch and Branch?
1448
 
    if hasattr(branch, "baseurl"):
1449
 
        return branch.baseurl
1450
 
    else:
1451
 
        return branch.base
1452
 
 
1453
 
 
1454
 
def copy_branch(branch_from, to_location, revision=None):
1455
 
    """Copy branch_from into the existing directory to_location.
1456
 
 
1457
 
    If revision is not None, the head of the new branch will be revision.
1458
 
    """
1459
 
    from bzrlib.merge import merge
1460
 
    from bzrlib.branch import Branch
1461
 
    br_to = Branch(to_location, init=True)
1462
 
    br_to.set_root_id(branch_from.get_root_id())
1463
 
    if revision is None:
1464
 
        revno = branch_from.revno()
1465
 
    else:
1466
 
        revno, rev_id = branch_from.get_revision_info(revision)
1467
 
    br_to.update_revisions(branch_from, stop_revision=revno)
1468
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1469
 
          check_clean=False, ignore_zero=True)
1470
 
    from_location = pull_loc(branch_from)
1471
 
    br_to.controlfile("x-pull", "wb").write(from_location + "\n")
1472
 
 
 
1049
    return '-'.join((name, compact_date(time.time()), s))