~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: aaron.bentley at utoronto
  • Date: 2005-09-04 02:59:56 UTC
  • mfrom: (1172)
  • mto: (1185.3.4)
  • mto: This revision was merged to the branch mainline in revision 1178.
  • Revision ID: aaron.bentley@utoronto.ca-20050904025956-776ba4f07de97700
Merged mpool's latest changes (~0.0.7)

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, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
 
import traceback, socket, fnmatch, difflib, time
20
 
from binascii import hexlify
 
18
import sys
 
19
import os
21
20
 
22
21
import bzrlib
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
 
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
 
34
37
 
35
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
39
## TODO: Maybe include checks for common corruption of newlines, etc?
37
40
 
38
41
 
 
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
 
39
49
 
40
50
def find_branch(f, **args):
41
51
    if f and (f.startswith('http://') or f.startswith('https://')):
45
55
        return Branch(f, **args)
46
56
 
47
57
 
 
58
def find_cached_branch(f, cache_root, **args):
 
59
    from remotebranch import RemoteBranch
 
60
    br = find_branch(f, **args)
 
61
    def cacheify(br, store_name):
 
62
        from meta_store import CachedStore
 
63
        cache_path = os.path.join(cache_root, store_name)
 
64
        os.mkdir(cache_path)
 
65
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
66
        setattr(br, store_name, new_store)
 
67
 
 
68
    if isinstance(br, RemoteBranch):
 
69
        cacheify(br, 'inventory_store')
 
70
        cacheify(br, 'text_store')
 
71
        cacheify(br, 'revision_store')
 
72
    return br
 
73
 
48
74
 
49
75
def _relpath(base, path):
50
76
    """Return path relative to base, or raise exception.
82
108
    It is not necessary that f exists.
83
109
 
84
110
    Basically we keep looking up until we find the control directory or
85
 
    run into the root."""
 
111
    run into the root.  If there isn't one, raises NotBranchError.
 
112
    """
86
113
    if f == None:
87
114
        f = os.getcwd()
88
115
    elif hasattr(os.path, 'realpath'):
101
128
        head, tail = os.path.split(f)
102
129
        if head == f:
103
130
            # reached the root, whatever that may be
104
 
            raise BzrError('%r is not in a branch' % orig_f)
 
131
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
105
132
        f = head
106
 
    
 
133
 
 
134
 
 
135
 
 
136
# XXX: move into bzrlib.errors; subclass BzrError    
107
137
class DivergedBranches(Exception):
108
138
    def __init__(self, branch1, branch2):
109
139
        self.branch1 = branch1
111
141
        Exception.__init__(self, "These branches have diverged.")
112
142
 
113
143
 
114
 
class NoSuchRevision(BzrError):
115
 
    def __init__(self, branch, revision):
116
 
        self.branch = branch
117
 
        self.revision = revision
118
 
        msg = "Branch %s has no revision %d" % (branch, revision)
119
 
        BzrError.__init__(self, msg)
120
 
 
121
 
 
122
144
######################################################################
123
145
# branch objects
124
146
 
143
165
    _lock_count = None
144
166
    _lock = None
145
167
    
 
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
 
146
173
    def __init__(self, base, init=False, find_root=True):
147
174
        """Create new branch object at a particular location.
148
175
 
158
185
        In the test suite, creation of new trees is tested using the
159
186
        `ScratchBranch` class.
160
187
        """
 
188
        from bzrlib.store import ImmutableStore
161
189
        if init:
162
190
            self.base = os.path.realpath(base)
163
191
            self._make_control()
191
219
            self._lock.unlock()
192
220
 
193
221
 
194
 
 
195
222
    def lock_write(self):
196
223
        if self._lock_mode:
197
224
            if self._lock_mode != 'w':
207
234
            self._lock_count = 1
208
235
 
209
236
 
210
 
 
211
237
    def lock_read(self):
212
238
        if self._lock_mode:
213
239
            assert self._lock_mode in ('r', 'w'), \
220
246
            self._lock_mode = 'r'
221
247
            self._lock_count = 1
222
248
                        
223
 
 
224
 
            
225
249
    def unlock(self):
226
250
        if not self._lock_mode:
227
251
            from errors import LockError
234
258
            self._lock = None
235
259
            self._lock_mode = self._lock_count = None
236
260
 
237
 
 
238
261
    def abspath(self, name):
239
262
        """Return absolute filename for something in the branch"""
240
263
        return os.path.join(self.base, name)
241
264
 
242
 
 
243
265
    def relpath(self, path):
244
266
        """Return path relative to this branch of something inside it.
245
267
 
