~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-06-24 09:44:44 UTC
  • Revision ID: mbp@sourcefrog.net-20050624094443-d3c0009f76ea8972
- better display of test failure tracebacks

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
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
19
import traceback, socket, fnmatch, difflib, time
22
20
from binascii import hexlify
24
22
import bzrlib
25
23
from inventory import Inventory
26
24
from trace import mutter, note
27
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
 
25
from tree import Tree, EmptyTree, RevisionTree
28
26
from inventory import InventoryEntry, Inventory
29
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
28
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
 
     joinpath, sha_string, file_kind, local_time_offset, appendpath
 
29
     joinpath, sha_file, sha_string, file_kind, local_time_offset, appendpath
32
30
from store import ImmutableStore
33
31
from revision import Revision
34
 
from errors import bailout, BzrError
 
32
from errors import BzrError
35
33
from textui import show_status
36
 
from diff import diff_trees
37
34
 
38
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
36
## TODO: Maybe include checks for common corruption of newlines, etc?
41
38
 
42
39
 
43
40
def find_branch(f, **args):
44
 
    if f.startswith('http://') or f.startswith('https://'):
 
41
    if f and (f.startswith('http://') or f.startswith('https://')):
45
42
        import remotebranch 
46
43
        return remotebranch.RemoteBranch(f, **args)
47
44
    else:
48
45
        return Branch(f, **args)
 
46
 
 
47
 
 
48
 
 
49
def _relpath(base, path):
 
50
    """Return path relative to base, or raise exception.
 
51
 
 
52
    The path may be either an absolute path or a path relative to the
 
53
    current working directory.
 
54
 
 
55
    Lifted out of Branch.relpath for ease of testing.
 
56
 
 
57
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
58
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
59
    avoids that problem."""
 
60
    rp = os.path.abspath(path)
 
61
 
 
62
    s = []
 
63
    head = rp
 
64
    while len(head) >= len(base):
 
65
        if head == base:
 
66
            break
 
67
        head, tail = os.path.split(head)
 
68
        if tail:
 
69
            s.insert(0, tail)
 
70
    else:
 
71
        from errors import NotBranchError
 
72
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
73
 
 
74
    return os.sep.join(s)
49
75
        
50
76
 
51
77
def find_branch_root(f=None):
78
104
            raise BzrError('%r is not in a branch' % orig_f)
79
105
        f = head
80
106
    
 
107
class DivergedBranches(Exception):
 
108
    def __init__(self, branch1, branch2):
 
109
        self.branch1 = branch1
 
110
        self.branch2 = branch2
 
111
        Exception.__init__(self, "These branches have diverged.")
 
112
 
 
113
 
 
114
class NoSuchRevision(BzrError):
 
115
    def __init__(self, branch, revision):
 
116
        self.branch = branch
 
117
        self.revision = revision
 
118
        msg = "Branch %s has no revision %d" % (branch, revision)
 
119
        BzrError.__init__(self, msg)
81
120
 
82
121
 
83
122
######################################################################
84
123
# branch objects
85
124
 
86
 
class Branch:
 
125
class Branch(object):
87
126
    """Branch holding a history of revisions.
88
127
 
89
128
    base
90
129
        Base directory of the branch.
 
130
 
 
131
    _lock_mode
 
132
        None, or 'r' or 'w'
 
133
 
 
134
    _lock_count
 
135
        If _lock_mode is true, a positive count of the number of times the
 
136
        lock has been taken.
 
137
 
 
138
    _lock
 
139
        Lock object from bzrlib.lock.
91
140
    """
92
 
    _lockmode = None
 
141
    base = None
 
142
    _lock_mode = None
 
143
    _lock_count = None
 
144
    _lock = None
93
145
    
94
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
146
    def __init__(self, base, init=False, find_root=True):
95
147
        """Create new branch object at a particular location.
96
148
 
97
149
        base -- Base directory for the branch.
114
166
        else:
115
167
            self.base = os.path.realpath(base)
116
168
            if not isdir(self.controlfilename('.')):
117
 
                bailout("not a bzr branch: %s" % quotefn(base),
118
 
                        ['use "bzr init" to initialize a new working tree',
119
 
                         'current bzr can only operate from top-of-tree'])
 
169
                from errors import NotBranchError
 
170
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
171
                                     ['use "bzr init" to initialize a new working tree',
 
172
                                      'current bzr can only operate from top-of-tree'])
120
173
        self._check_format()
121
 
        self.lock(lock_mode)
122
174
 
123
175
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
124
176
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
132
184
    __repr__ = __str__
133
185
 
134
186
 
135
 
 
136
 
    def lock(self, mode='w'):
137
 
        """Lock the on-disk branch, excluding other processes."""
138
 
        try:
139
 
            import fcntl, errno
140
 
 
141
 
            if mode == 'w':
142
 
                lm = fcntl.LOCK_EX
143
 
                om = os.O_WRONLY | os.O_CREAT
144
 
            elif mode == 'r':
145
 
                lm = fcntl.LOCK_SH
146
 
                om = os.O_RDONLY
147
 
            else:
148
 
                raise BzrError("invalid locking mode %r" % mode)
149
 
 
150
 
            try:
151
 
                lockfile = os.open(self.controlfilename('branch-lock'), om)
152
 
            except OSError, e:
153
 
                if e.errno == errno.ENOENT:
154
 
                    # might not exist on branches from <0.0.4
155
 
                    self.controlfile('branch-lock', 'w').close()
156
 
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
157
 
                else:
158
 
                    raise e
 
187
    def __del__(self):
 
188
        if self._lock_mode or self._lock:
 
189
            from warnings import warn
 
190
            warn("branch %r was not explicitly unlocked" % self)
 
191
            self._lock.unlock()
 
192
 
 
193
 
 
194
 
 
195
    def lock_write(self):
 
196
        if self._lock_mode:
 
197
            if self._lock_mode != 'w':
 
198
                from errors import LockError
 
199
                raise LockError("can't upgrade to a write lock from %r" %
 
200
                                self._lock_mode)
 
201
            self._lock_count += 1
 
202
        else:
 
203
            from bzrlib.lock import WriteLock
 
204
 
 
205
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
206
            self._lock_mode = 'w'
 
207
            self._lock_count = 1
 
208
 
 
209
 
 
210
 
 
211
    def lock_read(self):
 
212
        if self._lock_mode:
 
213
            assert self._lock_mode in ('r', 'w'), \
 
214
                   "invalid lock mode %r" % self._lock_mode
 
215
            self._lock_count += 1
 
216
        else:
 
217
            from bzrlib.lock import ReadLock
 
218
 
 
219
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
220
            self._lock_mode = 'r'
 
221
            self._lock_count = 1
 
222
                        
 
223
 
159
224
            
160
 
            fcntl.lockf(lockfile, lm)
161
 
            def unlock():
162
 
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
163
 
                os.close(lockfile)
164
 
                self._lockmode = None
165
 
            self.unlock = unlock
166
 
            self._lockmode = mode
167
 
        except ImportError:
168
 
            warning("please write a locking method for platform %r" % sys.platform)
169
 
            def unlock():
170
 
                self._lockmode = None
171
 
            self.unlock = unlock
172
 
            self._lockmode = mode
173
 
 
174
 
 
175
 
    def _need_readlock(self):
176
 
        if self._lockmode not in ['r', 'w']:
177
 
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
178
 
 
179
 
    def _need_writelock(self):
180
 
        if self._lockmode not in ['w']:
181
 
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
225
    def unlock(self):
 
226
        if not self._lock_mode:
 
227
            from errors import LockError
 
228
            raise LockError('branch %r is not locked' % (self))
 
229
 
 
230
        if self._lock_count > 1:
 
231
            self._lock_count -= 1
 
232
        else:
 
233
            self._lock.unlock()
 
234
            self._lock = None
 
235
            self._lock_mode = self._lock_count = None
182
236
 
183
237
 
184
238
    def abspath(self, name):
190
244
        """Return path relative to this branch of something inside it.
