~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: mbp at sourcefrog
  • Date: 2005-04-01 08:22:11 UTC
  • Revision ID: mbp@sourcefrog.net-20050401082211-da0a0e8911740407
- 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

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# (C) 2005 Canonical Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
17
16
 
18
17
"""Inventories map files to their name in a revision."""
19
18
 
 
19
# TODO: Maybe store inventory_id in the file?  Not really needed.
20
20
 
21
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
22
__author__ = "Martin Pool <mbp@canonical.com>"
23
23
 
24
 
import sys, os.path, types
 
24
import sys, os.path, types, re
25
25
from sets import Set
26
26
 
27
27
try:
31
31
 
32
32
from xml import XMLMixin
33
33
from errors import bailout
34
 
from osutils import uuid, quotefn, splitpath, joinpath, appendpath
35
 
from trace import mutter
 
34
 
 
35
import bzrlib
 
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
37
from bzrlib.trace import mutter
36
38
 
37
39
class InventoryEntry(XMLMixin):
38
40
    """Description of a versioned file.
122
124
        self.parent_id = parent_id
123
125
        self.text_sha1 = None
124
126
        self.text_size = None
 
127
        if kind == 'directory':
 
128
            self.children = {}
 
129
 
 
130
 
 
131
    def sorted_children(self):
 
132
        l = self.children.items()
 
133
        l.sort()
 
134
        return l
125
135
 
126
136
 
127
137
    def copy(self):
195
205
 
196
206
 
197
207
 
 
208
class RootEntry(InventoryEntry):
 
209
    def __init__(self, file_id):
 
210
        self.file_id = file_id
 
211
        self.children = {}
 
212
        self.kind = 'root_directory'
 
213
        self.parent_id = None
 
214
        self.name = ''
 
215
 
 
216
    def __cmp__(self, other):
 
217
        if self is other:
 
218
            return 0
 
219
        if not isinstance(other, RootEntry):
 
220
            return NotImplemented
 
221
        return cmp(self.file_id, other.file_id) \
 
222
               or cmp(self.children, other.children)
 
223
 
 
224
 
 
225
 
198
226
class Inventory(XMLMixin):
199
227
    """Inventory of versioned files in a tree.
200
228
 
213
241
    returned quickly.
214
242
 
215
243
    InventoryEntry objects must not be modified after they are
216
 
    inserted.
 
244
    inserted, other than through the Inventory API.
217
245
 
218
246
    >>> inv = Inventory()
219
247
    >>> inv.write_xml(sys.stdout)
222
250
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
223
251
    >>> inv['123-123'].name
224
252
    'hello.c'
225
 
    >>> for file_id in inv: print file_id
226
 
    ...
227
 
    123-123
228
253
 
229
254
    May be treated as an iterator or set to look up file ids:
230
255
    
245
270
 
246
271
    """
247
272
 
248
 
    ## TODO: Clear up handling of files in subdirectories; we probably
249
 
    ## do want to be able to just look them up by name but this
250
 
    ## probably means gradually walking down the path, looking up as we go.
251
 
 
252
273
    ## TODO: Make sure only canonical filenames are stored.
253
274
 
254
275
    ## TODO: Do something sensible about the possible collisions on
255
276
    ## case-losing filesystems.  Perhaps we should just always forbid
256
277
    ## such collisions.
257
278
 
258
 
    ## _tree should probably just be stored as
259
 
    ## InventoryEntry._children on each directory.
 
279
    ## TODO: No special cases for root, rather just give it a file id
 
280
    ## like everything else.
 
281
 
 
282
    ## TODO: Probably change XML serialization to use nesting
260
283
 
261
284
    def __init__(self):
262
285
        """Create or read an inventory.
264
287
        If a working directory is specified, the inventory is read
265
288
        from there.  If the file is specified, read from that. If not,
266
289
        the inventory is created empty.
 
290
 
 
291
        The inventory is created with a default root directory, with
 
292
        an id of None.
