hg 7.47 KB
Newer Older
1 2 3 4 5 6
#!/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
7 8
# <https://github.com/bradobro/ansible-module-mercurial>. This module tends
# to follow the git module implementation.
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
#
# 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 ConfigParser

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:
38
            - The repository address.
39 40
        required: true
        default: null
41
        aliases: [ name ]
42 43 44 45 46 47 48
    dest:
        description:
            - Absolute path of where the repository should be cloned to.
        required: true
        default: null
    revision:
        description:
49
            - Equivalent C(-r) option in hg command which could be the changeset, revision number,
50
              branch name or even tag.
51 52
        required: false
        default: "default"
53
        aliases: [ version ]
54 55
    force:
        description:
56
            - Discards uncommitted changes. Runs C(hg update -C).
57 58
        required: false
        default: "yes"
59
        choices: [ "yes", "no" ]
60 61
    purge:
        description:
62
            - Deletes untracked files. Runs C(hg purge).
63 64
        required: false
        default: "no"
65
        choices: [ "yes", "no" ]
66 67 68 69 70 71
    executable:
        required: false
        default: null
        version_added: "1.4"
        description:
            - Path to hg executable to use. If not supplied,
72
              the normal mechanism for resolving binary paths will be used.
73
notes:
74
    - "If the task seems to be hanging, first verify remote host is in C(known_hosts).
75 76
      SSH will prompt user to authorize the first contact with a remote host.  To avoid this prompt, 
      one solution is to add the remote host public key in C(/etc/ssh/ssh_known_hosts) before calling 
77
      the hg module, with the following command: ssh-keyscan remote_host.com >> /etc/ssh/ssh_known_hosts."
78 79 80
requirements: [ ]
'''

81 82 83 84 85
EXAMPLES = '''
# Ensure the current working copy is inside the stable branch and deletes untracked files if any.
- hg: repo=https://bitbucket.org/user/repo1 dest=/home/user/repo1 revision=stable purge=yes
'''

86 87 88 89 90 91 92
class Hg(object):

    def __init__(self, module, dest, repo, revision, hg_path):
        self.module = module
        self.dest = dest
        self.repo = repo
        self.revision = revision
93
        self.hg_path = hg_path
94 95 96 97 98 99

    def _command(self, args_list):
        (rc, out, err) = self.module.run_command([self.hg_path] + args_list)
        return (rc, out, err)

    def _list_untracked(self):
100 101
        args = ['purge', '--config', 'extensions.purge=', '-R', self.dest, '--print']
        return self._command(args)
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

    def get_revision(self):
        """
        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) = self._command(['id', '-b', '-i', '-t', '-R', self.dest])
        if rc != 0:
            self.module.fail_json(msg=err)
        else:
            return out.strip('\n')
118

119 120 121 122 123 124 125 126 127 128 129
    def has_local_mods(self):
        now = self.get_revision()
        if '+' in now:
            return True
        else:
            return False

    def discard(self):
        before = self.has_local_mods()
        if not before:
            return False
130

131 132 133
        (rc, out, err) = self._command(['update', '-C', '-R', self.dest])
        if rc != 0:
            self.module.fail_json(msg=err)
134

135 136 137
        after = self.has_local_mods()
        if before != after and not after:   # no more local modification
            return True
138

139 140 141 142 143
    def purge(self):
        # before purge, find out if there are any untracked files
        (rc1, out1, err1) = self._list_untracked()
        if rc1 != 0:
            self.module.fail_json(msg=err1)
144

145 146
        # there are some untrackd files
        if out1 != '':
147 148 149
            args = ['purge', '--config', 'extensions.purge=', '-R', self.dest]
            (rc2, out2, err2) = self._command(args)
            if rc2 != 0:
150 151
                self.module.fail_json(msg=err2)
            return True
152
        else:
153 154 155 156 157
            return False

    def cleanup(self, force, purge):
        discarded = False
        purged = False
158

159 160 161 162 163 164 165 166 167 168 169 170
        if force:
            discarded = self.discard()
        if purge:
            purged = self.purge()
        if discarded or purged:
            return True
        else:
            return False

    def pull(self):
        return self._command(
            ['pull', '-r', self.revision, '-R', self.dest, self.repo])
171

172 173
    def update(self):
        return self._command(['update', '-R', self.dest])
174

175 176
    def clone(self):
        return self._command(['clone', self.repo, self.dest, '-r', self.revision])
177

178 179
    def switch_version(self):
        return self._command(['update', '-r', self.revision, '-R', self.dest])
180 181 182

# ===========================================

183 184 185
def main():
    module = AnsibleModule(
        argument_spec = dict(
186
            repo = dict(required=True, aliases=['name']),
187
            dest = dict(required=True),
188
            revision = dict(default="default", aliases=['version']),
189
            force = dict(default='yes', type='bool'),
190 191
            purge = dict(default='no', type='bool'),
            executable = dict(default=None),
192 193 194
        ),
    )
    repo = module.params['repo']
195
    dest = os.path.expanduser(module.params['dest'])
196
    revision = module.params['revision']
197 198
    force = module.params['force']
    purge = module.params['purge']
199
    hg_path = module.params['executable'] or module.get_bin_path('hg', True)
200
    hgrc = os.path.join(dest, '.hg/hgrc')
201

202 203 204 205 206
    # initial states
    before = ''
    changed = False
    cleaned = False

207 208
    hg = Hg(module, dest, repo, revision, hg_path)

209 210 211
    # 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):
212
        (rc, out, err) = hg.clone()
213 214 215 216
        if rc != 0:
            module.fail_json(msg=err)
    else:
        # get the current state before doing pulling
217
        before = hg.get_revision()
218

219
        # can perform force and purge
220
        cleaned = hg.cleanup(force, purge)
221

222
        (rc, out, err) = hg.pull()
223 224 225
        if rc != 0:
            module.fail_json(msg=err)

226
        (rc, out, err) = hg.update()
227 228 229
        if rc != 0:
            module.fail_json(msg=err)

230 231
    hg.switch_version()
    after = hg.get_revision()
232 233 234
    if before != after or cleaned:
        changed = True
    module.exit_json(before=before, after=after, changed=changed, cleaned=cleaned)
235

236 237
# import module snippets
from ansible.module_utils.basic import *
238
main()