~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

- doc and todo for ignore command

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
 
from sets import Set
 
18
import sys
 
19
import os
 
20
import errno
 
21
from warnings import warn
 
22
from cStringIO import StringIO
19
23
 
20
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
 
import traceback, socket, fnmatch, difflib, time
22
 
from binascii import hexlify
23
24
 
24
25
import bzrlib
25
 
from inventory import Inventory
26
 
from trace import mutter, note
27
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
 
from inventory import InventoryEntry, Inventory
29
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
30
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
 
     joinpath, sha_string, file_kind, local_time_offset, appendpath
32
 
from store import ImmutableStore
33
 
from revision import Revision
34
 
from errors import bailout
35
 
from textui import show_status
36
 
from diff import diff_trees
37
 
 
38
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
26
from bzrlib.inventory import InventoryEntry
 
27
import bzrlib.inventory as inventory
 
28
from bzrlib.trace import mutter, note
 
29
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
 
30
                            rename, splitpath, sha_file, appendpath, 
 
31
                            file_kind)
 
32
import bzrlib.errors as errors
 
33
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
34
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
35
                           DivergedBranches, LockError, UnlistableStore,
 
36
                           UnlistableBranch, NoSuchFile)
 
37
from bzrlib.textui import show_status
 
38
from bzrlib.revision import Revision
 
39
from bzrlib.delta import compare_trees
 
40
from bzrlib.tree import EmptyTree, RevisionTree
 
41
from bzrlib.inventory import Inventory
 
42
from bzrlib.store import copy_all
 
43
from bzrlib.store.compressed_text import CompressedTextStore
 
44
from bzrlib.store.text import TextStore
 
45
from bzrlib.store.weave import WeaveStore
 
46
import bzrlib.transactions as transactions
 
47
from bzrlib.transport import Transport, get_transport
 
48
import bzrlib.xml5
 
49
import bzrlib.ui
 
50
 
 
51
 
 
52
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
53
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
54
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
39
55
## TODO: Maybe include checks for common corruption of newlines, etc?
40
56
 
41
57
 
42
 
 
43
 
def find_branch_root(f=None):
44
 
    """Find the branch root enclosing f, or pwd.
45
 
 
46
 
    It is not necessary that f exists.
 
58
# TODO: Some operations like log might retrieve the same revisions
 
59
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
60
# cache in memory to make this faster.  In general anything can be
 
61
# cached in memory between lock and unlock operations.
 
62
 
 
63
def find_branch(*ignored, **ignored_too):
 
64
    # XXX: leave this here for about one release, then remove it
 
65
    raise NotImplementedError('find_branch() is not supported anymore, '
 
66
                              'please use one of the new branch constructors')
 
67
def _relpath(base, path):
 
68
    """Return path relative to base, or raise exception.
 
69
 
 
70
    The path may be either an absolute path or a path relative to the
 
71
    current working directory.
 
72
 
 
73
    Lifted out of Branch.relpath for ease of testing.
 
74
 
 
75
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
76
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
77
    avoids that problem."""
 
78
    rp = os.path.abspath(path)
 
79
 
 
80
    s = []
 
81
    head = rp
 
82
    while len(head) >= len(base):
 
83
        if head == base:
 
84
            break
 
85
        head, tail = os.path.split(head)
 
86
        if tail:
 
87
            s.insert(0, tail)
 
88
    else:
 
89
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
90
 
 
91
    return os.sep.join(s)
 
92
        
 
93
 
 
94
def find_branch_root(t):
 
95
    """Find the branch root enclosing the transport's base.
 
96
 
 
97
    t is a Transport object.
 
98
 
 
99
    It is not necessary that the base of t exists.
47
100
 
48
101
    Basically we keep looking up until we find the control directory or
49
 
    run into the root."""
50
 
    if f is None:
51
 
        f = os.getcwd()
52
 
    elif hasattr(os.path, 'realpath'):
53
 
        f = os.path.realpath(f)
54
 
    else:
55
 
        f = os.path.abspath(f)
56
 
 
57
 
    orig_f = f
58
 
 
59
 
    last_f = f
 
102
    run into the root.  If there isn't one, raises NotBranchError.
 
103
    """
 
104
    orig_base = t.base
60
105
    while True:
61
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
62
 
            return f
63
 
        head, tail = os.path.split(f)
64
 
        if head == f:
 
106
        if t.has(bzrlib.BZRDIR):
 
107
            return t
 
108
        new_t = t.clone('..')
 
109
        if new_t.base == t.base:
65
110
            # reached the root, whatever that may be
66
 
            bailout('%r is not in a branch' % orig_f)
67
 
        f = head
68
 
    
 
111
            raise NotBranchError('%s is not in a branch' % orig_base)
 
112
        t = new_t
69
113
 
70
114
 
71
115
######################################################################
72
116
# branch objects
73
117
 
74
 
class Branch:
 
118
class Branch(object):
75
119
    """Branch holding a history of revisions.
76
120
 
77
 
    :todo: Perhaps use different stores for different classes of object,
78
 
           so that we can keep track of how much space each one uses,
79
 
           or garbage-collect them.
80
 
 
81
 
    :todo: Add a RemoteBranch subclass.  For the basic case of read-only
82
 
           HTTP access this should be very easy by, 
83
 
           just redirecting controlfile access into HTTP requests.
84
 
           We would need a RemoteStore working similarly.
85
 
 
86
 
    :todo: Keep the on-disk branch locked while the object exists.
87
 
 
88
 
    :todo: mkdir() method.
89
 
    """
90
 
    def __init__(self, base, init=False, find_root=True):
 
121
    base
 
122
        Base directory/url of the branch.
 
123
    """
 
124
    base = None
 
125
 
 
126
    def __init__(self, *ignored, **ignored_too):
 
127
        raise NotImplementedError('The Branch class is abstract')
 
128
 
 
129
    @staticmethod
 
130
    def open_downlevel(base):
 
131
        """Open a branch which may be of an old format.
 
132
        
 
133
        Only local branches are supported."""
 
134
        return _Branch(get_transport(base), relax_version_check=True)
 
135
        
 
136
    @staticmethod
 
137
    def open(base):
 
138
        """Open an existing branch, rooted at 'base' (url)"""
 
139
        t = get_transport(base)
 
140
        mutter("trying to open %r with transport %r", base, t)
 
141
        return _Branch(t)
 
142
 
 
143
    @staticmethod
 
144
    def open_containing(url):
 
145
        """Open an existing branch which contains url.
 
146
        
 
147
        This probes for a branch at url, and searches upwards from there.
 
148
        """
 
149
        t = get_transport(url)
 
150
        t = find_branch_root(t)
 
151
        return _Branch(t)
 
152
 
 
153
    @staticmethod
 
154
    def initialize(base):
 
155
        """Create a new branch, rooted at 'base' (url)"""
 
156
        t = get_transport(base)
 
157
        return _Branch(t, init=True)
 
158
 
 
159
    def setup_caching(self, cache_root):
 
160
        """Subclasses that care about caching should override this, and set
 
161
        up cached stores located under cache_root.
 
162
        """
 
163
        self.cache_root = cache_root
 
164
 
 
165
 
 
166
class _Branch(Branch):
 
167
    """A branch stored in the actual filesystem.
 
168
 
 
169
    Note that it's "local" in the context of the filesystem; it doesn't
 
170
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
171
    it's writable, and can be accessed via the normal filesystem API.
 
172
 
 
173
    _lock_mode
 
174
        None, or 'r' or 'w'
 
175
 
 
176
    _lock_count
 
177
        If _lock_mode is true, a positive count of the number of times the
 
178
        lock has been taken.
 
179
 
 
180
    _lock
 
181
        Lock object from bzrlib.lock.
 
182
    """
 
183
    # We actually expect this class to be somewhat short-lived; part of its
 
184
    # purpose is to try to isolate what bits of the branch logic are tied to
 
185
    # filesystem access, so that in a later step, we can extricate them to
 
186
    # a separarte ("storage") class.
 
187
    _lock_mode = None
 
188
    _lock_count = None
 
189
    _lock = None
 
190
    _inventory_weave = None
 
191
    
 
192
    # Map some sort of prefix into a namespace
 
193
    # stuff like "revno:10", "revid:", etc.
 
194
    # This should match a prefix with a function which accepts
 
195
    REVISION_NAMESPACES = {}
 
196
 
 
197
    def push_stores(self, branch_to):
 
198
        """Copy the content of this branches store to branch_to."""
 
199
        if (self._branch_format != branch_to._branch_format
 
200
            or self._branch_format != 4):
 
201
            from bzrlib.fetch import greedy_fetch
 
202
            mutter("falling back to fetch logic to push between %s(%s) and %s(%s)",
 
203
                   self, self._branch_format, branch_to, branch_to._branch_format)
 
204
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
205
                         revision=self.last_revision())
 
206
            return
 
207
 
 
208
        store_pairs = ((self.text_store,      branch_to.text_store),
 
209
                       (self.inventory_store, branch_to.inventory_store),
 
210
                       (self.revision_store,  branch_to.revision_store))
 
211
        try:
 
212
            for from_store, to_store in store_pairs: 
 
213
                copy_all(from_store, to_store)
 
214
        except UnlistableStore:
 
215
            raise UnlistableBranch(from_store)
 
216
 
 
217
    def __init__(self, transport, init=False,
 
218
                 relax_version_check=False):
91
219
        """Create new branch object at a particular location.
92
220
 
93
 
        :param base: Base directory for the branch.
 
221
        transport -- A Transport object, defining how to access files.
 
222
                (If a string, transport.transport() will be used to
 
223
                create a Transport object)
94
224
        
95
 
        :param init: If True, create new control files in a previously
 
