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 with_writelock(method):
 
 
50
    """Method decorator for functions run with the branch locked."""
 
 
52
        # called with self set to the branch
 
 
55
            return method(self, *a, **k)
 
 
61
def with_readlock(method):
 
 
65
            return method(self, *a, **k)
 
 
71
def find_branch_root(f=None):
 
 
72
    """Find the branch root enclosing f, or pwd.
 
 
74
    f may be a filename or a URL.
 
 
76
    It is not necessary that f exists.
 
 
78
    Basically we keep looking up until we find the control directory or
 
 
82
    elif hasattr(os.path, 'realpath'):
 
 
83
        f = os.path.realpath(f)
 
 
85
        f = os.path.abspath(f)
 
 
86
    if not os.path.exists(f):
 
 
87
        raise BzrError('%r does not exist' % f)
 
 
93
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
 
95
        head, tail = os.path.split(f)
 
 
97
            # reached the root, whatever that may be
 
 
98
            raise BzrError('%r is not in a branch' % orig_f)
 
 
103
######################################################################
 
 
106
class Branch(object):
 
 
107
    """Branch holding a history of revisions.
 
 
110
        Base directory of the branch.
 
 
116
        If _lock_mode is true, a positive count of the number of times the
 
 
120
        Open file used for locking.
 
 
126
    def __init__(self, base, init=False, find_root=True):
 
 
127
        """Create new branch object at a particular location.
 
 
129
        base -- Base directory for the branch.
 
 
131
        init -- If True, create new control files in a previously
 
 
132
             unversioned directory.  If False, the branch must already
 
 
135
        find_root -- If true and init is false, find the root of the
 
 
136
             existing branch containing base.
 
 
138
        In the test suite, creation of new trees is tested using the
 
 
139
        `ScratchBranch` class.
 
 
142
            self.base = os.path.realpath(base)
 
 
145
            self.base = find_branch_root(base)
 
 
147
            self.base = os.path.realpath(base)
 
 
148
            if not isdir(self.controlfilename('.')):
 
 
149
                from errors import NotBranchError
 
 
150
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
 
151
                                     ['use "bzr init" to initialize a new working tree',
 
 
152
                                      'current bzr can only operate from top-of-tree'])
 
 
154
        self._lockfile = self.controlfile('branch-lock', 'wb')
 
 
156
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
 
157
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
 
158
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
 
162
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
 
170
            from warnings import warn
 
 
171
            warn("branch %r was not explicitly unlocked" % self)
 
 
175
    def lock(self, mode):
 
 
177
            if mode == 'w' and cur_lm == 'r':
 
 
178
                raise BzrError("can't upgrade to a write lock")
 
 
180
            assert self._lock_count >= 1
 
 
181
            self._lock_count += 1
 
 
183
            from bzrlib.lock import lock, LOCK_SH, LOCK_EX
 
 
189
                raise ValueError('invalid lock mode %r' % mode)
 
 
191
            lock(self._lockfile, m)
 
 
192
            self._lock_mode = mode
 
 
197
        if not self._lock_mode:
 
 
198
            raise BzrError('branch %r is not locked' % (self))
 
 
200
        if self._lock_count > 1:
 
 
201
            self._lock_count -= 1
 
 
203
            assert self._lock_count == 1
 
 
204
            from bzrlib.lock import unlock
 
 
205
            unlock(self._lockfile)
 
 
206
            self._lock_mode = self._lock_count = None
 
 
209
    def abspath(self, name):
 
 
210
        """Return absolute filename for something in the branch"""
 
 
211
        return os.path.join(self.base, name)
 
 
214
    def relpath(self, path):
 
 
215
        """Return path relative to this branch of something inside it.
 
 
217
        Raises an error if path is not in this branch."""
 
 
218
        rp = os.path.realpath(path)
 
 
220
        if not rp.startswith(self.base):
 
 
221
            from errors import NotBranchError
 
 
222
            raise NotBranchError("path %r is not within branch %r" % (rp, self.base))
 
 
223
        rp = rp[len(self.base):]
 
 
224
        rp = rp.lstrip(os.sep)
 
 
228
    def controlfilename(self, file_or_path):
 
 
229
        """Return location relative to branch."""
 
 
230
        if isinstance(file_or_path, types.StringTypes):
 
 
231
            file_or_path = [file_or_path]
 
 
232
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
 
235
    def controlfile(self, file_or_path, mode='r'):
 
 
236
        """Open a control file for this branch.
 
 
