~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-10-04 02:43:06 UTC
  • mto: (1393.1.30)
  • mto: This revision was merged to the branch mainline in revision 1400.
  • Revision ID: robertc@robertcollins.net-20051004024306-219d418df26a8f00
add new test script

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
 
from bzrlib.revision import Revision
 
37
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
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
        return _Branch(t)
 
138
 
 
139
    @staticmethod
 
140
    def open_containing(url):
 
141
        """Open an existing branch which contains url.
 
142
        
 
143
        This probes for a branch at url, and searches upwards from there.
 
144
        """
 
145
        t = get_transport(url)
 
146
        t = find_branch_root(t)
 
147
        return _Branch(t)
 
148
 
 
149
    @staticmethod
 
150
    def initialize(base):
 
151
        """Create a new branch, rooted at 'base' (url)"""
 
152
        t = get_transport(base)
 
153
        return _Branch(t, init=True)
 
154
 
 
155
    def setup_caching(self, cache_root):
 
156
        """Subclasses that care about caching should override this, and set
 
157
        up cached stores located under cache_root.
 
158
        """
 
159
 
 
160
 
 
161
class _Branch(Branch):
 
162
    """A branch stored in the actual filesystem.
 
163
 
 
164
    Note that it's "local" in the context of the filesystem; it doesn't
 
165
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
166
    it's writable, and can be accessed via the normal filesystem API.
144
167
 
145
168
    _lock_mode
146
169
        None, or 'r' or 'w'
152
175
    _lock
153
176
        Lock object from bzrlib.lock.
154
177
    """
155
 
    base = None
 
178
    # We actually expect this class to be somewhat short-lived; part of its
 
179
    # purpose is to try to isolate what bits of the branch logic are tied to
 
180
    # filesystem access, so that in a later step, we can extricate them to
 
181
    # a separarte ("storage") class.
156
182
    _lock_mode = None
157
183
    _lock_count = None
158
184
    _lock = None
 
185
    _inventory_weave = None
159
186
    
160
187
    # Map some sort of prefix into a namespace
161
188
    # stuff like "revno:10", "revid:", etc.
162
189
    # This should match a prefix with a function which accepts
163
190
    REVISION_NAMESPACES = {}
164
191
 
165
 
    def __init__(self, base, init=False, find_root=True):
 
192
    def push_stores(self, branch_to):
 
193
        """Copy the content of this branches store to branch_to."""
 
194
        if (self._branch_format != branch_to._branch_format
 
195
            or self._branch_format != 4):
 
196
            from bzrlib.fetch import greedy_fetch
 
197
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
 
198
                   self, self._branch_format, branch_to, branch_to._branch_format)
 
199
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
200
                         revision=self.last_revision())
 
201
            return
 
202
 
 
203
        store_pairs = ((self.text_store,      branch_to.text_store),
 
204
                       (self.inventory_store, branch_to.inventory_store),
 
205
                       (self.revision_store,  branch_to.revision_store))
 
206
        try:
 
207
            for from_store, to_store in store_pairs: 
 
208
                copy_all(from_store, to_store)
 
209
        except UnlistableStore:
 
210
            raise UnlistableBranch(from_store)
 
211
 
 
212
    def __init__(self, transport, init=False,
 
213
                 relax_version_check=False):
166
214
        """Create new branch object at a particular location.
167
215
 
168
 
        base -- Base directory for the branch. May be a file:// url.
 
216
        transport -- A Transport object, defining how to access files.
 
217
                (If a string, transport.transport() will be used to
 
218
                create a Transport object)
169
219
        
170
220
        init -- If True, create new control files in a previously
171
221
             unversioned directory.  If False, the branch must already
172
222
             be versioned.
173
223
 
174
 
        find_root -- If true and init is false, find the root of the
175
 
             existing branch containing base.
 
224
        relax_version_check -- If true, the usual check for the branch
 
225
            version is not applied.  This is intended only for
 
226
            upgrade/recovery type use; it's not guaranteed that
 
227
            all operations will work on old format branches.
176
228
 
177
229
        In the test suite, creation of new trees is tested using the
178
230
        `ScratchBranch` class.
179
231
        """
