1
# Copyright (C) 2005 Canonical Ltd
 
 
3
# This program is free software; you can redistribute it and/or modify
 
 
4
# it under the terms of the GNU General Public License as published by
 
 
5
# the Free Software Foundation; either version 2 of the License, or
 
 
6
# (at your option) any later version.
 
 
8
# This program is distributed in the hope that it will be useful,
 
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 
11
# GNU General Public License for more details.
 
 
13
# You should have received a copy of the GNU General Public License
 
 
14
# along with this program; if not, write to the Free Software
 
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
18
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
 
 
19
import traceback, socket, fnmatch, difflib, time
 
 
20
from binascii import hexlify
 
 
23
from inventory import Inventory
 
 
24
from trace import mutter, note
 
 
25
from tree import Tree, EmptyTree, RevisionTree
 
 
26
from inventory import InventoryEntry, Inventory
 
 
27
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
 
 
28
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
 
 
29
     joinpath, sha_string, file_kind, local_time_offset, appendpath
 
 
30
from store import ImmutableStore
 
 
31
from revision import Revision
 
 
32
from errors import BzrError
 
 
33
from textui import show_status
 
 
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
 
36
## TODO: Maybe include checks for common corruption of newlines, etc?
 
 
40
def find_branch(f, **args):
 
 
41
    if f and (f.startswith('http://') or f.startswith('https://')):
 
 
43
        return remotebranch.RemoteBranch(f, **args)
 
 
45
        return Branch(f, **args)
 
 
49
def _relpath(base, path):
 
 
50
    """Return path relative to base, or raise exception.
 
 
52
    The path may be either an absolute path or a path relative to the
 
 
53
    current working directory.
 
 
55
    Lifted out of Branch.relpath for ease of testing.
 
 
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)
 
 
64
    while len(head) >= len(base):
 
 
67
        head, tail = os.path.split(head)
 
 
71
        from errors import NotBranchError
 
 
72
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
 
77
def find_branch_root(f=None):
 
 
78
    """Find the branch root enclosing f, or pwd.
 
 
80
    f may be a filename or a URL.
 
 
82
    It is not necessary that f exists.
 
 
84
    Basically we keep looking up until we find the control directory or
 
 
88
    elif hasattr(os.path, 'realpath'):
 
 
89
        f = os.path.realpath(f)
 
 
91
        f = os.path.abspath(f)
 
 
92
    if not os.path.exists(f):
 
 
93
        raise BzrError('%r does not exist' % f)
 
 
99
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
 
101
        head, tail = os.path.split(f)
 
 
103
            # reached the root, whatever that may be
 
 
104
            raise BzrError('%r is not in a branch' % orig_f)
 
 
109
######################################################################
 
 
112
class Branch(object):
 
 
113
    """Branch holding a history of revisions.
 
 
116
        Base directory of the branch.
 
 
122
        If _lock_mode is true, a positive count of the number of times the
 
 
126
        Open file used for locking.
 
 
132
    def __init__(self, base, init=False, find_root=True):
 
 
133
        """Create new branch object at a particular location.
 
 
135
        base -- Base directory for the branch.
 
 
137
        init -- If True, create new control files in a previously
 
 
138
             unversioned directory.  If False, the branch must already
 
 
141
        find_root -- If true and init is false, find the root of the
 
 
142
             existing branch containing base.
 
 
144
        In the test suite, creation of new trees is tested using the
 
 
145
        `ScratchBranch` class.
 
 
148
            self.base = os.path.realpath(base)
 
 
151
            self.base = find_branch_root(base)
 
 
153
            self.base = os.path.realpath(base)
 
 
154
            if not isdir(self.controlfilename('.')):
 
 
155
                from errors import NotBranchError
 
 
156
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
 
157
                                     ['use "bzr init" to initialize a new working tree',
 
 
158
                                      'current bzr can only operate from top-of-tree'])
 
 
160
        self._lockfile = self.controlfile('branch-lock', 'wb')
 
 
