~bzr-pqm/bzr/bzr.dev

70 by mbp at sourcefrog
Prepare for smart recursive add.
1
# Copyright (C) 2005 Canonical Ltd
2
1 by mbp at sourcefrog
import from baz patch-364
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.
7
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.
12
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
16
17
18
from sets import Set
19
20
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
21
import traceback, socket, fnmatch, difflib, time
22
from binascii import hexlify
23
24
import bzrlib
25
from inventory import Inventory
26
from trace import mutter, note
453 by Martin Pool
- Split WorkingTree into its own file
27
from tree import Tree, EmptyTree, RevisionTree
1 by mbp at sourcefrog
import from baz patch-364
28
from inventory import InventoryEntry, Inventory
319 by Martin Pool
- remove trivial chomp() function
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
1 by mbp at sourcefrog
import from baz patch-364
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
160 by mbp at sourcefrog
- basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think
31
     joinpath, sha_string, file_kind, local_time_offset, appendpath
1 by mbp at sourcefrog
import from baz patch-364
32
from store import ImmutableStore
33
from revision import Revision
184 by mbp at sourcefrog
pychecker fixups
34
from errors import bailout, BzrError
1 by mbp at sourcefrog
import from baz patch-364
35
from textui import show_status
36
from diff import diff_trees
37
38
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
## TODO: Maybe include checks for common corruption of newlines, etc?
40
41
42
416 by Martin Pool
- bzr log and bzr root now accept an http URL
43
def find_branch(f, **args):
44
    if f.startswith('http://') or f.startswith('https://'):
45
        import remotebranch 
46
        return remotebranch.RemoteBranch(f, **args)
47
    else:
48
        return Branch(f, **args)
49
        
50
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
51
def find_branch_root(f=None):
52
    """Find the branch root enclosing f, or pwd.
53
416 by Martin Pool
- bzr log and bzr root now accept an http URL
54
    f may be a filename or a URL.
55
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
56
    It is not necessary that f exists.
57
58
    Basically we keep looking up until we find the control directory or
59
    run into the root."""
184 by mbp at sourcefrog
pychecker fixups
60
    if f == None:
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
61
        f = os.getcwd()
62
    elif hasattr(os.path, 'realpath'):
63
        f = os.path.realpath(f)
64
    else:
65
        f = os.path.abspath(f)
425 by Martin Pool
- check from aaron for existence of a branch
66
    if not os.path.exists(f):
67
        raise BzrError('%r does not exist' % f)
68
        
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
69
70
    orig_f = f
71
72
    while True:
73
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
74
            return f
75
        head, tail = os.path.split(f)
76
        if head == f:
77
            # reached the root, whatever that may be
184 by mbp at sourcefrog
pychecker fixups
78
            raise BzrError('%r is not in a branch' % orig_f)
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
79
        f = head
80
    
1 by mbp at sourcefrog
import from baz patch-364
81
82
83
######################################################################
84
# branch objects
85
86
class Branch:
87
    """Branch holding a history of revisions.
88
343 by Martin Pool
doc
89
    base
90
        Base directory of the branch.
1 by mbp at sourcefrog
import from baz patch-364
91
    """
353 by Martin Pool
- Per-branch locks in read and write modes.
92
    _lockmode = None
93
    
94
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
1 by mbp at sourcefrog
import from baz patch-364
95
        """Create new branch object at a particular location.
96
254 by Martin Pool
- Doc cleanups from Magnus Therning
97
        base -- Base directory for the branch.
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
98
        
254 by Martin Pool
- Doc cleanups from Magnus Therning
99
        init -- If True, create new control files in a previously
1 by mbp at sourcefrog
import from baz patch-364
100
             unversioned directory.  If False, the branch must already
101
             be versioned.
102
254 by Martin Pool
- Doc cleanups from Magnus Therning
103
        find_root -- If true and init is false, find the root of the
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
104
             existing branch containing base.
105
1 by mbp at sourcefrog
import from baz patch-364
106
        In the test suite, creation of new trees is tested using the
107
        `ScratchBranch` class.
108
        """
109
        if init:
64 by mbp at sourcefrog
- fix up init command for new find-branch-root function
110
            self.base = os.path.realpath(base)
1 by mbp at sourcefrog
import from baz patch-364
111
            self._make_control()
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
112
        elif find_root:
