~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 07:06:38 UTC
  • Revision ID: mbp@sourcefrog.net-20050624070638-4b1230afde50b1a8
- files are only reported as modified if their name or parent has changed,
  not if their parent is renamed

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
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?
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
341
        ## TODO: factor out to atomicfile?  is rename safe on windows?
291
342
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
292
343
        tmpfname = self.controlfilename('inventory.tmp')
298
349
            os.remove(inv_fname)
299
350
        os.rename(tmpfname, inv_fname)
300
351
        mutter('wrote working inventory')
301
 
 
 
352
            
302
353
 
303
354
    inventory = property(read_working_inventory, _write_inventory, None,
304
355
                         """Inventory for the working copy.""")
305
356
 
306
357
 
307
 
    def add(self, files, verbose=False):
 
358
    def add(self, files, verbose=False, ids=None):
308
359
        """Make files versioned.
309
360
 
310
361
        Note that the command line normally calls smart_add instead.
312
363
        This puts the files in the Added state, so that they will be
313
364
        recorded by the next commit.
314
365
 
 
366
        files
 
367
            List of paths to add, relative to the base of the tree.
 
368
 
 
369
        ids
 
370
            If set, use these instead of automatically generated ids.
 
371
            Must be the same length as the list of files, but may
 
372
            contain None for ids that are to be autogenerated.
 
373
 
315
374
        TODO: Perhaps have an option to add the ids even if the files do
316
 
               not (yet) exist.
 
375
              not (yet) exist.
317
376
 
318
377
        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.
 
378
              is easy to retrieve them if they're needed.
322
379
 
323
380
        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', [])
 
381
              add all non-ignored children.  Perhaps do that in a
 
382
              higher-level method.
348
383
        """
349
 
        self._need_writelock()
350
 
 
351
384
        # TODO: Re-adding a file that is removed in the working copy
352
385
        # should probably put it back with the previous ID.
353
386
        if isinstance(files, types.StringTypes):
 
387
            assert(ids is None or isinstance(ids, types.StringTypes))
354
388
            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
 
 
 
389
            if ids is not None:
 
390
                ids = [ids]
 
391
 
 
392
        if ids is None:
 
393
            ids = [None] * len(files)
 
394
        else:
 
395
            assert(len(ids) == len(files))
 
396
 
 
397
        self.lock_write()
 
398
        try:
 
399
            inv = self.read_working_inventory()
 
400
            for f,file_id in zip(files, ids):
 
401
                if is_control_file(f):
 
402
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
403
 
 
404
                fp = splitpath(f)
 
405
 
 
406
                if len(fp) == 0:
 
407
                    raise BzrError("cannot add top-level %r" % f)
 
408
 
 
409
                fullpath = os.path.normpath(self.abspath(f))
 
410
 
 
411
                try:
 
412
                    kind = file_kind(fullpath)
 
413
                except OSError:
 
414
                    # maybe something better?
 
415
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
416
 
 
417
                if kind != 'file' and kind != 'directory':
 
418
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
419
 
 
420
                if file_id is None:
 
421
                    file_id = gen_file_id(f)
 
422
                inv.add_path(f, kind=kind, file_id=file_id)
 
423
 
 
424
                if verbose:
 
425
                    show_status('A', kind, quotefn(f))
 
426
 
 
427
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
428
 
 
429
            self._write_inventory(inv)
 
430
        finally:
 
431
            self.unlock()
 
432
            
387
433
 
388
434
    def print_file(self, file, revno):
389
435
        """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
 
        
 
436
        self.lock_read()
 
437
        try:
 
438
            tree = self.revision_tree(self.lookup_revision(revno))
 
439
            # use inventory as it was in that revision
 
440
            file_id = tree.inventory.path2id(file)
 
441
            if not file_id:
 
442
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
443
            tree.print_file(file_id)
 
444
        finally:
 
445
            self.unlock()
 
446
 
398
447
 
399
448
    def remove(self, files, verbose=False):
400
449
        """Mark nominated files for removal from the inventory.