267
293
        """
268
 
        self._byid = dict()
269
 
 
270
 
        # _tree is indexed by parent_id; at each level a map from name
271
 
        # to ie.  The None entry is the root.
272
 
        self._tree = {None: {}}
 
294
        self.root = RootEntry(None)
 
295
        self._byid = {None: self.root}
273
296
 
274
297
 
275
298
    def __iter__(self):
281
304
        return len(self._byid)
282
305
 
283
306
 
284
 
    def iter_entries(self, parent_id=None):
 
307
    def iter_entries(self, from_dir=None):
285
308
        """Return (path, entry) pairs, in order by name."""
286
 
        kids = self._tree[parent_id].items()
 
309
        if from_dir == None:
 
310
            assert self.root
 
311
            from_dir = self.root
 
312
        elif isinstance(from_dir, basestring):
 
313
            from_dir = self._byid[from_dir]
 
314
            
 
315
        kids = from_dir.children.items()
287
316
        kids.sort()
288
317
        for name, ie in kids:
289
318
            yield name, ie
290
319
            if ie.kind == 'directory':
291
 
                for cn, cie in self.iter_entries(parent_id=ie.file_id):
292
 
                    yield joinpath([name, cn]), cie
293
 
 
294
 
 
295
 
    def directories(self, include_root=True):
 
320
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
321
                    yield '/'.join((name, cn)), cie
 
322
                    
 
323
 
 
324
 
 
325
    def directories(self, from_dir=None):
296
326
        """Return (path, entry) pairs for all directories.
297
327
        """
298
 
        if include_root:
299
 
            yield '', None
300
 
        for path, entry in self.iter_entries():
301
 
            if entry.kind == 'directory':
302
 
                yield path, entry
 
328
        def descend(parent_ie):
 
329
            parent_name = parent_ie.name
 
330
            yield parent_name, parent_ie
 
331
 
 
332
            # directory children in sorted order
 
333
            dn = []
 
334
            for ie in parent_ie.children.itervalues():
 
335
                if ie.kind == 'directory':
 
336
                    dn.append((ie.name, ie))
 
337
            dn.sort()
 
338
            
 
339
            for name, child_ie in dn:
 
340
                for sub_name, sub_ie in descend(child_ie):
 
341
                    yield appendpath(parent_name, sub_name), sub_ie
 
342
 
 
343
        for name, ie in descend(self.root):
 
344
            yield name, ie
303
345
        
304
346
 
305
347
 
306
 
    def children(self, parent_id):
307
 
        """Return entries that are direct children of parent_id."""
308
 
        return self._tree[parent_id]
309
 
                    
310
 
 
311
 
 
312
 
    # TODO: return all paths and entries
313
 
 
314
 
 
315
348
    def __contains__(self, file_id):
316
349
        """True if this entry contains a file with given id.
317
350
 
336
369
        return self._byid[file_id]
337
370
 
338
371
 
 
372
    def get_child(self, parent_id, filename):
 
373
        return self[parent_id].children.get(filename)
 
374
 
 
375
 
339
376
    def add(self, entry):
340
377
        """Add entry to inventory.
341
378
 
342
379
        To add  a file to a branch ready to be committed, use Branch.add,
343
380
        which calls this."""
344
 
        if entry.file_id in self:
 
381
        if entry.file_id in self._byid:
345
382
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
346
383
 
347
 
        if entry.parent_id != None:
348
 
            if entry.parent_id not in self:
349
 
                bailout("parent_id %s of new entry not found in inventory"
350
 
                        % entry.parent_id)
351
 
            
352
 
        if self._tree[entry.parent_id].has_key(entry.name):
353
 
            bailout("%s is already versioned"
354
 
                    % appendpath(self.id2path(entry.parent_id), entry.name))
 
384
        try:
 
385
            parent = self._byid[entry.parent_id]
 
386
        except KeyError:
 
387
            bailout("parent_id %r not in inventory" % entry.parent_id)
 
388
 
 
389
        if parent.children.has_key(entry.name):
 
390
            bailout("%s is already versioned" %
 
391
                    appendpath(self.id2path(parent.file_id), entry.name))
355
392
 
356
393
        self._byid[entry.file_id] = entry
357
 
        self._tree[entry.parent_id][entry.name] = entry
358
 
 
359
 
        if entry.kind == 'directory':
360
 
            self._tree[entry.file_id] = {}
 
394
        parent.children[entry.name] = entry
 
395
 
 
396
 
 
397
    def add_path(self, relpath, kind, file_id=None):
 
398
        """Add entry from a path.
 
399
 
 
400
        The immediate parent must already be versioned"""
 
401
        parts = bzrlib.osutils.splitpath(relpath)
 
