~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-06-27 01:39:26 UTC
  • Revision ID: mbp@sourcefrog.net-20050627013926-49413d5928809350
- import effbot.org http client

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
def find_cached_branch(f, cache_root, **args):
 
49
    from remotebranch import RemoteBranch
 
50
    br = find_branch(f, **args)
 
51
    def cacheify(br, store_name):
 
52
        from meta_store import CachedStore
 
53
        cache_path = os.path.join(cache_root, store_name)
 
54
        os.mkdir(cache_path)
 
55
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
56
        setattr(br, store_name, new_store)
 
57
 
 
58
    if isinstance(br, RemoteBranch):
 
59
        cacheify(br, 'inventory_store')
 
60
        cacheify(br, 'text_store')
 
61
        cacheify(br, 'revision_store')
 
62
    return br
 
63
 
 
64
 
 
65
def _relpath(base, path):
 
66
    """Return path relative to base, or raise exception.
 
67
 
 
68
    The path may be either an absolute path or a path relative to the
 
69
    current working directory.
 
70
 
 
71
    Lifted out of Branch.relpath for ease of testing.
 
72
 
 
73
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
74
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
75
    avoids that problem."""
 
76
    rp = os.path.abspath(path)
 
77
 
 
78
    s = []
 
79
    head = rp
 
80
    while len(head) >= len(base):
 
81
        if head == base:
 
82
            break
 
83
        head, tail = os.path.split(head)
 
84
        if tail:
 
85
            s.insert(0, tail)
 
86
    else:
 
87
        from errors import NotBranchError
 
88
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
89
 
 
90
    return os.sep.join(s)
49
91
        
50
92
 
51
93
def find_branch_root(f=None):
78
120
            raise BzrError('%r is not in a branch' % orig_f)
79
121
        f = head
80
122
    
 
123
class DivergedBranches(Exception):
 
124
    def __init__(self, branch1, branch2):
 
125
        self.branch1 = branch1
 
126
        self.branch2 = branch2
 
127
        Exception.__init__(self, "These branches have diverged.")
 
128
 
 
129
 
 
130
class NoSuchRevision(BzrError):
 
131
    def __init__(self, branch, revision):
 
132
        self.branch = branch
 
133
        self.revision = revision
 
134
        msg = "Branch %s has no revision %d" % (branch, revision)
 
135
        BzrError.__init__(self, msg)
81
136
 
82
137
 
83
138
######################################################################
84
139
# branch objects
85
140
 
86
 
class Branch:
 
141
class Branch(object):
87
142
    """Branch holding a history of revisions.
88
143
 
89
144
    base
90
145
        Base directory of the branch.
 
146
 
 
147
    _lock_mode
 
148
        None, or 'r' or 'w'
 
149
 
 
150
    _lock_count
 
151
        If _lock_mode is true, a positive count of the number of times the
 
152
        lock has been taken.
 
153
 
 
154
    _lock
 
155
        Lock object from bzrlib.lock.
91
156
    """
92
 
    _lockmode = None
 
157
    base = None
 
158
    _lock_mode = None
 
159
    _lock_count = None
 
160
    _lock = None
93
161
    
94
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
162
    def __init__(self, base, init=False, find_root=True):