180
 
        from bzrlib.store import ImmutableStore
 
232
        assert isinstance(transport, Transport), \
 
233
            "%r is not a Transport" % transport
 
234
        self._transport = transport
181
235
        if init:
182
 
            self.base = os.path.realpath(base)
183
236
            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
 
 
 
237
        self._check_format(relax_version_check)
 
238
 
 
239
        def get_store(name, compressed=True):
 
240
            relpath = self._rel_controlfilename(name)
 
241
            if compressed:
 
242
                store = CompressedTextStore(self._transport.clone(relpath))
 
243
            else:
 
244
                store = TextStore(self._transport.clone(relpath))
 
245
            if self._transport.should_cache():
 
246
                from meta_store import CachedStore
 
247
                cache_path = os.path.join(self.cache_root, name)
 
248
                os.mkdir(cache_path)
 
249
                store = CachedStore(store, cache_path)
 
250
            return store
 
251
        def get_weave(name):
 
252
            relpath = self._rel_controlfilename(name)
 
253
            ws = WeaveStore(self._transport.clone(relpath))
 
254
            if self._transport.should_cache():
 
255
                ws.enable_cache = True
 
256
            return ws
 
257
 
 
258
        if self._branch_format == 4:
 
259
            self.inventory_store = get_store('inventory-store')
 
260
            self.text_store = get_store('text-store')
 
261
            self.revision_store = get_store('revision-store')
 
262
        elif self._branch_format == 5:
 
263
            self.control_weaves = get_weave([])
 
264
            self.weave_store = get_weave('weaves')
 
265
            self.revision_store = get_store('revision-store', compressed=False)
200
266
 
201
267
    def __str__(self):
202
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
268
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
203
269
 
204
270
 
205
271
    __repr__ = __str__
207
273
 
208
274
    def __del__(self):
209
275
        if self._lock_mode or self._lock:
210
 
            from bzrlib.warnings import warn
 
276
            # XXX: This should show something every time, and be suitable for
 
277
            # headless operation and embedding
211
278
            warn("branch %r was not explicitly unlocked" % self)
212
279
            self._lock.unlock()
213
280
 
 
281
        # TODO: It might be best to do this somewhere else,
 
282
        # but it is nice for a Branch object to automatically
 
283
        # cache it's information.
 
284
        # Alternatively, we could have the Transport objects cache requests
 
285
        # See the earlier discussion about how major objects (like Branch)
 
286
        # should never expect their __del__ function to run.
 
287
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
288
            try:
 
289
                import shutil
 
290
                shutil.rmtree(self.cache_root)
 
291
            except:
 
292
                pass
 
293
            self.cache_root = None
 
294
 
 
295
    def _get_base(self):
 
296
        if self._transport:
 
297
            return self._transport.base
 
298
        return None
 
299
 
 
300
    base = property(_get_base)
 
301
 
 
302
 
214
303
    def lock_write(self):
 
304
        # TODO: Upgrade locking to support using a Transport,
 
305
        # and potentially a remote locking protocol
215
306
        if self._lock_mode:
216
307
            if self._lock_mode != 'w':
217
 
                from bzrlib.errors import LockError
218
308
                raise LockError("can't upgrade to a write lock from %r" %
219
309
                                self._lock_mode)
220
310
            self._lock_count += 1
221
311
        else:
222
 
            from bzrlib.lock import WriteLock
223
 
 
224
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
312
            self._lock = self._transport.lock_write(
 
313
                    self._rel_controlfilename('branch-lock'))
225
314
            self._lock_mode = 'w'
226
315
            self._lock_count = 1
227
316
 
232
321
                   "invalid lock mode %r" % self._lock_mode
233
322
            self._lock_count += 1