191
245
 
192
246
        Raises an error if path is not in this branch."""
193
 
        rp = os.path.realpath(path)
194
 
        # FIXME: windows
195
 
        if not rp.startswith(self.base):
196
 
            bailout("path %r is not within branch %r" % (rp, self.base))
197
 
        rp = rp[len(self.base):]
198
 
        rp = rp.lstrip(os.sep)
199
 
        return rp
 
247
        return _relpath(self.base, path)
200
248
 
201
249
 
202
250
    def controlfilename(self, file_or_path):
237
285
        os.mkdir(self.controlfilename([]))
238
286
        self.controlfile('README', 'w').write(
239
287
            "This is a Bazaar-NG control directory.\n"
240
 
            "Do not change any files in this directory.")
 
288
            "Do not change any files in this directory.\n")
241
289
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
242
290
        for d in ('text-store', 'inventory-store', 'revision-store'):
243
291
            os.mkdir(self.controlfilename(d))
263
311
        fmt = self.controlfile('branch-format', 'r').read()
264
312
        fmt.replace('\r\n', '')
265
313
        if fmt != BZR_BRANCH_FORMAT:
266
 
            bailout('sorry, branch format %r not supported' % fmt,
267
 
                    ['use a different bzr version',
268
 
                     'or remove the .bzr directory and "bzr init" again'])
 
314
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
315
                           ['use a different bzr version',
 
316
                            'or remove the .bzr directory and "bzr init" again'])
 
317
 
269
318
 
270
319
 
271
320
    def read_working_inventory(self):
272
321
        """Read the working inventory."""
273
 
        self._need_readlock()
274
322
        before = time.time()
275
323
        # ElementTree does its own conversion from UTF-8, so open in
276
324
        # binary.
277
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
278
 
        mutter("loaded inventory of %d items in %f"
279
 
               % (len(inv), time.time() - before))
280
 
        return inv
281
 
 
 
325
        self.lock_read()
 
326
        try:
 
327
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
328
            mutter("loaded inventory of %d items in %f"
 
329
                   % (len(inv), time.time() - before))
 
330
            return inv
 
331
        finally:
 
332
            self.unlock()
 
333
            
282
334
 
283
335
    def _write_inventory(self, inv):
284
336
        """Update the working inventory.
286
338
        That is to say, the inventory describing changes underway, that
287
339
        will be committed to the next revision.
288
340
        """
289
 
        self._need_writelock()
290
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
291
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
292
 
        tmpfname = self.controlfilename('inventory.tmp')
293
 
        tmpf = file(tmpfname, 'wb')
294
 
        inv.write_xml(tmpf)
295
 
        tmpf.close()
296
 
        inv_fname = self.controlfilename('inventory')
297
 
        if sys.platform == 'win32':
298
 
            os.remove(inv_fname)
299
 
        os.rename(tmpfname, inv_fname)
 
341
        self.lock_write()
 
342
        try:
 
343
            from bzrlib.atomicfile import AtomicFile
 
344
 
 
345
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
346
            try:
 
347
                inv.write_xml(f)
 
348
                f.commit()
 
349
            finally:
 
350
                f.close()
 
351
        finally:
 
352
            self.unlock()
 
353
        
300
354
        mutter('wrote working inventory')
301
 
 
 
355
            
302
356
 
303
357
    inventory = property(read_working_inventory, _write_inventory, None,
304
358
                         """Inventory for the working copy.""")
305
359
 
306
360
 
307
 
    def add(self, files, verbose=False):
 
361
    def add(self, files, verbose=False, ids=None):
308
362
        """Make files versioned.
309
363
 
310
364
        Note that the command line normally calls smart_add instead.
312
366
        This puts the files in the Added state, so that they will be
313
367
        recorded by the next commit.
314
368
 
 
369
        files
 
370
            List of paths to add, relative to the base of the tree.
 
371
 
 
372
        ids
 
373
            If set, use these instead of automatically generated ids.
 
374
            Must be the same length as the list of files, but may
 