95
163
        """Create new branch object at a particular location.
96
164
 
97
165
        base -- Base directory for the branch.
114
182
        else:
115
183
            self.base = os.path.realpath(base)
116
184
            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'])
 
185
                from errors import NotBranchError
 
186
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
187
                                     ['use "bzr init" to initialize a new working tree',
 
188
                                      'current bzr can only operate from top-of-tree'])
120
189
        self._check_format()
121
 
        self.lock(lock_mode)
122
190
 
123
191
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
124
192
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
132
200
    __repr__ = __str__
133
201
 
134
202
 
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
 
203
    def __del__(self):
 
204
        if self._lock_mode or self._lock:
 
205
            from warnings import warn
 
206
            warn("branch %r was not explicitly unlocked" % self)
 
207
            self._lock.unlock()
 
208
 
 
209
 
 
210
 
 
211
    def lock_write(self):
 
212
        if self._lock_mode:
 
213
            if self._lock_mode != 'w':
 
214
                from errors import LockError
 
215
                raise LockError("can't upgrade to a write lock from %r" %
 
216
                                self._lock_mode)
 
217
            self._lock_count += 1
 
218
        else:
 
219
            from bzrlib.lock import WriteLock
 
220
 
 
221
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
222
            self._lock_mode = 'w'
 
223
            self._lock_count = 1
 
224
 
 
225
 
 
226
 
 
227
    def lock_read(self):
 
228
        if self._lock_mode:
 
229
            assert self._lock_mode in ('r', 'w'), \
 
230
                   "invalid lock mode %r" % self._lock_mode
 
231
            self._lock_count += 1
 
232
        else:
 
233
            from bzrlib.lock import ReadLock
 
234
 
 
235
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
236
            self._lock_mode = 'r'
 
237
            self._lock_count = 1
 
238
                        
 
239
 
159
240
            
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)
 
241
    def unlock(self):
 
242
        if not self._lock_mode:
 
243
            from errors import LockError
 
244
            raise LockError('branch %r is not locked' % (self))
 
245
 
 
246
        if self._lock_count > 1:
 
247
            self._lock_count -= 1
 
248
        else:
 
249
            self._lock.unlock()
 
250
            self._lock = None
 
251
            self._lock_mode = self._lock_count = None
182
252
 
183
253
 
184
254
    def abspath(self, name):
190
260
        """Return path relative to this branch of something inside it.
191
261
 
192
262
        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
 
263
        return _relpath(self.base, path)
200
264
 
201
265
 
202
266
    def controlfilename(self, file_or_path):
237
301
        os.mkdir(self.controlfilename([]))
238
302
        self.controlfile('README', 'w').write(
239
303
            "This is a Bazaar-NG control directory.\n"
240
 
            "Do not change any files in this directory.")
 
304
            "Do not change any files in this directory.\n")
241
305
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
242
306
        for d in ('text-store', 'inventory-store', 'revision-store'):
243
307
            os.mkdir(self.controlfilename(d))
263
327
        fmt = self.controlfile('branch-format', 'r').read()
264
328
        fmt.replace('\r\n', '')
265
329
        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'])
 
330
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
331
                           ['use a different bzr version',
 
332
                            'or remove the .bzr directory and "bzr init" again'])
 
333
 
269
334
 
270
335
 
271
336
    def read_working_inventory(self):
272
337
        """Read the working inventory."""
273
 
        self._need_readlock()
274
338
        before = time.time()
275
339
        # ElementTree does its own conversion from UTF-8, so open in
276
340
        # 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
 
 
 
341
        self.lock_read()
 
342
        try:
 
343
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
344
            mutter("loaded inventory of %d items in %f"
 
345
                   % (len(inv), time.time() - before))
 
346
            return inv
 
347
        finally:
 
348
            self.unlock()
 
349
            
282
350
 
283
351
    def _write_inventory(self, inv):
284
352
        """Update the working inventory.
286
354
        That is to say, the inventory describing changes underway, that
287
355
        will be committed to the next revision.
288
356
        """
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)
 
357
        self.lock_write()
 
358
        try:
 
359
            from bzrlib.atomicfile import AtomicFile
 
360
 
 
361
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
362
            try:
 
363
                inv.write_xml(f)
 
364
                f.commit()
 
365
            finally:
 
366
                f.close()
 
367
        finally:
 
368
            self.unlock()
 
369
        
300
370
        mutter('wrote working inventory')
301
 
 
 
371
            
302
372
 
303
373
    inventory = property(read_working_inventory, _write_inventory, None,
304
374
                         """Inventory for the working copy.""")
305
375
 
306
376
 
307
 
    def add(self, files, verbose=False):
 
377
    def add(self, files, verbose=False, ids=None):