113
            self.base = find_branch_root(base)
1 by mbp at sourcefrog
import from baz patch-364
114
        else:
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
115
            self.base = os.path.realpath(base)
1 by mbp at sourcefrog
import from baz patch-364
116
            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'])
62 by mbp at sourcefrog
- new find_branch_root function; based on suggestion from aaron
120
        self._check_format()
353 by Martin Pool
- Per-branch locks in read and write modes.
121
        self.lock(lock_mode)
1 by mbp at sourcefrog
import from baz patch-364
122
123
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
124
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
125
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
126
127
128
    def __str__(self):
129
        return '%s(%r)' % (self.__class__.__name__, self.base)
130
131
132
    __repr__ = __str__
133
134
353 by Martin Pool
- Per-branch locks in read and write modes.
135
136
    def lock(self, mode='w'):
137
        """Lock the on-disk branch, excluding other processes."""
138
        try:
364 by Martin Pool
- Create lockfiles in old branches that don't have one
139
            import fcntl, errno
353 by Martin Pool
- Per-branch locks in read and write modes.
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
364 by Martin Pool
- Create lockfiles in old branches that don't have one
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
159
            
353 by Martin Pool
- Per-branch locks in read and write modes.
160
            fcntl.lockf(lockfile, lm)
358 by Martin Pool
- Fix Branch.unlock()
161
            def unlock():
353 by Martin Pool
- Per-branch locks in read and write modes.
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)
358 by Martin Pool
- Fix Branch.unlock()
169
            def unlock():
353 by Martin Pool
- Per-branch locks in read and write modes.
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)
182
183
67 by mbp at sourcefrog
use abspath() for the function that makes an absolute
184
    def abspath(self, name):
185
        """Return absolute filename for something in the branch"""
1 by mbp at sourcefrog
import from baz patch-364
186
        return os.path.join(self.base, name)
67 by mbp at sourcefrog
use abspath() for the function that makes an absolute
187
1 by mbp at sourcefrog
import from baz patch-364
188
68 by mbp at sourcefrog
- new relpath command and function
189
    def relpath(self, path):
190
        """Return path relative to this branch of something inside it.
191
192
        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
200
201
1 by mbp at sourcefrog
import from baz patch-364
202
    def controlfilename(self, file_or_path):
203
        """Return location relative to branch."""
204
        if isinstance(file_or_path, types.StringTypes):
205
            file_or_path = [file_or_path]
206
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
207
208
209
    def controlfile(self, file_or_path, mode='r'):
245 by mbp at sourcefrog
- control files always in utf-8-unix format
210
        """Open a control file for this branch.
211
212
        There are two classes of file in the control directory: text
213
        and binary.  binary files are untranslated byte streams.  Text
214
        control files are stored with Unix newlines and in UTF-8, even
215
        if the platform or locale defaults are different.
430 by Martin Pool
doc
216
217
        Controlfiles should almost never be opened in write mode but
218
        rather should be atomically copied and replaced using atomicfile.
245 by mbp at sourcefrog
- control files always in utf-8-unix format
219
        """
220
221
        fn = self.controlfilename(file_or_path)
222
223
        if mode == 'rb' or mode == 'wb':
224
            return file(fn, mode)
225
        elif mode == 'r' or mode == 'w':
259 by Martin Pool
- use larger file buffers when opening branch control file
226
            # open in binary mode anyhow so there's no newline translation;
227
            # codecs uses line buffering by default; don't want that.
245 by mbp at sourcefrog
- control files always in utf-8-unix format
228
            import codecs
259 by Martin Pool
- use larger file buffers when opening branch control file
229
            return codecs.open(fn, mode + 'b', 'utf-8',
230
                               buffering=60000)
245 by mbp at sourcefrog
- control files always in utf-8-unix format
231
        else:
232
            raise BzrError("invalid controlfile mode %r" % mode)
233
1 by mbp at sourcefrog
import from baz patch-364
234
235
236
    def _make_control(self):
237
        os.mkdir(self.controlfilename([]))
238
        self.controlfile('README', 'w').write(
239
            "This is a Bazaar-NG control directory.\n"
240
            "Do not change any files in this directory.")
245 by mbp at sourcefrog
- control files always in utf-8-unix format
241
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
1 by mbp at sourcefrog
import from baz patch-364
242
        for d in ('text-store', 'inventory-store', 'revision-store'):
243
            os.mkdir(self.controlfilename(d))
244
        for f in ('revision-history', 'merged-patches',
353 by Martin Pool
- Per-branch locks in read and write modes.
245
                  'pending-merged-patches', 'branch-name',
246
                  'branch-lock'):
1 by mbp at sourcefrog
import from baz patch-364
247
            self.controlfile(f, 'w').write('')
248
        mutter('created control directory in ' + self.base)
249
        Inventory().write_xml(self.controlfile('inventory','w'))
250
251
252
    def _check_format(self):
253
        """Check this branch format is supported.
