~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-09-28 09:35:50 UTC
  • mfrom: (1185.1.47)
  • Revision ID: robertc@robertcollins.net-20050928093550-3ca194dfaffc79f1
merge from integration

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
import sys
19
19
import os
20
 
from cStringIO import StringIO
 
20
import errno
 
21
from warnings import warn
 
22
 
21
23
 
22
24
import bzrlib
23
25
from bzrlib.trace import mutter, note
24
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
 
     splitpath, \
26
 
     sha_file, appendpath, file_kind
27
 
 
 
26
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
 
27
                            rename, splitpath, sha_file, appendpath, 
 
28
                            file_kind)
28
29
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
29
 
                           NoSuchRevision)
 
30
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
31
                           DivergedBranches, LockError, UnlistableStore,
 
32
                           UnlistableBranch)
30
33
from bzrlib.textui import show_status
31
 
from bzrlib.revision import Revision
 
34
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
32
35
from bzrlib.delta import compare_trees
33
36
from bzrlib.tree import EmptyTree, RevisionTree
34
37
from bzrlib.inventory import Inventory
35
38
from bzrlib.weavestore import WeaveStore
36
 
from bzrlib.store import ImmutableStore
 
39
from bzrlib.store import copy_all, ImmutableStore
37
40
import bzrlib.xml5
38
41
import bzrlib.ui
39
42
 
40
43
 
41
 
INVENTORY_FILEID = '__inventory'
42
 
ANCESTRY_FILEID = '__ancestry'
43
 
 
44
 
 
45
44
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
46
45
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
47
46
## TODO: Maybe include checks for common corruption of newlines, etc?
52
51
# cache in memory to make this faster.  In general anything can be
53
52
# cached in memory between lock and unlock operations.
54
53
 
55
 
# TODO: please move the revision-string syntax stuff out of the branch
56
 
# object; it's clutter
57
 
 
58
 
 
59
 
def find_branch(f, **args):
60
 
    if f and (f.startswith('http://') or f.startswith('https://')):
61
 
        import remotebranch 
62
 
        return remotebranch.RemoteBranch(f, **args)
63
 
    else:
64
 
        return Branch(f, **args)
65
 
 
66
 
 
67
 
def find_cached_branch(f, cache_root, **args):
68
 
    from remotebranch import RemoteBranch
69
 
    br = find_branch(f, **args)
70
 
    def cacheify(br, store_name):
71
 
        from meta_store import CachedStore
72
 
        cache_path = os.path.join(cache_root, store_name)
73
 
        os.mkdir(cache_path)
74
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
75
 
        setattr(br, store_name, new_store)
76
 
 
77
 
    if isinstance(br, RemoteBranch):
78
 
        cacheify(br, 'inventory_store')
79
 
        cacheify(br, 'text_store')
80
 
        cacheify(br, 'revision_store')
81
 
    return br
82
 
 
 
54
def find_branch(*ignored, **ignored_too):
 
55
    # XXX: leave this here for about one release, then remove it
 
56
    raise NotImplementedError('find_branch() is not supported anymore, '
 
57
                              'please use one of the new branch constructors')
83
58
 
84
59
def _relpath(base, path):
85
60
    """Return path relative to base, or raise exception.
103
78
        if tail:
104
79
            s.insert(0, tail)
105
80
    else:
106
 
        from errors import NotBranchError
107
81
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
108
82
 
109
83
    return os.sep.join(s)
137
111
        head, tail = os.path.split(f)
138
112
        if head == f:
139
113
            # reached the root, whatever that may be
140
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
114
            raise NotBranchError('%s is not in a branch' % orig_f)
141
115
        f = head
142
116
 
143
117
 
144
118
 
145
 
# XXX: move into bzrlib.errors; subclass BzrError    
146
 
class DivergedBranches(Exception):
147
 
    def __init__(self, branch1, branch2):
148
 
        self.branch1 = branch1
149
 
        self.branch2 = branch2
150
 
        Exception.__init__(self, "These branches have diverged.")
151
 
 
152
119
 
153
120
######################################################################
154
121
# branch objects
157
124
    """Branch holding a history of revisions.
158
125
 
159
126
    base
160
 
        Base directory of the branch.
 
127
        Base directory/url of the branch.
 