308
378
        """Make files versioned.
309
379
 
310
380
        Note that the command line normally calls smart_add instead.
312
382
        This puts the files in the Added state, so that they will be
313
383
        recorded by the next commit.
314
384
 
 
385
        files
 
386
            List of paths to add, relative to the base of the tree.
 
387
 
 
388
        ids
 
389
            If set, use these instead of automatically generated ids.
 
390
            Must be the same length as the list of files, but may
 
391
            contain None for ids that are to be autogenerated.
 
392
 
315
393
        TODO: Perhaps have an option to add the ids even if the files do
316
 
               not (yet) exist.
 
394
              not (yet) exist.
317
395
 
318
396
        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.
 
397
              is easy to retrieve them if they're needed.
322
398
 
323
399
        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', [])
 
400
              add all non-ignored children.  Perhaps do that in a
 
401
              higher-level method.
348
402
        """
349
 
        self._need_writelock()
350
 
 
351
403
        # TODO: Re-adding a file that is removed in the working copy
352
404
        # should probably put it back with the previous ID.
353
405
        if isinstance(files, types.StringTypes):
 
406
            assert(ids is None or isinstance(ids, types.StringTypes))
354
407
            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
 
 
 
408
            if ids is not None:
 
409
                ids = [ids]
 
410
 
 
411
        if ids is None:
 
412
            ids = [None] * len(files)
 
413
        else:
 
414
            assert(len(ids) == len(files))
 
415
 
 
416
        self.lock_write()
 
417
        try:
 
418
            inv = self.read_working_inventory()
 
419
            for f,file_id in zip(files, ids):
 
420
                if is_control_file(f):
 
421
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
422
 
 
423
                fp = splitpath(f)
 
424
 
 
425
                if len(fp) == 0:
 
426
                    raise BzrError("cannot add top-level %r" % f)
 
427
 
 
428
                fullpath = os.path.normpath(self.abspath(f))
 
429
 
 
430
                try:
 
431
                    kind = file_kind(fullpath)
 
432
                except OSError:
 
433
                    # maybe something better?
 
434
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
435
 
 
436
                if kind != 'file' and kind != 'directory':
 
437
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
438
 
 
439
                if file_id is None:
 
440
                    file_id = gen_file_id(f)
 
441
                inv.add_path(f, kind=kind, file_id=file_id)
 
442
 
 
443
                if verbose:
 
444
                    print 'added', quotefn(f)
 
445
 
 
446
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
447
 
 
448
            self._write_inventory(inv)
 
449
        finally:
 
450
            self.unlock()
 
451
            
387
452
 
388
453
    def print_file(self, file, revno):
389
454
        """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
 
        
 
455
        self.lock_read()
 
456
        try:
 
457
            tree = self.revision_tree(self.lookup_revision(revno))
 
458
            # use inventory as it was in that revision
 
459
            file_id = tree.inventory.path2id(file)
 
460
            if not file_id:
 
461
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
462
            tree.print_file(file_id)
 
463
        finally:
 
464
            self.unlock()
 
465
 
398
466
 
399
467
    def remove(self, files, verbose=False):
400
468
        """Mark nominated files for removal from the inventory.
403
471
 
404
472
        TODO: Refuse to remove modified files unless --force is given?
405
473
 
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
474
        TODO: Do something useful with directories.
429
475
 
430
476
        TODO: Should this remove the text or not?  Tough call; not