254
255
        The current tool only supports the current unstable format.
256
257
        In the future, we might need different in-memory Branch
258
        classes to support downlevel branches.  But not yet.
163 by mbp at sourcefrog
merge win32 portability fixes
259
        """
260
        # This ignores newlines so that we can open branches created
261
        # on Windows from Linux and so on.  I think it might be better
262
        # to always make all internal files in unix format.
245 by mbp at sourcefrog
- control files always in utf-8-unix format
263
        fmt = self.controlfile('branch-format', 'r').read()
163 by mbp at sourcefrog
merge win32 portability fixes
264
        fmt.replace('\r\n', '')
1 by mbp at sourcefrog
import from baz patch-364
265
        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'])
269
270
271
    def read_working_inventory(self):
272
        """Read the working inventory."""
353 by Martin Pool
- Per-branch locks in read and write modes.
273
        self._need_readlock()
1 by mbp at sourcefrog
import from baz patch-364
274
        before = time.time()
245 by mbp at sourcefrog
- control files always in utf-8-unix format
275
        # ElementTree does its own conversion from UTF-8, so open in
276
        # binary.
277
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
1 by mbp at sourcefrog
import from baz patch-364
278
        mutter("loaded inventory of %d items in %f"
279
               % (len(inv), time.time() - before))
280
        return inv
281
282
283
    def _write_inventory(self, inv):
284
        """Update the working inventory.
285
286
        That is to say, the inventory describing changes underway, that
287
        will be committed to the next revision.
288
        """
353 by Martin Pool
- Per-branch locks in read and write modes.
289
        self._need_writelock()
14 by mbp at sourcefrog
write inventory to temporary file and atomically replace
290
        ## TODO: factor out to atomicfile?  is rename safe on windows?
70 by mbp at sourcefrog
Prepare for smart recursive add.
291
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
14 by mbp at sourcefrog
write inventory to temporary file and atomically replace
292
        tmpfname = self.controlfilename('inventory.tmp')
245 by mbp at sourcefrog
- control files always in utf-8-unix format
293
        tmpf = file(tmpfname, 'wb')
14 by mbp at sourcefrog
write inventory to temporary file and atomically replace
294
        inv.write_xml(tmpf)
295
        tmpf.close()
163 by mbp at sourcefrog
merge win32 portability fixes
296
        inv_fname = self.controlfilename('inventory')
297
        if sys.platform == 'win32':
298
            os.remove(inv_fname)
299
        os.rename(tmpfname, inv_fname)
14 by mbp at sourcefrog
write inventory to temporary file and atomically replace
300
        mutter('wrote working inventory')
1 by mbp at sourcefrog
import from baz patch-364
301
302
303
    inventory = property(read_working_inventory, _write_inventory, None,
304
                         """Inventory for the working copy.""")
305
306
307
    def add(self, files, verbose=False):
308
        """Make files versioned.
309
247 by mbp at sourcefrog
doc
310
        Note that the command line normally calls smart_add instead.
311
1 by mbp at sourcefrog
import from baz patch-364
312
        This puts the files in the Added state, so that they will be
313
        recorded by the next commit.
314
254 by Martin Pool
- Doc cleanups from Magnus Therning
315
        TODO: Perhaps have an option to add the ids even if the files do
1 by mbp at sourcefrog
import from baz patch-364
316
               not (yet) exist.
317
254 by Martin Pool
- Doc cleanups from Magnus Therning
318
        TODO: Perhaps return the ids of the files?  But then again it
1 by mbp at sourcefrog
import from baz patch-364
319
               is easy to retrieve them if they're needed.