234
323
        else:
235
 
            from bzrlib.lock import ReadLock
236
 
 
237
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
324
            self._lock = self._transport.lock_read(
 
325
                    self._rel_controlfilename('branch-lock'))
238
326
            self._lock_mode = 'r'
239
327
            self._lock_count = 1
240
328
                        
241
329
    def unlock(self):
242
330
        if not self._lock_mode:
243
 
            from bzrlib.errors import LockError
244
331
            raise LockError('branch %r is not locked' % (self))
245
332
 
246
333
        if self._lock_count > 1:
252
339
 
253
340
    def abspath(self, name):
254
341
        """Return absolute filename for something in the branch"""
255
 
        return os.path.join(self.base, name)
 
342
        return self._transport.abspath(name)
256
343
 
257
344
    def relpath(self, path):
258
345
        """Return path relative to this branch of something inside it.
259
346
 
260
347
        Raises an error if path is not in this branch."""
261
 
        return _relpath(self.base, path)
 
348
        return self._transport.relpath(path)
 
349
 
 
350
 
 
351
    def _rel_controlfilename(self, file_or_path):
 
352
        if isinstance(file_or_path, basestring):
 
353
            file_or_path = [file_or_path]
 
354
        return [bzrlib.BZRDIR] + file_or_path
262
355
 
263
356
    def controlfilename(self, file_or_path):
264
357
        """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)
 
358
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
268
359
 
269
360
 
270
361
    def controlfile(self, file_or_path, mode='r'):
278
369
        Controlfiles should almost never be opened in write mode but
279
370
        rather should be atomically copied and replaced using atomicfile.
280
371
        """
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)
 
372
        import codecs
 
373
 
 
374
        relpath = self._rel_controlfilename(file_or_path)
 
375
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
376
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
377
        if mode == 'rb': 
 
378
            return self._transport.get(relpath)
 
379
        elif mode == 'wb':
 
380
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
381
        elif mode == 'r':
 
382
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
383
        elif mode == 'w':
 
384
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
292
385
        else:
293
386
            raise BzrError("invalid controlfile mode %r" % mode)
294
387
 
 
388
    def put_controlfile(self, path, f, encode=True):
 
389
        """Write an entry as a controlfile.
 
390
 
 
391
        :param path: The path to put the file, relative to the .bzr control
 
392
                     directory
 
393
        :param f: A file-like or string object whose contents should be copied.
 
394
        :param encode:  If true, encode the contents as utf-8
 
395
        """
 
396
        self.put_controlfiles([(path, f)], encode=encode)
 
397
 
 
398
    def put_controlfiles(self, files, encode=True):
 
399
        """Write several entries as controlfiles.
 
400
 
 
401
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
402
                      underneath the bzr control directory
 
403
        :param encode:  If true, encode the contents as utf-8
 
404
        """
 
405
        import codecs
 
406
        ctrl_files = []
 
407
        for path, f in files:
 
408
            if encode:
 
409
                if isinstance(f, basestring):
 
410
                    f = f.encode('utf-8', 'replace')
 
411
                else:
 
412
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
413
            path = self._rel_controlfilename(path)
 
414
            ctrl_files.append((path, f))
 
415
        self._transport.put_multi(ctrl_files)
 
416
 
295
417
    def _make_control(self):
296
418
        from bzrlib.inventory import Inventory
 
419
        from bzrlib.weavefile import write_weave_v5
 
420
        from bzrlib.weave import Weave
297
421
        
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
 
 
 
422
        # Create an empty inventory
 
423
        sio = StringIO()
312
424
        # if we want per-tree root ids then this is the place to set
313
425
        # them; they're not needed for now and so ommitted for
314
426
        # simplicity.
315
 
        f = self.controlfile('inventory','w')
316
 
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
317
 
 
318
 
 
319
 
    def _check_format(self):
 
427
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
428
        empty_inv = sio.getvalue()
 
429
        sio = StringIO()
 
