~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

[merge] 0.7-bugfix: Fix fileid_involved to unescape xml characters, fix StubServer to handle paramiko > 1.5.2

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 shutil
18
19
import sys
19
20
import os
 
21
import errno
 
22
from warnings import warn
 
23
import xml.sax.saxutils
20
24
from cStringIO import StringIO
21
25
 
 
26
 
22
27
import bzrlib
 
28
import bzrlib.inventory as inventory
23
29
from bzrlib.trace import mutter, note
24
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
25
 
     splitpath, \
26
 
     sha_file, appendpath, file_kind
27
 
 
 
30
from bzrlib.osutils import (isdir, quotefn,
 
31
                            rename, splitpath, sha_file,
 
32
                            file_kind, abspath, normpath, pathjoin)
 
33
import bzrlib.errors as errors
28
34
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
29
 
                           NoSuchRevision)
 
35
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
36
                           DivergedBranches, LockError, UnlistableStore,
 
37
                           UnlistableBranch, NoSuchFile, NotVersionedError,
 
38
                           NoWorkingTree)
30
39
from bzrlib.textui import show_status
31
 
from bzrlib.revision import Revision
 
40
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
 
41
                             NULL_REVISION)
 
42
 
32
43
from bzrlib.delta import compare_trees
33
44
from bzrlib.tree import EmptyTree, RevisionTree
34
45
from bzrlib.inventory import Inventory
35
 
from bzrlib.weavestore import WeaveStore
36
 
from bzrlib.store import ImmutableStore
 
46
from bzrlib.store import copy_all
 
47
from bzrlib.store.text import TextStore
 
48
from bzrlib.store.weave import WeaveStore
 
49
from bzrlib.testament import Testament
 
50
import bzrlib.transactions as transactions
 
51
from bzrlib.transport import Transport, get_transport
37
52
import bzrlib.xml5
38
53
import bzrlib.ui
39
 
 
40
 
 
41
 
INVENTORY_FILEID = '__inventory'
42
 
ANCESTRY_FILEID = '__ancestry'
 
54
from config import TreeConfig
43
55
 
44
56
 
45
57
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
46
58
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
59
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
47
60
## TODO: Maybe include checks for common corruption of newlines, etc?
48
61
 
49
62
 
52
65
# cache in memory to make this faster.  In general anything can be
53
66
# cached in memory between lock and unlock operations.
54
67
 
55
 
# TODO: please move the revision-string syntax stuff out of the branch
56
 
# object; it's clutter
57
 
 
58
 
 
59
 
def find_branch(f, **args):
60
 
    if f and (f.startswith('http://') or f.startswith('https://')):
61
 
        import remotebranch 
62
 
        return remotebranch.RemoteBranch(f, **args)
63
 
    else:
64
 
        return Branch(f, **args)
65
 
 
66
 
 
67
 
def find_cached_branch(f, cache_root, **args):
68
 
    from remotebranch import RemoteBranch
69
 
    br = find_branch(f, **args)
70
 
    def cacheify(br, store_name):
71
 
        from meta_store import CachedStore
72
 
        cache_path = os.path.join(cache_root, store_name)
73
 
        os.mkdir(cache_path)
74
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
75
 
        setattr(br, store_name, new_store)
76
 
 
77
 
    if isinstance(br, RemoteBranch):
78
 
        cacheify(br, 'inventory_store')
79
 
        cacheify(br, 'text_store')
80
 
        cacheify(br, 'revision_store')
81
 
    return br
82
 
 
83
 
 
84
 
def _relpath(base, path):
85
 
    """Return path relative to base, or raise exception.
86
 
 
87
 
    The path may be either an absolute path or a path relative to the
88
 
    current working directory.
89
 
 
90
 
    Lifted out of Branch.relpath for ease of testing.
91
 
 
92
 
    os.path.commonprefix (python2.4) has a bad bug that it works just
93
 
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
94
 
    avoids that problem."""
95
 
    rp = os.path.abspath(path)
96
 
 
97
 
    s = []
98
 
    head = rp
99
 
    while len(head) >= len(base):
100
 
        if head == base:
101
 
            break
102
 
        head, tail = os.path.split(head)
103
 
        if tail:
104
 
            s.insert(0, tail)
105
 
    else:
106
 
        from errors import NotBranchError
107
 
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
108
 
 
109
 
    return os.sep.join(s)
110
 
        
111
 
 
112
 
def find_branch_root(f=None):
113
 
    """Find the branch root enclosing f, or pwd.
114
 
 
115
 
    f may be a filename or a URL.
116
 
 
117
 
    It is not necessary that f exists.
118
 
 
119
 
    Basically we keep looking up until we find the control directory or
120
 
    run into the root.  If there isn't one, raises NotBranchError.
121
 
    """
122
 
    if f == None:
123
 
        f = os.getcwd()
124
 
    elif hasattr(os.path, 'realpath'):
125
 
        f = os.path.realpath(f)
126
 
    else:
127
 
        f = os.path.abspath(f)
128
 
    if not os.path.exists(f):
129
 
        raise BzrError('%r does not exist' % f)
130
 
        
131
 
 
132
 
    orig_f = f
133
 
 
134
 
    while True:
135
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
136
 
            return f
137
 
        head, tail = os.path.split(f)
138
 
        if head == f:
139
 
            # reached the root, whatever that may be
140
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
141
 
        f = head
142
 
 
143
 
 
144
 
 
145
 
# XXX: move into bzrlib.errors; subclass BzrError    
146
 
class DivergedBranches(Exception):
147
 
    def __init__(self, branch1, branch2):
148
 
        self.branch1 = branch1
149
 
        self.branch2 = branch2
150
 
        Exception.__init__(self, "These branches have diverged.")
151
 
 
 
68
def find_branch(*ignored, **ignored_too):
 
69
    # XXX: leave this here for about one release, then remove it
 
70
    raise NotImplementedError('find_branch() is not supported anymore, '
 
71
                              'please use one of the new branch constructors')
 
72
 
 
73
 
 
74
def needs_read_lock(unbound):
 
75
    """Decorate unbound to take out and release a read lock."""
 
76
    def decorated(self, *args, **kwargs):
 
77
        self.lock_read()
 
78
        try:
 
79
            return unbound(self, *args, **kwargs)
 
80
        finally:
 
81
            self.unlock()
 
82
    return decorated
 
83
 
 
84
 
 
85
def needs_write_lock(unbound):
 
86
    """Decorate unbound to take out and release a write lock."""
 
87
    def decorated(self, *args, **kwargs):
 
88
        self.lock_write()
 
89
        try:
 
90
            return unbound(self, *args, **kwargs)
 
91
        finally:
 
92
            self.unlock()
 
93
    return decorated
152
94
 
153
95
######################################################################
154
96
# branch objects
157
99
    """Branch holding a history of revisions.
158
100
 
159
101
    base
160
 
        Base directory of the branch.
161
 
 
162
 
    _lock_mode
163
 
        None, or 'r' or 'w'
164
 
 
165
 
    _lock_count
166
 
        If _lock_mode is true, a positive count of the number of times the
167
 
        lock has been taken.
168
 
 
169
 
    _lock
170
 
        Lock object from bzrlib.lock.
 
102
        Base directory/url of the branch.
171
103
    """
172
104
    base = None
173
 
    _lock_mode = None
174
 
    _lock_count = None
175
 
    _lock = None
176
 
    _inventory_weave = None
177
 
    
178
 
    # Map some sort of prefix into a namespace
179
 
    # stuff like "revno:10", "revid:", etc.
180
 
    # This should match a prefix with a function which accepts
181
 
    REVISION_NAMESPACES = {}
182
 
 
183
 
    def __init__(self, base, init=False, find_root=True):
184
 
        """Create new branch object at a particular location.
185
 
 
186
 
        base -- Base directory for the branch.
187
 
        
188
 
        init -- If True, create new control files in a previously
189
 
             unversioned directory.  If False, the branch must already
190
 
             be versioned.
191
 
 
192
 
        find_root -- If true and init is false, find the root of the
193
 
             existing branch containing base.
194
 
 
195
 
        In the test suite, creation of new trees is tested using the
196
 
        `ScratchBranch` class.
197
 
        """
198
 
        if init:
199
 
            self.base = os.path.realpath(base)
200
 
            self._make_control()
201
 
        elif find_root:
202
 
            self.base = find_branch_root(base)
203
 
        else:
204
 
            self.base = os.path.realpath(base)
205
 
            if not isdir(self.controlfilename('.')):
206
 
                from errors import NotBranchError
207
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
208
 
                                     ['use "bzr init" to initialize a new working tree',
209
 
                                      'current bzr can only operate from top-of-tree'])
210
 
        self._check_format()
211
 
 
212
 
        self.weave_store = WeaveStore(self.controlfilename('weaves'))
213
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
214
 
 
215
 
 
216
 
    def __str__(self):
217
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
218
 
 
219
 
 
220
 
    __repr__ = __str__
221
 
 
222
 
 
223
 
    def __del__(self):
224
 
        if self._lock_mode or self._lock:
225
 
            from warnings import warn
226
 
            warn("branch %r was not explicitly unlocked" % self)
227
 
            self._lock.unlock()
228
 
 
 
105
 
 
106
    def __init__(self, *ignored, **ignored_too):
 
107
        raise NotImplementedError('The Branch class is abstract')
 
108
 
 
109
    @staticmethod
 
110
    def open_downlevel(base):
 
111
        """Open a branch which may be of an old format.
 
112
        
 
113
        Only local branches are supported."""
 
114
        return BzrBranch(get_transport(base), relax_version_check=True)
 
115
        
 
116
    @staticmethod
 
117
    def open(base):
 
118
        """Open an existing branch, rooted at 'base' (url)"""
 
119
        t = get_transport(base)
 
120
        mutter("trying to open %r with transport %r", base, t)
 
121
        return BzrBranch(t)
 
122
 
 
123
    @staticmethod
 
124
    def open_containing(url):
 
125
        """Open an existing branch which contains url.
 
126
        
 
127
        This probes for a branch at url, and searches upwards from there.
 
128
 
 
129
        Basically we keep looking up until we find the control directory or
 
130
        run into the root.  If there isn't one, raises NotBranchError.
 
131
        If there is one, it is returned, along with the unused portion of url.
 
132
        """
 
133
        t = get_transport(url)
 
134
        while True:
 
135
            try:
 
136
                return BzrBranch(t), t.relpath(url)
 
137
            except NotBranchError, e:
 
138
                mutter('not a branch in: %r %s', t.base, e)
 
139
            new_t = t.clone('..')
 
140
            if new_t.base == t.base:
 
141
                # reached the root, whatever that may be
 
142
                raise NotBranchError(path=url)
 
143
            t = new_t
 
144
 
 
145
    @staticmethod
 
146
    def initialize(base):
 
147
        """Create a new branch, rooted at 'base' (url)"""
 
148
        t = get_transport(base)
 
149
        return BzrBranch(t, init=True)
 
150
 
 
151
    def setup_caching(self, cache_root):
 
152
        """Subclasses that care about caching should override this, and set
 
153
        up cached stores located under cache_root.
 
154
        """
 
155
        self.cache_root = cache_root
 
156
 
 
157
    def _get_nick(self):
 
158
        cfg = self.tree_config()
 
159
        return cfg.get_option(u"nickname", default=self.base.split('/')[-2])
 
160
 
 
161
    def _set_nick(self, nick):
 
162
        cfg = self.tree_config()
 
163
        cfg.set_option(nick, "nickname")
 
164
        assert cfg.get_option("nickname") == nick
 
165
 
 
166
    nick = property(_get_nick, _set_nick)
 