225
        init -- If True, create new control files in a previously
96
226
             unversioned directory.  If False, the branch must already
97
227
             be versioned.
98
228
 
99
 
        :param find_root: If true and init is false, find the root of the
100
 
             existing branch containing base.
 
229
        relax_version_check -- If true, the usual check for the branch
 
230
            version is not applied.  This is intended only for
 
231
            upgrade/recovery type use; it's not guaranteed that
 
232
            all operations will work on old format branches.
101
233
 
102
234
        In the test suite, creation of new trees is tested using the
103
235
        `ScratchBranch` class.
104
236
        """
 
237
        assert isinstance(transport, Transport), \
 
238
            "%r is not a Transport" % transport
 
239
        self._transport = transport
105
240
        if init:
106
 
            self.base = os.path.realpath(base)
107
241
            self._make_control()
108
 
        elif find_root:
109
 
            self.base = find_branch_root(base)
110
 
        else:
111
 
            self.base = os.path.realpath(base)
112
 
            if not isdir(self.controlfilename('.')):
113
 
                bailout("not a bzr branch: %s" % quotefn(base),
114
 
                        ['use "bzr init" to initialize a new working tree',
115
 
                         'current bzr can only operate from top-of-tree'])
116
 
        self._check_format()
117
 
 
118
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
119
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
120
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
121
 
 
 
242
        self._check_format(relax_version_check)
 
243
 
 
244
        def get_store(name, compressed=True, prefixed=False):
 
245
            # FIXME: This approach of assuming stores are all entirely compressed
 
246
            # or entirely uncompressed is tidy, but breaks upgrade from 
 
247
            # some existing branches where there's a mixture; we probably 
 
248
            # still want the option to look for both.
 
249
            relpath = self._rel_controlfilename(name)
 
250
            if compressed:
 
251
                store = CompressedTextStore(self._transport.clone(relpath),
 
252
                                            prefixed=prefixed)
 
253
            else:
 
254
                store = TextStore(self._transport.clone(relpath),
 
255
                                  prefixed=prefixed)
 
256
            #if self._transport.should_cache():
 
257
            #    cache_path = os.path.join(self.cache_root, name)
 
258
            #    os.mkdir(cache_path)
 
259
            #    store = bzrlib.store.CachedStore(store, cache_path)
 
260
            return store
 
261
        def get_weave(name, prefixed=False):
 
262
            relpath = self._rel_controlfilename(name)
 
263
            ws = WeaveStore(self._transport.clone(relpath), prefixed=prefixed)
 
264
            if self._transport.should_cache():
 
265
                ws.enable_cache = True
 
266
            return ws
 
267
 
 
268
        if self._branch_format == 4:
 
269
            self.inventory_store = get_store('inventory-store')
 
270
            self.text_store = get_store('text-store')
 
271
            self.revision_store = get_store('revision-store')
 
272
        elif self._branch_format == 5:
 
273
            self.control_weaves = get_weave([])
 
274
            self.weave_store = get_weave('weaves')
 
275
            self.revision_store = get_store('revision-store', compressed=False)
 
276
        elif self._branch_format == 6:
 
277
            self.control_weaves = get_weave([])
 
278
            self.weave_store = get_weave('weaves', prefixed=True)
 
279
            self.revision_store = get_store('revision-store', compressed=False,
 
280
                                            prefixed=True)
 
281
        self._transaction = None
122
282
 
123
283
    def __str__(self):
124
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
284
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
125
285
 
126
286
 
127
287
    __repr__ = __str__
128
288
 
129
289
 
 
290
    def __del__(self):
 
291
        if self._lock_mode or self._lock:
 
292
            # XXX: This should show something every time, and be suitable for
 
293
            # headless operation and embedding
 
294
            warn("branch %r was not explicitly unlocked" % self)
 
295
            self._lock.unlock()
 
296
 
 
297
        # TODO: It might be best to do this somewhere else,
 
298
        # but it is nice for a Branch object to automatically
 
299
        # cache it's information.
 
300
        # Alternatively, we could have the Transport objects cache requests
 
301
        # See the earlier discussion about how major objects (like Branch)
 
302
        # should never expect their __del__ function to run.
 
303
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
304
            try:
 
305
                import shutil
 
306
                shutil.rmtree(self.cache_root)
 
307
            except:
 
308
                pass
 
309
            self.cache_root = None
 
310
 
 
311
    def _get_base(self):
 
312
        if self._transport:
 
313
            return self._transport.base
 
314
        return None
 
315
 
 
316
    base = property(_get_base)
 
317
 
 
318
    def _finish_transaction(self):
 
319
        """Exit the current transaction."""
 
320
        if self._transaction is None:
 
321
            raise errors.LockError('Branch %s is not in a transaction' %
 
322
                                   self)
 
323
        transaction = self._transaction
 
324
        self._transaction = None
 
325
        transaction.finish()
 
326
 
 
327
    def get_transaction(self):
 
328
        """Return the current active transaction.
 
329
 
 
330
        If no transaction is active, this returns a passthrough object
 
331
        for which all data is immedaitely flushed and no caching happens.
 
332
        """
 
333
        if self._transaction is None:
 
334
            return transactions.PassThroughTransaction()
 
335
        else:
 
336
            return self._transaction
 
337
 
 
338
    def _set_transaction(self, new_transaction):
 
339
        """Set a new active transaction."""
 
340
        if self._transaction is not None:
 
341
            raise errors.LockError('Branch %s is in a transaction already.' %
 
342
                                   self)
 
343
        self._transaction = new_transaction
 
344
 
 
345
    def lock_write(self):
 
346
        # TODO: Upgrade locking to support using a Transport,
 
347
        # and potentially a remote locking protocol
 
348
        if self._lock_mode:
 
349
            if self._lock_mode != 'w':
 
350
                raise LockError("can't upgrade to a write lock from %r" %
 
351
                                self._lock_mode)
 
352
            self._lock_count += 1
 
353
        else:
 
354
            self._lock = self._transport.lock_write(
 
355
                    self._rel_controlfilename('branch-lock'))
 
356
            self._lock_mode = 'w'
 
357
            self._lock_count = 1
 
358
            self._set_transaction(transactions.PassThroughTransaction())
 
359
 
 
360
 
 
361
    def lock_read(self):
 
362
        if self._lock_mode:
 
363
            assert self._lock_mode in ('r', 'w'), \
 
364
                   "invalid lock mode %r" % self._lock_mode
 
365
            self._lock_count += 1
 
366
        else:
 
367
            self._lock = self._transport.lock_read(
 
368
                    self._rel_controlfilename('branch-lock'))
 
369
            self._lock_mode = 'r'
 
370
            self._lock_count = 1
 
371
            self._set_transaction(transactions.ReadOnlyTransaction())
 
372
                        
 
373
    def unlock(self):
 
374
        if not self._lock_mode:
 
375
            raise LockError('branch %r is not locked' % (self))
 
376
 
 
377
        if self._lock_count > 1:
 
378
            self._lock_count -= 1
 
379
        else:
 
380
            self._finish_transaction()
 
381
            self._lock.unlock()
 
382
            self._lock = None
 
383
            self._lock_mode = self._lock_count = None
 
384
 
130
385
    def abspath(self, name):
131
386
        """Return absolute filename for something in the branch"""
132
 
        return os.path.join(self.base, name)
133
 
 
 
387
        return self._transport.abspath(name)
134
388
 
135
389
    def relpath(self, path):
136
390
        """Return path relative to this branch of something inside it.
137
391
 
138
392
        Raises an error if path is not in this branch."""
139
 
        rp = os.path.realpath(path)
140
 
        # FIXME: windows
141
 
        if not rp.startswith(self.base):
142
 
            bailout("path %r is not within branch %r" % (rp, self.base))
143
 
        rp = rp[len(self.base):]
144
 
        rp = rp.lstrip(os.sep)
145
 
        return rp
146
 
 
 
393
        return self._transport.relpath(path)
 
394
 
 
395
 
 
396
    def _rel_controlfilename(self, file_or_path):
 
397
        if isinstance(file_or_path, basestring):
 
398
            file_or_path = [file_or_path]
 
399
        return [bzrlib.BZRDIR] + file_or_path
147
400
 
148
401
    def controlfilename(self, file_or_path):
149
402
        """Return location relative to branch."""
150
 
        if isinstance(file_or_path, types.StringTypes):
151
 
            file_or_path = [file_or_path]
152
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
403
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
153
404
 
154
405
 
155
406
    def controlfile(self, file_or_path, mode='r'):
156
 
        """Open a control file for this branch"""
157
 
        return file(self.controlfilename(file_or_path), mode)
158
 
 
 
407
        """Open a control file for this branch.
 
408
 
 
409
        There are two classes of file in the control directory: text
 
410
        and binary.  binary files are untranslated byte streams.  Text
 
411
        control files are stored with Unix newlines and in UTF-8, even
 
412
        if the platform or locale defaults are different.
 
413
 
 
414
        Controlfiles should almost never be opened in write mode but
 
415
        rather should be atomically copied and replaced using atomicfile.
 
416
        """
 
417
        import codecs
 
418
 
 
419
        relpath = self._rel_controlfilename(file_or_path)
 
420
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
421
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
422
        if mode == 'rb': 
 
423
            return self._transport.get(relpath)
 
424
        elif mode == 'wb':
 
425
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
426
        elif mode == 'r':
 
427
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
428
        elif mode == 'w':
 
429
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
 
430
        else:
 
431
            raise BzrError("invalid controlfile mode %r" % mode)
 
432
 
 
433
    def put_controlfile(self, path, f, encode=True):
 
434
        """Write an entry as a controlfile.
 
