~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-10-17 11:56:54 UTC
  • mfrom: (1185.16.59)
  • Revision ID: robertc@robertcollins.net-20051017115654-662239e1587524a8
mergeĀ fromĀ martin.

Show diffs side-by-side

added added

removed removed

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