~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-10-06 08:11:46 UTC
  • mto: (1185.13.3)
  • mto: This revision was merged to the branch mainline in revision 1418.
  • Revision ID: mbp@sourcefrog.net-20051006081146-6be9d0613fd0d24d
- fix join of weaves where parents occur at different offsets
- test for this

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
import sys
19
19
import os
 
20
import errno
 
21
from warnings import warn
 
22
from cStringIO import StringIO
 
23
 
20
24
 
21
25
import bzrlib
 
26
from bzrlib.inventory import InventoryEntry
 
27
import bzrlib.inventory as inventory
22
28
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
 
     DivergedBranches, NotBranchError
 
29
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
 
30
                            rename, splitpath, sha_file, appendpath, 
 
31
                            file_kind)
 
32
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
33
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
34
                           DivergedBranches, LockError, UnlistableStore,
 
35
                           UnlistableBranch, NoSuchFile)
29
36
from bzrlib.textui import show_status
30
37
from bzrlib.revision import Revision
31
38
from bzrlib.delta import compare_trees
32
39
from bzrlib.tree import EmptyTree, RevisionTree
33
 
import bzrlib.xml
 
40
from bzrlib.inventory import Inventory
 
41
from bzrlib.store import copy_all
 
42
from bzrlib.store.compressed_text import CompressedTextStore
 
43
from bzrlib.store.text import TextStore
 
44
from bzrlib.store.weave import WeaveStore
 
45
from bzrlib.transport import Transport, get_transport
 
46
import bzrlib.xml5
34
47
import bzrlib.ui
35
48
 
36
49
 
37
 
 
38
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
50
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
51
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
39
52
## TODO: Maybe include checks for common corruption of newlines, etc?
40
53
 
41
54
 
42
55
# TODO: Some operations like log might retrieve the same revisions
43
56
# repeatedly to calculate deltas.  We could perhaps have a weakref
44
 
# cache in memory to make this faster.
45
 
 
46
 
# TODO: please move the revision-string syntax stuff out of the branch
47
 
# object; it's clutter
48
 
 
49
 
 
50
 
def find_branch(f, **args):
51
 
    if f and (f.startswith('http://') or f.startswith('https://')):
52
 
        from bzrlib.remotebranch import RemoteBranch
53
 
        return RemoteBranch(f, **args)
54
 
    else:
55
 
        return Branch(f, **args)
56
 
 
57
 
 
58
 
def find_cached_branch(f, cache_root, **args):
59
 
    from bzrlib.remotebranch import RemoteBranch
60
 
    br = find_branch(f, **args)
61
 
    def cacheify(br, store_name):
62
 
        from bzrlib.meta_store import CachedStore
63
 
        cache_path = os.path.join(cache_root, store_name)
64
 
        os.mkdir(cache_path)
65
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
66
 
        setattr(br, store_name, new_store)
67
 
 
68
 
    if isinstance(br, RemoteBranch):
69
 
        cacheify(br, 'inventory_store')
70
 
        cacheify(br, 'text_store')
71
 
        cacheify(br, 'revision_store')
72
 
    return br
73
 
 
74
 
 
 
57
# cache in memory to make this faster.  In general anything can be
 
58
# cached in memory between lock and unlock operations.
 
59
 
 
60
def find_branch(*ignored, **ignored_too):
 
61
    # XXX: leave this here for about one release, then remove it
 
62
    raise NotImplementedError('find_branch() is not supported anymore, '
 
63
                              'please use one of the new branch constructors')
75
64
def _relpath(base, path):
76
65
    """Return path relative to base, or raise exception.
77
66
 
99
88
    return os.sep.join(s)
100
89
        
101
90
 
102
 
def find_branch_root(f=None):
103
 
    """Find the branch root enclosing f, or pwd.
104
 
 
105
 
    f may be a filename or a URL.
106
 
 
107
 
    It is not necessary that f exists.
 
91
def find_branch_root(t):
 
92
    """Find the branch root enclosing the transport's base.
 
93
 
 
94
    t is a Transport object.
 
95
 
 
96
    It is not necessary that the base of t exists.
108
97
 
109
98
    Basically we keep looking up until we find the control directory or
110
99
    run into the root.  If there isn't one, raises NotBranchError.
111
100
    """
112
 
    if f == None:
113
 
        f = os.getcwd()
114
 
    elif hasattr(os.path, 'realpath'):
115
 
        f = os.path.realpath(f)
116
 
    else:
117
 
        f = os.path.abspath(f)
118
 
    if not os.path.exists(f):
119
 
        raise BzrError('%r does not exist' % f)
120
 
        
121
 
 
122
 
    orig_f = f
123
 
 
 
101
    orig_base = t.base
124
102
    while True:
125
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
126
 
            return f
127
 
        head, tail = os.path.split(f)
128
 
        if head == f:
 
103
        if t.has(bzrlib.BZRDIR):
 
104
            return t
 
105
        new_t = t.clone('..')
 
106
        if new_t.base == t.base:
129
107
            # reached the root, whatever that may be
130
 
            raise NotBranchError('%s is not in a branch' % orig_f)
131
 
        f = head
132
 
 
133
 
 
 
108
            raise NotBranchError('%s is not in a branch' % orig_base)
 
109
        t = new_t
134
110
 
135
111
 
136
112
######################################################################
140
116
    """Branch holding a history of revisions.
141
117
 
142
118
    base
143
 
        Base directory of the branch.
 
119
        Base directory/url of the branch.
 
120
    """
 
121
    base = None
 
122
 
 
123
    def __init__(self, *ignored, **ignored_too):
 
124
        raise NotImplementedError('The Branch class is abstract')
 
125
 
 
126
    @staticmethod
 
127
    def open_downlevel(base):
 
128
        """Open a branch which may be of an old format.
 
129
        
 
130
        Only local branches are supported."""
 
131
        return _Branch(get_transport(base), relax_version_check=True)
 
132
        
 
133
    @staticmethod
 
134
    def open(base):
 
135
        """Open an existing branch, rooted at 'base' (url)"""
 
136
        t = get_transport(base)
 
137
        mutter("trying to open %r with transport %r", base, t)
 
138
        return _Branch(t)
 
139
 
 
140
    @staticmethod
 
141
    def open_containing(url):
 
142
        """Open an existing branch which contains url.
 
143
        
 
144
        This probes for a branch at url, and searches upwards from there.
 
145
        """
 
146
        t = get_transport(url)
 
147
        t = find_branch_root(t)
 
148
        return _Branch(t)
 
149
 
 
150
    @staticmethod
 
151
    def initialize(base):
 
152
        """Create a new branch, rooted at 'base' (url)"""
 
