Source

hg-importfs / importfs.py

Full commit
# importfs.py
#
# Copyright 2010 Lantiq Beteiligungs- GmbH & Co. KG
#
# This software may be used and distributed according to the terms of
# the GNU General Public License version 2 or any later version.
"""import a set of files from a file-system into a repository"""
import os
import shutil

from mercurial import commands, hg, util
from mercurial.error import Abort, RepoError
from mercurial.i18n import _

__version__ = '1.0.0'


def get_repo(ui, path):
    """Initialize or create a repository."""
    try:
        repo = hg.repository(ui, path)
    except RepoError:
        repo = hg.repository(ui, path, True)
        ui.status(_('created repository %s\n' % repo.root))
    return repo


def update_repo(ui, repo, rev, branch=None):
    """Update the repository.

    Creates a new branch if necessary.
    """
    if branch:
        if branch not in repo.branchmap():
            # Create a named branch.
            commands.update(ui, repo, rev=rev)
            commands.branch(ui, repo, label=branch)
        else:
            # Update to the named branch.
            commands.update(ui, repo, rev=branch)
    elif rev:
        # Create an anonymous branch.
        commands.update(ui, repo, rev=rev)
        commands.branch(ui, repo)
    elif repo.dirstate.branch() != 'default':
        # Update to the default branch.
        commands.update(ui, repo, rev='default')
    else:
        commands.update(ui, repo)


def onerror(func, path, exc_info=tuple()):
    """Error handler for shutil.rmtree and methods of the os package.

    If the error is due to an access error (read only file)
    it attempts to add write permission and then retries.

    If the error is for another reason it re-raises the error.
    """
    import stat
    if not os.access(path, os.W_OK):
        os.chmod(path, stat.S_IWUSR)
        func(path)
    else:
        raise


def purge_repo(repo):
    """Empty the repository except the metadata."""
    for node in os.listdir(repo.root):
        if node.startswith('.hg'):
            continue
        path = os.path.join(repo.root, node)
        if os.path.isdir(path):
            shutil.rmtree(path, onerror=onerror)
        else:
            try:
                os.remove(path)
            except WindowsError:
                onerror(os.remove, path)


def copyfiles(src, dst, exclude=None, ignore_errors=False):
    """Copy recursively from src to dst directory.

    If src is a file copy only the file.

    If src is a directory copy recursively the entire directory tree. The
    destination directory, named by dst, must not already exist; it will be
    created as well as missing parent directories.

    All files and directories matching the exclude pattern will be ignored. The
    pattern '*.pyc tmp*' will copy everything except .pyc files and files or
    directories whose name starts with tmp.

    If ignore_errors is True all errors will be returned as a list of warnings.
    """
    warnings = []
    try:
        if exclude:
            ignore = shutil.ignore_patterns(*exclude.split())
            if len(ignore(src, os.path.split(src))):
                # Terminate immediately if the src path matches the exclude
                # pattern.
                return warnings
        else:
            ignore = None
        if os.path.isdir(src):
            shutil.copytree(src, dst, ignore=ignore)
        else:
            shutil.copy(src, dst)
    except shutil.Error, err:
        if not ignore_errors:
            raise
        warnings.append(err)
    return warnings


def importfs(ui, repo, source, *pats, **opts):
    """Import a set of files from a file-system into a repository.

    Imports a set of files from a given file-system into a Mercurial
    repository as a changeset. If anything fails, the program exits, leaving
    the repository as it is.

    The specified repository is created if it doesn't exist. It is updated to
    the specified parent revision or null.

    If the repository is updated to a revision other than "tip" the new
    revision is created on a new branch. If the branch option is used the
    branch has the given name. Otherwise the name "default" is used.

    The files in the working directory are deleted and the files from the
    source directories are copied into the working directory. If a file exists
    in more than one source directory the file in the rightmost directory wins.
    "hg addremove" with the specified similarity is executed. "hg commit" with
    the specified message is executed. If a tag is specified, "hg tag" with the
    name is executed.
    """
    sources = []
    for node in (source,) + pats:
        path = util.expandpath(node)
        if not os.path.exists(path):
            raise Abort(_('directory %s does not exist') % path)
        sources.append(util.expandpath(path))
    repo = get_repo(ui, repo)
    update_repo(ui, repo, opts.get('rev'), opts.get('branch'))
    purge_repo(repo)
    # Copy all files into the repository.
    for sourcepath in sources:
        for node in os.listdir(sourcepath):
            src = os.path.join(sourcepath, node)
            dst = os.path.join(repo.root, node)
            warnings = copyfiles(src, dst, opts.get('exclude'),
                opts.get('ignore_copy_errors'))
            if len(warnings) == 0:
                continue
            # Print warnings.
            for warning in warnings:
                ui.warn('Warning: Failed to copy %s to %s (%s).\n' %
                    warning.args[0][0])
    commands.addremove(ui, repo, similarity=opts.get('similarity'))
    message = opts.get('message') or 'importfs commit.'
    commands.commit(ui, repo, message=message)
    tag = opts.get('tag')
    if tag:
        commands.tag(ui, repo, tag)


cmdtable = {'importfs':
    (importfs,
    [('r', 'rev', '', _('The revision to use as the immediate predecessor of '
        'the new revision. If omitted the revision null is used.'), _('REV')),
    ('b', 'branch', '', _("The name of a branch for the new revision. If it "
        "doesn't exist it is created. If omitted the default branch is used. "
        "This option is required if a revision other than tip is specified."),
        _('NAME')),
    ('s', 'similarity', 100, _('A number to pass as the value of the '
        'similarity option to "hg addremove" for guessing file renames. (See '
        'hg help for an explanation.) If omitted the value "100" is used.'),
        _('SIMILARITY')),
    ('m', 'message', '', _('The commit message to be used. If omitted the '
        'tag string is used.'), _('TEXT')),
    ('t', 'tag', '', _('The tag for the resulting revision. If omitted the '
        'revision is not tagged.'), _('NAME')),
    ('', 'exclude', '', _('Exclude all files matching the given pattern.'),
        _('PATTERN')),
    ('', 'ignore-copy-errors', None, _('Turn all errors during the file copy '
        'operation into warnings.'))],
    '[OPTION]... REPO SOURCE...')
}
commands.norepo += ' importfs'