ansible/library/hg
Yeukhon Wong 99a8e95c98 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
2013-01-29 17:29:35 -05:00

256 lines
8.9 KiB
Python

#!/usr/bin/python
#-*- coding: utf-8 -*-
# (c) 2013, Yeukhon Wong <yeukhon@acm.org>
#
# This module was originally inspired by Brad Olson's ansible-module-mercurial
# <https://github.com/bradobro/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 <http://www.gnu.org/licenses/>.
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
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:
- 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).
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: [ ]
'''
def _set_hgrc(hgrc, vals):
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
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(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
"<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.
"""
# 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:
"<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])
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) # hg purge requires purge extension
(rc, out, err) = _hg_command(module, ['purge', '-R', dest])
if rc == 0:
_undo_hgrc(hgrc, purge_option)
else:
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():
module = AnsibleModule(
argument_spec = dict(
repo = dict(required=True),
dest = dict(required=True),
revision = dict(default="default"),
force = dict(default='yes', choices=['yes', 'no']),
purge = dict(default='no', choices=['yes', 'no'])
),
)
repo = module.params['repo']
dest = module.params['dest']
revision = module.params['revision']
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)
# 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_ANSIBLE_MODULE_COMMON>>
main()