434
480
        """
435
481
        ## TODO: Normalize names
436
482
        ## TODO: Remove nested loops; better scalability
437
 
        self._need_writelock()
438
 
 
439
483
        if isinstance(files, types.StringTypes):
440
484
            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
 
 
 
485
 
 
486
        self.lock_write()
 
487
 
 
488
        try:
 
489
            tree = self.working_tree()
 
490
            inv = tree.inventory
 
491
 
 
492
            # do this before any modifications
 
493
            for f in files:
 
494
                fid = inv.path2id(f)
 
495
                if not fid:
 
496
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
497
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
498
                if verbose:
 
499
                    # having remove it, it must be either ignored or unknown
 
500
                    if tree.is_ignored(f):
 
501
                        new_status = 'I'
 
502
                    else:
 
503
                        new_status = '?'
 
504
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
505
                del inv[fid]
 
506
 
 
507
            self._write_inventory(inv)
 
508
        finally:
 
509
            self.unlock()
 
510
 
 
511
 
 
512
    # FIXME: this doesn't need to be a branch method
 
513
    def set_inventory(self, new_inventory_list):
 
514
        inv = Inventory()
 
515
        for path, file_id, parent, kind in new_inventory_list:
 
516
            name = os.path.basename(path)
 
517
            if name == "":
 
518
                continue
 
519
            inv.add(InventoryEntry(file_id, name, kind, parent))
460
520
        self._write_inventory(inv)
461
521
 
462
522
 
479
539
        return self.working_tree().unknowns()
480
540
 
481
541
 
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
542
    def append_revision(self, revision_id):
 
543
        from bzrlib.atomicfile import AtomicFile
 
544
 
654
545
        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
 
        
 
546
        rev_history = self.revision_history() + [revision_id]
 
547
 
 
548
        f = AtomicFile(self.controlfilename('revision-history'))
 
549
        try:
 
550
            for rev_id in rev_history:
 
551
                print >>f, rev_id
 
552
            f.commit()
 
553
        finally:
 
554
            f.close()
670
555
 
671
556
 
672
557
    def get_revision(self, revision_id):
673
558
        """Return the Revision object for a named revision"""
674
 
        self._need_readlock()
 
559
        if not revision_id or not isinstance(revision_id, basestring):
 
560
            raise ValueError('invalid revision-id: %r' % revision_id)
675
561
        r = Revision.read_xml(self.revision_store[revision_id])
676
562
        assert r.revision_id == revision_id
677
563
        return r
678
564
 
 
565
    def get_revision_sha1(self, revision_id):
 
566
        """Hash the stored value of a revision, and return it."""
 
567
        # In the future, revision entries will be signed. At that
 
568
        # point, it is probably best *not* to include the signature
 
569
        # in the revision hash. Because that lets you re-sign
 
570
        # the revision, (add signatures/remove signatures) and still
 
571
        # have all hash pointers stay consistent.
 
572
        # But for now, just hash the contents.
 
573
        return sha_file(self.revision_store[revision_id])
 
574
 
679
575
 
680
576
    def get_inventory(self, inventory_id):
681
577
        """Get Inventory object by hash.
683
579
        TODO: Perhaps for this and similar methods, take a revision
684
580
               parameter which can be either an integer revno or a
685
581
               string hash."""
686
 
        self._need_readlock()
687
582
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
583
        return i
689
584
 
 
585
    def get_inventory_sha1(self, inventory_id):
 
586
        """Return the sha1 hash of the inventory entry
 
587
        """
 
588
        return sha_file(self.inventory_store[inventory_id])
 
589
 
690
590
 
691
591
    def get_revision_inventory(self, revision_id):
692
592
        """Return inventory of a past revision."""
693
 
        self._need_readlock()
694
593
        if revision_id == None:
695
594
            return Inventory()
696
595
        else:
703
602
        >>> ScratchBranch().revision_history()
704
603
        []
705
604
        """
706
 
        self._need_readlock()
707
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
708
 
 
 
605
        self.lock_read()
 
606
        try:
 
607
            return [l.rstrip('\r\n') for l in
 
608
                    self.controlfile('revision-history', 'r').readlines()]
 
609
        finally:
 
610
            self.unlock()
 
611
 
 
612
 
 
613
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
614
        """
 
615
        >>> import commit
 
616
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
617
        >>> sb.common_ancestor(sb) == (None, None)
 
618
        True
 
619
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
620
        >>> sb.common_ancestor(sb)[0]
 
621
        1
 
622
        >>> clone = sb.clone()
 
623
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
624
        >>> sb.common_ancestor(sb)[0]
 
625
        2
 
626
        >>> sb.common_ancestor(clone)[0]
 
627
        1
 
628
        >>> commit.commit(clone, "Committing divergent second revision", 
 
629
        ...               verbose=False)
 
630
        >>> sb.common_ancestor(clone)[0]
 
631
        1
 
632
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
633
        True
 
634
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
635
        True
 
636
        >>> clone2 = sb.clone()
 
637
        >>> sb.common_ancestor(clone2)[0]
 
638
        2
 
639
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
640
        1
 
641
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
642
        1
 
643
        """
 