167
        
 
168
    def push_stores(self, branch_to):
 
169
        """Copy the content of this branches store to branch_to."""
 
170
        raise NotImplementedError('push_stores is abstract')
 
171
 
 
172
    def get_transaction(self):
 
173
        """Return the current active transaction.
 
174
 
 
175
        If no transaction is active, this returns a passthrough object
 
176
        for which all data is immediately flushed and no caching happens.
 
177
        """
 
178
        raise NotImplementedError('get_transaction is abstract')
229
179
 
230
180
    def lock_write(self):
231
 
        if self._lock_mode:
232
 
            if self._lock_mode != 'w':
233
 
                from errors import LockError
234
 
                raise LockError("can't upgrade to a write lock from %r" %
235
 
                                self._lock_mode)
236
 
            self._lock_count += 1
237
 
        else:
238
 
            from bzrlib.lock import WriteLock
239
 
 
240
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
241
 
            self._lock_mode = 'w'
242
 
            self._lock_count = 1
243
 
 
244
 
 
 
181
        raise NotImplementedError('lock_write is abstract')
 
182
        
245
183
    def lock_read(self):
246
 
        if self._lock_mode:
247
 
            assert self._lock_mode in ('r', 'w'), \
248
 
                   "invalid lock mode %r" % self._lock_mode
249
 
            self._lock_count += 1
250
 
        else:
251
 
            from bzrlib.lock import ReadLock
 
184
        raise NotImplementedError('lock_read is abstract')
252
185
 
253
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
254
 
            self._lock_mode = 'r'
255
 
            self._lock_count = 1
256
 
                        
257
186
    def unlock(self):
258
 
        if not self._lock_mode:
259
 
            from errors import LockError
260
 
            raise LockError('branch %r is not locked' % (self))
261
 
 
262
 
        if self._lock_count > 1:
263
 
            self._lock_count -= 1
264
 
        else:
265
 
            self._lock.unlock()
266
 
            self._lock = None
267
 
            self._lock_mode = self._lock_count = None
 
187
        raise NotImplementedError('unlock is abstract')
268
188
 
269
189
    def abspath(self, name):
270
 
        """Return absolute filename for something in the branch"""
271
 
        return os.path.join(self.base, name)
272
 
 
273
 
    def relpath(self, path):
274
 
        """Return path relative to this branch of something inside it.
275
 
 
276
 
        Raises an error if path is not in this branch."""
277
 
        return _relpath(self.base, path)
 
190
        """Return absolute filename for something in the branch
 
191
        
 
192
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
 
193
        method and not a tree method.
 
194
        """
 
195
        raise NotImplementedError('abspath is abstract')
278
196
 
279
197
    def controlfilename(self, file_or_path):
280
198
        """Return location relative to branch."""
281
 
        if isinstance(file_or_path, basestring):
282
 
            file_or_path = [file_or_path]
283
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
284
 
 
 
199
        raise NotImplementedError('controlfilename is abstract')
285
200
 
286
201
    def controlfile(self, file_or_path, mode='r'):
287
202
        """Open a control file for this branch.
294
209
        Controlfiles should almost never be opened in write mode but
295
210
        rather should be atomically copied and replaced using atomicfile.
296
211
        """
297
 
 
298
 
        fn = self.controlfilename(file_or_path)
299
 
 
300
 
        if mode == 'rb' or mode == 'wb':
301
 
            return file(fn, mode)
302
 
        elif mode == 'r' or mode == 'w':
303
 
            # open in binary mode anyhow so there's no newline translation;
304
 
            # codecs uses line buffering by default; don't want that.
305
 
            import codecs
306
 
            return codecs.open(fn, mode + 'b', 'utf-8',
307
 
                               buffering=60000)
308
 
        else:
309
 
            raise BzrError("invalid controlfile mode %r" % mode)
310
 
 
311
 
    def _make_control(self):
312
 
        os.mkdir(self.controlfilename([]))
313
 
        self.controlfile('README', 'w').write(
314
 
            "This is a Bazaar-NG control directory.\n"
315
 
            "Do not change any files in this directory.\n")
316
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
317
 
        for d in ('text-store', 'revision-store',
318
 
                  'weaves'):
319
 
            os.mkdir(self.controlfilename(d))
320
 
        for f in ('revision-history', 'merged-patches',
321
 
                  'pending-merged-patches', 'branch-name',
322
 
                  'branch-lock',
323
 
                  'pending-merges'):
324
 
            self.controlfile(f, 'w').write('')
325
 
        mutter('created control directory in ' + self.base)
326
 
 
327
 
        # if we want per-tree root ids then this is the place to set
328
 
        # them; they're not needed for now and so ommitted for
329
 
        # simplicity.
330
 
        f = self.controlfile('inventory','w')
331
 
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
332
 
        
333
 
 
334
 
 
335
 
    def _check_format(self):
336
 
        """Check this branch format is supported.
337
 
 
338
 
        The format level is stored, as an integer, in
339
 
        self._branch_format for code that needs to check it later.
340
 
 
341
 
        In the future, we might need different in-memory Branch
342
 
        classes to support downlevel branches.  But not yet.
343
 
        """
344
 
        fmt = self.controlfile('branch-format', 'r').read()
345
 
        if fmt == BZR_BRANCH_FORMAT_5:
346
 
            self._branch_format = 5
347
 
        else:
348
 
            raise BzrError('sorry, branch format "%s" not supported; ' 
349
 
                           'use a different bzr version, '
350
 
                           'or run "bzr upgrade", '
351
 
                           'or remove the .bzr directory and "bzr init" again'
352
 
                           % fmt.rstrip('\n\r'))
 
212
        raise NotImplementedError('controlfile is abstract')
 
213
 
 
214
    def put_controlfile(self, path, f, encode=True):
 
215
        """Write an entry as a controlfile.
 
216
 
 
217
        :param path: The path to put the file, relative to the .bzr control
 
218
                     directory
 
219
        :param f: A file-like or string object whose contents should be copied.
 
220
        :param encode:  If true, encode the contents as utf-8
 
221
        """
 
222
        raise NotImplementedError('put_controlfile is abstract')
 
223
 
 
224
    def put_controlfiles(self, files, encode=True):
 
225
        """Write several entries as controlfiles.
 
226
 
 
227
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
228
                      underneath the bzr control directory
 
229
        :param encode:  If true, encode the contents as utf-8
 
230
        """
 
231
        raise NotImplementedError('put_controlfiles is abstract')
353
232
 
354
233
    def get_root_id(self):
355
234
        """Return the id of this branches root"""
356
 
        inv = self.read_working_inventory()
357
 
        return inv.root.file_id
 
235
        raise NotImplementedError('get_root_id is abstract')
358
236
 
359
237
    def set_root_id(self, file_id):
360
 
        inv = self.read_working_inventory()
361
 
        orig_root_id = inv.root.file_id
362
 
        del inv._byid[inv.root.file_id]
363
 
        inv.root.file_id = file_id
364
 
        inv._byid[inv.root.file_id] = inv.root
365
 
        for fid in inv:
366
 
            entry = inv[fid]
367
 
            if entry.parent_id in (None, orig_root_id):
368
 
                entry.parent_id = inv.root.file_id
369
 
        self._write_inventory(inv)
370
 
 
371
 
    def read_working_inventory(self):
372
 
        """Read the working inventory."""
373
 
        self.lock_read()
374
 
        try:
375
 
            # ElementTree does its own conversion from UTF-8, so open in
376
 
            # binary.
377
 
            f = self.controlfile('inventory', 'rb')
378
 
            return bzrlib.xml5.serializer_v5.read_inventory(f)
379
 
        finally:
380
 
            self.unlock()
381
 
            
382
 
 
383
 
    def _write_inventory(self, inv):
384
 
        """Update the working inventory.
385
 
 
386
 
        That is to say, the inventory describing changes underway, that
387
 
        will be committed to the next revision.
388
 
        """
389
 
        from bzrlib.atomicfile import AtomicFile
390
 
        
391
 
        self.lock_write()
392
 
        try:
393
 
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
394
 
            try:
395
 
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
396
 
                f.commit()
397
 
            finally:
398
 
                f.close()
399
 
        finally:
400
 
            self.unlock()
401
 
        
402
 
        mutter('wrote working inventory')
403
 
            
404
 
 
405
 
    inventory = property(read_working_inventory, _write_inventory, None,
406
 
                         """Inventory for the working copy.""")
407
 
 
408
 
 
409
 
    def add(self, files, ids=None):
410
 
        """Make files versioned.
411
 
 
412
 
        Note that the command line normally calls smart_add instead,
413
 
        which can automatically recurse.
414
 
 
415
 
        This puts the files in the Added state, so that they will be
416
 
        recorded by the next commit.
417
 
 
418
 
        files
419
 
            List of paths to add, relative to the base of the tree.
420
 
 
421
 
        ids
422
 
            If set, use these instead of automatically generated ids.
423
 
            Must be the same length as the list of files, but may
424
 
            contain None for ids that are to be autogenerated.
425
 
 
426
 
        TODO: Perhaps have an option to add the ids even if the files do
427
 
              not (yet) exist.
428
 
 
429
 
        TODO: Perhaps yield the ids and paths as they're added.
430
 
        """
431
 
        # TODO: Re-adding a file that is removed in the working copy
432
 
        # should probably put it back with the previous ID.
433
 
        if isinstance(files, basestring):
434
 
            assert(ids is None or isinstance(ids, basestring))
435
 
            files = [files]
436
 
            if ids is not None:
437
 
                ids = [ids]
438
 
 
439
 
        if ids is None:
440
 
            ids = [None] * len(files)
441
 
        else:
442
 
            assert(len(ids) == len(files))
443
 
 
444
 
        self.lock_write()
445
 
        try:
446
 
            inv = self.read_working_inventory()
447
 
            for f,file_id in zip(files, ids):
448
 
                if is_control_file(f):
449
 
                    raise BzrError("cannot add control file %s" % quotefn(f))
450
 
 
451
 
                fp = splitpath(f)
452
 
 
453
 
                if len(fp) == 0:
454
 
                    raise BzrError("cannot add top-level %r" % f)
455
 
 
456
 
                fullpath = os.path.normpath(self.abspath(f))
457
 
 
458
 
                try:
459
 
                    kind = file_kind(fullpath)
460
 
                except OSError:
461
 
                    # maybe something better?
462
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
463
 
 
464
 
                if kind != 'file' and kind != 'directory':
465
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
466
 
 
467
 
                if file_id is None:
468
 
                    file_id = gen_file_id(f)
469
 
                inv.add_path(f, kind=kind, file_id=file_id)
470
 
 
471
 
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
472
 
 
473
 
            self._write_inventory(inv)
474
 
        finally:
475
 
            self.unlock()
476
 
            
477
 
 
478
 
    def print_file(self, file, revno):
 
238
        raise NotImplementedError('set_root_id is abstract')
 
239
 
 
240
    def print_file(self, file, revision_id):
479
241
        """Print `file` to stdout."""
480
 
        self.lock_read()
481
 
        try:
482
 
            tree = self.revision_tree(self.lookup_revision(revno))
483
 
            # use inventory as it was in that revision
484
 
            file_id = tree.inventory.path2id(file)
485
 
            if not file_id:
486
 
                raise BzrError("%r is not present in revision %s" % (file, revno))
487
 
            tree.print_file(file_id)
488
 
        finally:
489
 
            self.unlock()
490
 
 
491
 
 
492
 
    def remove(self, files, verbose=False):
