~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-01 11:53:02 UTC
  • Revision ID: mbp@sourcefrog.net-20050901115302-2fcc6c750f0abe34
- make external commands work again

  code is now much simpler; no translation to objects and back again

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)
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)
761
590
        try:
762
591
            try:
763
592
                return self.revision_store[revision_id]
764
 
            except (IndexError, KeyError):
 
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
 
1031
 
    def get_rev_id(self, revno, history=None):
1032
 
        """Find the revision id of the specified revno."""
1033
 
        if revno == 0:
1034
 
            return None
1035
 
        if history is None:
1036
 
            history = self.revision_history()
1037
 
        elif revno <= 0 or revno > len(history):
1038
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
1039
 
        return history[revno - 1]
 
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
        if revision is None:
 
901
            return 0, None
 
902
        revno = None
 
903
        try:# Convert to int if possible
 
904
            revision = int(revision)
 
905
        except ValueError:
 
906
            pass
 
907
        revs = self.revision_history()
 
908
        if isinstance(revision, int):
 
909
            if revision == 0:
 
910
                return 0, None
 
911
            # Mabye we should do this first, but we don't need it if revision == 0
 
912
            if revision < 0:
 
913
                revno = len(revs) + revision + 1
 
914
            else:
 
915
                revno = revision
 
916
        elif isinstance(revision, basestring):
 
917
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
918
                if revision.startswith(prefix):
 
919
                    revno = func(self, revs, revision)
 
920
                    break
 
921
            else:
 
922
                raise BzrError('No namespace registered for string: %r' % revision)
 
923
 
 
924
        if revno is None or revno <= 0 or revno > len(revs):
 
925
            raise BzrError("no such revision %s" % revision)
 
926
        return revno, revs[revno-1]
 
927
 
 
928
    def _namespace_revno(self, revs, revision):
 
929
        """Lookup a revision by revision number"""
 
930
        assert revision.startswith('revno:')
 
931
        try:
 
932
            return int(revision[6:])
 
933
        except ValueError:
 
934
            return None
 
935
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
936
 
 
937
    def _namespace_revid(self, revs, revision):
 
938
        assert revision.startswith('revid:')
 
939
        try:
 
940
            return revs.index(revision[6:]) + 1
 
941
        except ValueError:
 
942
            return None
 
943
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
944
 
 
945
    def _namespace_last(self, revs, revision):
 
946
        assert revision.startswith('last:')
 
947
        try:
 
948
            offset = int(revision[5:])
 
949
        except ValueError:
 
950
            return None
 
951
        else:
 
952
            if offset <= 0:
 
953
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
954
            return len(revs) - offset + 1
 
955
    REVISION_NAMESPACES['last:'] = _namespace_last
 
956
 
 
957
    def _namespace_tag(self, revs, revision):
 
958
        assert revision.startswith('tag:')
 
959
        raise BzrError('tag: namespace registered, but not implemented.')
 
960
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
961
 
 
962
    def _namespace_date(self, revs, revision):
 
963
        assert revision.startswith('date:')
 
964
        import datetime
 
965
        # Spec for date revisions:
 
966
        #   date:value
 
967
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
968
        #   it can also start with a '+/-/='. '+' says match the first
 
969
        #   entry after the given date. '-' is match the first entry before the date
 
970
        #   '=' is match the first entry after, but still on the given date.
 
971
        #
 
972
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
973
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
974
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
975
        #       May 13th, 2005 at 0:00
 
976
        #
 
977
        #   So the proper way of saying 'give me all entries for today' is:
 
978
        #       -r {date:+today}:{date:-tomorrow}
 
979
        #   The default is '=' when not supplied
 
980
        val = revision[5:]
 
981
        match_style = '='
 
982
        if val[:1] in ('+', '-', '='):
 
983
            match_style = val[:1]
 
984
            val = val[1:]
 
985
 
 
986
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
987
        if val.lower() == 'yesterday':
 
988
            dt = today - datetime.timedelta(days=1)
 
989
        elif val.lower() == 'today':
 
990
            dt = today
 
991
        elif val.lower() == 'tomorrow':
 
992
            dt = today + datetime.timedelta(days=1)
 
993
        else:
 
994
            import re
 
995
            # This should be done outside the function to avoid recompiling it.
 
996
            _date_re = re.compile(
 
997
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
998
                    r'(,|T)?\s*'
 
999
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1000
                )
 
1001
            m = _date_re.match(val)
 
1002
            if not m or (not m.group('date') and not m.group('time')):
 
1003
                raise BzrError('Invalid revision date %r' % revision)
 
1004
 
 
1005
            if m.group('date'):
 
1006
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1007
            else:
 
