~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-01 07:28:14 UTC
  • Revision ID: mbp@sourcefrog.net-20050901072814-b2685355951a6a6a
- add test that 'bzr add' reports the files as they're added

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."""
795
 
        if revno == 0:
796
 
            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
 
 
 
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
        if revision is None:
 
901
            return 0, None
 
902
        revno = None
 
903
        try:# Convert to int if possible
 
904
            revision = int(revision)
 
905
        except ValueError:
 
906
            pass
 
907
        revs = self.revision_history()
 
908
        if isinstance(revision, int):
 
909
            if revision == 0:
 
910
                return 0, None
 
911
            # Mabye we should do this first, but we don't need it if revision == 0
 
912
            if revision < 0:
 
913
                revno = len(revs) + revision + 1
 
914
            else:
 
915
                revno = revision
 
916
        elif isinstance(revision, basestring):
 
917
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
918
                if revision.startswith(prefix):
 
919
                    revno = func(self, revs, revision)
 
920
                    break
 
921
            else:
 
922
                raise BzrError('No namespace registered for string: %r' % revision)
 
923
 
 
924
        if revno is None or revno <= 0 or revno > len(revs):
 
925
            raise BzrError("no such revision %s" % revision)
 
926
        return revno, revs[revno-1]
 
927
 
 
928
    def _namespace_revno(self, revs, revision):
 
929
        """Lookup a revision by revision number"""
 
930
        assert revision.startswith('revno:')
 
931
        try:
 
932
            return int(revision[6:])
 
933
        except ValueError:
 
934
            return None
 
935
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
936
 
 
937
    def _namespace_revid(self, revs, revision):
 
938
        assert revision.startswith('revid:')
 
939
        try:
 
940
            return revs.index(revision[6:]) + 1
 
941
        except ValueError:
 
942
            return None
 
943
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
944
 
 
945
    def _namespace_last(self, revs, revision):
 
946
        assert revision.startswith('last:')
 
947
        try:
 
948
            offset = int(revision[5:])
 
949
        except ValueError:
 
950
            return None
 
951
        else:
 
952
            if offset <= 0:
 
953
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
954
            return len(revs) - offset + 1
 
955
    REVISION_NAMESPACES['last:'] = _namespace_last
 
956
 
 
957
    def _namespace_tag(self, revs, revision):
 
958
        assert revision.startswith('tag:')
 
959
        raise BzrError('tag: namespace registered, but not implemented.')
 
960
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
961
 
 
962
    def _namespace_date(self, revs, revision):
 
963
        assert revision.startswith('date:')
 
964
        import datetime
 
965
        # Spec for date revisions:
 
966
        #   date:value
 
967
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
968
        #   it can also start with a '+/-/='. '+' says match the first
 
969
        #   entry after the given date. '-' is match the first entry before the date
 
970
        #   '=' is match the first entry after, but still on the given date.
 
971
        #
 
972
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
973
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
974
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
975
        #       May 13th, 2005 at 0:00
 
976
        #
 
977
        #   So the proper way of saying 'give me all entries for today' is:
 
978
        #       -r {date:+today}:{date:-tomorrow}
 
979
        #   The default is '=' when not supplied
 
980
        val = revision[5:]
 
981
        match_style = '='
 
982
        if val[:1] in ('+', '-', '='):
 
983
            match_style = val[:1]
 
984
            val = val[1:]
 
985
 
 
986
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
987
        if val.lower() == 'yesterday':
 
988
            dt = today - datetime.timedelta(days=1)
 
989
        elif val.lower() == 'today':
 
990
            dt = today
 
991
        elif val.lower() == 'tomorrow':
 
992
            dt = today + datetime.timedelta(days=1)
 
993
        else:
 
994
            import re
 
995
            # This should be done outside the function to avoid recompiling it.
 
996
            _date_re = re.compile(
 
997
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
998
                    r'(,|T)?\s*'
 
999
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1000
                )
 
1001
            m = _date_re.match(val)
 
1002
            if not m or (not m.group('date') and not m.group('time')):
 
1003
                raise BzrError('Invalid revision date %r' % revision)
 
1004
 
 
1005
            if m.group('date'):
 
1006
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1007
            else:
 
1008
                year, month, day = today.year, today.month, today.day
 
1009
            if m.group('time'):
 
1010
                hour = int(m.group('hour'))
 
1011
                minute = int(m.group('minute'))
 
1012
                if m.group('second'):
 
1013
                    second = int(m.group('second'))
 
1014
                else:
 
1015
                    second = 0
 
1016
            else:
 
1017
                hour, minute, second = 0,0,0
 
1018
 
 
1019
            dt = datetime.datetime(year=year, month=month, day=day,
 
1020
                    hour=hour, minute=minute, second=second)
 
1021
        first = dt
 
1022
        last = None
 
1023
        reversed = False
 
1024
        if match_style == '-':
 
1025
            reversed = True
 
1026
        elif match_style == '=':
 
1027
            last = dt + datetime.timedelta(days=1)
 
1028
 
 
1029
        if reversed:
 
1030
            for i in range(len(revs)-1, -1, -1):
 
1031
                r = self.get_revision(revs[i])
 
1032
                # TODO: Handle timezone.
 
1033
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1034
                if first >= dt and (last is None or dt >= last):
 
1035
                    return i+1
 
1036
        else:
 
1037
            for i in range(len(revs)):
 
1038
                r = self.get_revision(revs[i])
 
1039
                # TODO: Handle timezone.
 
1040
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1041
                if first <= dt and (last is None or dt <= last):
 
1042
                    return i+1
 
1043
    REVISION_NAMESPACES['date:'] = _namespace_date
804
1044
 
805
1045
    def revision_tree(self, revision_id):
806
1046
        """Return Tree for a revision on this branch.