153
        t = get_transport(base)
 
154
        return _Branch(t, init=True)
 
155
 
 
156
    def setup_caching(self, cache_root):
 
157
        """Subclasses that care about caching should override this, and set
 
158
        up cached stores located under cache_root.
 
159
        """
 
160
        self.cache_root = cache_root
 
161
 
 
162
 
 
163
class _Branch(Branch):
 
164
    """A branch stored in the actual filesystem.
 
165
 
 
166
    Note that it's "local" in the context of the filesystem; it doesn't
 
167
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
168
    it's writable, and can be accessed via the normal filesystem API.
144
169
 
145
170
    _lock_mode
146
171
        None, or 'r' or 'w'
152
177
    _lock
153
178
        Lock object from bzrlib.lock.
154
179
    """
155
 
    base = None
 
180
    # We actually expect this class to be somewhat short-lived; part of its
 
181
    # purpose is to try to isolate what bits of the branch logic are tied to
 
182
    # filesystem access, so that in a later step, we can extricate them to
 
183
    # a separarte ("storage") class.
156
184
    _lock_mode = None
157
185
    _lock_count = None
158
186
    _lock = None
 
187
    _inventory_weave = None
159
188
    
160
189
    # Map some sort of prefix into a namespace
161
190
    # stuff like "revno:10", "revid:", etc.
162
191
    # This should match a prefix with a function which accepts
163
192
    REVISION_NAMESPACES = {}
164
193
 
165
 
    def __init__(self, base, init=False, find_root=True):
 
194
    def push_stores(self, branch_to):
 
195
        """Copy the content of this branches store to branch_to."""
 
196
        if (self._branch_format != branch_to._branch_format
 
197
            or self._branch_format != 4):
 
198
            from bzrlib.fetch import greedy_fetch
 
199
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
 
200
                   self, self._branch_format, branch_to, branch_to._branch_format)
 
201
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
202
                         revision=self.last_revision())
 
203
            return
 
204
 
 
205
        store_pairs = ((self.text_store,      branch_to.text_store),
 
206
                       (self.inventory_store, branch_to.inventory_store),
 
207
                       (self.revision_store,  branch_to.revision_store))
 
208
        try:
 
209
            for from_store, to_store in store_pairs: 
 
210
                copy_all(from_store, to_store)
 
211
        except UnlistableStore:
 
212
            raise UnlistableBranch(from_store)
 
213
 
 
214
    def __init__(self, transport, init=False,
 
215
                 relax_version_check=False):
166
216
        """Create new branch object at a particular location.
167
217
 
168
 
        base -- Base directory for the branch. May be a file:// url.
 
218
        transport -- A Transport object, defining how to access files.
 
219
                (If a string, transport.transport() will be used to
 
220
                create a Transport object)
169
221
        
170
222
        init -- If True, create new control files in a previously
171
223
             unversioned directory.  If False, the branch must already
172
224
             be versioned.
173
225
 
174
 
        find_root -- If true and init is false, find the root of the
175
 
             existing branch containing base.
 
226
        relax_version_check -- If true, the usual check for the branch
 
227
            version is not applied.  This is intended only for
 
228
            upgrade/recovery type use; it's not guaranteed that
 
229
            all operations will work on old format branches.
176
230
 
177
231
        In the test suite, creation of new trees is tested using the
178
232
        `ScratchBranch` class.
179
233
        """
180
 
        from bzrlib.store import ImmutableStore
 
234
        assert isinstance(transport, Transport), \
 
235
            "%r is not a Transport" % transport
 
236
        self._transport = transport
181
237
        if init:
182
 
            self.base = os.path.realpath(base)
183
238
            self._make_control()
184
 
        elif find_root:
185
 
            self.base = find_branch_root(base)
186
 
        else:
187
 
            if base.startswith("file://"):
188
 
                base = base[7:]
189
 
            self.base = os.path.realpath(base)
190
 
            if not isdir(self.controlfilename('.')):
191
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
192
 
                                     ['use "bzr init" to initialize a new working tree',
193
 
                                      'current bzr can only operate from top-of-tree'])
194
 
        self._check_format()
195
 
 
196
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
197
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
198
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
199
 
 
 
239
        self._check_format(relax_version_check)
 
240
 
 
241
        def get_store(name, compressed=True):
 
242
            # FIXME: This approach of assuming stores are all entirely compressed
 
243
            # or entirely uncompressed is tidy, but breaks upgrade from 
 
244
            # some existing branches where there's a mixture; we probably 
 
245
            # still want the option to look for both.
 
246
            relpath = self._rel_controlfilename(name)
 
247
            if compressed:
 
248
                store = CompressedTextStore(self._transport.clone(relpath))
 
249
            else:
 
250
                store = TextStore(self._transport.clone(relpath))
 
251
            #if self._transport.should_cache():
 
252
            #    cache_path = os.path.join(self.cache_root, name)
 
253
            #    os.mkdir(cache_path)
 
254
            #    store = bzrlib.store.CachedStore(store, cache_path)
 
255
            return store
 
256
        def get_weave(name):
 
257
            relpath = self._rel_controlfilename(name)
 
258
            ws = WeaveStore(self._transport.clone(relpath))
 
259
            if self._transport.should_cache():
 
260
                ws.enable_cache = True
 
261
            return ws
 
262
 
 
263
        if self._branch_format == 4:
 
264
            self.inventory_store = get_store('inventory-store')
 
265
            self.text_store = get_store('text-store')
 
266
            self.revision_store = get_store('revision-store')
 
267
        elif self._branch_format == 5:
 
268
            self.control_weaves = get_weave([])
 
269
            self.weave_store = get_weave('weaves')
 
270
            self.revision_store = get_store('revision-store', compressed=False)
200
271
 
201
272
    def __str__(self):
202
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
273
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
203
274
 
204
275
 
205
276
    __repr__ = __str__
207
278
 
208
279
    def __del__(self):
209
280
        if self._lock_mode or self._lock:
210
 
            from bzrlib.warnings import warn
 
281
            # XXX: This should show something every time, and be suitable for
 
282
            # headless operation and embedding
211
283
            warn("branch %r was not explicitly unlocked" % self)
212
284
            self._lock.unlock()
213
285
 
 
286
        # TODO: It might be best to do this somewhere else,
 
287
        # but it is nice for a Branch object to automatically
 
288
        # cache it's information.
 
289
        # Alternatively, we could have the Transport objects cache requests
 
290
        # See the earlier discussion about how major objects (like Branch)
 
291
        # should never expect their __del__ function to run.
 
292
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
293
            try:
 
294
                import shutil
 
295
                shutil.rmtree(self.cache_root)
 