128
    """
 
129
    base = None
 
130
 
 
131
    def __init__(self, *ignored, **ignored_too):
 
132
        raise NotImplementedError('The Branch class is abstract')
 
133
 
 
134
    @staticmethod
 
135
    def open(base):
 
136
        """Open an existing branch, rooted at 'base' (url)"""
 
137
        if base and (base.startswith('http://') or base.startswith('https://')):
 
138
            from bzrlib.remotebranch import RemoteBranch
 
139
            return RemoteBranch(base, find_root=False)
 
140
        else:
 
141
            return LocalBranch(base, find_root=False)
 
142
 
 
143
    @staticmethod
 
144
    def open_containing(url):
 
145
        """Open an existing branch which contains url.
 
146
        
 
147
        This probes for a branch at url, and searches upwards from there.
 
148
        """
 
149
        if url and (url.startswith('http://') or url.startswith('https://')):
 
150
            from bzrlib.remotebranch import RemoteBranch
 
151
            return RemoteBranch(url)
 
152
        else:
 
153
            return LocalBranch(url)
 
154
 
 
155
    @staticmethod
 
156
    def initialize(base):
 
157
        """Create a new branch, rooted at 'base' (url)"""
 
158
        if base and (base.startswith('http://') or base.startswith('https://')):
 
159
            from bzrlib.remotebranch import RemoteBranch
 
160
            return RemoteBranch(base, init=True)
 
161
        else:
 
162
            return LocalBranch(base, init=True)
 
163
 
 
164
    def setup_caching(self, cache_root):
 
165
        """Subclasses that care about caching should override this, and set
 
166
        up cached stores located under cache_root.
 
167
        """
 
168
 
 
169
 
 
170
class LocalBranch(Branch):
 
171
    """A branch stored in the actual filesystem.
 
172
 
 
173
    Note that it's "local" in the context of the filesystem; it doesn't
 
174
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
175
    it's writable, and can be accessed via the normal filesystem API.
161
176
 
162
177
    _lock_mode
163
178
        None, or 'r' or 'w'
169
184
    _lock
170
185
        Lock object from bzrlib.lock.
171
186
    """
172
 
    base = None
 
187
    # We actually expect this class to be somewhat short-lived; part of its
 
188
    # purpose is to try to isolate what bits of the branch logic are tied to
 
189
    # filesystem access, so that in a later step, we can extricate them to
 
190
    # a separarte ("storage") class.
173
191
    _lock_mode = None
174
192
    _lock_count = None
175
193
    _lock = None
180
198
    # This should match a prefix with a function which accepts
181
199
    REVISION_NAMESPACES = {}
182
200
 
183
 
    def __init__(self, base, init=False, find_root=True):
 
201
    def push_stores(self, branch_to):
 
202
        """Copy the content of this branches store to branch_to."""
 
203
        if (self._branch_format != branch_to._branch_format
 
204
            or self._branch_format != 4):
 
205
            from bzrlib.fetch import greedy_fetch
 
206
            mutter("falling back to fetch logic to push between %s and %s",
 
207
                   self, branch_to)
 
208
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
209
                         revision=self.last_revision())
 
210
            return
 
211
 
 
212
        store_pairs = ((self.text_store,      branch_to.text_store),
 
213
                       (self.inventory_store, branch_to.inventory_store),
 
214
                       (self.revision_store,  branch_to.revision_store))
 
215
        try:
 
216
            for from_store, to_store in store_pairs: 
 
217
                copy_all(from_store, to_store)
 
218
        except UnlistableStore:
 
219
            raise UnlistableBranch(from_store)
 
220
 
 
221
    def __init__(self, base, init=False, find_root=True,
 
222
                 relax_version_check=False):
184
223
        """Create new branch object at a particular location.
185
224
 
186
 
        base -- Base directory for the branch.
 
225
        base -- Base directory for the branch. May be a file:// url.
187
226
        
188
227
        init -- If True, create new control files in a previously
189
228
             unversioned directory.  If False, the branch must already
192
231
        find_root -- If true and init is false, find the root of the
193
232
             existing branch containing base.
194
233
 
 
234
        relax_version_check -- If true, the usual check for the branch
 
235
            version is not applied.  This is intended only for
 
236
            upgrade/recovery type use; it's not guaranteed that
 
237
            all operations will work on old format branches.
 
238
 
195
239
        In the test suite, creation of new trees is tested using the
196
240
        `ScratchBranch` class.