435
 
 
436
        :param path: The path to put the file, relative to the .bzr control
 
437
                     directory
 
438
        :param f: A file-like or string object whose contents should be copied.
 
439
        :param encode:  If true, encode the contents as utf-8
 
440
        """
 
441
        self.put_controlfiles([(path, f)], encode=encode)
 
442
 
 
443
    def put_controlfiles(self, files, encode=True):
 
444
        """Write several entries as controlfiles.
 
445
 
 
446
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
447
                      underneath the bzr control directory
 
448
        :param encode:  If true, encode the contents as utf-8
 
449
        """
 
450
        import codecs
 
451
        ctrl_files = []
 
452
        for path, f in files:
 
453
            if encode:
 
454
                if isinstance(f, basestring):
 
455
                    f = f.encode('utf-8', 'replace')
 
456
                else:
 
457
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
458
            path = self._rel_controlfilename(path)
 
459
            ctrl_files.append((path, f))
 
460
        self._transport.put_multi(ctrl_files)
159
461
 
160
462
    def _make_control(self):
161
 
        os.mkdir(self.controlfilename([]))
162
 
        self.controlfile('README', 'w').write(
 
463
        from bzrlib.inventory import Inventory
 
464
        from bzrlib.weavefile import write_weave_v5
 
465
        from bzrlib.weave import Weave
 
466
        
 
467
        # Create an empty inventory
 
468
        sio = StringIO()
 
469
        # if we want per-tree root ids then this is the place to set
 
470
        # them; they're not needed for now and so ommitted for
 
471
        # simplicity.
 
472
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
473
        empty_inv = sio.getvalue()
 
474
        sio = StringIO()
 
475
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
476
        empty_weave = sio.getvalue()
 
477
 
 
478
        dirs = [[], 'revision-store', 'weaves']
 
479
        files = [('README', 
163
480
            "This is a Bazaar-NG control directory.\n"
164
 
            "Do not change any files in this directory.")
165
 
        self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT)
166
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
167
 
            os.mkdir(self.controlfilename(d))
168
 
        for f in ('revision-history', 'merged-patches',
169
 
                  'pending-merged-patches', 'branch-name'):
170
 
            self.controlfile(f, 'w').write('')
171
 
        mutter('created control directory in ' + self.base)
172
 
        Inventory().write_xml(self.controlfile('inventory','w'))
173
 
 
174
 
 
175
 
    def _check_format(self):
 
481
            "Do not change any files in this directory.\n"),
 
482
            ('branch-format', BZR_BRANCH_FORMAT_6),
 
483
            ('revision-history', ''),
 
484
            ('branch-name', ''),
 
485
            ('branch-lock', ''),
 
486
            ('pending-merges', ''),
 
487
            ('inventory', empty_inv),
 
488
            ('inventory.weave', empty_weave),
 
489
            ('ancestry.weave', empty_weave)
 
490
        ]
 
491
        cfn = self._rel_controlfilename
 
492
        self._transport.mkdir_multi([cfn(d) for d in dirs])
 
493
        self.put_controlfiles(files)
 
494
        mutter('created control directory in ' + self._transport.base)
 
495
 
 
496
    def _check_format(self, relax_version_check):
176
497
        """Check this branch format is supported.
177
498
 
178
 
        The current tool only supports the current unstable format.
 
499
        The format level is stored, as an integer, in
 
500
        self._branch_format for code that needs to check it later.
179
501
 
180
502
        In the future, we might need different in-memory Branch
181
503
        classes to support downlevel branches.  But not yet.
182
504
        """
183
 
        # This ignores newlines so that we can open branches created
184
 
        # on Windows from Linux and so on.  I think it might be better
185
 
        # to always make all internal files in unix format.
186
 
        fmt = self.controlfile('branch-format', 'rb').read()
187
 
        fmt.replace('\r\n', '')
188
 
        if fmt != BZR_BRANCH_FORMAT:
189
 
            bailout('sorry, branch format %r not supported' % fmt,
190
 
                    ['use a different bzr version',
191
 
                     'or remove the .bzr directory and "bzr init" again'])
192
 
 
 
505
        try:
 
506
            fmt = self.controlfile('branch-format', 'r').read()
 
507
        except NoSuchFile:
 
508
            raise NotBranchError(self.base)
 
509
        mutter("got branch format %r", fmt)
 
510
        if fmt == BZR_BRANCH_FORMAT_6:
 
511
            self._branch_format = 6
 
512
        elif fmt == BZR_BRANCH_FORMAT_5:
 
513
            self._branch_format = 5
 
514
        elif fmt == BZR_BRANCH_FORMAT_4:
 
515
            self._branch_format = 4
 
516
 
 
517
        if (not relax_version_check
 
518
            and self._branch_format not in (5, 6)):
 
519
            raise errors.UnsupportedFormatError(
 
520
                           'sorry, branch format %r not supported' % fmt,
 
521
                           ['use a different bzr version',
 
522
                            'or remove the .bzr directory'
 
523
                            ' and "bzr init" again'])
 
524
 
 
525
    def get_root_id(self):
 
526
        """Return the id of this branches root"""
 
527
        inv = self.read_working_inventory()
 
528
        return inv.root.file_id
 
529
 
 
530
    def set_root_id(self, file_id):
 
531
        inv = self.read_working_inventory()
 
532
        orig_root_id = inv.root.file_id
 
533
        del inv._byid[inv.root.file_id]
 
534
        inv.root.file_id = file_id
 
535
        inv._byid[inv.root.file_id] = inv.root
 
536
        for fid in inv:
 
537
            entry = inv[fid]
 
538
            if entry.parent_id in (None, orig_root_id):
 
539
                entry.parent_id = inv.root.file_id
 
540
        self._write_inventory(inv)
193
541
 
194
542
    def read_working_inventory(self):
195
543
        """Read the working inventory."""
196
 
        before = time.time()
197
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
198
 
        mutter("loaded inventory of %d items in %f"
199
 
               % (len(inv), time.time() - before))
200
 
        return inv
201
 
 
 
544
        self.lock_read()
 
545
        try:
 
546
            # ElementTree does its own conversion from UTF-8, so open in
 
547
            # binary.
 
548
            f = self.controlfile('inventory', 'rb')
 
549
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
550
        finally:
 
551
            self.unlock()
 
552
            
202
553
 
203
554
    def _write_inventory(self, inv):
204
555
        """Update the working inventory.
206
557
        That is to say, the inventory describing changes underway, that
207
558
        will be committed to the next revision.
208
559
        """
209
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
210
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
211
 
        tmpfname = self.controlfilename('inventory.tmp')
212
 
        tmpf = file(tmpfname, 'w')
213
 
        inv.write_xml(tmpf)
214
 
        tmpf.close()
215
 
        inv_fname = self.controlfilename('inventory')
216
 
        if sys.platform == 'win32':
217
 
            os.remove(inv_fname)
218
 
        os.rename(tmpfname, inv_fname)
 
560
        from cStringIO import StringIO
 
561
        self.lock_write()
 
562
        try:
 
563
            sio = StringIO()
 
564
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
565
            sio.seek(0)
 
566
            # Transport handles atomicity
 
567
            self.put_controlfile('inventory', sio)
 
568
        finally:
 
569
            self.unlock()
 
570
        
219
571
        mutter('wrote working inventory')
220
 
 
221
 
 
 
572
            
222
573
    inventory = property(read_working_inventory, _write_inventory, None,
223
574
                         """Inventory for the working copy.""")
224
575
 
225
 
 
226
 
    def add(self, files, verbose=False):
 
576
    def add(self, files, ids=None):
227
577
        """Make files versioned.
228
578
 
 
579
        Note that the command line normally calls smart_add instead,
 
580
        which can automatically recurse.
 
581
 
229
582
        This puts the files in the Added state, so that they will be
230
583
        recorded by the next commit.
231
584
 
232
 
        :todo: Perhaps have an option to add the ids even if the files do
233
 
               not (yet) exist.
234
 
 
235
 
        :todo: Perhaps return the ids of the files?  But then again it
236
 
               is easy to retrieve them if they're needed.
237
 
 
238
 
        :todo: Option to specify file id.
239
 
 
240
 
        :todo: Adding a directory should optionally recurse down and
241
 
               add all non-ignored children.  Perhaps do that in a
242
 
               higher-level method.
243
 
 
244
 
        >>> b = ScratchBranch(files=['foo'])
245
 
        >>> 'foo' in b.unknowns()
246
 
        True
247
 
        >>> b.show_status()
248
 
        ?       foo
249
 
        >>> b.add('foo')
250
 
        >>> 'foo' in b.unknowns()
251
 
        False
252
 
        >>> bool(b.inventory.path2id('foo'))
253
 
        True
254
 
        >>> b.show_status()
255
 
        A       foo
256
 
 
257
 
        >>> b.add('foo')
258
 
        Traceback (most recent call last):
259
 
        ...
260
 
        BzrError: ('foo is already versioned', [])
261
 
 
262
 
        >>> b.add(['nothere'])
263
 
        Traceback (most recent call last):
264
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
 
585
        files
 
586
            List of paths to add, relative to the base of the tree.
 
587
 
 
588
        ids
 
589
            If set, use these instead of automatically generated ids.
 
590
            Must be the same length as the list of files, but may
 
591
            contain None for ids that are to be autogenerated.
 
592
 
 
593
        TODO: Perhaps have an option to add the ids even if the files do
 
594
              not (yet) exist.
 
595
 
 
596
        TODO: Perhaps yield the ids and paths as they're added.