403
452
 
404
453
        TODO: Refuse to remove modified files unless --force is given?
405
454
 
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
455
        TODO: Do something useful with directories.
429
456
 
430
457
        TODO: Should this remove the text or not?  Tough call; not
434
461
        """
435
462
        ## TODO: Normalize names
436
463
        ## TODO: Remove nested loops; better scalability
437
 
        self._need_writelock()
438
 
 
439
464
        if isinstance(files, types.StringTypes):
440
465
            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
 
 
 
466
 
 
467
        self.lock_write()
 
468
 
 
469
        try:
 
470
            tree = self.working_tree()
 
471
            inv = tree.inventory
 
472
 
 
473
            # do this before any modifications
 
474
            for f in files:
 
475
                fid = inv.path2id(f)
 
476
                if not fid:
 
477
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
478
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
479
                if verbose:
 
480
                    # having remove it, it must be either ignored or unknown
 
481
                    if tree.is_ignored(f):
 
482
                        new_status = 'I'
 
483
                    else:
 
484
                        new_status = '?'
 
485
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
486
                del inv[fid]
 
487
 
 
488
            self._write_inventory(inv)
 
489
        finally:
 
490
            self.unlock()
 
491
 
 
492
 
 
493
    # FIXME: this doesn't need to be a branch method
 
494
    def set_inventory(self, new_inventory_list):
 
495
        inv = Inventory()
 
496
        for path, file_id, parent, kind in new_inventory_list:
 
497
            name = os.path.basename(path)
 
498
            if name == "":
 
499
                continue
 
500
            inv.add(InventoryEntry(file_id, name, kind, parent))
460
501
        self._write_inventory(inv)
461
502
 
462
503
 
479
520
        return self.working_tree().unknowns()
480
521
 
481
522
 
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
523
    def append_revision(self, revision_id):
654
524
        mutter("add {%s} to revision-history" % revision_id)
655
525
        rev_history = self.revision_history()
671
541
 
672
542
    def get_revision(self, revision_id):
673
543
        """Return the Revision object for a named revision"""
674
 
        self._need_readlock()
 
544
        if not revision_id or not isinstance(revision_id, basestring):
 
545
            raise ValueError('invalid revision-id: %r' % revision_id)
675
546
        r = Revision.read_xml(self.revision_store[revision_id])
676
547
        assert r.revision_id == revision_id
677
548
        return r
678
549
 
 
550
    def get_revision_sha1(self, revision_id):
 
551
        """Hash the stored value of a revision, and return it."""
 
552
        # In the future, revision entries will be signed. At that
 
553
        # point, it is probably best *not* to include the signature
 
554
        # in the revision hash. Because that lets you re-sign
 
555
        # the revision, (add signatures/remove signatures) and still
 
556
        # have all hash pointers stay consistent.
 
557
        # But for now, just hash the contents.
 
558
        return sha_file(self.revision_store[revision_id])
 
559
 
679
560
 
680
561
    def get_inventory(self, inventory_id):
681
562
        """Get Inventory object by hash.
683
564
        TODO: Perhaps for this and similar methods, take a revision
684
565
               parameter which can be either an integer revno or a
685
566
               string hash."""
686
 
        self._need_readlock()
687
567
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
568
        return i
689
569
 
 
570
    def get_inventory_sha1(self, inventory_id):
 
571
        """Return the sha1 hash of the inventory entry
 
572
        """
 
573
        return sha_file(self.inventory_store[inventory_id])
 
574
 
690
575
 
691
576
    def get_revision_inventory(self, revision_id):
692
577
        """Return inventory of a past revision."""
693
 
        self._need_readlock()
694
578
        if revision_id == None:
695
579
            return Inventory()
696
580
        else:
703
587
        >>> ScratchBranch().revision_history()
704
588
        []
705
589
        """
