git 9.27 KB
Newer Older
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3

4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# 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/>.

21 22 23 24 25 26 27
DOCUMENTATION = '''
---
module: git
author: Michael DeHaan
version_added: 0.0.1
short_description: Deploy software (or files) from git checkouts
description:
Jan-Piet Mens committed
28
    - Manage I(git) checkouts of repositories to deploy files or software.
29 30 31 32 33
options:
    repo:
        required: true
        aliases: [ name ]
        description:
Jan-Piet Mens committed
34
            - git, SSH, or HTTP protocol address of the git repository.
35 36 37 38 39 40 41 42 43
    dest:
        required: true
        description:
            - Absolute path of where the repository should be checked out to.
    version:
        required: false
        default: "HEAD"
        description:
            - What version of the repository to check out.  This can be the
Jan-Piet Mens committed
44
              git I(SHA), the literal string C(HEAD), a branch name, or a tag name.
45 46 47 48
    remote:
        required: false
        default: "origin"
        description:
49
            - Name of the remote.
50 51 52 53
    force:
        required: false
        default: "yes"
        choices: [ yes, no ]
Jan-Piet Mens committed
54
        version_added: "0.7"
55
        description:
Jan-Piet Mens committed
56
            - If C(yes), any modified files in the working
57 58 59
              repository will be discarded.  Prior to 0.7, this was always
              'yes' and could not be disabled.
examples:
60
    - code: "git: repo=git://foosball.example.org/path/to/repo.git dest=/srv/checkout version=release-0.22"
61
      description: Example git checkout from Ansible Playbooks
62 63
    - code: "git: repo=ssh://git@github.com/mylogin/hello.git dest=/home/mylogin/hello"
      description: Example read-write git checkout from github
64 65
'''

66
import re
67
import tempfile
68 69

def get_version(dest):
70 71 72 73 74 75
    ''' samples the version of the git repo '''
    os.chdir(dest)
    cmd = "git show --abbrev-commit"
    sha = os.popen(cmd).read().split("\n")
    sha = sha[0].split()[1]
    return sha
76

77
def clone(module, repo, dest, remote):
78
    ''' makes a new git repo if it does not already exist '''
79
    dest_dirname = os.path.dirname(dest)
80
    try:
81
        os.makedirs(dest_dirname)
82 83
    except:
        pass
84
    os.chdir(dest_dirname)
85
    return module.run_command("git clone -o %s %s %s" % (remote, repo, dest),
86
                              check_rc=True)
87 88 89 90 91

def has_local_mods(dest):
    os.chdir(dest)
    cmd = "git status -s"
    lines = os.popen(cmd).read().splitlines()
92
    lines = filter(lambda c: not re.search('^\\?\\?.*$', c), lines)
93 94 95
    return len(lines) > 0

def reset(module,dest,force):
96 97 98 99 100 101
    '''
    Resets the index and working tree to HEAD.
    Discards any changes to tracked files in working
    tree since that commit.
    '''
    os.chdir(dest)
102 103
    if not force and has_local_mods(dest):
        module.fail_json(msg="Local modifications exist in repository (force=no).")
104
    return module.run_command("git reset --hard HEAD", check_rc=True)
105

106
def get_branches(module, dest):
107 108
    os.chdir(dest)
    branches = []
109
    (rc, out, err) = module.run_command("git branch -a")
Stephen Fromm committed
110
    if rc != 0:
111
        module.fail_json(msg="Could not determine branch data - received %s" % out)
112 113 114 115
    for line in out.split('\n'):
        branches.append(line.strip())
    return branches

116 117
def is_remote_branch(module, dest, remote, branch):
    branches = get_branches(module, dest)
118 119 120 121 122 123
    rbranch = 'remotes/%s/%s' % (remote, branch)
    if rbranch in branches:
        return True
    else:
        return False

124 125
def is_local_branch(module, dest, branch):
    branches = get_branches(module, dest)
126 127 128
    lbranch = '%s' % branch
    if lbranch in branches:
        return True
129 130
    elif '* %s' % branch in branches:
        return True
131 132 133
    else:
        return False

134 135 136 137 138 139 140 141 142 143
def is_current_branch(module, dest, branch):
    branches = get_branches(module, dest)
    for b in branches:
        if b.startswith('* '):
            cur_branch = b
    if branch == cur_branch or '* %s' % branch == cur_branch:
        return True
    else:
        return True

144 145 146 147 148 149 150 151
def is_not_a_branch(module, dest):
    branches = get_branches(module, dest)
    for b in branches:
        if b.startswith('* ') and 'no branch' in b:
            return True
    return False

