~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: aaron.bentley at utoronto
  • Date: 2005-08-19 12:06:01 UTC
  • mto: (1092.1.41) (1185.3.4) (974.1.47)
  • mto: This revision was merged to the branch mainline in revision 1110.
  • Revision ID: aaron.bentley@utoronto.ca-20050819120601-58525b75283a9c1c
Initial greedy fetch work

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
19
 
 
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
 
18
import sys
 
19
import os
23
20
 
24
21
import bzrlib
25
 
from inventory import Inventory
26
 
from trace import mutter, note
27
 
from tree import Tree, EmptyTree, RevisionTree
28
 
from inventory import InventoryEntry, Inventory
29
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
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, BzrError
35
 
from textui import show_status
36
 
 
 
22
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
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
 
27
import bzrlib.errors
 
28
from bzrlib.textui import show_status
 
29
from bzrlib.revision import Revision
 
30
from bzrlib.xml import unpack_xml
 
31
from bzrlib.delta import compare_trees
 
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
        
37
34
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
38
35
## TODO: Maybe include checks for common corruption of newlines, etc?
39
36
 
40
37
 
 
38
# TODO: Some operations like log might retrieve the same revisions
 
39
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
40
# cache in memory to make this faster.
 
41
 
 
42
# TODO: please move the revision-string syntax stuff out of the branch
 
43
# object; it's clutter
 
44
 
41
45
 
42
46
def find_branch(f, **args):
43
47
    if f and (f.startswith('http://') or f.startswith('https://')):
45
49
        return remotebranch.RemoteBranch(f, **args)
46
50
    else:
47
51
        return Branch(f, **args)
 
52
 
 
53
 
 
54
def find_cached_branch(f, cache_root, **args):
 
55
    from remotebranch import RemoteBranch
 
56
    br = find_branch(f, **args)
 
57
    def cacheify(br, store_name):
 
58
        from meta_store import CachedStore
 
59
        cache_path = os.path.join(cache_root, store_name)
 
60
        os.mkdir(cache_path)
 
61
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
62
        setattr(br, store_name, new_store)
 
63
 
 
64
    if isinstance(br, RemoteBranch):
 
65
        cacheify(br, 'inventory_store')
 
66
        cacheify(br, 'text_store')
 
67
        cacheify(br, 'revision_store')
 
68
    return br
 
69
 
 
70
 
 
71
def _relpath(base, path):
 
72
    """Return path relative to base, or raise exception.
 
73
 
 
74
    The path may be either an absolute path or a path relative to the
 
75
    current working directory.
 
76
 
 
77
    Lifted out of Branch.relpath for ease of testing.
 
78
 
 
79
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
80
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
81
    avoids that problem."""
 
82
    rp = os.path.abspath(path)
 
83
 
 
84
    s = []
 
85
    head = rp
 
86
    while len(head) >= len(base):
 
87
        if head == base:
 
88
            break
 
89
        head, tail = os.path.split(head)
 
90
        if tail:
 
91
            s.insert(0, tail)
 
92
    else:
 
93
        from errors import NotBranchError
 
94
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
95
 
 
96
    return os.sep.join(s)
48
97
        
49
98
 
50
99
def find_branch_root(f=None):
77
126
            raise BzrError('%r is not in a branch' % orig_f)
78
127
        f = head
79
128
    
 
129
class DivergedBranches(Exception):
 
130
    def __init__(self, branch1, branch2):
 
131
        self.branch1 = branch1
 
132
        self.branch2 = branch2
 
133
        Exception.__init__(self, "These branches have diverged.")
80
134
 
81
135
 
82
136
######################################################################
83
137
# branch objects
84
138
 
85
 
class Branch:
 
139
class Branch(object):
86
140
    """Branch holding a history of revisions.
87
141
 
88
142
    base
89
143
        Base directory of the branch.
 
144
 
 
145
    _lock_mode
 
146
        None, or 'r' or 'w'
 
147
 
 
148
    _lock_count
 
149
        If _lock_mode is true, a positive count of the number of times the
 
150
        lock has been taken.
 
151
 
 
152
    _lock
 
153
        Lock object from bzrlib.lock.
90
154
    """
91
 
    _lockmode = None
 
155
    base = None
 
156
    _lock_mode = None
 
157
    _lock_count = None
 
158
    _lock = None
92
159
    
93
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
160
    # Map some sort of prefix into a namespace
 
161
    # stuff like "revno:10", "revid:", etc.
 
162
    # This should match a prefix with a function which accepts
 
163
    REVISION_NAMESPACES = {}
 
164
 
 
165
    def __init__(self, base, init=False, find_root=True):
94
166
        """Create new branch object at a particular location.
95
167
 
96
168
        base -- Base directory for the branch.
105
177
        In the test suite, creation of new trees is tested using the
106
178
        `ScratchBranch` class.
107
179
        """
 
180
        from bzrlib.store import ImmutableStore
108
181
        if init:
109
182
            self.base = os.path.realpath(base)
110
183
            self._make_control()
113
186
        else:
114
187
            self.base = os.path.realpath(base)
115
188
            if not isdir(self.controlfilename('.')):
116
 
                bailout("not a bzr branch: %s" % quotefn(base),
117
 
                        ['use "bzr init" to initialize a new working tree',
118
 
                         'current bzr can only operate from top-of-tree'])
 
189
                from errors import NotBranchError
 
190
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
191
                                     ['use "bzr init" to initialize a new working tree',
 
192
                                      'current bzr can only operate from top-of-tree'])
119
193
        self._check_format()
120
 
        self.lock(lock_mode)
121
194
 
122
195
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
123
196
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
131
204
    __repr__ = __str__
132
205
 
133
206
 
134
 
 
135
 
    def lock(self, mode='w'):
136
 
        """Lock the on-disk branch, excluding other processes."""
137
 
        try:
138
 
            import fcntl, errno
139
 
 
140
 
            if mode == 'w':
141
 
                lm = fcntl.LOCK_EX
142
 
                om = os.O_WRONLY | os.O_CREAT
143
 
            elif mode == 'r':
144
 
                lm = fcntl.LOCK_SH
145
 
                om = os.O_RDONLY
146
 
            else:
147
 
                raise BzrError("invalid locking mode %r" % mode)
148
 
 
149
 
            try:
150
 
                lockfile = os.open(self.controlfilename('branch-lock'), om)
151
 
            except OSError, e:
152
 
                if e.errno == errno.ENOENT:
153
 
                    # might not exist on branches from <0.0.4
154
 
                    self.controlfile('branch-lock', 'w').close()
155
 
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
156
 
                else:
157
 
                    raise e
 
207
    def __del__(self):
 
208
        if self._lock_mode or self._lock:
 
209
            from warnings import warn
 
210
            warn("branch %r was not explicitly unlocked" % self)
 
211
            self._lock.unlock()
 
212
 
 
213
 
 
214
 
 
215
    def lock_write(self):
 
216
        if self._lock_mode:
 
217
            if self._lock_mode != 'w':
 
218
                from errors import LockError
 
219
                raise LockError("can't upgrade to a write lock from %r" %
 
220
                                self._lock_mode)
 
221
            self._lock_count += 1
 
222
        else:
 
223
            from bzrlib.lock import WriteLock
 
224
 
 
225
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
226
            self._lock_mode = 'w'
 
227
            self._lock_count = 1
 
228
 
 
229
 
 
230
 
 
231
    def lock_read(self):
 
232
        if self._lock_mode:
 
233
            assert self._lock_mode in ('r', 'w'), \
 
234
                   "invalid lock mode %r" % self._lock_mode
 
235
            self._lock_count += 1
 
236
        else:
 
237
            from bzrlib.lock import ReadLock
 
238
 
 
239
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
240
            self._lock_mode = 'r'
 
241
            self._lock_count = 1
 
242
                        
 
243
 
158
244
            
159
 
            fcntl.lockf(lockfile, lm)
160
 
            def unlock():
161
 
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
162
 
                os.close(lockfile)
163
 
                self._lockmode = None
164
 
            self.unlock = unlock
165
 
            self._lockmode = mode
166
 
        except ImportError:
167
 
            warning("please write a locking method for platform %r" % sys.platform)
168
 
            def unlock():
169
 
                self._lockmode = None
170
 
            self.unlock = unlock