706
 
        self._need_readlock()
707
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
708
 
 
 
590
        self.lock_read()
 
591
        try:
 
592
            return [l.rstrip('\r\n') for l in
 
593
                    self.controlfile('revision-history', 'r').readlines()]
 
594
        finally:
 
595
            self.unlock()
 
596
 
 
597
 
 
598
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
599
        """
 
600
        >>> import commit
 
601
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
602
        >>> sb.common_ancestor(sb) == (None, None)
 
603
        True
 
604
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
605
        >>> sb.common_ancestor(sb)[0]
 
606
        1
 
607
        >>> clone = sb.clone()
 
608
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
609
        >>> sb.common_ancestor(sb)[0]
 
610
        2
 
611
        >>> sb.common_ancestor(clone)[0]
 
612
        1
 
613
        >>> commit.commit(clone, "Committing divergent second revision", 
 
614
        ...               verbose=False)
 
615
        >>> sb.common_ancestor(clone)[0]
 
616
        1
 
617
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
618
        True
 
619
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
620
        True
 
621
        >>> clone2 = sb.clone()
 
622
        >>> sb.common_ancestor(clone2)[0]
 
623
        2
 
624
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
625
        1
 
626
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
627
        1
 
628
        """
 
629
        my_history = self.revision_history()
 
630
        other_history = other.revision_history()
 
631
        if self_revno is None:
 
632
            self_revno = len(my_history)
 
633
        if other_revno is None:
 
634
            other_revno = len(other_history)
 
635
        indices = range(min((self_revno, other_revno)))
 
636
        indices.reverse()
 
637
        for r in indices:
 
638
            if my_history[r] == other_history[r]:
 
639
                return r+1, my_history[r]
 
640
        return None, None
709
641
 
710
642
    def enum_history(self, direction):
711
643
        """Return (revno, revision_id) for history of branch.
726
658
                yield i, rh[i-1]
727
659
                i -= 1
728
660
        else:
729
 
            raise BzrError('invalid history direction %r' % direction)
 
661
            raise ValueError('invalid history direction', direction)
730
662
 
731
663
 
732
664
    def revno(self):
734
666
 
735
667
        That is equivalent to the number of revisions committed to
736
668
        this branch.
737
 
 
738
 
        >>> b = ScratchBranch()
739
 
        >>> b.revno()
740
 
        0
741
 
        >>> b.commit('no foo')
742
 
        >>> b.revno()
743
 
        1
744
669
        """
745
670
        return len(self.revision_history())
746
671
 
747
672
 
748
673
    def last_patch(self):
749
674
        """Return last patch hash, or None if no history.
750
 
 
751
 
        >>> ScratchBranch().last_patch() == None
752
 
        True
753
675
        """
754
676
        ph = self.revision_history()
755
677
        if ph:
756
678
            return ph[-1]
757
679
        else:
758
680
            return None
 
681
 
 
682
 
 
683
    def missing_revisions(self, other, stop_revision=None):
 
684
        """
 
685
        If self and other have not diverged, return a list of the revisions
 
686
        present in other, but missing from self.
 
687
 
 
688
        >>> from bzrlib.commit import commit
 
689
        >>> bzrlib.trace.silent = True
 
690
        >>> br1 = ScratchBranch()
 
691
        >>> br2 = ScratchBranch()
 
692
        >>> br1.missing_revisions(br2)
 
693
        []
 
694
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
695
        >>> br1.missing_revisions(br2)
 
696
        [u'REVISION-ID-1']
 
697
        >>> br2.missing_revisions(br1)
 
698
        []
 
699
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
700
        >>> br1.missing_revisions(br2)
 
701
        []
 
702
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
703
        >>> br1.missing_revisions(br2)
 
704
        [u'REVISION-ID-2A']
 
705
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
706
        >>> br1.missing_revisions(br2)
 
707
        Traceback (most recent call last):
 
708
        DivergedBranches: These branches have diverged.
 
709
        """
 