197
241
        """
201
245
        elif find_root:
202
246
            self.base = find_branch_root(base)
203
247
        else:
 
248
            if base.startswith("file://"):
 
249
                base = base[7:]
204
250
            self.base = os.path.realpath(base)
205
251
            if not isdir(self.controlfilename('.')):
206
 
                from errors import NotBranchError
207
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
208
 
                                     ['use "bzr init" to initialize a new working tree',
209
 
                                      'current bzr can only operate from top-of-tree'])
210
 
        self._check_format()
211
 
 
212
 
        self.weave_store = WeaveStore(self.controlfilename('weaves'))
213
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
252
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
 
253
                                     ['use "bzr init" to initialize a '
 
254
                                      'new working tree'])
 
255
        self._check_format(relax_version_check)
 
256
        cfn = self.controlfilename
 
257
        if self._branch_format == 4:
 
258
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
 
259
            self.text_store = ImmutableStore(cfn('text-store'))
 
260
        elif self._branch_format == 5:
 
261
            self.control_weaves = WeaveStore(cfn([]))
 
262
            self.weave_store = WeaveStore(cfn('weaves'))
 
263
            if init:
 
264
                # FIXME: Unify with make_control_files
 
265
                self.control_weaves.put_empty_weave('inventory')
 
266
                self.control_weaves.put_empty_weave('ancestry')
 
267
        self.revision_store = ImmutableStore(cfn('revision-store'))
214
268
 
215
269
 
216
270
    def __str__(self):
222
276
 
223
277
    def __del__(self):
224
278
        if self._lock_mode or self._lock:
225
 
            from warnings import warn
 
279
            # XXX: This should show something every time, and be suitable for
 
280
            # headless operation and embedding
226
281
            warn("branch %r was not explicitly unlocked" % self)
227
282
            self._lock.unlock()
228
283
 
229
 
 
230
284
    def lock_write(self):
231
285
        if self._lock_mode:
232
286
            if self._lock_mode != 'w':
233
 
                from errors import LockError
234
287
                raise LockError("can't upgrade to a write lock from %r" %
235
288
                                self._lock_mode)
236
289
            self._lock_count += 1
256
309
                        
257
310
    def unlock(self):
258
311
        if not self._lock_mode:
259
 
            from errors import LockError
260
312
            raise LockError('branch %r is not locked' % (self))
261
313
 
262
314
        if self._lock_count > 1:
317
369
        for d in ('text-store', 'revision-store',
318
370
                  'weaves'):
319
371
            os.mkdir(self.controlfilename(d))
320
 
        for f in ('revision-history', 'merged-patches',
321
 
                  'pending-merged-patches', 'branch-name',
 
372
        for f in ('revision-history',
 
373
                  'branch-name',
322
374
                  'branch-lock',
323
375
                  'pending-merges'):
324
376
            self.controlfile(f, 'w').write('')
329
381
        # simplicity.
330
382
        f = self.controlfile('inventory','w')
331
383
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
332
 
        
333
 
 
334
 
 
335
 
    def _check_format(self):
 
384
 
 
385
 
 
386
    def _check_format(self, relax_version_check):
336
387
        """Check this branch format is supported.
337
388
 
338
389
        The format level is stored, as an integer, in
341
392
        In the future, we might need different in-memory Branch
342
393
        classes to support downlevel branches.  But not yet.
343
394
        """
344
 
        fmt = self.controlfile('branch-format', 'r').read()
 
395
        try:
 
396
            fmt = self.controlfile('branch-format', 'r').read()
 
397
        except IOError, e:
 
398
            if e.errno == errno.ENOENT:
 
399
                raise NotBranchError(self.base)
 
400
            else:
 
401
                raise
 
402
 
345
403
        if fmt == BZR_BRANCH_FORMAT_5:
346
404
            self._branch_format = 5
347
 
        else:
348
 
            raise BzrError('sorry, branch format "%s" not supported; ' 
349
 
                           'use a different bzr version, '
350
 
                           'or run "bzr upgrade", '
351
 
                           'or remove the .bzr directory and "bzr init" again'
352
 
                           % fmt.rstrip('\n\r'))
 
405
        elif fmt == BZR_BRANCH_FORMAT_4:
 
406
            self._branch_format = 4
 
407
 
 
408
        if (not relax_version_check
 
409
            and self._branch_format != 5):
 
410
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
411
                           ['use a different bzr version',
 
412
                            'or remove the .bzr directory and "bzr init" again'])
353
413
 
354
414
    def get_root_id(self):
355
415
        """Return the id of this branches root"""
479
539
        """Print `file` to stdout."""
480
540
        self.lock_read()
481
541
        try:
482
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
542
            tree = self.revision_tree(self.get_rev_id(revno))
483
543
            # use inventory as it was in that revision
484
544
            file_id = tree.inventory.path2id(file)
485
545
            if not file_id:
583
643
            f.close()
584
644
 
585
645
 
 
646
    def has_revision(self, revision_id):
 
647
        """True if this branch has a copy of the revision.
 