320
254 by Martin Pool
- Doc cleanups from Magnus Therning
321
        TODO: Option to specify file id.
1 by mbp at sourcefrog
import from baz patch-364
322
254 by Martin Pool
- Doc cleanups from Magnus Therning
323
        TODO: Adding a directory should optionally recurse down and
1 by mbp at sourcefrog
import from baz patch-364
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', [])
348
        """
353 by Martin Pool
- Per-branch locks in read and write modes.
349
        self._need_writelock()
1 by mbp at sourcefrog
import from baz patch-364
350
351
        # TODO: Re-adding a file that is removed in the working copy
352
        # should probably put it back with the previous ID.
353
        if isinstance(files, types.StringTypes):
354
            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
                
67 by mbp at sourcefrog
use abspath() for the function that makes an absolute
366
            fullpath = os.path.normpath(self.abspath(f))
1 by mbp at sourcefrog
import from baz patch-364
367
70 by mbp at sourcefrog
Prepare for smart recursive add.
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
1 by mbp at sourcefrog
import from baz patch-364
380
            if verbose:
381
                show_status('A', kind, quotefn(f))
382
                
70 by mbp at sourcefrog
Prepare for smart recursive add.
383
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
384
            
1 by mbp at sourcefrog
import from baz patch-364
385
        self._write_inventory(inv)
386
387
176 by mbp at sourcefrog
New cat command contributed by janmar.
388
    def print_file(self, file, revno):
389
        """Print `file` to stdout."""
353 by Martin Pool
- Per-branch locks in read and write modes.
390
        self._need_readlock()
176 by mbp at sourcefrog
New cat command contributed by janmar.
391
        tree = self.revision_tree(self.lookup_revision(revno))
178 by mbp at sourcefrog
- Use a non-null file_id for the branch root directory. At the moment
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)
176 by mbp at sourcefrog
New cat command contributed by janmar.
397
        
1 by mbp at sourcefrog
import from baz patch-364
398
399
    def remove(self, files, verbose=False):
400
        """Mark nominated files for removal from the inventory.
401
402
        This does not remove their text.  This does not run on 
403
254 by Martin Pool
- Doc cleanups from Magnus Therning
404
        TODO: Refuse to remove modified files unless --force is given?
1 by mbp at sourcefrog
import from baz patch-364
405
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
254 by Martin Pool
- Doc cleanups from Magnus Therning
428
        TODO: Do something useful with directories.
1 by mbp at sourcefrog
import from baz patch-364
429
254 by Martin Pool
- Doc cleanups from Magnus Therning
430
        TODO: Should this remove the text or not?  Tough call; not
1 by mbp at sourcefrog
import from baz patch-364
431
        removing may be useful and the user can just use use rm, and
432
        is the opposite of add.  Removing it is consistent with most
433
        other tools.  Maybe an option.
434
        """
435
        ## TODO: Normalize names
436
        ## TODO: Remove nested loops; better scalability
353 by Martin Pool
- Per-branch locks in read and write modes.
437
        self._need_writelock()
1 by mbp at sourcefrog
import from baz patch-364
438
439
        if isinstance(files, types.StringTypes):
440
            files = [files]
441
        
29 by Martin Pool
When removing files, new status should be I or ?, not D
442
        tree = self.working_tree()
443
        inv = tree.inventory
1 by mbp at sourcefrog
import from baz patch-364
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:
29 by Martin Pool
When removing files, new status should be I or ?, not D
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))
1 by mbp at sourcefrog
import from baz patch-364
458
            del inv[fid]
459
460
        self._write_inventory(inv)
461
462
463
    def unknowns(self):
464
        """Return all unknown files.
465
466
        These are files in the working directory that are not versioned or
467
        control files or ignored.
468
        
469
        >>> b = ScratchBranch(files=['foo', 'foo~'])
470
        >>> list(b.unknowns())
471
        ['foo']
472
        >>> b.add('foo')
473
        >>> list(b.unknowns())
474
        []
475
        >>> b.remove('foo')
476
        >>> list(b.unknowns())
477
        ['foo']
