diff --git a/library/hg b/library/hg index 2559d5a5d2..9878134910 100644 --- a/library/hg +++ b/library/hg @@ -4,7 +4,8 @@ # (c) 2013, Yeukhon Wong # # This module was originally inspired by Brad Olson's ansible-module-mercurial -# . +# . This module tends +# to follow the git module implementation. # # This file is part of Ansible # @@ -37,7 +38,7 @@ author: Yeukhon Wong options: repo: description: - - The repository location. + - The repository address. required: true default: null dest: @@ -45,62 +46,46 @@ options: - Absolute path of where the repository should be cloned to. required: true 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: description: - - Equivalent C(-r) option in hg command, which can either be a changeset number or a branch - name. + - Equivalent C(-r) option in hg command which could be the changeset, revision number, + branch name or even tag. required: false default: "default" force: description: - - Whether to discard uncommitted changes and remove untracked files or not. Basically, it - combines C(hg up -C) and C(hg purge). + - Discards uncommited changes. Runs C(hg update -C). required: false default: "yes" - choices: [ "yes", "no" ] - -examples: - - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name" - description: Clone the default branch of repo_name. - - - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name force=yes state=latest" - description: Ensure the repository at dest is latest and discard any uncommitted and/or untracked files. - + choices: [ yes, no ] + purge: + description: + - Delets untracked files. Runs C(hg purge). Note this requires C(purge) extension to + 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. + required: false + default: "no" + choices: [ yes, no ] notes: - 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 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), 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: [ ] ''' -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): - # val is a list of triple-tuple of the form [(section, option, value),...] parser = ConfigParser.SafeConfigParser() parser.read(hgrc) + # val is a list of triple-tuple of the form [(section, option, value),...] for each in vals: - section,option, value = each + (section, option, value) = each if not parser.has_section(section): parser.add_section(section) parser.set(section, option, value) @@ -112,9 +97,9 @@ def _set_hgrc(hgrc, vals): def _undo_hgrc(hgrc, vals): parser = ConfigParser.SafeConfigParser() parser.read(hgrc) - + for each in vals: - section, option, value = each + (section, option, value) = each if parser.has_section(section): parser.remove_option(section, option) @@ -124,137 +109,143 @@ def _undo_hgrc(hgrc, vals): def _hg_command(module, 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_list_untracked(module, dest): + return _hg_command(module, ['purge', '-R', dest, '--print']) -def _hg_purge(module, dest): +def get_revision(module, dest): + """ + hg id -b -i -t returns a string in the format: + "[+] " + 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') purge_option = [('extensions', 'purge', '')] - _set_hgrc(hgrc, purge_option) - 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): - """ - 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 + _set_hgrc(hgrc, purge_option) # enable purge extension + + # before purge, find out if there are any untracked files + (rc1, out1, err1) = _hg_list_untracked(module, dest) + if rc1 != 0: + module.fail_json(msg=err) + + # there are some untrackd files + if out1 != '': + (rc2, out2, err2) = _hg_command(module, ['purge', '-R', dest]) + if rc2 == 0: + _undo_hgrc(hgrc, purge_option) 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) - 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) + module.fail_json(msg=err) return True 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(): module = AnsibleModule( argument_spec = dict( repo = dict(required=True), dest = dict(required=True), - state = dict(default='present', choices=['present', 'absent', 'latest']), revision = dict(default="default"), force = dict(default='yes', choices=['yes', 'no']), + purge = dict(default='no', choices=['yes', 'no']) ), ) repo = module.params['repo'] - state = module.params['state'] dest = module.params['dest'] 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 state == 'absent': - if not os.path.exists(dest): - shutil.rmtree(dest) - changed = True - elif state == 'present': - changed = clone(module, repo, dest, revision, force) - elif state == 'latest': - changed = pull_and_update(module, repo, dest, revision, force) + # If there is no hgrc file, then assume repo is absent + # and perform clone. Otherwise, perform pull and update. + if not os.path.exists(hgrc): + (rc, out, err) = hg_clone(module, repo, dest, revision) + if rc != 0: + module.fail_json(msg=err) + else: + # get the current state before doing pulling + before = get_revision(module, dest) - module.exit_json(dest=dest, changed=changed) - except Exception as e: - module.fail_json(msg=str(e), params=module.params) + # can perform force and purge + cleaned = hg_cleanup(module, dest, force, purge) + + (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 #<>