648
 
 
649
        This does not necessarily imply the revision is merge
 
650
        or on the mainline."""
 
651
        return (revision_id is None
 
652
                or revision_id in self.revision_store)
 
653
 
 
654
 
586
655
    def get_revision_xml_file(self, revision_id):
587
656
        """Return XML file object for revision object."""
588
657
        if not revision_id or not isinstance(revision_id, basestring):
592
661
        try:
593
662
            try:
594
663
                return self.revision_store[revision_id]
595
 
            except IndexError:
 
664
            except (IndexError, KeyError):
596
665
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
597
666
        finally:
598
667
            self.unlock()
599
668
 
600
669
 
601
 
    #deprecated
602
 
    get_revision_xml = get_revision_xml_file
 
670
    def get_revision_xml(self, revision_id):
 
671
        return self.get_revision_xml_file(revision_id).read()
603
672
 
604
673
 
605
674
    def get_revision(self, revision_id):
638
707
 
639
708
        return compare_trees(old_tree, new_tree)
640
709
 
641
 
        
642
710
 
643
711
    def get_revision_sha1(self, revision_id):
644
712
        """Hash the stored value of a revision, and return it."""
645
 
        # In the future, revision entries will be signed. At that
646
 
        # point, it is probably best *not* to include the signature
647
 
        # in the revision hash. Because that lets you re-sign
648
 
        # the revision, (add signatures/remove signatures) and still
649
 
        # have all hash pointers stay consistent.
650
 
        # But for now, just hash the contents.
651
 
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
652
 
 
 
713
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
714
 
 
715
 
 
716
    def _get_ancestry_weave(self):
 
717
        return self.control_weaves.get_weave('ancestry')
 
718
        
653
719
 
654
720
    def get_ancestry(self, revision_id):
655
721
        """Return a list of revision-ids integrated by a revision.
656
722
        """
657
 
        w = self.weave_store.get_weave(ANCESTRY_FILEID)
658
723
        # strip newlines
659
 
        return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
724
        if revision_id is None:
 
725
            return [None]
 
726
        w = self._get_ancestry_weave()
 
727
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
660
728
 
661
729
 
662
730
    def get_inventory_weave(self):
663
 
        return self.weave_store.get_weave(INVENTORY_FILEID)
 
731
        return self.control_weaves.get_weave('inventory')
664
732
 
665
733
 
666
734
    def get_inventory(self, revision_id):
667
735
        """Get Inventory object by hash."""
668
 
        # FIXME: The text gets passed around a lot coming from the weave.
669
 
        f = StringIO(self.get_inventory_xml(revision_id))
670
 
        return bzrlib.xml5.serializer_v5.read_inventory(f)
 
736
        xml = self.get_inventory_xml(revision_id)
 
737
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
671
738
 
672
739
 
673
740
    def get_inventory_xml(self, revision_id):
688
755
 
689
756
    def get_revision_inventory(self, revision_id):
690
757
        """Return inventory of a past revision."""
 
758
        # TODO: Unify this with get_inventory()
691
759
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
692
760
        # must be the same as its revision, so this is trivial.
693
761
        if revision_id == None:
697
765
 
698
766
 
699
767
    def revision_history(self):
700
 
        """Return sequence of revision hashes on to this branch.
701
 
 
702
 
        >>> ScratchBranch().revision_history()
703
 
        []
704
 
        """
 
768
        """Return sequence of revision hashes on to this branch."""
705
769
        self.lock_read()
706
770
        try:
707
771
            return [l.rstrip('\r\n') for l in
712
776
 
713
777
    def common_ancestor(self, other, self_revno=None, other_revno=None):
714
778
        """