246
268
        Raises an error if path is not in this branch."""
247
269
        return _relpath(self.base, path)
248
270
 
249
 
 
250
271
    def controlfilename(self, file_or_path):
251
272
        """Return location relative to branch."""
252
 
        if isinstance(file_or_path, types.StringTypes):
 
273
        if isinstance(file_or_path, basestring):
253
274
            file_or_path = [file_or_path]
254
275
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
255
276
 
279
300
        else:
280
301
            raise BzrError("invalid controlfile mode %r" % mode)
281
302
 
282
 
 
283
 
 
284
303
    def _make_control(self):
 
304
        from bzrlib.inventory import Inventory
 
305
        from bzrlib.xml import pack_xml
 
306
        
285
307
        os.mkdir(self.controlfilename([]))
286
308
        self.controlfile('README', 'w').write(
287
309
            "This is a Bazaar-NG control directory.\n"
291
313
            os.mkdir(self.controlfilename(d))
292
314
        for f in ('revision-history', 'merged-patches',
293
315
                  'pending-merged-patches', 'branch-name',
294
 
                  'branch-lock'):
 
316
                  'branch-lock',
 
317
                  'pending-merges'):
295
318
            self.controlfile(f, 'w').write('')
296
319
        mutter('created control directory in ' + self.base)
297
 
        Inventory().write_xml(self.controlfile('inventory','w'))
298
320
 
 
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'))
299
325
 
300
326
    def _check_format(self):
301
327
        """Check this branch format is supported.
315
341
                           ['use a different bzr version',
316
342
                            'or remove the .bzr directory and "bzr init" again'])
317
343
 
 
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
318
348
 
 
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)
319
360
 
320
361
    def read_working_inventory(self):
321
362
        """Read the working inventory."""
322
 
        before = time.time()
323
 
        # ElementTree does its own conversion from UTF-8, so open in
324
 
        # binary.
 
363
        from bzrlib.inventory import Inventory
 
364
        from bzrlib.xml import unpack_xml
 
365
        from time import time
 
366
        before = time()
325
367
        self.lock_read()
326
368
        try:
327
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
369
            # ElementTree does its own conversion from UTF-8, so open in
 
370
            # binary.
 
371
            inv = unpack_xml(Inventory,
 
372
                             self.controlfile('inventory', 'rb'))
328
373
            mutter("loaded inventory of %d items in %f"
329
 
                   % (len(inv), time.time() - before))
 
374
                   % (len(inv), time() - before))
330
375
            return inv
331
376
        finally:
332
377
            self.unlock()
338
383
        That is to say, the inventory describing changes underway, that
339
384
        will be committed to the next revision.
340
385
        """
341
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
342
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
343
 
        tmpfname = self.controlfilename('inventory.tmp')
344
 
        tmpf = file(tmpfname, 'wb')
345
 
        inv.write_xml(tmpf)
346
 
        tmpf.close()
347
 
        inv_fname = self.controlfilename('inventory')
348
 
        if sys.platform == 'win32':
349
 
            os.remove(inv_fname)
350
 
        os.rename(tmpfname, inv_fname)
 
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
        
351
400
        mutter('wrote working inventory')
352
401
            
353
402
 
355
404
                         """Inventory for the working copy.""")
356
405
 
357
406
 
358
 
    def add(self, files, verbose=False, ids=None):
 
407
    def add(self, files, ids=None):
359
408
        """Make files versioned.
360
409
 
361
 
        Note that the command line normally calls smart_add instead.
 
410
        Note that the command line normally calls smart_add instead,
 
411
        which can automatically recurse.
362
412
 
363
413
        This puts the files in the Added state, so that they will be
364
414
        recorded by the next commit.
374
424
        TODO: Perhaps have an option to add the ids even if the files do
375
425
              not (yet) exist.
376
426
 
377
 
        TODO: Perhaps return the ids of the files?  But then again it
378
 
              is easy to retrieve them if they're needed.
379
 
 
380
 
        TODO: Adding a directory should optionally recurse down and
381
 
              add all non-ignored children.  Perhaps do that in a
382
 
              higher-level method.
 
427
        TODO: Perhaps yield the ids and paths as they're added.
383
428
        """
384
429
        # TODO: Re-adding a file that is removed in the working copy
385
430
        # should probably put it back with the previous ID.
386
 
        if isinstance(files, types.StringTypes):
387
 
            assert(ids is None or isinstance(ids, types.StringTypes))
 
431
        if isinstance(files, basestring):
 
432
            assert(ids is None or isinstance(ids, basestring))
388
433
            files = [files]
389
434
            if ids is not None:
390
435
                ids = [ids]
421
466
                    file_id = gen_file_id(f)
422
467
                inv.add_path(f, kind=kind, file_id=file_id)
423
468
 
424
 
                if verbose:
425
 
                    show_status('A', kind, quotefn(f))
426
 
 
427
469
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
428
470
 
429
471
            self._write_inventory(inv)
439
481
            # use inventory as it was in that revision
440
482
            file_id = tree.inventory.path2id(file)
441
483
            if not file_id:
442
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
484
                raise BzrError("%r is not present in revision %s" % (file, revno))
443
485
            tree.print_file(file_id)
444
486
        finally:
445
487
            self.unlock()
461
503
        """