162
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
 
163
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
 
164
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
 
168
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
 
176
            from warnings import warn
 
 
177
            warn("branch %r was not explicitly unlocked" % self)
 
 
182
    def lock_write(self):
 
 
184
            if self._lock_mode != 'w':
 
 
185
                from errors import LockError
 
 
186
                raise LockError("can't upgrade to a write lock from %r" %
 
 
188
            self._lock_count += 1
 
 
190
            from bzrlib.lock import lock, LOCK_EX
 
 
192
            lock(self._lockfile, LOCK_EX)
 
 
193
            self._lock_mode = 'w'
 
 
200
            assert self._lock_mode in ('r', 'w'), \
 
 
201
                   "invalid lock mode %r" % self._lock_mode
 
 
202
            self._lock_count += 1
 
 
204
            from bzrlib.lock import lock, LOCK_SH
 
 
206
            lock(self._lockfile, LOCK_SH)
 
 
207
            self._lock_mode = 'r'
 
 
213
        if not self._lock_mode:
 
 
214
            from errors import LockError
 
 
215
            raise LockError('branch %r is not locked' % (self))
 
 
217
        if self._lock_count > 1:
 
 
218
            self._lock_count -= 1
 
 
220
            assert self._lock_count == 1
 
 
221
            from bzrlib.lock import unlock
 
 
222
            unlock(self._lockfile)
 
 
223
            self._lock_mode = self._lock_count = None
 
 
226
    def abspath(self, name):
 
 
227
        """Return absolute filename for something in the branch"""
 
 
228
        return os.path.join(self.base, name)
 
 
231
    def relpath(self, path):
 
 
232
        """Return path relative to this branch of something inside it.
 
 
234
        Raises an error if path is not in this branch."""
 
 
235
        return _relpath(self.base, path)
 
 
238
    def controlfilename(self, file_or_path):
 
 
239
        """Return location relative to branch."""
 
 
240
        if isinstance(file_or_path, types.StringTypes):
 
 
241
            file_or_path = [file_or_path]
 
 
242
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
 
245
    def controlfile(self, file_or_path, mode='r'):
 
 
246
        """Open a control file for this branch.
 
 
248
        There are two classes of file in the control directory: text
 
 
249
        and binary.  binary files are untranslated byte streams.  Text
 
 
250
        control files are stored with Unix newlines and in UTF-8, even
 
 
251
        if the platform or locale defaults are different.
 
 
253
        Controlfiles should almost never be opened in write mode but
 
 
254
        rather should be atomically copied and replaced using atomicfile.
 
 
257
        fn = self.controlfilename(file_or_path)
 
 
259
        if mode == 'rb' or mode == 'wb':
 
 
260
            return file(fn, mode)
 
 
261
        elif mode == 'r' or mode == 'w':
 
 
262
            # open in binary mode anyhow so there's no newline translation;
 
 
263
            # codecs uses line buffering by default; don't want that.
 
 
265
            return codecs.open(fn, mode + 'b', 'utf-8',
 
 
268
            raise BzrError("invalid controlfile mode %r" % mode)
 
 
272
    def _make_control(self):
 
 
273
        os.mkdir(self.controlfilename([]))
 
 
274
        self.controlfile('README', 'w').write(
 
 
275
            "This is a Bazaar-NG control directory.\n"
 
 
276
            "Do not change any files in this directory.")
 
 
277
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
 
278
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
 
279
            os.mkdir(self.controlfilename(d))
 
 
280
        for f in ('revision-history', 'merged-patches',
 
 
281
                  'pending-merged-patches', 'branch-name',
 
 
283
            self.controlfile(f, 'w').write('')
 
 
284
        mutter('created control directory in ' + self.base)
 
 
285
        Inventory().write_xml(self.controlfile('inventory','w'))
 
 
288
    def _check_format(self):
 
 
289
        """Check this branch format is supported.
 
 
291
        The current tool only supports the current unstable format.
 
 
293
        In the future, we might need different in-memory Branch
 
 
294
        classes to support downlevel branches.  But not yet.
 
 
296
        # This ignores newlines so that we can open branches created
 
 
297
        # on Windows from Linux and so on.  I think it might be better
 
 
298
        # to always make all internal files in unix format.
 
 
299
        fmt = self.controlfile('branch-format', 'r').read()
 
 
300
        fmt.replace('\r\n', '')
 
 
301
        if fmt != BZR_BRANCH_FORMAT:
 
 
302
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
 
303
                           ['use a different bzr version',
 
 
304
                            'or remove the .bzr directory and "bzr init" again'])
 
 