238
        There are two classes of file in the control directory: text
 
 
239
        and binary.  binary files are untranslated byte streams.  Text
 
 
240
        control files are stored with Unix newlines and in UTF-8, even
 
 
241
        if the platform or locale defaults are different.
 
 
243
        Controlfiles should almost never be opened in write mode but
 
 
244
        rather should be atomically copied and replaced using atomicfile.
 
 
247
        fn = self.controlfilename(file_or_path)
 
 
249
        if mode == 'rb' or mode == 'wb':
 
 
250
            return file(fn, mode)
 
 
251
        elif mode == 'r' or mode == 'w':
 
 
252
            # open in binary mode anyhow so there's no newline translation;
 
 
253
            # codecs uses line buffering by default; don't want that.
 
 
255
            return codecs.open(fn, mode + 'b', 'utf-8',
 
 
258
            raise BzrError("invalid controlfile mode %r" % mode)
 
 
262
    def _make_control(self):
 
 
263
        os.mkdir(self.controlfilename([]))
 
 
264
        self.controlfile('README', 'w').write(
 
 
265
            "This is a Bazaar-NG control directory.\n"
 
 
266
            "Do not change any files in this directory.")
 
 
267
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
 
268
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
 
269
            os.mkdir(self.controlfilename(d))
 
 
270
        for f in ('revision-history', 'merged-patches',
 
 
271
                  'pending-merged-patches', 'branch-name',
 
 
273
            self.controlfile(f, 'w').write('')
 
 
274
        mutter('created control directory in ' + self.base)
 
 
275
        Inventory().write_xml(self.controlfile('inventory','w'))
 
 
278
    def _check_format(self):
 
 
279
        """Check this branch format is supported.
 
 
281
        The current tool only supports the current unstable format.
 
 
283
        In the future, we might need different in-memory Branch
 
 
284
        classes to support downlevel branches.  But not yet.
 
 
286
        # This ignores newlines so that we can open branches created
 
 
287
        # on Windows from Linux and so on.  I think it might be better
 
 
288
        # to always make all internal files in unix format.
 
 
289
        fmt = self.controlfile('branch-format', 'r').read()
 
 
290
        fmt.replace('\r\n', '')
 
 
291
        if fmt != BZR_BRANCH_FORMAT:
 
 
292
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
 
293
                           ['use a different bzr version',
 
 
294
                            'or remove the .bzr directory and "bzr init" again'])
 
 
299
    def read_working_inventory(self):
 
 
300
        """Read the working inventory."""
 
 
302
        # ElementTree does its own conversion from UTF-8, so open in
 
 
304
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
 
305
        mutter("loaded inventory of %d items in %f"
 
 
306
               % (len(inv), time.time() - before))
 
 
310
    def _write_inventory(self, inv):
 
 
311
        """Update the working inventory.
 
 
313
        That is to say, the inventory describing changes underway, that
 
 
314
        will be committed to the next revision.
 
 
316
        ## TODO: factor out to atomicfile?  is rename safe on windows?
 
 
317
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
 
 
318
        tmpfname = self.controlfilename('inventory.tmp')
 
 
319
        tmpf = file(tmpfname, 'wb')
 
 
322
        inv_fname = self.controlfilename('inventory')
 
 
323
        if sys.platform == 'win32':
 
 
325
        os.rename(tmpfname, inv_fname)
 
 
326
        mutter('wrote working inventory')
 
 
329
    inventory = property(read_working_inventory, _write_inventory, None,
 
 
330
                         """Inventory for the working copy.""")
 
 
334
    def add(self, files, verbose=False, ids=None):
 
 
335
        """Make files versioned.
 
 
337
        Note that the command line normally calls smart_add instead.
 
 
339
        This puts the files in the Added state, so that they will be
 
 
340
        recorded by the next commit.
 
 
342
        TODO: Perhaps have an option to add the ids even if the files do
 
 
345
        TODO: Perhaps return the ids of the files?  But then again it
 
 
346
               is easy to retrieve them if they're needed.
 
 
348
        TODO: Option to specify file id.
 
 
350
        TODO: Adding a directory should optionally recurse down and
 
 
351
               add all non-ignored children.  Perhaps do that in a
 
 
354
        # TODO: Re-adding a file that is removed in the working copy
 
 
355
        # should probably put it back with the previous ID.
 
 
356
        if isinstance(files, types.StringTypes):
 
 
357
            assert(ids is None or isinstance(ids, types.StringTypes))
 
 
363
            ids = [None] * len(files)
 
 
365
            assert(len(ids) == len(files))
 
 
367
        inv = self.read_working_inventory()
 
 
368
        for f,file_id in zip(files, ids):
 
 
369
            if is_control_file(f):
 
 
370
                raise BzrError("cannot add control file %s" % quotefn(f))
 
 
375
                raise BzrError("cannot add top-level %r" % f)
 
 
377
            fullpath = os.path.normpath(self.abspath(f))
 
 
380
                kind = file_kind(fullpath)
 
 
382
                # maybe something better?
 
 
383
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
385
            if kind != 'file' and kind != 'directory':
 
 
386
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
389
                file_id = gen_file_id(f)
 
 
390
            inv.add_path(f, kind=kind, file_id=file_id)
 
 
393
                show_status('A', kind, quotefn(f))
 
 
395
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
 
397
        self._write_inventory(inv)
 
 
400
    def print_file(self, file, revno):
 
 
401
        """Print `file` to stdout."""
 
 
402
        tree = self.revision_tree(self.lookup_revision(revno))
 
 
403
        # use inventory as it was in that revision
 
 
404
        file_id = tree.inventory.path2id(file)
 
 
406
            raise BzrError("%r is not present in revision %d" % (file, revno))
 
 
407
        tree.print_file(file_id)
 
 
411
    def remove(self, files, verbose=False):
 
 
412
        """Mark nominated files for removal from the inventory.
 
 
414
        This does not remove their text.  This does not run on 
 
 
416
        TODO: Refuse to remove modified files unless --force is given?
 
 
418
        TODO: Do something useful with directories.
 
 
420
        TODO: Should this remove the text or not?  Tough call; not
 
 
421
        removing may be useful and the user can just use use rm, and
 
 
422
        is the opposite of add.  Removing it is consistent with most
 
 
423
        other tools.  Maybe an option.
 
 
425
        ## TODO: Normalize names
 
 
426
        ## TODO: Remove nested loops; better scalability
 
 
427
        if isinstance(files, types.StringTypes):
 
 
430
        tree = self.working_tree()
 
 
433
        # do this before any modifications
 
 
437
                raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
 
438
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
 
440
                # having remove it, it must be either ignored or unknown
 
 
441
                if tree.is_ignored(f):
 
 
445
                show_status(new_status, inv[fid].kind, quotefn(f))
 
 
448
        self._write_inventory(inv)
 
 
451
    def set_inventory(self, new_inventory_list):
 
 
453
        for path, file_id, parent, kind in new_inventory_list:
 
 
454
            name = os.path.basename(path)
 
 
457
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
 
458
        self._write_inventory(inv)
 
 
462
        """Return all unknown files.
 
 
464
        These are files in the working directory that are not versioned or
 
 
465
        control files or ignored.
 
 
467
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
 
468
        >>> list(b.unknowns())
 
 
471
        >>> list(b.unknowns())
 
 
474
        >>> list(b.unknowns())
 
 
477
        return self.working_tree().unknowns()
 
 
480
    def append_revision(self, revision_id):
 
 
481
        mutter("add {%s} to revision-history" % revision_id)
 
 
482
        rev_history = self.revision_history()
 
 
484
        tmprhname = self.controlfilename('revision-history.tmp')
 
 
485
        rhname = self.controlfilename('revision-history')
 
 
487
        f = file(tmprhname, 'wt')
 
 
488
        rev_history.append(revision_id)
 
 
489
        f.write('\n'.join(rev_history))
 
 
493
        if sys.platform == 'win32':
 
 
495
        os.rename(tmprhname, rhname)
 
 
499
    def get_revision(self, revision_id):
 
 
500
        """Return the Revision object for a named revision"""
 
 
501
        r = Revision.read_xml(self.revision_store[revision_id])
 
 
502
        assert r.revision_id == revision_id
 
 
506
    def get_inventory(self, inventory_id):
 
 
507
        """Get Inventory object by hash.
 
 
509
        TODO: Perhaps for this and similar methods, take a revision
 
 
510
               parameter which can be either an integer revno or a
 
 
512
        i = Inventory.read_xml(self.inventory_store[inventory_id])
 
 
516
    def get_revision_inventory(self, revision_id):
 
 
517
        """Return inventory of a past revision."""
 
 
518
        if revision_id == None:
 
 
521
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
 
 
525
    def revision_history(self):
 
 
526
        """Return sequence of revision hashes on to this branch.
 
 
528
        >>> ScratchBranch().revision_history()
 
 
531
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
 
534
    def enum_history(self, direction):
 
 
535
        """Return (revno, revision_id) for history of branch.
 
 
538
            'forward' is from earliest to latest
 
 
539
            'reverse' is from latest to earliest
 
 
541
        rh = self.revision_history()
 
 
542
        if direction == 'forward':
 
 
547
        elif direction == 'reverse':
 
 
553
            raise ValueError('invalid history direction', direction)
 
 
557
        """Return current revision number for this branch.
 
 
559
        That is equivalent to the number of revisions committed to
 
 
562
        return len(self.revision_history())
 
 
565
    def last_patch(self):
 
 
566
        """Return last patch hash, or None if no history.
 
 
568
        ph = self.revision_history()
 
 
575
    def commit(self, *args, **kw):
 
 
577
        from bzrlib.commit import commit
 
 
578
        commit(self, *args, **kw)
 
 
581
    def lookup_revision(self, revno):
 
 
582
        """Return revision hash for revision number."""
 
 
587
            # list is 0-based; revisions are 1-based
 
 
588
            return self.revision_history()[revno-1]
 
 
590
            raise BzrError("no such revision %s" % revno)
 
 
593
    def revision_tree(self, revision_id):
 
 
594
        """Return Tree for a revision on this branch.
 
 
596
        `revision_id` may be None for the null revision, in which case
 
 
597
        an `EmptyTree` is returned."""
 
 
598
        # TODO: refactor this to use an existing revision object
 
 
599
        # so we don't need to read it in twice.
 
 
600
        if revision_id == None:
 
 
603
            inv = self.get_revision_inventory(revision_id)
 
 
604
            return RevisionTree(self.text_store, inv)
 
 
607
    def working_tree(self):
 
 
608
        """Return a `Tree` for the working copy."""
 
 
609
        from workingtree import WorkingTree
 
 
610
        return WorkingTree(self.base, self.read_working_inventory())
 
 
613
    def basis_tree(self):
 
 
614
        """Return `Tree` object for last revision.
 
 
616
        If there are no revisions yet, return an `EmptyTree`.
 
 
618
        r = self.last_patch()
 
 
622
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
 
627
    def rename_one(self, from_rel, to_rel):
 
 
630
        This can change the directory or the filename or both.
 
 
632
        tree = self.working_tree()
 
 
634
        if not tree.has_filename(from_rel):
 
 
635
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
 
636
        if tree.has_filename(to_rel):
 
 
637
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
 
639
        file_id = inv.path2id(from_rel)
 
 
641
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
 
643
        if inv.path2id(to_rel):
 
 
644
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
 
646
        to_dir, to_tail = os.path.split(to_rel)
 
 
647
        to_dir_id = inv.path2id(to_dir)
 
 
648
        if to_dir_id == None and to_dir != '':
 
 
649
            raise BzrError("can't determine destination directory id for %r" % to_dir)
 
 
651
        mutter("rename_one:")
 
 
652
        mutter("  file_id    {%s}" % file_id)
 
 
653
        mutter("  from_rel   %r" % from_rel)
 
 
654
        mutter("  to_rel     %r" % to_rel)
 
 
655
        mutter("  to_dir     %r" % to_dir)
 
 
656
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
 
658
        inv.rename(file_id, to_dir_id, to_tail)
 
 
660
        print "%s => %s" % (from_rel, to_rel)
 
 
662
        from_abs = self.abspath(from_rel)
 
 
663
        to_abs = self.abspath(to_rel)
 
 
665
            os.rename(from_abs, to_abs)
 
 
667
            raise BzrError("failed to rename %r to %r: %s"
 
 
668
                    % (from_abs, to_abs, e[1]),
 
 
669
                    ["rename rolled back"])
 
 
671
        self._write_inventory(inv)
 
 
676
    def move(self, from_paths, to_name):
 
 
679
        to_name must exist as a versioned directory.
 
 
681
        If to_name exists and is a directory, the files are moved into
 
 
682
        it, keeping their old names.  If it is a directory, 
 
 
684
        Note that to_name is only the last component of the new name;
 
 
685
        this doesn't change the directory.
 
 
687
        ## TODO: Option to move IDs only
 
 
688
        assert not isinstance(from_paths, basestring)
 
 
689
        tree = self.working_tree()
 
 
691
        to_abs = self.abspath(to_name)
 
 
692
        if not isdir(to_abs):
 
 
693
            raise BzrError("destination %r is not a directory" % to_abs)
 
 
694
        if not tree.has_filename(to_name):
 
 
695
            raise BzrError("destination %r not in working directory" % to_abs)
 
 
696
        to_dir_id = inv.path2id(to_name)
 
 
697
        if to_dir_id == None and to_name != '':
 
 
698
            raise BzrError("destination %r is not a versioned directory" % to_name)
 
 
699
        to_dir_ie = inv[to_dir_id]
 
 
700
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
 
701
            raise BzrError("destination %r is not a directory" % to_abs)
 
 
703
        to_idpath = inv.get_idpath(to_dir_id)
 
 
706
            if not tree.has_filename(f):
 
 
707
                raise BzrError("%r does not exist in working tree" % f)
 
 
708
            f_id = inv.path2id(f)
 
 
710
                raise BzrError("%r is not versioned" % f)
 
 
711
            name_tail = splitpath(f)[-1]
 
 
712
            dest_path = appendpath(to_name, name_tail)
 
 
713
            if tree.has_filename(dest_path):
 
 
714
                raise BzrError("destination %r already exists" % dest_path)
 
 
715
            if f_id in to_idpath:
 
 
716
                raise BzrError("can't move %r to a subdirectory of itself" % f)
 
 
718
        # OK, so there's a race here, it's possible that someone will
 
 
719
        # create a file in this interval and then the rename might be
 
 
720
        # left half-done.  But we should have caught most problems.
 
 
723
            name_tail = splitpath(f)[-1]
 
 
724
            dest_path = appendpath(to_name, name_tail)
 
 
725
            print "%s => %s" % (f, dest_path)
 
 
726
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
 
728
                os.rename(self.abspath(f), self.abspath(dest_path))
 
 
730
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
 
731
                        ["rename rolled back"])
 
 
733
        self._write_inventory(inv)
 
 
738
class ScratchBranch(Branch):
 
 
739
    """Special test class: a branch that cleans up after itself.
 
 
741
    >>> b = ScratchBranch()
 
 
749
    def __init__(self, files=[], dirs=[]):
 
 
750
        """Make a test branch.
 
 
752
        This creates a temporary directory and runs init-tree in it.
 
 
754
        If any files are listed, they are created in the working copy.
 
 
756
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
 
758
            os.mkdir(self.abspath(d))
 
 
761
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
 
768
        """Destroy the test branch, removing the scratch directory."""
 
 
770
            mutter("delete ScratchBranch %s" % self.base)
 
 
771
            shutil.rmtree(self.base)
 
 
773
            # Work around for shutil.rmtree failing on Windows when
 
 
774
            # readonly files are encountered
 
 
775
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
 
776
            for root, dirs, files in os.walk(self.base, topdown=False):
 
 
778
                    os.chmod(os.path.join(root, name), 0700)
 
 
779
            shutil.rmtree(self.base)
 
 
784
######################################################################
 
 
788
def is_control_file(filename):
 
 
789
    ## FIXME: better check
 
 
790
    filename = os.path.normpath(filename)
 
 
791
    while filename != '':
 
 
792
        head, tail = os.path.split(filename)
 
 
793
        ## mutter('check %r for control file' % ((head, tail), ))
 
 
794
        if tail == bzrlib.BZRDIR:
 
 
803
def gen_file_id(name):
 
 
804
    """Return new file id.
 
 
806
    This should probably generate proper UUIDs, but for the moment we
 
 
807
    cope with just randomness because running uuidgen every time is
 
 
812
    idx = name.rfind('/')
 
 
814
        name = name[idx+1 : ]
 
 
815
    idx = name.rfind('\\')
 
 
817
        name = name[idx+1 : ]
 
 
819
    # make it not a hidden file
 
 
820
    name = name.lstrip('.')
 
 
822
    # remove any wierd characters; we don't escape them but rather
 
 
824
    name = re.sub(r'[^\w.]', '', name)
 
 
826
    s = hexlify(rand_bytes(8))
 
 
827
    return '-'.join((name, compact_date(time.time()), s))