644
        my_history = self.revision_history()
 
645
        other_history = other.revision_history()
 
646
        if self_revno is None:
 
647
            self_revno = len(my_history)
 
648
        if other_revno is None:
 
649
            other_revno = len(other_history)
 
650
        indices = range(min((self_revno, other_revno)))
 
651
        indices.reverse()
 
652
        for r in indices:
 
653
            if my_history[r] == other_history[r]:
 
654
                return r+1, my_history[r]
 
655
        return None, None
709
656
 
710
657
    def enum_history(self, direction):
711
658
        """Return (revno, revision_id) for history of branch.
726
673
                yield i, rh[i-1]
727
674
                i -= 1
728
675
        else:
729
 
            raise BzrError('invalid history direction %r' % direction)
 
676
            raise ValueError('invalid history direction', direction)
730
677
 
731
678
 
732
679
    def revno(self):
734
681
 
735
682
        That is equivalent to the number of revisions committed to
736
683
        this branch.
737
 
 
738
 
        >>> b = ScratchBranch()
739
 
        >>> b.revno()
740
 
        0
741
 
        >>> b.commit('no foo')
742
 
        >>> b.revno()
743
 
        1
744
684
        """
745
685
        return len(self.revision_history())
746
686
 
747
687
 
748
688
    def last_patch(self):
749
689
        """Return last patch hash, or None if no history.
750
 
 
751
 
        >>> ScratchBranch().last_patch() == None
752
 
        True
753
690
        """
754
691
        ph = self.revision_history()
755
692
        if ph:
756
693
            return ph[-1]
757
694
        else:
758
695
            return None
 
696
 
 
697
 
 
698
    def missing_revisions(self, other, stop_revision=None):
 
699
        """
 
700
        If self and other have not diverged, return a list of the revisions
 
701
        present in other, but missing from self.
 
702
 
 
703
        >>> from bzrlib.commit import commit
 
704
        >>> bzrlib.trace.silent = True
 
705
        >>> br1 = ScratchBranch()
 
706
        >>> br2 = ScratchBranch()
 
707
        >>> br1.missing_revisions(br2)
 
708
        []
 
709
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
710
        >>> br1.missing_revisions(br2)
 
711
        [u'REVISION-ID-1']
 
712
        >>> br2.missing_revisions(br1)
 
713
        []
 
714
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
715
        >>> br1.missing_revisions(br2)
 
716
        []
 
717
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
718
        >>> br1.missing_revisions(br2)
 
719
        [u'REVISION-ID-2A']
 
720
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
721
        >>> br1.missing_revisions(br2)
 
722
        Traceback (most recent call last):
 
723
        DivergedBranches: These branches have diverged.
 
724
        """
 
725
        self_history = self.revision_history()
 
726
        self_len = len(self_history)
 
727
        other_history = other.revision_history()
 
728
        other_len = len(other_history)
 
729
        common_index = min(self_len, other_len) -1
 
730
        if common_index >= 0 and \
 
731
            self_history[common_index] != other_history[common_index]:
 
732
            raise DivergedBranches(self, other)
 
733
 
 
734
        if stop_revision is None:
 
735
            stop_revision = other_len
 
736
        elif stop_revision > other_len:
 
737
            raise NoSuchRevision(self, stop_revision)
 
738
        
 
739
        return other_history[self_len:stop_revision]
 
740
 
 
741
 
 
742
    def update_revisions(self, other, stop_revision=None):
 
743
        """Pull in all new revisions from other branch.
 
744
        
 
745
        >>> from bzrlib.commit import commit
 
746
        >>> bzrlib.trace.silent = True
 
747
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
748
        >>> br1.add('foo')
 
749
        >>> br1.add('bar')
 
750
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
751
        >>> br2 = ScratchBranch()
 
752
        >>> br2.update_revisions(br1)
 
753
        Added 2 texts.
 
754
        Added 1 inventories.
 
755
        Added 1 revisions.
 
756
        >>> br2.revision_history()
 
757
        [u'REVISION-ID-1']
 
758
        >>> br2.update_revisions(br1)
 
759
        Added 0 texts.
 
760
        Added 0 inventories.
 
761
        Added 0 revisions.
 
762
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
763
        True
 
764
        """
 