308
    def read_working_inventory(self):
 
 
309
        """Read the working inventory."""
 
 
311
        # ElementTree does its own conversion from UTF-8, so open in
 
 
315
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
 
316
            mutter("loaded inventory of %d items in %f"
 
 
317
                   % (len(inv), time.time() - before))
 
 
323
    def _write_inventory(self, inv):
 
 
324
        """Update the working inventory.
 
 
326
        That is to say, the inventory describing changes underway, that
 
 
327
        will be committed to the next revision.
 
 
329
        ## TODO: factor out to atomicfile?  is rename safe on windows?
 
 
330
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
 
 
331
        tmpfname = self.controlfilename('inventory.tmp')
 
 
332
        tmpf = file(tmpfname, 'wb')
 
 
335
        inv_fname = self.controlfilename('inventory')
 
 
336
        if sys.platform == 'win32':
 
 
338
        os.rename(tmpfname, inv_fname)
 
 
339
        mutter('wrote working inventory')
 
 
342
    inventory = property(read_working_inventory, _write_inventory, None,
 
 
343
                         """Inventory for the working copy.""")
 
 
346
    def add(self, files, verbose=False, ids=None):
 
 
347
        """Make files versioned.
 
 
349
        Note that the command line normally calls smart_add instead.
 
 
351
        This puts the files in the Added state, so that they will be
 
 
352
        recorded by the next commit.
 
 
355
            List of paths to add, relative to the base of the tree.
 
 
358
            If set, use these instead of automatically generated ids.
 
 
359
            Must be the same length as the list of files, but may
 
 
360
            contain None for ids that are to be autogenerated.
 
 
362
        TODO: Perhaps have an option to add the ids even if the files do
 
 
365
        TODO: Perhaps return the ids of the files?  But then again it
 
 
366
              is easy to retrieve them if they're needed.
 
 
368
        TODO: Adding a directory should optionally recurse down and
 
 
369
              add all non-ignored children.  Perhaps do that in a
 
 
372
        # TODO: Re-adding a file that is removed in the working copy
 
 
373
        # should probably put it back with the previous ID.
 
 
374
        if isinstance(files, types.StringTypes):
 
 
375
            assert(ids is None or isinstance(ids, types.StringTypes))
 
 
381
            ids = [None] * len(files)
 
 
383
            assert(len(ids) == len(files))
 
 
387
            inv = self.read_working_inventory()
 
 
388
            for f,file_id in zip(files, ids):
 
 
389
                if is_control_file(f):
 
 
390
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
 
395
                    raise BzrError("cannot add top-level %r" % f)
 
 
397
                fullpath = os.path.normpath(self.abspath(f))
 
 
400
                    kind = file_kind(fullpath)
 
 
402
                    # maybe something better?
 
 
403
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
405
                if kind != 'file' and kind != 'directory':
 
 
406
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
409
                    file_id = gen_file_id(f)
 
 
410
                inv.add_path(f, kind=kind, file_id=file_id)
 
 
413
                    show_status('A', kind, quotefn(f))
 
 
415
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
 
417
            self._write_inventory(inv)
 
 
422
    def print_file(self, file, revno):
 
 
423
        """Print `file` to stdout."""
 
 
426
            tree = self.revision_tree(self.lookup_revision(revno))
 
 