715
 
        >>> import commit
 
779
        >>> from bzrlib.commit import commit
716
780
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
717
781
        >>> sb.common_ancestor(sb) == (None, None)
718
782
        True
719
 
        >>> commit.commit(sb, "Committing first revision")
 
783
        >>> commit(sb, "Committing first revision", verbose=False)
720
784
        >>> sb.common_ancestor(sb)[0]
721
785
        1
722
786
        >>> clone = sb.clone()
723
 
        >>> commit.commit(sb, "Committing second revision")
 
787
        >>> commit(sb, "Committing second revision", verbose=False)
724
788
        >>> sb.common_ancestor(sb)[0]
725
789
        2
726
790
        >>> sb.common_ancestor(clone)[0]
727
791
        1
728
 
        >>> commit.commit(clone, "Committing divergent second revision")
 
792
        >>> commit(clone, "Committing divergent second revision", 
 
793
        ...               verbose=False)
729
794
        >>> sb.common_ancestor(clone)[0]
730
795
        1
731
796
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
763
828
        return len(self.revision_history())
764
829
 
765
830
 
766
 
    def last_patch(self):
 
831
    def last_revision(self):
767
832
        """Return last patch hash, or None if no history.
768
833
        """
769
834
        ph = self.revision_history()
774
839
 
775
840
 
776
841
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
777
 
        """
 
842
        """Return a list of new revisions that would perfectly fit.
 
843
        
778
844
        If self and other have not diverged, return a list of the revisions
779
845
        present in other, but missing from self.
780
846
 
800
866
        Traceback (most recent call last):
801
867
        DivergedBranches: These branches have diverged.
802
868
        """
 
869
        # FIXME: If the branches have diverged, but the latest
 
870
        # revision in this branch is completely merged into the other,
 
871
        # then we should still be able to pull.
803
872
        self_history = self.revision_history()
804
873
        self_len = len(self_history)
805
874
        other_history = other.revision_history()
811
880
 
812
881
        if stop_revision is None:
813
882
            stop_revision = other_len
814
 
        elif stop_revision > other_len:
815
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
816
 
        
 
883
        else:
 
884
            assert isinstance(stop_revision, int)
 
885
            if stop_revision > other_len:
 
886
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
817
887
        return other_history[self_len:stop_revision]
818
888
 
819
 
 
820
889
    def update_revisions(self, other, stop_revision=None):
821
 
        """Pull in all new revisions from other branch.
822
 
        """
 
890
        """Pull in new perfect-fit revisions."""
823
891
        from bzrlib.fetch import greedy_fetch
824
 
 
825
 
        pb = bzrlib.ui.ui_factory.progress_bar()
826
 
        pb.update('comparing histories')
827
 
 
828
 
        revision_ids = self.missing_revisions(other, stop_revision)
829
 
 
830
 
        if len(revision_ids) > 0:
831
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
832
 
        else:
833
 
            count = 0
834
 
        self.append_revision(*revision_ids)
835
 
        ## note("Added %d revisions." % count)
836
 
        pb.clear()
837
 
 
 
892
        from bzrlib.revision import get_intervening_revisions
 
893
        if stop_revision is None:
 
894
            stop_revision = other.last_revision()
 
895
        greedy_fetch(to_branch=self, from_branch=other,
 
896
                     revision=stop_revision)
 
897
        pullable_revs = self.missing_revisions(
 
898
            other, other.revision_id_to_revno(stop_revision))
 
899
        if pullable_revs:
 
900
            greedy_fetch(to_branch=self,
 
901
                         from_branch=other,
 
902
                         revision=pullable_revs[-1])
 
903
            self.append_revision(*pullable_revs)
 
904
    
838
905
 
839
906
    def commit(self, *args, **kw):
840
907
        from bzrlib.commit import Commit
841
908
        Commit().commit(self, *args, **kw)
842
 
        
843
 
 
844
 
    def lookup_revision(self, revision):
845
 
        """Return the revision identifier for a given revision information."""
846
 
        revno, info = self._get_revision_info(revision)
847
 
        return info
848
 
 
849
 
 
 
909
    
850
910
    def revision_id_to_revno(self, revision_id):
851
911
        """Given a revision id, return its revno"""
 
912
        if revision_id is None:
 
913
            return 0
852
914
        history = self.revision_history()
853
915
        try:
854
916
            return history.index(revision_id) + 1
855
917
        except ValueError:
856
918
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
857
919
 
858
 
 
859
 
    def get_revision_info(self, revision):
860
 
        """Return (revno, revision id) for revision identifier.