171
 
            self._lockmode = mode
172
 
 
173
 
 
174
 
    def _need_readlock(self):
175
 
        if self._lockmode not in ['r', 'w']:
176
 
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
177
 
 
178
 
    def _need_writelock(self):
179
 
        if self._lockmode not in ['w']:
180
 
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
245
    def unlock(self):
 
246
        if not self._lock_mode:
 
247
            from errors import LockError
 
248
            raise LockError('branch %r is not locked' % (self))
 
249
 
 
250
        if self._lock_count > 1:
 
251
            self._lock_count -= 1
 
252
        else:
 
253
            self._lock.unlock()
 
254
            self._lock = None
 
255
            self._lock_mode = self._lock_count = None
181
256
 
182
257
 
183
258
    def abspath(self, name):
189
264
        """Return path relative to this branch of something inside it.
190
265
 
191
266
        Raises an error if path is not in this branch."""
192
 
        rp = os.path.realpath(path)
193
 
        # FIXME: windows
194
 
        if not rp.startswith(self.base):
195
 
            bailout("path %r is not within branch %r" % (rp, self.base))
196
 
        rp = rp[len(self.base):]
197
 
        rp = rp.lstrip(os.sep)
198
 
        return rp
 
267
        return _relpath(self.base, path)
199
268
 
200
269
 
201
270
    def controlfilename(self, file_or_path):
202
271
        """Return location relative to branch."""
203
 
        if isinstance(file_or_path, types.StringTypes):
 
272
        if isinstance(file_or_path, basestring):
204
273
            file_or_path = [file_or_path]
205
274
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
206
275
 
233
302
 
234
303
 
235
304
    def _make_control(self):
 
305
        from bzrlib.inventory import Inventory
 
306
        from bzrlib.xml import pack_xml
 
307
        
236
308
        os.mkdir(self.controlfilename([]))
237
309
        self.controlfile('README', 'w').write(
238
310
            "This is a Bazaar-NG control directory.\n"
239
 
            "Do not change any files in this directory.")
 
311
            "Do not change any files in this directory.\n")
240
312
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
241
313
        for d in ('text-store', 'inventory-store', 'revision-store'):
242
314
            os.mkdir(self.controlfilename(d))
243
315
        for f in ('revision-history', 'merged-patches',
244
316
                  'pending-merged-patches', 'branch-name',
245
 
                  'branch-lock'):
 
317
                  'branch-lock',
 
318
                  'pending-merges'):
246
319
            self.controlfile(f, 'w').write('')
247
320
        mutter('created control directory in ' + self.base)
248
 
        Inventory().write_xml(self.controlfile('inventory','w'))
 
321
 
 
322
        # if we want per-tree root ids then this is the place to set
 
323
        # them; they're not needed for now and so ommitted for
 
324
        # simplicity.
 
325
        pack_xml(Inventory(), self.controlfile('inventory','w'))
249
326
 
250
327
 
251
328
    def _check_format(self):
262
339
        fmt = self.controlfile('branch-format', 'r').read()
263
340
        fmt.replace('\r\n', '')
264
341
        if fmt != BZR_BRANCH_FORMAT:
265
 
            bailout('sorry, branch format %r not supported' % fmt,
266
 
                    ['use a different bzr version',
267
 
                     'or remove the .bzr directory and "bzr init" again'])
268
 
 
 
342
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
343
                           ['use a different bzr version',
 
344
                            'or remove the .bzr directory and "bzr init" again'])
 
345
 
 
346
    def get_root_id(self):
 
347
        """Return the id of this branches root"""
 
348
        inv = self.read_working_inventory()
 
349
        return inv.root.file_id
 
350
 
 
351
    def set_root_id(self, file_id):
 
352
        inv = self.read_working_inventory()
 
353
        orig_root_id = inv.root.file_id
 
354
        del inv._byid[inv.root.file_id]
 
355
        inv.root.file_id = file_id
 
356
        inv._byid[inv.root.file_id] = inv.root
 
357
        for fid in inv:
 
358
            entry = inv[fid]
 
359
            if entry.parent_id in (None, orig_root_id):
 
360
                entry.parent_id = inv.root.file_id
 
361
        self._write_inventory(inv)
269
362
 
270
363
    def read_working_inventory(self):
271
364
        """Read the working inventory."""
272
 
        self._need_readlock()
273
 
        before = time.time()
274
 
        # ElementTree does its own conversion from UTF-8, so open in
275
 
        # binary.
276
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
277
 
        mutter("loaded inventory of %d items in %f"
278
 
               % (len(inv), time.time() - before))
279
 
        return inv
280
 
 
 
365
        from bzrlib.inventory import Inventory
 
366
        from bzrlib.xml import unpack_xml
 
367
        from time import time
 
368
        before = time()
 
369
        self.lock_read()
 
370
        try:
 
371
            # ElementTree does its own conversion from UTF-8, so open in
 
372
            # binary.
 
373
            inv = unpack_xml(Inventory,
 
374
                             self.controlfile('inventory', 'rb'))
 
375
            mutter("loaded inventory of %d items in %f"
 
376
                   % (len(inv), time() - before))
 
377
            return inv
 
378
        finally:
 
379
            self.unlock()
 
380
            
281
381
 
282
382
    def _write_inventory(self, inv):
283
383
        """Update the working inventory.
285
385
        That is to say, the inventory describing changes underway, that
286
386
        will be committed to the next revision.
287
387
        """
288
 
        self._need_writelock()
289
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
290
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
291
 
        tmpfname = self.controlfilename('inventory.tmp')
292
 
        tmpf = file(tmpfname, 'wb')
293
 
        inv.write_xml(tmpf)
294
 
        tmpf.close()
295
 
        inv_fname = self.controlfilename('inventory')
296
 
        if sys.platform == 'win32':
297
 
            os.remove(inv_fname)
298
 
        os.rename(tmpfname, inv_fname)
 
388
        from bzrlib.atomicfile import AtomicFile
 
389
        from bzrlib.xml import pack_xml
 
390
        
 
391
        self.lock_write()
 
392
        try:
 
393
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
394
            try:
 
395
                pack_xml(inv, f)
 
396
                f.commit()
 
397
            finally:
 
398
                f.close()
 
399
        finally:
 
400
            self.unlock()
 
401
        
299
402
        mutter('wrote working inventory')
300
 
 
 
403
            
301
404
 
302
405
    inventory = property(read_working_inventory, _write_inventory, None,
303
406
                         """Inventory for the working copy.""")
304
407
 
305
408
 
306
 
    def add(self, files, verbose=False):
 
409
    def add(self, files, verbose=False, ids=None):
307
410
        """Make files versioned.
308
411
 
309
412
        Note that the command line normally calls smart_add instead.
311
414
        This puts the files in the Added state, so that they will be
312
415
        recorded by the next commit.
313
416
 
 
417
        files
 
418
            List of paths to add, relative to the base of the tree.
 
419
 
 
420
        ids
 
421
            If set, use these instead of automatically generated ids.
 
422
            Must be the same length as the list of files, but may
 
423
            contain None for ids that are to be autogenerated.
 
424
 
314
425
        TODO: Perhaps have an option to add the ids even if the files do
315
 
               not (yet) exist.
 
426
              not (yet) exist.
316
427
 
317
428
        TODO: Perhaps return the ids of the files?  But then again it
318
 
               is easy to retrieve them if they're needed.
319
 
 
320
 
        TODO: Option to specify file id.
 
429
              is easy to retrieve them if they're needed.
321
430
 
322
431
        TODO: Adding a directory should optionally recurse down and
323
 
               add all non-ignored children.  Perhaps do that in a
324
 
               higher-level method.
325
 
 
326
 
        >>> b = ScratchBranch(files=['foo'])
327
 
        >>> 'foo' in b.unknowns()
328
 
        True
329
 
        >>> b.show_status()
330
 
        ?       foo
331
 
        >>> b.add('foo')
332
 
        >>> 'foo' in b.unknowns()
333
 
        False
334
 
        >>> bool(b.inventory.path2id('foo'))
335
 
        True
336
 
        >>> b.show_status()
337
 
        A       foo
338
 
 
339
 
        >>> b.add('foo')
340
 
        Traceback (most recent call last):
341
 
        ...
342
 
        BzrError: ('foo is already versioned', [])
343
 
 
344
 
        >>> b.add(['nothere'])
345
 
        Traceback (most recent call last):
346
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
 
432
              add all non-ignored children.  Perhaps do that in a
 
433
              higher-level method.
347
434
        """