462
504
        ## TODO: Normalize names
463
505
        ## TODO: Remove nested loops; better scalability
464
 
        if isinstance(files, types.StringTypes):
 
506
        if isinstance(files, basestring):
465
507
            files = [files]
466
508
 
467
509
        self.lock_write()
492
534
 
493
535
    # FIXME: this doesn't need to be a branch method
494
536
    def set_inventory(self, new_inventory_list):
495
 
        inv = Inventory()
 
537
        from bzrlib.inventory import Inventory, InventoryEntry
 
538
        inv = Inventory(self.get_root_id())
496
539
        for path, file_id, parent, kind in new_inventory_list:
497
540
            name = os.path.basename(path)
498
541
            if name == "":
520
563
        return self.working_tree().unknowns()
521
564
 
522
565
 
523
 
    def append_revision(self, revision_id):
524
 
        mutter("add {%s} to revision-history" % revision_id)
 
566
    def append_revision(self, *revision_ids):
 
567
        from bzrlib.atomicfile import AtomicFile
 
568
 
 
569
        for revision_id in revision_ids:
 
570
            mutter("add {%s} to revision-history" % revision_id)
 
571
 
525
572
        rev_history = self.revision_history()
526
 
 
527
 
        tmprhname = self.controlfilename('revision-history.tmp')
528
 
        rhname = self.controlfilename('revision-history')
529
 
        
530
 
        f = file(tmprhname, 'wt')
531
 
        rev_history.append(revision_id)
532
 
        f.write('\n'.join(rev_history))
533
 
        f.write('\n')
534
 
        f.close()
535
 
 
536
 
        if sys.platform == 'win32':
537
 
            os.remove(rhname)
538
 
        os.rename(tmprhname, rhname)
539
 
        
 
573
        rev_history.extend(revision_ids)
 
574
 
 
575
        f = AtomicFile(self.controlfilename('revision-history'))
 
576
        try:
 
577
            for rev_id in rev_history:
 
578
                print >>f, rev_id
 
579
            f.commit()
 
580
        finally:
 
581
            f.close()
 
582
 
 
583
 
 
584
    def get_revision_xml(self, revision_id):
 
585
        """Return XML file object for revision object."""
 
586
        if not revision_id or not isinstance(revision_id, basestring):
 
587
            raise InvalidRevisionId(revision_id)
 
588
 
 
589
        self.lock_read()
 
590
        try:
 
591
            try:
 
592
                return self.revision_store[revision_id]
 
593
            except IndexError:
 
594
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
595
        finally:
 
596
            self.unlock()
540
597
 
541
598
 
542
599
    def get_revision(self, revision_id):
543
600
        """Return the Revision object for a named revision"""
544
 
        if not revision_id or not isinstance(revision_id, basestring):
545
 
            raise ValueError('invalid revision-id: %r' % revision_id)
546
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
601
        xml_file = self.get_revision_xml(revision_id)
 
602
 
 
603
        try:
 
604
            r = unpack_xml(Revision, xml_file)
 
605
        except SyntaxError, e:
 
606
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
607
                                         [revision_id,
 
608
                                          str(e)])
 
609
            
547
610
        assert r.revision_id == revision_id
548
611
        return r
549
612
 
 
613
 
 
614
    def get_revision_delta(self, revno):
 
615
        """Return the delta for one revision.
 
616
 
 
617
        The delta is relative to its mainline predecessor, or the
 
618
        empty tree for revision 1.
 
619
        """
 
620
        assert isinstance(revno, int)
 
621
        rh = self.revision_history()
 
622
        if not (1 <= revno <= len(rh)):
 
623
            raise InvalidRevisionNumber(revno)
 
624
 
 
625
        # revno is 1-based; list is 0-based
 
626
 
 
627
        new_tree = self.revision_tree(rh[revno-1])
 
628
        if revno == 1:
 
629
            old_tree = EmptyTree()
 
630
        else:
 
631
            old_tree = self.revision_tree(rh[revno-2])
 
632
 
 
633
        return compare_trees(old_tree, new_tree)
 
634
 
 
635
        
 
636
 
550
637
    def get_revision_sha1(self, revision_id):
551
638
        """Hash the stored value of a revision, and return it."""
552
639
        # In the future, revision entries will be signed. At that
555
642
        # the revision, (add signatures/remove signatures) and still
556
643
        # have all hash pointers stay consistent.
557
644
        # But for now, just hash the contents.
558
 
        return sha_file(self.revision_store[revision_id])
 
645
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
559
646
 
560
647
 
561
648
    def get_inventory(self, inventory_id):
564
651
        TODO: Perhaps for this and similar methods, take a revision