265
597
        """
266
 
 
267
598
        # TODO: Re-adding a file that is removed in the working copy
268
599
        # should probably put it back with the previous ID.
269
 
        if isinstance(files, types.StringTypes):
 
600
        if isinstance(files, basestring):
 
601
            assert(ids is None or isinstance(ids, basestring))
270
602
            files = [files]
271
 
        
272
 
        inv = self.read_working_inventory()
273
 
        for f in files:
274
 
            if is_control_file(f):
275
 
                bailout("cannot add control file %s" % quotefn(f))
276
 
 
277
 
            fp = splitpath(f)
278
 
 
279
 
            if len(fp) == 0:
280
 
                bailout("cannot add top-level %r" % f)
281
 
                
282
 
            fullpath = os.path.normpath(self.abspath(f))
283
 
 
284
 
            try:
285
 
                kind = file_kind(fullpath)
286
 
            except OSError:
287
 
                # maybe something better?
288
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
289
 
            
290
 
            if kind != 'file' and kind != 'directory':
291
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
292
 
 
293
 
            file_id = gen_file_id(f)
294
 
            inv.add_path(f, kind=kind, file_id=file_id)
295
 
 
296
 
            if verbose:
297
 
                show_status('A', kind, quotefn(f))
298
 
                
299
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
300
 
            
301
 
        self._write_inventory(inv)
302
 
 
 
603
            if ids is not None:
 
604
                ids = [ids]
 
605
 
 
606
        if ids is None:
 
607
            ids = [None] * len(files)
 
608
        else:
 
609
            assert(len(ids) == len(files))
 
610
 
 
611
        self.lock_write()
 
612
        try:
 
613
            inv = self.read_working_inventory()
 
614
            for f,file_id in zip(files, ids):
 
615
                if is_control_file(f):
 
616
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
617
 
 
618
                fp = splitpath(f)
 
619
 
 
620
                if len(fp) == 0:
 
621
                    raise BzrError("cannot add top-level %r" % f)
 
622
 
 
623
                fullpath = os.path.normpath(self.abspath(f))
 
624
 
 
625
                try:
 
626
                    kind = file_kind(fullpath)
 
627
                except OSError:
 
628
                    # maybe something better?
 
629
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
630
 
 
631
                if not InventoryEntry.versionable_kind(kind):
 
632
                    raise BzrError('cannot add: not a versionable file ('
 
633
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
634
 
 
635
                if file_id is None:
 
636
                    file_id = gen_file_id(f)
 
637
                inv.add_path(f, kind=kind, file_id=file_id)
 
638
 
 
639
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
640
 
 
641
            self._write_inventory(inv)
 
642
        finally:
 
643
            self.unlock()
 
644
            
 
645
 
 
646
    def print_file(self, file, revno):
 
647
        """Print `file` to stdout."""
 
648
        self.lock_read()
 
649
        try:
 
650
            tree = self.revision_tree(self.get_rev_id(revno))
 
651
            # use inventory as it was in that revision
 
652
            file_id = tree.inventory.path2id(file)
 
653
            if not file_id:
 
654
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
655
            tree.print_file(file_id)
 
656
        finally:
 
657
            self.unlock()
303
658
 
304
659
 
305
660
    def remove(self, files, verbose=False):
307
662
 
308
663
        This does not remove their text.  This does not run on 
309
664
 
310
 
        :todo: Refuse to remove modified files unless --force is given?
311
 
 
312
 
        >>> b = ScratchBranch(files=['foo'])
313
 
        >>> b.add('foo')
314
 
        >>> b.inventory.has_filename('foo')
315
 
        True
316
 
        >>> b.remove('foo')
317
 
        >>> b.working_tree().has_filename('foo')
318
 
        True
319
 
        >>> b.inventory.has_filename('foo')
320
 
        False
321
 
        
322
 
        >>> b = ScratchBranch(files=['foo'])
323
 
        >>> b.add('foo')
324
 
        >>> b.commit('one')
325
 
        >>> b.remove('foo')
326
 
        >>> b.commit('two')
327
 
        >>> b.inventory.has_filename('foo') 
328
 
        False
329
 
        >>> b.basis_tree().has_filename('foo') 
330
 
        False
331
 
        >>> b.working_tree().has_filename('foo') 
332
 
        True
333
 
 
334
 
        :todo: Do something useful with directories.
335
 
 
336
 
        :todo: Should this remove the text or not?  Tough call; not
 
665
        TODO: Refuse to remove modified files unless --force is given?
 
666
 
 
667
        TODO: Do something useful with directories.
 
668
 
 
669
        TODO: Should this remove the text or not?  Tough call; not
337
670
        removing may be useful and the user can just use use rm, and
338
671
        is the opposite of add.  Removing it is consistent with most
339
672
        other tools.  Maybe an option.
340
673
        """
341
674
        ## TODO: Normalize names
342
675
        ## TODO: Remove nested loops; better scalability
343
 
 
344
 
        if isinstance(files, types.StringTypes):
 
676
        if isinstance(files, basestring):
345
677
            files = [files]
346
 
        
347
 
        tree = self.working_tree()
348
 
        inv = tree.inventory
349
 
 
350
 
        # do this before any modifications
351
 
        for f in files:
352
 
            fid = inv.path2id(f)
353
 
            if not fid:
354
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
355
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
356
 
            if verbose:
357
 
                # having remove it, it must be either ignored or unknown
358
 
                if tree.is_ignored(f):
359
 
                    new_status = 'I'
360
 
                else:
361
 
                    new_status = '?'
362
 
                show_status(new_status, inv[fid].kind, quotefn(f))
363
 
            del inv[fid]
364
 
 
 
678
 
 
679
        self.lock_write()
 
680
 
 
681
        try:
 
682
            tree = self.working_tree()
 
683
            inv = tree.inventory
 
684
 
 
685
            # do this before any modifications
 
686
            for f in files:
 
687
                fid = inv.path2id(f)
 
688
                if not fid:
 
689
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
690
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
691
                if verbose:
 
692
                    # having remove it, it must be either ignored or unknown
 
693
                    if tree.is_ignored(f):
 
694
                        new_status = 'I'
 
695
                    else:
 
696
                        new_status = '?'
 
697
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
698
                del inv[fid]
 
699
 
 
700
            self._write_inventory(inv)
 
701
        finally:
 
702
            self.unlock()
 
703
 
 
704
    # FIXME: this doesn't need to be a branch method
 
705
    def set_inventory(self, new_inventory_list):
 
706
        from bzrlib.inventory import Inventory, InventoryEntry
 
707
        inv = Inventory(self.get_root_id())
 
708
        for path, file_id, parent, kind in new_inventory_list:
 
709
            name = os.path.basename(path)
 
710
            if name == "":
 
711
                continue
 
712
            # fixme, there should be a factory function inv,add_?? 
 
713
            if kind == 'directory':
 
714
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
 
715
            elif kind == 'file':
 
716
                inv.add(inventory.InventoryFile(file_id, name, parent))
 
717
            elif kind == 'symlink':
 
718
                inv.add(inventory.InventoryLink(file_id, name, parent))
 
719
            else:
 
720
                raise BzrError("unknown kind %r" % kind)
365
721
        self._write_inventory(inv)
366
722
 
367
 
 
368
723
    def unknowns(self):
369
724
        """Return all unknown files.
370
725
 
384
739
        return self.working_tree().unknowns()
385
740
 
386
741
 
387
 
    def commit(self, message, timestamp=None, timezone=None,
388
 
               committer=None,
389
 
               verbose=False):
390
 
        """Commit working copy as a new revision.
391
 
        
392
 
        The basic approach is to add all the file texts into the
393
 
        store, then the inventory, then make a new revision pointing
394
 
        to that inventory and store that.
395
 
        
396
 
        This is not quite safe if the working copy changes during the
397
 
        commit; for the moment that is simply not allowed.  A better
398
 
        approach is to make a temporary copy of the files before
399
 
        computing their hashes, and then add those hashes in turn to
400
 
        the inventory.  This should mean at least that there are no
401
 
        broken hash pointers.  There is no way we can get a snapshot
402
 
        of the whole directory at an instant.  This would also have to
403
 
        be robust against files disappearing, moving, etc.  So the
404
 
        whole thing is a bit hard.
405
 
 
406
 
        :param timestamp: if not None, seconds-since-epoch for a
407
 
             postdated/predated commit.