427
            # use inventory as it was in that revision
 
 
428
            file_id = tree.inventory.path2id(file)
 
 
430
                raise BzrError("%r is not present in revision %d" % (file, revno))
 
 
431
            tree.print_file(file_id)
 
 
436
    def remove(self, files, verbose=False):
 
 
437
        """Mark nominated files for removal from the inventory.
 
 
439
        This does not remove their text.  This does not run on 
 
 
441
        TODO: Refuse to remove modified files unless --force is given?
 
 
443
        TODO: Do something useful with directories.
 
 
445
        TODO: Should this remove the text or not?  Tough call; not
 
 
446
        removing may be useful and the user can just use use rm, and
 
 
447
        is the opposite of add.  Removing it is consistent with most
 
 
448
        other tools.  Maybe an option.
 
 
450
        ## TODO: Normalize names
 
 
451
        ## TODO: Remove nested loops; better scalability
 
 
452
        if isinstance(files, types.StringTypes):
 
 
458
            tree = self.working_tree()
 
 
461
            # do this before any modifications
 
 
465
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
 
466
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
 
468
                    # having remove it, it must be either ignored or unknown
 
 
469
                    if tree.is_ignored(f):
 
 
473
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
 
476
            self._write_inventory(inv)
 
 
481
    # FIXME: this doesn't need to be a branch method
 
 
482
    def set_inventory(self, new_inventory_list):
 
 
484
        for path, file_id, parent, kind in new_inventory_list:
 
 
485
            name = os.path.basename(path)
 
 
488
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
 
489
        self._write_inventory(inv)
 
 
493
        """Return all unknown files.
 
 
495
        These are files in the working directory that are not versioned or
 
 
496
        control files or ignored.
 
 
498
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
 
499
        >>> list(b.unknowns())
 
 
502
        >>> list(b.unknowns())
 
 
505
        >>> list(b.unknowns())
 
 
508
        return self.working_tree().unknowns()
 
 
511
    def append_revision(self, revision_id):
 
 
512
        mutter("add {%s} to revision-history" % revision_id)
 
 
513
        rev_history = self.revision_history()
 
 
515
        tmprhname = self.controlfilename('revision-history.tmp')
 
 
516
        rhname = self.controlfilename('revision-history')
 
 
518
        f = file(tmprhname, 'wt')
 
 
519
        rev_history.append(revision_id)
 
 
520
        f.write('\n'.join(rev_history))
 
 
524
        if sys.platform == 'win32':
 
 
526
        os.rename(tmprhname, rhname)
 
 
530
    def get_revision(self, revision_id):
 
 
531
        """Return the Revision object for a named revision"""
 
 
532
        r = Revision.read_xml(self.revision_store[revision_id])
 
 
533
        assert r.revision_id == revision_id
 
 
537
    def get_inventory(self, inventory_id):
 
 
538
        """Get Inventory object by hash.
 
 
540
        TODO: Perhaps for this and similar methods, take a revision
 
 
541
               parameter which can be either an integer revno or a
 
 
543
        i = Inventory.read_xml(self.inventory_store[inventory_id])
 
 
547
    def get_revision_inventory(self, revision_id):
 
 
548
        """Return inventory of a past revision."""
 
 
549
        if revision_id == None:
 
 
552
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
 
555
    def revision_history(self):
 
 
556
        """Return sequence of revision hashes on to this branch.
 
 
558
        >>> ScratchBranch().revision_history()
 
 
563
            return [l.rstrip('\r\n') for l in
 
 
564
                    self.controlfile('revision-history', 'r').readlines()]
 
 
569
    def enum_history(self, direction):
 
 
570
        """Return (revno, revision_id) for history of branch.
 
 
573
            'forward' is from earliest to latest
 
 
574
            'reverse' is from latest to earliest
 
 
576
        rh = self.revision_history()
 
 
577
        if direction == 'forward':
 
 
582
        elif direction == 'reverse':
 
 
588
            raise ValueError('invalid history direction', direction)
 
 
592
        """Return current revision number for this branch.
 
 