765
        from bzrlib.progress import ProgressBar
 
766
 
 
767
        pb = ProgressBar()
 
768
 
 
769
        pb.update('comparing histories')
 
770
        revision_ids = self.missing_revisions(other, stop_revision)
 
771
 
 
772
        if hasattr(other.revision_store, "prefetch"):
 
773
            other.revision_store.prefetch(revision_ids)
 
774
        if hasattr(other.inventory_store, "prefetch"):
 
775
            inventory_ids = [other.get_revision(r).inventory_id
 
776
                             for r in revision_ids]
 
777
            other.inventory_store.prefetch(inventory_ids)
 
778
                
 
779
        revisions = []
 
780
        needed_texts = sets.Set()
 
781
        i = 0
 
782
        for rev_id in revision_ids:
 
783
            i += 1
 
784
            pb.update('fetching revision', i, len(revision_ids))
 
785
            rev = other.get_revision(rev_id)
 
786
            revisions.append(rev)
 
787
            inv = other.get_inventory(str(rev.inventory_id))
 
788
            for key, entry in inv.iter_entries():
 
789
                if entry.text_id is None:
 
790
                    continue
 
791
                if entry.text_id not in self.text_store:
 
792
                    needed_texts.add(entry.text_id)
 
793
 
 
794
        pb.clear()
 
795
                    
 
796
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
797
        print "Added %d texts." % count 
 
798
        inventory_ids = [ f.inventory_id for f in revisions ]
 
799
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
800
                                                inventory_ids)
 
801
        print "Added %d inventories." % count 
 
802
        revision_ids = [ f.revision_id for f in revisions]
 
803
        count = self.revision_store.copy_multi(other.revision_store, 
 
804
                                               revision_ids)
 
805
        for revision_id in revision_ids:
 
806
            self.append_revision(revision_id)
 
807
        print "Added %d revisions." % count
 
808
                    
 
809
        
 
810
    def commit(self, *args, **kw):
 
811
        from bzrlib.commit import commit
 
812
        commit(self, *args, **kw)
759
813
        
760
814
 
761
815
    def lookup_revision(self, revno):
775
829
 
776
830
        `revision_id` may be None for the null revision, in which case
777
831
        an `EmptyTree` is returned."""
778
 
        self._need_readlock()
 
832
        # TODO: refactor this to use an existing revision object
 
833
        # so we don't need to read it in twice.
779
834
        if revision_id == None:
780
835
            return EmptyTree()
781
836
        else:
785
840
 
786
841
    def working_tree(self):
787
842
        """Return a `Tree` for the working copy."""
 
843
        from workingtree import WorkingTree
788
844
        return WorkingTree(self.base, self.read_working_inventory())
789
845
 
790
846
 
792
848
        """Return `Tree` object for last revision.
793
849
 
794
850
        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
851
        """
806
852
        r = self.last_patch()
807
853
        if r == None:
816
862
 
817
863
        This can change the directory or the filename or both.
818
864
        """
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)
 
865
        self.lock_write()
852
866
        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
 
            
 
867
            tree = self.working_tree()
 
868
            inv = tree.inventory
 
869
            if not tree.has_filename(from_rel):
 
870
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
871
            if tree.has_filename(to_rel):
 
872
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
873
 
 
874
            file_id = inv.path2id(from_rel)
 
875
            if file_id == None:
 
876
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
877
 
 
878
            if inv.path2id(to_rel):
 
879
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
880
 
 
881
            to_dir, to_tail = os.path.split(to_rel)
 
882
            to_dir_id = inv.path2id(to_dir)
 
883
            if to_dir_id == None and to_dir != '':
 
884
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
885
 
 
886
            mutter("rename_one:")
 