493
 
        """Mark nominated files for removal from the inventory.
494
 
 
495
 
        This does not remove their text.  This does not run on 
496
 
 
497
 
        TODO: Refuse to remove modified files unless --force is given?
498
 
 
499
 
        TODO: Do something useful with directories.
500
 
 
501
 
        TODO: Should this remove the text or not?  Tough call; not
502
 
        removing may be useful and the user can just use use rm, and
503
 
        is the opposite of add.  Removing it is consistent with most
504
 
        other tools.  Maybe an option.
505
 
        """
506
 
        ## TODO: Normalize names
507
 
        ## TODO: Remove nested loops; better scalability
508
 
        if isinstance(files, basestring):
509
 
            files = [files]
510
 
 
511
 
        self.lock_write()
512
 
 
513
 
        try:
514
 
            tree = self.working_tree()
515
 
            inv = tree.inventory
516
 
 
517
 
            # do this before any modifications
518
 
            for f in files:
519
 
                fid = inv.path2id(f)
520
 
                if not fid:
521
 
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
522
 
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
523
 
                if verbose:
524
 
                    # having remove it, it must be either ignored or unknown
525
 
                    if tree.is_ignored(f):
526
 
                        new_status = 'I'
527
 
                    else:
528
 
                        new_status = '?'
529
 
                    show_status(new_status, inv[fid].kind, quotefn(f))
530
 
                del inv[fid]
531
 
 
532
 
            self._write_inventory(inv)
533
 
        finally:
534
 
            self.unlock()
535
 
 
536
 
 
537
 
    # FIXME: this doesn't need to be a branch method
538
 
    def set_inventory(self, new_inventory_list):
539
 
        from bzrlib.inventory import Inventory, InventoryEntry
540
 
        inv = Inventory(self.get_root_id())
541
 
        for path, file_id, parent, kind in new_inventory_list:
542
 
            name = os.path.basename(path)
543
 
            if name == "":
544
 
                continue
545
 
            inv.add(InventoryEntry(file_id, name, kind, parent))
546
 
        self._write_inventory(inv)
547
 
 
548
 
 
549
 
    def unknowns(self):
550
 
        """Return all unknown files.
551
 
 
552
 
        These are files in the working directory that are not versioned or
553
 
        control files or ignored.
554
 
        
555
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
556
 
        >>> list(b.unknowns())
557
 
        ['foo']
558
 
        >>> b.add('foo')
559
 
        >>> list(b.unknowns())
560
 
        []
561
 
        >>> b.remove('foo')
562
 
        >>> list(b.unknowns())
563
 
        ['foo']
564
 
        """
565
 
        return self.working_tree().unknowns()
566
 
 
 
242
        raise NotImplementedError('print_file is abstract')
567
243
 
568
244
    def append_revision(self, *revision_ids):
569
 
        from bzrlib.atomicfile import AtomicFile
570
 
 
571
 
        for revision_id in revision_ids:
572
 
            mutter("add {%s} to revision-history" % revision_id)
573
 
 
574
 
        rev_history = self.revision_history()
575
 
        rev_history.extend(revision_ids)
576
 
 
577
 
        f = AtomicFile(self.controlfilename('revision-history'))
578
 
        try:
579
 
            for rev_id in rev_history:
580
 
                print >>f, rev_id
581
 
            f.commit()
582
 
        finally:
583
 
            f.close()
584
 
 
585
 
 
586
 
    def get_revision_xml_file(self, revision_id):
587
 
        """Return XML file object for revision object."""
588
 
        if not revision_id or not isinstance(revision_id, basestring):
589
 
            raise InvalidRevisionId(revision_id)
590
 
 
591
 
        self.lock_read()
592
 
        try:
593
 
            try:
594
 
                return self.revision_store[revision_id]
595
 
            except IndexError:
596
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
597
 
        finally:
598
 
            self.unlock()
599
 
 
 
245
        raise NotImplementedError('append_revision is abstract')
 
246
 
 
247
    def set_revision_history(self, rev_history):
 
248
        raise NotImplementedError('set_revision_history is abstract')
 
249
 
 
250
    def has_revision(self, revision_id):
 
251
        """True if this branch has a copy of the revision.
 
252
 
 
253
        This does not necessarily imply the revision is merge
 
254
        or on the mainline."""
 
255
        raise NotImplementedError('has_revision is abstract')
600
256
 
601
257
    def get_revision_xml(self, revision_id):
602
 
        return self.get_revision_xml_file(revision_id).read()
603
 
 
 
258
        raise NotImplementedError('get_revision_xml is abstract')
604
259
 
605
260
    def get_revision(self, revision_id):
606
261
        """Return the Revision object for a named revision"""
607
 
        xml_file = self.get_revision_xml_file(revision_id)
608
 
 
609
 
        try:
610
 
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
611
 
        except SyntaxError, e:
612
 
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
613
 
                                         [revision_id,
614
 
                                          str(e)])
615
 
            
616
 
        assert r.revision_id == revision_id
617
 
        return r
618
 
 
 
262
        raise NotImplementedError('get_revision is abstract')
619
263
 
620
264
    def get_revision_delta(self, revno):
621
265
        """Return the delta for one revision.
638
282
 
639
283
        return compare_trees(old_tree, new_tree)
640
284
 
641
 
        
642
 
 
643
285
    def get_revision_sha1(self, revision_id):
644
286
        """Hash the stored value of a revision, and return it."""
645
 
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
646
 
 
 
287
        raise NotImplementedError('get_revision_sha1 is abstract')
647
288
 
648
289
    def get_ancestry(self, revision_id):
649
290
        """Return a list of revision-ids integrated by a revision.
 
291
        
 
292
        This currently returns a list, but the ordering is not guaranteed:
 
293
        treat it as a set.
650
294
        """
651
 
        w = self.weave_store.get_weave(ANCESTRY_FILEID)
652
 
        # strip newlines
653
 
        return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
654
 
 
655
 
 
656
 
    def get_inventory_weave(self):
657
 
        return self.weave_store.get_weave(INVENTORY_FILEID)
658
 
 
 
295
        raise NotImplementedError('get_ancestry is abstract')
659
296
 
660
297
    def get_inventory(self, revision_id):
661
298
        """Get Inventory object by hash."""
662
 
        # FIXME: The text gets passed around a lot coming from the weave.
663
 
        f = StringIO(self.get_inventory_xml(revision_id))
664
 
        return bzrlib.xml5.serializer_v5.read_inventory(f)
665
 
 
 
299
        raise NotImplementedError('get_inventory is abstract')
666
300
 
667
301
    def get_inventory_xml(self, revision_id):
668
302
        """Get inventory XML as a file object."""
669
 
        try:
670
 
            assert isinstance(revision_id, basestring), type(revision_id)
671
 
            iw = self.get_inventory_weave()
672
 
            return iw.get_text(iw.lookup(revision_id))
673
 
        except IndexError:
674
 
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
675
 
 
 
303
        raise NotImplementedError('get_inventory_xml is abstract')
676
304
 
677
305
    def get_inventory_sha1(self, revision_id):
678
 
        """Return the sha1 hash of the inventory entry
679
 
        """
680
 
        return self.get_revision(revision_id).inventory_sha1
681
 
 
 
306
        """Return the sha1 hash of the inventory entry."""
 
307
        raise NotImplementedError('get_inventory_sha1 is abstract')
682
308
 
683
309
    def get_revision_inventory(self, revision_id):
684
310
        """Return inventory of a past revision."""
685
 
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
686
 
        # must be the same as its revision, so this is trivial.
687
 
        if revision_id == None:
688
 
            return Inventory(self.get_root_id())
689
 
        else:
690
 
            return self.get_inventory(revision_id)
691
 
 
 
311
        raise NotImplementedError('get_revision_inventory is abstract')
692
312
 
693
313
    def revision_history(self):
694
 
        """Return sequence of revision hashes on to this branch.
695
 
 
696
 
        >>> ScratchBranch().revision_history()
697
 
        []
698
 
        """
699
 
        self.lock_read()
700
 
        try:
701
 
            return [l.rstrip('\r\n') for l in
702
 
                    self.controlfile('revision-history', 'r').readlines()]
703
 
        finally:
704
 
            self.unlock()
705
 
 
706
 
 
707
 
    def common_ancestor(self, other, self_revno=None, other_revno=None):
708
 
        """
709
 
        >>> import commit
710
 
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
711
 
        >>> sb.common_ancestor(sb) == (None, None)
712
 
        True
713
 
        >>> commit.commit(sb, "Committing first revision")
714
 
        >>> sb.common_ancestor(sb)[0]
715
 
        1
716
 
        >>> clone = sb.clone()
717
 
        >>> commit.commit(sb, "Committing second revision")
718
 
        >>> sb.common_ancestor(sb)[0]
719
 
        2
720
 
        >>> sb.common_ancestor(clone)[0]
721
 
        1
722
 
        >>> commit.commit(clone, "Committing divergent second revision")
723
 
        >>> sb.common_ancestor(clone)[0]
724
 
        1
725
 
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
726
 
        True
727
 
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
728
 
        True
729
 
        >>> clone2 = sb.clone()
730
 
        >>> sb.common_ancestor(clone2)[0]
731
 
        2
732
 
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
733
 
        1
734
 
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
735
 
        1
736
 
        """
737
 
        my_history = self.revision_history()
738
 
        other_history = other.revision_history()
739
 
        if self_revno is None:
740
 
            self_revno = len(my_history)
741
 
        if other_revno is None:
742
 
            other_revno = len(other_history)
743
 
        indices = range(min((self_revno, other_revno)))
744
 
        indices.reverse()
745
 
        for r in indices:
746
 
            if my_history[r] == other_history[r]:
747
 
                return r+1, my_history[r]
748
 
        return None, None
749
 
 
 
314
        """Return sequence of revision hashes on to this branch."""
 
315
        raise NotImplementedError('revision_history is abstract')
750
316
 
751
317
    def revno(self):
752
318
        """Return current revision number for this branch.
756
322
        """
757
323
        return len(self.revision_history())
758
324
 
759
 
 
760
 
    def last_patch(self):
761
 
        """Return last patch hash, or None if no history.
762
 
        """
 
325
    def last_revision(self):
 
326
        """Return last patch hash, or None if no history."""
763
327
        ph = self.revision_history()
764
328
        if ph:
765
329
            return ph[-1]
766
330
        else:
767
331
            return None
768
332
 
769
 
 
770
333
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
771
 
        """
 
334
        """Return a list of new revisions that would perfectly fit.
 
335
        
772
336
        If self and other have not diverged, return a list of the revisions
773
337
        present in other, but missing from self.
774
338
 
792
356
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
793
357
        >>> br1.missing_revisions(br2)
794
358
        Traceback (most recent call last):
795
 
        DivergedBranches: These branches have diverged.
 
359
        DivergedBranches: These branches have diverged.  Try merge.
796
360
        """
797
361
        self_history = self.revision_history()
798
362
        self_len = len(self_history)
805
369
 
806
370
        if stop_revision is None:
807
371
            stop_revision = other_len
808
 
        elif stop_revision > other_len:
809
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
810
 
        
 
372
        else:
 
373
            assert isinstance(stop_revision, int)
 
374
            if stop_revision > other_len:
 
375
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
811
376
        return other_history[self_len:stop_revision]
812
 
 
813
 
 
 
377
    
814
378
    def update_revisions(self, other, stop_revision=None):
815
 
        """Pull in all new revisions from other branch.
816
 
        """
817
 
        from bzrlib.fetch import greedy_fetch
818
 
 
819
 
        pb = bzrlib.ui.ui_factory.progress_bar()
820
 
        pb.update('comparing histories')
821
 
 
822
 
        revision_ids = self.missing_revisions(other, stop_revision)
823
 
 
824
 
        if len(revision_ids) > 0:
825
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
826
 
        else:
827
 
            count = 0
828
 
        self.append_revision(*revision_ids)
829
 
        ## note("Added %d revisions." % count)
830
 
        pb.clear()
831
 
 
832
 
 
833
 
    def commit(self, *args, **kw):
834
 
        from bzrlib.commit import Commit
835
 
        Commit().commit(self, *args, **kw)
 
379
        """Pull in new perfect-fit revisions."""
 
380
        raise NotImplementedError('update_revisions is abstract')
 
381
 
 
382
    def pullable_revisions(self, other, stop_revision):
 
383
        raise NotImplementedError('pullable_revisions is abstract')
836
384
        
837
 
 
838
 
    def lookup_revision(self, revision):
839
 
        """Return the revision identifier for a given revision information."""
840
 
        revno, info = self._get_revision_info(revision)
841
 
        return info
842
 
 
843
 
 
844
385
    def revision_id_to_revno(self, revision_id):
845
386
        """Given a revision id, return its revno"""
 
387
        if revision_id is None:
 
388
            return 0
846
389
        history = self.revision_history()
847
390
        try:
848
391
            return history.index(revision_id) + 1
849
392
        except ValueError:
850
393
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
851
394
 
852
 
 
853
 
    def get_revision_info(self, revision):
854
 
        """Return (revno, revision id) for revision identifier.
855
 
 
856
 
        revision can be an integer, in which case it is assumed to be revno (though
857
 
            this will translate negative values into positive ones)
858
 
        revision can also be a string, in which case it is parsed for something like
859
 
            'date:' or 'revid:' etc.
860
 
        """
861
 
        revno, rev_id = self._get_revision_info(revision)
862
 
        if revno is None:
863
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
864
 
        return revno, rev_id
865
 
 
866
395
    def get_rev_id(self, revno, history=None):
867
396
        """Find the revision id of the specified revno."""
868
397
        if revno == 0:
873
402
            raise bzrlib.errors.NoSuchRevision(self, revno)
874
403
        return history[revno - 1]
875
404
 
876
 
    def _get_revision_info(self, revision):
877
 
        """Return (revno, revision id) for revision specifier.
878
 
 
879
 
        revision can be an integer, in which case it is assumed to be revno
880
 
        (though this will translate negative values into positive ones)
881
 
        revision can also be a string, in which case it is parsed for something
882
 
        like 'date:' or 'revid:' etc.
883
 
 
884
 
        A revid is always returned.  If it is None, the specifier referred to
885
 
        the null revision.  If the revid does not occur in the revision
886
 
        history, revno will be None.
887
 
        """
888
 
        
889
 
        if revision is None:
890
 
            return 0, None
891
 
        revno = None
892
 
        try:# Convert to int if possible
893
 
            revision = int(revision)
894
 
        except ValueError:
895
 
            pass
896
 
        revs = self.revision_history()
897
 
        if isinstance(revision, int):
898
 
            if revision < 0:
899
 
                revno = len(revs) + revision + 1
900
 
            else:
901
 
                revno = revision
902
 
            rev_id = self.get_rev_id(revno, revs)
903
 
        elif isinstance(revision, basestring):
904
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
905
 
                if revision.startswith(prefix):
906
 
                    result = func(self, revs, revision)
907
 
                    if len(result) > 1:
908
 
                        revno, rev_id = result
909
 
                    else:
910
 
                        revno = result[0]
911
 
                        rev_id = self.get_rev_id(revno, revs)
912
 
                    break
913
 
            else:
914
 
                raise BzrError('No namespace registered for string: %r' %
915
 
                               revision)
916
 
        else:
917
 
            raise TypeError('Unhandled revision type %s' % revision)
918
 
 
919
 
        if revno is None:
920
 
            if rev_id is None:
921
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
922
 
        return revno, rev_id
923
 
 
924
 
    def _namespace_revno(self, revs, revision):
925
 
        """Lookup a revision by revision number"""
926
 
        assert revision.startswith('revno:')
927
 
        try:
928
 
            return (int(revision[6:]),)
929
 
        except ValueError:
930
 
            return None
931
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
932
 
 
933
 
    def _namespace_revid(self, revs, revision):
934
 
        assert revision.startswith('revid:')
935
 
        rev_id = revision[len('revid:'):]
936
 
        try:
937
 
            return revs.index(rev_id) + 1, rev_id
938
 
        except ValueError:
939
 
            return None, rev_id
940
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
941
 
 
942
 
    def _namespace_last(self, revs, revision):
943
 
        assert revision.startswith('last:')
944
 
        try:
945
 
            offset = int(revision[5:])
946
 
        except ValueError:
947
 
            return (None,)
948
 
        else:
949
 
            if offset <= 0:
950
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
951
 
            return (len(revs) - offset + 1,)
952
 
    REVISION_NAMESPACES['last:'] = _namespace_last
953
 
 
954
 
    def _namespace_tag(self, revs, revision):
955
 
        assert revision.startswith('tag:')
956
 
        raise BzrError('tag: namespace registered, but not implemented.')
957
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
958
 
 
959
 
    def _namespace_date(self, revs, revision):
960
 
        assert revision.startswith('date:')
961
 
        import datetime
962
 
        # Spec for date revisions:
963
 
        #   date:value
964
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
965
 
        #   it can also start with a '+/-/='. '+' says match the first
966
 
        #   entry after the given date. '-' is match the first entry before the date
967
 
        #   '=' is match the first entry after, but still on the given date.
968
 
        #
969
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
970
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
971
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
972
 
        #       May 13th, 2005 at 0:00
973
 
        #
974
 
        #   So the proper way of saying 'give me all entries for today' is:
975
 
        #       -r {date:+today}:{date:-tomorrow}
976
 
        #   The default is '=' when not supplied
977
 
        val = revision[5:]
978
 
        match_style = '='
979
 
        if val[:1] in ('+', '-', '='):
980
 
            match_style = val[:1]
981
 
            val = val[1:]
982
 
 
983
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
984
 
        if val.lower() == 'yesterday':
985
 
            dt = today - datetime.timedelta(days=1)
986
 
        elif val.lower() == 'today':
987
 
            dt = today
988
 
        elif val.lower() == 'tomorrow':
989
 
            dt = today + datetime.timedelta(days=1)
990
 
        else:
991
 
            import re
992
 
            # This should be done outside the function to avoid recompiling it.
993
 
            _date_re = re.compile(
994
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
995
 
                    r'(,|T)?\s*'
996
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
997
 
                )
998
 
            m = _date_re.match(val)
999
 
            if not m or (not m.group('date') and not m.group('time')):
1000
 
                raise BzrError('Invalid revision date %r' % revision)
1001
 
 
1002
 
            if m.group('date'):
1003
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
1004
 
            else:
1005
 
                year, month, day = today.year, today.month, today.day
1006
 
            if m.group('time'):
1007
 
                hour = int(m.group('hour'))
1008
 
                minute = int(m.group('minute'))
1009
 
                if m.group('second'):
1010
 
                    second = int(m.group('second'))
1011
 
                else:
1012
 
                    second = 0
1013
 
            else:
1014
 
                hour, minute, second = 0,0,0
1015
 
 
1016
 
            dt = datetime.datetime(year=year, month=month, day=day,
1017
 
                    hour=hour, minute=minute, second=second)
1018
 
        first = dt
1019
 
        last = None
1020
 
        reversed = False
1021
 
        if match_style == '-':
1022
 
            reversed = True
1023
 
        elif match_style == '=':
1024
 
            last = dt + datetime.timedelta(days=1)
1025
 
 
1026
 
        if reversed:
1027
 
            for i in range(len(revs)-1, -1, -1):
1028
 
                r = self.get_revision(revs[i])
1029
 
                # TODO: Handle timezone.
1030
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1031
 
                if first >= dt and (last is None or dt >= last):
1032
 
                    return (i+1,)
1033
 
        else:
1034
 
            for i in range(len(revs)):
1035
 
                r = self.get_revision(revs[i])
1036
 
                # TODO: Handle timezone.
1037
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1038
 
                if first <= dt and (last is None or dt <= last):
1039
 
                    return (i+1,)
1040
 
    REVISION_NAMESPACES['date:'] = _namespace_date
1041
 
 
1042
405
    def revision_tree(self, revision_id):
1043
406
        """Return Tree for a revision on this branch.
1044
407
 
1045
408
        `revision_id` may be None for the null revision, in which case
1046
409
        an `EmptyTree` is returned."""
1047
 
        # TODO: refactor this to use an existing revision object
1048
 
        # so we don't need to read it in twice.
1049
 
        if revision_id == None:
1050
 
            return EmptyTree()
1051
 
        else:
1052
 
            inv = self.get_revision_inventory(revision_id)
1053
 
            return RevisionTree(self.weave_store, inv, revision_id)
1054
 
 
 
410
        raise NotImplementedError('revision_tree is abstract')
1055
411
 
1056
412
    def working_tree(self):
1057
 
        """Return a `Tree` for the working copy."""
1058
 
        from workingtree import WorkingTree
1059
 
        return WorkingTree(self.base, self.read_working_inventory())
 
413
        """Return a `Tree` for the working copy if this is a local branch."""
 
414
        raise NotImplementedError('working_tree is abstract')
1060
415
 
 
416
    def pull(self, source, overwrite=False):
 
417
        raise NotImplementedError('pull is abstract')
1061
418
 
1062
419
    def basis_tree(self):
1063
420
        """Return `Tree` object for last revision.
1064
421
 
1065
422
        If there are no revisions yet, return an `EmptyTree`.
1066
423
        """
1067
 
        return self.revision_tree(self.last_patch())
1068
 
 
 
424
        return self.revision_tree(self.last_revision())
1069
425
 
1070
426
    def rename_one(self, from_rel, to_rel):
1071
427
        """Rename one file.
1072
428
 
1073
429
        This can change the directory or the filename or both.
1074
430
        """
1075
 
        self.lock_write()
1076
 
        try:
1077
 
            tree = self.working_tree()
1078
 
            inv = tree.inventory
1079
 
            if not tree.has_filename(from_rel):
1080
 
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1081
 
            if tree.has_filename(to_rel):
1082
 
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
1083
 
 
1084
 
            file_id = inv.path2id(from_rel)
1085
 
            if file_id == None:
1086
 
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1087
 
 
1088
 
            if inv.path2id(to_rel):
1089
 
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1090
 
 
1091
 
            to_dir, to_tail = os.path.split(to_rel)
1092
 
            to_dir_id = inv.path2id(to_dir)
1093
 
            if to_dir_id == None and to_dir != '':
1094
 
                raise BzrError("can't determine destination directory id for %r" % to_dir)
1095
 
 
1096
 
            mutter("rename_one:")
1097
 
            mutter("  file_id    {%s}" % file_id)
1098
 
            mutter("  from_rel   %r" % from_rel)
1099
 
            mutter("  to_rel     %r" % to_rel)
1100
 
            mutter("  to_dir     %r" % to_dir)
1101
 
            mutter("  to_dir_id  {%s}" % to_dir_id)
1102
 
 
1103
 
            inv.rename(file_id, to_dir_id, to_tail)
1104
 
 
1105
 
            from_abs = self.abspath(from_rel)
1106
 
            to_abs = self.abspath(to_rel)
1107
 
            try:
1108
 
                os.rename(from_abs, to_abs)
1109
 
            except OSError, e:
1110
 
                raise BzrError("failed to rename %r to %r: %s"
1111
 
                        % (from_abs, to_abs, e[1]),
1112
 
                        ["rename rolled back"])
1113
 
 
1114
 
            self._write_inventory(inv)
1115
 
        finally:
1116
 
            self.unlock()