565
652
               parameter which can be either an integer revno or a
566
653
               string hash."""
567
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
568
 
        return i
 
654
        from bzrlib.inventory import Inventory
 
655
        from bzrlib.xml import unpack_xml
 
656
 
 
657
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
 
658
 
 
659
 
 
660
    def get_inventory_xml(self, inventory_id):
 
661
        """Get inventory XML as a file object."""
 
662
        return self.inventory_store[inventory_id]
 
663
            
569
664
 
570
665
    def get_inventory_sha1(self, inventory_id):
571
666
        """Return the sha1 hash of the inventory entry
572
667
        """
573
 
        return sha_file(self.inventory_store[inventory_id])
 
668
        return sha_file(self.get_inventory_xml(inventory_id))
574
669
 
575
670
 
576
671
    def get_revision_inventory(self, revision_id):
577
672
        """Return inventory of a past revision."""
 
673
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
674
        # must be the same as its revision, so this is trivial.
578
675
        if revision_id == None:
579
 
            return Inventory()
 
676
            from bzrlib.inventory import Inventory
 
677
            return Inventory(self.get_root_id())
580
678
        else:
581
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
679
            return self.get_inventory(revision_id)
582
680
 
583
681
 
584
682
    def revision_history(self):
639
737
                return r+1, my_history[r]
640
738
        return None, None
641
739
 
642
 
    def enum_history(self, direction):
643
 
        """Return (revno, revision_id) for history of branch.
644
 
 
645
 
        direction
646
 
            'forward' is from earliest to latest
647
 
            'reverse' is from latest to earliest
648
 
        """
649
 
        rh = self.revision_history()
650
 
        if direction == 'forward':
651
 
            i = 1
652
 
            for rid in rh:
653
 
                yield i, rid
654
 
                i += 1
655
 
        elif direction == 'reverse':
656
 
            i = len(rh)
657
 
            while i > 0:
658
 
                yield i, rh[i-1]
659
 
                i -= 1
660
 
        else:
661
 
            raise ValueError('invalid history direction', direction)
662
 
 
663
740
 
664
741
    def revno(self):
665
742
        """Return current revision number for this branch.
680
757
            return None
681
758
 
682
759
 
683
 
    def missing_revisions(self, other, stop_revision=None):
 
760
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
684
761
        """
685
762
        If self and other have not diverged, return a list of the revisions
686
763
        present in other, but missing from self.
719
796
        if stop_revision is None:
720
797
            stop_revision = other_len
721
798
        elif stop_revision > other_len:
722
 
            raise NoSuchRevision(self, stop_revision)
 
799
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
723
800
        
724
801
        return other_history[self_len:stop_revision]
725
802
 
726
803
 
727
804
    def update_revisions(self, other, stop_revision=None):
728
805
        """Pull in all new revisions from other branch.
729
 
        
730
 
        >>> from bzrlib.commit import commit
731
 
        >>> bzrlib.trace.silent = True
732
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
733
 
        >>> br1.add('foo')
734
 
        >>> br1.add('bar')
735
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
736
 
        >>> br2 = ScratchBranch()
737
 
        >>> br2.update_revisions(br1)
738
 
        Added 2 texts.
739
 
        Added 1 inventories.
740
 
        Added 1 revisions.
741
 
        >>> br2.revision_history()
742
 
        [u'REVISION-ID-1']
743
 
        >>> br2.update_revisions(br1)
744
 
        Added 0 texts.
745
 
        Added 0 inventories.
746
 
        Added 0 revisions.
747
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
748
 
        True
