Rewrote hg module based on feedback.

1. state option is removed
2. force is hg update -C
3. purge is hg clean/hg purge but default to no
4. relies on hg abililty to handle errors
pull/1948/head
Yeukhon Wong 12 years ago
parent 92fd1c6578
commit 99a8e95c98

@ -45,14 +45,6 @@ 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 can either be a changeset number or a branch
@ -61,18 +53,28 @@ options:
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:
description:
- Deletes untracked files. C(hg purge) is the same as C(hg clean).
To use this option, the C(purge = ) extension must be enabled.
This module can edit the hgrc file on behalf of the user and
undo the edit for you. Remember deleting untracked files is
an irreversible action.
required: false
default: "no"
choices: ["yes", "no" ]
examples: examples:
- code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name" - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name"
description: Clone the default branch of repo_name. description: Clone the latest default branch from repo_name repository on Bitbucket.
- code: "hg: repo=ssh://hg@bitbucket.org/user/repo_name dest=/home/user/repo_name"
- code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name force=yes state=latest" description: Similar to the previous one, except this uses SSH protocol.
description: Ensure the repository at dest is latest and discard any uncommitted and/or untracked files. - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name -r BRANCH_NAME
description: Clone the repo and set the working copy to be at BRANCH_NAME
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).
@ -84,21 +86,11 @@ notes:
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):
@ -124,137 +116,140 @@ 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):
out, err, code = _hg_command(module, ['up', '-C', '-R', dest])
if code != 0:
raise HgError(err)
def _hg_purge(module, dest): def determine_changed(module, before, after, expecting):
hgrc = os.path.join(dest, '.hg/hgrc') """
purge_option = [('extensions', 'purge', '')] This compares the user supplied revision to the before
_set_hgrc(hgrc, purge_option) and after revision (actually, id).
out, err, code = _hg_command(module, ['purge', '-R', dest])
if code == 0:
_undo_hgrc(hgrc, purge_option)
else:
raise HgError(err)
def _hg_verify(module, dest):
error1 = "hg verify failed."
error2 = "{dest} is not a repository.".format(dest=dest)
out, err, code = _hg_command(module, ['verify', '-R', dest])
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): get_revision calls hg id -b -i -t which returns the string
"<changeset>[+] <branch_name> <tag>" and we compare if
expected revision (which could be changeset,
branch name) is part of the result string from hg id.
""" """
Verify the tip is the same as `revision`.
This function is usually called after some hg operations # some custom error messages
such as `clone`. However, this check is skipped if `revision` err1 = "no changes found. You supplied {0} but repo is \
is the string `default` since it will result an error. currently at {1}".format(expecting, after)
Instead, pull is performed.
err2 = "Unknown error. The state before operation was {0},\
after the operation was {1}, but we were expecting \
{2} as part of the state.".format(before, after, expecting)
# if before and after are equal, only two possible explainations
# case one: when current working copy is already what user want
# case two: when current working copy is ahead of what user want
# in case two, hg will exist successfully
if before == after and expecting in after:
return module.exit_json(changed=False, before=before, after=after)
elif before == after and not expecting in after: # this is case two
return module.fail_json(msg=err2)
elif before != after and expecting in after: # bingo. pull and update to expecting
return module.exit_json(changed=True, before=before, after=after)
else:
return module.fail_json(msg=err2)
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.
err1 = "Unable to perform hg tip." Read the full description via hg id --help
err2 = "tip is different from %s. See below for extended summary." % revision """
(rc, out, err) = _hg_command(module, ['id', '-b', '-i', '-t', '-R', dest])
return out.strip('\n')
if revision == 'default': def has_local_mods(module, dest):
out, err, code = _hg_command(module, ['pull', '-R', dest]) (rc, out, err) = get_revision(module, dest)
if "no changes found" in out: if rc == 0:
if '+' in out:
return True return True
else: else:
raise HgError(err2, stderr=out)
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)t
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 return False
else:
module.fail_json(msg=err)
cmd = ['clone', repo, dest, '-r', revision] def hg_discard(module, dest, force):
out, err, code = _hg_command(module, cmd) if not force and has_local_mods(module, dest):
if code == 0: module.fail_json(msg="Respository has uncommited changes.")
_hg_verify(module, dest) (rc, out, err) = _hg_command(module, ['update', '-C', '-R', dest])
_post_op_hg_revision_check(module, dest, revision) return (rc, out, err)
return True
def hg_purge(module, dest):
hgrc = os.path.join(dest, '.hg/hgrc')
purge_option = [('extensions', 'purge', '')]
_set_hgrc(hgrc, purge_option) # hg purge requires purge extension
(rc, out, err) = _hg_command(module, ['purge', '-R', dest])
if rc == 0:
_undo_hgrc(hgrc, purge_option)
else: else:
raise HgError(err, stderr='') module.fail_json(msg=err)
def hg_pull(module, dest, revision):
(rc, out, err) = _hg_command(module, ['pull', '-r', revision, '-R', dest])
return (rc, out, err)
def hg_update(module, dest, revision):
(rc, out, err) = _hg_command(module, ['update', '-R', dest])
return (rc, out, err)
def hg_clone(module, repo, dest, revision):
return _hg_command(module, ['clone', repo, dest, '-r', revision])
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'])
try: hgrc = os.path.join(dest, '.hg/hgrc')
if state == 'absent':
if not os.path.exists(dest): # If there is no hgrc file, then assume repo is absent
shutil.rmtree(dest) # and perform clone. Otherwise, perform pull and update.
changed = True if not os.path.exists(hgrc):
elif state == 'present': before = ''
changed = clone(module, repo, dest, revision, force) (rc, out, err) = hg_clone(module, repo, dest, revision)
elif state == 'latest': if rc != 0:
changed = pull_and_update(module, repo, dest, revision, force) module.fail_json(msg=err)
after = get_revision(module, dest)
module.exit_json(dest=dest, changed=changed) determine_changed(module, before, after, revision)
except Exception as e: else:
module.fail_json(msg=str(e), params=module.params) # get the current state before doing pulling
before = get_revision(module, dest)
# calls hg update -C and abort when uncommited changes
# are present if force=no
(rc, out, err) = hg_discard(module, dest, force)
if rc != 0:
module.fail_json(msg=err)
if purge:
hg_purge(module, dest)
(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)
after = get_revision(module, dest)
determine_changed(module, before, after, revision)
# include magic from lib/ansible/module_common.py # include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>> #<<INCLUDE_ANSIBLE_MODULE_COMMON>>

Loading…
Cancel
Save