375
            contain None for ids that are to be autogenerated.
 
376
 
315
377
        TODO: Perhaps have an option to add the ids even if the files do
316
 
               not (yet) exist.
 
378
              not (yet) exist.
317
379
 
318
380
        TODO: Perhaps return the ids of the files?  But then again it
319
 
               is easy to retrieve them if they're needed.
320
 
 
321
 
        TODO: Option to specify file id.
 
381
              is easy to retrieve them if they're needed.
322
382
 
323
383
        TODO: Adding a directory should optionally recurse down and
324
 
               add all non-ignored children.  Perhaps do that in a
325
 
               higher-level method.
326
 
 
327
 
        >>> b = ScratchBranch(files=['foo'])
328
 
        >>> 'foo' in b.unknowns()
329
 
        True
330
 
        >>> b.show_status()
331
 
        ?       foo
332
 
        >>> b.add('foo')
333
 
        >>> 'foo' in b.unknowns()
334
 
        False
335
 
        >>> bool(b.inventory.path2id('foo'))
336
 
        True
337
 
        >>> b.show_status()
338
 
        A       foo
339
 
 
340
 
        >>> b.add('foo')
341
 
        Traceback (most recent call last):
342
 
        ...
343
 
        BzrError: ('foo is already versioned', [])
344
 
 
345
 
        >>> b.add(['nothere'])
346
 
        Traceback (most recent call last):
347
 
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
 
384
              add all non-ignored children.  Perhaps do that in a
 
385
              higher-level method.
348
386
        """
349
 
        self._need_writelock()
350
 
 
351
387
        # TODO: Re-adding a file that is removed in the working copy
352
388
        # should probably put it back with the previous ID.
353
389
        if isinstance(files, types.StringTypes):
 
390
            assert(ids is None or isinstance(ids, types.StringTypes))
354
391
            files = [files]
355
 
        
356
 
        inv = self.read_working_inventory()
357
 
        for f in files:
358
 
            if is_control_file(f):
359
 
                bailout("cannot add control file %s" % quotefn(f))
360
 
 
361
 
            fp = splitpath(f)
362
 
 
363
 
            if len(fp) == 0:
364
 
                bailout("cannot add top-level %r" % f)
365
 
                
366
 
            fullpath = os.path.normpath(self.abspath(f))
367
 
 
368
 
            try:
369
 
                kind = file_kind(fullpath)
370
 
            except OSError:
371
 
                # maybe something better?
372
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
373
 
            
374
 
            if kind != 'file' and kind != 'directory':
375
 
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
376
 
 
377
 
            file_id = gen_file_id(f)
378
 
            inv.add_path(f, kind=kind, file_id=file_id)
379
 
 
380
 
            if verbose:
381
 
                show_status('A', kind, quotefn(f))
382
 
                
383
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
384
 
            
385
 
        self._write_inventory(inv)
386
 
 
 
392
            if ids is not None:
 
393
                ids = [ids]
 
394
 
 
395
        if ids is None:
 
396
            ids = [None] * len(files)
 
397
        else:
 
398
            assert(len(ids) == len(files))
 
399
 
 
400
        self.lock_write()
 
401
        try:
 
402
            inv = self.read_working_inventory()
 
403
            for f,file_id in zip(files, ids):
 
404
                if is_control_file(f):
 
405
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
406
 
 
407
                fp = splitpath(f)
 
408
 
 
409
                if len(fp) == 0:
 
410
                    raise BzrError("cannot add top-level %r" % f)
 
411
 
 
412
                fullpath = os.path.normpath(self.abspath(f))
 
413
 
 
414
                try:
 
415
                    kind = file_kind(fullpath)
 
416
                except OSError:
 
417
                    # maybe something better?
 
418
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
419
 
 
420
                if kind != 'file' and kind != 'directory':
 
421
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
422
 
 
423
                if file_id is None:
 
424
                    file_id = gen_file_id(f)
 
425
                inv.add_path(f, kind=kind, file_id=file_id)
 
426
 
 
427
                if verbose:
 
428
                    print 'added', quotefn(f)
 
429
 
 
430
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
431
 
 
432
            self._write_inventory(inv)
 
433
        finally:
 
434
            self.unlock()
 
435
            
387
436
 
388
437
    def print_file(self, file, revno):
389
438
        """Print `file` to stdout."""
390
 
        self._need_readlock()
391
 
        tree = self.revision_tree(self.lookup_revision(revno))
392
 
        # use inventory as it was in that revision
393
 
        file_id = tree.inventory.path2id(file)
394
 
        if not file_id:
395
 
            bailout("%r is not present in revision %d" % (file, revno))
396
 
        tree.print_file(file_id)
397
 
        
 
439
        self.lock_read()
 
440
        try:
 
441
            tree = self.revision_tree(self.lookup_revision(revno))
 
442
            # use inventory as it was in that revision
 
443
            file_id = tree.inventory.path2id(file)
 
444
            if not file_id:
 
445
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
446
            tree.print_file(file_id)
 
447
        finally:
 
448
            self.unlock()
 
449
 
398
450
 
399
451
    def remove(self, files, verbose=False):
400
452
        """Mark nominated files for removal from the inventory.
403
455
 
404
456
        TODO: Refuse to remove modified files unless --force is given?
405
457
 
406
 
        >>> b = ScratchBranch(files=['foo'])
407
 
        >>> b.add('foo')
408
 
        >>> b.inventory.has_filename('foo')
409
 
        True
410
 
        >>> b.remove('foo')
411
 
        >>> b.working_tree().has_filename('foo')
412
 
        True
413
 
        >>> b.inventory.has_filename('foo')
414
 
        False
415
 
        
416
 
        >>> b = ScratchBranch(files=['foo'])
417
 
        >>> b.add('foo')
418
 
        >>> b.commit('one')
419
 
        >>> b.remove('foo')
420
 
        >>> b.commit('two')
421
 
        >>> b.inventory.has_filename('foo') 
422
 
        False
423
 
        >>> b.basis_tree().has_filename('foo') 