478
        """
479
        return self.working_tree().unknowns()
480
481
8 by mbp at sourcefrog
store committer's timezone in revision and show
482
    def commit(self, message, timestamp=None, timezone=None,
483
               committer=None,
1 by mbp at sourcefrog
import from baz patch-364
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
254 by Martin Pool
- Doc cleanups from Magnus Therning
501
        timestamp -- if not None, seconds-since-epoch for a
1 by mbp at sourcefrog
import from baz patch-364
502
             postdated/predated commit.
503
        """
353 by Martin Pool
- Per-branch locks in read and write modes.
504
        self._need_writelock()
1 by mbp at sourcefrog
import from baz patch-364
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
67 by mbp at sourcefrog
use abspath() for the function that makes an absolute
531
            p = self.abspath(path)
1 by mbp at sourcefrog
import from baz patch-364
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:
70 by mbp at sourcefrog
Prepare for smart recursive add.
577
                    entry.text_id = gen_file_id(entry.name)
1 by mbp at sourcefrog
import from baz patch-364
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):
93 by mbp at sourcefrog
Fix inverted display of 'R' and 'M' during 'commit -v'
585
                            state = 'M'
586
                        else:
1 by mbp at sourcefrog
import from baz patch-364
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
8 by mbp at sourcefrog
store committer's timezone in revision and show
618
        if timezone == None:
619
            timezone = local_time_offset()
620
1 by mbp at sourcefrog
import from baz patch-364
621
        mutter("building commit log message")