296
            except:
 
297
                pass
 
298
            self.cache_root = None
 
299
 
 
300
    def _get_base(self):
 
301
        if self._transport:
 
302
            return self._transport.base
 
303
        return None
 
304
 
 
305
    base = property(_get_base)
 
306
 
 
307
 
214
308
    def lock_write(self):
 
309
        # TODO: Upgrade locking to support using a Transport,
 
310
        # and potentially a remote locking protocol
215
311
        if self._lock_mode:
216
312
            if self._lock_mode != 'w':
217
 
                from bzrlib.errors import LockError
218
313
                raise LockError("can't upgrade to a write lock from %r" %
219
314
                                self._lock_mode)
220
315
            self._lock_count += 1
221
316
        else:
222
 
            from bzrlib.lock import WriteLock
223
 
 
224
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
317
            self._lock = self._transport.lock_write(
 
318
                    self._rel_controlfilename('branch-lock'))
225
319
            self._lock_mode = 'w'
226
320
            self._lock_count = 1
227
321
 
232
326
                   "invalid lock mode %r" % self._lock_mode
233
327
            self._lock_count += 1
234
328
        else:
235
 
            from bzrlib.lock import ReadLock
236
 
 
237
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
329
            self._lock = self._transport.lock_read(
 
330
                    self._rel_controlfilename('branch-lock'))
238
331
            self._lock_mode = 'r'
239
332
            self._lock_count = 1
240
333
                        
241
334
    def unlock(self):
242
335
        if not self._lock_mode:
243
 
            from bzrlib.errors import LockError
244
336
            raise LockError('branch %r is not locked' % (self))
245
337
 
246
338
        if self._lock_count > 1:
252
344
 
253
345
    def abspath(self, name):
254
346
        """Return absolute filename for something in the branch"""
255
 
        return os.path.join(self.base, name)
 
347
        return self._transport.abspath(name)
256
348
 
257
349
    def relpath(self, path):
258
350
        """Return path relative to this branch of something inside it.
259
351
 
260
352
        Raises an error if path is not in this branch."""
261
 
        return _relpath(self.base, path)
 
353
        return self._transport.relpath(path)
 
354
 
 
355
 
 
356
    def _rel_controlfilename(self, file_or_path):
 
357
        if isinstance(file_or_path, basestring):
 
358
            file_or_path = [file_or_path]
 
359
        return [bzrlib.BZRDIR] + file_or_path
262
360
 
263
361
    def controlfilename(self, file_or_path):
264
362
        """Return location relative to branch."""
265
 
        if isinstance(file_or_path, basestring):
266
 
            file_or_path = [file_or_path]
267
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
363
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
268
364
 
269
365
 
270
366
    def controlfile(self, file_or_path, mode='r'):
278
374
        Controlfiles should almost never be opened in write mode but
279
375
        rather should be atomically copied and replaced using atomicfile.
280
376
        """
281
 
 
282
 
        fn = self.controlfilename(file_or_path)
283
 
 
284
 
        if mode == 'rb' or mode == 'wb':
285
 
            return file(fn, mode)
286
 
        elif mode == 'r' or mode == 'w':
287
 
            # open in binary mode anyhow so there's no newline translation;
288
 
            # codecs uses line buffering by default; don't want that.
289
 
            import codecs
290
 
            return codecs.open(fn, mode + 'b', 'utf-8',
291
 
                               buffering=60000)
 
377
        import codecs
 
378
 
 
379
        relpath = self._rel_controlfilename(file_or_path)
 
380
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
381
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
382
        if mode == 'rb': 
 
383
            return self._transport.get(relpath)
 
384
        elif mode == 'wb':
 
385
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
386
        elif mode == 'r':
 
387
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
388
        elif mode == 'w':
 
389
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
292
390
        else:
293
391
            raise BzrError("invalid controlfile mode %r" % mode)
294
392
 
 
393
    def put_controlfile(self, path, f, encode=True):
 
394
        """Write an entry as a controlfile.
 
395
 
 
396
        :param path: The path to put the file, relative to the .bzr control
 
397
                     directory
 
398
        :param f: A file-like or string object whose contents should be copied.
 
399
        :param encode:  If true, encode the contents as utf-8
 
400
        """
 
401
        self.put_controlfiles([(path, f)], encode=encode)
 
402
 
 
403
    def put_controlfiles(self, files, encode=True):
 
404
        """Write several entries as controlfiles.
 
405
 
 
406
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
407
                      underneath the bzr control directory
 
408
        :param encode:  If true, encode the contents as utf-8
 
409
        """
 
410
        import codecs
 
411
        ctrl_files = []
 
412
        for path, f in files:
 
413
            if encode:
 
414
                if isinstance(f, basestring):
 
415
                    f = f.encode('utf-8', 'replace')
 
416
                else:
 
417
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
418
            path = self._rel_controlfilename(path)
 
419
            ctrl_files.append((path, f))
 
420
        self._transport.put_multi(ctrl_files)
 
421
 
295
422
    def _make_control(self):
296
423
        from bzrlib.inventory import Inventory
 
424
        from bzrlib.weavefile import write_weave_v5
 
425
        from bzrlib.weave import Weave
297
426
        
298
 
        os.mkdir(self.controlfilename([]))
299
 
        self.controlfile('README', 'w').write(
300
 
            "This is a Bazaar-NG control directory.\n"
301
 
            "Do not change any files in this directory.\n")
302
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
303
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
304
 
            os.mkdir(self.controlfilename(d))
305
 
        for f in ('revision-history', 'merged-patches',
306
 
                  'pending-merged-patches', 'branch-name',
307
 
                  'branch-lock',
308
 
                  'pending-merges'):
309
 
            self.controlfile(f, 'w').write('')
310
 
        mutter('created control directory in ' + self.base)
311
 
 
 
427
        # Create an empty inventory
 
428
        sio = StringIO()
312
429
        # if we want per-tree root ids then this is the place to set
313
430
        # them; they're not needed for now and so ommitted for
314
431
        # simplicity.
315
 
        f = self.controlfile('inventory','w')
316
 
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
317
 
 
318
 
 
319
 
    def _check_format(self):
 
432
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
433
        empty_inv = sio.getvalue()
 
434
        sio = StringIO()
 
435
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
436
        empty_weave = sio.getvalue()
 
437
 
 
438
        dirs = [[], 'revision-store', 'weaves']
 
439
        files = [('README', 
 
440
            "This is a Bazaar-NG control directory.\n"
 
441
            "Do not change any files in this directory.\n"),
 
442
            ('branch-format', BZR_BRANCH_FORMAT_5),
 
443
            ('revision-history', ''),
 
444
            ('branch-name', ''),
 
445
            ('branch-lock', ''),
 
446
            ('pending-merges', ''),
 
447
            ('inventory', empty_inv),
 
448
            ('inventory.weave', empty_weave),
 
449
            ('ancestry.weave', empty_weave)
 
450
        ]
 
451
        cfn = self._rel_controlfilename
 
452
        self._transport.mkdir_multi([cfn(d) for d in dirs])
 
453
        self.put_controlfiles(files)
 
454
        mutter('created control directory in ' + self._transport.base)
 
455
 
 
456
    def _check_format(self, relax_version_check):
320
457
        """Check this branch format is supported.