408
 
        """
409
 
 
410
 
        ## TODO: Show branch names
411
 
 
412
 
        # TODO: Don't commit if there are no changes, unless forced?
413
 
 
414
 
        # First walk over the working inventory; and both update that
415
 
        # and also build a new revision inventory.  The revision
416
 
        # inventory needs to hold the text-id, sha1 and size of the
417
 
        # actual file versions committed in the revision.  (These are
418
 
        # not present in the working inventory.)  We also need to
419
 
        # detect missing/deleted files, and remove them from the
420
 
        # working inventory.
421
 
 
422
 
        work_inv = self.read_working_inventory()
423
 
        inv = Inventory()
424
 
        basis = self.basis_tree()
425
 
        basis_inv = basis.inventory
426
 
        missing_ids = []
427
 
        for path, entry in work_inv.iter_entries():
428
 
            ## TODO: Cope with files that have gone missing.
429
 
 
430
 
            ## TODO: Check that the file kind has not changed from the previous
431
 
            ## revision of this file (if any).
432
 
 
433
 
            entry = entry.copy()
434
 
 
435
 
            p = self.abspath(path)
436
 
            file_id = entry.file_id
437
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
438
 
 
439
 
            if not os.path.exists(p):
440
 
                mutter("    file is missing, removing from inventory")
441
 
                if verbose:
442
 
                    show_status('D', entry.kind, quotefn(path))
443
 
                missing_ids.append(file_id)
444
 
                continue
445
 
 
446
 
            # TODO: Handle files that have been deleted
447
 
 
448
 
            # TODO: Maybe a special case for empty files?  Seems a
449
 
            # waste to store them many times.
450
 
 
451
 
            inv.add(entry)
452
 
 
453
 
            if basis_inv.has_id(file_id):
454
 
                old_kind = basis_inv[file_id].kind
455
 
                if old_kind != entry.kind:
456
 
                    bailout("entry %r changed kind from %r to %r"
457
 
                            % (file_id, old_kind, entry.kind))
458
 
 
459
 
            if entry.kind == 'directory':
460
 
                if not isdir(p):
461
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
462
 
            elif entry.kind == 'file':
463
 
                if not isfile(p):
464
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
465
 
 
466
 
                content = file(p, 'rb').read()
467
 
 
468
 
                entry.text_sha1 = sha_string(content)
469
 
                entry.text_size = len(content)
470
 
 
471
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
472
 
                if (old_ie
473
 
                    and (old_ie.text_size == entry.text_size)
474
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
475
 
                    ## assert content == basis.get_file(file_id).read()
476
 
                    entry.text_id = basis_inv[file_id].text_id
477
 
                    mutter('    unchanged from previous text_id {%s}' %
478
 
                           entry.text_id)
479
 
                    
480
 
                else:
481
 
                    entry.text_id = gen_file_id(entry.name)
482
 
                    self.text_store.add(content, entry.text_id)
483
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
484
 
                    if verbose:
485
 
                        if not old_ie:
486
 
                            state = 'A'
487
 
                        elif (old_ie.name == entry.name
488
 
                              and old_ie.parent_id == entry.parent_id):
489
 
                            state = 'M'
490
 
                        else:
491
 
                            state = 'R'
492
 
 
493
 
                        show_status(state, entry.kind, quotefn(path))
494
 
 
495
 
        for file_id in missing_ids:
496
 
            # have to do this later so we don't mess up the iterator.
497
 
            # since parents may be removed before their children we
498
 
            # have to test.
499
 
 
500
 
            # FIXME: There's probably a better way to do this; perhaps
501
 
            # the workingtree should know how to filter itself.
502
 
            if work_inv.has_id(file_id):
503
 
                del work_inv[file_id]
504
 
 
505
 
 
506
 
        inv_id = rev_id = _gen_revision_id(time.time())
507
 
        
508
 
        inv_tmp = tempfile.TemporaryFile()
509
 
        inv.write_xml(inv_tmp)
510
 
        inv_tmp.seek(0)
511
 
        self.inventory_store.add(inv_tmp, inv_id)
512
 
        mutter('new inventory_id is {%s}' % inv_id)
513
 
 
514
 
        self._write_inventory(work_inv)
515
 
 
516
 
        if timestamp == None:
517
 
            timestamp = time.time()
518
 
 
519
 
        if committer == None:
520
 
            committer = username()
521
 
 
522
 
        if timezone == None:
523
 
            timezone = local_time_offset()
524
 
 
525
 
        mutter("building commit log message")
526
 
        rev = Revision(timestamp=timestamp,
527
 
                       timezone=timezone,
528
 
                       committer=committer,
529
 
                       precursor = self.last_patch(),
530
 
                       message = message,
531
 
                       inventory_id=inv_id,
532
 
                       revision_id=rev_id)
533
 
 
534
 
        rev_tmp = tempfile.TemporaryFile()
535
 
        rev.write_xml(rev_tmp)
536
 
        rev_tmp.seek(0)
537
 
        self.revision_store.add(rev_tmp, rev_id)
538
 
        mutter("new revision_id is {%s}" % rev_id)
539
 
        
540
 
        ## XXX: Everything up to here can simply be orphaned if we abort
541
 
        ## the commit; it will leave junk files behind but that doesn't
542
 
        ## matter.
543
 
 
544
 
        ## TODO: Read back the just-generated changeset, and make sure it
545
 
        ## applies and recreates the right state.
546
 
 
547
 
        ## TODO: Also calculate and store the inventory SHA1
548
 
        mutter("committing patch r%d" % (self.revno() + 1))
549
 
 
550
 
        mutter("append to revision-history")
551
 
        f = self.controlfile('revision-history', 'at')
552
 
        f.write(rev_id + '\n')
553
 
        f.close()
554
 
 
555
 
        if verbose:
556
 
            note("commited r%d" % self.revno())
 
742
    def append_revision(self, *revision_ids):
 
743
        for revision_id in revision_ids:
 
744
            mutter("add {%s} to revision-history" % revision_id)
 
745
        self.lock_write()
 
746
        try:
 
747
            rev_history = self.revision_history()
 
748
            rev_history.extend(revision_ids)
 
749
            self.put_controlfile('revision-history', '\n'.join(rev_history))
 
750
        finally:
 
751
            self.unlock()
 
752
 
 
753
    def has_revision(self, revision_id):
 
754
        """True if this branch has a copy of the revision.
 
755
 
 
756
        This does not necessarily imply the revision is merge
 
757
        or on the mainline."""
 
758
        return (revision_id is None
 
759
                or revision_id in self.revision_store)
 
760
 
 
761
    def get_revision_xml_file(self, revision_id):
 
762
        """Return XML file object for revision object."""
 
763
        if not revision_id or not isinstance(revision_id, basestring):
 
764
            raise InvalidRevisionId(revision_id)
 
765
 
 
766
        self.lock_read()
 
767
        try:
 
768
            try:
 
769
                return self.revision_store[revision_id]
 
770
            except (IndexError, KeyError):
 
771
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
772
        finally:
 
773
            self.unlock()
 
774
 
 
775
    #deprecated
 
776
    get_revision_xml = get_revision_xml_file
 
777
 
 
778
    def get_revision_xml(self, revision_id):
 
779
        return self.get_revision_xml_file(revision_id).read()
557
780
 
558
781
 
559
782
    def get_revision(self, revision_id):
560
783
        """Return the Revision object for a named revision"""
561
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
784
        xml_file = self.get_revision_xml_file(revision_id)
 
785
 
 
786
        try:
 
787
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
788
        except SyntaxError, e:
 
789
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
790
                                         [revision_id,
 
791
                                          str(e)])
 
792
            
562
793
        assert r.revision_id == revision_id
563
794
        return r
564
795
 
565
 
 
566
 
    def get_inventory(self, inventory_id):
567
 
        """Get Inventory object by hash.
568
 
 
569
 
        :todo: Perhaps for this and similar methods, take a revision
570
 
               parameter which can be either an integer revno or a
571
 
               string hash."""
572
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
573
 
        return i
574
 
 
 
796
    def get_revision_delta(self, revno):
 
797
        """Return the delta for one revision.
 
798
 
 
799
        The delta is relative to its mainline predecessor, or the
 
800
        empty tree for revision 1.
 
801
        """
 
802
        assert isinstance(revno, int)
 
803
        rh = self.revision_history()
 
804
        if not (1 <= revno <= len(rh)):
 
805
            raise InvalidRevisionNumber(revno)
 
806
 
 
807
        # revno is 1-based; list is 0-based
 
808
 
 
809
        new_tree = self.revision_tree(rh[revno-1])
 
810
        if revno == 1:
 
811
            old_tree = EmptyTree()
 
812
        else:
 
813
            old_tree = self.revision_tree(rh[revno-2])
 
814
 
 
815
        return compare_trees(old_tree, new_tree)
 
816
 
 
817
    def get_revision_sha1(self, revision_id):
 
818
        """Hash the stored value of a revision, and return it."""
 
819
        # In the future, revision entries will be signed. At that
 
820
        # point, it is probably best *not* to include the signature
 
821
        # in the revision hash. Because that lets you re-sign
 
822
        # the revision, (add signatures/remove signatures) and still
 
823
        # have all hash pointers stay consistent.
 
824
        # But for now, just hash the contents.
 
825
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
826
 
 
827
    def get_ancestry(self, revision_id):
 
828
        """Return a list of revision-ids integrated by a revision.
 
829
        
 
830
        This currently returns a list, but the ordering is not guaranteed:
 
831
        treat it as a set.
 
832
        """
 
833
        if revision_id is None:
 
834
            return [None]
 
835
        w = self.get_inventory_weave()
 
836
        return [None] + map(w.idx_to_name,
 
837
                            w.inclusions([w.lookup(revision_id)]))
 
838
 
 
839
    def get_inventory_weave(self):
 
840
        return self.control_weaves.get_weave('inventory',
 
841
                                             self.get_transaction())
 
842
 
 
843
    def get_inventory(self, revision_id):
 
844
        """Get Inventory object by hash."""
 
845
        xml = self.get_inventory_xml(revision_id)
 
846
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
847
 
 
848
    def get_inventory_xml(self, revision_id):
 
849
        """Get inventory XML as a file object."""
 
850
        try:
 
851
            assert isinstance(revision_id, basestring), type(revision_id)
 
852
            iw = self.get_inventory_weave()
 
853
            return iw.get_text(iw.lookup(revision_id))
 
854
        except IndexError:
 
855
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
856
 
 
857
    def get_inventory_sha1(self, revision_id):
 
858
        """Return the sha1 hash of the inventory entry
 
859
        """
 
860
        return self.get_revision(revision_id).inventory_sha1
575
861
 
576
862
    def get_revision_inventory(self, revision_id):
577
863
        """Return inventory of a past revision."""
 
864
        # TODO: Unify this with get_inventory()
 
865
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
866
        # must be the same as its revision, so this is trivial.
578
867
        if revision_id == None:
579
 
            return Inventory()
 
868
            return Inventory(self.get_root_id())
580
869
        else:
581
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
582
 
 
 
870
            return self.get_inventory(revision_id)
583
871
 
584
872
    def revision_history(self):
585
 
        """Return sequence of revision hashes on to this branch.
 