749
806
        """
750
 
        from bzrlib.progress import ProgressBar
751
 
 
752
 
        pb = ProgressBar()
753
 
 
 
807
        from bzrlib.fetch import greedy_fetch
 
808
 
 
809
        pb = bzrlib.ui.ui_factory.progress_bar()
754
810
        pb.update('comparing histories')
 
811
 
755
812
        revision_ids = self.missing_revisions(other, stop_revision)
 
813
 
 
814
        if len(revision_ids) > 0:
 
815
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
816
        else:
 
817
            count = 0
 
818
        self.append_revision(*revision_ids)
 
819
        ## note("Added %d revisions." % count)
 
820
        pb.clear()
 
821
 
 
822
    def install_revisions(self, other, revision_ids, pb):
 
823
        if hasattr(other.revision_store, "prefetch"):
 
824
            other.revision_store.prefetch(revision_ids)
 
825
        if hasattr(other.inventory_store, "prefetch"):
 
826
            inventory_ids = [other.get_revision(r).inventory_id
 
827
                             for r in revision_ids]
 
828
            other.inventory_store.prefetch(inventory_ids)
 
829
 
 
830
        if pb is None:
 
831
            pb = bzrlib.ui.ui_factory.progress_bar()
 
832
                
756
833
        revisions = []
757
 
        needed_texts = sets.Set()
 
834
        needed_texts = set()
758
835
        i = 0
759
 
        for rev_id in revision_ids:
760
 
            i += 1
761
 
            pb.update('fetching revision', i, len(revision_ids))
762
 
            rev = other.get_revision(rev_id)
 
836
 
 
837
        failures = set()
 
838
        for i, rev_id in enumerate(revision_ids):
 
839
            pb.update('fetching revision', i+1, len(revision_ids))
 
840
            try:
 
841
                rev = other.get_revision(rev_id)
 
842
            except bzrlib.errors.NoSuchRevision:
 
843
                failures.add(rev_id)
 
844
                continue
 
845
 
763
846
            revisions.append(rev)
764
847
            inv = other.get_inventory(str(rev.inventory_id))
765
848
            for key, entry in inv.iter_entries():
770
853
 
771
854
        pb.clear()
772
855
                    
773
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
774
 
        print "Added %d texts." % count 
 
856
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
857
                                                    needed_texts)
 
858
        #print "Added %d texts." % count 
775
859
        inventory_ids = [ f.inventory_id for f in revisions ]
776
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
777
 
                                                inventory_ids)
778
 
        print "Added %d inventories." % count 
 
860
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
861
                                                         inventory_ids)
 
862
        #print "Added %d inventories." % count 
779
863
        revision_ids = [ f.revision_id for f in revisions]
780
 
        count = self.revision_store.copy_multi(other.revision_store, 
781
 
                                               revision_ids)
782
 
        for revision_id in revision_ids:
783
 
            self.append_revision(revision_id)
784
 
        print "Added %d revisions." % count
785
 
                    
786
 
        
 
864
 
 
865
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
866
                                                          revision_ids,
 
867
                                                          permit_failure=True)
 
868
        assert len(cp_fail) == 0 
 
869
        return count, failures
 
870
       
 
871
 
787
872
    def commit(self, *args, **kw):
788
 
        """Deprecated"""
789
873
        from bzrlib.commit import commit
790
874
        commit(self, *args, **kw)
791
875
        
792
876
 
793
 
    def lookup_revision(self, revno):
794
 
        """Return revision hash for revision number."""
 
877
    def lookup_revision(self, revision):
 
878
        """Return the revision identifier for a given revision information."""
 
879
        revno, info = self._get_revision_info(revision)
 
880
        return info
 
881
 
 
882
 
 
883
    def revision_id_to_revno(self, revision_id):
 
884
        """Given a revision id, return its revno"""
 
885
        history = self.revision_history()
 
886
        try:
 
887
            return history.index(revision_id) + 1
 
888
        except ValueError:
 
889
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
890
 
 
891
 
 
892
    def get_revision_info(self, revision):
 
893
        """Return (revno, revision id) for revision identifier.
 
894
 
 
895
        revision can be an integer, in which case it is assumed to be revno (though
 
896
            this will translate negative values into positive ones)
 
897
        revision can also be a string, in which case it is parsed for something like
 
898
            'date:' or 'revid:' etc.
 
899
        """
 
900
        revno, rev_id = self._get_revision_info(revision)
 
901
        if revno is None:
 
902
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
903
        return revno, rev_id
 
904
 
 
905
    def get_rev_id(self, revno, history=None):
 
906
        """Find the revision id of the specified revno."""
795
907
        if revno == 0:
796
908
            return None
797
 
 
798
 
        try:
799
 
            # list is 0-based; revisions are 1-based
800
 
            return self.revision_history()[revno-1]
801
 
        except IndexError:
802
 
            raise BzrError("no such revision %s" % revno)
803
 
 
 
909
        if history is None:
 
910
            history = self.revision_history()
 
911
        elif revno <= 0 or revno > len(history):
 
912
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
913
        return history[revno - 1]
 
914
 
 
915
    def _get_revision_info(self, revision):
 
916
        """Return (revno, revision id) for revision specifier.
 
917
 
 
918
        revision can be an integer, in which case it is assumed to be revno
 
919
        (though this will translate negative values into positive ones)
 
920
        revision can also be a string, in which case it is parsed for something
 
921
        like 'date:' or 'revid:' etc.
 
922
 
 
923
        A revid is always returned.  If it is None, the specifier referred to
 
924
        the null revision.  If the revid does not occur in the revision
 
925
        history, revno will be None.
 