870
1110
 
871
1111
            inv.rename(file_id, to_dir_id, to_tail)
872
1112
 
873
 
            print "%s => %s" % (from_rel, to_rel)
874
 
 
875
1113
            from_abs = self.abspath(from_rel)
876
1114
            to_abs = self.abspath(to_rel)
877
1115
            try:
896
1134
 
897
1135
        Note that to_name is only the last component of the new name;
898
1136
        this doesn't change the directory.
 
1137
 
 
1138
        This returns a list of (from_path, to_path) pairs for each
 
1139
        entry that is moved.
899
1140
        """
 
1141
        result = []
900
1142
        self.lock_write()
901
1143
        try:
902
1144
            ## TODO: Option to move IDs only
937
1179
            for f in from_paths:
938
1180
                name_tail = splitpath(f)[-1]
939
1181
                dest_path = appendpath(to_name, name_tail)
940
 
                print "%s => %s" % (f, dest_path)
 
1182
                result.append((f, dest_path))
941
1183
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
942
1184
                try:
943
1185
                    os.rename(self.abspath(f), self.abspath(dest_path))
949
1191
        finally:
950
1192
            self.unlock()
951
1193
 
 
1194
        return result
 
1195
 
 
1196
 
 
1197
    def revert(self, filenames, old_tree=None, backups=True):
 
1198
        """Restore selected files to the versions from a previous tree.
 
1199
 
 
1200
        backups
 
1201
            If true (default) backups are made of files before
 
1202
            they're renamed.
 