873
        """Return sequence of revision hashes on to this branch."""
 
874
        self.lock_read()
 
875
        try:
 
876
            return [l.rstrip('\r\n') for l in
 
877
                    self.controlfile('revision-history', 'r').readlines()]
 
878
        finally:
 
879
            self.unlock()
586
880
 
587
 
        >>> ScratchBranch().revision_history()
588
 
        []
589
 
        """
590
 
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
 
881
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
882
        """
 
883
        >>> from bzrlib.commit import commit
 
884
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
885
        >>> sb.common_ancestor(sb) == (None, None)
 
886
        True
 
887
        >>> commit(sb, "Committing first revision", verbose=False)
 
888
        >>> sb.common_ancestor(sb)[0]
 
889
        1
 
890
        >>> clone = sb.clone()
 
891
        >>> commit(sb, "Committing second revision", verbose=False)
 
892
        >>> sb.common_ancestor(sb)[0]
 
893
        2
 
894
        >>> sb.common_ancestor(clone)[0]
 
895
        1
 
896
        >>> commit(clone, "Committing divergent second revision", 
 
897
        ...               verbose=False)
 
898
        >>> sb.common_ancestor(clone)[0]
 
899
        1
 
900
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
901
        True
 
902
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
903
        True
 
904
        >>> clone2 = sb.clone()
 
905
        >>> sb.common_ancestor(clone2)[0]
 
906
        2
 
907
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
908
        1
 
909
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
910
        1
 
911
        """
 
912
        my_history = self.revision_history()
 
913
        other_history = other.revision_history()
 
914
        if self_revno is None:
 
915
            self_revno = len(my_history)
 
916
        if other_revno is None:
 
917
            other_revno = len(other_history)
 
918
        indices = range(min((self_revno, other_revno)))
 
919
        indices.reverse()
 
920
        for r in indices:
 
921
            if my_history[r] == other_history[r]:
 
922
                return r+1, my_history[r]
 
923
        return None, None
591
924
 
592
925
 
593
926
    def revno(self):
595
928
 
596
929
        That is equivalent to the number of revisions committed to
597
930
        this branch.
598
 
 
599
 
        >>> b = ScratchBranch()
600
 
        >>> b.revno()
601
 
        0
602
 
        >>> b.commit('no foo')
603
 
        >>> b.revno()
604
 
        1
605
931
        """
606
932
        return len(self.revision_history())
607
933
 
608
934
 
609
 
    def last_patch(self):
 
935
    def last_revision(self):
610
936
        """Return last patch hash, or None if no history.
611
 
 
612
 
        >>> ScratchBranch().last_patch() == None
613
 
        True
614
937
        """
615
938
        ph = self.revision_history()
616
939
        if ph:
617
940
            return ph[-1]
618
 
 
619
 
 
620
 
    def lookup_revision(self, revno):
621
 
        """Return revision hash for revision number."""
 
941
        else:
 
942
            return None
 
943
 
 
944
 
 
945
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
946
        """Return a list of new revisions that would perfectly fit.
 
947
        
 
948
        If self and other have not diverged, return a list of the revisions
 
949
        present in other, but missing from self.
 
950
 
 
951
        >>> from bzrlib.commit import commit
 
952
        >>> bzrlib.trace.silent = True
 
953
        >>> br1 = ScratchBranch()
 
954
        >>> br2 = ScratchBranch()
 
955
        >>> br1.missing_revisions(br2)
 
956
        []
 
957
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
958
        >>> br1.missing_revisions(br2)
 
959
        [u'REVISION-ID-1']
 
960
        >>> br2.missing_revisions(br1)
 
961
        []
 
962
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
963
        >>> br1.missing_revisions(br2)
 
964
        []
 
965
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
966
        >>> br1.missing_revisions(br2)
 
967
        [u'REVISION-ID-2A']
 
968
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
969
        >>> br1.missing_revisions(br2)
 
970
        Traceback (most recent call last):
 
971
        DivergedBranches: These branches have diverged.
 
972
        """
 
973
        # FIXME: If the branches have diverged, but the latest
 
974
        # revision in this branch is completely merged into the other,
 
975
        # then we should still be able to pull.
 
976
        self_history = self.revision_history()
 
977
        self_len = len(self_history)
 
978
        other_history = other.revision_history()
 
979
        other_len = len(other_history)
 
980
        common_index = min(self_len, other_len) -1
 
981
        if common_index >= 0 and \
 
982
            self_history[common_index] != other_history[common_index]:
 
983
            raise DivergedBranches(self, other)
 
984
 
 
985
        if stop_revision is None:
 
986
            stop_revision = other_len
 
987
        else:
 
988
            assert isinstance(stop_revision, int)
 
989
            if stop_revision > other_len:
 
990
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
991
        return other_history[self_len:stop_revision]
 
992
 
 
993
    def update_revisions(self, other, stop_revision=None):
 
994
        """Pull in new perfect-fit revisions."""
 
995
        from bzrlib.fetch import greedy_fetch
 
996
        from bzrlib.revision import get_intervening_revisions
 
997
        if stop_revision is None:
 
998
            stop_revision = other.last_revision()
 
999
        greedy_fetch(to_branch=self, from_branch=other,
 
1000
                     revision=stop_revision)
 
1001
        pullable_revs = self.missing_revisions(
 
1002
            other, other.revision_id_to_revno(stop_revision))
 
1003
        if pullable_revs:
 
1004
            greedy_fetch(to_branch=self,
 
1005
                         from_branch=other,
 
1006
                         revision=pullable_revs[-1])
 
1007
            self.append_revision(*pullable_revs)
 
1008
    
 
1009
 
 
1010
    def commit(self, *args, **kw):
 
1011
        from bzrlib.commit import Commit
 
1012
        Commit().commit(self, *args, **kw)
 
1013
    
 
1014
    def revision_id_to_revno(self, revision_id):
 
1015
        """Given a revision id, return its revno"""
 
1016
        if revision_id is None:
 
1017
            return 0
 
1018
        history = self.revision_history()
 
1019
        try:
 
1020
            return history.index(revision_id) + 1
 
1021
        except ValueError:
 
1022
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
1023
 
 
1024
    def get_rev_id(self, revno, history=None):
 
1025
        """Find the revision id of the specified revno."""
622
1026
        if revno == 0:
623
1027
            return None
624
 
 
625
 
        try:
626
 
            # list is 0-based; revisions are 1-based
627
 
            return self.revision_history()[revno-1]
628
 
        except IndexError:
629
 
            bailout("no such revision %s" % revno)
630
 
 
 
1028
        if history is None:
 
1029
            history = self.revision_history()
 
1030
        elif revno <= 0 or revno > len(history):
 
1031
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
1032
        return history[revno - 1]
631
1033
 
632
1034
    def revision_tree(self, revision_id):
633
1035
        """Return Tree for a revision on this branch.
634
1036
 
635
1037
        `revision_id` may be None for the null revision, in which case
636
1038
        an `EmptyTree` is returned."""
637
 
 
 
1039
        # TODO: refactor this to use an existing revision object
 
1040
        # so we don't need to read it in twice.
638
1041
        if revision_id == None:
639
1042
            return EmptyTree()
640
1043
        else:
641
1044
            inv = self.get_revision_inventory(revision_id)
642
 
            return RevisionTree(self.text_store, inv)
 
1045
            return RevisionTree(self.weave_store, inv, revision_id)
643
1046
 
644
1047
 
645
1048
    def working_tree(self):
646
1049
        """Return a `Tree` for the working copy."""
647
 
        return WorkingTree(self.base, self.read_working_inventory())
 
1050
        from bzrlib.workingtree import WorkingTree
 
1051
        # TODO: In the future, WorkingTree should utilize Transport
 
1052
        # RobertCollins 20051003 - I don't think it should - working trees are
 
1053
        # much more complex to keep consistent than our careful .bzr subset.
 
1054
        # instead, we should say that working trees are local only, and optimise
 
1055
        # for that.
 
1056
        return WorkingTree(self._transport.base, self.read_working_inventory())
648
1057
 
649
1058
 
650
1059
    def basis_tree(self):
651
1060
        """Return `Tree` object for last revision.
652
1061
 
653
1062
        If there are no revisions yet, return an `EmptyTree`.
654
 
 
655
 
        >>> b = ScratchBranch(files=['foo'])
656
 
        >>> b.basis_tree().has_filename('foo')
657
 
        False
658
 
        >>> b.working_tree().has_filename('foo')
659
 
        True
660
 
        >>> b.add('foo')
661
 
        >>> b.commit('add foo')
662
 
        >>> b.basis_tree().has_filename('foo')
663
 
        True
664
1063
        """
665
 
        r = self.last_patch()
666
 
        if r == None:
667
 
            return EmptyTree()
668
 
        else:
669
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
670
 
 
671
 
 
672
 
 
673
 
    def write_log(self, show_timezone='original'):
674
 
        """Write out human-readable log of commits to this branch
675
 
 
676
 
        :param utc: If true, show dates in universal time, not local time."""
677
 
        ## TODO: Option to choose either original, utc or local timezone
678
 
        revno = 1
679
 
        precursor = None
680
 
        for p in self.revision_history():
681
 
            print '-' * 40
682
 
            print 'revno:', revno
683
 
            ## TODO: Show hash if --id is given.
684
 
            ##print 'revision-hash:', p
685
 
            rev = self.get_revision(p)
686
 
            print 'committer:', rev.committer
687
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
688
 
                                                 show_timezone))
689
 
 
690
 
            ## opportunistic consistency check, same as check_patch_chaining
691
 
            if rev.precursor != precursor:
692
 
                bailout("mismatched precursor!")