926
        """
 
927
        
 
928
        if revision is None:
 
929
            return 0, None
 
930
        revno = None
 
931
        try:# Convert to int if possible
 
932
            revision = int(revision)
 
933
        except ValueError:
 
934
            pass
 
935
        revs = self.revision_history()
 
936
        if isinstance(revision, int):
 
937
            if revision < 0:
 
938
                revno = len(revs) + revision + 1
 
939
            else:
 
940
                revno = revision
 
941
            rev_id = self.get_rev_id(revno, revs)
 
942
        elif isinstance(revision, basestring):
 
943
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
944
                if revision.startswith(prefix):
 
945
                    result = func(self, revs, revision)
 
946
                    if len(result) > 1:
 
947
                        revno, rev_id = result
 
948
                    else:
 
949
                        revno = result[0]
 
950
                        rev_id = self.get_rev_id(revno, revs)
 
951
                    break
 
952
            else:
 
953
                raise BzrError('No namespace registered for string: %r' %
 
954
                               revision)
 
955
        else:
 
956
            raise TypeError('Unhandled revision type %s' % revision)
 
957
 
 
958
        if revno is None:
 
959
            if rev_id is None:
 
960
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
961
        return revno, rev_id
 
962
 
 
963
    def _namespace_revno(self, revs, revision):
 
964
        """Lookup a revision by revision number"""
 
965
        assert revision.startswith('revno:')
 
966
        try:
 
967
            return (int(revision[6:]),)
 
968
        except ValueError:
 
969
            return None
 
970
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
971
 
 
972
    def _namespace_revid(self, revs, revision):
 
973
        assert revision.startswith('revid:')
 
974
        rev_id = revision[len('revid:'):]
 
975
        try:
 
976
            return revs.index(rev_id) + 1, rev_id
 
977
        except ValueError:
 
978
            return None, rev_id
 
979
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
980
 
 
981
    def _namespace_last(self, revs, revision):
 
982
        assert revision.startswith('last:')
 
983
        try:
 
984
            offset = int(revision[5:])
 
985
        except ValueError:
 
986
            return (None,)
 
987
        else:
 
988
            if offset <= 0:
 
989
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
990
            return (len(revs) - offset + 1,)
 
991
    REVISION_NAMESPACES['last:'] = _namespace_last
 
992
 
 
993
    def _namespace_tag(self, revs, revision):
 
994
        assert revision.startswith('tag:')
 
995
        raise BzrError('tag: namespace registered, but not implemented.')
 
996
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
997
 
 
998
    def _namespace_date(self, revs, revision):
 
999
        assert revision.startswith('date:')
 
1000
        import datetime
 
1001
        # Spec for date revisions:
 
1002
        #   date:value
 
1003
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
1004
        #   it can also start with a '+/-/='. '+' says match the first
 
1005
        #   entry after the given date. '-' is match the first entry before the date
 
1006
        #   '=' is match the first entry after, but still on the given date.
 
1007
        #
 
1008
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
1009
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
1010
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
1011
        #       May 13th, 2005 at 0:00
 
1012
        #
 
1013
        #   So the proper way of saying 'give me all entries for today' is:
 
1014
        #       -r {date:+today}:{date:-tomorrow}
 
1015
        #   The default is '=' when not supplied
 
1016
        val = revision[5:]
 
1017
        match_style = '='
 
1018
        if val[:1] in ('+', '-', '='):
 
1019
            match_style = val[:1]
 
1020
            val = val[1:]
 
1021
 
 
1022
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
1023
        if val.lower() == 'yesterday':
 
1024
            dt = today - datetime.timedelta(days=1)
 
1025
        elif val.lower() == 'today':
 
1026
            dt = today
 
1027
        elif val.lower() == 'tomorrow':
 
1028
            dt = today + datetime.timedelta(days=1)
 
1029
        else:
 
1030
            import re
 
1031
            # This should be done outside the function to avoid recompiling it.
 
1032
            _date_re = re.compile(
 
1033
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
1034
                    r'(,|T)?\s*'
 
1035
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1036
                )
 
1037
            m = _date_re.match(val)
 
1038
            if not m or (not m.group('date') and not m.group('time')):
 
1039
                raise BzrError('Invalid revision date %r' % revision)
 
1040
 
 
1041
            if m.group('date'):
 
1042
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1043
            else:
 
1044
                year, month, day = today.year, today.month, today.day
 
1045
            if m.group('time'):
 
1046
                hour = int(m.group('hour'))
 
1047
                minute = int(m.group('minute'))
 
1048
                if m.group('second'):
 
1049
                    second = int(m.group('second'))
 
1050
                else:
 
1051
                    second = 0
 
1052
            else:
 
1053
                hour, minute, second = 0,0,0
 
1054
 
 
1055
            dt = datetime.datetime(year=year, month=month, day=day,
 
1056
                    hour=hour, minute=minute, second=second)
 
1057
        first = dt
 
1058
        last = None
 
1059
        reversed = False
 
1060
        if match_style == '-':
 
1061
            reversed = True
 
1062
        elif match_style == '=':
 
1063
            last = dt + datetime.timedelta(days=1)
 
1064
 
 
1065
        if reversed:
 
1066
            for i in range(len(revs)-1, -1, -1):
 
1067
                r = self.get_revision(revs[i])
 
1068
                # TODO: Handle timezone.
 
1069
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1070
                if first >= dt and (last is None or dt >= last):
 
1071
                    return (i+1,)
 
1072
        else:
 
1073
            for i in range(len(revs)):
 
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
    REVISION_NAMESPACES['date:'] = _namespace_date
804
1080
 
805
1081
    def revision_tree(self, revision_id):
806
1082
        """Return Tree for a revision on this branch.