424
 
        False
425
 
        >>> b.working_tree().has_filename('foo') 
426
 
        True
427
 
 
428
458
        TODO: Do something useful with directories.
429
459
 
430
460
        TODO: Should this remove the text or not?  Tough call; not
434
464
        """
435
465
        ## TODO: Normalize names
436
466
        ## TODO: Remove nested loops; better scalability
437
 
        self._need_writelock()
438
 
 
439
467
        if isinstance(files, types.StringTypes):
440
468
            files = [files]
441
 
        
442
 
        tree = self.working_tree()
443
 
        inv = tree.inventory
444
 
 
445
 
        # do this before any modifications
446
 
        for f in files:
447
 
            fid = inv.path2id(f)
448
 
            if not fid:
449
 
                bailout("cannot remove unversioned file %s" % quotefn(f))
450
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
451
 
            if verbose:
452
 
                # having remove it, it must be either ignored or unknown
453
 
                if tree.is_ignored(f):
454
 
                    new_status = 'I'
455
 
                else:
456
 
                    new_status = '?'
457
 
                show_status(new_status, inv[fid].kind, quotefn(f))
458
 
            del inv[fid]
459
 
 
 
469
 
 
470
        self.lock_write()
 
471
 
 
472
        try:
 
473
            tree = self.working_tree()
 
474
            inv = tree.inventory
 
475
 
 
476
            # do this before any modifications
 
477
            for f in files:
 
478
                fid = inv.path2id(f)
 
479
                if not fid:
 
480
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
481
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
482
                if verbose:
 
483
                    # having remove it, it must be either ignored or unknown
 
484
                    if tree.is_ignored(f):
 
485
                        new_status = 'I'
 
486
                    else:
 
487
                        new_status = '?'
 
488
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
489
                del inv[fid]
 
490
 
 
491
            self._write_inventory(inv)
 
492
        finally:
 
493
            self.unlock()
 
494
 
 
495
 
 
496
    # FIXME: this doesn't need to be a branch method
 
497
    def set_inventory(self, new_inventory_list):
 
498
        inv = Inventory()
 
499
        for path, file_id, parent, kind in new_inventory_list:
 
500
            name = os.path.basename(path)
 
501
            if name == "":
 
502
                continue
 
503
            inv.add(InventoryEntry(file_id, name, kind, parent))
460
504
        self._write_inventory(inv)
461
505
 
462
506
 
479
523
        return self.working_tree().unknowns()
480
524
 
481
525
 
482
 
    def commit(self, message, timestamp=None, timezone=None,
483
 
               committer=None,
484
 
               verbose=False):
485
 
        """Commit working copy as a new revision.
486
 
        
487
 
        The basic approach is to add all the file texts into the
488
 
        store, then the inventory, then make a new revision pointing
489
 
        to that inventory and store that.
490
 
        
491
 
        This is not quite safe if the working copy changes during the
492
 
        commit; for the moment that is simply not allowed.  A better
493
 
        approach is to make a temporary copy of the files before
494
 
        computing their hashes, and then add those hashes in turn to
495
 
        the inventory.  This should mean at least that there are no
496
 
        broken hash pointers.  There is no way we can get a snapshot
497
 
        of the whole directory at an instant.  This would also have to
498
 
        be robust against files disappearing, moving, etc.  So the
499
 
        whole thing is a bit hard.
500
 
 
501
 
        timestamp -- if not None, seconds-since-epoch for a
502
 
             postdated/predated commit.