348
 
        self._need_writelock()
349
 
 
350
435
        # TODO: Re-adding a file that is removed in the working copy
351
436
        # should probably put it back with the previous ID.
352
 
        if isinstance(files, types.StringTypes):
 
437
        if isinstance(files, basestring):
 
438
            assert(ids is None or isinstance(ids, basestring))
353
439
            files = [files]
354
 
        
355
 
        inv = self.read_working_inventory()
356
 
        for f in files:
357
 
            if is_control_file(f):
358
 
                bailout("cannot add control file %s" % quotefn(f))
359
 
 
360
 
            fp = splitpath(f)
361
 
 
362
 
            if len(fp) == 0:
363
 
                bailout("cannot add top-level %r" % f)
364
 
                
365
 
            fullpath = os.path.normpath(self.abspath(f))
366
 
 
367
 
            try:
368
 
                kind = file_kind(fullpath)
369
 
            except OSError:
370
 
                # maybe something better?
371
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
372
 
            
373
 
            if kind != 'file' and kind != 'directory':
374
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
375
 
 
376
 
            file_id = gen_file_id(f)
377
 
            inv.add_path(f, kind=kind, file_id=file_id)
378
 
 
379
 
            if verbose:
380
 
                show_status('A', kind, quotefn(f))
381
 
                
382
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
383
 
            
384
 
        self._write_inventory(inv)
385
 
 
 
440
            if ids is not None:
 
441
                ids = [ids]
 
442
 
 
443
        if ids is None:
 
444
            ids = [None] * len(files)
 
445
        else:
 
446
            assert(len(ids) == len(files))
 
447
 
 
448
        self.lock_write()
 
449
        try:
 
450
            inv = self.read_working_inventory()
 
451
            for f,file_id in zip(files, ids):
 
452
                if is_control_file(f):
 
453
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
454
 
 
455
                fp = splitpath(f)
 
456
 
 
457
                if len(fp) == 0:
 
458
                    raise BzrError("cannot add top-level %r" % f)
 
459
 
 
460
                fullpath = os.path.normpath(self.abspath(f))
 
461
 
 
462
                try:
 
463
                    kind = file_kind(fullpath)
 
464
                except OSError:
 
465
                    # maybe something better?
 
466
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
467
 
 
468
                if kind != 'file' and kind != 'directory':
 
469
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
470
 
 
471
                if file_id is None:
 
472
                    file_id = gen_file_id(f)
 
473
                inv.add_path(f, kind=kind, file_id=file_id)
 
474
 
 
475
                if verbose:
 
476
                    print 'added', quotefn(f)
 
477
 
 
478
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
479
 
 
480
            self._write_inventory(inv)
 
481
        finally:
 
482
            self.unlock()
 
483
            
386
484
 
387
485
    def print_file(self, file, revno):
388
486
        """Print `file` to stdout."""
389
 
        self._need_readlock()
390
 
        tree = self.revision_tree(self.lookup_revision(revno))
391
 
        # use inventory as it was in that revision
392
 
        file_id = tree.inventory.path2id(file)
393
 
        if not file_id:
394
 
            bailout("%r is not present in revision %d" % (file, revno))
395
 
        tree.print_file(file_id)
396
 
        
 
487
        self.lock_read()
 
488
        try:
 
489
            tree = self.revision_tree(self.lookup_revision(revno))
 
490
            # use inventory as it was in that revision
 
491
            file_id = tree.inventory.path2id(file)
 
492
            if not file_id:
 
493
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
494
            tree.print_file(file_id)
 
495
        finally:
 
496
            self.unlock()
 
497
 
397
498
 
398
499
    def remove(self, files, verbose=False):
399
500
        """Mark nominated files for removal from the inventory.
402
503
 
403
504
        TODO: Refuse to remove modified files unless --force is given?
404
505
 
405
 
        >>> b = ScratchBranch(files=['foo'])
406
 
        >>> b.add('foo')
407
 
        >>> b.inventory.has_filename('foo')
408
 
        True
409
 
        >>> b.remove('foo')
410
 
        >>> b.working_tree().has_filename('foo')
411
 
        True
412
 
        >>> b.inventory.has_filename('foo')
413
 
        False
414
 
        
415
 
        >>> b = ScratchBranch(files=['foo'])
416
 
        >>> b.add('foo')
417
 
        >>> b.commit('one')
418
 
        >>> b.remove('foo')
419
 
        >>> b.commit('two')
420
 
        >>> b.inventory.has_filename('foo') 
421
 
        False
422
 
        >>> b.basis_tree().has_filename('foo') 
423
 
        False
424
 
        >>> b.working_tree().has_filename('foo') 
425
 
        True
426
 
 
427
506
        TODO: Do something useful with directories.
428
507
 
429
508
        TODO: Should this remove the text or not?  Tough call; not
433
512
        """
434
513
        ## TODO: Normalize names
435
514
        ## TODO: Remove nested loops; better scalability
436
 
        self._need_writelock()
437
 
 
438
 
        if isinstance(files, types.StringTypes):
 
515
        if isinstance(files, basestring):
439
516
            files = [files]
440
 
        
441
 
        tree = self.working_tree()
442
 
        inv = tree.inventory
443
 
 
444
 
        # do this before any modifications
445
 
        for f in files:
446
 
            fid = inv.path2id(f)
447
 
            if not fid:
448
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
449
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
450
 
            if verbose:
451
 
                # having remove it, it must be either ignored or unknown
452
 
                if tree.is_ignored(f):
453
 
                    new_status = 'I'
454
 
                else:
455
 
                    new_status = '?'
456
 
                show_status(new_status, inv[fid].kind, quotefn(f))
457
 
            del inv[fid]
458
 
 
 
517
 
 
518
        self.lock_write()
 
519
 
 
520
        try:
 
521
            tree = self.working_tree()
 
522
            inv = tree.inventory
 
523
 
 
524
            # do this before any modifications
 
525
            for f in files:
 
526
                fid = inv.path2id(f)
 
527
                if not fid:
 
528
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
529
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
530
                if verbose:
 
531
                    # having remove it, it must be either ignored or unknown
 
532
                    if tree.is_ignored(f):
 
533
                        new_status = 'I'
 
534
                    else:
 
535
                        new_status = '?'
 
536
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
537
                del inv[fid]
 
538
 
 
539
            self._write_inventory(inv)
 
540
        finally:
 
541
            self.unlock()
 
542
 
 
543
 
 
544
    # FIXME: this doesn't need to be a branch method
 
545
    def set_inventory(self, new_inventory_list):
 
546
        from bzrlib.inventory import Inventory, InventoryEntry
 
547
        inv = Inventory(self.get_root_id())
 
548
        for path, file_id, parent, kind in new_inventory_list:
 
549
            name = os.path.basename(path)
 
550
            if name == "":
 
551
                continue
 
552
            inv.add(InventoryEntry(file_id, name, kind, parent))
459
553
        self._write_inventory(inv)
460
554
 
461
555
 
478
572
        return self.working_tree().unknowns()
479
573
 
480
574
 
481
 
    def commit(self, message, timestamp=None, timezone=None,
482
 
               committer=None,
483
 
               verbose=False):
484
 
        """Commit working copy as a new revision.
485
 
        
486
 
        The basic approach is to add all the file texts into the
487
 
        store, then the inventory, then make a new revision pointing
488
 
        to that inventory and store that.
489
 
        
490
 
        This is not quite safe if the working copy changes during the
491
 
        commit; for the moment that is simply not allowed.  A better
492
 
        approach is to make a temporary copy of the files before
493
 
        computing their hashes, and then add those hashes in turn to
494
 
        the inventory.  This should mean at least that there are no
495
 
        broken hash pointers.  There is no way we can get a snapshot
496
 
        of the whole directory at an instant.  This would also have to
497
 
        be robust against files disappearing, moving, etc.  So the
498
 
        whole thing is a bit hard.
499
 
 
500
 
        timestamp -- if not None, seconds-since-epoch for a
501
 
             postdated/predated commit.
502
 
        """
503
 
        self._need_writelock()
