Merge pull request #1948 from yeukhon/devel

hg module cleaned up
This commit is contained in:
Michael DeHaan 2013-02-02 10:27:06 -08:00
commit 34fe59c6a4

283
hg
View file

@ -4,7 +4,8 @@
# (c) 2013, Yeukhon Wong <yeukhon@acm.org> # (c) 2013, Yeukhon Wong <yeukhon@acm.org>
# #
# This module was originally inspired by Brad Olson's ansible-module-mercurial # This module was originally inspired by Brad Olson's ansible-module-mercurial
# <https://github.com/bradobro/ansible-module-mercurial>. # <https://github.com/bradobro/ansible-module-mercurial>. This module tends
# to follow the git module implementation.
# #
# This file is part of Ansible # This file is part of Ansible
# #
@ -37,7 +38,7 @@ author: Yeukhon Wong
options: options:
repo: repo:
description: description:
- The repository location. - The repository address.
required: true required: true
default: null default: null
dest: dest:
@ -45,62 +46,46 @@ options:
- Absolute path of where the repository should be cloned to. - Absolute path of where the repository should be cloned to.
required: true required: true
default: null default: null
state:
description:
- C(hg clone) is performed when state is set to C(present). C(hg pull) and C(hg update) is
performed when state is set to C(latest). If you want the latest copy of the repository,
just rely on C(present). C(latest) assumes the repository is already on disk.
required: false
default: present
choices: [ "present", "absent", "latest" ]
revision: revision:
description: description:
- Equivalent C(-r) option in hg command, which can either be a changeset number or a branch - Equivalent C(-r) option in hg command which could be the changeset, revision number,
name. branch name or even tag.
required: false required: false
default: "default" default: "default"
force: force:
description: description:
- Whether to discard uncommitted changes and remove untracked files or not. Basically, it - Discards uncommited changes. Runs C(hg update -C).
combines C(hg up -C) and C(hg purge).
required: false required: false
default: "yes" default: "yes"
choices: [ "yes", "no" ] choices: [ yes, no ]
purge:
examples: description:
- code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name" - Delets untracked files. Runs C(hg purge). Note this requires C(purge) extension to
description: Clone the default branch of repo_name. be enabled if C(purge=yes). This module will modify hgrc file on behalf of the user
and undo the changes before exiting the task.
- code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name force=yes state=latest" required: false
description: Ensure the repository at dest is latest and discard any uncommitted and/or untracked files. default: "no"
choices: [ yes, no ]
notes: notes:
- If the task seems to be hanging, first verify remote host is in C(known_hosts). - If the task seems to be hanging, first verify remote host is in C(known_hosts).
SSH will prompt user to authorize the first contact with a remote host. One solution is to add SSH will prompt user to authorize the first contact with a remote host. One solution is to add
C(StrictHostKeyChecking no) in C(.ssh/config) which will accept and authorize the connection C(StrictHostKeyChecking no) in C(.ssh/config) which will accept and authorize the connection
on behalf of the user. However, if you run as a different user such as setting sudo to True), on behalf of the user. However, if you run as a different user such as setting sudo to True),
for example, root will not look at the user .ssh/config setting. for example, root will not look at the user .ssh/config setting.
examples:
- code: "hg: repo=https://bitbucket.org/user/repo1 dest=/home/user/repo1 revision=stable purge=yes"
description: Ensure the current working copy is inside the stable branch and deletes untracked files if any.
requirements: [ ] requirements: [ ]
''' '''
class HgError(Exception):
""" Custom exception class to report hg command error. """
def __init__(self, msg, stderr=''):
self.msg = msg + \
"\n\nExtra information on this error: \n" + \
stderr
def __str__(self):
return self.msg
def _set_hgrc(hgrc, vals): def _set_hgrc(hgrc, vals):
# val is a list of triple-tuple of the form [(section, option, value),...]
parser = ConfigParser.SafeConfigParser() parser = ConfigParser.SafeConfigParser()
parser.read(hgrc) parser.read(hgrc)
# val is a list of triple-tuple of the form [(section, option, value),...]
for each in vals: for each in vals:
section,option, value = each (section, option, value) = each
if not parser.has_section(section): if not parser.has_section(section):
parser.add_section(section) parser.add_section(section)
parser.set(section, option, value) parser.set(section, option, value)
@ -112,9 +97,9 @@ def _set_hgrc(hgrc, vals):
def _undo_hgrc(hgrc, vals): def _undo_hgrc(hgrc, vals):
parser = ConfigParser.SafeConfigParser() parser = ConfigParser.SafeConfigParser()
parser.read(hgrc) parser.read(hgrc)
for each in vals: for each in vals:
section, option, value = each (section, option, value) = each
if parser.has_section(section): if parser.has_section(section):
parser.remove_option(section, option) parser.remove_option(section, option)
@ -124,137 +109,143 @@ def _undo_hgrc(hgrc, vals):
def _hg_command(module, args_list): def _hg_command(module, args_list):
(rc, out, err) = module.run_command(['hg'] + args_list) (rc, out, err) = module.run_command(['hg'] + args_list)
return (out, err, rc) return (rc, out, err)
def _hg_discard(module, dest): def _hg_list_untracked(module, dest):
out, err, code = _hg_command(module, ['up', '-C', '-R', dest]) return _hg_command(module, ['purge', '-R', dest, '--print'])
if code != 0:
raise HgError(err)
def _hg_purge(module, dest): def get_revision(module, dest):
"""
hg id -b -i -t returns a string in the format:
"<changeset>[+] <branch_name> <tag>"
This format lists the state of the current working copy,
and indicates whether there are uncommitted changes by the
plus sign. Otherwise, the sign is omitted.
Read the full description via hg id --help
"""
(rc, out, err) = _hg_command(module, ['id', '-b', '-i', '-t', '-R', dest])
if rc != 0:
module.fail_json(msg=err)
else:
return out.strip('\n')
def has_local_mods(module, dest):
now = get_revision(module, dest)
if '+' in now:
return True
else:
return False
def hg_discard(module, dest):
before = has_local_mods(module, dest)
if not before:
return False
(rc, out, err) = _hg_command(module, ['update', '-C', '-R', dest])
if rc != 0:
module.fail_json(msg=err)
after = has_local_mods(module, dest)
if before != after and not after: # no more local modification
return True
def hg_purge(module, dest):
hgrc = os.path.join(dest, '.hg/hgrc') hgrc = os.path.join(dest, '.hg/hgrc')
purge_option = [('extensions', 'purge', '')] purge_option = [('extensions', 'purge', '')]
_set_hgrc(hgrc, purge_option) _set_hgrc(hgrc, purge_option) # enable purge extension
out, err, code = _hg_command(module, ['purge', '-R', dest])
if code == 0: # before purge, find out if there are any untracked files
_undo_hgrc(hgrc, purge_option) (rc1, out1, err1) = _hg_list_untracked(module, dest)
else: if rc1 != 0:
raise HgError(err) module.fail_json(msg=err)
def _hg_verify(module, dest): # there are some untrackd files
error1 = "hg verify failed." if out1 != '':
error2 = "{dest} is not a repository.".format(dest=dest) (rc2, out2, err2) = _hg_command(module, ['purge', '-R', dest])
if rc2 == 0:
out, err, code = _hg_command(module, ['verify', '-R', dest]) _undo_hgrc(hgrc, purge_option)
if code == 1:
raise HgError(error1, stderr=err)
elif code == 255:
raise HgError(error2, stderr=err)
elif code == 0:
return True
def _post_op_hg_revision_check(module, dest, revision):
"""
Verify the tip is the same as `revision`.
This function is usually called after some hg operations
such as `clone`. However, this check is skipped if `revision`
is the string `default` since it will result an error.
Instead, pull is performed.
"""
err1 = "Unable to perform hg tip."
err2 = "tip is different from %s. See below for extended summary." % revision
if revision == 'default':
out, err, code = _hg_command(module, ['pull', '-R', dest])
if "no changes found" in out:
return True
else: else:
raise HgError(err2, stderr=out) module.fail_json(msg=err)
else:
out, err, code = _hg_command(module, ['tip', '-R', dest])
if revision in out: # revision should be part of the output (changeset: $revision ...)
return True
else:
if code != 0: # something went wrong with hg tip
raise HgError(err1, stderr=err)
else: # hg tip is fine, but tip != revision
raise HgError(err2, stderr=out)
def force_and_clean(module, dest):
_hg_discard(module, dest)
_hg_purge(module, dest)
def pull_and_update(module, repo, dest, revision, force):
if force == 'yes':
force_and_clean(module, dest)
if _hg_verify(module, dest):
cmd1 = ['pull', '-R', dest, '-r', revision]
out, err, code = _hg_command(module, cmd1)
if code == 1:
raise HgError("Unable to perform pull on %s" % dest, stderr=err)
elif code == 0:
cmd2 = ['update', '-R', dest, '-r', revision]
out, err, code = _hg_command(module, cmd2)
if code == 1:
raise HgError("There are unresolved files in %s" % dest, stderr=err)
elif code == 0:
# so far pull and update seems to be working, check revision and $revision are equal
_post_op_hg_revision_check(module, dest, revision)
return True
# when code aren't 1 or 0 in either command
raise HgError("", stderr=err)
def clone(module, repo, dest, revision, force):
if os.path.exists(dest):
if _hg_verify(module, dest): # make sure it's a real repo
if _post_op_hg_revision_check(module, dest, revision): # make sure revision and $revision are equal
if force == 'yes':
force_and_clean(module, dest)
return False
cmd = ['clone', repo, dest, '-r', revision]
out, err, code = _hg_command(module, cmd)
if code == 0:
_hg_verify(module, dest)
_post_op_hg_revision_check(module, dest, revision)
return True return True
else: else:
raise HgError(err, stderr='') return False
def hg_cleanup(module, dest, force, purge):
discarded = False
purged = False
if force:
discarded = hg_discard(module, dest)
if purge:
purged = hg_purge(module, dest)
if discarded or purged:
return True
else:
return False
def hg_pull(module, dest, revision):
return _hg_command(module, ['pull', '-r', revision, '-R', dest])
def hg_update(module, dest, revision):
return _hg_command(module, ['update', '-R', dest])
def hg_clone(module, repo, dest, revision):
return _hg_command(module, ['clone', repo, dest, '-r', revision])
def switch_version(module, dest, revision):
return _hg_command(module, ['update', '-r', revision, '-R', dest])
# ===========================================
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec = dict( argument_spec = dict(
repo = dict(required=True), repo = dict(required=True),
dest = dict(required=True), dest = dict(required=True),
state = dict(default='present', choices=['present', 'absent', 'latest']),
revision = dict(default="default"), revision = dict(default="default"),
force = dict(default='yes', choices=['yes', 'no']), force = dict(default='yes', choices=['yes', 'no']),
purge = dict(default='no', choices=['yes', 'no'])
), ),
) )
repo = module.params['repo'] repo = module.params['repo']
state = module.params['state']
dest = module.params['dest'] dest = module.params['dest']
revision = module.params['revision'] revision = module.params['revision']
force = module.params['force'] force = module.boolean(module.params['force'])
purge = module.boolean(module.params['purge'])
hgrc = os.path.join(dest, '.hg/hgrc')
# initial states
before = ''
changed = False
cleaned = False
try: # If there is no hgrc file, then assume repo is absent
if state == 'absent': # and perform clone. Otherwise, perform pull and update.
if not os.path.exists(dest): if not os.path.exists(hgrc):
shutil.rmtree(dest) (rc, out, err) = hg_clone(module, repo, dest, revision)
changed = True if rc != 0:
elif state == 'present': module.fail_json(msg=err)
changed = clone(module, repo, dest, revision, force) else:
elif state == 'latest': # get the current state before doing pulling
changed = pull_and_update(module, repo, dest, revision, force) before = get_revision(module, dest)
module.exit_json(dest=dest, changed=changed) # can perform force and purge
except Exception as e: cleaned = hg_cleanup(module, dest, force, purge)
module.fail_json(msg=str(e), params=module.params)
(rc, out, err) = hg_pull(module, dest, revision)
if rc != 0:
module.fail_json(msg=err)
(rc, out, err) = hg_update(module, dest, revision)
if rc != 0:
module.fail_json(msg=err)
switch_version(module, dest, revision)
after = get_revision(module, dest)
if before != after or cleaned:
changed = True
module.exit_json(before=before, after=after, changed=changed, cleaned=cleaned)
# include magic from lib/ansible/module_common.py # include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>> #<<INCLUDE_ANSIBLE_MODULE_COMMON>>