622
        rev = Revision(timestamp=timestamp,
8 by mbp at sourcefrog
store committer's timezone in revision and show
623
                       timezone=timezone,
1 by mbp at sourcefrog
import from baz patch-364
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
233 by mbp at sourcefrog
- more output from test.sh
647
        self.append_revision(rev_id)
648
        
96 by mbp at sourcefrog
with commit -v, show committed revision number
649
        if verbose:
650
            note("commited r%d" % self.revno())
1 by mbp at sourcefrog
import from baz patch-364
651
652
233 by mbp at sourcefrog
- more output from test.sh
653
    def append_revision(self, revision_id):
654
        mutter("add {%s} to revision-history" % revision_id)
655
        rev_history = self.revision_history()
656
657
        tmprhname = self.controlfilename('revision-history.tmp')
658
        rhname = self.controlfilename('revision-history')
659
        
660
        f = file(tmprhname, 'wt')
661
        rev_history.append(revision_id)
662
        f.write('\n'.join(rev_history))
663
        f.write('\n')
664
        f.close()
665
666
        if sys.platform == 'win32':
667
            os.remove(rhname)
668
        os.rename(tmprhname, rhname)
669
        
670
671
1 by mbp at sourcefrog
import from baz patch-364
672
    def get_revision(self, revision_id):
673
        """Return the Revision object for a named revision"""
353 by Martin Pool
- Per-branch locks in read and write modes.
674
        self._need_readlock()
1 by mbp at sourcefrog
import from baz patch-364
675
        r = Revision.read_xml(self.revision_store[revision_id])
676
        assert r.revision_id == revision_id
677
        return r
678
679
680
    def get_inventory(self, inventory_id):
681
        """Get Inventory object by hash.
682
254 by Martin Pool
- Doc cleanups from Magnus Therning
683
        TODO: Perhaps for this and similar methods, take a revision
1 by mbp at sourcefrog
import from baz patch-364
684
               parameter which can be either an integer revno or a
685
               string hash."""
353 by Martin Pool
- Per-branch locks in read and write modes.
686
        self._need_readlock()
1 by mbp at sourcefrog
import from baz patch-364
687
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
        return i
689
690
691
    def get_revision_inventory(self, revision_id):
692
        """Return inventory of a past revision."""
353 by Martin Pool
- Per-branch locks in read and write modes.
693
        self._need_readlock()
1 by mbp at sourcefrog
import from baz patch-364
694
        if revision_id == None:
695
            return Inventory()
696
        else:
697
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
698
699
700
    def revision_history(self):
701
        """Return sequence of revision hashes on to this branch.
702
703
        >>> ScratchBranch().revision_history()
704
        []
705
        """
353 by Martin Pool
- Per-branch locks in read and write modes.
706
        self._need_readlock()
319 by Martin Pool
- remove trivial chomp() function
707
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
1 by mbp at sourcefrog
import from baz patch-364
708
709
385 by Martin Pool
- New Branch.enum_history method
710
    def enum_history(self, direction):
711
        """Return (revno, revision_id) for history of branch.
712
713
        direction
714
            'forward' is from earliest to latest
715
            'reverse' is from latest to earliest
716
        """
717
        rh = self.revision_history()
718
        if direction == 'forward':
719
            i = 1
720
            for rid in rh:
721
                yield i, rid
722
                i += 1
723
        elif direction == 'reverse':
724
            i = len(rh)
725
            while i > 0:
726
                yield i, rh[i-1]
727
                i -= 1
728
        else:
729
            raise BzrError('invalid history direction %r' % direction)
730
731
1 by mbp at sourcefrog
import from baz patch-364
732
    def revno(self):
733
        """Return current revision number for this branch.
734
735
        That is equivalent to the number of revisions committed to
736
        this branch.
737
738
        >>> b = ScratchBranch()
739
        >>> b.revno()
740
        0
741
        >>> b.commit('no foo')
742
        >>> b.revno()
743
        1
744
        """
745
        return len(self.revision_history())
746
747
748
    def last_patch(self):
749
        """Return last patch hash, or None if no history.
750
751
        >>> ScratchBranch().last_patch() == None
752
        True
753
        """
754
        ph = self.revision_history()
755
        if ph:
756
            return ph[-1]
184 by mbp at sourcefrog
pychecker fixups
757
        else:
758
            return None
759
        
1 by mbp at sourcefrog
import from baz patch-364
760
761
    def lookup_revision(self, revno):
762
        """Return revision hash for revision number."""
763
        if revno == 0:
764
            return None
765
766
        try:
767
            # list is 0-based; revisions are 1-based
768
            return self.revision_history()[revno-1]
769
        except IndexError:
184 by mbp at sourcefrog
pychecker fixups
770
            raise BzrError("no such revision %s" % revno)
1 by mbp at sourcefrog
import from baz patch-364
771
772
773
    def revision_tree(self, revision_id):
774
        """Return Tree for a revision on this branch.
775
776
        `revision_id` may be None for the null revision, in which case
777
        an `EmptyTree` is returned."""
353 by Martin Pool
- Per-branch locks in read and write modes.
778
        self._need_readlock()
1 by mbp at sourcefrog
import from baz patch-364
779
        if revision_id == None:
780
            return EmptyTree()
781
        else:
782
            inv = self.get_revision_inventory(revision_id)
783
            return RevisionTree(self.text_store, inv)
784
785
786
    def working_tree(self):
787
        """Return a `Tree` for the working copy."""
453 by Martin Pool
- Split WorkingTree into its own file
788
        from workingtree import WorkingTree
1 by mbp at sourcefrog
import from baz patch-364
789
        return WorkingTree(self.base, self.read_working_inventory())
790
791
792
    def basis_tree(self):
793
        """Return `Tree` object for last revision.
794
795
        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
        """
807
        r = self.last_patch()
808
        if r == None:
809
            return EmptyTree()
810
        else:
811
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
812
813
814
168 by mbp at sourcefrog
new "rename" command
815
    def rename_one(self, from_rel, to_rel):
309 by Martin Pool
doc
816
        """Rename one file.
817
818
        This can change the directory or the filename or both.
353 by Martin Pool
- Per-branch locks in read and write modes.
819
        """
820
        self._need_writelock()
168 by mbp at sourcefrog
new "rename" command
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)
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
848
849
        print "%s => %s" % (from_rel, to_rel)
171 by mbp at sourcefrog
better error message when working file rename fails
850
        
851
        from_abs = self.abspath(from_rel)
852
        to_abs = self.abspath(to_rel)
853
        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"])
168 by mbp at sourcefrog
new "rename" command
859
860
        self._write_inventory(inv)
861
            
862
1 by mbp at sourcefrog
import from baz patch-364
863
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
864
    def move(self, from_paths, to_name):
160 by mbp at sourcefrog
- basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think
865
        """Rename files.
866
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
867
        to_name must exist as a versioned directory.
868
160 by mbp at sourcefrog
- basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think
869
        If to_name exists and is a directory, the files are moved into
870
        it, keeping their old names.  If it is a directory, 
871
872
        Note that to_name is only the last component of the new name;
873
        this doesn't change the directory.
874
        """