1117
 
 
 
431
        raise NotImplementedError('rename_one is abstract')
1118
432
 
1119
433
    def move(self, from_paths, to_name):
1120
434
        """Rename files.
1130
444
        This returns a list of (from_path, to_path) pairs for each
1131
445
        entry that is moved.
1132
446
        """
1133
 
        result = []
1134
 
        self.lock_write()
1135
 
        try:
1136
 
            ## TODO: Option to move IDs only
1137
 
            assert not isinstance(from_paths, basestring)
1138
 
            tree = self.working_tree()
1139
 
            inv = tree.inventory
1140
 
            to_abs = self.abspath(to_name)
1141
 
            if not isdir(to_abs):
1142
 
                raise BzrError("destination %r is not a directory" % to_abs)
1143
 
            if not tree.has_filename(to_name):
1144
 
                raise BzrError("destination %r not in working directory" % to_abs)
1145
 
            to_dir_id = inv.path2id(to_name)
1146
 
            if to_dir_id == None and to_name != '':
1147
 
                raise BzrError("destination %r is not a versioned directory" % to_name)
1148
 
            to_dir_ie = inv[to_dir_id]
1149
 
            if to_dir_ie.kind not in ('directory', 'root_directory'):
1150
 
                raise BzrError("destination %r is not a directory" % to_abs)
1151
 
 
1152
 
            to_idpath = inv.get_idpath(to_dir_id)
1153
 
 
1154
 
            for f in from_paths:
1155
 
                if not tree.has_filename(f):
1156
 
                    raise BzrError("%r does not exist in working tree" % f)
1157
 
                f_id = inv.path2id(f)
1158
 
                if f_id == None:
1159
 
                    raise BzrError("%r is not versioned" % f)
1160
 
                name_tail = splitpath(f)[-1]
1161
 
                dest_path = appendpath(to_name, name_tail)
1162
 
                if tree.has_filename(dest_path):
1163
 
                    raise BzrError("destination %r already exists" % dest_path)
1164
 
                if f_id in to_idpath:
1165
 
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
1166
 
 
1167
 
            # OK, so there's a race here, it's possible that someone will
1168
 
            # create a file in this interval and then the rename might be
1169
 
            # left half-done.  But we should have caught most problems.
1170
 
 
1171
 
            for f in from_paths:
1172
 
                name_tail = splitpath(f)[-1]
1173
 
                dest_path = appendpath(to_name, name_tail)
1174
 
                result.append((f, dest_path))
1175
 
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1176
 
                try:
1177
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
1178
 
                except OSError, e:
1179
 
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1180
 
                            ["rename rolled back"])
1181
 
 
1182
 
            self._write_inventory(inv)
1183
 
        finally:
1184
 
            self.unlock()
1185
 
 
1186
 
        return result
1187
 
 
1188
 
 
1189
 
    def revert(self, filenames, old_tree=None, backups=True):
1190
 
        """Restore selected files to the versions from a previous tree.
1191
 
 
1192
 
        backups
1193
 
            If true (default) backups are made of files before
1194
 
            they're renamed.
1195
 
        """
1196
 
        from bzrlib.errors import NotVersionedError, BzrError
1197
 
        from bzrlib.atomicfile import AtomicFile
1198
 
        from bzrlib.osutils import backup_file
1199
 
        
1200
 
        inv = self.read_working_inventory()
1201
 
        if old_tree is None:
1202
 
            old_tree = self.basis_tree()
1203
 
        old_inv = old_tree.inventory
1204
 
 
1205
 
        nids = []
1206
 
        for fn in filenames:
1207
 
            file_id = inv.path2id(fn)
1208
 
            if not file_id:
1209
 
                raise NotVersionedError("not a versioned file", fn)
1210
 
            if not old_inv.has_id(file_id):
1211
 
                raise BzrError("file not present in old tree", fn, file_id)
1212
 
            nids.append((fn, file_id))
1213
 
            
1214
 
        # TODO: Rename back if it was previously at a different location
1215
 
 
1216
 
        # TODO: If given a directory, restore the entire contents from
1217
 
        # the previous version.
1218
 
 
1219
 
        # TODO: Make a backup to a temporary file.
1220
 
 
1221
 
        # TODO: If the file previously didn't exist, delete it?
1222
 
        for fn, file_id in nids:
1223
 
            backup_file(fn)
1224
 
            
1225
 
            f = AtomicFile(fn, 'wb')
1226
 
            try:
1227
 
                f.write(old_tree.get_file(file_id).read())
1228
 
                f.commit()
1229
 
            finally:
1230
 
                f.close()
1231
 
 
1232
 
 
1233
 
    def pending_merges(self):
1234
 
        """Return a list of pending merges.
1235
 
 
1236
 
        These are revisions that have been merged into the working
1237
 
        directory but not yet committed.
1238
 
        """
1239
 
        cfn = self.controlfilename('pending-merges')
1240
 
        if not os.path.exists(cfn):
1241
 
            return []
1242
 
        p = []
1243
 
        for l in self.controlfile('pending-merges', 'r').readlines():
1244
 
            p.append(l.rstrip('\n'))
1245
 
        return p
1246
 
 
1247
 
 
1248
 
    def add_pending_merge(self, revision_id):
1249
 
        from bzrlib.revision import validate_revision_id
1250
 
 
1251
 
        validate_revision_id(revision_id)
1252
 
 
1253
 
        p = self.pending_merges()
1254
 
        if revision_id in p:
1255
 
            return
1256
 
        p.append(revision_id)
1257
 
        self.set_pending_merges(p)
1258
 
 
1259
 
 
1260
 
    def set_pending_merges(self, rev_list):
1261
 
        from bzrlib.atomicfile import AtomicFile
1262
 
        self.lock_write()
1263
 
        try:
1264
 
            f = AtomicFile(self.controlfilename('pending-merges'))
1265
 
            try:
1266
 
                for l in rev_list:
1267
 
                    print >>f, l
1268
 
                f.commit()
1269
 
            finally:
1270
 
                f.close()
1271
 
        finally:
1272
 
            self.unlock()
1273
 
 
 
447
        raise NotImplementedError('move is abstract')
1274
448
 
1275
449
    def get_parent(self):
1276
450
        """Return the parent location of the branch.
1279
453
        pattern is that the user can override it by specifying a
1280
454
        location.
1281
455
        """
1282
 
        import errno
1283
 
        _locs = ['parent', 'pull', 'x-pull']
1284
 
        for l in _locs:
1285
 
            try:
1286
 
                return self.controlfile(l, 'r').read().strip('\n')
1287
 
            except IOError, e:
1288
 
                if e.errno != errno.ENOENT:
1289
 
                    raise
1290
 
        return None
1291
 
 
 
456
        raise NotImplementedError('get_parent is abstract')
 
457
 
 
458
    def get_push_location(self):
 
459
        """Return the None or the location to push this branch to."""
 
460
        raise NotImplementedError('get_push_location is abstract')
 
461
 
 
462
    def set_push_location(self, location):
 
463
        """Set a new push location for this branch."""
 
464
        raise NotImplementedError('set_push_location is abstract')
1292
465
 
1293
466
    def set_parent(self, url):
1294
 
        # TODO: Maybe delete old location files?
1295
 
        from bzrlib.atomicfile import AtomicFile
1296
 
        self.lock_write()
1297
 
        try:
1298
 
            f = AtomicFile(self.controlfilename('parent'))
1299
 
            try:
1300
 
                f.write(url + '\n')
1301
 
                f.commit()
1302
 
            finally:
1303
 
                f.close()
1304
 
        finally:
1305
 
            self.unlock()
 
467
        raise NotImplementedError('set_parent is abstract')
1306
468
 
1307
469
    def check_revno(self, revno):
1308
470
        """\
1320
482
        if revno < 1 or revno > self.revno():
1321
483
            raise InvalidRevisionNumber(revno)
1322
484
        
1323
 
        
1324
 
 
1325
 
 
1326
 
class ScratchBranch(Branch):
 
485
    def sign_revision(self, revision_id, gpg_strategy):
 
486
        raise NotImplementedError('sign_revision is abstract')
 
487
 
 
488
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
489
        raise NotImplementedError('store_revision_signature is abstract')
 
490
 
 
491
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
492
        """ This function returns the file_id(s) involved in the
 
493
            changes between the from_revid revision and the to_revid
 
494
            revision
 
495
        """
 
496
        raise NotImplementedError('fileid_involved_between_revs is abstract')
 
497
 
 
498
    def fileid_involved(self, last_revid=None):
 
499
        """ This function returns the file_id(s) involved in the
 
500
            changes up to the revision last_revid
 
501
            If no parametr is passed, then all file_id[s] present in the
 
502
            repository are returned
 
503
        """
 
504
        raise NotImplementedError('fileid_involved is abstract')
 
505
 
 
506
    def fileid_involved_by_set(self, changes):
 
507
        """ This function returns the file_id(s) involved in the
 
508
            changes present in the set 'changes'
 
509
        """
 
510
        raise NotImplementedError('fileid_involved_by_set is abstract')
 
511
 
 
512
class BzrBranch(Branch):
 
513
    """A branch stored in the actual filesystem.
 
514
 
 
515
    Note that it's "local" in the context of the filesystem; it doesn't
 
516
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
517
    it's writable, and can be accessed via the normal filesystem API.
 
518
 
 
519
    _lock_mode
 
520
        None, or 'r' or 'w'
 
521
 
 
522
    _lock_count
 
523
        If _lock_mode is true, a positive count of the number of times the
 
524
        lock has been taken.
 
525
 
 
526
    _lock
 
527
        Lock object from bzrlib.lock.
 
528
    """
 
529
    # We actually expect this class to be somewhat short-lived; part of its
 
530
    # purpose is to try to isolate what bits of the branch logic are tied to
 
531
    # filesystem access, so that in a later step, we can extricate them to
 
532
    # a separarte ("storage") class.
 
533
    _lock_mode = None
 
534
    _lock_count = None
 
535
    _lock = None
 
536
    _inventory_weave = None
 
537
    # If set to False (by a plugin, etc) BzrBranch will not set the
 
538
    # mode on created files or directories
 
539
    _set_file_mode = True
 
540
    _set_dir_mode = True
 
541
    
 
542
    # Map some sort of prefix into a namespace
 
543
    # stuff like "revno:10", "revid:", etc.
 
544
    # This should match a prefix with a function which accepts
 
545
    REVISION_NAMESPACES = {}
 
546
 
 
547
    def push_stores(self, branch_to):
 
548
        """See Branch.push_stores."""
 
549
        if (self._branch_format != branch_to._branch_format
 
550
            or self._branch_format != 4):
 
551
            from bzrlib.fetch import greedy_fetch
 
552
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
 
553
                   self, self._branch_format, branch_to, branch_to._branch_format)
 
554
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
555
                         revision=self.last_revision())
 
556
            return
 
557
 
 
558
        store_pairs = ((self.text_store,      branch_to.text_store),
 
559
                       (self.inventory_store, branch_to.inventory_store),
 
560
                       (self.revision_store,  branch_to.revision_store))
 
561
        try:
 
562
            for from_store, to_store in store_pairs: 
 
563
                copy_all(from_store, to_store)
 
564
        except UnlistableStore:
 
565
            raise UnlistableBranch(from_store)
 
566
 
 
567
    def __init__(self, transport, init=False,
 
568
                 relax_version_check=False):
 
569
        """Create new branch object at a particular location.
 
570
 
 
571
        transport -- A Transport object, defining how to access files.
 
572
        
 
573
        init -- If True, create new control files in a previously
 
574
             unversioned directory.  If False, the branch must already
 
575
             be versioned.
 
