~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-09-29 21:07:17 UTC
  • mfrom: (1393.1.6)
  • mto: (1185.25.1)
  • mto: This revision was merged to the branch mainline in revision 1419.
  • Revision ID: abentley@panoramicfeedback.com-20050929210717-cd73981590f17017
Merged the weave changes

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_downlevel(base):
 
136
        """Open a branch which may be of an old format.
 
137
        
 
138
        Only local branches are supported."""
 
139
        return LocalBranch(base, find_root=False, relax_version_check=True)
 
140
        
 
141
    @staticmethod
 
142
    def open(base):
 
143
        """Open an existing branch, rooted at 'base' (url)"""
 
144
        if base and (base.startswith('http://') or base.startswith('https://')):
 
145
            from bzrlib.remotebranch import RemoteBranch
 
146
            return RemoteBranch(base, find_root=False)
 
147
        else:
 
148
            return LocalBranch(base, find_root=False)
 
149
 
 
150
    @staticmethod
 
151
    def open_containing(url):
 
152
        """Open an existing branch which contains url.
 
153
        
 
154
        This probes for a branch at url, and searches upwards from there.
 
155
        """
 
156
        if url and (url.startswith('http://') or url.startswith('https://')):
 
157
            from bzrlib.remotebranch import RemoteBranch
 
158
            return RemoteBranch(url)
 
159
        else:
 
160
            return LocalBranch(url)
 
161
 
 
162
    @staticmethod
 
163
    def initialize(base):
 
164
        """Create a new branch, rooted at 'base' (url)"""
 
165
        if base and (base.startswith('http://') or base.startswith('https://')):
 
166
            from bzrlib.remotebranch import RemoteBranch
 
167
            return RemoteBranch(base, init=True)
 
168
        else:
 
169
            return LocalBranch(base, init=True)
 
170
 
 
171
    def setup_caching(self, cache_root):
 
172
        """Subclasses that care about caching should override this, and set
 
173
        up cached stores located under cache_root.
 
174
        """
 
175
 
 
176
 
 
177
class LocalBranch(Branch):
 
178
    """A branch stored in the actual filesystem.
 
179
 
 
180
    Note that it's "local" in the context of the filesystem; it doesn't
 
181
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
182
    it's writable, and can be accessed via the normal filesystem API.
161
183
 
162
184
    _lock_mode
163
185
        None, or 'r' or 'w'
169
191
    _lock
170
192
        Lock object from bzrlib.lock.
171
193
    """
172
 
    base = None
 
194
    # We actually expect this class to be somewhat short-lived; part of its
 
195
    # purpose is to try to isolate what bits of the branch logic are tied to
 
196
    # filesystem access, so that in a later step, we can extricate them to
 
197
    # a separarte ("storage") class.
173
198
    _lock_mode = None
174
199
    _lock_count = None
175
200
    _lock = None
180
205
    # This should match a prefix with a function which accepts
181
206
    REVISION_NAMESPACES = {}
182
207
 
183
 
    def __init__(self, base, init=False, find_root=True):
 
208
    def push_stores(self, branch_to):
 
209
        """Copy the content of this branches store to branch_to."""
 
210
        if (self._branch_format != branch_to._branch_format
 
211
            or self._branch_format != 4):
 
212
            from bzrlib.fetch import greedy_fetch
 
213
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
 
214
                   self, self._branch_format, branch_to, branch_to._branch_format)
 
215
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
216
                         revision=self.last_revision())
 
217
            return
 
218
 
 
219
        store_pairs = ((self.text_store,      branch_to.text_store),
 
220
                       (self.inventory_store, branch_to.inventory_store),
 
221
                       (self.revision_store,  branch_to.revision_store))
 
222
        try:
 
223
            for from_store, to_store in store_pairs: 
 
224
                copy_all(from_store, to_store)
 
225
        except UnlistableStore:
 
226
            raise UnlistableBranch(from_store)
 
227
 
 
228
    def __init__(self, base, init=False, find_root=True,
 
229
                 relax_version_check=False):
