~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/mutabletree.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-07-04 08:08:08 UTC
  • mfrom: (2568.2.10 add)
  • Revision ID: pqm@pqm.ubuntu.com-20070704080808-0ptk5p5yiwxjgnt7
(robertc) Overhaul smart_add to be an api on MutableTree allowing specialisation and reducing some cruft. (Robert Collins, Martin Pool)

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
"""
21
21
 
22
22
 
 
23
from bzrlib.lazy_import import lazy_import
 
24
lazy_import(globals(), """
 
25
import os
 
26
 
 
27
from bzrlib import (
 
28
    add,
 
29
    bzrdir,
 
30
    )
 
31
from bzrlib.osutils import dirname
 
32
from bzrlib.trace import mutter, warning
 
33
""")
 
34
 
23
35
from bzrlib import (
24
36
    errors,
25
37
    osutils,
260
272
            parent tree - i.e. a ghost.
261
273
        """
262
274
        raise NotImplementedError(self.set_parent_trees)
 
275
 
 
276
    @needs_tree_write_lock
 
277
    def smart_add(self, file_list, recurse=True, action=None, save=True):
 
278
        """Version file_list, optionally recursing into directories.
 
279
 
 
280
        This is designed more towards DWIM for humans than API clarity.
 
281
        For the specific behaviour see the help for cmd_add().
 
282
 
 
283
        :param action: A reporter to be called with the inventory, parent_ie,
 
284
            path and kind of the path being added. It may return a file_id if 
 
285
            a specific one should be used.
 
286
        :param save: Save the inventory after completing the adds. If False
 
287
            this provides dry-run functionality by doing the add and not saving
 
288
            the inventory.  Note that the modified inventory is left in place,
 
289
            allowing further dry-run tasks to take place. To restore the
 
290
            original inventory call self.read_working_inventory().
 
291
        :return: A tuple - files_added, ignored_files. files_added is the count
 
292
            of added files, and ignored_files is a dict mapping files that were
 
293
            ignored to the rule that caused them to be ignored.
 
294
        """
 
295
        # not in an inner loop; and we want to remove direct use of this,
 
296
        # so here as a reminder for now. RBC 20070703
 
297
        from bzrlib.inventory import InventoryEntry
 
298
        assert isinstance(recurse, bool)
 
299
        if action is None:
 
300
            action = add.AddAction()
 
301
        
 
302
        if not file_list:
 
303
            # no paths supplied: add the entire tree.
 
304
            file_list = [u'.']
 
305
        # mutter("smart add of %r")
 
306
        inv = self.inventory
 
307
        added = []
 
308
        ignored = {}
 
309
        dirs_to_add = []
 
310
        user_dirs = set()
 
311
 
 
312
        # validate user file paths and convert all paths to tree 
 
313
        # relative : it's cheaper to make a tree relative path an abspath
 
314
        # than to convert an abspath to tree relative.
 
315
        for filepath in file_list:
 
316
            rf = _FastPath(self.relpath(filepath))
 
317
            # validate user parameters. Our recursive code avoids adding new files
 
318
            # that need such validation 
 
319
            if self.is_control_filename(rf.raw_path):
 
320
                raise errors.ForbiddenControlFileError(filename=rf.raw_path)
 
321
            
 
322
            abspath = self.abspath(rf.raw_path)
 
323
            kind = osutils.file_kind(abspath)
 
324
            if kind == 'directory':
 
325
                # schedule the dir for scanning
 
326
                user_dirs.add(rf)
 
327
            else:
 
328
                if not InventoryEntry.versionable_kind(kind):
 
329
                    raise errors.BadFileKindError(filename=abspath, kind=kind)
 
330
            # ensure the named path is added, so that ignore rules in the later directory
 
331
            # walk dont skip it.
 
332
            # we dont have a parent ie known yet.: use the relatively slower inventory 
 
333
            # probing method
 
334
            versioned = inv.has_filename(rf.raw_path)
 
335
            if versioned:
 
336
                continue
 
337
            added.extend(_add_one_and_parent(self, inv, None, rf, kind, action))
 
338
 
 
339
        if not recurse:
 