321
458
 
322
 
        The current tool only supports the current unstable format.
 
459
        The format level is stored, as an integer, in
 
460
        self._branch_format for code that needs to check it later.
323
461
 
324
462
        In the future, we might need different in-memory Branch
325
463
        classes to support downlevel branches.  But not yet.
326
464
        """
327
 
        # This ignores newlines so that we can open branches created
328
 
        # on Windows from Linux and so on.  I think it might be better
329
 
        # to always make all internal files in unix format.
330
 
        fmt = self.controlfile('branch-format', 'r').read()
331
 
        fmt = fmt.replace('\r\n', '\n')
332
 
        if fmt != BZR_BRANCH_FORMAT:
 
465
        try:
 
466
            fmt = self.controlfile('branch-format', 'r').read()
 
467
        except NoSuchFile:
 
468
            raise NotBranchError(self.base)
 
469
        mutter("got branch format %r", fmt)
 
470
        if fmt == BZR_BRANCH_FORMAT_5:
 
471
            self._branch_format = 5
 
472
        elif fmt == BZR_BRANCH_FORMAT_4:
 
473
            self._branch_format = 4
 
474
 
 
475
        if (not relax_version_check
 
476
            and self._branch_format != 5):
333
477
            raise BzrError('sorry, branch format %r not supported' % fmt,
334
478
                           ['use a different bzr version',
335
 
                            'or remove the .bzr directory and "bzr init" again'])
 
479
                            'or remove the .bzr directory'
 
480
                            ' and "bzr init" again'])
336
481
 
337
482
    def get_root_id(self):
338
483
        """Return the id of this branches root"""
353
498
 
354
499
    def read_working_inventory(self):
355
500
        """Read the working inventory."""
356
 
        from bzrlib.inventory import Inventory
357
501
        self.lock_read()
358
502
        try:
359
503
            # ElementTree does its own conversion from UTF-8, so open in
360
504
            # binary.
361
505
            f = self.controlfile('inventory', 'rb')
362
 
            return bzrlib.xml.serializer_v4.read_inventory(f)
 
506
            return bzrlib.xml5.serializer_v5.read_inventory(f)
363
507
        finally:
364
508
            self.unlock()
365
509
            
370
514
        That is to say, the inventory describing changes underway, that
371
515
        will be committed to the next revision.
372
516
        """
373
 
        from bzrlib.atomicfile import AtomicFile
374
 
        
 
517
        from cStringIO import StringIO
375
518
        self.lock_write()
376
519
        try:
377
 
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
378
 
            try:
379
 
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
380
 
                f.commit()
381
 
            finally:
382
 
                f.close()
 
520
            sio = StringIO()
 
521
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
522
            sio.seek(0)
 
523
            # Transport handles atomicity
 
524
            self.put_controlfile('inventory', sio)
383
525
        finally:
384
526
            self.unlock()
385
527
        
386
528
        mutter('wrote working inventory')
387
529
            
388
 
 
389
530
    inventory = property(read_working_inventory, _write_inventory, None,
390
531
                         """Inventory for the working copy.""")
391
532
 
392
 
 
393
533
    def add(self, files, ids=None):
394
534
        """Make files versioned.
395
535
 
443
583
                    kind = file_kind(fullpath)
444
584
                except OSError:
445
585
                    # maybe something better?
446
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
586
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
447
587
 
448
 
                if kind != 'file' and kind != 'directory':
449
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
588
                if not InventoryEntry.versionable_kind(kind):
 
589
                    raise BzrError('cannot add: not a versionable file ('
 
590
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
450
591
 
451
592
                if file_id is None:
452
593
                    file_id = gen_file_id(f)
463
604
        """Print `file` to stdout."""
464
605
        self.lock_read()
465
606
        try:
466
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
607
            tree = self.revision_tree(self.get_rev_id(revno))
467
608
            # use inventory as it was in that revision
468
609
            file_id = tree.inventory.path2id(file)
469
610
            if not file_id:
517
658
        finally:
518
659
            self.unlock()
519
660
 
520
 
 
521
661
    # FIXME: this doesn't need to be a branch method
522
662
    def set_inventory(self, new_inventory_list):
523
663
        from bzrlib.inventory import Inventory, InventoryEntry
526
666
            name = os.path.basename(path)
527
667
            if name == "":
528
668
                continue
529
 
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
669
            # fixme, there should be a factory function inv,add_?? 
 
670
            if kind == 'directory':
 
671
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
 
672
            elif kind == 'file':
 
673
                inv.add(inventory.InventoryFile(file_id, name, parent))
 
674
            elif kind == 'symlink':
 
675
                inv.add(inventory.InventoryLink(file_id, name, parent))
 
676
            else:
 
677
                raise BzrError("unknown kind %r" % kind)
530
678
        self._write_inventory(inv)
531
679
 
532
 
 
533
680
    def unknowns(self):
534
681
        """Return all unknown files.
535
682
 
550
697
 
551
698
 
552
699
    def append_revision(self, *revision_ids):
553
 
        from bzrlib.atomicfile import AtomicFile
554
 
 
555
700
        for revision_id in revision_ids:
556
701
            mutter("add {%s} to revision-history" % revision_id)
557
 
 
558
 
        rev_history = self.revision_history()
559
 
        rev_history.extend(revision_ids)
560
 
 
561
 
        f = AtomicFile(self.controlfilename('revision-history'))
 
702
        self.lock_write()
562
703
        try:
563
 
            for rev_id in rev_history:
564
 
                print >>f, rev_id
565
 
            f.commit()
 
704
            rev_history = self.revision_history()
 
705
            rev_history.extend(revision_ids)
 
706
            self.put_controlfile('revision-history', '\n'.join(rev_history))
566
707
        finally:
567
 
            f.close()
568
 
 
 
708
            self.unlock()
 
709
 
 
710
    def has_revision(self, revision_id):
 
711
        """True if this branch has a copy of the revision.
 
712
 
 
713
        This does not necessarily imply the revision is merge
 
714
        or on the mainline."""
 