504
 
 
505
 
        ## TODO: Show branch names
506
 
 
507
 
        # TODO: Don't commit if there are no changes, unless forced?
508
 
 
509
 
        # First walk over the working inventory; and both update that
510
 
        # and also build a new revision inventory.  The revision
511
 
        # inventory needs to hold the text-id, sha1 and size of the
512
 
        # actual file versions committed in the revision.  (These are
513
 
        # not present in the working inventory.)  We also need to
514
 
        # detect missing/deleted files, and remove them from the
515
 
        # working inventory.
516
 
 
517
 
        work_inv = self.read_working_inventory()
518
 
        inv = Inventory()
519
 
        basis = self.basis_tree()
520
 
        basis_inv = basis.inventory
521
 
        missing_ids = []
522
 
        for path, entry in work_inv.iter_entries():
523
 
            ## TODO: Cope with files that have gone missing.
524
 
 
525
 
            ## TODO: Check that the file kind has not changed from the previous
526
 
            ## revision of this file (if any).
527
 
 
528
 
            entry = entry.copy()
529
 
 
530
 
            p = self.abspath(path)
531
 
            file_id = entry.file_id
532
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
533
 
 
534
 
            if not os.path.exists(p):
535
 
                mutter("    file is missing, removing from inventory")
536
 
                if verbose:
537
 
                    show_status('D', entry.kind, quotefn(path))
538
 
                missing_ids.append(file_id)
539
 
                continue
540
 
 
541
 
            # TODO: Handle files that have been deleted
542
 
 
543
 
            # TODO: Maybe a special case for empty files?  Seems a
544
 
            # waste to store them many times.
545
 
 
546
 
            inv.add(entry)
547
 
 
548
 
            if basis_inv.has_id(file_id):
549
 
                old_kind = basis_inv[file_id].kind
550
 
                if old_kind != entry.kind:
551
 
                    bailout("entry %r changed kind from %r to %r"
552
 
                            % (file_id, old_kind, entry.kind))
553
 
 
554
 
            if entry.kind == 'directory':
555
 
                if not isdir(p):
556
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
557
 
            elif entry.kind == 'file':
558
 
                if not isfile(p):
559
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
560
 
 
561
 
                content = file(p, 'rb').read()
562
 
 
563
 
                entry.text_sha1 = sha_string(content)
564
 
                entry.text_size = len(content)
565
 
 
566
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
567
 
                if (old_ie
568
 
                    and (old_ie.text_size == entry.text_size)
569
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
570
 
                    ## assert content == basis.get_file(file_id).read()
571
 
                    entry.text_id = basis_inv[file_id].text_id
572
 
                    mutter('    unchanged from previous text_id {%s}' %
573
 
                           entry.text_id)
574
 
                    
575
 
                else:
576
 
                    entry.text_id = gen_file_id(entry.name)
577
 
                    self.text_store.add(content, entry.text_id)
578
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
579
 
                    if verbose:
580
 
                        if not old_ie:
581
 
                            state = 'A'
582
 
                        elif (old_ie.name == entry.name
583
 
                              and old_ie.parent_id == entry.parent_id):
584
 
                            state = 'M'
585
 
                        else:
586
 
                            state = 'R'
587
 
 
588
 
                        show_status(state, entry.kind, quotefn(path))
589
 
 
590
 
        for file_id in missing_ids:
591
 
            # have to do this later so we don't mess up the iterator.
592
 
            # since parents may be removed before their children we
593
 
            # have to test.
594
 
 
595
 
            # FIXME: There's probably a better way to do this; perhaps
596
 
            # the workingtree should know how to filter itself.
597
 
            if work_inv.has_id(file_id):
598
 
                del work_inv[file_id]
599
 
 
600
 
 
601
 
        inv_id = rev_id = _gen_revision_id(time.time())
602
 
        
603
 
        inv_tmp = tempfile.TemporaryFile()
604
 
        inv.write_xml(inv_tmp)
605
 
        inv_tmp.seek(0)
606
 
        self.inventory_store.add(inv_tmp, inv_id)
607
 
        mutter('new inventory_id is {%s}' % inv_id)
608
 
 
609
 
        self._write_inventory(work_inv)
610
 
 
611
 
        if timestamp == None:
612
 
            timestamp = time.time()
613
 
 
614
 
        if committer == None:
615
 
            committer = username()
616
 
 
617
 
        if timezone == None:
618
 
            timezone = local_time_offset()
619
 
 
620
 
        mutter("building commit log message")
621
 
        rev = Revision(timestamp=timestamp,
622
 
                       timezone=timezone,
623
 
                       committer=committer,
624
 
                       precursor = self.last_patch(),
625
 
                       message = message,
626
 
                       inventory_id=inv_id,
627
 
                       revision_id=rev_id)
628
 
 
629
 
        rev_tmp = tempfile.TemporaryFile()
630
 
        rev.write_xml(rev_tmp)
631
 
        rev_tmp.seek(0)
632
 
        self.revision_store.add(rev_tmp, rev_id)
633
 
        mutter("new revision_id is {%s}" % rev_id)
634
 
        
635
 
        ## XXX: Everything up to here can simply be orphaned if we abort
636
 
        ## the commit; it will leave junk files behind but that doesn't
637
 
        ## matter.
638
 
 
639
 
        ## TODO: Read back the just-generated changeset, and make sure it
640
 
        ## applies and recreates the right state.
641
 
 
642
 
        ## TODO: Also calculate and store the inventory SHA1
643
 
        mutter("committing patch r%d" % (self.revno() + 1))
644
 
 
645
 
 
646
 
        self.append_revision(rev_id)
647
 
        
648
 
        if verbose:
649
 
            note("commited r%d" % self.revno())
650
 
 
651
 
 
652
 
    def append_revision(self, revision_id):
653
 
        mutter("add {%s} to revision-history" % revision_id)
 
575
    def append_revision(self, *revision_ids):
 
576
        from bzrlib.atomicfile import AtomicFile
 
577
 
 
578
        for revision_id in revision_ids:
 
579
            mutter("add {%s} to revision-history" % revision_id)
 
580
 
654
581
        rev_history = self.revision_history()
655
 
 
656
 
        tmprhname = self.controlfilename('revision-history.tmp')
657
 
        rhname = self.controlfilename('revision-history')
658
 
        
659
 
        f = file(tmprhname, 'wt')
660
 
        rev_history.append(revision_id)
661
 
        f.write('\n'.join(rev_history))
662
 
        f.write('\n')
663
 
        f.close()
664
 
 
665
 
        if sys.platform == 'win32':
666
 
            os.remove(rhname)
667
 
        os.rename(tmprhname, rhname)
668
 
        
 
582
        rev_history.extend(revision_ids)
 
583
 
 
584
        f = AtomicFile(self.controlfilename('revision-history'))
 
585
        try:
 
586
            for rev_id in rev_history:
 
587
                print >>f, rev_id
 
588
            f.commit()
 
589
        finally:
 
590
            f.close()
 
591
 
 
592
 
 
593
    def get_revision_xml(self, revision_id):
 
594
        """Return XML file object for revision object."""
 
595
        if not revision_id or not isinstance(revision_id, basestring):
 
596
            raise InvalidRevisionId(revision_id)
 
597
 
 
598
        self.lock_read()
 
599
        try:
 
600
            try:
 
601
                return self.revision_store[revision_id]
 
602
            except IndexError:
 
603
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
604
        finally:
 
605
            self.unlock()
669
606
 
670
607
 
671
608
    def get_revision(self, revision_id):
672
609
        """Return the Revision object for a named revision"""
673
 
        self._need_readlock()
674
 
        r = Revision.read_xml(self.revision_store[revision_id])
 
610
        xml_file = self.get_revision_xml(revision_id)
 
611
 
 
612
        try:
 
613
            r = unpack_xml(Revision, xml_file)
 
614
        except SyntaxError, e:
 
615
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
616
                                         [revision_id,
 
617
                                          str(e)])
 
618
            
675
619
        assert r.revision_id == revision_id
676
620
        return r
677
621
 
678
622
 
 
623
    def get_revision_delta(self, revno):
 
624
        """Return the delta for one revision.
 
625
 
 
626
        The delta is relative to its mainline predecessor, or the
 
627
        empty tree for revision 1.
 
628
        """
 
629
        assert isinstance(revno, int)
 