340
            # no need to walk any directories at all.
 
341
            if len(added) > 0 and save:
 
342
                self._write_inventory(inv)
 
343
            return added, ignored
 
344
 
 
345
        # only walk the minimal parents needed: we have user_dirs to override
 
346
        # ignores.
 
347
        prev_dir = None
 
348
 
 
349
        is_inside = osutils.is_inside_or_parent_of_any
 
350
        for path in sorted(user_dirs):
 
351
            if (prev_dir is None or not is_inside([prev_dir], path.raw_path)):
 
352
                dirs_to_add.append((path, None))
 
353
            prev_dir = path.raw_path
 
354
 
 
355
        # dirs_to_add is initialised to a list of directories, but as we scan
 
356
        # directories we append files to it.
 
357
        # XXX: We should determine kind of files when we scan them rather than
 
358
        # adding to this list. RBC 20070703
 
359
        for directory, parent_ie in dirs_to_add:
 
360
            # directory is tree-relative
 
361
            abspath = self.abspath(directory.raw_path)
 
362
 
 
363
            # get the contents of this directory.
 
364
 
 
365
            # find the kind of the path being added.
 
366
            kind = osutils.file_kind(abspath)
 
367
 
 
368
            if not InventoryEntry.versionable_kind(kind):
 
369
                warning("skipping %s (can't add file of kind '%s')", abspath, kind)
 
370
                continue
 
371
 
 
372
            if parent_ie is not None:
 
373
                versioned = directory.base_path in parent_ie.children
 
374
            else:
 
375
                # without the parent ie, use the relatively slower inventory 
 
376
                # probing method
 
377
                versioned = inv.has_filename(directory.raw_path)
 
378
 
 
379
            if kind == 'directory':
 
380
                try:
 
381
                    sub_branch = bzrdir.BzrDir.open(abspath)
 
382
                    sub_tree = True
 
383
                except errors.NotBranchError:
 
384
                    sub_tree = False
 
385
                except errors.UnsupportedFormatError:
 
386
                    sub_tree = True
 
387
            else:
 
388
                sub_tree = False
 
389
 
 
390
            if directory.raw_path == '':
 
391
                # mutter("tree root doesn't need to be added")
 
392
                sub_tree = False
 
393
            elif versioned:
 
394
                pass
 
395
                # mutter("%r is already versioned", abspath)
 
396
            elif sub_tree:
 
397
                # XXX: This is wrong; people *might* reasonably be trying to add
 
398
                # subtrees as subtrees.  This should probably only be done in formats 
 
399
                # which can represent subtrees, and even then perhaps only when
 
400
                # the user asked to add subtrees.  At the moment you can add them
 
401
                # specially through 'join --reference', which is perhaps
 
402
                # reasonable: adding a new reference is a special operation and
 
403
                # can have a special behaviour.  mbp 20070306
 
404
                mutter("%r is a nested bzr tree", abspath)
 
405
            else:
 
406
                _add_one(self, inv, parent_ie, directory, kind, action)
 
407
                added.append(directory.raw_path)
 
408
 
 
409
            if kind == 'directory' and not sub_tree:
 
410
                if parent_ie is not None:
 
411
                    # must be present:
 
412
                    this_ie = parent_ie.children[directory.base_path]
 
413
                else:
 
414
                    # without the parent ie, use the relatively slower inventory 
 
415
                    # probing method
 
416
                    this_id = inv.path2id(directory.raw_path)
 
417
                    if this_id is None:
 
418
                        this_ie = None
 
419
                    else:
 
420
                        this_ie = inv[this_id]
 
421
 
 
422
                for subf in sorted(os.listdir(abspath)):
 
423
                    # here we could use TreeDirectory rather than 
 
424
                    # string concatenation.
 
425
                    subp = osutils.pathjoin(directory.raw_path, subf)
 
426
                    # TODO: is_control_filename is very slow. Make it faster. 
 
427
                    # TreeDirectory.is_control_filename could also make this 
 
428
                    # faster - its impossible for a non root dir to have a 
 
429
                    # control file.
 
430
                    if self.is_control_filename(subp):
 