1008
                year, month, day = today.year, today.month, today.day
 
1009
            if m.group('time'):
 
1010
                hour = int(m.group('hour'))
 
1011
                minute = int(m.group('minute'))
 
1012
                if m.group('second'):
 
1013
                    second = int(m.group('second'))
 
1014
                else:
 
1015
                    second = 0
 
1016
            else:
 
1017
                hour, minute, second = 0,0,0
 
1018
 
 
1019
            dt = datetime.datetime(year=year, month=month, day=day,
 
1020
                    hour=hour, minute=minute, second=second)
 
1021
        first = dt
 
1022
        last = None
 
1023
        reversed = False
 
1024
        if match_style == '-':
 
1025
            reversed = True
 
1026
        elif match_style == '=':
 
1027
            last = dt + datetime.timedelta(days=1)
 
1028
 
 
1029
        if reversed:
 
1030
            for i in range(len(revs)-1, -1, -1):
 
1031
                r = self.get_revision(revs[i])
 
1032
                # TODO: Handle timezone.
 
1033
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1034
                if first >= dt and (last is None or dt >= last):
 
1035
                    return i+1
 
1036
        else:
 
1037
            for i in range(len(revs)):
 
1038
                r = self.get_revision(revs[i])
 
1039
                # TODO: Handle timezone.
 
1040
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1041
                if first <= dt and (last is None or dt <= last):
 
1042
                    return i+1
 
1043
    REVISION_NAMESPACES['date:'] = _namespace_date
1040
1044
 
1041
1045
    def revision_tree(self, revision_id):
1042
1046
        """Return Tree for a revision on this branch.
1049
1053
            return EmptyTree()
1050
1054
        else:
1051
1055
            inv = self.get_revision_inventory(revision_id)
1052
 
            return RevisionTree(self.weave_store, inv, revision_id)
 
1056
            return RevisionTree(self.text_store, inv)
1053
1057
 
1054
1058
 
1055
1059
    def working_tree(self):
1056
1060
        """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())
 
1061
        from workingtree import WorkingTree
 
1062
        return WorkingTree(self.base, self.read_working_inventory())
1064
1063
 
1065
1064
 
1066
1065
    def basis_tree(self):
1068
1067
 
1069
1068
        If there are no revisions yet, return an `EmptyTree`.
1070
1069
        """
1071
 
        return self.revision_tree(self.last_revision())
 
1070
        r = self.last_patch()
 
1071
        if r == None:
 
1072
            return EmptyTree()
 
1073
        else:
 
1074
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
1075
 
1072
1076
 
1073
1077
 
1074
1078
    def rename_one(self, from_rel, to_rel):
1109
1113
            from_abs = self.abspath(from_rel)
1110
1114
            to_abs = self.abspath(to_rel)
1111
1115
            try:
1112
 
                rename(from_abs, to_abs)
 
1116
                os.rename(from_abs, to_abs)
1113
1117
            except OSError, e:
1114
1118
                raise BzrError("failed to rename %r to %r: %s"
1115
1119
                        % (from_abs, to_abs, e[1]),
1178
1182
                result.append((f, dest_path))
1179
1183
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1180
1184
                try:
1181
 
                    rename(self.abspath(f), self.abspath(dest_path))
 
1185
                    os.rename(self.abspath(f), self.abspath(dest_path))
1182
1186
                except OSError, e:
1183
1187
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1184
1188
                            ["rename rolled back"])
1240
1244
        These are revisions that have been merged into the working
1241
1245
        directory but not yet committed.
1242
1246
        """
1243
 
        cfn = self._rel_controlfilename('pending-merges')
1244
 
        if not self._transport.has(cfn):
 
1247
        cfn = self.controlfilename('pending-merges')
 
1248
        if not os.path.exists(cfn):
1245
1249
            return []
1246
1250
        p = []
1247
1251
        for l in self.controlfile('pending-merges', 'r').readlines():
1249
1253
        return p
1250
1254
 
1251
1255
 
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?
 
1256
    def add_pending_merge(self, revision_id):
 
1257
        from bzrlib.revision import validate_revision_id
 
1258
 
 
1259
        validate_revision_id(revision_id)
 
1260
 
1255
1261
        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)
 
1262
        if revision_id in p:
 
1263
            return
 
1264
        p.append(revision_id)
 
1265
        self.set_pending_merges(p)
 
1266
 
1264
1267
 
1265
1268
    def set_pending_merges(self, rev_list):
 
1269
        from bzrlib.atomicfile import AtomicFile
1266
1270
        self.lock_write()
1267
1271
        try:
1268
 
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1272
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1273
            try:
 