576
 
 
577
        relax_version_check -- If true, the usual check for the branch
 
578
            version is not applied.  This is intended only for
 
579
            upgrade/recovery type use; it's not guaranteed that
 
580
            all operations will work on old format branches.
 
581
 
 
582
        In the test suite, creation of new trees is tested using the
 
583
        `ScratchBranch` class.
 
584
        """
 
585
        assert isinstance(transport, Transport), \
 
586
            "%r is not a Transport" % transport
 
587
        self._transport = transport
 
588
        if init:
 
589
            self._make_control()
 
590
        self._check_format(relax_version_check)
 
591
        self._find_modes()
 
592
 
 
593
        def get_store(name, compressed=True, prefixed=False):
 
594
            relpath = self._rel_controlfilename(unicode(name))
 
595
            store = TextStore(self._transport.clone(relpath),
 
596
                              dir_mode=self._dir_mode,
 
597
                              file_mode=self._file_mode,
 
598
                              prefixed=prefixed,
 
599
                              compressed=compressed)
 
600
            return store
 
601
 
 
602
        def get_weave(name, prefixed=False):
 
603
            relpath = self._rel_controlfilename(unicode(name))
 
604
            ws = WeaveStore(self._transport.clone(relpath),
 
605
                            prefixed=prefixed,
 
606
                            dir_mode=self._dir_mode,
 
607
                            file_mode=self._file_mode)
 
608
            if self._transport.should_cache():
 
609
                ws.enable_cache = True
 
610
            return ws
 
611
 
 
612
        if self._branch_format == 4:
 
613
            self.inventory_store = get_store('inventory-store')
 
614
            self.text_store = get_store('text-store')
 
615
            self.revision_store = get_store('revision-store')
 
616
        elif self._branch_format == 5:
 
617
            self.control_weaves = get_weave(u'')
 
618
            self.weave_store = get_weave(u'weaves')
 
619
            self.revision_store = get_store(u'revision-store', compressed=False)
 
620
        elif self._branch_format == 6:
 
621
            self.control_weaves = get_weave(u'')
 
622
            self.weave_store = get_weave(u'weaves', prefixed=True)
 
623
            self.revision_store = get_store(u'revision-store', compressed=False,
 
624
                                            prefixed=True)
 
625
        self.revision_store.register_suffix('sig')
 
626
        self._transaction = None
 
627
 
 
628
    def __str__(self):
 
629
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
 
630
 
 
631
    __repr__ = __str__
 
632
 
 
633
    def __del__(self):
 
634
        if self._lock_mode or self._lock:
 
635
            # XXX: This should show something every time, and be suitable for
 
636
            # headless operation and embedding
 
637
            warn("branch %r was not explicitly unlocked" % self)
 
638
            self._lock.unlock()
 
639
 
 
640
        # TODO: It might be best to do this somewhere else,
 
641
        # but it is nice for a Branch object to automatically
 
642
        # cache it's information.
 
643
        # Alternatively, we could have the Transport objects cache requests
 
644
        # See the earlier discussion about how major objects (like Branch)
 
645
        # should never expect their __del__ function to run.
 
646
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
647
            try:
 
648
                shutil.rmtree(self.cache_root)
 
649
            except:
 
650
                pass
 
651
            self.cache_root = None
 
652
 
 
653
    def _get_base(self):
 
654
        if self._transport:
 
655
            return self._transport.base
 
656
        return None
 
657
 
 
658
    base = property(_get_base, doc="The URL for the root of this branch.")
 
659
 
 
660
    def _finish_transaction(self):
 
661
        """Exit the current transaction."""
 
662
        if self._transaction is None:
 
663
            raise errors.LockError('Branch %s is not in a transaction' %
 
664
                                   self)
 
665
        transaction = self._transaction
 
666
        self._transaction = None
 
667
        transaction.finish()
 
668
 
 
669
    def get_transaction(self):
 
670
        """See Branch.get_transaction."""
 
671
        if self._transaction is None:
 
672
            return transactions.PassThroughTransaction()
 
673
        else:
 
674
            return self._transaction
 
675
 
 
676
    def _set_transaction(self, new_transaction):
 
677
        """Set a new active transaction."""
 
678
        if self._transaction is not None:
 
679
            raise errors.LockError('Branch %s is in a transaction already.' %
 
680
                                   self)
 
681
        self._transaction = new_transaction
 
682
 
 
683
    def lock_write(self):
 
684
        #mutter("lock write: %s (%s)", self, self._lock_count)
 
685
        # TODO: Upgrade locking to support using a Transport,
 
686
        # and potentially a remote locking protocol
 
687
        if self._lock_mode:
 
688
            if self._lock_mode != 'w':
 
689
                raise LockError("can't upgrade to a write lock from %r" %
 
690
                                self._lock_mode)
 
691
            self._lock_count += 1
 
692
        else:
 
693
            self._lock = self._transport.lock_write(
 
694
                    self._rel_controlfilename('branch-lock'))
 
695
            self._lock_mode = 'w'
 
696
            self._lock_count = 1
 
697
            self._set_transaction(transactions.PassThroughTransaction())
 
698
 
 
699
    def lock_read(self):
 
700
        #mutter("lock read: %s (%s)", self, self._lock_count)
 
701
        if self._lock_mode:
 
702
            assert self._lock_mode in ('r', 'w'), \
 
703
                   "invalid lock mode %r" % self._lock_mode
 
704
            self._lock_count += 1
 
705
        else:
 
706
            self._lock = self._transport.lock_read(
 
707
                    self._rel_controlfilename('branch-lock'))
 
708
            self._lock_mode = 'r'
 
709
            self._lock_count = 1
 
710
            self._set_transaction(transactions.ReadOnlyTransaction())
 
711
            # 5K may be excessive, but hey, its a knob.
 
712
            self.get_transaction().set_cache_size(5000)
 
713
                        
 
714
    def unlock(self):
 
715
        #mutter("unlock: %s (%s)", self, self._lock_count)
 
716
        if not self._lock_mode:
 
717
            raise LockError('branch %r is not locked' % (self))
 
718
 
 
719
        if self._lock_count > 1:
 
720
            self._lock_count -= 1
 
721
        else:
 
722
            self._finish_transaction()
 
723
            self._lock.unlock()
 
724
            self._lock = None
 
725
            self._lock_mode = self._lock_count = None
 
726
 
 
727
    def abspath(self, name):
 
728
        """See Branch.abspath."""
 
729
        return self._transport.abspath(name)
 
730
 
 
731
    def _rel_controlfilename(self, file_or_path):
 
732
        if not isinstance(file_or_path, basestring):
 
733
            file_or_path = u'/'.join(file_or_path)
 
734
        if file_or_path == '':
 
735
            return bzrlib.BZRDIR
 
736
        return bzrlib.transport.urlescape(bzrlib.BZRDIR + u'/' + file_or_path)
 
737
 
 
738
    def controlfilename(self, file_or_path):
 
739
        """See Branch.controlfilename."""
 
740
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
 
741
 
 
742
    def controlfile(self, file_or_path, mode='r'):
 
743
        """See Branch.controlfile."""
 
744
        import codecs
 
745
 
 
746
        relpath = self._rel_controlfilename(file_or_path)
 
747
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
748
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
749
        if mode == 'rb': 
 
750
            return self._transport.get(relpath)
 
751
        elif mode == 'wb':
 
752
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
753
        elif mode == 'r':
 
754
            # XXX: Do we really want errors='replace'?   Perhaps it should be
 
755
            # an error, or at least reported, if there's incorrectly-encoded
 
756
            # data inside a file.
 
757
            # <https://launchpad.net/products/bzr/+bug/3823>
 
758
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
759
        elif mode == 'w':
 
760
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
 
761
        else:
 
762
            raise BzrError("invalid controlfile mode %r" % mode)
 
763
 
 
764
    def put_controlfile(self, path, f, encode=True):
 
765
        """See Branch.put_controlfile."""
 
766
        self.put_controlfiles([(path, f)], encode=encode)
 
767
 
 
768
    def put_controlfiles(self, files, encode=True):
 
769
        """See Branch.put_controlfiles."""
 
770
        import codecs
 
771
        ctrl_files = []
 
772
        for path, f in files:
 
773
            if encode:
 
774
                if isinstance(f, basestring):
 
775
                    f = f.encode('utf-8', 'replace')
 
776
                else:
 
777
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
778
            path = self._rel_controlfilename(path)
 
779
            ctrl_files.append((path, f))
 
780
        self._transport.put_multi(ctrl_files, mode=self._file_mode)
 
781
 
 
782
    def _find_modes(self, path=None):
 
783
        """Determine the appropriate modes for files and directories."""
 
784
        try:
 
785
            if path is None:
 
786
                path = self._rel_controlfilename('')
 
787
            st = self._transport.stat(path)
 
788
        except errors.TransportNotPossible:
 
789
            self._dir_mode = 0755
 
790
            self._file_mode = 0644
 
791
        else:
 
792
            self._dir_mode = st.st_mode & 07777
 
793
            # Remove the sticky and execute bits for files
 
794
            self._file_mode = self._dir_mode & ~07111
 
795
        if not self._set_dir_mode:
 
796
            self._dir_mode = None
 
797
        if not self._set_file_mode:
 
798
            self._file_mode = None
 
799
 
 
800
    def _make_control(self):
 
801
        from bzrlib.inventory import Inventory
 
802
        from bzrlib.weavefile import write_weave_v5
 
803
        from bzrlib.weave import Weave
 
804
        
 
805
        # Create an empty inventory
 
806
        sio = StringIO()
 
807
        # if we want per-tree root ids then this is the place to set
 
808
        # them; they're not needed for now and so ommitted for
 
809
        # simplicity.
 
810
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
811
        empty_inv = sio.getvalue()
 
812
        sio = StringIO()
 
813
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
814
        empty_weave = sio.getvalue()
 
815
 
 
816
        cfn = self._rel_controlfilename
 
817
        # Since we don't have a .bzr directory, inherit the
 
818
        # mode from the root directory
 
819
        self._find_modes(u'.')
 
820
 
 
821
        dirs = ['', 'revision-store', 'weaves']
 
822
        files = [('README', 
 
823
            "This is a Bazaar-NG control directory.\n"
 
824
            "Do not change any files in this directory.\n"),
 
825
            ('branch-format', BZR_BRANCH_FORMAT_6),
 
826
            ('revision-history', ''),
 
827
            ('branch-name', ''),
 
828
            ('branch-lock', ''),
 
829
            ('pending-merges', ''),
 
830
            ('inventory', empty_inv),
 
831
            ('inventory.weave', empty_weave),
 
832
        ]
 
833
        self._transport.mkdir_multi([cfn(d) for d in dirs], mode=self._dir_mode)
 
834
        self.put_controlfiles(files)
 
835
        mutter('created control directory in ' + self._transport.base)
 
836
 
 
837
    def _check_format(self, relax_version_check):
 
838
        """Check this branch format is supported.
 
839
 
 
840
        The format level is stored, as an integer, in
 
841
        self._branch_format for code that needs to check it later.
 
842
 
 
843
        In the future, we might need different in-memory Branch
 
844
        classes to support downlevel branches.  But not yet.
 