887
            mutter("  file_id    {%s}" % file_id)
 
888
            mutter("  from_rel   %r" % from_rel)
 
889
            mutter("  to_rel     %r" % to_rel)
 
890
            mutter("  to_dir     %r" % to_dir)
 
891
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
892
 
 
893
            inv.rename(file_id, to_dir_id, to_tail)
 
894
 
 
895
            print "%s => %s" % (from_rel, to_rel)
 
896
 
 
897
            from_abs = self.abspath(from_rel)
 
898
            to_abs = self.abspath(to_rel)
 
899
            try:
 
900
                os.rename(from_abs, to_abs)
 
901
            except OSError, e:
 
902
                raise BzrError("failed to rename %r to %r: %s"
 
903
                        % (from_abs, to_abs, e[1]),
 
904
                        ["rename rolled back"])
 
905
 
 
906
            self._write_inventory(inv)
 
907
        finally:
 
908
            self.unlock()
861
909
 
862
910
 
863
911
    def move(self, from_paths, to_name):
871
919
        Note that to_name is only the last component of the new name;
872
920
        this doesn't change the directory.
873
921
        """
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)
 
922
        self.lock_write()
 
923
        try:
 
924
            ## TODO: Option to move IDs only
 
925
            assert not isinstance(from_paths, basestring)
 
926
            tree = self.working_tree()
 
927
            inv = tree.inventory
 
928
            to_abs = self.abspath(to_name)
 
929
            if not isdir(to_abs):
 
930
                raise BzrError("destination %r is not a directory" % to_abs)
 
931
            if not tree.has_filename(to_name):
 
932
                raise BzrError("destination %r not in working directory" % to_abs)
 
933
            to_dir_id = inv.path2id(to_name)
 
934
            if to_dir_id == None and to_name != '':
 
935
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
936
            to_dir_ie = inv[to_dir_id]
 
937
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
938
                raise BzrError("destination %r is not a directory" % to_abs)
 
939
 
 
940
            to_idpath = inv.get_idpath(to_dir_id)
 
941
 
 
942
            for f in from_paths:
 
943
                if not tree.has_filename(f):
 
944
                    raise BzrError("%r does not exist in working tree" % f)
 
945
                f_id = inv.path2id(f)
 
946
                if f_id == None:
 
947
                    raise BzrError("%r is not versioned" % f)
 
948
                name_tail = splitpath(f)[-1]
 
949
                dest_path = appendpath(to_name, name_tail)
 
950
                if tree.has_filename(dest_path):
 
951
                    raise BzrError("destination %r already exists" % dest_path)
 
952
                if f_id in to_idpath:
 
953
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
954
 
 
955
            # OK, so there's a race here, it's possible that someone will
 
956
            # create a file in this interval and then the rename might be
 
957
            # left half-done.  But we should have caught most problems.
 
958
 
 
959
            for f in from_paths:
 
960
                name_tail = splitpath(f)[-1]
 
961
                dest_path = appendpath(to_name, name_tail)
 
962
                print "%s => %s" % (f, dest_path)
 
963
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
964
                try:
 
965
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
966
                except OSError, e:
 
967
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
968
                            ["rename rolled back"])
 
969
 
 
970
            self._write_inventory(inv)
 
971
        finally:
 
972
            self.unlock()
 
973
 
 
974
 
 
975
    def revert(self, filenames, old_tree=None, backups=True):
 
976
        """Restore selected files to the versions from a previous tree.
 
977
 
 
978
        backups
 
979
            If true (default) backups are made of files before
 
980
            they're renamed.
 