1274
                for l in rev_list:
 
1275
                    print >>f, l
 
1276
                f.commit()
 
1277
            finally:
 
1278
                f.close()
1269
1279
        finally:
1270
1280
            self.unlock()
1271
1281
 
1302
1312
        finally:
1303
1313
            self.unlock()
1304
1314
 
1305
 
    def check_revno(self, revno):
1306
 
        """\
1307
 
        Check whether a revno corresponds to any revision.
1308
 
        Zero (the NULL revision) is considered valid.
1309
 
        """
1310
 
        if revno != 0:
1311
 
            self.check_real_revno(revno)
1312
 
            
1313
 
    def check_real_revno(self, revno):
1314
 
        """\
1315
 
        Check whether a revno corresponds to a real revision.
1316
 
        Zero (the NULL revision) is considered invalid
1317
 
        """
1318
 
        if revno < 1 or revno > self.revno():
1319
 
            raise InvalidRevisionNumber(revno)
1320
 
        
1321
 
        
1322
 
        
1323
 
 
1324
 
 
1325
 
class ScratchBranch(_Branch):
 
1315
        
 
1316
 
 
1317
 
 
1318
class ScratchBranch(Branch):
1326
1319
    """Special test class: a branch that cleans up after itself.
1327
1320
 
1328
1321
    >>> b = ScratchBranch()
1345
1338
        if base is None:
1346
1339
            base = mkdtemp()
1347
1340
            init = True
1348
 
        if isinstance(base, basestring):
1349
 
            base = get_transport(base)
1350
 
        _Branch.__init__(self, base, init=init)
 
1341
        Branch.__init__(self, base, init=init)
1351
1342
        for d in dirs:
1352
 
            self._transport.mkdir(d)
 
1343
            os.mkdir(self.abspath(d))
1353
1344
            
1354
1345
        for f in files:
1355
 
            self._transport.put(f, 'content of %s' % f)
 
1346
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1356
1347
 
1357
1348
 
1358
1349
    def clone(self):
1359
1350
        """
1360
1351
        >>> orig = ScratchBranch(files=["file1", "file2"])
1361
1352
        >>> clone = orig.clone()
1362
 
        >>> if os.name != 'nt':
1363
 
        ...   os.path.samefile(orig.base, clone.base)
1364
 
        ... else:
1365
 
        ...   orig.base == clone.base
1366
 
        ...
 
1353
        >>> os.path.samefile(orig.base, clone.base)
1367
1354
        False
1368
1355
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1369
1356
        True
1375
1362
        copytree(self.base, base, symlinks=True)
1376
1363
        return ScratchBranch(base=base)
1377
1364
 
 
1365
 
 
1366
        
1378
1367
    def __del__(self):
1379
1368
        self.destroy()
1380
1369
 
1393
1382
                for name in files:
1394
1383
                    os.chmod(os.path.join(root, name), 0700)
1395
1384
            rmtree(self.base)
1396
 
        self._transport = None
 
1385
        self.base = None
1397
1386
 
1398
1387
    
1399
1388
 
1450
1439
    return gen_file_id('TREE_ROOT')
1451
1440
 
1452
1441
 
 
1442
def pull_loc(branch):
 
1443
    # TODO: Should perhaps just make attribute be 'base' in
 
1444
    # RemoteBranch and Branch?
 
1445
    if hasattr(branch, "baseurl"):
 
1446
        return branch.baseurl
 
1447
    else:
 
1448
        return branch.base
 
1449
 
 
1450
 
 
1451
def copy_branch(branch_from, to_location, revision=None):
 
1452
    """Copy branch_from into the existing directory to_location.
 
1453
 
 
1454
    revision
 
1455
        If not None, only revisions up to this point will be copied.
 
1456
        The head of the new branch will be that revision.
 
1457
 
 
1458
    to_location
 
1459
        The name of a local directory that exists but is empty.
 
1460
    """
 
1461
    from bzrlib.merge import merge
 
1462
    from bzrlib.branch import Branch
 
1463
 
 
1464
    assert isinstance(branch_from, Branch)
 
1465
    assert isinstance(to_location, basestring)
 
1466
    
 
1467
    br_to = Branch(to_location, init=True)
 
1468
    br_to.set_root_id(branch_from.get_root_id())
 
1469
    if revision is None:
 
1470
        revno = branch_from.revno()
 
1471
    else:
 
1472
        revno, rev_id = branch_from.get_revision_info(revision)
 
1473
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1474
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1475
          check_clean=False, ignore_zero=True)
 
1476
    
 
1477
    from_location = pull_loc(branch_from)
 
1478
    br_to.set_parent(pull_loc(branch_from))
 
1479