710
        self_history = self.revision_history()
 
711
        self_len = len(self_history)
 
712
        other_history = other.revision_history()
 
713
        other_len = len(other_history)
 
714
        common_index = min(self_len, other_len) -1
 
715
        if common_index >= 0 and \
 
716
            self_history[common_index] != other_history[common_index]:
 
717
            raise DivergedBranches(self, other)
 
718
 
 
719
        if stop_revision is None:
 
720
            stop_revision = other_len
 
721
        elif stop_revision > other_len:
 
722
            raise NoSuchRevision(self, stop_revision)
 
723
        
 
724
        return other_history[self_len:stop_revision]
 
725
 
 
726
 
 
727
    def update_revisions(self, other, stop_revision=None):
 
728
        """Pull in all new revisions from other branch.
 
729
        
 
730
        >>> from bzrlib.commit import commit
 
731
        >>> bzrlib.trace.silent = True
 
732
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
733
        >>> br1.add('foo')
 
734
        >>> br1.add('bar')
 
735
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
736
        >>> br2 = ScratchBranch()
 
737
        >>> br2.update_revisions(br1)
 
738
        Added 2 texts.
 
739
        Added 1 inventories.
 
740
        Added 1 revisions.
 
741
        >>> br2.revision_history()
 
742
        [u'REVISION-ID-1']
 
743
        >>> br2.update_revisions(br1)
 
744
        Added 0 texts.
 
745
        Added 0 inventories.
 
746
        Added 0 revisions.
 
747
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
748
        True
 
749
        """
 
750
        from bzrlib.progress import ProgressBar
 
751
 
 
752
        pb = ProgressBar()
 
753
 
 
754
        pb.update('comparing histories')
 
755
        revision_ids = self.missing_revisions(other, stop_revision)
 
756
        revisions = []
 
757
        needed_texts = sets.Set()
 
758
        i = 0
 
759
        for rev_id in revision_ids:
 
760
            i += 1
 
761
            pb.update('fetching revision', i, len(revision_ids))
 
762
            rev = other.get_revision(rev_id)
 
763
            revisions.append(rev)
 
764
            inv = other.get_inventory(str(rev.inventory_id))
 
765
            for key, entry in inv.iter_entries():
 
766
                if entry.text_id is None:
 
767
                    continue
 
768
                if entry.text_id not in self.text_store:
 
769
                    needed_texts.add(entry.text_id)
 
770
 
 
771
        pb.clear()
 
772
                    
 
773
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
774
        print "Added %d texts." % count 
 
775
        inventory_ids = [ f.inventory_id for f in revisions ]
 
776
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
777
                                                inventory_ids)
 
778
        print "Added %d inventories." % count 
 
779
        revision_ids = [ f.revision_id for f in revisions]
 
780
        count = self.revision_store.copy_multi(other.revision_store, 
 
781
                                               revision_ids)
 
782
        for revision_id in revision_ids:
 
783
            self.append_revision(revision_id)
 
784
        print "Added %d revisions." % count
 
785
                    
 
786
        
 
787
    def commit(self, *args, **kw):
 
788
        from bzrlib.commit import commit
 
789
        commit(self, *args, **kw)
759
790
        
760
791
 
761
792
    def lookup_revision(self, revno):
775
806
 
776
807
        `revision_id` may be None for the null revision, in which case
777
808
        an `EmptyTree` is returned."""
778
 
        self._need_readlock()
 
809
        # TODO: refactor this to use an existing revision object
 
810
        # so we don't need to read it in twice.
779
811
        if revision_id == None:
780
812
            return EmptyTree()
781
813
        else:
793
825
        """Return `Tree` object for last revision.
794
826
 
795
827
        If there are no revisions yet, return an `EmptyTree`.