870
1146
 
871
1147
            inv.rename(file_id, to_dir_id, to_tail)
872
1148
 
873
 
            print "%s => %s" % (from_rel, to_rel)
874
 
 
875
1149
            from_abs = self.abspath(from_rel)
876
1150
            to_abs = self.abspath(to_rel)
877
1151
            try:
896
1170
 
897
1171
        Note that to_name is only the last component of the new name;
898
1172
        this doesn't change the directory.
 
1173
 
 
1174
        This returns a list of (from_path, to_path) pairs for each
 
1175
        entry that is moved.
899
1176
        """
 
1177
        result = []
900
1178
        self.lock_write()
901
1179
        try:
902
1180
            ## TODO: Option to move IDs only
937
1215
            for f in from_paths:
938
1216
                name_tail = splitpath(f)[-1]
939
1217
                dest_path = appendpath(to_name, name_tail)
940
 
                print "%s => %s" % (f, dest_path)
 
1218
                result.append((f, dest_path))
941
1219
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
942
1220
                try:
943
1221
                    os.rename(self.abspath(f), self.abspath(dest_path))
949
1227
        finally:
950
1228
            self.unlock()
951
1229
 
 
1230
        return result
 
1231
 
 
1232
 
 
1233
    def revert(self, filenames, old_tree=None, backups=True):
 
1234
        """Restore selected files to the versions from a previous tree.
 
1235
 
 
1236
        backups
 
1237
            If true (default) backups are made of files before
 
1238
            they're renamed.
 
1239
        """
 
1240
        from bzrlib.errors import NotVersionedError, BzrError
 
1241
        from bzrlib.atomicfile import AtomicFile
 
1242
        from bzrlib.osutils import backup_file
 
1243
        
 
1244
        inv = self.read_working_inventory()
 
1245
        if old_tree is None:
 
1246
            old_tree = self.basis_tree()
 
1247
        old_inv = old_tree.inventory
 
1248
 
 
1249
        nids = []
 
1250
        for fn in filenames:
 
1251
            file_id = inv.path2id(fn)
 
1252
            if not file_id:
 
1253
                raise NotVersionedError("not a versioned file", fn)
 
1254
            if not old_inv.has_id(file_id):
 
1255
                raise BzrError("file not present in old tree", fn, file_id)
 
1256
            nids.append((fn, file_id))
 
1257
            
 
1258
        # TODO: Rename back if it was previously at a different location
 
1259
 
 
1260
        # TODO: If given a directory, restore the entire contents from
 
1261
        # the previous version.
 
1262
 
 
1263
        # TODO: Make a backup to a temporary file.
 
1264
 
 
1265
        # TODO: If the file previously didn't exist, delete it?
 
1266
        for fn, file_id in nids:
 
1267
            backup_file(fn)
 
1268
            
 
1269
            f = AtomicFile(fn, 'wb')
 
1270
            try:
 
1271
                f.write(old_tree.get_file(file_id).read())
 
1272
                f.commit()
 
1273
            finally:
 
1274
                f.close()
 
1275
 
 
1276
 
 
1277
    def pending_merges(self):
 
1278
        """Return a list of pending merges.
 
1279
 
 
1280
        These are revisions that have been merged into the working
 
1281
        directory but not yet committed.
 
1282
        """
 
1283
        cfn = self.controlfilename('pending-merges')
 
1284
        if not os.path.exists(cfn):
 
1285
            return []
 
1286
        p = []
 
1287
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1288
            p.append(l.rstrip('\n'))
 
1289
        return p
 
1290
 
 
1291
 
 
1292
    def add_pending_merge(self, revision_id):
 
1293
        from bzrlib.revision import validate_revision_id
 
1294
 
 
1295
        validate_revision_id(revision_id)
 
1296
 
 
1297
        p = self.pending_merges()
 
1298
        if revision_id in p:
 
1299
            return
 
1300
        p.append(revision_id)
 
1301
        self.set_pending_merges(p)
 
1302
 
 
1303
 
 
1304
    def set_pending_merges(self, rev_list):
 
1305
        from bzrlib.atomicfile import AtomicFile
 
1306
        self.lock_write()
 
1307
        try:
 
1308
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1309
            try:
 
1310
                for l in rev_list:
 
1311
                    print >>f, l
 
1312
                f.commit()
 
1313
            finally:
 
1314
                f.close()
 
1315
        finally:
 
1316
            self.unlock()
 
1317
 
 
1318
 
 
1319
    def get_parent(self):
 
1320
        """Return the parent location of the branch.
 