353 by Martin Pool
- Per-branch locks in read and write modes.
875
        self._need_writelock()
160 by mbp at sourcefrog
- basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think
876
        ## TODO: Option to move IDs only
877
        assert not isinstance(from_paths, basestring)
878
        tree = self.working_tree()
879
        inv = tree.inventory
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
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):
175 by mbp at sourcefrog
fix up moving files into branch root
884
            bailout("destination %r not in working directory" % to_abs)
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
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]
175 by mbp at sourcefrog
fix up moving files into branch root
889
        if to_dir_ie.kind not in ('directory', 'root_directory'):
890
            bailout("destination %r is not a directory" % to_abs)
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
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:
160 by mbp at sourcefrog
- basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think
917
                os.rename(self.abspath(f), self.abspath(dest_path))
174 by mbp at sourcefrog
- New 'move' command; now separated out from rename
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)
160 by mbp at sourcefrog
- basic support for moving files to different directories - have not done support for renaming them yet, but should be straightforward - some tests, but many cases are not handled yet i think
923
924
925
404 by Martin Pool
- bzr status now optionally takes filenames to check
926
    def show_status(self, show_all=False, file_list=None):
1 by mbp at sourcefrog
import from baz patch-364
927
        """Display single-line status for non-ignored working files.
928
929
        The list is show sorted in order by file name.
930
931
        >>> b = ScratchBranch(files=['foo', 'foo~'])
932
        >>> b.show_status()
933
        ?       foo
934
        >>> b.add('foo')
935
        >>> b.show_status()
936
        A       foo
937
        >>> b.commit("add foo")
938
        >>> b.show_status()
67 by mbp at sourcefrog
use abspath() for the function that makes an absolute
939
        >>> os.unlink(b.abspath('foo'))
15 by mbp at sourcefrog
files that have been deleted are not considered present in the WorkingTree
940
        >>> b.show_status()
941
        D       foo
1 by mbp at sourcefrog
import from baz patch-364
942
        """
353 by Martin Pool
- Per-branch locks in read and write modes.
943
        self._need_readlock()
1 by mbp at sourcefrog
import from baz patch-364
944
945
        # We have to build everything into a list first so that it can
946
        # sorted by name, incorporating all the different sources.
947
948
        # FIXME: Rather than getting things in random order and then sorting,
949
        # just step through in order.
950
951
        # Interesting case: the old ID for a file has been removed,
952
        # but a new file has been created under that name.
953
184 by mbp at sourcefrog
pychecker fixups
954
        old = self.basis_tree()
955
        new = self.working_tree()
1 by mbp at sourcefrog
import from baz patch-364
956
404 by Martin Pool
- bzr status now optionally takes filenames to check
957
        items = diff_trees(old, new)
958
        # We want to filter out only if any file was provided in the file_list.
959
        if isinstance(file_list, list) and len(file_list):
960
            items = [item for item in items if item[3] in file_list]
961
962
        for fs, fid, oldname, newname, kind in items:
1 by mbp at sourcefrog
import from baz patch-364
963
            if fs == 'R':
964
                show_status(fs, kind,
965
                            oldname + ' => ' + newname)
966
            elif fs == 'A' or fs == 'M':
967
                show_status(fs, kind, newname)
968
            elif fs == 'D':
969
                show_status(fs, kind, oldname)
970
            elif fs == '.':
971
                if show_all:
972
                    show_status(fs, kind, newname)
973
            elif fs == 'I':
974
                if show_all:
975
                    show_status(fs, kind, newname)
976
            elif fs == '?':
977
                show_status(fs, kind, newname)
978
            else:
254 by Martin Pool
- Doc cleanups from Magnus Therning
979
                bailout("weird file state %r" % ((fs, fid),))
1 by mbp at sourcefrog
import from baz patch-364
980
                
981
982
983
class ScratchBranch(Branch):
984
    """Special test class: a branch that cleans up after itself.
985
986
    >>> b = ScratchBranch()
987
    >>> isdir(b.base)
988
    True
989
    >>> bd = b.base
396 by Martin Pool
- Using the destructor on a ScratchBranch is not reliable;
990
    >>> b.destroy()
1 by mbp at sourcefrog
import from baz patch-364
991
    >>> isdir(bd)
992
    False
993
    """