693
 
 
694
 
            print 'message:'
695
 
            if not rev.message:
696
 
                print '  (no message)'
697
 
            else:
698
 
                for l in rev.message.split('\n'):
699
 
                    print '  ' + l
700
 
 
701
 
            revno += 1
702
 
            precursor = p
 
1064
        return self.revision_tree(self.last_revision())
703
1065
 
704
1066
 
705
1067
    def rename_one(self, from_rel, to_rel):
706
 
        tree = self.working_tree()
707
 
        inv = tree.inventory
708
 
        if not tree.has_filename(from_rel):
709
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
710
 
        if tree.has_filename(to_rel):
711
 
            bailout("can't rename: new working file %r already exists" % to_rel)
712
 
            
713
 
        file_id = inv.path2id(from_rel)
714
 
        if file_id == None:
715
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
716
 
 
717
 
        if inv.path2id(to_rel):
718
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
719
 
 
720
 
        to_dir, to_tail = os.path.split(to_rel)
721
 
        to_dir_id = inv.path2id(to_dir)
722
 
        if to_dir_id == None and to_dir != '':
723
 
            bailout("can't determine destination directory id for %r" % to_dir)
724
 
 
725
 
        mutter("rename_one:")
726
 
        mutter("  file_id    {%s}" % file_id)
727
 
        mutter("  from_rel   %r" % from_rel)
728
 
        mutter("  to_rel     %r" % to_rel)
729
 
        mutter("  to_dir     %r" % to_dir)
730
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
731
 
            
732
 
        inv.rename(file_id, to_dir_id, to_tail)
733
 
        os.rename(self.abspath(from_rel), self.abspath(to_rel))
734
 
 
735
 
        self._write_inventory(inv)
736
 
            
737
 
 
738
 
 
739
 
    def rename(self, from_paths, to_name):
 
1068
        """Rename one file.
 
1069
 
 
1070
        This can change the directory or the filename or both.
 
1071
        """
 
1072
        self.lock_write()
 
1073
        try:
 
1074
            tree = self.working_tree()
 
1075
            inv = tree.inventory
 
1076
            if not tree.has_filename(from_rel):
 
1077
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1078
            if tree.has_filename(to_rel):
 
1079
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1080
 
 
1081
            file_id = inv.path2id(from_rel)
 
1082
            if file_id == None:
 
1083
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1084
 
 
1085
            if inv.path2id(to_rel):
 
1086
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1087
 
 
1088
            to_dir, to_tail = os.path.split(to_rel)
 
1089
            to_dir_id = inv.path2id(to_dir)
 
1090
            if to_dir_id == None and to_dir != '':
 
1091
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1092
 
 
1093
            mutter("rename_one:")
 
1094
            mutter("  file_id    {%s}" % file_id)
 
1095
            mutter("  from_rel   %r" % from_rel)
 
1096
            mutter("  to_rel     %r" % to_rel)
 
1097
            mutter("  to_dir     %r" % to_dir)
 
1098
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1099
 
 
1100
            inv.rename(file_id, to_dir_id, to_tail)
 
1101
 
 
1102
            from_abs = self.abspath(from_rel)
 
1103
            to_abs = self.abspath(to_rel)
 
1104
            try:
 
1105
                rename(from_abs, to_abs)
 
1106
            except OSError, e:
 
1107
                raise BzrError("failed to rename %r to %r: %s"
 
1108
                        % (from_abs, to_abs, e[1]),
 
1109
                        ["rename rolled back"])
 
1110
 
 
1111
            self._write_inventory(inv)
 
1112
        finally:
 
1113
            self.unlock()
 
1114
 
 
1115
 
 
1116
    def move(self, from_paths, to_name):
740
1117
        """Rename files.
741
1118
 
 
1119
        to_name must exist as a versioned directory.
 
1120
 
742
1121
        If to_name exists and is a directory, the files are moved into
743
1122
        it, keeping their old names.  If it is a directory, 
744
1123
 
745
1124
        Note that to_name is only the last component of the new name;
746
1125
        this doesn't change the directory.
 
1126
 
 
1127
        This returns a list of (from_path, to_path) pairs for each
 
1128
        entry that is moved.
747
1129
        """
748
 
        ## TODO: Option to move IDs only
749
 
        assert not isinstance(from_paths, basestring)
750
 
        tree = self.working_tree()
751
 
        inv = tree.inventory
752
 
        dest_dir = isdir(self.abspath(to_name))
753
 
        if dest_dir:
754
 
            # TODO: Wind back properly if some can't be moved?
755
 
            dest_dir_id = inv.path2id(to_name)
756
 
            if not dest_dir_id and to_name != '':
757
 
                bailout("destination %r is not a versioned directory" % to_name)
758
 
            for f in from_paths:
759
 
                name_tail = splitpath(f)[-1]
760
 
                dest_path = appendpath(to_name, name_tail)
761
 
                print "%s => %s" % (f, dest_path)
762
 
                inv.rename(inv.path2id(f), dest_dir_id, name_tail)
763
 
                os.rename(self.abspath(f), self.abspath(dest_path))
 
1130
        result = []
 
1131
        self.lock_write()
 
1132
        try:
 
1133
            ## TODO: Option to move IDs only
 
1134
            assert not isinstance(from_paths, basestring)
 
1135
            tree = self.working_tree()
 
1136
            inv = tree.inventory
 
1137
            to_abs = self.abspath(to_name)
 
1138
            if not isdir(to_abs):
 
1139
                raise BzrError("destination %r is not a directory" % to_abs)
 
1140
            if not tree.has_filename(to_name):
 
1141
                raise BzrError("destination %r not in working directory" % to_abs)
 
1142
            to_dir_id = inv.path2id(to_name)
 
1143
            if to_dir_id == None and to_name != '':
 
1144
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1145
            to_dir_ie = inv[to_dir_id]
 
1146
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1147
                raise BzrError("destination %r is not a directory" % to_abs)
 
1148
 
 
1149
            to_idpath = inv.get_idpath(to_dir_id)
 
1150
 
 
1151
            for f in from_paths:
 
1152
                if not tree.has_filename(f):
 
1153
                    raise BzrError("%r does not exist in working tree" % f)
 
1154
                f_id = inv.path2id(f)
 
1155
                if f_id == None:
 
1156
                    raise BzrError("%r is not versioned" % f)
 
1157
                name_tail = splitpath(f)[-1]
 
1158
                dest_path = appendpath(to_name, name_tail)
 
1159
                if tree.has_filename(dest_path):
 
1160
                    raise BzrError("destination %r already exists" % dest_path)
 
1161
                if f_id in to_idpath:
 
1162
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1163
 
 
1164
            # OK, so there's a race here, it's possible that someone will
 
1165
            # create a file in this interval and then the rename might be
 
1166
            # left half-done.  But we should have caught most problems.
 
1167
 
 
1168
            for f in from_paths:
 
1169
                name_tail = splitpath(f)[-1]
 
1170
                dest_path = appendpath(to_name, name_tail)
 
1171
                result.append((f, dest_path))
 
1172
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1173
                try:
 
1174
                    rename(self.abspath(f), self.abspath(dest_path))
 
1175
                except OSError, e:
 
1176
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1177
                            ["rename rolled back"])
 
1178
 
764
1179
            self._write_inventory(inv)
765
 
        else:
766
 
            if len(from_paths) != 1:
767
 
                bailout("when moving multiple files, destination must be a directory")
768
 
            bailout("rename to non-directory %r not implemented sorry" % to_name)
769
 
 
770
 
 
771
 
 
772
 
    def show_status(branch, show_all=False):
773
 
        """Display single-line status for non-ignored working files.
774
 
 
775
 
        The list is show sorted in order by file name.
776
 
 
777
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
778
 
        >>> b.show_status()
779
 
        ?       foo
780
 
        >>> b.add('foo')
781
 
        >>> b.show_status()
782
 
        A       foo
783
 
        >>> b.commit("add foo")
784
 
        >>> b.show_status()
785
 
        >>> os.unlink(b.abspath('foo'))
786
 
        >>> b.show_status()
787
 
        D       foo
788
 
        
789
 
 
790
 
        :todo: Get state for single files.
791
 
 
792
 
        :todo: Perhaps show a slash at the end of directory names.        
793
 
 
794
 
        """
795
 
 
796
 
        # We have to build everything into a list first so that it can
797
 
        # sorted by name, incorporating all the different sources.
798
 
 
799
 
        # FIXME: Rather than getting things in random order and then sorting,
800
 
        # just step through in order.
801
 
 
802
 
        # Interesting case: the old ID for a file has been removed,
803
 
        # but a new file has been created under that name.
804
 
 
805
 
        old = branch.basis_tree()
806
 
        old_inv = old.inventory
807
 
        new = branch.working_tree()
808
 
        new_inv = new.inventory
809
 
 
810
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
811
 
            if fs == 'R':
812
 
                show_status(fs, kind,
813
 
                            oldname + ' => ' + newname)
814
 
            elif fs == 'A' or fs == 'M':
815
 
                show_status(fs, kind, newname)
816
 
            elif fs == 'D':
817
 
                show_status(fs, kind, oldname)
818
 
            elif fs == '.':
819
 
                if show_all:
820
 
                    show_status(fs, kind, newname)
821
 
            elif fs == 'I':
822
 
                if show_all:
823
 
                    show_status(fs, kind, newname)
824
 
            elif fs == '?':
825
 
                show_status(fs, kind, newname)
826
 
            else:
827
 
                bailout("wierd file state %r" % ((fs, fid),))
828
 
                
829
 
 
830
 
 
831
 
class ScratchBranch(Branch):
 
1180
        finally:
 
1181
            self.unlock()
 