796
 
 
797
 
        >>> b = ScratchBranch(files=['foo'])
798
 
        >>> b.basis_tree().has_filename('foo')
799
 
        False
800
 
        >>> b.working_tree().has_filename('foo')
801
 
        True
802
 
        >>> b.add('foo')
803
 
        >>> b.commit('add foo')
804
 
        >>> b.basis_tree().has_filename('foo')
805
 
        True
806
828
        """
807
829
        r = self.last_patch()
808
830
        if r == None:
817
839
 
818
840
        This can change the directory or the filename or both.
819
841
        """
820
 
        self._need_writelock()
821
 
        tree = self.working_tree()
822
 
        inv = tree.inventory
823
 
        if not tree.has_filename(from_rel):
824
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
825
 
        if tree.has_filename(to_rel):
826
 
            bailout("can't rename: new working file %r already exists" % to_rel)
827
 
            
828
 
        file_id = inv.path2id(from_rel)
829
 
        if file_id == None:
830
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
831
 
 
832
 
        if inv.path2id(to_rel):
833
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
834
 
 
835
 
        to_dir, to_tail = os.path.split(to_rel)
836
 
        to_dir_id = inv.path2id(to_dir)
837
 
        if to_dir_id == None and to_dir != '':
838
 
            bailout("can't determine destination directory id for %r" % to_dir)
839
 
 
840
 
        mutter("rename_one:")
841
 
        mutter("  file_id    {%s}" % file_id)
842
 
        mutter("  from_rel   %r" % from_rel)
843
 
        mutter("  to_rel     %r" % to_rel)
844
 
        mutter("  to_dir     %r" % to_dir)
845
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
846
 
            
847
 
        inv.rename(file_id, to_dir_id, to_tail)
848
 
 
849
 
        print "%s => %s" % (from_rel, to_rel)
850
 
        
851
 
        from_abs = self.abspath(from_rel)
852
 
        to_abs = self.abspath(to_rel)
 
842
        self.lock_write()
853
843
        try:
854
 
            os.rename(from_abs, to_abs)
855
 
        except OSError, e:
856
 
            bailout("failed to rename %r to %r: %s"
857
 
                    % (from_abs, to_abs, e[1]),
858
 
                    ["rename rolled back"])
859
 
 
860
 
        self._write_inventory(inv)
861
 
            
 
844
            tree = self.working_tree()
 
845
            inv = tree.inventory
 
846
            if not tree.has_filename(from_rel):
 
847
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
848
            if tree.has_filename(to_rel):
 
849
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
850
 
 
851
            file_id = inv.path2id(from_rel)
 
852
            if file_id == None:
 
853
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
854
 
 
855
            if inv.path2id(to_rel):
 
856
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
857
 
 
858
            to_dir, to_tail = os.path.split(to_rel)
 
859
            to_dir_id = inv.path2id(to_dir)
 
860
            if to_dir_id == None and to_dir != '':
 
861
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
862
 
 
863
            mutter("rename_one:")
 
864
            mutter("  file_id    {%s}" % file_id)
 
865
            mutter("  from_rel   %r" % from_rel)
 
866
            mutter("  to_rel     %r" % to_rel)
 
867
            mutter("  to_dir     %r" % to_dir)
 
868
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
869
 
 
870
            inv.rename(file_id, to_dir_id, to_tail)
 
871
 
 
872
            print "%s => %s" % (from_rel, to_rel)
 
873
 
 
874
            from_abs = self.abspath(from_rel)
 
875
            to_abs = self.abspath(to_rel)
 
876
            try:
 
877
                os.rename(from_abs, to_abs)
 
878
            except OSError, e:
 
879
                raise BzrError("failed to rename %r to %r: %s"
 
880
                        % (from_abs, to_abs, e[1]),
 
881
                        ["rename rolled back"])
 
882
 
 
883
            self._write_inventory(inv)
 
884
        finally:
 