594
        That is equivalent to the number of revisions committed to
 
 
597
        return len(self.revision_history())
 
 
600
    def last_patch(self):
 
 
601
        """Return last patch hash, or None if no history.
 
 
603
        ph = self.revision_history()
 
 
610
    def commit(self, *args, **kw):
 
 
612
        from bzrlib.commit import commit
 
 
613
        commit(self, *args, **kw)
 
 
616
    def lookup_revision(self, revno):
 
 
617
        """Return revision hash for revision number."""
 
 
622
            # list is 0-based; revisions are 1-based
 
 
623
            return self.revision_history()[revno-1]
 
 
625
            raise BzrError("no such revision %s" % revno)
 
 
628
    def revision_tree(self, revision_id):
 
 
629
        """Return Tree for a revision on this branch.
 
 
631
        `revision_id` may be None for the null revision, in which case
 
 
632
        an `EmptyTree` is returned."""
 
 
633
        # TODO: refactor this to use an existing revision object
 
 
634
        # so we don't need to read it in twice.
 
 
635
        if revision_id == None:
 
 
638
            inv = self.get_revision_inventory(revision_id)
 
 
639
            return RevisionTree(self.text_store, inv)
 
 
642
    def working_tree(self):
 
 
643
        """Return a `Tree` for the working copy."""
 
 
644
        from workingtree import WorkingTree
 
 
645
        return WorkingTree(self.base, self.read_working_inventory())
 
 
648
    def basis_tree(self):
 
 
649
        """Return `Tree` object for last revision.
 
 
651
        If there are no revisions yet, return an `EmptyTree`.
 
 
653
        r = self.last_patch()
 
 
657
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
 
661
    def rename_one(self, from_rel, to_rel):
 
 
664
        This can change the directory or the filename or both.
 
 
668
            tree = self.working_tree()
 
 
670
            if not tree.has_filename(from_rel):
 
 
671
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
 
672
            if tree.has_filename(to_rel):
 
 
673
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
 
675
            file_id = inv.path2id(from_rel)
 
 
677
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
 
679
            if inv.path2id(to_rel):
 
 
680
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
 
682
            to_dir, to_tail = os.path.split(to_rel)
 
 
683
            to_dir_id = inv.path2id(to_dir)
 
 
684
            if to_dir_id == None and to_dir != '':
 
 
685
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
 
687
            mutter("rename_one:")
 
 
688
            mutter("  file_id    {%s}" % file_id)
 
 
689
            mutter("  from_rel   %r" % from_rel)
 
 
690
            mutter("  to_rel     %r" % to_rel)
 
 
691
            mutter("  to_dir     %r" % to_dir)
 
 
692
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
 
694
            inv.rename(file_id, to_dir_id, to_tail)
 
 
696
            print "%s => %s" % (from_rel, to_rel)
 
 
698
            from_abs = self.abspath(from_rel)
 
 
699
            to_abs = self.abspath(to_rel)
 
 
701
                os.rename(from_abs, to_abs)
 
 
703
                raise BzrError("failed to rename %r to %r: %s"
 
 
704
                        % (from_abs, to_abs, e[1]),
 
 
705
                        ["rename rolled back"])
 
 
707
            self._write_inventory(inv)
 
 
712
    def move(self, from_paths, to_name):
 
 
715
        to_name must exist as a versioned directory.
 
 
717
        If to_name exists and is a directory, the files are moved into
 
 
718
        it, keeping their old names.  If it is a directory, 
 
 
720
        Note that to_name is only the last component of the new name;
 
 
721
        this doesn't change the directory.
 
 
725
            ## TODO: Option to move IDs only
 
 
726
            assert not isinstance(from_paths, basestring)
 
 
727
            tree = self.working_tree()
 
 
729
            to_abs = self.abspath(to_name)
 
 