1203
        """
 
1204
        from bzrlib.errors import NotVersionedError, BzrError
 
1205
        from bzrlib.atomicfile import AtomicFile
 
1206
        from bzrlib.osutils import backup_file
 
1207
        
 
1208
        inv = self.read_working_inventory()
 
1209
        if old_tree is None:
 
1210
            old_tree = self.basis_tree()
 
1211
        old_inv = old_tree.inventory
 
1212
 
 
1213
        nids = []
 
1214
        for fn in filenames:
 
1215
            file_id = inv.path2id(fn)
 
1216
            if not file_id:
 
1217
                raise NotVersionedError("not a versioned file", fn)
 
1218
            if not old_inv.has_id(file_id):
 
1219
                raise BzrError("file not present in old tree", fn, file_id)
 
1220
            nids.append((fn, file_id))
 
1221
            
 
1222
        # TODO: Rename back if it was previously at a different location
 
1223
 
 
1224
        # TODO: If given a directory, restore the entire contents from
 
1225
        # the previous version.
 
1226
 
 
1227
        # TODO: Make a backup to a temporary file.
 
1228
 
 
1229
        # TODO: If the file previously didn't exist, delete it?
 
1230
        for fn, file_id in nids:
 
1231
            backup_file(fn)
 
1232
            
 
1233
            f = AtomicFile(fn, 'wb')
 
1234
            try:
 
1235
                f.write(old_tree.get_file(file_id).read())
 
1236
                f.commit()
 
1237
            finally:
 
1238
                f.close()
 
1239
 
 
1240
 
 
1241
    def pending_merges(self):
 
1242
        """Return a list of pending merges.
 
1243
 
 
1244
        These are revisions that have been merged into the working
 
1245
        directory but not yet committed.
 
1246
        """
 
1247
        cfn = self.controlfilename('pending-merges')
 
1248
        if not os.path.exists(cfn):
 
1249
            return []
 
1250
        p = []
 
1251
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1252
            p.append(l.rstrip('\n'))
 
1253
        return p
 
1254
 
 
1255
 
 
1256
    def add_pending_merge(self, revision_id):
 
1257
        from bzrlib.revision import validate_revision_id
 
1258
 
 
1259
        validate_revision_id(revision_id)
 
1260
 
 
1261
        p = self.pending_merges()
 
1262
        if revision_id in p:
 
1263
            return
 
1264
        p.append(revision_id)
 
1265
        self.set_pending_merges(p)
 
1266
 
 
1267
 
 
1268
    def set_pending_merges(self, rev_list):
 
1269
        from bzrlib.atomicfile import AtomicFile
 
1270
        self.lock_write()
 
1271
        try:
 
1272
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1273
            try:
 
1274
                for l in rev_list:
 
1275
                    print >>f, l
 
1276
                f.commit()
 
1277
            finally:
 
1278
                f.close()
 
1279
        finally:
 
1280
            self.unlock()
 
1281
 
 
1282
 
 
1283
    def get_parent(self):
 
1284
        """Return the parent location of the branch.
 
1285
 
 
1286
        This is the default location for push/pull/missing.  The usual
 
1287
        pattern is that the user can override it by specifying a
 
1288
        location.
 
1289
        """
 
1290
        import errno
 
1291
        _locs = ['parent', 'pull', 'x-pull']
 
1292
        for l in _locs:
 
1293
            try:
 
1294
                return self.controlfile(l, 'r').read().strip('\n')
 
1295
            except IOError, e:
 
1296
                if e.errno != errno.ENOENT:
 
1297
                    raise
 
1298
        return None
 
1299
 
 
1300
 
 
1301
    def set_parent(self, url):
 
1302
        # TODO: Maybe delete old location files?
 
1303
        from bzrlib.atomicfile import AtomicFile
 
1304
        self.lock_write()
 
1305
        try:
 
1306
            f = AtomicFile(self.controlfilename('parent'))
 
1307
            try:
 
1308
                f.write(url + '\n')
 
1309
                f.commit()
 
1310
            finally:
 
1311
                f.close()
 
1312
        finally:
 
1313
            self.unlock()
 
1314
 
 
1315
        
952
1316
 
953
1317
 
954
1318
class ScratchBranch(Branch):
969
1333
 
970
1334
        If any files are listed, they are created in the working copy.
971
1335
        """
 
1336
        from tempfile import mkdtemp
972
1337
        init = False
973
1338
        if base is None:
974
 
            base = tempfile.mkdtemp()
 