630
        rh = self.revision_history()
 
631
        if not (1 <= revno <= len(rh)):
 
632
            raise InvalidRevisionNumber(revno)
 
633
 
 
634
        # revno is 1-based; list is 0-based
 
635
 
 
636
        new_tree = self.revision_tree(rh[revno-1])
 
637
        if revno == 1:
 
638
            old_tree = EmptyTree()
 
639
        else:
 
640
            old_tree = self.revision_tree(rh[revno-2])
 
641
 
 
642
        return compare_trees(old_tree, new_tree)
 
643
 
 
644
        
 
645
 
 
646
    def get_revision_sha1(self, revision_id):
 
647
        """Hash the stored value of a revision, and return it."""
 
648
        # In the future, revision entries will be signed. At that
 
649
        # point, it is probably best *not* to include the signature
 
650
        # in the revision hash. Because that lets you re-sign
 
651
        # the revision, (add signatures/remove signatures) and still
 
652
        # have all hash pointers stay consistent.
 
653
        # But for now, just hash the contents.
 
654
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
655
 
 
656
 
679
657
    def get_inventory(self, inventory_id):
680
658
        """Get Inventory object by hash.
681
659
 
682
660
        TODO: Perhaps for this and similar methods, take a revision
683
661
               parameter which can be either an integer revno or a
684
662
               string hash."""
685
 
        self._need_readlock()
686
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
687
 
        return i
 
663
        from bzrlib.inventory import Inventory
 
664
        from bzrlib.xml import unpack_xml
 
665
 
 
666
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
 
667
 
 
668
 
 
669
    def get_inventory_xml(self, inventory_id):
 
670
        """Get inventory XML as a file object."""
 
671
        return self.inventory_store[inventory_id]
 
672
            
 
673
 
 
674
    def get_inventory_sha1(self, inventory_id):
 
675
        """Return the sha1 hash of the inventory entry
 
676
        """
 
677
        return sha_file(self.get_inventory_xml(inventory_id))
688
678
 
689
679
 
690
680
    def get_revision_inventory(self, revision_id):
691
681
        """Return inventory of a past revision."""
692
 
        self._need_readlock()
 
682
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
683
        # must be the same as its revision, so this is trivial.
693
684
        if revision_id == None:
694
 
            return Inventory()
 
685
            from bzrlib.inventory import Inventory
 
686
            return Inventory(self.get_root_id())
695
687
        else:
696
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
688
            return self.get_inventory(revision_id)
697
689
 
698
690
 
699
691
    def revision_history(self):
702
694
        >>> ScratchBranch().revision_history()
703
695
        []
704
696
        """
705
 
        self._need_readlock()
706
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
707
 
 
708
 
 
709
 
    def enum_history(self, direction):
710
 
        """Return (revno, revision_id) for history of branch.
711
 
 
712
 
        direction
713
 
            'forward' is from earliest to latest
714
 
            'reverse' is from latest to earliest
715
 
        """
716
 
        rh = self.revision_history()
717
 
        if direction == 'forward':
718
 
            i = 1
719
 
            for rid in rh:
720
 
                yield i, rid
721
 
                i += 1
722
 
        elif direction == 'reverse':
723
 
            i = len(rh)
724
 
            while i > 0:
725
 
                yield i, rh[i-1]
726
 
                i -= 1
727
 
        else:
728
 
            raise BzrError('invalid history direction %r' % direction)
 
697
        self.lock_read()
 
698
        try:
 
699
            return [l.rstrip('\r\n') for l in
 
700
                    self.controlfile('revision-history', 'r').readlines()]
 
701
        finally:
 
702
            self.unlock()
 
703
 
 
704
 
 
705
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
706
        """
 
707
        >>> import commit
 
708
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
709
        >>> sb.common_ancestor(sb) == (None, None)
 
710
        True
 
711
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
712
        >>> sb.common_ancestor(sb)[0]
 
713
        1
 
714
        >>> clone = sb.clone()
 
715
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
716
        >>> sb.common_ancestor(sb)[0]
 
717
        2
 
718
        >>> sb.common_ancestor(clone)[0]
 
719
        1
 
720
        >>> commit.commit(clone, "Committing divergent second revision", 
 
721
        ...               verbose=False)
 
722
        >>> sb.common_ancestor(clone)[0]
 
723
        1
 
724
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
725
        True
 
726
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
727
        True
 
728
        >>> clone2 = sb.clone()
 
729
        >>> sb.common_ancestor(clone2)[0]
 
730
        2
 
731
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
732
        1
 
733
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
734
        1
 
735
        """
 
736
        my_history = self.revision_history()
 
737
        other_history = other.revision_history()
 
738
        if self_revno is None:
 
739
            self_revno = len(my_history)
 
740
        if other_revno is None:
 
741
            other_revno = len(other_history)
 
742
        indices = range(min((self_revno, other_revno)))
 
743
        indices.reverse()
 
744
        for r in indices:
 
745
            if my_history[r] == other_history[r]:
 
746
                return r+1, my_history[r]
 
747
        return None, None
729
748
 
730
749
 
731
750
    def revno(self):
733
752
 
734
753
        That is equivalent to the number of revisions committed to
735
754
        this branch.
736
 
 
737
 
        >>> b = ScratchBranch()
738
 
        >>> b.revno()
739
 
        0
740
 
        >>> b.commit('no foo')
741
 
        >>> b.revno()
742
 
        1
743
755
        """
744
756
        return len(self.revision_history())
745
757
 
746
758
 
747
759
    def last_patch(self):
748
760
        """Return last patch hash, or None if no history.
749
 
 
750
 
        >>> ScratchBranch().last_patch() == None
751
 
        True
752
761
        """
753
762
        ph = self.revision_history()
754
763
        if ph:
755
764
            return ph[-1]
756
765
        else:
757
766
            return None
758
 
        
759
 
 
760
 
    def lookup_revision(self, revno):
761
 
        """Return revision hash for revision number."""
762
 
        if revno == 0:
763
 
            return None
764
 
 
765
 
        try:
766
 
            # list is 0-based; revisions are 1-based
767
 
            return self.revision_history()[revno-1]
768
 
        except IndexError:
769
 
            raise BzrError("no such revision %s" % revno)
770
 
 
 
767
 
 
768
 
 
769
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
770
        """
 
771
        If self and other have not diverged, return a list of the revisions
 
772
        present in other, but missing from self.
 
773
 
 
774
        >>> from bzrlib.commit import commit
 
775
        >>> bzrlib.trace.silent = True
 
776
        >>> br1 = ScratchBranch()
 
777
        >>> br2 = ScratchBranch()
 
778
        >>> br1.missing_revisions(br2)
 
779
        []
 
780
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
781
        >>> br1.missing_revisions(br2)
 
782
        [u'REVISION-ID-1']
 
783
        >>> br2.missing_revisions(br1)
 
784
        []
 
785
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
786
        >>> br1.missing_revisions(br2)
 
787
        []
 
788
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
789
        >>> br1.missing_revisions(br2)
 
790
        [u'REVISION-ID-2A']
 
791
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
792
        >>> br1.missing_revisions(br2)
 
793
        Traceback (most recent call last):
 
794
        DivergedBranches: These branches have diverged.
 
795
        """
 
796
        self_history = self.revision_history()
 
797
        self_len = len(self_history)
 
798
        other_history = other.revision_history()
 
799
        other_len = len(other_history)
 
800
        common_index = min(self_len, other_len) -1
 
801
        if common_index >= 0 and \
 
802
            self_history[common_index] != other_history[common_index]:
 
803
            raise DivergedBranches(self, other)
 
804
 
 
805
        if stop_revision is None:
 
806
            stop_revision = other_len
 
807
        elif stop_revision > other_len:
 
808
            raise NoSuchRevision(self, stop_revision)
 
809
        
 
810
        return other_history[self_len:stop_revision]
 
811
 
 
812
 
 
813
    def update_revisions(self, other, stop_revision=None, revision_ids=None):
 