100 by mbp at sourcefrog
- add test case for ignore files
994
    def __init__(self, files=[], dirs=[]):
1 by mbp at sourcefrog
import from baz patch-364
995
        """Make a test branch.
996
997
        This creates a temporary directory and runs init-tree in it.
998
999
        If any files are listed, they are created in the working copy.
1000
        """
1001
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
100 by mbp at sourcefrog
- add test case for ignore files
1002
        for d in dirs:
1003
            os.mkdir(self.abspath(d))
1004
            
1 by mbp at sourcefrog
import from baz patch-364
1005
        for f in files:
1006
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
1007
1008
1009
    def __del__(self):
396 by Martin Pool
- Using the destructor on a ScratchBranch is not reliable;
1010
        self.destroy()
1011
1012
    def destroy(self):
1 by mbp at sourcefrog
import from baz patch-364
1013
        """Destroy the test branch, removing the scratch directory."""
163 by mbp at sourcefrog
merge win32 portability fixes
1014
        try:
396 by Martin Pool
- Using the destructor on a ScratchBranch is not reliable;
1015
            mutter("delete ScratchBranch %s" % self.base)
163 by mbp at sourcefrog
merge win32 portability fixes
1016
            shutil.rmtree(self.base)
396 by Martin Pool
- Using the destructor on a ScratchBranch is not reliable;
1017
        except OSError, e:
163 by mbp at sourcefrog
merge win32 portability fixes
1018
            # Work around for shutil.rmtree failing on Windows when
1019
            # readonly files are encountered
396 by Martin Pool
- Using the destructor on a ScratchBranch is not reliable;
1020
            mutter("hit exception in destroying ScratchBranch: %s" % e)
163 by mbp at sourcefrog
merge win32 portability fixes
1021
            for root, dirs, files in os.walk(self.base, topdown=False):
1022
                for name in files:
1023
                    os.chmod(os.path.join(root, name), 0700)
1024
            shutil.rmtree(self.base)
396 by Martin Pool
- Using the destructor on a ScratchBranch is not reliable;
1025
        self.base = None
1 by mbp at sourcefrog
import from baz patch-364
1026
1027
    
1028
1029
######################################################################
1030
# predicates
1031
1032
1033
def is_control_file(filename):
1034
    ## FIXME: better check
1035
    filename = os.path.normpath(filename)
1036
    while filename != '':
1037
        head, tail = os.path.split(filename)
1038
        ## mutter('check %r for control file' % ((head, tail), ))
1039
        if tail == bzrlib.BZRDIR:
1040
            return True
70 by mbp at sourcefrog
Prepare for smart recursive add.
1041
        if filename == head:
1042
            break
1 by mbp at sourcefrog
import from baz patch-364
1043
        filename = head
1044
    return False
1045
1046
1047
1048
def _gen_revision_id(when):
1049
    """Return new revision-id."""
1050
    s = '%s-%s-' % (user_email(), compact_date(when))
190 by mbp at sourcefrog
64 bits of randomness in file/revision ids
1051
    s += hexlify(rand_bytes(8))
1 by mbp at sourcefrog
import from baz patch-364
1052
    return s
1053
1054
70 by mbp at sourcefrog
Prepare for smart recursive add.
1055
def gen_file_id(name):
1 by mbp at sourcefrog
import from baz patch-364
1056
    """Return new file id.
1057
1058
    This should probably generate proper UUIDs, but for the moment we
1059
    cope with just randomness because running uuidgen every time is
1060
    slow."""
70 by mbp at sourcefrog
Prepare for smart recursive add.
1061
    idx = name.rfind('/')
1062
    if idx != -1:
1063
        name = name[idx+1 : ]
262 by Martin Pool
- gen_file_id: break the file on either / or \ when looking
1064
    idx = name.rfind('\\')
1065
    if idx != -1:
1066
        name = name[idx+1 : ]
70 by mbp at sourcefrog
Prepare for smart recursive add.
1067
1068
    name = name.lstrip('.')
1069
190 by mbp at sourcefrog
64 bits of randomness in file/revision ids
1070
    s = hexlify(rand_bytes(8))
1 by mbp at sourcefrog
import from baz patch-364
1071
    return '-'.join((name, compact_date(time.time()), s))