861
 
 
862
 
        revision can be an integer, in which case it is assumed to be revno (though
863
 
            this will translate negative values into positive ones)
864
 
        revision can also be a string, in which case it is parsed for something like
865
 
            'date:' or 'revid:' etc.
866
 
        """
867
 
        revno, rev_id = self._get_revision_info(revision)
868
 
        if revno is None:
869
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
870
 
        return revno, rev_id
871
 
 
872
920
    def get_rev_id(self, revno, history=None):
873
921
        """Find the revision id of the specified revno."""
874
922
        if revno == 0:
879
927
            raise bzrlib.errors.NoSuchRevision(self, revno)
880
928
        return history[revno - 1]
881
929
 
882
 
    def _get_revision_info(self, revision):
883
 
        """Return (revno, revision id) for revision specifier.
884
 
 
885
 
        revision can be an integer, in which case it is assumed to be revno
886
 
        (though this will translate negative values into positive ones)
887
 
        revision can also be a string, in which case it is parsed for something
888
 
        like 'date:' or 'revid:' etc.
889
 
 
890
 
        A revid is always returned.  If it is None, the specifier referred to
891
 
        the null revision.  If the revid does not occur in the revision
892
 
        history, revno will be None.
893
 
        """
894
 
        
895
 
        if revision is None:
896
 
            return 0, None
897
 
        revno = None
898
 
        try:# Convert to int if possible
899
 
            revision = int(revision)
900
 
        except ValueError:
901
 
            pass
902
 
        revs = self.revision_history()
903
 
        if isinstance(revision, int):
904
 
            if revision < 0:
905
 
                revno = len(revs) + revision + 1
906
 
            else:
907
 
                revno = revision
908
 
            rev_id = self.get_rev_id(revno, revs)
909
 
        elif isinstance(revision, basestring):
910
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
911
 
                if revision.startswith(prefix):
912
 
                    result = func(self, revs, revision)
913
 
                    if len(result) > 1:
914
 
                        revno, rev_id = result
915
 
                    else:
916
 
                        revno = result[0]
917
 
                        rev_id = self.get_rev_id(revno, revs)
918
 
                    break
919
 
            else:
920
 
                raise BzrError('No namespace registered for string: %r' %
921
 
                               revision)
922
 
        else:
923
 
            raise TypeError('Unhandled revision type %s' % revision)
924
 
 
925
 
        if revno is None:
926
 
            if rev_id is None:
927
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
928
 
        return revno, rev_id
929
 
 
930
 
    def _namespace_revno(self, revs, revision):
931
 
        """Lookup a revision by revision number"""
932
 
        assert revision.startswith('revno:')
933
 
        try:
934
 
            return (int(revision[6:]),)
935
 
        except ValueError:
936
 
            return None
937
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
938
 
 
939
 
    def _namespace_revid(self, revs, revision):
940
 
        assert revision.startswith('revid:')
941
 
        rev_id = revision[len('revid:'):]
942
 
        try:
943
 
            return revs.index(rev_id) + 1, rev_id
944
 
        except ValueError:
945
 
            return None, rev_id
946
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
947
 
 
948
 
    def _namespace_last(self, revs, revision):
949
 
        assert revision.startswith('last:')
950
 
        try:
951
 
            offset = int(revision[5:])
952
 
        except ValueError:
953
 
            return (None,)
954
 
        else:
955
 
            if offset <= 0:
956
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
957
 
            return (len(revs) - offset + 1,)
958
 
    REVISION_NAMESPACES['last:'] = _namespace_last
959
 
 
960
 
    def _namespace_tag(self, revs, revision):
961
 
        assert revision.startswith('tag:')
962
 
        raise BzrError('tag: namespace registered, but not implemented.')
963
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
964
 
 
965
 
    def _namespace_date(self, revs, revision):
966
 
        assert revision.startswith('date:')
967
 
        import datetime
968
 
        # Spec for date revisions:
969
 
        #   date:value
970
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
971
 
        #   it can also start with a '+/-/='. '+' says match the first
972
 
        #   entry after the given date. '-' is match the first entry before the date
973
 
        #   '=' is match the first entry after, but still on the given date.
974
 
        #
975
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
976
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
977
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
978
 
        #       May 13th, 2005 at 0:00
979
 
        #
980
 
        #   So the proper way of saying 'give me all entries for today' is:
981
 
        #       -r {date:+today}:{date:-tomorrow}
982
 
        #   The default is '=' when not supplied
983
 
        val = revision[5:]
984
 
        match_style = '='
985
 
        if val[:1] in ('+', '-', '='):
986
 
            match_style = val[:1]
987
 
            val = val[1:]
988
 
 
989
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
990
 
        if val.lower() == 'yesterday':
991
 
            dt = today - datetime.timedelta(days=1)
992
 
        elif val.lower() == 'today':
993
 
            dt = today
994
 
        elif val.lower() == 'tomorrow':
995
 
            dt = today + datetime.timedelta(days=1)
996
 
        else:
997
 
            import re
998
 
            # This should be done outside the function to avoid recompiling it.
999
 
            _date_re = re.compile(
1000
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1001
 
                    r'(,|T)?\s*'
1002
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1003
 
                )
1004
 
            m = _date_re.match(val)
1005
 
            if not m or (not m.group('date') and not m.group('time')):
1006
 
                raise BzrError('Invalid revision date %r' % revision)
1007
 
 
1008
 
            if m.group('date'):
1009
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1010
 
            else:
1011
 
                year, month, day = today.year, today.month, today.day
1012
 
            if m.group('time'):
1013
 
                hour = int(m.group('hour'))
1014
 
                minute = int(m.group('minute'))
1015
 
                if m.group('second'):
1016
 
                    second = int(m.group('second'))
1017
 
                else:
1018
 
                    second = 0
1019
 
            else:
1020
 
                hour, minute, second = 0,0,0
1021
 
 
1022
 
            dt = datetime.datetime(year=year, month=month, day=day,
1023
 
                    hour=hour, minute=minute, second=second)
1024
 
        first = dt
1025
 
        last = None
1026
 
        reversed = False
1027
 
        if match_style == '-':
1028
 
            reversed = True
1029
 
        elif match_style == '=':
1030
 
            last = dt + datetime.timedelta(days=1)
1031
 
 
1032
 
        if reversed:
1033
 
            for i in range(len(revs)-1, -1, -1):
1034
 
                r = self.get_revision(revs[i])
1035
 
                # TODO: Handle timezone.
1036
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1037
 
                if first >= dt and (last is None or dt >= last):
1038
 
                    return (i+1,)
1039
 
        else:
1040
 
            for i in range(len(revs)):
1041
 
                r = self.get_revision(revs[i])
1042
 
                # TODO: Handle timezone.
1043
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1044
 
                if first <= dt and (last is None or dt <= last):
1045
 
                    return (i+1,)
1046
 
    REVISION_NAMESPACES['date:'] = _namespace_date
1047
 
 
1048
930
    def revision_tree(self, revision_id):
1049
931
        """Return Tree for a revision on this branch.
