From 7b0a3c3f2d6c67cef52b51d002a7b974867e13d7 Mon Sep 17 00:00:00 2001 From: Yeukhon Wong Date: Fri, 25 Jan 2013 22:51:20 -0500 Subject: [PATCH 1/4] Added hg module to the core. --- library/hg | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 library/hg diff --git a/library/hg b/library/hg new file mode 100644 index 0000000000..04d74c44fd --- /dev/null +++ b/library/hg @@ -0,0 +1,267 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- + +# (c) 2013, Yeukhon Wong +# +# This module was originally inspired by Brad Olson's ansible-module-mercurial +# . +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import os +import shutil +import ConfigParser +from subprocess import Popen, PIPE + +DOCUMENTATION = ''' +--- +module: hg +short_description: Manages Mercurial (hg) repositories. +description: + - Manages Mercurial (hg) repositories. Supports SSH, HTTP/S and local address. +version_added: "1.0" +author: Yeukhon Wong +options: + repo: + description: + - The repository location. + required: true + default: null + dest: + description: + - 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. + 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). + 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. + +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. + +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) + + for each in vals: + section,option, value = each + if not parser.has_section(section): + parser.add_section(section) + parser.set(section, option, value) + + f = open(hgrc, 'w') + parser.write(f) + f.close() + +def _undo_hgrc(hgrc, vals): + parser = ConfigParser.SafeConfigParser() + parser.read(hgrc) + + for each in vals: + section, option, value = each + if parser.has_section(section): + parser.remove_option(section, option) + + f = open(hgrc, 'w') + parser.write(f) + f.close() + +def _hg_command(args_list): + cmd = ['hg'] + args_list + p = Popen(cmd, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + return out, err, p.returncode + +def _hg_discard(dest): + out, err, code = _hg_command(['up', '-C', '-R', dest]) + if code != 0: + raise HgError(err) + +def _hg_purge(dest): + hgrc = os.path.join(dest, '.hg/hgrc') + purge_option = [('extensions', 'purge', '')] + _set_hgrc(hgrc, purge_option) + out, err, code = _hg_command(['purge', '-R', dest]) + if code == 0: + _undo_hgrc(hgrc, purge_option) + else: + raise HgError(err) + +def _hg_verify(dest): + error1 = "hg verify failed." + error2 = "{dest} is not a repository.".format(dest=dest) + + out, err, code = _hg_command(['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(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(['pull', '-R', dest]) + if "no changes found" in out: + return True + else: + raise HgError(err2, stderr=out) + else: + out, err, code = _hg_command(['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(dest): + _hg_discard(dest) + _hg_purge(dest) + +def pull_and_update(repo, dest, revision, force): + if force == 'yes': + force_and_clean(dest) + + if _hg_verify(dest): + cmd1 = ['pull', '-R', dest, '-r', revision] + out, err, code = _hg_command(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(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(dest, revision) + return True + # when code aren't 1 or 0 in either command + raise HgError("", stderr=err) + +def clone(repo, dest, revision, force): + if os.path.exists(dest): + if _hg_verify(dest): # make sure it's a real repo + if _post_op_hg_revision_check(dest, revision): # make sure revision and $revision are equal + if force == 'yes': + force_and_clean(dest) + return False + + cmd = ['clone', repo, dest, '-r', revision] + out, err, code = _hg_command(cmd) + if code == 0: + _hg_verify(dest) + _post_op_hg_revision_check(dest, revision) + return True + else: + raise HgError(err, stderr='') + +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']), + ), + ) + repo = module.params['repo'] + state = module.params['state'] + dest = module.params['dest'] + revision = module.params['revision'] + force = module.params['force'] + + try: + if state == 'absent': + if not os.path.exists(dest): + shutil.rmtree(dest) + changed = True + elif state == 'present': + changed = clone(repo, dest, revision, force) + elif state == 'latest': + changed = pull_and_update(repo, dest, revision, force) + + module.exit_json(dest=dest, changed=changed) + #except HgError as e: + # module.fail_json(msg=str(e), params=module.params) + #except IOError as e: + # module.fail_json(msg=str(e), params=module.params) + except Exception as e: + module.fail_json(msg=str(e), params=module.params) + +# include magic from lib/ansible/module_common.py +#<> +main() From 99a8e95c9879be3465912b057fd4a1e62182643b Mon Sep 17 00:00:00 2001 From: Yeukhon Wong Date: Tue, 29 Jan 2013 17:29:35 -0500 Subject: [PATCH 2/4] 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 --- library/hg | 263 ++++++++++++++++++++++++++--------------------------- 1 file changed, 129 insertions(+), 134 deletions(-) diff --git a/library/hg b/library/hg index 1998d898fb..e95a4d280b 100644 --- a/library/hg +++ b/library/hg @@ -45,14 +45,6 @@ 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 @@ -61,18 +53,28 @@ options: 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" ] + 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: - 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. + 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" + description: Similar to the previous one, except this uses SSH protocol. + - 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: - If the task seems to be hanging, first verify remote host is in C(known_hosts). @@ -84,23 +86,13 @@ notes: 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,7 +104,7 @@ def _set_hgrc(hgrc, vals): def _undo_hgrc(hgrc, vals): parser = ConfigParser.SafeConfigParser() parser.read(hgrc) - + for each in vals: section, option, value = each if parser.has_section(section): @@ -124,137 +116,140 @@ 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 determine_changed(module, before, after, expecting): + """ + This compares the user supplied revision to the before + and after revision (actually, id). + + get_revision calls hg id -b -i -t which returns the string + "[+] " and we compare if + expected revision (which could be changeset, + branch name) is part of the result string from hg id. + """ -def _hg_purge(module, dest): + # some custom error messages + err1 = "no changes found. You supplied {0} but repo is \ +currently at {1}".format(expecting, after) + + 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: + "[+] " + 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]) + return out.strip('\n') + +def has_local_mods(module, dest): + (rc, out, err) = get_revision(module, dest) + if rc == 0: + if '+' in out: + return True + else: + return False + else: + module.fail_json(msg=err) + +def hg_discard(module, dest, force): + if not force and has_local_mods(module, dest): + module.fail_json(msg="Respository has uncommited changes.") + (rc, out, err) = _hg_command(module, ['update', '-C', '-R', dest]) + return (rc, out, err) + +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: + _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: - raise HgError(err) - -def _hg_verify(module, dest): - error1 = "hg verify failed." - error2 = "{dest} is not a repository.".format(dest=dest) + module.fail_json(msg=err) - 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 hg_pull(module, dest, revision): + (rc, out, err) = _hg_command(module, ['pull', '-r', revision, '-R', dest]) + return (rc, out, err) -def _post_op_hg_revision_check(module, dest, revision): - """ - Verify the tip is the same as `revision`. +def hg_update(module, dest, revision): + (rc, out, err) = _hg_command(module, ['update', '-R', dest]) + return (rc, out, err) - 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: - 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 - - 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 - else: - raise HgError(err, stderr='') +def hg_clone(module, repo, dest, revision): + return _hg_command(module, ['clone', repo, dest, '-r', revision]) 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') + + # 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): + before = '' + (rc, out, err) = hg_clone(module, repo, dest, revision) + if rc != 0: + module.fail_json(msg=err) + after = get_revision(module, dest) + determine_changed(module, before, after, revision) + else: + # get the current state before doing pulling + before = get_revision(module, dest) - 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) + # 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) - module.exit_json(dest=dest, changed=changed) - except Exception as e: - 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) + + after = get_revision(module, dest) + determine_changed(module, before, after, revision) # include magic from lib/ansible/module_common.py #<> From ccc903216b2aa6650bc056eccf661ec88834904b Mon Sep 17 00:00:00 2001 From: Yeukhon Wong Date: Wed, 30 Jan 2013 22:40:26 -0500 Subject: [PATCH 3/4] Use %s instead of .format for string replacement. Revisied the documentation. --- library/hg | 74 +++++++++++++++++++++--------------------------------- 1 file changed, 28 insertions(+), 46 deletions(-) diff --git a/library/hg b/library/hg index e95a4d280b..6f07085a21 100644 --- a/library/hg +++ b/library/hg @@ -37,7 +37,7 @@ author: Yeukhon Wong options: repo: description: - - The repository location. + - The repository address. required: true default: null dest: @@ -53,28 +53,12 @@ options: default: "default" force: description: - - Discards uncommited changes. Runs c(hg update -c). + - Discards uncommited changes. Runs C(hg update -c). required: false default: "yes" 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: - - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/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" - description: Similar to the previous one, except this uses SSH protocol. - - 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: - If the task seems to be hanging, first verify remote host is in C(known_hosts). @@ -92,7 +76,7 @@ def _set_hgrc(hgrc, vals): # 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) @@ -106,7 +90,7 @@ def _undo_hgrc(hgrc, vals): 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) @@ -130,25 +114,26 @@ def determine_changed(module, before, after, expecting): """ # some custom error messages - err1 = "no changes found. You supplied {0} but repo is \ -currently at {1}".format(expecting, after) + err1 = "no changes found. You supplied %s but repo is \ +currently at %s" %(expecting, after) - 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) + err2 = "Unknown error. The state before operation was %s,\ +after the operation was %s, but we were expecting \ +%s as part of the state." %(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 + # in case two, hg will exist successfully although that contradict + # user assumption. Therefore, alert the user by failing the task. if before == after and expecting in after: - return module.exit_json(changed=False, before=before, after=after) + 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) + module.fail_json(msg=err2) + elif before != after and expecting in after: # updated to expecting + module.exit_json(changed=True, before=before, after=after) else: - return module.fail_json(msg=err2) + module.fail_json(msg=err2) def get_revision(module, dest): """ @@ -161,23 +146,22 @@ def get_revision(module, dest): Read the full description via hg id --help """ (rc, out, err) = _hg_command(module, ['id', '-b', '-i', '-t', '-R', dest]) - return out.strip('\n') + if rc != 0: + module.fail_json(msg=err) + else: + return out.strip('\n') def has_local_mods(module, dest): - (rc, out, err) = get_revision(module, dest) - if rc == 0: - if '+' in out: - return True - else: - return False + out = get_revision(module, dest) + if '+' in out: + return True else: - module.fail_json(msg=err) + return False def hg_discard(module, dest, force): if not force and has_local_mods(module, dest): module.fail_json(msg="Respository has uncommited changes.") - (rc, out, err) = _hg_command(module, ['update', '-C', '-R', dest]) - return (rc, out, err) + return _hg_command(module, ['update', '-C', '-R', dest]) def hg_purge(module, dest): hgrc = os.path.join(dest, '.hg/hgrc') @@ -191,12 +175,10 @@ def hg_purge(module, dest): 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) + return _hg_command(module, ['pull', '-r', revision, '-R', dest]) def hg_update(module, dest, revision): - (rc, out, err) = _hg_command(module, ['update', '-R', dest]) - return (rc, out, err) + return _hg_command(module, ['update', '-R', dest]) def hg_clone(module, repo, dest, revision): return _hg_command(module, ['clone', repo, dest, '-r', revision]) From 980ffde192dabe4db4779ab76f1ce61a31f731a1 Mon Sep 17 00:00:00 2001 From: Yeukhon Wong Date: Thu, 31 Jan 2013 02:11:28 -0500 Subject: [PATCH 4/4] Changes is now determined by simply comparing before,after and cleaned (purge and/or force). Doc is updated. --- library/hg | 144 +++++++++++++++++++++++++++++------------------------ 1 file changed, 79 insertions(+), 65 deletions(-) diff --git a/library/hg b/library/hg index 6f07085a21..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 # @@ -47,25 +48,33 @@ options: default: null 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: - - Discards uncommited changes. Runs C(hg update -c). + - Discards uncommited changes. Runs C(hg update -C). required: false default: "yes" - choices: [ "yes", "no" ] - - - + 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: [ ] ''' @@ -102,38 +111,8 @@ def _hg_command(module, args_list): (rc, out, err) = module.run_command(['hg'] + args_list) return (rc, out, err) -def determine_changed(module, before, after, expecting): - """ - This compares the user supplied revision to the before - and after revision (actually, id). - - get_revision calls hg id -b -i -t which returns the string - "[+] " and we compare if - expected revision (which could be changeset, - branch name) is part of the result string from hg id. - """ - - # some custom error messages - err1 = "no changes found. You supplied %s but repo is \ -currently at %s" %(expecting, after) - - err2 = "Unknown error. The state before operation was %s,\ -after the operation was %s, but we were expecting \ -%s as part of the state." %(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 although that contradict - # user assumption. Therefore, alert the user by failing the task. - if before == after and expecting in after: - module.exit_json(changed=False, before=before, after=after) - elif before == after and not expecting in after: # this is case two - module.fail_json(msg=err2) - elif before != after and expecting in after: # updated to expecting - module.exit_json(changed=True, before=before, after=after) - else: - module.fail_json(msg=err2) +def _hg_list_untracked(module, dest): + return _hg_command(module, ['purge', '-R', dest, '--print']) def get_revision(module, dest): """ @@ -152,27 +131,58 @@ def get_revision(module, dest): return out.strip('\n') def has_local_mods(module, dest): - out = get_revision(module, dest) - if '+' in out: + now = get_revision(module, dest) + if '+' in now: return True else: return False -def hg_discard(module, dest, force): - if not force and has_local_mods(module, dest): - module.fail_json(msg="Respository has uncommited changes.") - return _hg_command(module, ['update', '-C', '-R', dest]) +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) # hg purge requires purge extension + _set_hgrc(hgrc, purge_option) # enable purge extension - (rc, out, err) = _hg_command(module, ['purge', '-R', dest]) - if rc == 0: - _undo_hgrc(hgrc, purge_option) - else: + # 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: + module.fail_json(msg=err) + return True + else: + 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]) @@ -183,6 +193,11 @@ def hg_update(module, dest, revision): 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( @@ -199,28 +214,24 @@ def main(): 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 + # 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): - before = '' (rc, out, err) = hg_clone(module, repo, dest, revision) if rc != 0: module.fail_json(msg=err) - after = get_revision(module, dest) - determine_changed(module, before, after, revision) else: # 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) + # can perform force and purge + cleaned = hg_cleanup(module, dest, force, purge) (rc, out, err) = hg_pull(module, dest, revision) if rc != 0: @@ -230,8 +241,11 @@ def main(): if rc != 0: module.fail_json(msg=err) - after = get_revision(module, dest) - determine_changed(module, before, after, revision) + 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 #<>