1339
            base = mkdtemp()
975
1340
            init = True
976
1341
        Branch.__init__(self, base, init=init)
977
1342
        for d in dirs:
990
1355
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
991
1356
        True
992
1357
        """
993
 
        base = tempfile.mkdtemp()
 
1358
        from shutil import copytree
 
1359
        from tempfile import mkdtemp
 
1360
        base = mkdtemp()
994
1361
        os.rmdir(base)
995
 
        shutil.copytree(self.base, base, symlinks=True)
 
1362
        copytree(self.base, base, symlinks=True)
996
1363
        return ScratchBranch(base=base)
 
1364
 
 
1365
 
997
1366
        
998
1367
    def __del__(self):
999
1368
        self.destroy()
1000
1369
 
1001
1370
    def destroy(self):
1002
1371
        """Destroy the test branch, removing the scratch directory."""
 
1372
        from shutil import rmtree
1003
1373
        try:
1004
1374
            if self.base:
1005
1375
                mutter("delete ScratchBranch %s" % self.base)
1006
 
                shutil.rmtree(self.base)
 
1376
                rmtree(self.base)
1007
1377
        except OSError, e:
1008
1378
            # Work around for shutil.rmtree failing on Windows when
1009
1379
            # readonly files are encountered
1011
1381
            for root, dirs, files in os.walk(self.base, topdown=False):
1012
1382
                for name in files:
1013
1383
                    os.chmod(os.path.join(root, name), 0700)
1014
 
            shutil.rmtree(self.base)
 
1384
            rmtree(self.base)
1015
1385
        self.base = None
1016
1386
 
1017
1387
    
1042
1412
    cope with just randomness because running uuidgen every time is
1043
1413
    slow."""
1044
1414
    import re
 
1415
    from binascii import hexlify
 
1416
    from time import time
1045
1417
 
1046
1418
    # get last component
1047
1419
    idx = name.rfind('/')
1059
1431
    name = re.sub(r'[^\w.]', '', name)
1060
1432
 
1061
1433
    s = hexlify(rand_bytes(8))
1062
 
    return '-'.join((name, compact_date(time.time()), s))
 
1434
    return '-'.join((name, compact_date(time()), s))
 
1435
 
 
1436
 
 
1437
def gen_root_id():
 
1438
    """Return a new tree-root file id."""
 
1439
    return gen_file_id('TREE_ROOT')
 
1440
 
 
1441
 
 
1442
def pull_loc(branch):
 
1443
    # TODO: Should perhaps just make attribute be 'base' in
 
1444
    # RemoteBranch and Branch?
 
1445
    if hasattr(branch, "baseurl"):
 
1446
        return branch.baseurl
 
1447
    else:
 
1448
        return branch.base
 
1449
 
 
1450
 
 
1451
def copy_branch(branch_from, to_location, revision=None):
 
1452
    """Copy branch_from into the existing directory to_location.
 
1453
 
 
1454
    revision
 
1455
        If not None, only revisions up to this point will be copied.
 
1456
        The head of the new branch will be that revision.
 
1457
 
 
1458
    to_location
 
1459
        The name of a local directory that exists but is empty.
 
1460
    """
 
1461
    from bzrlib.merge import merge
 
1462
    from bzrlib.branch import Branch
 
1463
 
 
1464
    assert isinstance(branch_from, Branch)
 
1465
    assert isinstance(to_location, basestring)
 
1466
    
 
1467
    br_to = Branch(to_location, init=True)
 
1468
    br_to.set_root_id(branch_from.get_root_id())
 
1469
    if revision is None:
 
1470
        revno = branch_from.revno()
 
1471
    else:
 
1472
        revno, rev_id = branch_from.get_revision_info(revision)
 
1473
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1474
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1475
          check_clean=False, ignore_zero=True)
 
1476
    
 
1477
    from_location = pull_loc(branch_from)
 
1478
    br_to.set_parent(pull_loc(branch_from))
 
1479