~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-10-08 02:36:30 UTC
  • mto: (1185.25.1)
  • mto: This revision was merged to the branch mainline in revision 1425.
  • Revision ID: aaron.bentley@utoronto.ca-20051008023630-6c0325363f2daf20
Set bzr executible

Show diffs side-by-side

added added

removed removed

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