def get_head_branch(module, dest, remote):
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
    '''
    Determine what branch HEAD is associated with.  This is partly
    taken from lib/ansible/utils/__init__.py.  It finds the correct
    path to .git/HEAD and reads from that file the branch that HEAD is
    associated with.  In the case of a detached HEAD, this will look
    up the branch in .git/refs/remotes/<remote>/HEAD.
    '''
    repo_path = os.path.join(dest, '.git')
    # Check if the .git is a file. If it is a file, it means that we are in a submodule structure.
    if os.path.isfile(repo_path):
        try:
            gitdir = yaml.load(open(repo_path)).get('gitdir')
            # There is a posibility the .git file to have an absolute path.
            if os.path.isabs(gitdir):
                repo_path = gitdir
            else:
                repo_path = os.path.join(repo_path.split('.git')[0], gitdir)
        except (IOError, AttributeError):
            return ''
    # Read .git/HEAD for the name of the branch.
    # If we're in a detached HEAD state, look up the branch associated with
    # the remote HEAD in .git/refs/remotes/<remote>/HEAD
    f = open(os.path.join(repo_path, "HEAD"))
    if is_not_a_branch(module, dest):
        f.close()
        f = open(os.path.join(repo_path, 'refs', 'remotes', remote, 'HEAD'))
    branch = f.readline().split('/')[-1].rstrip("\n")
    f.close()
    return branch
181

Stephen Fromm committed
182
def fetch(module, repo, dest, version, remote):
183 184
    ''' updates repo from remote sources '''
    os.chdir(dest)
185
    (rc, out1, err1) = module.run_command("git fetch %s" % remote)
Stephen Fromm committed
186 187
    if rc != 0:
        module.fail_json(msg="Failed to download remote objects and refs")
188

189
    (rc, out2, err2) = module.run_command("git fetch --tags %s" % remote)
Stephen Fromm committed
190 191 192
    if rc != 0:
        module.fail_json(msg="Failed to download remote objects and refs")
    return (rc, out1 + out2, err1 + err2)
193

194 195
def switch_version(module, dest, remote, version):
    ''' once pulled, switch to a particular SHA, tag, or branch '''
196 197 198
    os.chdir(dest)
    cmd = ''
    if version != 'HEAD':
199 200 201 202
        if is_remote_branch(module, dest, remote, version):
            if not is_local_branch(module, dest, version):
                cmd = "git checkout --track -b %s %s/%s" % (version, remote, version)
            else:
203
                (rc, out, err) = module.run_command("git checkout --force %s" % version)
204 205 206
                if rc != 0:
                    module.fail_json(msg="Failed to checkout branch %s" % version)
                cmd = "git reset --hard %s/%s" % (remote, version)
207 208 209
        else:
            cmd = "git checkout --force %s" % version
    else:
210
        branch = get_head_branch(module, dest, remote)
211
        (rc, out, err) = module.run_command("git checkout --force %s" % branch)
212 213
        if rc != 0:
            module.fail_json(msg="Failed to checkout branch %s" % branch)
214
        cmd = "git reset --hard %s" % remote
215
    return module.run_command(cmd, check_rc=True)
216

217
# ===========================================
218

219 220 221 222
def main():
    module = AnsibleModule(
        argument_spec = dict(
            dest=dict(required=True),
223
            repo=dict(required=True, aliases=['name']),
224
            version=dict(default='HEAD'),
225 226
            remote=dict(default='origin'),
            force=dict(default='yes', choices=['yes', 'no'], aliases=['force'])
227 228 229
        )
    )

230
    dest    = os.path.abspath(os.path.expanduser(module.params['dest']))
231 232 233
    repo    = module.params['repo']
    version = module.params['version']
    remote  = module.params['remote']
234
    force   = module.boolean(module.params['force'])
235 236 237

    gitconfig = os.path.join(dest, '.git', 'config')

238
    rc, out, err, status = (0, None, None, None)
239 240 241 242

    # if there is no git configuration, do a clone operation
    # else pull and switch the version
    before = None
243
    local_mods = False
244
    if not os.path.exists(gitconfig):
245
        (rc, out, err) = clone(module, repo, dest, remote)
246
    else:
247
        # else do a pull
248
        local_mods = has_local_mods(dest)
249
        before = get_version(dest)
250
        (rc, out, err) = reset(module,dest,force)
251
        if rc != 0:
252
            module.fail_json(msg=err)
Stephen Fromm committed
253
        (rc, out, err) = fetch(module, repo, dest, version, remote)
254 255
        if rc != 0:
            module.fail_json(msg=err)
256 257 258 259 260 261 262 263 264

    # switch to version specified regardless of whether
    # we cloned or pulled
    (rc, out, err) = switch_version(module, dest, remote, version)

    # determine if we changed anything
    after = get_version(dest)
    changed = False

265
    if before != after or local_mods:
266 267 268 269 270 271 272
        changed = True

    module.exit_json(changed=changed, before=before, after=after)

# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()