hghooks / hgrcd.py

#!/usr/bin/env python
"""Install symlinks for *.hgrc into repositories.

Command Example::

    $ ./hgrcd.py -f -t ../repos

        Reinstall all symlinks.

    $ ./hgrcd.py -t ../repos foo.hgrc bar/baz.hgrc

        Install the specified hgrc files only.

Hook Usage::

    [hooks]
    changegroup.install = python:/path/to/hgrcd.py:hook

    [hgrcd]
    targetdir = /path/to/repo
    createrepo = true
    deleterepo = bundle
"""
import os, re, shutil
from optparse import OptionParser
from mercurial import commands, hg, ui as uimod

def walk_hgrcfiles(rootdir):
    """Iterates *.hgrc files.

    Yields each filename.
    """
    for dirpath, dirnames, filenames in os.walk(rootdir):
        if '.hg' in dirpath.split(os.path.sep):
            continue

        for e in filenames:
            if e.endswith('.hgrc'):
                path = os.path.join(dirpath, e)
                yield path[len(rootdir) + 1:]

def walk_specified_hgrcfiles(rootdir, files):
    """Iterates specified *.hgrc files with rootdir elimination."""
    for e in files:
        e = os.path.abspath(e)

        if not (e.startswith(os.path.abspath(rootdir) + os.path.sep)
                and e.endswith('.hgrc')):
            raise ValueError('unexpected file: %s' % e)

        if not os.path.exists(e):
            raise ValueError('unknown file: %s' % e)

        yield e[len(os.path.abspath(rootdir)) + 1:]

def manglereponame(name):
    """Returns repository name for the given rc name."""
    return os.path.sep.join(re.sub(r'^dot\.', '.', e)
                            for e in name.split(os.path.sep))

def relpath(path, start=os.curdir):
    """Generates shortest relative path."""
    path, start = os.path.abspath(path), os.path.abspath(start)
    common = os.path.dirname(os.path.commonprefix([path, start])).rstrip(os.path.sep) + os.path.sep
    elems = ['..'] * start[len(common):].count(os.path.sep) + [path[len(common):]]
    return os.path.join(*elems)

def do_symlink(src, dest, force=False):
    """Actually creates symlinks pointing to src, named dest."""
    if force and os.path.lexists(dest):
        os.unlink(dest)

    os.symlink(src, dest)

def _rctorepopaths(rcpath, targetdir):
    reponame, cfgname = rcpath.rsplit('.', 1)
    reponame = manglereponame(reponame)
    dest = os.path.join(targetdir, reponame, '.hg', cfgname)
    repodir = os.path.join(targetdir, reponame)
    return repodir, dest

def install(ui, rootdir, files, targetdir, dryrun=False, force=False,
            createrepo=True):
    """Installs symlinks to hgrc files.

    files must be relative path to rootdir.
    """
    for e in files:
        if '.' not in e or e.startswith('.'):
            continue
        repodir, dest = _rctorepopaths(e, targetdir)
        src = relpath(os.path.join(rootdir, e), start=dest)
        if (not force) and os.path.exists(dest):
            continue

        ui.status('hgrcd: creating %s\n' % repodir)
        if not os.path.exists(repodir) and not dryrun:
            os.makedirs(repodir)
        if createrepo and not os.path.exists(os.path.join(repodir, '.hg')):
            ui.note('hg init %s\n' % repodir)
            if not dryrun:
                hg.repository(ui, repodir, create=True)

        if os.path.exists(os.path.join(repodir, '.hg')):
            ui.note('%s -> %s\n' % (dest, src))
            if not dryrun:
                do_symlink(src, dest, force=force)

def uninstall(ui, rootdir, files, targetdir, dryrun=False, deleterepo=None):
    """Uninstall hgrc symlinks from repos.

    files must be relative path to rootdir.
    """
    if not deleterepo or deleterepo not in ('bundle', 'clean'):
        raise ValueError('unknown deleterepo option: %r' % deleterepo)

    for e in files:
        if '.' not in e or e.startswith('.'):
            continue
        repodir, dest = _rctorepopaths(e, targetdir)
        if os.path.exists(dest):
            ui.note('rm %s\n' % dest)
            if not dryrun:
                os.unlink(dest)

        if deleterepo and os.path.exists(repodir):
            ui.status('hgrcd: deleting %s\n' % repodir)
            if deleterepo == 'bundle':
                ui.note('hg bundle %s.hg~\n', repodir)
                if not dryrun:
                    commands.bundle(ui, hg.repository(ui, repodir),
                                    '%s.hg~' % repodir, all=True)
            ui.note('rm -R %s\n' % repodir)
            if not dryrun:
                shutil.rmtree(repodir)

def hook(ui, repo, hooktype, node=None, **kwargs):
    targetdir = ui.config('hgrcd', 'targetdir')
    if not targetdir:
        raise ValueError('targetdir not specified')

    commands.update(ui, repo, clean=True)
    modified, added, removed = repo.status(repo[node].p1())[:3]
    install(ui, repo.root, added, targetdir=targetdir,
            force=ui.configbool('hgrcd', 'force', False),
            createrepo=ui.configbool('hgrcd', 'createrepo', True))
    # TODO: handle copy action
    uninstall(ui, repo.root, removed, targetdir=targetdir,
              deleterepo=ui.config('hgrcd', 'deleterepo'))

def main():
    parser = OptionParser()
    parser.usage += " [hgrcfile ...]\n\n" + __doc__
    parser.add_option('-R', '--root-directory', default='.')
    parser.add_option('-v', '--verbose', action='store_true', default=False)
    parser.add_option('-n', '--dry-run', action='store_true', default=False)
    parser.add_option('-f', '--force', action='store_true', default=False)
    parser.add_option('-c', '--create-repository', action='store_true', default=True)
    parser.add_option('-t', '--target-directory')
    (opts, args) = parser.parse_args()
    if opts.dry_run:
        opts.verbose = True

    if not opts.target_directory:
        raise ValueError('target directory not specified')

    if not os.path.isdir(opts.target_directory):
        raise ValueError('bad target directory: %s' % opts.target_directory)

    if args:
        files = walk_specified_hgrcfiles(opts.root_directory, args)
    else:
        files = walk_hgrcfiles(opts.root_directory)

    ui = uimod.ui()
    ui.setconfig('ui', 'verbose', 'true')
    install(ui, opts.root_directory, files, targetdir=opts.target_directory,
            dryrun=opts.dry_run, force=opts.force,
            createrepo=opts.create_repository)

if __name__ == '__main__':
    main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.