503
 
        """
504
 
        self._need_writelock()
505
 
 
506
 
        ## TODO: Show branch names
507
 
 
508
 
        # TODO: Don't commit if there are no changes, unless forced?
509
 
 
510
 
        # First walk over the working inventory; and both update that
511
 
        # and also build a new revision inventory.  The revision
512
 
        # inventory needs to hold the text-id, sha1 and size of the
513
 
        # actual file versions committed in the revision.  (These are
514
 
        # not present in the working inventory.)  We also need to
515
 
        # detect missing/deleted files, and remove them from the
516
 
        # working inventory.
517
 
 
518
 
        work_inv = self.read_working_inventory()
519
 
        inv = Inventory()
520
 
        basis = self.basis_tree()
521
 
        basis_inv = basis.inventory
522
 
        missing_ids = []
523
 
        for path, entry in work_inv.iter_entries():
524
 
            ## TODO: Cope with files that have gone missing.
525
 
 
526
 
            ## TODO: Check that the file kind has not changed from the previous
527
 
            ## revision of this file (if any).
528
 
 
529
 
            entry = entry.copy()
530
 
 
531
 
            p = self.abspath(path)
532
 
            file_id = entry.file_id
533
 
            mutter('commit prep file %s, id %r ' % (p, file_id))
534
 
 
535
 
            if not os.path.exists(p):
536
 
                mutter("    file is missing, removing from inventory")
537
 
                if verbose:
538
 
                    show_status('D', entry.kind, quotefn(path))
539
 
                missing_ids.append(file_id)
540
 
                continue
541
 
 
542
 
            # TODO: Handle files that have been deleted
543
 
 
544
 
            # TODO: Maybe a special case for empty files?  Seems a
545
 
            # waste to store them many times.
546
 
 
547
 
            inv.add(entry)
548
 
 
549
 
            if basis_inv.has_id(file_id):
550
 
                old_kind = basis_inv[file_id].kind
551
 
                if old_kind != entry.kind:
552
 
                    bailout("entry %r changed kind from %r to %r"
553
 
                            % (file_id, old_kind, entry.kind))
554
 
 
555
 
            if entry.kind == 'directory':
556
 
                if not isdir(p):
557
 
                    bailout("%s is entered as directory but not a directory" % quotefn(p))
558
 
            elif entry.kind == 'file':
559
 
                if not isfile(p):
560
 
                    bailout("%s is entered as file but is not a file" % quotefn(p))
561
 
 
562
 
                content = file(p, 'rb').read()
563
 
 
564
 
                entry.text_sha1 = sha_string(content)
565
 
                entry.text_size = len(content)
566
 
 
567
 
                old_ie = basis_inv.has_id(file_id) and basis_inv[file_id]
568
 
                if (old_ie
569
 
                    and (old_ie.text_size == entry.text_size)
570
 
                    and (old_ie.text_sha1 == entry.text_sha1)):
571
 
                    ## assert content == basis.get_file(file_id).read()
572
 
                    entry.text_id = basis_inv[file_id].text_id
573
 
                    mutter('    unchanged from previous text_id {%s}' %
574
 
                           entry.text_id)
575
 
                    
576
 
                else:
577
 
                    entry.text_id = gen_file_id(entry.name)
578
 
                    self.text_store.add(content, entry.text_id)
579
 
                    mutter('    stored with text_id {%s}' % entry.text_id)
580
 
                    if verbose:
581
 
                        if not old_ie:
582
 
                            state = 'A'
583
 
                        elif (old_ie.name == entry.name
584
 
                              and old_ie.parent_id == entry.parent_id):
585
 
                            state = 'M'
586
 
                        else:
587
 
                            state = 'R'
588
 
 
589
 
                        show_status(state, entry.kind, quotefn(path))
590
 
 
591
 
        for file_id in missing_ids:
592
 
            # have to do this later so we don't mess up the iterator.
593
 
            # since parents may be removed before their children we
594
 
            # have to test.
595
 
 
596
 
            # FIXME: There's probably a better way to do this; perhaps
597
 
            # the workingtree should know how to filter itself.
598
 
            if work_inv.has_id(file_id):
599
 
                del work_inv[file_id]
600
 
 
601
 
 
602
 
        inv_id = rev_id = _gen_revision_id(time.time())
603
 
        
604
 
        inv_tmp = tempfile.TemporaryFile()
605
 
        inv.write_xml(inv_tmp)
606
 
        inv_tmp.seek(0)
607
 
        self.inventory_store.add(inv_tmp, inv_id)
608
 
        mutter('new inventory_id is {%s}' % inv_id)
609
 
 
610
 
        self._write_inventory(work_inv)
611
 
 
612
 
        if timestamp == None:
613
 
            timestamp = time.time()
614
 
 
615
 
        if committer == None:
616
 
            committer = username()
617
 
 
618
 
        if timezone == None:
619
 
            timezone = local_time_offset()
620
 
 
621
 
        mutter("building commit log message")
622
 
        rev = Revision(timestamp=timestamp,
623
 
                       timezone=timezone,
624
 
                       committer=committer,
625
 
                       precursor = self.last_patch(),
626
 
                       message = message,
627
 
                       inventory_id=inv_id,
628
 
                       revision_id=rev_id)
629
 
 
630
 
        rev_tmp = tempfile.TemporaryFile()
631
 
        rev.write_xml(rev_tmp)
632
 
        rev_tmp.seek(0)
633
 
        self.revision_store.add(rev_tmp, rev_id)
634
 
        mutter("new revision_id is {%s}" % rev_id)
635
 
        
636
 
        ## XXX: Everything up to here can simply be orphaned if we abort
637
 
        ## the commit; it will leave junk files behind but that doesn't
638
 
        ## matter.
639
 
 
640
 
        ## TODO: Read back the just-generated changeset, and make sure it
641
 
        ## applies and recreates the right state.
642
 
 
643
 
        ## TODO: Also calculate and store the inventory SHA1
644
 
        mutter("committing patch r%d" % (self.revno() + 1))
645
 
 
646
 
 
647
 
        self.append_revision(rev_id)
648
 
        
649
 
        if verbose:
650
 
            note("commited r%d" % self.revno())
651
 
 
652
 
 
653
526
    def append_revision(self, revision_id):
 
527
        from bzrlib.atomicfile import AtomicFile
 
528
 
654
529
        mutter("add {%s} to revision-history" % revision_id)
655
 
        rev_history = self.revision_history()
656
 
 
657
 
        tmprhname = self.controlfilename('revision-history.tmp')
658
 
        rhname = self.controlfilename('revision-history')
659
 
        
660
 
        f = file(tmprhname, 'wt')
661
 
        rev_history.append(revision_id)
662
 
        f.write('\n'.join(rev_history))
663
 
        f.write('\n')
664
 
        f.close()
665
 
 
666
 
        if sys.platform == 'win32':
667
 
            os.remove(rhname)
668
 
        os.rename(tmprhname, rhname)
669
 
        
 
530
        rev_history = self.revision_history() + [revision_id]
 
531
 
 
532
        f = AtomicFile(self.controlfilename('revision-history'))
 
533
        try:
 
534
            for rev_id in rev_history:
 
535
                print >>f, rev_id
 
536
            f.commit()
 
537
        finally:
 
538
            f.close()
670
539
 
671
540
 
672
541
    def get_revision(self, revision_id):
673
542
        """Return the Revision object for a named revision"""
674
 
        self._need_readlock()
 
543
        if not revision_id or not isinstance(revision_id, basestring):
 
544
            raise ValueError('invalid revision-id: %r' % revision_id)
675
545
        r = Revision.read_xml(self.revision_store[revision_id])
676
546
        assert r.revision_id == revision_id
677
547
        return r
678
548
 
 
549
    def get_revision_sha1(self, revision_id):
 
550
        """Hash the stored value of a revision, and return it."""
 
551
        # In the future, revision entries will be signed. At that
 
552
        # point, it is probably best *not* to include the signature
 
553
        # in the revision hash. Because that lets you re-sign
 
554
        # the revision, (add signatures/remove signatures) and still
 
555
        # have all hash pointers stay consistent.
 
556
        # But for now, just hash the contents.
 
557
        return sha_file(self.revision_store[revision_id])
 
558
 
679
559
 
680
560
    def get_inventory(self, inventory_id):
681
561
        """Get Inventory object by hash.
683
563
        TODO: Perhaps for this and similar methods, take a revision
684
564
               parameter which can be either an integer revno or a