1050
932
 
1061
943
 
1062
944
    def working_tree(self):
1063
945
        """Return a `Tree` for the working copy."""
1064
 
        from workingtree import WorkingTree
 
946
        from bzrlib.workingtree import WorkingTree
1065
947
        return WorkingTree(self.base, self.read_working_inventory())
1066
948
 
1067
949
 
1070
952
 
1071
953
        If there are no revisions yet, return an `EmptyTree`.
1072
954
        """
1073
 
        return self.revision_tree(self.last_patch())
 
955
        return self.revision_tree(self.last_revision())
1074
956
 
1075
957
 
1076
958
    def rename_one(self, from_rel, to_rel):
1111
993
            from_abs = self.abspath(from_rel)
1112
994
            to_abs = self.abspath(to_rel)
1113
995
            try:
1114
 
                os.rename(from_abs, to_abs)
 
996
                rename(from_abs, to_abs)
1115
997
            except OSError, e:
1116
998
                raise BzrError("failed to rename %r to %r: %s"
1117
999
                        % (from_abs, to_abs, e[1]),
1180
1062
                result.append((f, dest_path))
1181
1063
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1182
1064
                try:
1183
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1065
                    rename(self.abspath(f), self.abspath(dest_path))
1184
1066
                except OSError, e:
1185
1067
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1186
1068
                            ["rename rolled back"])
1252
1134
 
1253
1135
 
1254
1136
    def add_pending_merge(self, revision_id):
1255
 
        from bzrlib.revision import validate_revision_id
1256
 
 
1257
1137
        validate_revision_id(revision_id)
1258
 
 
 
1138
        # TODO: Perhaps should check at this point that the
 
1139
        # history of the revision is actually present?
1259
1140
        p = self.pending_merges()
1260
1141
        if revision_id in p:
1261
1142
            return
1327
1208
            raise InvalidRevisionNumber(revno)
1328
1209
        
1329
1210
        
1330
 
 
1331
 
 
1332
 
class ScratchBranch(Branch):
 
1211
        
 
1212
 
 
1213
 
 
1214
class ScratchBranch(LocalBranch):
1333
1215
    """Special test class: a branch that cleans up after itself.