885
            self.unlock()
862
886
 
863
887
 
864
888
    def move(self, from_paths, to_name):
872
896
        Note that to_name is only the last component of the new name;
873
897
        this doesn't change the directory.
874
898
        """
875
 
        self._need_writelock()
876
 
        ## TODO: Option to move IDs only
877
 
        assert not isinstance(from_paths, basestring)
878
 
        tree = self.working_tree()
879
 
        inv = tree.inventory
880
 
        to_abs = self.abspath(to_name)
881
 
        if not isdir(to_abs):
882
 
            bailout("destination %r is not a directory" % to_abs)
883
 
        if not tree.has_filename(to_name):
884
 
            bailout("destination %r not in working directory" % to_abs)
885
 
        to_dir_id = inv.path2id(to_name)
886
 
        if to_dir_id == None and to_name != '':
887
 
            bailout("destination %r is not a versioned directory" % to_name)
888
 
        to_dir_ie = inv[to_dir_id]
889
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
890
 
            bailout("destination %r is not a directory" % to_abs)
891
 
 
892
 
        to_idpath = Set(inv.get_idpath(to_dir_id))
893
 
 
894
 
        for f in from_paths:
895
 
            if not tree.has_filename(f):
896
 
                bailout("%r does not exist in working tree" % f)
897
 
            f_id = inv.path2id(f)
898
 
            if f_id == None:
899
 
                bailout("%r is not versioned" % f)
900
 
            name_tail = splitpath(f)[-1]
901
 
            dest_path = appendpath(to_name, name_tail)
902
 
            if tree.has_filename(dest_path):
903
 
                bailout("destination %r already exists" % dest_path)
904
 
            if f_id in to_idpath:
905
 
                bailout("can't move %r to a subdirectory of itself" % f)
906
 
 
907
 
        # OK, so there's a race here, it's possible that someone will
908
 
        # create a file in this interval and then the rename might be
909
 
        # left half-done.  But we should have caught most problems.
910
 
 
911
 
        for f in from_paths:
912
 
            name_tail = splitpath(f)[-1]
913
 
            dest_path = appendpath(to_name, name_tail)
914
 
            print "%s => %s" % (f, dest_path)
915
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
916
 
            try:
917
 
                os.rename(self.abspath(f), self.abspath(dest_path))
918
 
            except OSError, e:
919
 
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
920
 
                        ["rename rolled back"])
921
 
 
922
 
        self._write_inventory(inv)
923
 
 
 
899
        self.lock_write()
 
900
        try:
 
901
            ## TODO: Option to move IDs only
 
902
            assert not isinstance(from_paths, basestring)
 
903
            tree = self.working_tree()
 
904
            inv = tree.inventory
 
905
            to_abs = self.abspath(to_name)
 
906
            if not isdir(to_abs):
 
907
                raise BzrError("destination %r is not a directory" % to_abs)
 
908
            if not tree.has_filename(to_name):
 
909
                raise BzrError("destination %r not in working directory" % to_abs)
 
910
            to_dir_id = inv.path2id(to_name)
 
911
            if to_dir_id == None and to_name != '':
 
912
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
913
            to_dir_ie = inv[to_dir_id]
 
914
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
915
                raise BzrError("destination %r is not a directory" % to_abs)
 
916
 
 
917
            to_idpath = inv.get_idpath(to_dir_id)
 
918
 
 
919
            for f in from_paths:
 
920
                if not tree.has_filename(f):
 
921
                    raise BzrError("%r does not exist in working tree" % f)
 
922
                f_id = inv.path2id(f)
 
923
                if f_id == None:
 
924
                    raise BzrError("%r is not versioned" % f)
 
925
                name_tail = splitpath(f)[-1]
 
926
                dest_path = appendpath(to_name, name_tail)
 
927
                if tree.has_filename(dest_path):
 
928
                    raise BzrError("destination %r already exists" % dest_path)
 
929
                if f_id in to_idpath:
 
930
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
931
 
 
932
            # OK, so there's a race here, it's possible that someone will
 
933
            # create a file in this interval and then the rename might be
 
934
            # left half-done.  But we should have caught most problems.
 
935
 
 
936
            for f in from_paths:
 
937
                name_tail = splitpath(f)[-1]
 
938
                dest_path = appendpath(to_name, name_tail)
 
939
                print "%s => %s" % (f, dest_path)
 
940
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
941
                try:
 
942
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
943
                except OSError, e:
 
944
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
945
                            ["rename rolled back"])
 
946
 
 
947
            self._write_inventory(inv)
 
948
        finally:
 
949
            self.unlock()
924
950
 
925
951
 
926
952
 
935
961
    >>> isdir(bd)
936
962
    False
937
963
    """