715
        return (revision_id is None
 
716
                or revision_id in self.revision_store)
569
717
 
570
718
    def get_revision_xml_file(self, revision_id):
571
719
        """Return XML file object for revision object."""
581
729
        finally:
582
730
            self.unlock()
583
731
 
584
 
 
585
732
    #deprecated
586
733
    get_revision_xml = get_revision_xml_file
587
734
 
 
735
    def get_revision_xml(self, revision_id):
 
736
        return self.get_revision_xml_file(revision_id).read()
 
737
 
588
738
 
589
739
    def get_revision(self, revision_id):
590
740
        """Return the Revision object for a named revision"""
591
741
        xml_file = self.get_revision_xml_file(revision_id)
592
742
 
593
743
        try:
594
 
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
744
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
595
745
        except SyntaxError, e:
596
746
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
597
747
                                         [revision_id,
600
750
        assert r.revision_id == revision_id
601
751
        return r
602
752
 
603
 
 
604
753
    def get_revision_delta(self, revno):
605
754
        """Return the delta for one revision.
606
755
 
622
771
 
623
772
        return compare_trees(old_tree, new_tree)
624
773
 
625
 
        
626
 
 
627
774
    def get_revision_sha1(self, revision_id):
628
775
        """Hash the stored value of a revision, and return it."""
629
776
        # In the future, revision entries will be signed. At that
632
779
        # the revision, (add signatures/remove signatures) and still
633
780
        # have all hash pointers stay consistent.
634
781
        # But for now, just hash the contents.
635
 
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
636
 
 
637
 
 
638
 
    def get_inventory(self, inventory_id):
639
 
        """Get Inventory object by hash.
640
 
 
641
 
        TODO: Perhaps for this and similar methods, take a revision
642
 
               parameter which can be either an integer revno or a
643
 
               string hash."""
644
 
        from bzrlib.inventory import Inventory
645
 
 
646
 
        f = self.get_inventory_xml_file(inventory_id)
647
 
        return bzrlib.xml.serializer_v4.read_inventory(f)
648
 
 
649
 
 
650
 
    def get_inventory_xml(self, inventory_id):
 
782
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
783
 
 
784
    def get_ancestry(self, revision_id):
 
785
        """Return a list of revision-ids integrated by a revision.
 
786
        
 
787
        This currently returns a list, but the ordering is not guaranteed:
 
788
        treat it as a set.
 
789
        """
 
790
        if revision_id is None:
 
791
            return [None]
 
792
        w = self.control_weaves.get_weave('inventory')
 
793
        return [None] + map(w.idx_to_name,
 
794
                            w.inclusions([w.lookup(revision_id)]))
 
795
 
 
796
    def get_inventory_weave(self):
 
797
        return self.control_weaves.get_weave('inventory')
 
798
 
 
799
    def get_inventory(self, revision_id):
 
800
        """Get Inventory object by hash."""
 
801
        xml = self.get_inventory_xml(revision_id)
 
802
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
803
 
 
804
    def get_inventory_xml(self, revision_id):
651
805
        """Get inventory XML as a file object."""
652
 
        return self.inventory_store[inventory_id]
653
 
 
654
 
    get_inventory_xml_file = get_inventory_xml
655
 
            
656
 
 
657
 
    def get_inventory_sha1(self, inventory_id):
 
806
        try:
 
807
            assert isinstance(revision_id, basestring), type(revision_id)
 
808
            iw = self.get_inventory_weave()
 
809
            return iw.get_text(iw.lookup(revision_id))
 
810
        except IndexError:
 
811
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
812
 
 
813
    def get_inventory_sha1(self, revision_id):
658
814
        """Return the sha1 hash of the inventory entry
659
815
        """
660
 
        return sha_file(self.get_inventory_xml(inventory_id))
661
 
 
 
816
        return self.get_revision(revision_id).inventory_sha1
662
817
 
663
818
    def get_revision_inventory(self, revision_id):
664
819
        """Return inventory of a past revision."""
665
 
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
820
        # TODO: Unify this with get_inventory()
 
821
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
666
822
        # must be the same as its revision, so this is trivial.
667
823
        if revision_id == None:
668
 
            from bzrlib.inventory import Inventory
669
824
            return Inventory(self.get_root_id())
670
825
        else:
671
826
            return self.get_inventory(revision_id)
672
827
 
673
 
 
674
828
    def revision_history(self):
675
 
        """Return sequence of revision hashes on to this branch.
676
 
 
677
 
        >>> ScratchBranch().revision_history()
678
 
        []
679
 
        """
 
829
        """Return sequence of revision hashes on to this branch."""
680
830
        self.lock_read()
681
831
        try:
682
832
            return [l.rstrip('\r\n') for l in
684
834
        finally:
685
835
            self.unlock()
686
836
 
687
 
 
688
837
    def common_ancestor(self, other, self_revno=None, other_revno=None):
689
838
        """
690
839
        >>> from bzrlib.commit import commit
739
888
        return len(self.revision_history())
740
889
 
741
890
 
742
 
    def last_patch(self):
 
891
    def last_revision(self):
743
892
        """Return last patch hash, or None if no history.
744
893
        """
745
894
        ph = self.revision_history()
750
899
 
751
900
 
752
901
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
753
 
        """
 
902
        """Return a list of new revisions that would perfectly fit.
 
903
        
754
904
        If self and other have not diverged, return a list of the revisions
755
905
        present in other, but missing from self.
756
906
 
776
926
        Traceback (most recent call last):
777
927
        DivergedBranches: These branches have diverged.
778
928
        """
 
929
        # FIXME: If the branches have diverged, but the latest
 
930
        # revision in this branch is completely merged into the other,
 
931
        # then we should still be able to pull.
779
932
        self_history = self.revision_history()
780
933
        self_len = len(self_history)
781
934
        other_history = other.revision_history()
787
940
 
788
941
        if stop_revision is None:
789
942
            stop_revision = other_len
790
 
        elif stop_revision > other_len:
791
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
792
 
        
 
943
        else:
 
944
            assert isinstance(stop_revision, int)
 
945
            if stop_revision > other_len:
 
946
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
793
947
        return other_history[self_len:stop_revision]
794
948
 
795
 
 
796
949
    def update_revisions(self, other, stop_revision=None):
797
 
        """Pull in all new revisions from other branch.
798
 
        """
 
950
        """Pull in new perfect-fit revisions."""
799
951
        from bzrlib.fetch import greedy_fetch
800
952
        from bzrlib.revision import get_intervening_revisions
801
 
 
802
 
        pb = bzrlib.ui.ui_factory.progress_bar()
803
 
        pb.update('comparing histories')
804
953
        if stop_revision is None:
805
 
            other_revision = other.last_patch()
806
 
        else:
807
 
            other_revision = other.lookup_revision(stop_revision)
808
 
        count = greedy_fetch(self, other, other_revision, pb)[0]
809
 
        try:
810
 
            revision_ids = self.missing_revisions(other, stop_revision)
811
 
        except DivergedBranches, e:
812
 
            try:
813
 
                revision_ids = get_intervening_revisions(self.last_patch(), 
814
 
                                                         other_revision, self)
815
 
                assert self.last_patch() not in revision_ids
816
 
            except bzrlib.errors.NotAncestor:
817
 
                raise e
818
 
 
819
 
        self.append_revision(*revision_ids)
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 = []
827
 
            for rev_id in revision_ids:
828
 
                try:
829
 
                    revision = other.get_revision(rev_id).inventory_id
830
 
                    inventory_ids.append(revision)
831
 
                except bzrlib.errors.NoSuchRevision:
832
 
                    pass
833
 
            other.inventory_store.prefetch(inventory_ids)
834
 
 
835
 
        if pb is None:
836
 
            pb = bzrlib.ui.ui_factory.progress_bar()
837
 
                
838
 
        revisions = []
839
 
        needed_texts = set()
840
 
        i = 0
841
 
 
842
 
        failures = set()
843
 
        for i, rev_id in enumerate(revision_ids):
844
 
            pb.update('fetching revision', i+1, len(revision_ids))
845
 
            try:
846
 
                rev = other.get_revision(rev_id)
847
 
            except bzrlib.errors.NoSuchRevision:
848
 
                failures.add(rev_id)
849
 
                continue
850
 
 
851
 
            revisions.append(rev)
852
 
            inv = other.get_inventory(str(rev.inventory_id))
853
 
            for key, entry in inv.iter_entries():
854
 
                if entry.text_id is None:
855
 
                    continue
856
 
                if entry.text_id not in self.text_store:
857
 
                    needed_texts.add(entry.text_id)
858
 
 
859
 
        pb.clear()
860
 
                    
861
 
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
862
 
                                                    needed_texts)
863
 
        #print "Added %d texts." % count 
864
 
        inventory_ids = [ f.inventory_id for f in revisions ]
865
 
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
866
 
                                                         inventory_ids)
867
 
        #print "Added %d inventories." % count 
868
 
        revision_ids = [ f.revision_id for f in revisions]
869
 
 
870
 
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
871
 
                                                          revision_ids,
872
 
                                                          permit_failure=True)
873
 
        assert len(cp_fail) == 0 
874
 
        return count, failures
875
 
       
 
954
            stop_revision = other.last_revision()
 
955
        greedy_fetch(to_branch=self, from_branch=other,
 
956
                     revision=stop_revision)
 
957
        pullable_revs = self.missing_revisions(
 
958
            other, other.revision_id_to_revno(stop_revision))
 
959
        if pullable_revs:
 
960
            greedy_fetch(to_branch=self,
 
961
                         from_branch=other,
 
962
                         revision=pullable_revs[-1])
 
963
            self.append_revision(*pullable_revs)
 
964
    
876
965
 
877
966
    def commit(self, *args, **kw):
878
 
        from bzrlib.commit import commit
879
 
        commit(self, *args, **kw)
880
 
        
881
 
 
882
 
    def lookup_revision(self, revision):
883
 
        """Return the revision identifier for a given revision information."""
884
 
        revno, info = self._get_revision_info(revision)
885
 
        return info
886
 
 
887
 
 
 
967
        from bzrlib.commit import Commit
 
968
        Commit().commit(self, *args, **kw)
 
969
    
888
970
    def revision_id_to_revno(self, revision_id):
889
971
        """Given a revision id, return its revno"""
 
972
        if revision_id is None:
 
973
            return 0
890
974
        history = self.revision_history()
891
975
        try:
892
976
            return history.index(revision_id) + 1
893
977
        except ValueError:
894
978
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
895
979
 
896
 
 
897
 
    def get_revision_info(self, revision):
898
 
        """Return (revno, revision id) for revision identifier.
899
 
 
900
 
        revision can be an integer, in which case it is assumed to be revno (though
901
 
            this will translate negative values into positive ones)
902
 
        revision can also be a string, in which case it is parsed for something like
903
 
            'date:' or 'revid:' etc.
904
 
        """
905
 
        revno, rev_id = self._get_revision_info(revision)
906
 
        if revno is None:
907
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
908
 
        return revno, rev_id
909
 
 
910
980
    def get_rev_id(self, revno, history=None):
911
981
        """Find the revision id of the specified revno."""
912
982
        if revno == 0:
917
987
            raise bzrlib.errors.NoSuchRevision(self, revno)
918
988
        return history[revno - 1]
919
989
 
920
 
    def _get_revision_info(self, revision):
921
 
        """Return (revno, revision id) for revision specifier.
922
 
 
923
 
        revision can be an integer, in which case it is assumed to be revno
924
 
        (though this will translate negative values into positive ones)
925
 
        revision can also be a string, in which case it is parsed for something
926
 
        like 'date:' or 'revid:' etc.
927
 
 
928
 
        A revid is always returned.  If it is None, the specifier referred to
929
 
        the null revision.  If the revid does not occur in the revision
930
 
        history, revno will be None.
931
 
        """
932
 
        
933
 
        if revision is None:
934
 
            return 0, None
935
 
        revno = None
936
 
        try:# Convert to int if possible
937
 
            revision = int(revision)
938
 
        except ValueError:
939
 
            pass
940
 
        revs = self.revision_history()
941
 
        if isinstance(revision, int):
942
 
            if revision < 0:
943
 
                revno = len(revs) + revision + 1
944
 
            else:
945
 
                revno = revision
946
 
            rev_id = self.get_rev_id(revno, revs)
947
 
        elif isinstance(revision, basestring):
948
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
949
 
                if revision.startswith(prefix):
950
 
                    result = func(self, revs, revision)
951
 
                    if len(result) > 1:
952
 
                        revno, rev_id = result
953
 
                    else:
954
 
                        revno = result[0]
955
 
                        rev_id = self.get_rev_id(revno, revs)
956
 
                    break
957
 
            else:
958
 
                raise BzrError('No namespace registered for string: %r' %
959
 
                               revision)
960
 
        else:
961
 
            raise TypeError('Unhandled revision type %s' % revision)
962
 
 
963
 
        if revno is None:
964
 
            if rev_id is None:
965
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
966
 
        return revno, rev_id
967
 
 
968
 
    def _namespace_revno(self, revs, revision):
969
 
        """Lookup a revision by revision number"""
970
 
        assert revision.startswith('revno:')
971
 
        try:
972
 
            return (int(revision[6:]),)
973
 
        except ValueError:
974
 
            return None
975
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
976
 
 
977
 
    def _namespace_revid(self, revs, revision):
978
 
        assert revision.startswith('revid:')
979
 
        rev_id = revision[len('revid:'):]
980
 
        try:
981
 
            return revs.index(rev_id) + 1, rev_id
982
 
        except ValueError:
983
 
            return None, rev_id
984
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
985
 
 
986
 
    def _namespace_last(self, revs, revision):
987
 
        assert revision.startswith('last:')
988
 
        try:
989
 
            offset = int(revision[5:])
990
 
        except ValueError:
991
 
            return (None,)
992
 
        else:
993
 
            if offset <= 0:
994
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
995
 
            return (len(revs) - offset + 1,)
996
 
    REVISION_NAMESPACES['last:'] = _namespace_last
997
 
 
998
 
    def _namespace_tag(self, revs, revision):
999
 
        assert revision.startswith('tag:')
1000
 
        raise BzrError('tag: namespace registered, but not implemented.')
1001
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
1002
 
 
1003
 
    def _namespace_date(self, revs, revision):
1004
 
        assert revision.startswith('date:')
1005
 
        import datetime
1006
 
        # Spec for date revisions:
1007
 
        #   date:value
1008
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
1009
 
        #   it can also start with a '+/-/='. '+' says match the first
1010
 
        #   entry after the given date. '-' is match the first entry before the date
1011
 
        #   '=' is match the first entry after, but still on the given date.
1012
 
        #
1013
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
1014
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
1015
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
1016
 
        #       May 13th, 2005 at 0:00
1017
 
        #
1018
 
        #   So the proper way of saying 'give me all entries for today' is:
1019
 
        #       -r {date:+today}:{date:-tomorrow}
1020
 
        #   The default is '=' when not supplied
1021
 
        val = revision[5:]
1022
 
        match_style = '='
1023
 
        if val[:1] in ('+', '-', '='):
1024
 
            match_style = val[:1]
1025
 
            val = val[1:]
1026
 
 
1027
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
1028
 
        if val.lower() == 'yesterday':
1029
 
            dt = today - datetime.timedelta(days=1)
1030
 
        elif val.lower() == 'today':
1031
 
            dt = today
1032
 
        elif val.lower() == 'tomorrow':
1033
 
            dt = today + datetime.timedelta(days=1)
1034
 
        else:
1035
 
            import re
1036
 
            # This should be done outside the function to avoid recompiling it.
1037
 
            _date_re = re.compile(
1038
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
1039
 
                    r'(,|T)?\s*'
1040
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
1041
 
                )
1042
 
            m = _date_re.match(val)
1043
 
            if not m or (not m.group('date') and not m.group('time')):
1044
 
                raise BzrError('Invalid revision date %r' % revision)
1045
 
 
1046
 
            if m.group('date'):
1047
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1048
 
            else:
1049
 
                year, month, day = today.year, today.month, today.day
1050
 
            if m.group('time'):
1051
 
                hour = int(m.group('hour'))
1052
 
                minute = int(m.group('minute'))
1053
 
                if m.group('second'):
1054
 
                    second = int(m.group('second'))
1055
 
                else:
1056
 
                    second = 0
1057
 
            else:
1058
 
                hour, minute, second = 0,0,0
1059
 
 
1060
 
            dt = datetime.datetime(year=year, month=month, day=day,
1061
 
                    hour=hour, minute=minute, second=second)
1062
 
        first = dt
1063
 
        last = None
1064
 
        reversed = False
1065
 
        if match_style == '-':
1066
 
            reversed = True
1067
 
        elif match_style == '=':
1068
 
            last = dt + datetime.timedelta(days=1)
1069
 
 
1070
 
        if reversed:
1071
 
            for i in range(len(revs)-1, -1, -1):
1072
 
                r = self.get_revision(revs[i])
1073
 
                # TODO: Handle timezone.
1074
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1075
 
                if first >= dt and (last is None or dt >= last):
1076
 
                    return (i+1,)
1077
 
        else:
1078
 
            for i in range(len(revs)):
1079
 
                r = self.get_revision(revs[i])
1080
 
                # TODO: Handle timezone.
1081
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1082
 
                if first <= dt and (last is None or dt <= last):
1083
 
                    return (i+1,)
1084
 
    REVISION_NAMESPACES['date:'] = _namespace_date
1085
 
 
1086
 
 
1087
 
    def _namespace_ancestor(self, revs, revision):
1088
 
        from revision import common_ancestor, MultipleRevisionSources
1089
 
        other_branch = find_branch(_trim_namespace('ancestor', revision))
1090
 
        revision_a = self.last_patch()
1091
 
        revision_b = other_branch.last_patch()
1092
 
        for r, b in ((revision_a, self), (revision_b, other_branch)):
1093
 
            if r is None:
1094
 
                raise bzrlib.errors.NoCommits(b)
1095
 
        revision_source = MultipleRevisionSources(self, other_branch)
1096
 
        result = common_ancestor(revision_a, revision_b, revision_source)
1097
 
        try:
1098
 
            revno = self.revision_id_to_revno(result)
1099
 
        except bzrlib.errors.NoSuchRevision:
1100
 
            revno = None
1101
 
        return revno,result
1102
 
        
1103
 
 
1104
 
    REVISION_NAMESPACES['ancestor:'] = _namespace_ancestor
1105
 
 
1106
990
    def revision_tree(self, revision_id):
1107
991
        """Return Tree for a revision on this branch.
1108
992
 
1114
998
            return EmptyTree()
1115
999
        else:
1116
1000
            inv = self.get_revision_inventory(revision_id)
1117
 
            return RevisionTree(self.text_store, inv)
 
1001
            return RevisionTree(self.weave_store, inv, revision_id)
1118
1002
 
1119
1003
 
1120
1004
    def working_tree(self):
1121
1005
        """Return a `Tree` for the working copy."""
1122
1006
        from bzrlib.workingtree import WorkingTree
1123
 
        return WorkingTree(self.base, self.read_working_inventory())
 
1007
        # TODO: In the future, WorkingTree should utilize Transport
 
1008
        # RobertCollins 20051003 - I don't think it should - working trees are
 
1009
        # much more complex to keep consistent than our careful .bzr subset.
 
1010
        # instead, we should say that working trees are local only, and optimise
 
1011
        # for that.
 
1012
        return WorkingTree(self._transport.base, self.read_working_inventory())
1124
1013
 
1125
1014
 
1126
1015
    def basis_tree(self):
1128
1017
 
1129
1018
        If there are no revisions yet, return an `EmptyTree`.
1130
1019
        """
1131
 
        r = self.last_patch()
1132
 
        if r == None:
1133
 
            return EmptyTree()
1134
 
        else:
1135
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1136
 
 
 
1020
        return self.revision_tree(self.last_revision())
1137
1021
 
1138
1022
 
1139
1023
    def rename_one(self, from_rel, to_rel):
1174
1058
            from_abs = self.abspath(from_rel)
1175
1059
            to_abs = self.abspath(to_rel)
1176
1060
            try:
1177
 
                os.rename(from_abs, to_abs)
 
1061
                rename(from_abs, to_abs)
1178
1062
            except OSError, e:
1179
1063
                raise BzrError("failed to rename %r to %r: %s"
1180
1064
                        % (from_abs, to_abs, e[1]),
1243
1127
                result.append((f, dest_path))
1244
1128
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1245
1129
                try:
1246
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1130
                    rename(self.abspath(f), self.abspath(dest_path))
1247
1131
                except OSError, e:
1248
1132
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1249
1133
                            ["rename rolled back"])
1305
1189
        These are revisions that have been merged into the working
1306
1190
        directory but not yet committed.
1307
1191
        """
1308
 
        cfn = self.controlfilename('pending-merges')
1309
 
        if not os.path.exists(cfn):
 
1192
        cfn = self._rel_controlfilename('pending-merges')
 
1193
        if not self._transport.has(cfn):
1310
1194
            return []
1311
1195
        p = []
1312
1196
        for l in self.controlfile('pending-merges', 'r').readlines():
1314
1198
        return p
1315
1199
 
1316
1200
 
1317
 
    def add_pending_merge(self, revision_id):
1318
 
        from bzrlib.revision import validate_revision_id
1319
 
 
1320
 
        validate_revision_id(revision_id)
1321
 
 
 
1201
    def add_pending_merge(self, *revision_ids):
 
1202
        # TODO: Perhaps should check at this point that the
 
1203
        # history of the revision is actually present?
1322
1204
        p = self.pending_merges()
1323
 
        if revision_id in p:
1324
 
            return
1325
 
        p.append(revision_id)
1326
 
        self.set_pending_merges(p)
1327
 
 
 
1205
        updated = False
 
1206
        for rev_id in revision_ids:
 
1207
            if rev_id in p:
 
1208
                continue
 
1209
            p.append(rev_id)
 
1210
            updated = True
 
1211
        if updated:
 
1212
            self.set_pending_merges(p)
1328
1213
 
1329
1214
    def set_pending_merges(self, rev_list):
1330
 
        from bzrlib.atomicfile import AtomicFile
1331
1215
        self.lock_write()
1332
1216
        try:
1333
 
            f = AtomicFile(self.controlfilename('pending-merges'))
1334
 
            try:
1335
 
                for l in rev_list:
1336
 
                    print >>f, l
1337
 
                f.commit()
1338
 
            finally:
1339
 
                f.close()
 
1217
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1340
1218
        finally:
1341
1219
            self.unlock()
1342
1220
 
1390
1268
            raise InvalidRevisionNumber(revno)
1391
1269
        
1392
1270
        
1393
 
 
1394
 
 
1395
 
class ScratchBranch(Branch):
 
1271
        
 
1272
 
 
1273
 
 
1274
class ScratchBranch(_Branch):
1396
1275
    """Special test class: a branch that cleans up after itself.
1397
1276
 
1398
1277
    >>> b = ScratchBranch()
1415
1294
        if base is None:
1416
1295
            base = mkdtemp()
1417
1296
            init = True
1418
 
        Branch.__init__(self, base, init=init)
 
1297
        if isinstance(base, basestring):
 
1298
            base = get_transport(base)
 
1299
        _Branch.__init__(self, base, init=init)
1419
1300
        for d in dirs:
1420
 
            os.mkdir(self.abspath(d))
 
1301
            self._transport.mkdir(d)
1421
1302
            
1422
1303
        for f in files:
1423
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1304
            self._transport.put(f, 'content of %s' % f)
1424
1305
 
1425
1306
 
1426
1307
    def clone(self):
1427
1308
        """
1428
1309
        >>> orig = ScratchBranch(files=["file1", "file2"])
1429
1310
        >>> clone = orig.clone()
1430
 
        >>> os.path.samefile(orig.base, clone.base)
 
1311
        >>> if os.name != 'nt':
 
1312
        ...   os.path.samefile(orig.base, clone.base)
 
1313
        ... else:
 
1314
        ...   orig.base == clone.base
 
1315
        ...
1431
1316
        False
1432
1317
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1433
1318
        True
1439
1324
        copytree(self.base, base, symlinks=True)
1440
1325
        return ScratchBranch(base=base)
1441
1326
 
1442
 
 
1443
 
        
1444
1327
    def __del__(self):
1445
1328
        self.destroy()
1446
1329
 
1459
1342
                for name in files:
1460
1343
                    os.chmod(os.path.join(root, name), 0700)
1461
1344
            rmtree(self.base)
1462
 
        self.base = None
 
1345
        self._transport = None
1463
1346
 
1464
1347
    
1465
1348
 
1516
1399
    return gen_file_id('TREE_ROOT')
1517
1400
 
1518
1401
 
1519
 
def copy_branch(branch_from, to_location, revision=None):
1520
 
    """Copy branch_from into the existing directory to_location.
1521
 
 
1522
 
    revision
1523
 
        If not None, only revisions up to this point will be copied.
1524
 
        The head of the new branch will be that revision.
1525
 
 
1526
 
    to_location
1527
 
        The name of a local directory that exists but is empty.
1528
 
    """
1529
 
    from bzrlib.merge import merge
1530
 
 
1531
 
    assert isinstance(branch_from, Branch)
1532
 
    assert isinstance(to_location, basestring)
1533
 
    
1534
 
    br_to = Branch(to_location, init=True)
1535
 
    br_to.set_root_id(branch_from.get_root_id())
1536
 
    if revision is None:
1537
 
        revno = branch_from.revno()
1538
 
    else:
1539
 
        revno, rev_id = branch_from.get_revision_info(revision)
1540
 
    br_to.update_revisions(branch_from, stop_revision=revno)
1541
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1542
 
          check_clean=False, ignore_zero=True)
1543
 
    br_to.set_parent(branch_from.base)
1544
 
    return br_to
1545
 
 
1546
 
def _trim_namespace(namespace, spec):
1547
 
    full_namespace = namespace + ':'
1548
 
    assert spec.startswith(full_namespace)
1549
 
    return spec[len(full_namespace):]