845
        """
 
846
        try:
 
847
            fmt = self.controlfile('branch-format', 'r').read()
 
848
        except NoSuchFile:
 
849
            raise NotBranchError(path=self.base)
 
850
        mutter("got branch format %r", fmt)
 
851
        if fmt == BZR_BRANCH_FORMAT_6:
 
852
            self._branch_format = 6
 
853
        elif fmt == BZR_BRANCH_FORMAT_5:
 
854
            self._branch_format = 5
 
855
        elif fmt == BZR_BRANCH_FORMAT_4:
 
856
            self._branch_format = 4
 
857
 
 
858
        if (not relax_version_check
 
859
            and self._branch_format not in (5, 6)):
 
860
            raise errors.UnsupportedFormatError(
 
861
                           'sorry, branch format %r not supported' % fmt,
 
862
                           ['use a different bzr version',
 
863
                            'or remove the .bzr directory'
 
864
                            ' and "bzr init" again'])
 
865
 
 
866
    @needs_read_lock
 
867
    def get_root_id(self):
 
868
        """See Branch.get_root_id."""
 
869
        inv = self.get_inventory(self.last_revision())
 
870
        return inv.root.file_id
 
871
 
 
872
    @needs_read_lock
 
873
    def print_file(self, file, revision_id):
 
874
        """See Branch.print_file."""
 
875
        tree = self.revision_tree(revision_id)
 
876
        # use inventory as it was in that revision
 
877
        file_id = tree.inventory.path2id(file)
 
878
        if not file_id:
 
879
            try:
 
880
                revno = self.revision_id_to_revno(revision_id)
 
881
            except errors.NoSuchRevision:
 
882
                # TODO: This should not be BzrError,
 
883
                # but NoSuchFile doesn't fit either
 
884
                raise BzrError('%r is not present in revision %s' 
 
885
                                % (file, revision_id))
 
886
            else:
 
887
                raise BzrError('%r is not present in revision %s'
 
888
                                % (file, revno))
 
889
        tree.print_file(file_id)
 
890
 
 
891
    @needs_write_lock
 
892
    def append_revision(self, *revision_ids):
 
893
        """See Branch.append_revision."""
 
894
        for revision_id in revision_ids:
 
895
            mutter("add {%s} to revision-history" % revision_id)
 
896
        rev_history = self.revision_history()
 
897
        rev_history.extend(revision_ids)
 
898
        self.set_revision_history(rev_history)
 
899
 
 
900
    @needs_write_lock
 
901
    def set_revision_history(self, rev_history):
 
902
        """See Branch.set_revision_history."""
 
903
        old_revision = self.last_revision()
 
904
        new_revision = rev_history[-1]
 
905
        self.put_controlfile('revision-history', '\n'.join(rev_history))
 
906
        try:
 
907
            self.working_tree().set_last_revision(new_revision, old_revision)
 
908
        except NoWorkingTree:
 
909
            mutter('Unable to set_last_revision without a working tree.')
 
910
 
 
911
    def has_revision(self, revision_id):
 
912
        """See Branch.has_revision."""
 
913
        return (revision_id is None
 
914
                or self.revision_store.has_id(revision_id))
 
915
 
 
916
    @needs_read_lock
 
917
    def _get_revision_xml_file(self, revision_id):
 
918
        if not revision_id or not isinstance(revision_id, basestring):
 
919
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
920
        try:
 
921
            return self.revision_store.get(revision_id)
 
922
        except (IndexError, KeyError):
 
923
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
924
 
 
925
    def get_revision_xml(self, revision_id):
 
926
        """See Branch.get_revision_xml."""
 
927
        return self._get_revision_xml_file(revision_id).read()
 
928
 
 
929
    def get_revision(self, revision_id):
 
930
        """See Branch.get_revision."""
 
931
        xml_file = self._get_revision_xml_file(revision_id)
 
932
 
 
933
        try:
 
934
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
935
        except SyntaxError, e:
 
936
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
937
                                         [revision_id,
 
938
                                          str(e)])
 
939
            
 
940
        assert r.revision_id == revision_id
 
941
        return r
 
942
 
 
943
    def get_revision_sha1(self, revision_id):
 
944
        """See Branch.get_revision_sha1."""
 
945
        # In the future, revision entries will be signed. At that
 
946
        # point, it is probably best *not* to include the signature
 
947
        # in the revision hash. Because that lets you re-sign
 
948
        # the revision, (add signatures/remove signatures) and still
 
949
        # have all hash pointers stay consistent.
 
950
        # But for now, just hash the contents.
 
951
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
952
 
 
953
    def get_ancestry(self, revision_id):
 
954
        """See Branch.get_ancestry."""
 
955
        if revision_id is None:
 
956
            return [None]
 
957
        w = self._get_inventory_weave()
 
958
        return [None] + map(w.idx_to_name,
 
959
                            w.inclusions([w.lookup(revision_id)]))
 
960
 
 
961
    def _get_inventory_weave(self):
 
962
        return self.control_weaves.get_weave('inventory',
 
963
                                             self.get_transaction())
 
964
 
 
965
    def get_inventory(self, revision_id):
 
966
        """See Branch.get_inventory."""
 
967
        xml = self.get_inventory_xml(revision_id)
 
968
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
969
 
 
970
    def get_inventory_xml(self, revision_id):
 
971
        """See Branch.get_inventory_xml."""
 
972
        try:
 
973
            assert isinstance(revision_id, basestring), type(revision_id)
 
974
            iw = self._get_inventory_weave()
 
975
            return iw.get_text(iw.lookup(revision_id))
 
976
        except IndexError:
 
977
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
978
 
 
979
    def get_inventory_sha1(self, revision_id):
 
980
        """See Branch.get_inventory_sha1."""
 
981
        return self.get_revision(revision_id).inventory_sha1
 
982
 
 
983
    def get_revision_inventory(self, revision_id):
 
984
        """See Branch.get_revision_inventory."""
 
985
        # TODO: Unify this with get_inventory()
 
986
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
987
        # must be the same as its revision, so this is trivial.
 
988
        if revision_id == None:
 
989
            # This does not make sense: if there is no revision,
 
990
            # then it is the current tree inventory surely ?!
 
991
            # and thus get_root_id() is something that looks at the last
 
992
            # commit on the branch, and the get_root_id is an inventory check.
 
993
            raise NotImplementedError
 
994
            # return Inventory(self.get_root_id())
 
995
        else:
 
996
            return self.get_inventory(revision_id)
 
997
 
 
998
    @needs_read_lock
 
999
    def revision_history(self):
 
1000
        """See Branch.revision_history."""
 
1001
        transaction = self.get_transaction()
 
1002
        history = transaction.map.find_revision_history()
 
1003
        if history is not None:
 
1004
            mutter("cache hit for revision-history in %s", self)
 
1005
            return list(history)
 
1006
        history = [l.rstrip('\r\n') for l in
 
1007
                self.controlfile('revision-history', 'r').readlines()]
 
1008
        transaction.map.add_revision_history(history)
 
1009
        # this call is disabled because revision_history is 
 
1010
        # not really an object yet, and the transaction is for objects.
 
1011
        # transaction.register_clean(history, precious=True)
 
1012
        return list(history)
 
1013
 
 
1014
    def update_revisions(self, other, stop_revision=None):
 
1015
        """See Branch.update_revisions."""
 
1016
        from bzrlib.fetch import greedy_fetch
 
1017
        if stop_revision is None:
 
1018
            stop_revision = other.last_revision()
 
1019
        ### Should this be checking is_ancestor instead of revision_history?
 
1020
        if (stop_revision is not None and 
 
1021
            stop_revision in self.revision_history()):
 
1022
            return
 
1023
        greedy_fetch(to_branch=self, from_branch=other,
 
1024
                     revision=stop_revision)
 
1025
        pullable_revs = self.pullable_revisions(other, stop_revision)
 
1026
        if len(pullable_revs) > 0:
 
1027
            self.append_revision(*pullable_revs)
 
1028
 
 
1029
    def pullable_revisions(self, other, stop_revision):
 
1030
        """See Branch.pullable_revisions."""
 
1031
        other_revno = other.revision_id_to_revno(stop_revision)
 
1032
        try:
 
1033
            return self.missing_revisions(other, other_revno)
 
1034
        except DivergedBranches, e:
 
1035
            try:
 
1036
                pullable_revs = get_intervening_revisions(self.last_revision(),
 
1037
                                                          stop_revision, self)
 
1038
                assert self.last_revision() not in pullable_revs
 
1039
                return pullable_revs
 
1040
            except bzrlib.errors.NotAncestor:
 
1041
                if is_ancestor(self.last_revision(), stop_revision, self):
 
1042
                    return []
 
1043
                else:
 
1044
                    raise e
 
1045
        
 
1046
    def revision_tree(self, revision_id):
 
1047
        """See Branch.revision_tree."""
 
1048
        # TODO: refactor this to use an existing revision object
 
1049
        # so we don't need to read it in twice.
 
1050
        if revision_id == None or revision_id == NULL_REVISION:
 
1051
            return EmptyTree()
 
1052
        else:
 
1053
            inv = self.get_revision_inventory(revision_id)
 
1054
            return RevisionTree(self, inv, revision_id)
 
1055
 
 
1056
    def basis_tree(self):
 
1057
        """See Branch.basis_tree."""
 
1058
        try:
 
1059
            revision_id = self.revision_history()[-1]
 
1060
            xml = self.working_tree().read_basis_inventory(revision_id)
 
1061
            inv = bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
1062
            return RevisionTree(self, inv, revision_id)
 
1063
        except (IndexError, NoSuchFile, NoWorkingTree), e:
 
1064
            return self.revision_tree(self.last_revision())
 
1065
 
 
1066
    def working_tree(self):
 
1067
        """See Branch.working_tree."""
 
1068
        from bzrlib.workingtree import WorkingTree
 
1069
        if self._transport.base.find('://') != -1:
 
1070
            raise NoWorkingTree(self.base)
 
1071
        return WorkingTree(self.base, branch=self)
 
1072
 
 
1073
    @needs_write_lock
 
1074
    def pull(self, source, overwrite=False):
 
1075
        """See Branch.pull."""
 
1076
        source.lock_read()
 
1077
        try:
 
1078
            old_count = len(self.revision_history())
 
1079
            try:
 
1080
                self.update_revisions(source)
 
1081
            except DivergedBranches:
 
1082
                if not overwrite:
 
1083
                    raise
 
1084
            if overwrite:
 
1085
                self.set_revision_history(source.revision_history())
 
1086
            new_count = len(self.revision_history())
 
1087
            return new_count - old_count
 
1088
        finally:
 
1089
            source.unlock()
 
1090
 
 
1091
    def get_parent(self):
 
1092
        """See Branch.get_parent."""
 
1093
        import errno
 
1094
        _locs = ['parent', 'pull', 'x-pull']
 
1095
        for l in _locs:
 
1096
            try:
 
1097
                return self.controlfile(l, 'r').read().strip('\n')
 
1098
            except NoSuchFile:
 
1099
                pass
 
1100
        return None
 
1101
 
 
1102
    def get_push_location(self):
 
1103
        """See Branch.get_push_location."""
 
1104
        config = bzrlib.config.BranchConfig(self)
 
1105
        push_loc = config.get_user_option('push_location')
 
1106
        return push_loc
 
1107
 
 
1108
    def set_push_location(self, location):
 
1109
        """See Branch.set_push_location."""
 
1110
        config = bzrlib.config.LocationConfig(self.base)
 
1111
        config.set_user_option('push_location', location)
 
1112
 
 
1113
    @needs_write_lock
 
1114
    def set_parent(self, url):
 
1115
        """See Branch.set_parent."""
 
1116
        # TODO: Maybe delete old location files?
 
1117
        from bzrlib.atomicfile import AtomicFile
 
1118
        f = AtomicFile(self.controlfilename('parent'))
 
1119
        try:
 
1120
            f.write(url + '\n')
 
1121
            f.commit()
 
1122
        finally:
 
1123
            f.close()
 
1124
 
 
1125
    def tree_config(self):
 
1126
        return TreeConfig(self)
 
1127
 
 
1128
    def sign_revision(self, revision_id, gpg_strategy):
 
1129
        """See Branch.sign_revision."""
 
1130
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
 
1131
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
 
1132
 
 
1133
    @needs_write_lock
 
1134
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
1135
        """See Branch.store_revision_signature."""
 
1136
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
 
1137
                                revision_id, "sig")
 
1138
 
 
1139
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
1140
        """Find file_id(s) which are involved in the changes between revisions.
 