938
 
    def __init__(self, files=[], dirs=[]):
 
964
    def __init__(self, files=[], dirs=[], base=None):
939
965
        """Make a test branch.
940
966
 
941
967
        This creates a temporary directory and runs init-tree in it.
942
968
 
943
969
        If any files are listed, they are created in the working copy.
944
970
        """
945
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
971
        init = False
 
972
        if base is None:
 
973
            base = tempfile.mkdtemp()
 
974
            init = True
 
975
        Branch.__init__(self, base, init=init)
946
976
        for d in dirs:
947
977
            os.mkdir(self.abspath(d))
948
978
            
950
980
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
951
981
 
952
982
 
 
983
    def clone(self):
 
984
        """
 
985
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
986
        >>> clone = orig.clone()
 
987
        >>> os.path.samefile(orig.base, clone.base)
 
988
        False
 
989
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
990
        True
 
991
        """
 
992
        base = tempfile.mkdtemp()
 
993
        os.rmdir(base)
 
994
        shutil.copytree(self.base, base, symlinks=True)
 
995
        return ScratchBranch(base=base)
 
996
        
953
997
    def __del__(self):
954
998
        self.destroy()
955
999
 
956
1000
    def destroy(self):
957
1001
        """Destroy the test branch, removing the scratch directory."""
958
1002
        try:
959
 
            mutter("delete ScratchBranch %s" % self.base)
960
 
            shutil.rmtree(self.base)
 
1003
            if self.base:
 
1004
                mutter("delete ScratchBranch %s" % self.base)
 
1005
                shutil.rmtree(self.base)
961
1006
        except OSError, e:
962
1007
            # Work around for shutil.rmtree failing on Windows when
963
1008
            # readonly files are encountered
989
1034
 
990
1035
 
991
1036
 
992
 
def _gen_revision_id(when):
993
 
    """Return new revision-id."""
994
 
    s = '%s-%s-' % (user_email(), compact_date(when))
995
 
    s += hexlify(rand_bytes(8))
996
 
    return s
997
 
 
998
 
 
999
1037
def gen_file_id(name):
1000
1038
    """Return new file id.
1001
1039
 
1002
1040
    This should probably generate proper UUIDs, but for the moment we
1003
1041
    cope with just randomness because running uuidgen every time is
1004
1042
    slow."""
 
1043
    import re
 
1044
 
 
1045
    # get last component
1005
1046
    idx = name.rfind('/')
1006
1047
    if idx != -1:
1007
1048
        name = name[idx+1 : ]
1009
1050
    if idx != -1:
1010
1051
        name = name[idx+1 : ]
1011
1052
 
 
1053
    # make it not a hidden file
1012
1054
    name = name.lstrip('.')
1013
1055
 
 
1056
    # remove any wierd characters; we don't escape them but rather
 
1057
    # just pull them out
 
1058
    name = re.sub(r'[^\w.]', '', name)
 
1059
 
1014
1060
    s = hexlify(rand_bytes(8))
1015
1061
    return '-'.join((name, compact_date(time.time()), s))