430
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
431
        empty_weave = sio.getvalue()
 
432
 
 
433
        dirs = [[], 'revision-store', 'weaves']
 
434
        files = [('README', 
 
435
            "This is a Bazaar-NG control directory.\n"
 
436
            "Do not change any files in this directory.\n"),
 
437
            ('branch-format', BZR_BRANCH_FORMAT_5),
 
438
            ('revision-history', ''),
 
439
            ('branch-name', ''),
 
440
            ('branch-lock', ''),
 
441
            ('pending-merges', ''),
 
442
            ('inventory', empty_inv),
 
443
            ('inventory.weave', empty_weave),
 
444
            ('ancestry.weave', empty_weave)
 
445
        ]
 
446
        cfn = self._rel_controlfilename
 
447
        self._transport.mkdir_multi([cfn(d) for d in dirs])
 
448
        self.put_controlfiles(files)
 
449
        mutter('created control directory in ' + self._transport.base)
 
450
 
 
451
    def _check_format(self, relax_version_check):
320
452
        """Check this branch format is supported.
321
453
 
322
 
        The current tool only supports the current unstable format.
 
454
        The format level is stored, as an integer, in
 
455
        self._branch_format for code that needs to check it later.
323
456
 
324
457
        In the future, we might need different in-memory Branch
325
458
        classes to support downlevel branches.  But not yet.
326
459
        """
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:
 
460
        try:
 
461
            fmt = self.controlfile('branch-format', 'r').read()
 
462
        except NoSuchFile:
 
463
            raise NotBranchError(self.base)
 
464
 
 
465
        if fmt == BZR_BRANCH_FORMAT_5:
 
466
            self._branch_format = 5
 
467
        elif fmt == BZR_BRANCH_FORMAT_4:
 
468
            self._branch_format = 4
 
469
 
 
470
        if (not relax_version_check
 
471
            and self._branch_format != 5):
333
472
            raise BzrError('sorry, branch format %r not supported' % fmt,
334
473
                           ['use a different bzr version',
335
 
                            'or remove the .bzr directory and "bzr init" again'])
 
474
                            'or remove the .bzr directory'
 
475
                            ' and "bzr init" again'])
336
476
 
337
477
    def get_root_id(self):
338
478
        """Return the id of this branches root"""
353
493
 
354
494
    def read_working_inventory(self):
355
495
        """Read the working inventory."""
356
 
        from bzrlib.inventory import Inventory
357
496
        self.lock_read()
358
497
        try:
359
498
            # ElementTree does its own conversion from UTF-8, so open in
360
499
            # binary.
361
500
            f = self.controlfile('inventory', 'rb')
362
 
            return bzrlib.xml.serializer_v4.read_inventory(f)
 
501
            return bzrlib.xml5.serializer_v5.read_inventory(f)
363
502
        finally:
364
503
            self.unlock()
365
504
            
370
509
        That is to say, the inventory describing changes underway, that
371
510
        will be committed to the next revision.
372
511
        """
373
 
        from bzrlib.atomicfile import AtomicFile
374
 
        
 
512
        from cStringIO import StringIO
375
513
        self.lock_write()
376
514
        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()
 
515
            sio = StringIO()
 
516
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
517
            sio.seek(0)
 
518
            # Transport handles atomicity
 
519
            self.put_controlfile('inventory', sio)
383
520
        finally:
384
521
            self.unlock()
385
522
        
386
523
        mutter('wrote working inventory')
387
524
            
388
 
 
389
525
    inventory = property(read_working_inventory, _write_inventory, None,
390
526
                         """Inventory for the working copy.""")
391
527
 
392
 
 
393
528
    def add(self, files, ids=None):
394
529
        """Make files versioned.
395
530
 
443
578
                    kind = file_kind(fullpath)
444
579
                except OSError:
445
580
                    # maybe something better?
446
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
581
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
447
582
 
448
 
                if kind != 'file' and kind != 'directory':