1321
 
 
1322
        This is the default location for push/pull/missing.  The usual
 
1323
        pattern is that the user can override it by specifying a
 
1324
        location.
 
1325
        """
 
1326
        import errno
 
1327
        _locs = ['parent', 'pull', 'x-pull']
 
1328
        for l in _locs:
 
1329
            try:
 
1330
                return self.controlfile(l, 'r').read().strip('\n')
 
1331
            except IOError, e:
 
1332
                if e.errno != errno.ENOENT:
 
1333
                    raise
 
1334
        return None
 
1335
 
 
1336
 
 
1337
    def set_parent(self, url):
 
1338
        # TODO: Maybe delete old location files?
 
1339
        from bzrlib.atomicfile import AtomicFile
 
1340
        self.lock_write()
 
1341
        try:
 
1342
            f = AtomicFile(self.controlfilename('parent'))
 
1343
            try:
 
1344
                f.write(url + '\n')
 
1345
                f.commit()
 
1346
            finally:
 
1347
                f.close()
 
1348
        finally:
 
1349
            self.unlock()
 
1350
 
 
1351
        
952
1352
 
953
1353
 
954
1354
class ScratchBranch(Branch):
969
1369
 
970
1370
        If any files are listed, they are created in the working copy.
971
1371
        """
 
1372
        from tempfile import mkdtemp
972
1373
        init = False
973
1374
        if base is None:
974
 
            base = tempfile.mkdtemp()
 
1375
            base = mkdtemp()
975
1376
            init = True
976
1377
        Branch.__init__(self, base, init=init)
977
1378
        for d in dirs:
990
1391
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
991
1392
        True
992
1393
        """
993
 
        base = tempfile.mkdtemp()
 
1394
        from shutil import copytree
 
1395
        from tempfile import mkdtemp
 
1396
        base = mkdtemp()
994
1397
        os.rmdir(base)
995
 
        shutil.copytree(self.base, base, symlinks=True)
 
1398
        copytree(self.base, base, symlinks=True)
996
1399
        return ScratchBranch(base=base)
 
1400
 
 
1401
 
997
1402
        
998
1403
    def __del__(self):
999
1404
        self.destroy()
1000
1405
 
1001
1406
    def destroy(self):
1002
1407
        """Destroy the test branch, removing the scratch directory."""
 
1408
        from shutil import rmtree
1003
1409
        try:
1004
1410
            if self.base:
1005
1411
                mutter("delete ScratchBranch %s" % self.base)
1006
 
                shutil.rmtree(self.base)
 
1412
                rmtree(self.base)
1007
1413
        except OSError, e:
1008
1414
            # Work around for shutil.rmtree failing on Windows when
1009
1415
            # readonly files are encountered
1011
1417
            for root, dirs, files in os.walk(self.base, topdown=False):
1012
1418
                for name in files:
1013
1419
                    os.chmod(os.path.join(root, name), 0700)
1014
 
            shutil.rmtree(self.base)
 
1420
            rmtree(self.base)
1015
1421
        self.base = None
1016
1422
 
1017
1423
    
1042
1448
    cope with just randomness because running uuidgen every time is
1043
1449
    slow."""
1044
1450
    import re
 
1451
    from binascii import hexlify
 
1452
    from time import time
1045
1453
 
1046
1454
    # get last component
1047
1455
    idx = name.rfind('/')
1059
1467
    name = re.sub(r'[^\w.]', '', name)
1060
1468
 
1061
1469
    s = hexlify(rand_bytes(8))
1062
 
    return '-'.join((name, compact_date(time.time()), s))
 
1470
    return '-'.join((name, compact_date(time()), s))
 
1471
 
 
1472
 
 
1473
def gen_root_id():
 
1474
    """Return a new tree-root file id."""
 
1475
    return gen_file_id('TREE_ROOT')
 
1476
 
 
1477
 
 
1478
def pull_loc(branch):
 
1479
    # TODO: Should perhaps just make attribute be 'base' in
 
1480
    # RemoteBranch and Branch?
 
1481
    if hasattr(branch, "baseurl"):
 
1482
        return branch.baseurl
 
1483
    else:
 
1484
        return branch.base
 
1485
 
 
1486
 
 
1487
def copy_branch(branch_from, to_location, revision=None):
 
1488
    """Copy branch_from into the existing directory to_location.
 
1489
 
 
1490
    revision
 
1491
        If not None, only revisions up to this point will be copied.
 
1492
        The head of the new branch will be that revision.
 
1493
 
 
1494
    to_location
 
1495
        The name of a local directory that exists but is empty.
 
1496
    """
 
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 = branch_from.revno()
 
1507
    else:
 
1508
        revno, rev_id = branch_from.get_revision_info(revision)
 
1509
    br_to.update_revisions(branch_from, stop_revision=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