184
230
        """Create new branch object at a particular location.
185
231
 
186
 
        base -- Base directory for the branch.
 
232
        base -- Base directory for the branch. May be a file:// url.
187
233
        
188
234
        init -- If True, create new control files in a previously
189
235
             unversioned directory.  If False, the branch must already
192
238
        find_root -- If true and init is false, find the root of the
193
239
             existing branch containing base.
194
240
 
 
241
        relax_version_check -- If true, the usual check for the branch
 
242
            version is not applied.  This is intended only for
 
243
            upgrade/recovery type use; it's not guaranteed that
 
244
            all operations will work on old format branches.
 
245
 
195
246
        In the test suite, creation of new trees is tested using the
196
247
        `ScratchBranch` class.
197
248
        """
201
252
        elif find_root:
202
253
            self.base = find_branch_root(base)
203
254
        else:
 
255
            if base.startswith("file://"):
 
256
                base = base[7:]
204
257
            self.base = os.path.realpath(base)
205
258
            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'))
 
259
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
 
260
                                     ['use "bzr init" to initialize a '
 
261
                                      'new working tree'])
 
262
        self._check_format(relax_version_check)
 
263
        cfn = self.controlfilename
 
264
        if self._branch_format == 4:
 
265
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
 
266
            self.text_store = ImmutableStore(cfn('text-store'))
 
267
        elif self._branch_format == 5:
 
268
            self.control_weaves = WeaveStore(cfn([]))
 
269
            self.weave_store = WeaveStore(cfn('weaves'))
 
270
            if init:
 
271
                # FIXME: Unify with make_control_files
 
272
                self.control_weaves.put_empty_weave('inventory')
 
273
                self.control_weaves.put_empty_weave('ancestry')
 
274
        self.revision_store = ImmutableStore(cfn('revision-store'))
214
275
 
215
276
 
216
277
    def __str__(self):
222
283
 
223
284
    def __del__(self):
224
285
        if self._lock_mode or self._lock:
225
 
            from warnings import warn
 
286
            # XXX: This should show something every time, and be suitable for
 
287
            # headless operation and embedding
226
288
            warn("branch %r was not explicitly unlocked" % self)
227
289
            self._lock.unlock()
228
290
 
229
 
 
230
291
    def lock_write(self):
231
292
        if self._lock_mode:
232
293
            if self._lock_mode != 'w':
233
 
                from errors import LockError
234
294
                raise LockError("can't upgrade to a write lock from %r" %
235
295
                                self._lock_mode)
236
296
            self._lock_count += 1
256
316
                        
257
317
    def unlock(self):
258
318
        if not self._lock_mode:
259
 
            from errors import LockError
260
319
            raise LockError('branch %r is not locked' % (self))
261
320
 
262
321
        if self._lock_count > 1:
317
376
        for d in ('text-store', 'revision-store',
318
377
                  'weaves'):
319
378
            os.mkdir(self.controlfilename(d))
320
 
        for f in ('revision-history', 'merged-patches',
321
 
                  'pending-merged-patches', 'branch-name',
 
379
        for f in ('revision-history',
 
380
                  'branch-name',
322
381
                  'branch-lock',
323
382
                  'pending-merges'):
324
383
            self.controlfile(f, 'w').write('')
329
388
        # simplicity.
330
389
        f = self.controlfile('inventory','w')
331
390
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
332
 
        
333
 
 
334
 
 
335
 
    def _check_format(self):
 
391
 
 
392
 
 
393
    def _check_format(self, relax_version_check):
336
394
        """Check this branch format is supported.
337
395
 
338
396
        The format level is stored, as an integer, in
341
399
        In the future, we might need different in-memory Branch
342
400
        classes to support downlevel branches.  But not yet.
343
401
        """
344
 
        fmt = self.controlfile('branch-format', 'r').read()
 
402
        try:
 
403
            fmt = self.controlfile('branch-format', 'r').read()
 
404
        except IOError, e:
 
405
            if e.errno == errno.ENOENT:
 
406
                raise NotBranchError(self.base)
 
407
            else:
 
408
                raise
 
409
 
345
410
        if fmt == BZR_BRANCH_FORMAT_5:
346
411
            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'))
 
412
        elif fmt == BZR_BRANCH_FORMAT_4:
 
413
            self._branch_format = 4
 
414
 
 
415
        if (not relax_version_check
 
416
            and self._branch_format != 5):
 
417
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
418
                           ['use a different bzr version',
 
419
                            'or remove the .bzr directory and "bzr init" again'])