402
        if len(parts) == 0:
 
403
            bailout("cannot re-add root of inventory")
 
404
 
 
405
        if file_id is None:
 
406
            file_id = bzrlib.branch.gen_file_id(relpath)
 
407
 
 
408
        parent_id = self.path2id(parts[:-1])
 
409
        ie = InventoryEntry(file_id, parts[-1],
 
410
                            kind=kind, parent_id=parent_id)
 
411
        return self.add(ie)
361
412
 
362
413
 
363
414
    def __delitem__(self, file_id):
373
424
        """
374
425
        ie = self[file_id]
375
426
 
376
 
        assert self._tree[ie.parent_id][ie.name] == ie
 
427
        assert self[ie.parent_id].children[ie.name] == ie
377
428
        
378
429
        # TODO: Test deleting all children; maybe hoist to a separate
379
430
        # deltree method?
380
431
        if ie.kind == 'directory':
381
 
            for cie in self._tree[file_id].values():
 
432
            for cie in ie.children.values():
382
433
                del self[cie.file_id]
383
 
            del self._tree[file_id]
 
434
            del ie.children
384
435
 
385
436
        del self._byid[file_id]
386
 
        del self._tree[ie.parent_id][ie.name]
 
437
        del self[ie.parent_id].children[ie.name]
387
438
 
388
439
 
389
440
    def id_set(self):
452
503
        """Return as a list the path to file_id."""
453
504
        p = []
454
505
        while file_id != None:
455
 
            ie = self[file_id]
456
 
            p = [ie.name] + p
 
506
            ie = self._byid[file_id]
 
507
            p.insert(0, ie.name)
457
508
            file_id = ie.parent_id
458
 
        return joinpath(p)
 
509
        return '/'.join(p)
459
510
            
460
511
 
461
512
 
468
519
        This returns the entry of the last component in the path,
469
520
        which may be either a file or a directory.
470
521
        """
471
 
        assert isinstance(name, types.StringTypes)
 
522
        if isinstance(name, types.StringTypes):
 
523
            name = splitpath(name)
472
524
 
473
 
        parent_id = None
474
 
        for f in splitpath(name):
 
525
        parent = self[None]
 
526
        for f in name:
475
527
            try:
476
 
                cie = self._tree[parent_id][f]
 
528
                cie = parent.children[f]
477
529
                assert cie.name == f
478
 
                parent_id = cie.file_id
 
530
                parent = cie
479
531
            except KeyError:
480
532
                # or raise an error?
481
533
                return None
482
534
 
483
 
        return parent_id
484
 
 
485
 
 
486
 
    def get_child(self, parent_id, child_name):
487
 
        return self._tree[parent_id].get(child_name)
 
535
        return parent.file_id
488
536
 
489
537
 
490
538
    def has_filename(self, names):
492
540
 
493
541
 
494
542
    def has_id(self, file_id):
495
 
        assert isinstance(file_id, str)
496
543
        return self._byid.has_key(file_id)
497
544
 
498
545
 
 
546
    def rename(self, file_id, new_parent_id, new_name):
 
547
        """Move a file within the inventory.
 
548
 
 
549
        This can change either the name, or the parent, or both.
 
550
 
 
551
        This does not move the working file."""
 
552
        if not is_valid_name(new_name):
 
553
            bailout("not an acceptable filename: %r" % new_name)
 
554
 
 
555
        new_parent = self._byid[new_parent_id]
 
556
        if new_name in new_parent.children:
 
557
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
558
 
 
559
        file_ie = self._byid[file_id]
 
560
        old_parent = self._byid[file_ie.parent_id]
 
561
 
 
562
        # TODO: Don't leave things messed up if this fails
 
563
 
 
564
        del old_parent.children[file_ie.name]
 
565
        new_parent.children[new_name] = file_ie
 
566
        
 
567
        file_ie.name = new_name
 
568
        file_ie.parent_id = new_parent_id
 
569
 
 
570
 
 
571
 
 
572
 
 
573
_NAME_RE = re.compile(r'^[^/\\]+$')
 
574
 
 
575
def is_valid_name(name):
 
576
    return bool(_NAME_RE.match(name))
 
577
 
 
578
 
499
579
 
500
580
if __name__ == '__main__':
501
581
    import doctest, inventory