431
                        mutter("skip control directory %r", subp)
 
432
                    elif subf in this_ie.children:
 
433
                        # recurse into this already versioned subdir.
 
434
                        dirs_to_add.append((_FastPath(subp, subf), this_ie))
 
435
                    else:
 
436
                        # user selection overrides ignoes
 
437
                        # ignore while selecting files - if we globbed in the
 
438
                        # outer loop we would ignore user files.
 
439
                        ignore_glob = self.is_ignored(subp)
 
440
                        if ignore_glob is not None:
 
441
                            # mutter("skip ignored sub-file %r", subp)
 
442
                            ignored.setdefault(ignore_glob, []).append(subp)
 
443
                        else:
 
444
                            #mutter("queue to add sub-file %r", subp)
 
445
                            dirs_to_add.append((_FastPath(subp, subf), this_ie))
 
446
 
 
447
        if len(added) > 0 and save:
 
448
            self._write_inventory(inv)
 
449
        return added, ignored
 
450
 
 
451
 
 
452
class _FastPath(object):
 
453
    """A path object with fast accessors for things like basename."""
 
454
 
 
455
    __slots__ = ['raw_path', 'base_path']
 
456
 
 
457
    def __init__(self, path, base_path=None):
 
458
        """Construct a FastPath from path."""
 
459
        if base_path is None:
 
460
            self.base_path = osutils.basename(path)
 
461
        else:
 
462
            self.base_path = base_path
 
463
        self.raw_path = path
 
464
 
 
465
    def __cmp__(self, other):
 
466
        return cmp(self.raw_path, other.raw_path)
 
467
 
 
468
    def __hash__(self):
 
469
        return hash(self.raw_path)
 
470
 
 
471
 
 
472
def _add_one_and_parent(tree, inv, parent_ie, path, kind, action):
 
473
    """Add a new entry to the inventory and automatically add unversioned parents.
 
474
 
 
475
    :param inv: Inventory which will receive the new entry.
 
476
    :param parent_ie: Parent inventory entry if known, or None.  If
 
477
        None, the parent is looked up by name and used if present, otherwise it
 
478
        is recursively added.
 
479
    :param kind: Kind of new entry (file, directory, etc)
 
480
    :param action: callback(inv, parent_ie, path, kind); return ignored.
 
481
    :return: A list of paths which have been added.
 
482
    """
 
483
    # Nothing to do if path is already versioned.
 
484
    # This is safe from infinite recursion because the tree root is
 
485
    # always versioned.
 
486
    if parent_ie is not None:
 
487
        # we have a parent ie already
 
488
        added = []
 
489
    else:
 
490
        # slower but does not need parent_ie
 
491
        if inv.has_filename(path.raw_path):
 
492
            return []
 
493
        # its really not there : add the parent
 
494
        # note that the dirname use leads to some extra str copying etc but as
 
495
        # there are a limited number of dirs we can be nested under, it should
 
496
        # generally find it very fast and not recurse after that.
 
497
        added = _add_one_and_parent(tree, inv, None,
 
498
            _FastPath(dirname(path.raw_path)), 'directory', action)
 
499
        parent_id = inv.path2id(dirname(path.raw_path))
 
500
        parent_ie = inv[parent_id]
 
501
    _add_one(tree, inv, parent_ie, path, kind, action)
 
502
    return added + [path.raw_path]
 
503
 
 
504
 
 
505
def _add_one(tree, inv, parent_ie, path, kind, file_id_callback):
 
506
    """Add a new entry to the inventory.
 
507
 
 
508
    :param inv: Inventory which will receive the new entry.
 
509
    :param parent_ie: Parent inventory entry.
 
510
    :param kind: Kind of new entry (file, directory, etc)
 
511
    :param file_id_callback: callback(inv, parent_ie, path, kind); return a
 
512
        file_id or None to generate a new file id
 
513
    :returns: None
 
514
    """
 
515
    file_id = file_id_callback(inv, parent_ie, path, kind)
 
516
    entry = inv.make_entry(kind, path.base_path, parent_ie.file_id,
 
517
        file_id=file_id)
 
518
    inv.add(entry)