814
        """Pull in all new revisions from other branch.
 
815
        
 
816
        >>> from bzrlib.commit import commit
 
817
        >>> bzrlib.trace.silent = True
 
818
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
819
        >>> br1.add('foo')
 
820
        >>> br1.add('bar')
 
821
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
822
        >>> br2 = ScratchBranch()
 
823
        >>> br2.update_revisions(br1)
 
824
        Added 2 texts.
 
825
        Added 1 inventories.
 
826
        Added 1 revisions.
 
827
        >>> br2.revision_history()
 
828
        [u'REVISION-ID-1']
 
829
        >>> br2.update_revisions(br1)
 
830
        Added 0 texts.
 
831
        Added 0 inventories.
 
832
        Added 0 revisions.
 
833
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
834
        True
 
835
        """
 
836
        from bzrlib.progress import ProgressBar
 
837
 
 
838
        pb = ProgressBar()
 
839
 
 
840
        pb.update('comparing histories')
 
841
        if revision_ids is None:
 
842
            revision_ids = self.missing_revisions(other, stop_revision)
 
843
 
 
844
        if hasattr(other.revision_store, "prefetch"):
 
845
            other.revision_store.prefetch(revision_ids)
 
846
        if hasattr(other.inventory_store, "prefetch"):
 
847
            inventory_ids = [other.get_revision(r).inventory_id
 
848
                             for r in revision_ids]
 
849
            other.inventory_store.prefetch(inventory_ids)
 
850
                
 
851
        revisions = []
 
852
        needed_texts = set()
 
853
        i = 0
 
854
        for rev_id in revision_ids:
 
855
            i += 1
 
856
            pb.update('fetching revision', i, len(revision_ids))
 
857
            rev = other.get_revision(rev_id)
 
858
            revisions.append(rev)
 
859
            inv = other.get_inventory(str(rev.inventory_id))
 
860
            for key, entry in inv.iter_entries():
 
861
                if entry.text_id is None:
 
862
                    continue
 
863
                if entry.text_id not in self.text_store:
 
864
                    needed_texts.add(entry.text_id)
 
865
 
 
866
        pb.clear()
 
867
                    
 
868
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
869
        print "Added %d texts." % count 
 
870
        inventory_ids = [ f.inventory_id for f in revisions ]
 
871
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
872
                                                inventory_ids)
 
873
        print "Added %d inventories." % count 
 
874
        revision_ids = [ f.revision_id for f in revisions]
 
875
        count = self.revision_store.copy_multi(other.revision_store, 
 
876
                                               revision_ids)
 
877
        for revision_id in revision_ids:
 
878
            self.append_revision(revision_id)
 
879
        print "Added %d revisions." % count
 
880
                    
 
881
        
 
882
    def commit(self, *args, **kw):
 
883
        from bzrlib.commit import commit
 
884
        commit(self, *args, **kw)
 
885
        
 
886
 
 
887
    def lookup_revision(self, revision):
 
888
        """Return the revision identifier for a given revision information."""
 
889
        revno, info = self.get_revision_info(revision)
 
890
        return info
 
891
 
 
892
    def get_revision_info(self, revision):
 
893
        """Return (revno, revision id) for revision identifier.
 
894
 
 
895
        revision can be an integer, in which case it is assumed to be revno (though
 
896
            this will translate negative values into positive ones)
 
897
        revision can also be a string, in which case it is parsed for something like
 
898
            'date:' or 'revid:' etc.
 
899
        """
 
900
        if revision is None:
 
901
            return 0, None
 
902
        revno = None
 
903
        try:# Convert to int if possible
 
904
            revision = int(revision)
 
905
        except ValueError:
 
906
            pass
 
907
        revs = self.revision_history()
 
908
        if isinstance(revision, int):
 
909
            if revision == 0:
 
910
                return 0, None
 
911
            # Mabye we should do this first, but we don't need it if revision == 0
 
912
            if revision < 0:
 
913
                revno = len(revs) + revision + 1
 
914
            else:
 
915
                revno = revision
 
916
        elif isinstance(revision, basestring):
 
917
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
918
                if revision.startswith(prefix):
 
919
                    revno = func(self, revs, revision)
 
920
                    break
 
921
            else:
 
922
                raise BzrError('No namespace registered for string: %r' % revision)
 
923
 
 
924
        if revno is None or revno <= 0 or revno > len(revs):
 
925
            raise BzrError("no such revision %s" % revision)
 
926
        return revno, revs[revno-1]
 
927
 
 
928
    def _namespace_revno(self, revs, revision):
 
929
        """Lookup a revision by revision number"""
 
930
        assert revision.startswith('revno:')
 
931
        try:
 
932
            return int(revision[6:])
 
933
        except ValueError:
 
934
            return None
 
935
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
936
 
 
937
    def _namespace_revid(self, revs, revision):
 
938
        assert revision.startswith('revid:')
 
939
        try:
 
940
            return revs.index(revision[6:]) + 1
 
941
        except ValueError:
 
942
            return None
 
943
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
944
 
 
945
    def _namespace_last(self, revs, revision):
 
946
        assert revision.startswith('last:')
 
947
        try:
 
948
            offset = int(revision[5:])
 
949
        except ValueError:
 
950
            return None
 
951
        else:
 
952
            if offset <= 0:
 
953
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
954
            return len(revs) - offset + 1
 
955
    REVISION_NAMESPACES['last:'] = _namespace_last
 
956
 
 
957
    def _namespace_tag(self, revs, revision):
 
958
        assert revision.startswith('tag:')
 
959
        raise BzrError('tag: namespace registered, but not implemented.')
 
960
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
961
 
 
962
    def _namespace_date(self, revs, revision):
 
963
        assert revision.startswith('date:')
 
964
        import datetime
 
965
        # Spec for date revisions:
 
966
        #   date:value
 
967
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
968
        #   it can also start with a '+/-/='. '+' says match the first
 
969
        #   entry after the given date. '-' is match the first entry before the date
 
970
        #   '=' is match the first entry after, but still on the given date.
 
971
        #
 
972
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
973
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
974
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
975
        #       May 13th, 2005 at 0:00
 
976
        #
 
977
        #   So the proper way of saying 'give me all entries for today' is:
 
978
        #       -r {date:+today}:{date:-tomorrow}
 
979
        #   The default is '=' when not supplied
 
980
        val = revision[5:]
 
981
        match_style = '='
 
982
        if val[:1] in ('+', '-', '='):
 
983
            match_style = val[:1]
 
984
            val = val[1:]
 
985
 
 
986
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
987
        if val.lower() == 'yesterday':
 
988
            dt = today - datetime.timedelta(days=1)
 
989
        elif val.lower() == 'today':
 
990
            dt = today
 
991
        elif val.lower() == 'tomorrow':
 
992
            dt = today + datetime.timedelta(days=1)
 
993
        else:
 
994
            import re
 
995
            # This should be done outside the function to avoid recompiling it.
 
996
            _date_re = re.compile(
 
997
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
998
                    r'(,|T)?\s*'
 
999
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1000
                )
 
1001
            m = _date_re.match(val)
 
1002
            if not m or (not m.group('date') and not m.group('time')):
 
1003
                raise BzrError('Invalid revision date %r' % revision)
 
1004
 
 
1005
            if m.group('date'):
 
1006
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1007
            else:
 
1008
                year, month, day = today.year, today.month, today.day
 
1009
            if m.group('time'):
 
1010
                hour = int(m.group('hour'))
 
1011
                minute = int(m.group('minute'))
 
1012
                if m.group('second'):
 
1013
                    second = int(m.group('second'))
 
1014
                else:
 
1015
                    second = 0
 
1016
            else:
 
1017
                hour, minute, second = 0,0,0
 
1018
 
 
1019
            dt = datetime.datetime(year=year, month=month, day=day,
 
1020
                    hour=hour, minute=minute, second=second)
 
1021
        first = dt
 
1022
        last = None
 
1023
        reversed = False
 
1024
        if match_style == '-':
 
1025
            reversed = True
 
1026
        elif match_style == '=':
 
1027
            last = dt + datetime.timedelta(days=1)
 
1028
 
 
1029
        if reversed:
 
1030
            for i in range(len(revs)-1, -1, -1):
 
1031
                r = self.get_revision(revs[i])
 
1032
                # TODO: Handle timezone.
 
1033
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1034
                if first >= dt and (last is None or dt >= last):
 
1035
                    return i+1
 
1036
        else:
 
1037
            for i in range(len(revs)):
 
1038
                r = self.get_revision(revs[i])
 
1039
                # TODO: Handle timezone.
 