685
565
               string hash."""
686
 
        self._need_readlock()
687
566
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
567
        return i
689
568
 
 
569
    def get_inventory_sha1(self, inventory_id):
 
570
        """Return the sha1 hash of the inventory entry
 
571
        """
 
572
        return sha_file(self.inventory_store[inventory_id])
 
573
 
690
574
 
691
575
    def get_revision_inventory(self, revision_id):
692
576
        """Return inventory of a past revision."""
693
 
        self._need_readlock()
694
577
        if revision_id == None:
695
578
            return Inventory()
696
579
        else:
703
586
        >>> ScratchBranch().revision_history()
704
587
        []
705
588
        """
706
 
        self._need_readlock()
707
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
708
 
 
 
589
        self.lock_read()
 
590
        try:
 
591
            return [l.rstrip('\r\n') for l in
 
592
                    self.controlfile('revision-history', 'r').readlines()]
 
593
        finally:
 
594
            self.unlock()
 
595
 
 
596
 
 
597
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
598
        """
 
599
        >>> import commit
 
600
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
601
        >>> sb.common_ancestor(sb) == (None, None)
 
602
        True
 
603
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
604
        >>> sb.common_ancestor(sb)[0]
 
605
        1
 
606
        >>> clone = sb.clone()
 
607
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
608
        >>> sb.common_ancestor(sb)[0]
 
609
        2
 
610
        >>> sb.common_ancestor(clone)[0]
 
611
        1
 
612
        >>> commit.commit(clone, "Committing divergent second revision", 
 
613
        ...               verbose=False)
 
614
        >>> sb.common_ancestor(clone)[0]
 
615
        1
 
616
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
617
        True
 
618
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
619
        True
 
620
        >>> clone2 = sb.clone()
 
621
        >>> sb.common_ancestor(clone2)[0]
 
622
        2
 
623
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
624
        1
 
625
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
626
        1
 
627
        """
 
628
        my_history = self.revision_history()
 
629
        other_history = other.revision_history()
 
630
        if self_revno is None:
 
631
            self_revno = len(my_history)
 
632
        if other_revno is None:
 
633
            other_revno = len(other_history)
 
634
        indices = range(min((self_revno, other_revno)))
 
635
        indices.reverse()
 
636
        for r in indices:
 
637
            if my_history[r] == other_history[r]:
 
638
                return r+1, my_history[r]
 
639
        return None, None
709
640
 
710
641
    def enum_history(self, direction):
711
642
        """Return (revno, revision_id) for history of branch.
726
657
                yield i, rh[i-1]
727
658
                i -= 1
728
659
        else:
729
 
            raise BzrError('invalid history direction %r' % direction)
 
660
            raise ValueError('invalid history direction', direction)
730
661
 
731
662
 
732
663
    def revno(self):
734
665
 
735
666
        That is equivalent to the number of revisions committed to
736
667
        this branch.
737
 
 
738
 
        >>> b = ScratchBranch()
739
 
        >>> b.revno()
740
 
        0
741
 
        >>> b.commit('no foo')
742
 
        >>> b.revno()
743
 
        1
744
668
        """
745
669
        return len(self.revision_history())
746
670
 
747
671
 
748
672
    def last_patch(self):
749
673
        """Return last patch hash, or None if no history.
750
 
 
751
 
        >>> ScratchBranch().last_patch() == None
752
 
        True
753
674
        """
754
675
        ph = self.revision_history()
755
676
        if ph:
756
677
            return ph[-1]
757
678
        else:
758
679
            return None
 
680
 
 
681
 
 
682
    def missing_revisions(self, other, stop_revision=None):
 
683
        """
 
684
        If self and other have not diverged, return a list of the revisions
 
685
        present in other, but missing from self.
 
686
 
 
687
        >>> from bzrlib.commit import commit
 
688
        >>> bzrlib.trace.silent = True
 
689
        >>> br1 = ScratchBranch()
 
690
        >>> br2 = ScratchBranch()
 
691
        >>> br1.missing_revisions(br2)
 
692
        []
 
693
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
694
        >>> br1.missing_revisions(br2)
 
695
        [u'REVISION-ID-1']
 
696
        >>> br2.missing_revisions(br1)
 
697
        []
 
698
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
699
        >>> br1.missing_revisions(br2)
 
700
        []
 
701
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
702
        >>> br1.missing_revisions(br2)
 
703
        [u'REVISION-ID-2A']
 
704
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
705
        >>> br1.missing_revisions(br2)
 
706
        Traceback (most recent call last):
 
707
        DivergedBranches: These branches have diverged.
 
708
        """
 
709
        self_history = self.revision_history()
 
710
        self_len = len(self_history)
 
711
        other_history = other.revision_history()
 
712
        other_len = len(other_history)
 
713
        common_index = min(self_len, other_len) -1
 
714
        if common_index >= 0 and \
 
715
            self_history[common_index] != other_history[common_index]:
 
716
            raise DivergedBranches(self, other)
 
717
 
 
718
        if stop_revision is None:
 
719
            stop_revision = other_len
 
720
        elif stop_revision > other_len:
 
721
            raise NoSuchRevision(self, stop_revision)
 
722
        
 
723
        return other_history[self_len:stop_revision]
 
724
 
 
725
 
 
726
    def update_revisions(self, other, stop_revision=None):
 
727
        """Pull in all new revisions from other branch.
 
728
        
 
729
        >>> from bzrlib.commit import commit
 
730
        >>> bzrlib.trace.silent = True
 
731
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
732
        >>> br1.add('foo')
 
733
        >>> br1.add('bar')
 
734
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
735
        >>> br2 = ScratchBranch()
 
736
        >>> br2.update_revisions(br1)
 
737
        Added 2 texts.
 
738
        Added 1 inventories.
 
739
        Added 1 revisions.
 
740
        >>> br2.revision_history()
 
741
        [u'REVISION-ID-1']
 
742
        >>> br2.update_revisions(br1)
 
743
        Added 0 texts.
 
744
        Added 0 inventories.
 
745
        Added 0 revisions.
 
746
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
747
        True
 
748
        """
 
