~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-05 05:35:25 UTC
  • mfrom: (974.1.55)
  • Revision ID: mbp@sourcefrog.net-20050905053525-2112bac069dbe331
- merge various bug fixes from aaron

aaron.bentley@utoronto.ca-20050905020131-a2d5b7711dd6cd98

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