1040
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1041
                if first <= dt and (last is None or dt <= last):
 
1042
                    return i+1
 
1043
    REVISION_NAMESPACES['date:'] = _namespace_date
771
1044
 
772
1045
    def revision_tree(self, revision_id):
773
1046
        """Return Tree for a revision on this branch.
774
1047
 
775
1048
        `revision_id` may be None for the null revision, in which case
776
1049
        an `EmptyTree` is returned."""
777
 
        self._need_readlock()
 
1050
        # TODO: refactor this to use an existing revision object
 
1051
        # so we don't need to read it in twice.
778
1052
        if revision_id == None:
779
1053
            return EmptyTree()
780
1054
        else:
792
1066
        """Return `Tree` object for last revision.
793
1067
 
794
1068
        If there are no revisions yet, return an `EmptyTree`.
795
 
 
796
 
        >>> b = ScratchBranch(files=['foo'])
797
 
        >>> b.basis_tree().has_filename('foo')
798
 
        False
799
 
        >>> b.working_tree().has_filename('foo')
800
 
        True
801
 
        >>> b.add('foo')
802
 
        >>> b.commit('add foo')
803
 
        >>> b.basis_tree().has_filename('foo')
804
 
        True
805
1069
        """
806
1070
        r = self.last_patch()
807
1071
        if r == None:
816
1080
 
817
1081
        This can change the directory or the filename or both.
818
1082
        """
819
 
        self._need_writelock()
820
 
        tree = self.working_tree()
821
 
        inv = tree.inventory
822
 
        if not tree.has_filename(from_rel):
823
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
824
 
        if tree.has_filename(to_rel):
825
 
            bailout("can't rename: new working file %r already exists" % to_rel)
826
 
            
827
 
        file_id = inv.path2id(from_rel)
828
 
        if file_id == None:
829
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
830
 
 
831
 
        if inv.path2id(to_rel):
832
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
833
 
 
834
 
        to_dir, to_tail = os.path.split(to_rel)
835
 
        to_dir_id = inv.path2id(to_dir)
836
 
        if to_dir_id == None and to_dir != '':
837
 
            bailout("can't determine destination directory id for %r" % to_dir)
838
 
 
839
 
        mutter("rename_one:")
840
 
        mutter("  file_id    {%s}" % file_id)
841
 
        mutter("  from_rel   %r" % from_rel)
842
 
        mutter("  to_rel     %r" % to_rel)
843
 
        mutter("  to_dir     %r" % to_dir)
844
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
845
 
            
846
 
        inv.rename(file_id, to_dir_id, to_tail)
847
 
 
848
 
        print "%s => %s" % (from_rel, to_rel)
849
 
        
850
 
        from_abs = self.abspath(from_rel)
851
 
        to_abs = self.abspath(to_rel)
 
1083
        self.lock_write()
852
1084
        try:
853
 
            os.rename(from_abs, to_abs)
854
 
        except OSError, e:
855
 
            bailout("failed to rename %r to %r: %s"
856
 
                    % (from_abs, to_abs, e[1]),
857
 
                    ["rename rolled back"])
858
 
 
859
 
        self._write_inventory(inv)
860
 
            
 
1085
            tree = self.working_tree()
 
1086
            inv = tree.inventory
 
1087
            if not tree.has_filename(from_rel):
 
1088
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1089
            if tree.has_filename(to_rel):
 
1090
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1091
 
 
1092
            file_id = inv.path2id(from_rel)
 
1093
            if file_id == None:
 
1094
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1095
 
 
1096
            if inv.path2id(to_rel):
 
1097
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1098
 
 
1099
            to_dir, to_tail = os.path.split(to_rel)
 
1100
            to_dir_id = inv.path2id(to_dir)
 
1101
            if to_dir_id == None and to_dir != '':
 
1102
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1103
 
 
1104
            mutter("rename_one:")
 
1105
            mutter("  file_id    {%s}" % file_id)
 
1106
            mutter("  from_rel   %r" % from_rel)
 
1107
            mutter("  to_rel     %r" % to_rel)
 
1108
            mutter("  to_dir     %r" % to_dir)
 
1109
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1110
 
 
1111
            inv.rename(file_id, to_dir_id, to_tail)
 
1112
 
 
1113
            print "%s => %s" % (from_rel, to_rel)
 
1114
 
 
1115
            from_abs = self.abspath(from_rel)
 
1116
            to_abs = self.abspath(to_rel)
 
1117
            try:
 
1118
                os.rename(from_abs, to_abs)
 
1119
            except OSError, e:
 
1120
                raise BzrError("failed to rename %r to %r: %s"
 
1121
                        % (from_abs, to_abs, e[1]),
 
1122
                        ["rename rolled back"])
 
1123
 
 
1124
            self._write_inventory(inv)
 
1125
        finally:
 
1126
            self.unlock()
861
1127
 
862
1128
 
863
1129
    def move(self, from_paths, to_name):
871
1137
        Note that to_name is only the last component of the new name;
872
1138
        this doesn't change the directory.
873
1139
        """
874
 
        self._need_writelock()
875
 
        ## TODO: Option to move IDs only
876
 
        assert not isinstance(from_paths, basestring)
877
 
        tree = self.working_tree()
878
 
        inv = tree.inventory
879
 
        to_abs = self.abspath(to_name)
880
 
        if not isdir(to_abs):
881
 
            bailout("destination %r is not a directory" % to_abs)
882
 
        if not tree.has_filename(to_name):
883
 
            bailout("destination %r not in working directory" % to_abs)
884
 
        to_dir_id = inv.path2id(to_name)
885
 
        if to_dir_id == None and to_name != '':
886
 
            bailout("destination %r is not a versioned directory" % to_name)
887
 
        to_dir_ie = inv[to_dir_id]
888
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
889
 
            bailout("destination %r is not a directory" % to_abs)
890
 
 
891
 
        to_idpath = Set(inv.get_idpath(to_dir_id))
892
 
 
893
 
        for f in from_paths:
894
 
            if not tree.has_filename(f):
895
 
                bailout("%r does not exist in working tree" % f)
896
 
            f_id = inv.path2id(f)
897
 
            if f_id == None:
898
 
                bailout("%r is not versioned" % f)
899
 
            name_tail = splitpath(f)[-1]
900
 
            dest_path = appendpath(to_name, name_tail)
901
 
            if tree.has_filename(dest_path):
902
 
                bailout("destination %r already exists" % dest_path)
903
 
            if f_id in to_idpath:
904
 
                bailout("can't move %r to a subdirectory of itself" % f)
905
 
 
906
 
        # OK, so there's a race here, it's possible that someone will
907
 
        # create a file in this interval and then the rename might be
908
 
        # left half-done.  But we should have caught most problems.
909
 
 
910
 
        for f in from_paths:
911
 
            name_tail = splitpath(f)[-1]
912
 
            dest_path = appendpath(to_name, name_tail)
913
 
            print "%s => %s" % (f, dest_path)
914
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
915
 
            try:
916
 
                os.rename(self.abspath(f), self.abspath(dest_path))
917
 
            except OSError, e:
918
 
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
919
 
                        ["rename rolled back"])
920
 
 
921
 
        self._write_inventory(inv)
922
 
 
 
1140
        self.lock_write()
 
1141
        try:
 
1142
            ## TODO: Option to move IDs only
 
1143
            assert not isinstance(from_paths, basestring)
 
1144
            tree = self.working_tree()
 
1145
            inv = tree.inventory
 
1146
            to_abs = self.abspath(to_name)
 
1147
            if not isdir(to_abs):
 
1148
                raise BzrError("destination %r is not a directory" % to_abs)
 
1149
            if not tree.has_filename(to_name):
 
1150
                raise BzrError("destination %r not in working directory" % to_abs)
 
1151
            to_dir_id = inv.path2id(to_name)
 
1152
            if to_dir_id == None and to_name != '':
 
1153
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1154
            to_dir_ie = inv[to_dir_id]
 
1155
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1156
                raise BzrError("destination %r is not a directory" % to_abs)
 
1157
 
 
1158
            to_idpath = inv.get_idpath(to_dir_id)
 
1159
 
 
1160
            for f in from_paths:
 
1161
                if not tree.has_filename(f):
 
1162
                    raise BzrError("%r does not exist in working tree" % f)
 