749
        from bzrlib.progress import ProgressBar
 
750
 
 
751
        pb = ProgressBar()
 
752
 
 
753
        pb.update('comparing histories')
 
754
        revision_ids = self.missing_revisions(other, stop_revision)
 
755
        revisions = []
 
756
        needed_texts = sets.Set()
 
757
        i = 0
 
758
        for rev_id in revision_ids:
 
759
            i += 1
 
760
            pb.update('fetching revision', i, len(revision_ids))
 
761
            rev = other.get_revision(rev_id)
 
762
            revisions.append(rev)
 
763
            inv = other.get_inventory(str(rev.inventory_id))
 
764
            for key, entry in inv.iter_entries():
 
765
                if entry.text_id is None:
 
766
                    continue
 
767
                if entry.text_id not in self.text_store:
 
768
                    needed_texts.add(entry.text_id)
 
769
 
 
770
        pb.clear()
 
771
                    
 
772
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
773
        print "Added %d texts." % count 
 
774
        inventory_ids = [ f.inventory_id for f in revisions ]
 
775
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
776
                                                inventory_ids)
 
777
        print "Added %d inventories." % count 
 
778
        revision_ids = [ f.revision_id for f in revisions]
 
779
        count = self.revision_store.copy_multi(other.revision_store, 
 
780
                                               revision_ids)
 
781
        for revision_id in revision_ids:
 
782
            self.append_revision(revision_id)
 
783
        print "Added %d revisions." % count
 
784
                    
 
785
        
 
786
    def commit(self, *args, **kw):
 
787
        from bzrlib.commit import commit
 
788
        commit(self, *args, **kw)
759
789
        
760
790
 
761
791
    def lookup_revision(self, revno):
775
805
 
776
806
        `revision_id` may be None for the null revision, in which case
777
807
        an `EmptyTree` is returned."""
778
 
        self._need_readlock()
 
808
        # TODO: refactor this to use an existing revision object
 
809
        # so we don't need to read it in twice.
779
810
        if revision_id == None:
780
811
            return EmptyTree()
781
812
        else:
785
816
 
786
817
    def working_tree(self):
787
818
        """Return a `Tree` for the working copy."""
 
819
        from workingtree import WorkingTree
788
820
        return WorkingTree(self.base, self.read_working_inventory())
789
821
 
790
822
 
792
824
        """Return `Tree` object for last revision.
793
825
 
794
826
        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
827
        """
806
828
        r = self.last_patch()
807
829
        if r == None:
816
838
 
817
839
        This can change the directory or the filename or both.
818
840
        """
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)
 
841
        self.lock_write()
852
842
        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
 
            
 
843
            tree = self.working_tree()
 
844
            inv = tree.inventory
 
845
            if not tree.has_filename(from_rel):
 
846
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
847
            if tree.has_filename(to_rel):
 
848
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
849
 
 
850
            file_id = inv.path2id(from_rel)
 
851
            if file_id == None:
 
852
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
853
 
 
854
            if inv.path2id(to_rel):
 
855
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
856
 
 
857
            to_dir, to_tail = os.path.split(to_rel)
 
858
            to_dir_id = inv.path2id(to_dir)
 
859
            if to_dir_id == None and to_dir != '':
 
860
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
861
 
 
862
            mutter("rename_one:")
 
863
            mutter("  file_id    {%s}" % file_id)
 
864
            mutter("  from_rel   %r" % from_rel)
 
865
            mutter("  to_rel     %r" % to_rel)
 
866
            mutter("  to_dir     %r" % to_dir)
 
867
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
868
 
 
869
            inv.rename(file_id, to_dir_id, to_tail)
 
870
 
 
871
            print "%s => %s" % (from_rel, to_rel)
 
872
 
 
873
            from_abs = self.abspath(from_rel)
 
874
            to_abs = self.abspath(to_rel)
 
875
            try:
 
876
                os.rename(from_abs, to_abs)
 
877
            except OSError, e:
 
878
                raise BzrError("failed to rename %r to %r: %s"
 
879
                        % (from_abs, to_abs, e[1]),
 
880
                        ["rename rolled back"])
 
881
 
 
882
            self._write_inventory(inv)
 
883
        finally:
 
884
            self.unlock()
861
885
 
862
886
 
863
887
    def move(self, from_paths, to_name):
871
895
        Note that to_name is only the last component of the new name;
872
896
        this doesn't change the directory.
873
897
        """
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
 
 
923
 
 
924
 
 
925
 
    def show_status(self, show_all=False, file_list=None):
926
 
        """Display single-line status for non-ignored working files.
927
 
 
928
 
        The list is show sorted in order by file name.
929
 
 
930
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
931
 
        >>> b.show_status()
932
 
        ?       foo
933
 
        >>> b.add('foo')
934
 
        >>> b.show_status()
935
 
        A       foo
936
 
        >>> b.commit("add foo")
937
 
        >>> b.show_status()
938
 
        >>> os.unlink(b.abspath('foo'))
939
 
        >>> b.show_status()
940
 
        D       foo