1141
 
 
1142
        This determines the set of revisions which are involved, and then
 
1143
        finds all file ids affected by those revisions.
 
1144
        """
 
1145
        # TODO: jam 20060119 This code assumes that w.inclusions will
 
1146
        #       always be correct. But because of the presence of ghosts
 
1147
        #       it is possible to be wrong.
 
1148
        #       One specific example from Robert Collins:
 
1149
        #       Two branches, with revisions ABC, and AD
 
1150
        #       C is a ghost merge of D.
 
1151
        #       Inclusions doesn't recognize D as an ancestor.
 
1152
        #       If D is ever merged in the future, the weave
 
1153
        #       won't be fixed, because AD never saw revision C
 
1154
        #       to cause a conflict which would force a reweave.
 
1155
        w = self._get_inventory_weave( )
 
1156
        from_set = set(w.inclusions([w.lookup(from_revid)]))
 
1157
        to_set = set(w.inclusions([w.lookup(to_revid)]))
 
1158
        included = to_set.difference(from_set)
 
1159
        changed = map(w.idx_to_name,included)
 
1160
        return self._fileid_involved_by_set(changed)
 
1161
 
 
1162
    def fileid_involved(self, last_revid=None):
 
1163
        """Find all file_ids modified in the ancestry of last_revid.
 
1164
 
 
1165
        :param last_revid: If None, last_revision() will be used.
 
1166
        """
 
1167
        w = self._get_inventory_weave( )
 
1168
        if not last_revid:
 
1169
            changed = set(w._names)
 
1170
        else:
 
1171
            included = w.inclusions([w.lookup(last_revid)])
 
1172
            changed = map(w.idx_to_name, included)
 
1173
        return self._fileid_involved_by_set(changed)
 
1174
 
 
1175
    def fileid_involved_by_set(self, changes):
 
1176
        """Find all file_ids modified by the set of revisions passed in.
 
1177
 
 
1178
        :param changes: A set() of revision ids
 
1179
        """
 
1180
        # TODO: jam 20060119 This line does *nothing*, remove it.
 
1181
        #       or better yet, change _fileid_involved_by_set so
 
1182
        #       that it takes the inventory weave, rather than
 
1183
        #       pulling it out by itself.
 
1184
        w = self._get_inventory_weave( )
 
1185
        return self._fileid_involved_by_set(changes)
 
1186
 
 
1187
    def _fileid_involved_by_set(self, changes):
 
1188
        """Find the set of file-ids affected by the set of revisions.
 
1189
 
 
1190
        :param changes: A set() of revision ids.
 
1191
        :return: A set() of file ids.
 
1192
        
 
1193
        This peaks at the Weave, interpreting each line, looking to
 
1194
        see if it mentions one of the revisions. And if so, includes
 
1195
        the file id mentioned.
 
1196
        This expects both the Weave format, and the serialization
 
1197
        to have a single line per file/directory, and to have
 
1198
        fileid="" and revision="" on that line.
 
1199
        """
 
1200
        assert self._branch_format in (5,6), \
 
1201
            "fileid_involved only supported for branches which store inventory as xml"
 
1202
 
 
1203
        w = self._get_inventory_weave( )
 
1204
        file_ids = set( )
 
1205
        for line in w._weave:
 
1206
 
 
1207
            # it is ugly, but it is due to the weave structure
 
1208
            if not isinstance(line,basestring): continue
 
1209
 
 
1210
            start = line.find('file_id="')+9
 
1211
            if start < 9: continue
 
1212
            end = line.find('"',start)
 
1213
            assert end>= 0
 
1214
            file_id = xml.sax.saxutils.unescape(line[start:end])
 
1215
 
 
1216
            # check if file_id is already present
 
1217
            if file_id in file_ids: continue
 
1218
 
 
1219
            start = line.find('revision="')+10
 
1220
            if start < 10: continue
 
1221
            end = line.find('"',start)
 
1222
            assert end>= 0
 
1223
            revision_id = xml.sax.saxutils.unescape(line[start:end])
 
1224
 
 
1225
            if revision_id in changes:
 
1226
                file_ids.add(file_id)
 
1227
 
 
1228
        return file_ids
 
1229
 
 
1230
 
 
1231
class ScratchBranch(BzrBranch):
1327
1232
    """Special test class: a branch that cleans up after itself.
1328
1233
 
1329
1234
    >>> b = ScratchBranch()
1330
1235
    >>> isdir(b.base)
1331
1236
    True
1332
1237
    >>> bd = b.base
1333
 
    >>> b.destroy()
 
1238
    >>> b._transport.__del__()
1334
1239
    >>> isdir(bd)
1335
1240
    False
1336
1241
    """
1337
 
    def __init__(self, files=[], dirs=[], base=None):
 
1242
 
 
1243
    def __init__(self, files=[], dirs=[], transport=None):
1338
1244
        """Make a test branch.
1339
1245
 
1340
1246
        This creates a temporary directory and runs init-tree in it.
1341
1247
 
1342
1248
        If any files are listed, they are created in the working copy.
1343
1249
        """
1344
 
        from tempfile import mkdtemp
1345
 
        init = False
1346
 
        if base is None:
1347
 
            base = mkdtemp()
1348
 
            init = True
1349
 
        Branch.__init__(self, base, init=init)
 
1250
        if transport is None:
 
1251
            transport = bzrlib.transport.local.ScratchTransport()
 
1252
            super(ScratchBranch, self).__init__(transport, init=True)
 
1253
        else:
 
1254
            super(ScratchBranch, self).__init__(transport)
 
1255
 
1350
1256
        for d in dirs:
1351
 
            os.mkdir(self.abspath(d))
 
1257
            self._transport.mkdir(d)
1352
1258
            
1353
1259
        for f in files:
1354
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1260
            self._transport.put(f, 'content of %s' % f)
1355
1261
 
1356
1262
 
1357
1263
    def clone(self):
1358
1264
        """
1359
1265
        >>> orig = ScratchBranch(files=["file1", "file2"])
1360
1266
        >>> clone = orig.clone()
1361
 
        >>> os.path.samefile(orig.base, clone.base)
 
1267
        >>> if os.name != 'nt':
 
1268
        ...   os.path.samefile(orig.base, clone.base)
 
1269
        ... else:
 
1270
        ...   orig.base == clone.base
 
1271
        ...
1362
1272
        False
1363
 
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
1273
        >>> os.path.isfile(pathjoin(clone.base, "file1"))
1364
1274
        True
1365
1275
        """
1366
1276
        from shutil import copytree
1367
 
        from tempfile import mkdtemp
 
1277
        from bzrlib.osutils import mkdtemp
1368
1278
        base = mkdtemp()
1369
1279
        os.rmdir(base)
1370
1280
        copytree(self.base, base, symlinks=True)
1371
 
        return ScratchBranch(base=base)
1372
 
 
1373
 
 
1374
 
        
1375
 
    def __del__(self):
1376
 
        self.destroy()
1377
 
 
1378
 
    def destroy(self):
1379
 
        """Destroy the test branch, removing the scratch directory."""
1380
 
        from shutil import rmtree
1381
 
        try:
1382
 
            if self.base:
1383
 
                mutter("delete ScratchBranch %s" % self.base)
1384
 
                rmtree(self.base)
1385
 
        except OSError, e:
1386
 
            # Work around for shutil.rmtree failing on Windows when
1387
 
            # readonly files are encountered
1388
 
            mutter("hit exception in destroying ScratchBranch: %s" % e)
1389
 
            for root, dirs, files in os.walk(self.base, topdown=False):
1390
 
                for name in files:
1391
 
                    os.chmod(os.path.join(root, name), 0700)
1392
 
            rmtree(self.base)
1393
 
        self.base = None
1394
 
 
 
1281
        return ScratchBranch(
 
1282
            transport=bzrlib.transport.local.ScratchTransport(base))
1395
1283
    
1396
1284
 
1397
1285
######################################################################
1400
1288
 
1401
1289
def is_control_file(filename):
1402
1290
    ## FIXME: better check
1403
 
    filename = os.path.normpath(filename)
 
1291
    filename = normpath(filename)
1404
1292
    while filename != '':
1405
1293
        head, tail = os.path.split(filename)
1406
1294
        ## mutter('check %r for control file' % ((head, tail), ))
1410
1298
            break
1411
1299
        filename = head
1412
1300
    return False
1413
 
 
1414
 
 
1415
 
 
1416
 
def gen_file_id(name):
1417
 
    """Return new file id.
1418
 
 
1419
 
    This should probably generate proper UUIDs, but for the moment we
1420
 
    cope with just randomness because running uuidgen every time is
1421
 
    slow."""
1422
 
    import re
1423
 
    from binascii import hexlify
1424
 
    from time import time
1425
 
 
1426
 
    # get last component
1427
 
    idx = name.rfind('/')
1428
 
    if idx != -1:
1429
 
        name = name[idx+1 : ]
1430
 
    idx = name.rfind('\\')
1431
 
    if idx != -1:
1432
 
        name = name[idx+1 : ]
1433
 
 
1434
 
    # make it not a hidden file
1435
 
    name = name.lstrip('.')
1436
 
 
1437
 
    # remove any wierd characters; we don't escape them but rather
1438
 
    # just pull them out
1439
 
    name = re.sub(r'[^\w.]', '', name)
1440
 
 
1441
 
    s = hexlify(rand_bytes(8))
1442
 
    return '-'.join((name, compact_date(time()), s))
1443
 
 
1444
 
 
1445
 
def gen_root_id():
1446
 
    """Return a new tree-root file id."""
1447
 
    return gen_file_id('TREE_ROOT')
1448
 
 
1449
 
 
1450
 
def pull_loc(branch):
1451
 
    # TODO: Should perhaps just make attribute be 'base' in
1452
 
    # RemoteBranch and Branch?
1453
 
    if hasattr(branch, "baseurl"):
1454
 
        return branch.baseurl
1455
 
    else:
1456
 
        return branch.base
1457
 
 
1458
 
 
1459
 
def copy_branch(branch_from, to_location, revision=None):
1460
 
    """Copy branch_from into the existing directory to_location.
1461
 
 
1462
 
    revision
1463
 
        If not None, only revisions up to this point will be copied.
1464
 
        The head of the new branch will be that revision.
1465
 
 
1466
 
    to_location
1467
 
        The name of a local directory that exists but is empty.
1468
 
    """
1469
 
    from bzrlib.merge import merge
1470
 
    from bzrlib.branch import Branch
1471
 
 
1472
 
    assert isinstance(branch_from, Branch)
1473
 
    assert isinstance(to_location, basestring)
1474
 
    
1475
 
    br_to = Branch(to_location, init=True)
1476
 
    br_to.set_root_id(branch_from.get_root_id())
1477
 
    if revision is None:
1478
 
        revno = branch_from.revno()
1479
 
    else:
1480
 
        revno, rev_id = branch_from.get_revision_info(revision)
1481
 
    br_to.update_revisions(branch_from, stop_revision=revno)
1482
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1483
 
          check_clean=False, ignore_zero=True)
1484
 
    
1485
 
    from_location = pull_loc(branch_from)
1486
 
    br_to.set_parent(pull_loc(branch_from))
1487