730
            if not isdir(to_abs):
 
 
731
                raise BzrError("destination %r is not a directory" % to_abs)
 
 
732
            if not tree.has_filename(to_name):
 
 
733
                raise BzrError("destination %r not in working directory" % to_abs)
 
 
734
            to_dir_id = inv.path2id(to_name)
 
 
735
            if to_dir_id == None and to_name != '':
 
 
736
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
 
737
            to_dir_ie = inv[to_dir_id]
 
 
738
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
 
739
                raise BzrError("destination %r is not a directory" % to_abs)
 
 
741
            to_idpath = inv.get_idpath(to_dir_id)
 
 
744
                if not tree.has_filename(f):
 
 
745
                    raise BzrError("%r does not exist in working tree" % f)
 
 
746
                f_id = inv.path2id(f)
 
 
748
                    raise BzrError("%r is not versioned" % f)
 
 
749
                name_tail = splitpath(f)[-1]
 
 
750
                dest_path = appendpath(to_name, name_tail)
 
 
751
                if tree.has_filename(dest_path):
 
 
752
                    raise BzrError("destination %r already exists" % dest_path)
 
 
753
                if f_id in to_idpath:
 
 
754
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
 
756
            # OK, so there's a race here, it's possible that someone will
 
 
757
            # create a file in this interval and then the rename might be
 
 
758
            # left half-done.  But we should have caught most problems.
 
 
761
                name_tail = splitpath(f)[-1]
 
 
762
                dest_path = appendpath(to_name, name_tail)
 
 
763
                print "%s => %s" % (f, dest_path)
 
 
764
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
 
766
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
 
768
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
 
769
                            ["rename rolled back"])
 
 
771
            self._write_inventory(inv)
 
 
777
class ScratchBranch(Branch):
 
 
778
    """Special test class: a branch that cleans up after itself.
 
 
780
    >>> b = ScratchBranch()
 
 
788
    def __init__(self, files=[], dirs=[]):
 
 
789
        """Make a test branch.
 
 
791
        This creates a temporary directory and runs init-tree in it.
 
 
793
        If any files are listed, they are created in the working copy.
 
 
795
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
 
797
            os.mkdir(self.abspath(d))
 
 
800
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
 
807
        """Destroy the test branch, removing the scratch directory."""
 
 
810
                mutter("delete ScratchBranch %s" % self.base)
 
 
811
                shutil.rmtree(self.base)
 
 
813
            # Work around for shutil.rmtree failing on Windows when
 
 
814
            # readonly files are encountered
 
 
815
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
 
816
            for root, dirs, files in os.walk(self.base, topdown=False):
 
 
818
                    os.chmod(os.path.join(root, name), 0700)
 
 
819
            shutil.rmtree(self.base)
 
 
824
######################################################################
 
 
828
def is_control_file(filename):
 
 
829
    ## FIXME: better check
 
 
830
    filename = os.path.normpath(filename)
 
 
831
    while filename != '':
 
 
832
        head, tail = os.path.split(filename)
 
 
833
        ## mutter('check %r for control file' % ((head, tail), ))
 
 
834
        if tail == bzrlib.BZRDIR:
 
 
843
def gen_file_id(name):
 
 
844
    """Return new file id.
 
 
846
    This should probably generate proper UUIDs, but for the moment we
 
 
847
    cope with just randomness because running uuidgen every time is
 
 
852
    idx = name.rfind('/')
 
 
854
        name = name[idx+1 : ]
 
 
855
    idx = name.rfind('\\')
 
 
857
        name = name[idx+1 : ]
 
 
859
    # make it not a hidden file
 
 
860
    name = name.lstrip('.')
 
 
862
    # remove any wierd characters; we don't escape them but rather
 
 
864
    name = re.sub(r'[^\w.]', '', name)
 
 
866
    s = hexlify(rand_bytes(8))
 
 
867
    return '-'.join((name, compact_date(time.time()), s))