449
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
583
                if not InventoryEntry.versionable_kind(kind):
 
584
                    raise BzrError('cannot add: not a versionable file ('
 
585
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
450
586
 
451
587
                if file_id is None:
452
588
                    file_id = gen_file_id(f)
463
599
        """Print `file` to stdout."""
464
600
        self.lock_read()
465
601
        try:
466
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
602
            tree = self.revision_tree(self.get_rev_id(revno))
467
603
            # use inventory as it was in that revision
468
604
            file_id = tree.inventory.path2id(file)
469
605
            if not file_id:
517
653
        finally:
518
654
            self.unlock()
519
655
 
520
 
 
521
656
    # FIXME: this doesn't need to be a branch method
522
657
    def set_inventory(self, new_inventory_list):
523
658
        from bzrlib.inventory import Inventory, InventoryEntry
526
661
            name = os.path.basename(path)
527
662
            if name == "":
528
663
                continue
529
 
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
664
            # fixme, there should be a factory function inv,add_?? 
 
665
            if kind == 'directory':
 
666
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
 
667
            elif kind == 'file':
 
668
                inv.add(inventory.InventoryFile(file_id, name, parent))
 
669
            elif kind == 'symlink':
 
670
                inv.add(inventory.InventoryLink(file_id, name, parent))
 
671
            else:
 
672
                raise BzrError("unknown kind %r" % kind)
530
673
        self._write_inventory(inv)
531
674
 
532
 
 
533
675
    def unknowns(self):
534
676
        """Return all unknown files.
535
677
 
550
692
 
551
693
 
552
694
    def append_revision(self, *revision_ids):
553
 
        from bzrlib.atomicfile import AtomicFile
554
 
 
555
695
        for revision_id in revision_ids:
556
696
            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'))
 
697
        self.lock_write()
562
698
        try:
563
 
            for rev_id in rev_history:
564
 
                print >>f, rev_id
565
 
            f.commit()
 
699
            rev_history = self.revision_history()
 
700
            rev_history.extend(revision_ids)
 
701
            self.put_controlfile('revision-history', '\n'.join(rev_history))
566
702
        finally:
567
 
            f.close()
568
 
 
 
703
            self.unlock()
 
704
 
 
705
    def has_revision(self, revision_id):
 
706
        """True if this branch has a copy of the revision.
 
707
 
 
708
        This does not necessarily imply the revision is merge
 
709
        or on the mainline."""
 
710
        return (revision_id is None
 
711
                or revision_id in self.revision_store)
569
712
 
570
713
    def get_revision_xml_file(self, revision_id):
571
714
        """Return XML file object for revision object."""
581
724
        finally:
582
725
            self.unlock()
583
726
 
584
 
 
585
727
    #deprecated
586
728
    get_revision_xml = get_revision_xml_file
587
729
 
 
730
    def get_revision_xml(self, revision_id):
 
731
        return self.get_revision_xml_file(revision_id).read()
 
732
 
588
733
 
589
734
    def get_revision(self, revision_id):
590
735
        """Return the Revision object for a named revision"""
591
736
        xml_file = self.get_revision_xml_file(revision_id)
592
737
 
593
738
        try:
594
 
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
739
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
595
740
        except SyntaxError, e:
596
741
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
597
742
                                         [revision_id,
600
745
        assert r.revision_id == revision_id
601
746
        return r
602
747
 
603
 
 
604
748
    def get_revision_delta(self, revno):
605
749
        """Return the delta for one revision.
606
750
 
622
766
 
623
767
        return compare_trees(old_tree, new_tree)
624
768
 
625
 
        
626
 
 
627
769
    def get_revision_sha1(self, revision_id):
628
770
        """Hash the stored value of a revision, and return it."""
629
771
        # In the future, revision entries will be signed. At that
632
774
        # the revision, (add signatures/remove signatures) and still
633
775
        # have all hash pointers stay consistent.
634
776
        # 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):
 
777
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
778
 
 
779
    def _get_ancestry_weave(self):
 
780
        return self.control_weaves.get_weave('ancestry')
 
781
 
 
782
    def get_ancestry(self, revision_id):
 
783
        """Return a list of revision-ids integrated by a revision.
 
784
        """
 
785
        # strip newlines
 
786
        if revision_id is None:
 
787
            return [None]
 
788
        w = self._get_ancestry_weave()
 
789
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
790
 
 
791
    def get_inventory_weave(self):
 
792
        return self.control_weaves.get_weave('inventory')
 
793
 
 
794
    def get_inventory(self, revision_id):
 
795
        """Get Inventory object by hash."""
 
796
        xml = self.get_inventory_xml(revision_id)
 
797
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
798
 
 
799
    def get_inventory_xml(self, revision_id):
651
800
        """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):
 
801
        try:
 
802
            assert isinstance(revision_id, basestring), type(revision_id)
 
803
            iw = self.get_inventory_weave()
 
804
            return iw.get_text(iw.lookup(revision_id))
 
805
        except IndexError:
 
806
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
807
 
 
808
    def get_inventory_sha1(self, revision_id):
658
809
        """Return the sha1 hash of the inventory entry
659
810
        """
660
 
        return sha_file(self.get_inventory_xml(inventory_id))
661
 
 
 
811
        return self.get_revision(revision_id).inventory_sha1
662
812
 
663
813
    def get_revision_inventory(self, revision_id):
664
814
        """Return inventory of a past revision."""
665
 
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
815
        # TODO: Unify this with get_inventory()
 
816
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
666
817
        # must be the same as its revision, so this is trivial.
667
818
        if revision_id == None:
668
 
            from bzrlib.inventory import Inventory
669
819
            return Inventory(self.get_root_id())
670
820
        else:
671
821
            return self.get_inventory(revision_id)
672
822
 
673
 
 
674
823
    def revision_history(self):
675
 
        """Return sequence of revision hashes on to this branch.
676
 
 
677
 
        >>> ScratchBranch().revision_history()
678
 
        []
679
 
        """
 
824
        """Return sequence of revision hashes on to this branch."""
680
825
        self.lock_read()
681
826
        try:
682
827
            return [l.rstrip('\r\n') for l in
684
829
        finally:
685
830
            self.unlock()
686
831
 
687
 
 
688
832
    def common_ancestor(self, other, self_revno=None, other_revno=None):
689
833
        """
690
834
        >>> from bzrlib.commit import commit
739
883
        return len(self.revision_history())
740
884
 
741
885
 
742
 
    def last_patch(self):
 
886
    def last_revision(self):
743
887
        """Return last patch hash, or None if no history.
744
888
        """
745
889
        ph = self.revision_history()
750
894
 
751
895
 
752
896
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
753
 
        """
 
897
        """Return a list of new revisions that would perfectly fit.
 
898
        
754
899
        If self and other have not diverged, return a list of the revisions
755
900
        present in other, but missing from self.
756
901
 
776
921
        Traceback (most recent call last):
777
922
        DivergedBranches: These branches have diverged.
778
923
        """
 
924
        # FIXME: If the branches have diverged, but the latest
 
925
        # revision in this branch is completely merged into the other,
 
926
        # then we should still be able to pull.
779
927
        self_history = self.revision_history()
780
928
        self_len = len(self_history)
781
929
        other_history = other.revision_history()
787
935
 
788
936
        if stop_revision is None:
789
937
            stop_revision = other_len
790
 
        elif stop_revision > other_len:
791
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
792
 
        
 
938
        else:
 
939
            assert isinstance(stop_revision, int)
 
940
            if stop_revision > other_len:
 
941
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
793
942
        return other_history[self_len:stop_revision]
794
943
 
795
 
 
796
944
    def update_revisions(self, other, stop_revision=None):
797
 
        """Pull in all new revisions from other branch.
798
 
        """
 
945
        """Pull in new perfect-fit revisions."""
799
946
        from bzrlib.fetch import greedy_fetch
800
947
        from bzrlib.revision import get_intervening_revisions
801
 
 
802
 
        pb = bzrlib.ui.ui_factory.progress_bar()
803
 
        pb.update('comparing histories')
804
948
        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
 
       
 
949
            stop_revision = other.last_revision()
 
950
        greedy_fetch(to_branch=self, from_branch=other,
 
951
                     revision=stop_revision)
 
952
        pullable_revs = self.missing_revisions(
 
953
            other, other.revision_id_to_revno(stop_revision))
 
954
        if pullable_revs:
 
955
            greedy_fetch(to_branch=self,
 
956
                         from_branch=other,
 
957
                         revision=pullable_revs[-1])
 
958
            self.append_revision(*pullable_revs)
 
959
    
876
960
 
877
961
    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
 
 
 
962
        from bzrlib.commit import Commit
 
963
        Commit().commit(self, *args, **kw)
 
964
    
888
965
    def revision_id_to_revno(self, revision_id):
889
966
        """Given a revision id, return its revno"""
 
967
        if revision_id is None:
 
968
            return 0
890
969
        history = self.revision_history()
891
970
        try:
892
971
            return history.index(revision_id) + 1
893
972
        except ValueError:
894
973
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
895
974
 
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
975
    def get_rev_id(self, revno, history=None):
911
976
        """Find the revision id of the specified revno."""
912
977
        if revno == 0:
917
982
            raise bzrlib.errors.NoSuchRevision(self, revno)
918
983
        return history[revno - 1]
919
984
 
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
985
    def revision_tree(self, revision_id):
1107
986
        """Return Tree for a revision on this branch.
1108
987
 
1114
993
            return EmptyTree()
1115
994
        else:
1116
995
            inv = self.get_revision_inventory(revision_id)
1117
 
            return RevisionTree(self.text_store, inv)
 
996
            return RevisionTree(self.weave_store, inv, revision_id)
1118
997
 
1119
998
 
1120
999
    def working_tree(self):
1121
1000
        """Return a `Tree` for the working copy."""
1122
1001
        from bzrlib.workingtree import WorkingTree
1123
 
        return WorkingTree(self.base, self.read_working_inventory())
 
1002
        # TODO: In the future, WorkingTree should utilize Transport
 
1003
        # RobertCollins 20051003 - I don't think it should - working trees are
 
1004
        # much more complex to keep consistent than our careful .bzr subset.
 
1005
        # instead, we should say that working trees are local only, and optimise
 
1006
        # for that.
 
1007
        return WorkingTree(self._transport.base, self.read_working_inventory())
1124
1008
 
1125
1009
 
1126
1010
    def basis_tree(self):
1128
1012
 
1129
1013
        If there are no revisions yet, return an `EmptyTree`.
1130
1014
        """
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
 
 
 
1015
        return self.revision_tree(self.last_revision())
1137
1016
 
1138
1017
 
1139
1018
    def rename_one(self, from_rel, to_rel):
1174
1053
            from_abs = self.abspath(from_rel)
1175
1054
            to_abs = self.abspath(to_rel)
1176
1055
            try:
1177
 
                os.rename(from_abs, to_abs)
 
1056
                rename(from_abs, to_abs)
1178
1057
            except OSError, e:
1179
1058
                raise BzrError("failed to rename %r to %r: %s"
1180
1059
                        % (from_abs, to_abs, e[1]),
1243
1122
                result.append((f, dest_path))
1244
1123
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1245
1124
                try:
1246
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1125
                    rename(self.abspath(f), self.abspath(dest_path))
1247
1126
                except OSError, e:
1248
1127
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1249
1128
                            ["rename rolled back"])
1305
1184
        These are revisions that have been merged into the working
1306
1185
        directory but not yet committed.
1307
1186
        """
1308
 
        cfn = self.controlfilename('pending-merges')
1309
 
        if not os.path.exists(cfn):
 
1187
        cfn = self._rel_controlfilename('pending-merges')
 
1188
        if not self._transport.has(cfn):
1310
1189
            return []
1311
1190
        p = []
1312
1191
        for l in self.controlfile('pending-merges', 'r').readlines():
1314
1193
        return p
1315
1194
 
1316
1195
 
1317
 
    def add_pending_merge(self, revision_id):
1318
 
        from bzrlib.revision import validate_revision_id
1319
 
 
1320
 
        validate_revision_id(revision_id)
 
1196
    def add_pending_merge(self, *revision_ids):
 
1197
        # TODO: Perhaps should check at this point that the
 
1198
        # history of the revision is actually present?
 
1199
        for rev_id in revision_ids:
 
1200
            validate_revision_id(rev_id)
1321
1201
 
1322
1202
        p = self.pending_merges()
1323
 
        if revision_id in p:
1324
 
            return
1325
 
        p.append(revision_id)
1326
 
        self.set_pending_merges(p)
1327
 
 
 
1203
        updated = False
 
1204
        for rev_id in revision_ids:
 
1205
            if rev_id in p:
 
1206
                continue
 
1207
            p.append(rev_id)
 
1208
            updated = True
 
1209
        if updated:
 
1210
            self.set_pending_merges(p)
1328
1211
 
1329
1212
    def set_pending_merges(self, rev_list):
1330
 
        from bzrlib.atomicfile import AtomicFile
1331
1213
        self.lock_write()
1332
1214
        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()
 
1215
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1340
1216
        finally:
1341
1217
            self.unlock()
1342
1218
 
1390
1266
            raise InvalidRevisionNumber(revno)
1391
1267
        
1392
1268
        
1393
 
 
1394
 
 
1395
 
class ScratchBranch(Branch):
 
1269
        
 
1270
 
 
1271
 
 
1272
class ScratchBranch(_Branch):
1396
1273
    """Special test class: a branch that cleans up after itself.
1397
1274
 
1398
1275
    >>> b = ScratchBranch()
1415
1292
        if base is None:
1416
1293
            base = mkdtemp()
1417
1294
            init = True
1418
 
        Branch.__init__(self, base, init=init)
 
1295
        if isinstance(base, basestring):
 
1296
            base = get_transport(base)
 
1297
        _Branch.__init__(self, base, init=init)
1419
1298
        for d in dirs:
1420
 
            os.mkdir(self.abspath(d))
 
1299
            self._transport.mkdir(d)
1421
1300
            
1422
1301
        for f in files:
1423
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1302
            self._transport.put(f, 'content of %s' % f)
1424
1303
 
1425
1304
 
1426
1305
    def clone(self):
1427
1306
        """
1428
1307
        >>> orig = ScratchBranch(files=["file1", "file2"])
1429
1308
        >>> clone = orig.clone()
1430
 
        >>> os.path.samefile(orig.base, clone.base)
 
1309
        >>> if os.name != 'nt':
 
1310
        ...   os.path.samefile(orig.base, clone.base)
 
1311
        ... else:
 
1312
        ...   orig.base == clone.base
 
1313
        ...
1431
1314
        False
1432
1315
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1433
1316
        True
1439
1322
        copytree(self.base, base, symlinks=True)
1440
1323
        return ScratchBranch(base=base)
1441
1324
 
1442
 
 
1443
 
        
1444
1325
    def __del__(self):
1445
1326
        self.destroy()
1446
1327
 
1459
1340
                for name in files:
1460
1341
                    os.chmod(os.path.join(root, name), 0700)
1461
1342
            rmtree(self.base)
1462
 
        self.base = None
 
1343
        self._transport = None
1463
1344
 
1464
1345
    
1465
1346
 
1516
1397
    return gen_file_id('TREE_ROOT')
1517
1398
 
1518
1399
 
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):]