353
420
 
354
421
    def get_root_id(self):
355
422
        """Return the id of this branches root"""
479
546
        """Print `file` to stdout."""
480
547
        self.lock_read()
481
548
        try:
482
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
549
            tree = self.revision_tree(self.get_rev_id(revno))
483
550
            # use inventory as it was in that revision
484
551
            file_id = tree.inventory.path2id(file)
485
552
            if not file_id:
583
650
            f.close()
584
651
 
585
652
 
 
653
    def has_revision(self, revision_id):
 
654
        """True if this branch has a copy of the revision.
 
655
 
 
656
        This does not necessarily imply the revision is merge
 
657
        or on the mainline."""
 
658
        return (revision_id is None
 
659
                or revision_id in self.revision_store)
 
660
 
 
661
 
586
662
    def get_revision_xml_file(self, revision_id):
587
663
        """Return XML file object for revision object."""
588
664
        if not revision_id or not isinstance(revision_id, basestring):
592
668
        try:
593
669
            try:
594
670
                return self.revision_store[revision_id]
595
 
            except IndexError:
 
671
            except (IndexError, KeyError):
596
672
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
597
673
        finally:
598
674
            self.unlock()
599
675
 
600
676
 
601
 
    #deprecated
602
 
    get_revision_xml = get_revision_xml_file
 
677
    def get_revision_xml(self, revision_id):
 
678
        return self.get_revision_xml_file(revision_id).read()
603
679
 
604
680
 
605
681
    def get_revision(self, revision_id):
638
714
 
639
715
        return compare_trees(old_tree, new_tree)
640
716
 
641
 
        
642
717
 
643
718
    def get_revision_sha1(self, revision_id):
644
719
        """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
 
 
 
720
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
721
 
 
722
 
 
723
    def _get_ancestry_weave(self):
 
724
        return self.control_weaves.get_weave('ancestry')
 
725
        
653
726
 
654
727
    def get_ancestry(self, revision_id):
655
728
        """Return a list of revision-ids integrated by a revision.
656
729
        """
657
 
        w = self.weave_store.get_weave(ANCESTRY_FILEID)
658
730
        # strip newlines
659
 
        return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
731
        if revision_id is None:
 
732
            return [None]
 
733
        w = self._get_ancestry_weave()
 
734
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
660
735
 
661
736
 
662
737
    def get_inventory_weave(self):
663
 
        return self.weave_store.get_weave(INVENTORY_FILEID)
 
738
        return self.control_weaves.get_weave('inventory')
664
739
 
665
740
 
666
741
    def get_inventory(self, revision_id):
667
742
        """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)
 
743
        xml = self.get_inventory_xml(revision_id)
 
744
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
671
745
 
672
746
 
673
747
    def get_inventory_xml(self, revision_id):
688
762
 
689
763
    def get_revision_inventory(self, revision_id):
690
764
        """Return inventory of a past revision."""
 
765
        # TODO: Unify this with get_inventory()
691
766
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
692
767
        # must be the same as its revision, so this is trivial.
693
768
        if revision_id == None:
697
772
 
698
773
 
699
774
    def revision_history(self):
700
 
        """Return sequence of revision hashes on to this branch.
701
 
 
702
 
        >>> ScratchBranch().revision_history()
703
 
        []
704
 
        """
 
775
        """Return sequence of revision hashes on to this branch."""
705
776
        self.lock_read()
706
777
        try:
707
778
            return [l.rstrip('\r\n') for l in
712
783
 
713
784
    def common_ancestor(self, other, self_revno=None, other_revno=None):
714
785
        """
715
 
        >>> import commit
 
786
        >>> from bzrlib.commit import commit
716
787
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
717
788
        >>> sb.common_ancestor(sb) == (None, None)
718
789
        True
719
 
        >>> commit.commit(sb, "Committing first revision")
 
790
        >>> commit(sb, "Committing first revision", verbose=False)
720
791
        >>> sb.common_ancestor(sb)[0]
721
792
        1
722
793
        >>> clone = sb.clone()
723
 
        >>> commit.commit(sb, "Committing second revision")
 
794
        >>> commit(sb, "Committing second revision", verbose=False)
724
795
        >>> sb.common_ancestor(sb)[0]
725
796
        2
726
797
        >>> sb.common_ancestor(clone)[0]
727
798
        1
728
 
        >>> commit.commit(clone, "Committing divergent second revision")
 
799
        >>> commit(clone, "Committing divergent second revision", 
 
800
        ...               verbose=False)
729
801
        >>> sb.common_ancestor(clone)[0]
730
802
        1
731
803
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
763
835
        return len(self.revision_history())
764
836
 
765
837
 
766
 
    def last_patch(self):
 
838
    def last_revision(self):
767
839
        """Return last patch hash, or None if no history.
768
840
        """
769
841
        ph = self.revision_history()
774
846
 
775
847
 
776
848
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
777
 
        """
 
849
        """Return a list of new revisions that would perfectly fit.
 
850
        
778
851
        If self and other have not diverged, return a list of the revisions
779
852
        present in other, but missing from self.
780
853
 
800
873
        Traceback (most recent call last):
801
874
        DivergedBranches: These branches have diverged.
802
875
        """
 
876
        # FIXME: If the branches have diverged, but the latest
 
877
        # revision in this branch is completely merged into the other,
 
878
        # then we should still be able to pull.
803
879
        self_history = self.revision_history()
804
880
        self_len = len(self_history)
805
881
        other_history = other.revision_history()
811
887
 
812
888
        if stop_revision is None:
813
889
            stop_revision = other_len
814
 
        elif stop_revision > other_len:
815
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
816
 
        
 
890
        else:
 
891
            assert isinstance(stop_revision, int)
 
892
            if stop_revision > other_len:
 
893
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
817
894
        return other_history[self_len:stop_revision]
818
895
 
819
 
 
820
896
    def update_revisions(self, other, stop_revision=None):
821
 
        """Pull in all new revisions from other branch.
822
 
        """
 
897
        """Pull in new perfect-fit revisions."""
823
898
        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
 
 
 
899
        from bzrlib.revision import get_intervening_revisions
 
900
        if stop_revision is None:
 
901
            stop_revision = other.last_revision()
 
902
        greedy_fetch(to_branch=self, from_branch=other,
 
903
                     revision=stop_revision)
 
904
        pullable_revs = self.missing_revisions(
 
905
            other, other.revision_id_to_revno(stop_revision))
 
906
        if pullable_revs:
 
907
            greedy_fetch(to_branch=self,
 
908
                         from_branch=other,
 
909
                         revision=pullable_revs[-1])
 
910
            self.append_revision(*pullable_revs)
 
911
    
838
912
 
839
913
    def commit(self, *args, **kw):
840
914
        from bzrlib.commit import Commit
841
915
        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
 
 
 
916
    
850
917
    def revision_id_to_revno(self, revision_id):
851
918
        """Given a revision id, return its revno"""
 
919
        if revision_id is None:
 
920
            return 0
852
921
        history = self.revision_history()
853
922
        try:
854
923
            return history.index(revision_id) + 1
855
924
        except ValueError:
856
925
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
857
926
 
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
927
    def get_rev_id(self, revno, history=None):
873
928
        """Find the revision id of the specified revno."""
874
929
        if revno == 0:
879
934
            raise bzrlib.errors.NoSuchRevision(self, revno)
880
935
        return history[revno - 1]
881
936
 
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
937
    def revision_tree(self, revision_id):
1049
938
        """Return Tree for a revision on this branch.
1050
939
 
1061
950
 
1062
951
    def working_tree(self):
1063
952
        """Return a `Tree` for the working copy."""
1064
 
        from workingtree import WorkingTree
 
953
        from bzrlib.workingtree import WorkingTree
1065
954
        return WorkingTree(self.base, self.read_working_inventory())
1066
955
 
1067
956
 
1070
959
 
1071
960
        If there are no revisions yet, return an `EmptyTree`.
1072
961
        """
1073
 
        return self.revision_tree(self.last_patch())
 
962
        return self.revision_tree(self.last_revision())
1074
963
 
1075
964
 
1076
965
    def rename_one(self, from_rel, to_rel):
1111
1000
            from_abs = self.abspath(from_rel)
1112
1001
            to_abs = self.abspath(to_rel)
1113
1002
            try:
1114
 
                os.rename(from_abs, to_abs)
 
1003
                rename(from_abs, to_abs)
1115
1004
            except OSError, e:
1116
1005
                raise BzrError("failed to rename %r to %r: %s"
1117
1006
                        % (from_abs, to_abs, e[1]),
1180
1069
                result.append((f, dest_path))
1181
1070
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1182
1071
                try:
1183
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1072
                    rename(self.abspath(f), self.abspath(dest_path))
1184
1073
                except OSError, e:
1185
1074
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1186
1075
                            ["rename rolled back"])
1252
1141
 
1253
1142
 
1254
1143
    def add_pending_merge(self, revision_id):
1255
 
        from bzrlib.revision import validate_revision_id
1256
 
 
1257
1144
        validate_revision_id(revision_id)
1258
 
 
 
1145
        # TODO: Perhaps should check at this point that the
 
1146
        # history of the revision is actually present?
1259
1147
        p = self.pending_merges()
1260
1148
        if revision_id in p:
1261
1149
            return
1327
1215
            raise InvalidRevisionNumber(revno)
1328
1216
        
1329
1217
        
1330
 
 
1331
 
 
1332
 
class ScratchBranch(Branch):
 
1218
        
 
1219
 
 
1220
 
 
1221
class ScratchBranch(LocalBranch):
1333
1222
    """Special test class: a branch that cleans up after itself.
1334
1223
 
1335
1224
    >>> b = ScratchBranch()
1352
1241
        if base is None:
1353
1242
            base = mkdtemp()
1354
1243
            init = True
1355
 
        Branch.__init__(self, base, init=init)
 
1244
        LocalBranch.__init__(self, base, init=init)
1356
1245
        for d in dirs:
1357
1246
            os.mkdir(self.abspath(d))
1358
1247
            
1364
1253
        """
1365
1254
        >>> orig = ScratchBranch(files=["file1", "file2"])
1366
1255
        >>> clone = orig.clone()
1367
 
        >>> os.path.samefile(orig.base, clone.base)
 
1256
        >>> if os.name != 'nt':
 
1257
        ...   os.path.samefile(orig.base, clone.base)
 
1258
        ... else:
 
1259
        ...   orig.base == clone.base
 
1260
        ...
1368
1261
        False
1369
1262
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1370
1263
        True
1453
1346
    return gen_file_id('TREE_ROOT')
1454
1347
 
1455
1348
 
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):
1466
 
    """Copy branch_from into the existing directory to_location.
1467
 
 
1468
 
    revision
1469
 
        If not None, only revisions up to this point will be copied.
1470
 
        The head of the new branch will be that revision.
1471
 
 
1472
 
    to_location
1473
 
        The name of a local directory that exists but is empty.
1474
 
    """
1475
 
    from bzrlib.merge import merge
1476
 
    from bzrlib.branch import Branch
1477
 
 
1478
 
    assert isinstance(branch_from, Branch)
1479
 
    assert isinstance(to_location, basestring)
1480
 
    
1481
 
    br_to = Branch(to_location, init=True)
1482
 
    br_to.set_root_id(branch_from.get_root_id())
1483
 
    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)
1488
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1489
 
          check_clean=False, ignore_zero=True)
1490
 
    
1491
 
    from_location = pull_loc(branch_from)
1492
 
    br_to.set_parent(pull_loc(branch_from))
1493