941
 
        """
942
 
        self._need_readlock()
943
 
 
944
 
        # We have to build everything into a list first so that it can
945
 
        # sorted by name, incorporating all the different sources.
946
 
 
947
 
        # FIXME: Rather than getting things in random order and then sorting,
948
 
        # just step through in order.
949
 
 
950
 
        # Interesting case: the old ID for a file has been removed,
951
 
        # but a new file has been created under that name.
952
 
 
953
 
        old = self.basis_tree()
954
 
        new = self.working_tree()
955
 
 
956
 
        items = diff_trees(old, new)
957
 
        # We want to filter out only if any file was provided in the file_list.
958
 
        if isinstance(file_list, list) and len(file_list):
959
 
            items = [item for item in items if item[3] in file_list]
960
 
 
961
 
        for fs, fid, oldname, newname, kind in items:
962
 
            if fs == 'R':
963
 
                show_status(fs, kind,
964
 
                            oldname + ' => ' + newname)
965
 
            elif fs == 'A' or fs == 'M':
966
 
                show_status(fs, kind, newname)
967
 
            elif fs == 'D':
968
 
                show_status(fs, kind, oldname)
969
 
            elif fs == '.':
970
 
                if show_all:
971
 
                    show_status(fs, kind, newname)
972
 
            elif fs == 'I':
973
 
                if show_all:
974
 
                    show_status(fs, kind, newname)
975
 
            elif fs == '?':
976
 
                show_status(fs, kind, newname)
977
 
            else:
978
 
                bailout("weird file state %r" % ((fs, fid),))
979
 
                
 
898
        self.lock_write()
 
899
        try:
 
900
            ## TODO: Option to move IDs only
 
901
            assert not isinstance(from_paths, basestring)
 
902
            tree = self.working_tree()
 
903
            inv = tree.inventory
 
904
            to_abs = self.abspath(to_name)
 
905
            if not isdir(to_abs):
 
906
                raise BzrError("destination %r is not a directory" % to_abs)
 
907
            if not tree.has_filename(to_name):
 
908
                raise BzrError("destination %r not in working directory" % to_abs)
 
909
            to_dir_id = inv.path2id(to_name)
 
910
            if to_dir_id == None and to_name != '':
 
911
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
912
            to_dir_ie = inv[to_dir_id]
 
913
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
914
                raise BzrError("destination %r is not a directory" % to_abs)
 
915
 
 
916
            to_idpath = inv.get_idpath(to_dir_id)
 
917
 
 
918
            for f in from_paths:
 
919
                if not tree.has_filename(f):
 
920
                    raise BzrError("%r does not exist in working tree" % f)
 
921
                f_id = inv.path2id(f)
 
922
                if f_id == None:
 
923
                    raise BzrError("%r is not versioned" % f)
 
924
                name_tail = splitpath(f)[-1]
 
925
                dest_path = appendpath(to_name, name_tail)
 
926
                if tree.has_filename(dest_path):
 
927
                    raise BzrError("destination %r already exists" % dest_path)
 
928
                if f_id in to_idpath:
 
929
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
930
 
 
931
            # OK, so there's a race here, it's possible that someone will
 
932
            # create a file in this interval and then the rename might be
 
933
            # left half-done.  But we should have caught most problems.
 
934
 
 
935
            for f in from_paths:
 
936
                name_tail = splitpath(f)[-1]
 
937
                dest_path = appendpath(to_name, name_tail)
 
938
                print "%s => %s" % (f, dest_path)
 
939
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
940
                try:
 
941
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
942
                except OSError, e:
 
943
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
944
                            ["rename rolled back"])
 
945
 
 
946
            self._write_inventory(inv)
 
947
        finally:
 
948
            self.unlock()
 
949
 
980
950
 
981
951
 
982
952
class ScratchBranch(Branch):
990
960
    >>> isdir(bd)
991
961
    False
992
962
    """
993
 
    def __init__(self, files=[], dirs=[]):
 
963
    def __init__(self, files=[], dirs=[], base=None):
994
964
        """Make a test branch.
995
965
 
996
966
        This creates a temporary directory and runs init-tree in it.
997
967
 
998
968
        If any files are listed, they are created in the working copy.
999
969
        """
1000
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
970
        init = False
 
971
        if base is None:
 
972
            base = tempfile.mkdtemp()
 
973
            init = True
 
974
        Branch.__init__(self, base, init=init)
1001
975
        for d in dirs:
1002
976
            os.mkdir(self.abspath(d))
1003
977
            
1005
979
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1006
980
 
1007
981
 
 
982
    def clone(self):
 
983
        """
 
984
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
985
        >>> clone = orig.clone()
 
986
        >>> os.path.samefile(orig.base, clone.base)
 
987
        False
 
988
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
989
        True
 
990
        """
 
991
        base = tempfile.mkdtemp()
 
992
        os.rmdir(base)
 
993
        shutil.copytree(self.base, base, symlinks=True)
 
994
        return ScratchBranch(base=base)
 
995
        
1008
996
    def __del__(self):
1009
997
        self.destroy()
1010
998
 
1011
999
    def destroy(self):
1012
1000
        """Destroy the test branch, removing the scratch directory."""
1013
1001
        try:
1014
 
            mutter("delete ScratchBranch %s" % self.base)
1015
 
            shutil.rmtree(self.base)
 
1002
            if self.base:
 
1003
                mutter("delete ScratchBranch %s" % self.base)
 
1004
                shutil.rmtree(self.base)
1016
1005
        except OSError, e:
1017
1006
            # Work around for shutil.rmtree failing on Windows when
1018
1007
            # readonly files are encountered
1044
1033
 
1045
1034
 
1046
1035
 
1047
 
def _gen_revision_id(when):
1048
 
    """Return new revision-id."""
1049
 
    s = '%s-%s-' % (user_email(), compact_date(when))
1050
 
    s += hexlify(rand_bytes(8))
1051
 
    return s
1052
 
 
1053
 
 
1054
1036
def gen_file_id(name):
1055
1037
    """Return new file id.
1056
1038
 
1057
1039
    This should probably generate proper UUIDs, but for the moment we
1058
1040
    cope with just randomness because running uuidgen every time is
1059
1041
    slow."""
 
1042
    import re
 
1043
 
 
1044
    # get last component
1060
1045
    idx = name.rfind('/')
1061
1046
    if idx != -1:
1062
1047
        name = name[idx+1 : ]
1064
1049
    if idx != -1:
1065
1050
        name = name[idx+1 : ]
1066
1051
 
 
1052
    # make it not a hidden file
1067
1053
    name = name.lstrip('.')
1068
1054
 
 
1055
    # remove any wierd characters; we don't escape them but rather
 
1056
    # just pull them out
 
1057
    name = re.sub(r'[^\w.]', '', name)
 
1058
 
1069
1059
    s = hexlify(rand_bytes(8))
1070
1060
    return '-'.join((name, compact_date(time.time()), s))