1334
1216
 
1335
1217
    >>> b = ScratchBranch()
1352
1234
        if base is None:
1353
1235
            base = mkdtemp()
1354
1236
            init = True
1355
 
        Branch.__init__(self, base, init=init)
 
1237
        LocalBranch.__init__(self, base, init=init)
1356
1238
        for d in dirs:
1357
1239
            os.mkdir(self.abspath(d))
1358
1240
            
1364
1246
        """
1365
1247
        >>> orig = ScratchBranch(files=["file1", "file2"])
1366
1248
        >>> clone = orig.clone()
1367
 
        >>> os.path.samefile(orig.base, clone.base)
 
1249
        >>> if os.name != 'nt':
 
1250
        ...   os.path.samefile(orig.base, clone.base)
 
1251
        ... else:
 
1252
        ...   orig.base == clone.base
 
1253
        ...
1368
1254
        False
1369
1255
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1370
1256
        True
1453
1339
    return gen_file_id('TREE_ROOT')
1454
1340
 
1455
1341
 
1456
 
def pull_loc(branch):
1457
 
    # TODO: Should perhaps just make attribute be 'base' in
1458
 
    # RemoteBranch and Branch?
1459
 
    if hasattr(branch, "baseurl"):
1460
 
        return branch.baseurl
1461
 
    else:
1462
 
        return branch.base
1463
 
 
1464
 
 
1465
 
def copy_branch(branch_from, to_location, revision=None):
 
1342
def copy_branch(branch_from, to_location, revision=None, basis_branch=None):
1466
1343
    """Copy branch_from into the existing directory to_location.
1467
1344
 
1468
1345
    revision
1469
1346
        If not None, only revisions up to this point will be copied.
1470
 
        The head of the new branch will be that revision.
 
1347
        The head of the new branch will be that revision.  Must be a
 
1348
        revid or None.
1471
1349
 
1472
1350
    to_location
1473
1351
        The name of a local directory that exists but is empty.
 
1352
 
 
1353
    revno
 
1354
        The revision to copy up to
 
1355
 
 
1356
    basis_branch
 
1357
        A local branch to copy revisions from, related to branch_from
1474
1358
    """
 
1359
    # TODO: This could be done *much* more efficiently by just copying
 
1360
    # all the whole weaves and revisions, rather than getting one
 
1361
    # revision at a time.
1475
1362
    from bzrlib.merge import merge
1476
 
    from bzrlib.branch import Branch
1477
1363
 
1478
1364
    assert isinstance(branch_from, Branch)
1479
1365
    assert isinstance(to_location, basestring)
1480
1366
    
1481
 
    br_to = Branch(to_location, init=True)
 
1367
    br_to = Branch.initialize(to_location)
 
1368
    if basis_branch is not None:
 
1369
        basis_branch.push_stores(br_to)
1482
1370
    br_to.set_root_id(branch_from.get_root_id())
1483
1371
    if revision is None:
1484
 
        revno = branch_from.revno()
1485
 
    else:
1486
 
        revno, rev_id = branch_from.get_revision_info(revision)
1487
 
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1372
        revision = branch_from.last_revision()
 
1373
    br_to.update_revisions(branch_from, stop_revision=revision)
1488
1374
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1489
1375
          check_clean=False, ignore_zero=True)
1490
 
    
1491
 
    from_location = pull_loc(branch_from)
1492
 
    br_to.set_parent(pull_loc(branch_from))
1493
 
 
 
1376
    br_to.set_parent(branch_from.base)
 
1377
    return br_to