1182
 
 
1183
        return result
 
1184
 
 
1185
 
 
1186
    def revert(self, filenames, old_tree=None, backups=True):
 
1187
        """Restore selected files to the versions from a previous tree.
 
1188
 
 
1189
        backups
 
1190
            If true (default) backups are made of files before
 
1191
            they're renamed.
 
1192
        """
 
1193
        from bzrlib.errors import NotVersionedError, BzrError
 
1194
        from bzrlib.atomicfile import AtomicFile
 
1195
        from bzrlib.osutils import backup_file
 
1196
        
 
1197
        inv = self.read_working_inventory()
 
1198
        if old_tree is None:
 
1199
            old_tree = self.basis_tree()
 
1200
        old_inv = old_tree.inventory
 
1201
 
 
1202
        nids = []
 
1203
        for fn in filenames:
 
1204
            file_id = inv.path2id(fn)
 
1205
            if not file_id:
 
1206
                raise NotVersionedError("not a versioned file", fn)
 
1207
            if not old_inv.has_id(file_id):
 
1208
                raise BzrError("file not present in old tree", fn, file_id)
 
1209
            nids.append((fn, file_id))
 
1210
            
 
1211
        # TODO: Rename back if it was previously at a different location
 
1212
 
 
1213
        # TODO: If given a directory, restore the entire contents from
 
1214
        # the previous version.
 
1215
 
 
1216
        # TODO: Make a backup to a temporary file.
 
1217
 
 
1218
        # TODO: If the file previously didn't exist, delete it?
 
1219
        for fn, file_id in nids:
 
1220
            backup_file(fn)
 
1221
            
 
1222
            f = AtomicFile(fn, 'wb')
 
1223
            try:
 
1224
                f.write(old_tree.get_file(file_id).read())
 
1225
                f.commit()
 
1226
            finally:
 
1227
                f.close()
 
1228
 
 
1229
 
 
1230
    def pending_merges(self):
 
1231
        """Return a list of pending merges.
 
1232
 
 
1233
        These are revisions that have been merged into the working
 
1234
        directory but not yet committed.
 
1235
        """
 
1236
        cfn = self._rel_controlfilename('pending-merges')
 
1237
        if not self._transport.has(cfn):
 
1238
            return []
 
1239
        p = []
 
1240
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1241
            p.append(l.rstrip('\n'))
 
1242
        return p
 
1243
 
 
1244
 
 
1245
    def add_pending_merge(self, *revision_ids):
 
1246
        # TODO: Perhaps should check at this point that the
 
1247
        # history of the revision is actually present?
 
1248
        p = self.pending_merges()
 
1249
        updated = False
 
1250
        for rev_id in revision_ids:
 
1251
            if rev_id in p:
 
1252
                continue
 
1253
            p.append(rev_id)
 
1254
            updated = True
 
1255
        if updated:
 
1256
            self.set_pending_merges(p)
 
1257
 
 
1258
    def set_pending_merges(self, rev_list):
 
1259
        self.lock_write()
 
1260
        try:
 
1261
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1262
        finally:
 
1263
            self.unlock()
 
1264
 
 
1265
 
 
1266
    def get_parent(self):
 
1267
        """Return the parent location of the branch.
 
1268
 
 
1269
        This is the default location for push/pull/missing.  The usual
 
1270
        pattern is that the user can override it by specifying a
 
1271
        location.
 
1272
        """
 
1273
        import errno
 
1274
        _locs = ['parent', 'pull', 'x-pull']
 
1275
        for l in _locs:
 
1276
            try:
 
1277
                return self.controlfile(l, 'r').read().strip('\n')
 
1278
            except IOError, e:
 
1279
                if e.errno != errno.ENOENT:
 
1280
                    raise
 
1281
        return None
 
1282
 
 
1283
 
 
1284
    def set_parent(self, url):
 
1285
        # TODO: Maybe delete old location files?
 
1286
        from bzrlib.atomicfile import AtomicFile
 
1287
        self.lock_write()
 
1288
        try:
 
1289
            f = AtomicFile(self.controlfilename('parent'))
 
1290
            try:
 
1291
                f.write(url + '\n')
 
1292
                f.commit()
 
1293
            finally:
 
1294
                f.close()
 
1295
        finally:
 
1296
            self.unlock()
 
1297
 
 
1298
    def check_revno(self, revno):
 
1299
        """\
 
1300
        Check whether a revno corresponds to any revision.
 
1301
        Zero (the NULL revision) is considered valid.
 
1302
        """
 
1303
        if revno != 0:
 
1304
            self.check_real_revno(revno)
 
1305
            
 
1306
    def check_real_revno(self, revno):
 
1307
        """\
 
1308
        Check whether a revno corresponds to a real revision.
 
1309
        Zero (the NULL revision) is considered invalid
 
1310
        """
 
1311
        if revno < 1 or revno > self.revno():
 
1312
            raise InvalidRevisionNumber(revno)
 
1313
        
 
1314
        
 
1315
        
 
1316
 
 
1317
 
 
1318
class ScratchBranch(_Branch):
832
1319
    """Special test class: a branch that cleans up after itself.
833
1320
 
834
1321
    >>> b = ScratchBranch()
835
1322
    >>> isdir(b.base)
836
1323
    True
837
1324
    >>> bd = b.base
838
 
    >>> del b
 
1325
    >>> b.destroy()
839
1326
    >>> isdir(bd)
840
1327
    False
841
1328
    """
842
 
    def __init__(self, files=[], dirs=[]):
 
1329
    def __init__(self, files=[], dirs=[], base=None):
843
1330
        """Make a test branch.
844
1331
 
845
1332
        This creates a temporary directory and runs init-tree in it.
846
1333
 
847
1334
        If any files are listed, they are created in the working copy.
848
1335
        """
849
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
1336
        from tempfile import mkdtemp
 
1337
        init = False
 
1338
        if base is None:
 
1339
            base = mkdtemp()
 
1340
            init = True
 
1341
        if isinstance(base, basestring):
 
1342
            base = get_transport(base)
 
1343
        _Branch.__init__(self, base, init=init)
850
1344
        for d in dirs:
851
 
            os.mkdir(self.abspath(d))
 
1345
            self._transport.mkdir(d)
852
1346
            
853
1347
        for f in files:
854
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
855
 
 
 
1348
            self._transport.put(f, 'content of %s' % f)
 
1349
 
 
1350
 
 
1351
    def clone(self):
 
1352
        """
 
1353
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
1354
        >>> clone = orig.clone()
 
1355
        >>> if os.name != 'nt':
 
1356
        ...   os.path.samefile(orig.base, clone.base)
 
1357
        ... else:
 
1358
        ...   orig.base == clone.base
 
1359
        ...
 
1360
        False
 
1361
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
1362
        True
 
1363
        """
 
1364
        from shutil import copytree
 
1365
        from tempfile import mkdtemp
 
1366
        base = mkdtemp()
 
1367
        os.rmdir(base)
 
1368
        copytree(self.base, base, symlinks=True)
 
1369
        return ScratchBranch(base=base)
856
1370
 
857
1371
    def __del__(self):
 
1372
        self.destroy()
 
1373
 
 
1374
    def destroy(self):
858
1375
        """Destroy the test branch, removing the scratch directory."""
 
1376
        from shutil import rmtree
859
1377
        try:
860
 
            shutil.rmtree(self.base)
861
 
        except OSError:
 
1378
            if self.base:
 
1379
                mutter("delete ScratchBranch %s" % self.base)
 
1380
                rmtree(self.base)
 
1381
        except OSError, e:
862
1382
            # Work around for shutil.rmtree failing on Windows when
863
1383
            # readonly files are encountered
 
1384
            mutter("hit exception in destroying ScratchBranch: %s" % e)
864
1385
            for root, dirs, files in os.walk(self.base, topdown=False):
865
1386
                for name in files:
866
1387
                    os.chmod(os.path.join(root, name), 0700)
867
 
            shutil.rmtree(self.base)
 
1388
            rmtree(self.base)
 
1389
        self._transport = None
868
1390
 
869
1391
    
870
1392
 
887
1409
 
888
1410
 
889
1411
 
890
 
def _gen_revision_id(when):
891
 
    """Return new revision-id."""
892
 
    s = '%s-%s-' % (user_email(), compact_date(when))
893
 
    s += hexlify(rand_bytes(8))
894
 
    return s
895
 
 
896
 
 
897
1412
def gen_file_id(name):
898
1413
    """Return new file id.
899
1414
 
900
1415
    This should probably generate proper UUIDs, but for the moment we
901
1416
    cope with just randomness because running uuidgen every time is
902
1417
    slow."""
 
1418
    import re
 
1419
    from binascii import hexlify
 
1420
    from time import time
 
1421
 
 
1422
    # get last component
903
1423
    idx = name.rfind('/')
904
1424
    if idx != -1:
905
1425
        name = name[idx+1 : ]
 
1426
    idx = name.rfind('\\')
 
1427
    if idx != -1:
 
1428
        name = name[idx+1 : ]
906
1429
 
 
1430
    # make it not a hidden file
907
1431
    name = name.lstrip('.')
908
1432
 
 
1433
    # remove any wierd characters; we don't escape them but rather
 
1434
    # just pull them out
 
1435
    name = re.sub(r'[^\w.]', '', name)
 
1436
 
909
1437
    s = hexlify(rand_bytes(8))
910
 
    return '-'.join((name, compact_date(time.time()), s))
 
1438
    return '-'.join((name, compact_date(time()), s))
 
1439
 
 
1440
 
 
1441
def gen_root_id():
 
1442
    """Return a new tree-root file id."""
 
1443
    return gen_file_id('TREE_ROOT')
911
1444
 
912
1445