1163
                f_id = inv.path2id(f)
 
1164
                if f_id == None:
 
1165
                    raise BzrError("%r is not versioned" % f)
 
1166
                name_tail = splitpath(f)[-1]
 
1167
                dest_path = appendpath(to_name, name_tail)
 
1168
                if tree.has_filename(dest_path):
 
1169
                    raise BzrError("destination %r already exists" % dest_path)
 
1170
                if f_id in to_idpath:
 
1171
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1172
 
 
1173
            # OK, so there's a race here, it's possible that someone will
 
1174
            # create a file in this interval and then the rename might be
 
1175
            # left half-done.  But we should have caught most problems.
 
1176
 
 
1177
            for f in from_paths:
 
1178
                name_tail = splitpath(f)[-1]
 
1179
                dest_path = appendpath(to_name, name_tail)
 
1180
                print "%s => %s" % (f, dest_path)
 
1181
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1182
                try:
 
1183
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1184
                except OSError, e:
 
1185
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1186
                            ["rename rolled back"])
 
1187
 
 
1188
            self._write_inventory(inv)
 
1189
        finally:
 
1190
            self.unlock()
 
1191
 
 
1192
 
 
1193
    def revert(self, filenames, old_tree=None, backups=True):
 
1194
        """Restore selected files to the versions from a previous tree.
 
1195
 
 
1196
        backups
 
1197
            If true (default) backups are made of files before
 
1198
            they're renamed.
 
1199
        """
 
1200
        from bzrlib.errors import NotVersionedError, BzrError
 
1201
        from bzrlib.atomicfile import AtomicFile
 
1202
        from bzrlib.osutils import backup_file
 
1203
        
 
1204
        inv = self.read_working_inventory()
 
1205
        if old_tree is None:
 
1206
            old_tree = self.basis_tree()
 
1207
        old_inv = old_tree.inventory
 
1208
 
 
1209
        nids = []
 
1210
        for fn in filenames:
 
1211
            file_id = inv.path2id(fn)
 
1212
            if not file_id:
 
1213
                raise NotVersionedError("not a versioned file", fn)
 
1214
            if not old_inv.has_id(file_id):
 
1215
                raise BzrError("file not present in old tree", fn, file_id)
 
1216
            nids.append((fn, file_id))
 
1217
            
 
1218
        # TODO: Rename back if it was previously at a different location
 
1219
 
 
1220
        # TODO: If given a directory, restore the entire contents from
 
1221
        # the previous version.
 
1222
 
 
1223
        # TODO: Make a backup to a temporary file.
 
1224
 
 
1225
        # TODO: If the file previously didn't exist, delete it?
 
1226
        for fn, file_id in nids:
 
1227
            backup_file(fn)
 
1228
            
 
1229
            f = AtomicFile(fn, 'wb')
 
1230
            try:
 
1231
                f.write(old_tree.get_file(file_id).read())
 
1232
                f.commit()
 
1233
            finally:
 
1234
                f.close()
 
1235
 
 
1236
 
 
1237
    def pending_merges(self):
 
1238
        """Return a list of pending merges.
 
1239
 
 
1240
        These are revisions that have been merged into the working
 
1241
        directory but not yet committed.
 
1242
        """
 
1243
        cfn = self.controlfilename('pending-merges')
 
1244
        if not os.path.exists(cfn):
 
1245
            return []
 
1246
        p = []
 
1247
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1248
            p.append(l.rstrip('\n'))
 
1249
        return p
 
1250
 
 
1251
 
 
1252
    def add_pending_merge(self, revision_id):
 
1253
        from bzrlib.revision import validate_revision_id
 
1254
 
 
1255
        validate_revision_id(revision_id)
 
1256
 
 
1257
        p = self.pending_merges()
 
1258
        if revision_id in p:
 
1259
            return
 
1260
        p.append(revision_id)
 
1261
        self.set_pending_merges(p)
 
1262
 
 
1263
 
 
1264
    def set_pending_merges(self, rev_list):
 
1265
        from bzrlib.atomicfile import AtomicFile
 
1266
        self.lock_write()
 
1267
        try:
 
1268
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1269
            try:
 
1270
                for l in rev_list:
 
1271
                    print >>f, l
 
1272
                f.commit()
 
1273
            finally:
 
1274
                f.close()
 
1275
        finally:
 
1276
            self.unlock()
923
1277
 
924
1278
 
925
1279
 
934
1288
    >>> isdir(bd)
935
1289
    False
936
1290
    """
937
 
    def __init__(self, files=[], dirs=[]):
 
1291
    def __init__(self, files=[], dirs=[], base=None):
938
1292
        """Make a test branch.
939
1293
 
940
1294
        This creates a temporary directory and runs init-tree in it.
941
1295
 
942
1296
        If any files are listed, they are created in the working copy.
943
1297
        """
944
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
1298
        from tempfile import mkdtemp
 
1299
        init = False
 
1300
        if base is None:
 
1301
            base = mkdtemp()
 
1302
            init = True
 
1303
        Branch.__init__(self, base, init=init)
945
1304
        for d in dirs:
946
1305
            os.mkdir(self.abspath(d))
947
1306
            
949
1308
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
950
1309
 
951
1310
 
 
1311
    def clone(self):
 
1312
        """
 
1313
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
1314
        >>> clone = orig.clone()
 
1315
        >>> os.path.samefile(orig.base, clone.base)
 
1316
        False
 
1317
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
1318
        True
 
1319
        """
 
1320
        from shutil import copytree
 
1321
        from tempfile import mkdtemp
 
1322
        base = mkdtemp()
 
1323
        os.rmdir(base)
 
1324
        copytree(self.base, base, symlinks=True)
 
1325
        return ScratchBranch(base=base)
 
1326
        
952
1327
    def __del__(self):
953
1328
        self.destroy()
954
1329
 
955
1330
    def destroy(self):
956
1331
        """Destroy the test branch, removing the scratch directory."""
 
1332
        from shutil import rmtree
957
1333
        try:
958
 
            mutter("delete ScratchBranch %s" % self.base)
959
 
            shutil.rmtree(self.base)
 
1334
            if self.base:
 
1335
                mutter("delete ScratchBranch %s" % self.base)
 
1336
                rmtree(self.base)
960
1337
        except OSError, e:
961
1338
            # Work around for shutil.rmtree failing on Windows when
962
1339
            # readonly files are encountered
964
1341
            for root, dirs, files in os.walk(self.base, topdown=False):
965
1342
                for name in files:
966
1343
                    os.chmod(os.path.join(root, name), 0700)
967
 
            shutil.rmtree(self.base)
 
1344
            rmtree(self.base)
968
1345
        self.base = None
969
1346
 
970
1347
    
988
1365
 
989
1366
 
990
1367
 
991
 
def _gen_revision_id(when):
992
 
    """Return new revision-id."""
993
 
    s = '%s-%s-' % (user_email(), compact_date(when))
994
 
    s += hexlify(rand_bytes(8))
995
 
    return s
996
 
 
997
 
 
998
1368
def gen_file_id(name):
999
1369
    """Return new file id.
1000
1370
 
1001
1371
    This should probably generate proper UUIDs, but for the moment we
1002
1372
    cope with just randomness because running uuidgen every time is
1003
1373
    slow."""
 
1374
    import re
 
1375
    from binascii import hexlify
 
1376
    from time import time
 
1377
 
 
1378
    # get last component
1004
1379
    idx = name.rfind('/')
1005
1380
    if idx != -1:
1006
1381
        name = name[idx+1 : ]
1008
1383
    if idx != -1:
1009
1384
        name = name[idx+1 : ]
1010
1385
 
 
1386
    # make it not a hidden file
1011
1387
    name = name.lstrip('.')
1012
1388
 
 
1389
    # remove any wierd characters; we don't escape them but rather
 
1390
    # just pull them out
 
1391
    name = re.sub(r'[^\w.]', '', name)
 
1392
 
1013
1393
    s = hexlify(rand_bytes(8))
1014
 
    return '-'.join((name, compact_date(time.time()), s))
 
1394
    return '-'.join((name, compact_date(time()), s))
 
1395
 
 
1396
 
 
1397
def gen_root_id():
 
1398
    """Return a new tree-root file id."""
 
1399
    return gen_file_id('TREE_ROOT')
 
1400