981
        """
 
982
        from bzrlib.errors import NotVersionedError, BzrError
 
983
        from bzrlib.atomicfile import AtomicFile
 
984
        from bzrlib.osutils import backup_file
 
985
        
 
986
        inv = self.read_working_inventory()
 
987
        if old_tree is None:
 
988
            old_tree = self.basis_tree()
 
989
        old_inv = old_tree.inventory
 
990
 
 
991
        nids = []
 
992
        for fn in filenames:
 
993
            file_id = inv.path2id(fn)
 
994
            if not file_id:
 
995
                raise NotVersionedError("not a versioned file", fn)
 
996
            if not old_inv.has_id(file_id):
 
997
                raise BzrError("file not present in old tree", fn, file_id)
 
998
            nids.append((fn, file_id))
 
999
            
 
1000
        # TODO: Rename back if it was previously at a different location
 
1001
 
 
1002
        # TODO: If given a directory, restore the entire contents from
 
1003
        # the previous version.
 
1004
 
 
1005
        # TODO: Make a backup to a temporary file.
 
1006
 
 
1007
        # TODO: If the file previously didn't exist, delete it?
 
1008
        for fn, file_id in nids:
 
1009
            backup_file(fn)
 
1010
            
 
1011
            f = AtomicFile(fn, 'wb')
915
1012
            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
 
                
 
1013
                f.write(old_tree.get_file(file_id).read())
 
1014
                f.commit()
 
1015
            finally:
 
1016
                f.close()
 
1017
 
980
1018
 
981
1019
 
982
1020
class ScratchBranch(Branch):
990
1028
    >>> isdir(bd)
991
1029
    False
992
1030
    """
993
 
    def __init__(self, files=[], dirs=[]):
 
1031
    def __init__(self, files=[], dirs=[], base=None):
994
1032
        """Make a test branch.
995
1033
 
996
1034
        This creates a temporary directory and runs init-tree in it.
997
1035
 
998
1036
        If any files are listed, they are created in the working copy.
999
1037
        """
1000
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
1038
        init = False
 
1039
        if base is None:
 
1040
            base = tempfile.mkdtemp()
 
1041
            init = True
 
1042
        Branch.__init__(self, base, init=init)
1001
1043
        for d in dirs:
1002
1044
            os.mkdir(self.abspath(d))
1003
1045
            
1005
1047
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1006
1048
 
1007
1049
 
 
1050
    def clone(self):
 
1051
        """
 
1052
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
1053
        >>> clone = orig.clone()
 
1054
        >>> os.path.samefile(orig.base, clone.base)
 
1055
        False
 
1056
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
1057
        True
 
1058
        """
 
1059
        base = tempfile.mkdtemp()
 
1060
        os.rmdir(base)
 
1061
        shutil.copytree(self.base, base, symlinks=True)
 
1062
        return ScratchBranch(base=base)
 
1063
        
1008
1064
    def __del__(self):
1009
1065
        self.destroy()
1010
1066
 
1011
1067
    def destroy(self):
1012
1068
        """Destroy the test branch, removing the scratch directory."""
1013
1069
        try:
1014
 
            mutter("delete ScratchBranch %s" % self.base)
1015
 
            shutil.rmtree(self.base)
 
1070
            if self.base:
 
1071
                mutter("delete ScratchBranch %s" % self.base)
 
1072
                shutil.rmtree(self.base)
1016
1073
        except OSError, e:
1017
1074
            # Work around for shutil.rmtree failing on Windows when
1018
1075
            # readonly files are encountered
1044
1101
 
1045
1102
 
1046
1103
 
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
1104
def gen_file_id(name):
1055
1105
    """Return new file id.
1056
1106
 
1057
1107
    This should probably generate proper UUIDs, but for the moment we
1058
1108
    cope with just randomness because running uuidgen every time is
1059
1109
    slow."""
 
1110
    import re
 
1111
 
 
1112
    # get last component
1060
1113
    idx = name.rfind('/')
1061
1114
    if idx != -1:
1062
1115
        name = name[idx+1 : ]
1064
1117
    if idx != -1:
1065
1118
        name = name[idx+1 : ]
1066
1119
 
 
1120
    # make it not a hidden file
1067
1121
    name = name.lstrip('.')
1068
1122
 
 
1123
    # remove any wierd characters; we don't escape them but rather
 
1124
    # just pull them out
 
1125
    name = re.sub(r'[^\w.]', '', name)
 
1126
 
1069
1127
    s = hexlify(rand_bytes(8))
1070
1128
    return '-'.join((name, compact_date(time.time()), s))