1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import absolute_import, division, print_function
8__metaclass__ = type
9
10
11DOCUMENTATION = '''
12---
13module: git
14author:
15    - "Ansible Core Team"
16    - "Michael DeHaan"
17version_added: "0.0.1"
18short_description: Deploy software (or files) from git checkouts
19description:
20    - Manage I(git) checkouts of repositories to deploy files or software.
21options:
22    repo:
23        description:
24            - git, SSH, or HTTP(S) protocol address of the git repository.
25        type: str
26        required: true
27        aliases: [ name ]
28    dest:
29        description:
30            - The path of where the repository should be checked out. This
31              is equivalent to C(git clone [repo_url] [directory]). The repository
32              named in I(repo) is not appended to this path and the destination directory must be empty. This
33              parameter is required, unless I(clone) is set to C(no).
34        type: path
35        required: true
36    version:
37        description:
38            - What version of the repository to check out. This can be
39              the literal string C(HEAD), a branch name, a tag name.
40              It can also be a I(SHA-1) hash, in which case I(refspec) needs
41              to be specified if the given revision is not already available.
42        type: str
43        default: "HEAD"
44    accept_hostkey:
45        description:
46            - If C(yes), ensure that "-o StrictHostKeyChecking=no" is
47              present as an ssh option.
48        type: bool
49        default: 'no'
50        version_added: "1.5"
51    ssh_opts:
52        description:
53            - Creates a wrapper script and exports the path as GIT_SSH
54              which git then automatically uses to override ssh arguments.
55              An example value could be "-o StrictHostKeyChecking=no"
56              (although this particular option is better set by
57              I(accept_hostkey)).
58        type: str
59        version_added: "1.5"
60    key_file:
61        description:
62            - Specify an optional private key file path, on the target host, to use for the checkout.
63        type: path
64        version_added: "1.5"
65    reference:
66        description:
67            - Reference repository (see "git clone --reference ...").
68        version_added: "1.4"
69    remote:
70        description:
71            - Name of the remote.
72        type: str
73        default: "origin"
74    refspec:
75        description:
76            - Add an additional refspec to be fetched.
77              If version is set to a I(SHA-1) not reachable from any branch
78              or tag, this option may be necessary to specify the ref containing
79              the I(SHA-1).
80              Uses the same syntax as the C(git fetch) command.
81              An example value could be "refs/meta/config".
82        type: str
83        version_added: "1.9"
84    force:
85        description:
86            - If C(yes), any modified files in the working
87              repository will be discarded.  Prior to 0.7, this was always
88              'yes' and could not be disabled.  Prior to 1.9, the default was
89              `yes`.
90        type: bool
91        default: 'no'
92        version_added: "0.7"
93    depth:
94        description:
95            - Create a shallow clone with a history truncated to the specified
96              number or revisions. The minimum possible value is C(1), otherwise
97              ignored. Needs I(git>=1.9.1) to work correctly.
98        type: int
99        version_added: "1.2"
100    clone:
101        description:
102            - If C(no), do not clone the repository even if it does not exist locally.
103        type: bool
104        default: 'yes'
105        version_added: "1.9"
106    update:
107        description:
108            - If C(no), do not retrieve new revisions from the origin repository.
109            - Operations like archive will work on the existing (old) repository and might
110              not respond to changes to the options version or remote.
111        type: bool
112        default: 'yes'
113        version_added: "1.2"
114    executable:
115        description:
116            - Path to git executable to use. If not supplied,
117              the normal mechanism for resolving binary paths will be used.
118        type: path
119        version_added: "1.4"
120    bare:
121        description:
122            - If C(yes), repository will be created as a bare repo, otherwise
123              it will be a standard repo with a workspace.
124        type: bool
125        default: 'no'
126        version_added: "1.4"
127    umask:
128        description:
129            - The umask to set before doing any checkouts, or any other
130              repository maintenance.
131        type: raw
132        version_added: "2.2"
133
134    recursive:
135        description:
136            - If C(no), repository will be cloned without the --recursive
137              option, skipping sub-modules.
138        type: bool
139        default: 'yes'
140        version_added: "1.6"
141
142    single_branch:
143        description:
144            - Clone only the history leading to the tip of the specified revision.
145        type: bool
146        default: 'no'
147        version_added: '2.11'
148
149    track_submodules:
150        description:
151            - If C(yes), submodules will track the latest commit on their
152              master branch (or other branch specified in .gitmodules).  If
153              C(no), submodules will be kept at the revision specified by the
154              main project. This is equivalent to specifying the --remote flag
155              to git submodule update.
156        type: bool
157        default: 'no'
158        version_added: "1.8"
159
160    verify_commit:
161        description:
162            - If C(yes), when cloning or checking out a I(version) verify the
163              signature of a GPG signed commit. This requires git version>=2.1.0
164              to be installed. The commit MUST be signed and the public key MUST
165              be present in the GPG keyring.
166        type: bool
167        default: 'no'
168        version_added: "2.0"
169
170    archive:
171        description:
172            - Specify archive file path with extension. If specified, creates an
173              archive file of the specified format containing the tree structure
174              for the source tree.
175              Allowed archive formats ["zip", "tar.gz", "tar", "tgz"].
176            - This will clone and perform git archive from local directory as not
177              all git servers support git archive.
178        type: path
179        version_added: "2.4"
180
181    archive_prefix:
182        description:
183            - Specify a prefix to add to each file path in archive. Requires I(archive) to be specified.
184        version_added: "2.10"
185        type: str
186
187    separate_git_dir:
188        description:
189            - The path to place the cloned repository. If specified, Git repository
190              can be separated from working tree.
191        type: path
192        version_added: "2.7"
193
194    gpg_whitelist:
195        description:
196           - A list of trusted GPG fingerprints to compare to the fingerprint of the
197             GPG-signed commit.
198           - Only used when I(verify_commit=yes).
199           - Use of this feature requires Git 2.6+ due to its reliance on git's C(--raw) flag to C(verify-commit) and C(verify-tag).
200        type: list
201        elements: str
202        default: []
203        version_added: "2.9"
204
205requirements:
206    - git>=1.7.1 (the command line tool)
207
208notes:
209    - "If the task seems to be hanging, first verify remote host is in C(known_hosts).
210      SSH will prompt user to authorize the first contact with a remote host.  To avoid this prompt,
211      one solution is to use the option accept_hostkey. Another solution is to
212      add the remote host public key in C(/etc/ssh/ssh_known_hosts) before calling
213      the git module, with the following command: ssh-keyscan -H remote_host.com >> /etc/ssh/ssh_known_hosts."
214    - Supports C(check_mode).
215'''
216
217EXAMPLES = '''
218- name: Git checkout
219  ansible.builtin.git:
220    repo: 'https://foosball.example.org/path/to/repo.git'
221    dest: /srv/checkout
222    version: release-0.22
223
224- name: Read-write git checkout from github
225  ansible.builtin.git:
226    repo: git@github.com:mylogin/hello.git
227    dest: /home/mylogin/hello
228
229- name: Just ensuring the repo checkout exists
230  ansible.builtin.git:
231    repo: 'https://foosball.example.org/path/to/repo.git'
232    dest: /srv/checkout
233    update: no
234
235- name: Just get information about the repository whether or not it has already been cloned locally
236  ansible.builtin.git:
237    repo: 'https://foosball.example.org/path/to/repo.git'
238    dest: /srv/checkout
239    clone: no
240    update: no
241
242- name: Checkout a github repo and use refspec to fetch all pull requests
243  ansible.builtin.git:
244    repo: https://github.com/ansible/ansible-examples.git
245    dest: /src/ansible-examples
246    refspec: '+refs/pull/*:refs/heads/*'
247
248- name: Create git archive from repo
249  ansible.builtin.git:
250    repo: https://github.com/ansible/ansible-examples.git
251    dest: /src/ansible-examples
252    archive: /tmp/ansible-examples.zip
253
254- name: Clone a repo with separate git directory
255  ansible.builtin.git:
256    repo: https://github.com/ansible/ansible-examples.git
257    dest: /src/ansible-examples
258    separate_git_dir: /src/ansible-examples.git
259
260- name: Example clone of a single branch
261  ansible.builtin.git:
262    repo: https://github.com/ansible/ansible-examples.git
263    dest: /src/ansible-examples
264    single_branch: yes
265    version: master
266
267- name: Avoid hanging when http(s) password is missing
268  ansible.builtin.git:
269    repo: https://github.com/ansible/could-be-a-private-repo
270    dest: /src/from-private-repo
271  environment:
272    GIT_TERMINAL_PROMPT: 0 # reports "terminal prompts disabled" on missing password
273    # or GIT_ASKPASS: /bin/true # for git before version 2.3.0, reports "Authentication failed" on missing password
274'''
275
276RETURN = '''
277after:
278    description: Last commit revision of the repository retrieved during the update.
279    returned: success
280    type: str
281    sample: 4c020102a9cd6fe908c9a4a326a38f972f63a903
282before:
283    description: Commit revision before the repository was updated, "null" for new repository.
284    returned: success
285    type: str
286    sample: 67c04ebe40a003bda0efb34eacfb93b0cafdf628
287remote_url_changed:
288    description: Contains True or False whether or not the remote URL was changed.
289    returned: success
290    type: bool
291    sample: True
292warnings:
293    description: List of warnings if requested features were not available due to a too old git version.
294    returned: error
295    type: str
296    sample: git version is too old to fully support the depth argument. Falling back to full checkouts.
297git_dir_now:
298    description: Contains the new path of .git directory if it is changed.
299    returned: success
300    type: str
301    sample: /path/to/new/git/dir
302git_dir_before:
303    description: Contains the original path of .git directory if it is changed.
304    returned: success
305    type: str
306    sample: /path/to/old/git/dir
307'''
308
309import filecmp
310import os
311import re
312import shlex
313import stat
314import sys
315import shutil
316import tempfile
317from distutils.version import LooseVersion
318
319from ansible.module_utils.basic import AnsibleModule
320from ansible.module_utils.six import b, string_types
321from ansible.module_utils._text import to_native, to_text
322
323
324def relocate_repo(module, result, repo_dir, old_repo_dir, worktree_dir):
325    if os.path.exists(repo_dir):
326        module.fail_json(msg='Separate-git-dir path %s already exists.' % repo_dir)
327    if worktree_dir:
328        dot_git_file_path = os.path.join(worktree_dir, '.git')
329        try:
330            shutil.move(old_repo_dir, repo_dir)
331            with open(dot_git_file_path, 'w') as dot_git_file:
332                dot_git_file.write('gitdir: %s' % repo_dir)
333            result['git_dir_before'] = old_repo_dir
334            result['git_dir_now'] = repo_dir
335        except (IOError, OSError) as err:
336            # if we already moved the .git dir, roll it back
337            if os.path.exists(repo_dir):
338                shutil.move(repo_dir, old_repo_dir)
339            module.fail_json(msg=u'Unable to move git dir. %s' % to_text(err))
340
341
342def head_splitter(headfile, remote, module=None, fail_on_error=False):
343    '''Extract the head reference'''
344    # https://github.com/ansible/ansible-modules-core/pull/907
345
346    res = None
347    if os.path.exists(headfile):
348        rawdata = None
349        try:
350            f = open(headfile, 'r')
351            rawdata = f.readline()
352            f.close()
353        except Exception:
354            if fail_on_error and module:
355                module.fail_json(msg="Unable to read %s" % headfile)
356        if rawdata:
357            try:
358                rawdata = rawdata.replace('refs/remotes/%s' % remote, '', 1)
359                refparts = rawdata.split(' ')
360                newref = refparts[-1]
361                nrefparts = newref.split('/', 2)
362                res = nrefparts[-1].rstrip('\n')
363            except Exception:
364                if fail_on_error and module:
365                    module.fail_json(msg="Unable to split head from '%s'" % rawdata)
366    return res
367
368
369def unfrackgitpath(path):
370    if path is None:
371        return None
372
373    # copied from ansible.utils.path
374    return os.path.normpath(os.path.realpath(os.path.expanduser(os.path.expandvars(path))))
375
376
377def get_submodule_update_params(module, git_path, cwd):
378    # or: git submodule [--quiet] update [--init] [-N|--no-fetch]
379    # [-f|--force] [--rebase] [--reference <repository>] [--merge]
380    # [--recursive] [--] [<path>...]
381
382    params = []
383
384    # run a bad submodule command to get valid params
385    cmd = "%s submodule update --help" % (git_path)
386    rc, stdout, stderr = module.run_command(cmd, cwd=cwd)
387    lines = stderr.split('\n')
388    update_line = None
389    for line in lines:
390        if 'git submodule [--quiet] update ' in line:
391            update_line = line
392    if update_line:
393        update_line = update_line.replace('[', '')
394        update_line = update_line.replace(']', '')
395        update_line = update_line.replace('|', ' ')
396        parts = shlex.split(update_line)
397        for part in parts:
398            if part.startswith('--'):
399                part = part.replace('--', '')
400                params.append(part)
401
402    return params
403
404
405def write_ssh_wrapper(module_tmpdir):
406    try:
407        # make sure we have full permission to the module_dir, which
408        # may not be the case if we're sudo'ing to a non-root user
409        if os.access(module_tmpdir, os.W_OK | os.R_OK | os.X_OK):
410            fd, wrapper_path = tempfile.mkstemp(prefix=module_tmpdir + '/')
411        else:
412            raise OSError
413    except (IOError, OSError):
414        fd, wrapper_path = tempfile.mkstemp()
415    fh = os.fdopen(fd, 'w+b')
416    template = b("""#!/bin/sh
417if [ -z "$GIT_SSH_OPTS" ]; then
418    BASEOPTS=""
419else
420    BASEOPTS=$GIT_SSH_OPTS
421fi
422
423# Let ssh fail rather than prompt
424BASEOPTS="$BASEOPTS -o BatchMode=yes"
425
426if [ -z "$GIT_KEY" ]; then
427    ssh $BASEOPTS "$@"
428else
429    ssh -i "$GIT_KEY" -o IdentitiesOnly=yes $BASEOPTS "$@"
430fi
431""")
432    fh.write(template)
433    fh.close()
434    st = os.stat(wrapper_path)
435    os.chmod(wrapper_path, st.st_mode | stat.S_IEXEC)
436    return wrapper_path
437
438
439def set_git_ssh(ssh_wrapper, key_file, ssh_opts):
440
441    if os.environ.get("GIT_SSH"):
442        del os.environ["GIT_SSH"]
443    os.environ["GIT_SSH"] = ssh_wrapper
444
445    if os.environ.get("GIT_KEY"):
446        del os.environ["GIT_KEY"]
447
448    if key_file:
449        os.environ["GIT_KEY"] = key_file
450
451    if os.environ.get("GIT_SSH_OPTS"):
452        del os.environ["GIT_SSH_OPTS"]
453
454    if ssh_opts:
455        os.environ["GIT_SSH_OPTS"] = ssh_opts
456
457
458def get_version(module, git_path, dest, ref="HEAD"):
459    ''' samples the version of the git repo '''
460
461    cmd = "%s rev-parse %s" % (git_path, ref)
462    rc, stdout, stderr = module.run_command(cmd, cwd=dest)
463    sha = to_native(stdout).rstrip('\n')
464    return sha
465
466
467def get_submodule_versions(git_path, module, dest, version='HEAD'):
468    cmd = [git_path, 'submodule', 'foreach', git_path, 'rev-parse', version]
469    (rc, out, err) = module.run_command(cmd, cwd=dest)
470    if rc != 0:
471        module.fail_json(
472            msg='Unable to determine hashes of submodules',
473            stdout=out,
474            stderr=err,
475            rc=rc)
476    submodules = {}
477    subm_name = None
478    for line in out.splitlines():
479        if line.startswith("Entering '"):
480            subm_name = line[10:-1]
481        elif len(line.strip()) == 40:
482            if subm_name is None:
483                module.fail_json()
484            submodules[subm_name] = line.strip()
485            subm_name = None
486        else:
487            module.fail_json(msg='Unable to parse submodule hash line: %s' % line.strip())
488    if subm_name is not None:
489        module.fail_json(msg='Unable to find hash for submodule: %s' % subm_name)
490
491    return submodules
492
493
494def clone(git_path, module, repo, dest, remote, depth, version, bare,
495          reference, refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch):
496    ''' makes a new git repo if it does not already exist '''
497    dest_dirname = os.path.dirname(dest)
498    try:
499        os.makedirs(dest_dirname)
500    except Exception:
501        pass
502    cmd = [git_path, 'clone']
503
504    if bare:
505        cmd.append('--bare')
506    else:
507        cmd.extend(['--origin', remote])
508
509    is_branch_or_tag = is_remote_branch(git_path, module, dest, repo, version) or is_remote_tag(git_path, module, dest, repo, version)
510    if depth:
511        if version == 'HEAD' or refspec:
512            cmd.extend(['--depth', str(depth)])
513        elif is_branch_or_tag:
514            cmd.extend(['--depth', str(depth)])
515            cmd.extend(['--branch', version])
516        else:
517            # only use depth if the remote object is branch or tag (i.e. fetchable)
518            module.warn("Ignoring depth argument. "
519                        "Shallow clones are only available for "
520                        "HEAD, branches, tags or in combination with refspec.")
521    if reference:
522        cmd.extend(['--reference', str(reference)])
523
524    if single_branch:
525        if git_version_used is None:
526            module.fail_json(msg='Cannot find git executable at %s' % git_path)
527
528        if git_version_used < LooseVersion('1.7.10'):
529            module.warn("git version '%s' is too old to use 'single-branch'. Ignoring." % git_version_used)
530        else:
531            cmd.append("--single-branch")
532
533            if is_branch_or_tag:
534                cmd.extend(['--branch', version])
535
536    needs_separate_git_dir_fallback = False
537    if separate_git_dir:
538        if git_version_used is None:
539            module.fail_json(msg='Cannot find git executable at %s' % git_path)
540        if git_version_used < LooseVersion('1.7.5'):
541            # git before 1.7.5 doesn't have separate-git-dir argument, do fallback
542            needs_separate_git_dir_fallback = True
543        else:
544            cmd.append('--separate-git-dir=%s' % separate_git_dir)
545
546    cmd.extend([repo, dest])
547    module.run_command(cmd, check_rc=True, cwd=dest_dirname)
548    if needs_separate_git_dir_fallback:
549        relocate_repo(module, result, separate_git_dir, os.path.join(dest, ".git"), dest)
550
551    if bare and remote != 'origin':
552        module.run_command([git_path, 'remote', 'add', remote, repo], check_rc=True, cwd=dest)
553
554    if refspec:
555        cmd = [git_path, 'fetch']
556        if depth:
557            cmd.extend(['--depth', str(depth)])
558        cmd.extend([remote, refspec])
559        module.run_command(cmd, check_rc=True, cwd=dest)
560
561    if verify_commit:
562        verify_commit_sign(git_path, module, dest, version, gpg_whitelist)
563
564
565def has_local_mods(module, git_path, dest, bare):
566    if bare:
567        return False
568
569    cmd = "%s status --porcelain" % (git_path)
570    rc, stdout, stderr = module.run_command(cmd, cwd=dest)
571    lines = stdout.splitlines()
572    lines = list(filter(lambda c: not re.search('^\\?\\?.*$', c), lines))
573
574    return len(lines) > 0
575
576
577def reset(git_path, module, dest):
578    '''
579    Resets the index and working tree to HEAD.
580    Discards any changes to tracked files in working
581    tree since that commit.
582    '''
583    cmd = "%s reset --hard HEAD" % (git_path,)
584    return module.run_command(cmd, check_rc=True, cwd=dest)
585
586
587def get_diff(module, git_path, dest, repo, remote, depth, bare, before, after):
588    ''' Return the difference between 2 versions '''
589    if before is None:
590        return {'prepared': '>> Newly checked out %s' % after}
591    elif before != after:
592        # Ensure we have the object we are referring to during git diff !
593        git_version_used = git_version(git_path, module)
594        fetch(git_path, module, repo, dest, after, remote, depth, bare, '', git_version_used)
595        cmd = '%s diff %s %s' % (git_path, before, after)
596        (rc, out, err) = module.run_command(cmd, cwd=dest)
597        if rc == 0 and out:
598            return {'prepared': out}
599        elif rc == 0:
600            return {'prepared': '>> No visual differences between %s and %s' % (before, after)}
601        elif err:
602            return {'prepared': '>> Failed to get proper diff between %s and %s:\n>> %s' % (before, after, err)}
603        else:
604            return {'prepared': '>> Failed to get proper diff between %s and %s' % (before, after)}
605    return {}
606
607
608def get_remote_head(git_path, module, dest, version, remote, bare):
609    cloning = False
610    cwd = None
611    tag = False
612    if remote == module.params['repo']:
613        cloning = True
614    elif remote == 'file://' + os.path.expanduser(module.params['repo']):
615        cloning = True
616    else:
617        cwd = dest
618    if version == 'HEAD':
619        if cloning:
620            # cloning the repo, just get the remote's HEAD version
621            cmd = '%s ls-remote %s -h HEAD' % (git_path, remote)
622        else:
623            head_branch = get_head_branch(git_path, module, dest, remote, bare)
624            cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, head_branch)
625    elif is_remote_branch(git_path, module, dest, remote, version):
626        cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version)
627    elif is_remote_tag(git_path, module, dest, remote, version):
628        tag = True
629        cmd = '%s ls-remote %s -t refs/tags/%s*' % (git_path, remote, version)
630    else:
631        # appears to be a sha1.  return as-is since it appears
632        # cannot check for a specific sha1 on remote
633        return version
634    (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=cwd)
635    if len(out) < 1:
636        module.fail_json(msg="Could not determine remote revision for %s" % version, stdout=out, stderr=err, rc=rc)
637
638    out = to_native(out)
639
640    if tag:
641        # Find the dereferenced tag if this is an annotated tag.
642        for tag in out.split('\n'):
643            if tag.endswith(version + '^{}'):
644                out = tag
645                break
646            elif tag.endswith(version):
647                out = tag
648
649    rev = out.split()[0]
650    return rev
651
652
653def is_remote_tag(git_path, module, dest, remote, version):
654    cmd = '%s ls-remote %s -t refs/tags/%s' % (git_path, remote, version)
655    (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest)
656    if to_native(version, errors='surrogate_or_strict') in out:
657        return True
658    else:
659        return False
660
661
662def get_branches(git_path, module, dest):
663    branches = []
664    cmd = '%s branch --no-color -a' % (git_path,)
665    (rc, out, err) = module.run_command(cmd, cwd=dest)
666    if rc != 0:
667        module.fail_json(msg="Could not determine branch data - received %s" % out, stdout=out, stderr=err)
668    for line in out.split('\n'):
669        if line.strip():
670            branches.append(line.strip())
671    return branches
672
673
674def get_annotated_tags(git_path, module, dest):
675    tags = []
676    cmd = [git_path, 'for-each-ref', 'refs/tags/', '--format', '%(objecttype):%(refname:short)']
677    (rc, out, err) = module.run_command(cmd, cwd=dest)
678    if rc != 0:
679        module.fail_json(msg="Could not determine tag data - received %s" % out, stdout=out, stderr=err)
680    for line in to_native(out).split('\n'):
681        if line.strip():
682            tagtype, tagname = line.strip().split(':')
683            if tagtype == 'tag':
684                tags.append(tagname)
685    return tags
686
687
688def is_remote_branch(git_path, module, dest, remote, version):
689    cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version)
690    (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest)
691    if to_native(version, errors='surrogate_or_strict') in out:
692        return True
693    else:
694        return False
695
696
697def is_local_branch(git_path, module, dest, branch):
698    branches = get_branches(git_path, module, dest)
699    lbranch = '%s' % branch
700    if lbranch in branches:
701        return True
702    elif '* %s' % branch in branches:
703        return True
704    else:
705        return False
706
707
708def is_not_a_branch(git_path, module, dest):
709    branches = get_branches(git_path, module, dest)
710    for branch in branches:
711        if branch.startswith('* ') and ('no branch' in branch or 'detached from' in branch or 'detached at' in branch):
712            return True
713    return False
714
715
716def get_repo_path(dest, bare):
717    if bare:
718        repo_path = dest
719    else:
720        repo_path = os.path.join(dest, '.git')
721    # Check if the .git is a file. If it is a file, it means that the repository is in external directory respective to the working copy (e.g. we are in a
722    # submodule structure).
723    if os.path.isfile(repo_path):
724        with open(repo_path, 'r') as gitfile:
725            data = gitfile.read()
726        ref_prefix, gitdir = data.rstrip().split('gitdir: ', 1)
727        if ref_prefix:
728            raise ValueError('.git file has invalid git dir reference format')
729
730        # There is a possibility the .git file to have an absolute path.
731        if os.path.isabs(gitdir):
732            repo_path = gitdir
733        else:
734            repo_path = os.path.join(repo_path.split('.git')[0], gitdir)
735        if not os.path.isdir(repo_path):
736            raise ValueError('%s is not a directory' % repo_path)
737    return repo_path
738
739
740def get_head_branch(git_path, module, dest, remote, bare=False):
741    '''
742    Determine what branch HEAD is associated with.  This is partly
743    taken from lib/ansible/utils/__init__.py.  It finds the correct
744    path to .git/HEAD and reads from that file the branch that HEAD is
745    associated with.  In the case of a detached HEAD, this will look
746    up the branch in .git/refs/remotes/<remote>/HEAD.
747    '''
748    try:
749        repo_path = get_repo_path(dest, bare)
750    except (IOError, ValueError) as err:
751        # No repo path found
752        """``.git`` file does not have a valid format for detached Git dir."""
753        module.fail_json(
754            msg='Current repo does not have a valid reference to a '
755            'separate Git dir or it refers to the invalid path',
756            details=to_text(err),
757        )
758    # Read .git/HEAD for the name of the branch.
759    # If we're in a detached HEAD state, look up the branch associated with
760    # the remote HEAD in .git/refs/remotes/<remote>/HEAD
761    headfile = os.path.join(repo_path, "HEAD")
762    if is_not_a_branch(git_path, module, dest):
763        headfile = os.path.join(repo_path, 'refs', 'remotes', remote, 'HEAD')
764    branch = head_splitter(headfile, remote, module=module, fail_on_error=True)
765    return branch
766
767
768def get_remote_url(git_path, module, dest, remote):
769    '''Return URL of remote source for repo.'''
770    command = [git_path, 'ls-remote', '--get-url', remote]
771    (rc, out, err) = module.run_command(command, cwd=dest)
772    if rc != 0:
773        # There was an issue getting remote URL, most likely
774        # command is not available in this version of Git.
775        return None
776    return to_native(out).rstrip('\n')
777
778
779def set_remote_url(git_path, module, repo, dest, remote):
780    ''' updates repo from remote sources '''
781    # Return if remote URL isn't changing.
782    remote_url = get_remote_url(git_path, module, dest, remote)
783    if remote_url == repo or unfrackgitpath(remote_url) == unfrackgitpath(repo):
784        return False
785
786    command = [git_path, 'remote', 'set-url', remote, repo]
787    (rc, out, err) = module.run_command(command, cwd=dest)
788    if rc != 0:
789        label = "set a new url %s for %s" % (repo, remote)
790        module.fail_json(msg="Failed to %s: %s %s" % (label, out, err))
791
792    # Return False if remote_url is None to maintain previous behavior
793    # for Git versions prior to 1.7.5 that lack required functionality.
794    return remote_url is not None
795
796
797def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, git_version_used, force=False):
798    ''' updates repo from remote sources '''
799    set_remote_url(git_path, module, repo, dest, remote)
800    commands = []
801
802    fetch_str = 'download remote objects and refs'
803    fetch_cmd = [git_path, 'fetch']
804
805    refspecs = []
806    if depth:
807        # try to find the minimal set of refs we need to fetch to get a
808        # successful checkout
809        currenthead = get_head_branch(git_path, module, dest, remote)
810        if refspec:
811            refspecs.append(refspec)
812        elif version == 'HEAD':
813            refspecs.append(currenthead)
814        elif is_remote_branch(git_path, module, dest, repo, version):
815            if currenthead != version:
816                # this workaround is only needed for older git versions
817                # 1.8.3 is broken, 1.9.x works
818                # ensure that remote branch is available as both local and remote ref
819                refspecs.append('+refs/heads/%s:refs/heads/%s' % (version, version))
820            refspecs.append('+refs/heads/%s:refs/remotes/%s/%s' % (version, remote, version))
821        elif is_remote_tag(git_path, module, dest, repo, version):
822            refspecs.append('+refs/tags/' + version + ':refs/tags/' + version)
823        if refspecs:
824            # if refspecs is empty, i.e. version is neither heads nor tags
825            # assume it is a version hash
826            # fall back to a full clone, otherwise we might not be able to checkout
827            # version
828            fetch_cmd.extend(['--depth', str(depth)])
829
830    if not depth or not refspecs:
831        # don't try to be minimalistic but do a full clone
832        # also do this if depth is given, but version is something that can't be fetched directly
833        if bare:
834            refspecs = ['+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*']
835        else:
836            # ensure all tags are fetched
837            if git_version_used >= LooseVersion('1.9'):
838                fetch_cmd.append('--tags')
839            else:
840                # old git versions have a bug in --tags that prevents updating existing tags
841                commands.append((fetch_str, fetch_cmd + [remote]))
842                refspecs = ['+refs/tags/*:refs/tags/*']
843        if refspec:
844            refspecs.append(refspec)
845
846    if force:
847        fetch_cmd.append('--force')
848
849    fetch_cmd.extend([remote])
850
851    commands.append((fetch_str, fetch_cmd + refspecs))
852
853    for (label, command) in commands:
854        (rc, out, err) = module.run_command(command, cwd=dest)
855        if rc != 0:
856            module.fail_json(msg="Failed to %s: %s %s" % (label, out, err), cmd=command)
857
858
859def submodules_fetch(git_path, module, remote, track_submodules, dest):
860    changed = False
861
862    if not os.path.exists(os.path.join(dest, '.gitmodules')):
863        # no submodules
864        return changed
865
866    gitmodules_file = open(os.path.join(dest, '.gitmodules'), 'r')
867    for line in gitmodules_file:
868        # Check for new submodules
869        if not changed and line.strip().startswith('path'):
870            path = line.split('=', 1)[1].strip()
871            # Check that dest/path/.git exists
872            if not os.path.exists(os.path.join(dest, path, '.git')):
873                changed = True
874
875    # Check for updates to existing modules
876    if not changed:
877        # Fetch updates
878        begin = get_submodule_versions(git_path, module, dest)
879        cmd = [git_path, 'submodule', 'foreach', git_path, 'fetch']
880        (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest)
881        if rc != 0:
882            module.fail_json(msg="Failed to fetch submodules: %s" % out + err)
883
884        if track_submodules:
885            # Compare against submodule HEAD
886            # FIXME: determine this from .gitmodules
887            version = 'master'
888            after = get_submodule_versions(git_path, module, dest, '%s/%s' % (remote, version))
889            if begin != after:
890                changed = True
891        else:
892            # Compare against the superproject's expectation
893            cmd = [git_path, 'submodule', 'status']
894            (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest)
895            if rc != 0:
896                module.fail_json(msg='Failed to retrieve submodule status: %s' % out + err)
897            for line in out.splitlines():
898                if line[0] != ' ':
899                    changed = True
900                    break
901    return changed
902
903
904def submodule_update(git_path, module, dest, track_submodules, force=False):
905    ''' init and update any submodules '''
906
907    # get the valid submodule params
908    params = get_submodule_update_params(module, git_path, dest)
909
910    # skip submodule commands if .gitmodules is not present
911    if not os.path.exists(os.path.join(dest, '.gitmodules')):
912        return (0, '', '')
913    cmd = [git_path, 'submodule', 'sync']
914    (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest)
915    if 'remote' in params and track_submodules:
916        cmd = [git_path, 'submodule', 'update', '--init', '--recursive', '--remote']
917    else:
918        cmd = [git_path, 'submodule', 'update', '--init', '--recursive']
919    if force:
920        cmd.append('--force')
921    (rc, out, err) = module.run_command(cmd, cwd=dest)
922    if rc != 0:
923        module.fail_json(msg="Failed to init/update submodules: %s" % out + err)
924    return (rc, out, err)
925
926
927def set_remote_branch(git_path, module, dest, remote, version, depth):
928    """set refs for the remote branch version
929
930    This assumes the branch does not yet exist locally and is therefore also not checked out.
931    Can't use git remote set-branches, as it is not available in git 1.7.1 (centos6)
932    """
933
934    branchref = "+refs/heads/%s:refs/heads/%s" % (version, version)
935    branchref += ' +refs/heads/%s:refs/remotes/%s/%s' % (version, remote, version)
936    cmd = "%s fetch --depth=%s %s %s" % (git_path, depth, remote, branchref)
937    (rc, out, err) = module.run_command(cmd, cwd=dest)
938    if rc != 0:
939        module.fail_json(msg="Failed to fetch branch from remote: %s" % version, stdout=out, stderr=err, rc=rc)
940
941
942def switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_whitelist):
943    cmd = ''
944    if version == 'HEAD':
945        branch = get_head_branch(git_path, module, dest, remote)
946        (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, branch), cwd=dest)
947        if rc != 0:
948            module.fail_json(msg="Failed to checkout branch %s" % branch,
949                             stdout=out, stderr=err, rc=rc)
950        cmd = "%s reset --hard %s/%s --" % (git_path, remote, branch)
951    else:
952        # FIXME check for local_branch first, should have been fetched already
953        if is_remote_branch(git_path, module, dest, remote, version):
954            if depth and not is_local_branch(git_path, module, dest, version):
955                # git clone --depth implies --single-branch, which makes
956                # the checkout fail if the version changes
957                # fetch the remote branch, to be able to check it out next
958                set_remote_branch(git_path, module, dest, remote, version, depth)
959            if not is_local_branch(git_path, module, dest, version):
960                cmd = "%s checkout --track -b %s %s/%s" % (git_path, version, remote, version)
961            else:
962                (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, version), cwd=dest)
963                if rc != 0:
964                    module.fail_json(msg="Failed to checkout branch %s" % version, stdout=out, stderr=err, rc=rc)
965                cmd = "%s reset --hard %s/%s" % (git_path, remote, version)
966        else:
967            cmd = "%s checkout --force %s" % (git_path, version)
968    (rc, out1, err1) = module.run_command(cmd, cwd=dest)
969    if rc != 0:
970        if version != 'HEAD':
971            module.fail_json(msg="Failed to checkout %s" % (version),
972                             stdout=out1, stderr=err1, rc=rc, cmd=cmd)
973        else:
974            module.fail_json(msg="Failed to checkout branch %s" % (branch),
975                             stdout=out1, stderr=err1, rc=rc, cmd=cmd)
976
977    if verify_commit:
978        verify_commit_sign(git_path, module, dest, version, gpg_whitelist)
979
980    return (rc, out1, err1)
981
982
983def verify_commit_sign(git_path, module, dest, version, gpg_whitelist):
984    if version in get_annotated_tags(git_path, module, dest):
985        git_sub = "verify-tag"
986    else:
987        git_sub = "verify-commit"
988    cmd = "%s %s %s" % (git_path, git_sub, version)
989    if gpg_whitelist:
990        cmd += " --raw"
991    (rc, out, err) = module.run_command(cmd, cwd=dest)
992    if rc != 0:
993        module.fail_json(msg='Failed to verify GPG signature of commit/tag "%s"' % version, stdout=out, stderr=err, rc=rc)
994    if gpg_whitelist:
995        fingerprint = get_gpg_fingerprint(err)
996        if fingerprint not in gpg_whitelist:
997            module.fail_json(msg='The gpg_whitelist does not include the public key "%s" for this commit' % fingerprint, stdout=out, stderr=err, rc=rc)
998    return (rc, out, err)
999
1000
1001def get_gpg_fingerprint(output):
1002    """Return a fingerprint of the primary key.
1003
1004    Ref:
1005    https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l482
1006    """
1007    for line in output.splitlines():
1008        data = line.split()
1009        if data[1] != 'VALIDSIG':
1010            continue
1011
1012        # if signed with a subkey, this contains the primary key fingerprint
1013        data_id = 11 if len(data) == 11 else 2
1014        return data[data_id]
1015
1016
1017def git_version(git_path, module):
1018    """return the installed version of git"""
1019    cmd = "%s --version" % git_path
1020    (rc, out, err) = module.run_command(cmd)
1021    if rc != 0:
1022        # one could fail_json here, but the version info is not that important,
1023        # so let's try to fail only on actual git commands
1024        return None
1025    rematch = re.search('git version (.*)$', to_native(out))
1026    if not rematch:
1027        return None
1028    return LooseVersion(rematch.groups()[0])
1029
1030
1031def git_archive(git_path, module, dest, archive, archive_fmt, archive_prefix, version):
1032    """ Create git archive in given source directory """
1033    cmd = [git_path, 'archive', '--format', archive_fmt, '--output', archive, version]
1034    if archive_prefix is not None:
1035        cmd.insert(-1, '--prefix')
1036        cmd.insert(-1, archive_prefix)
1037    (rc, out, err) = module.run_command(cmd, cwd=dest)
1038    if rc != 0:
1039        module.fail_json(msg="Failed to perform archive operation",
1040                         details="Git archive command failed to create "
1041                                 "archive %s using %s directory."
1042                                 "Error: %s" % (archive, dest, err))
1043    return rc, out, err
1044
1045
1046def create_archive(git_path, module, dest, archive, archive_prefix, version, repo, result):
1047    """ Helper function for creating archive using git_archive """
1048    all_archive_fmt = {'.zip': 'zip', '.gz': 'tar.gz', '.tar': 'tar',
1049                       '.tgz': 'tgz'}
1050    _, archive_ext = os.path.splitext(archive)
1051    archive_fmt = all_archive_fmt.get(archive_ext, None)
1052    if archive_fmt is None:
1053        module.fail_json(msg="Unable to get file extension from "
1054                             "archive file name : %s" % archive,
1055                         details="Please specify archive as filename with "
1056                                 "extension. File extension can be one "
1057                                 "of ['tar', 'tar.gz', 'zip', 'tgz']")
1058
1059    repo_name = repo.split("/")[-1].replace(".git", "")
1060
1061    if os.path.exists(archive):
1062        # If git archive file exists, then compare it with new git archive file.
1063        # if match, do nothing
1064        # if does not match, then replace existing with temp archive file.
1065        tempdir = tempfile.mkdtemp()
1066        new_archive_dest = os.path.join(tempdir, repo_name)
1067        new_archive = new_archive_dest + '.' + archive_fmt
1068        git_archive(git_path, module, dest, new_archive, archive_fmt, archive_prefix, version)
1069
1070        # filecmp is supposed to be efficient than md5sum checksum
1071        if filecmp.cmp(new_archive, archive):
1072            result.update(changed=False)
1073            # Cleanup before exiting
1074            try:
1075                shutil.rmtree(tempdir)
1076            except OSError:
1077                pass
1078        else:
1079            try:
1080                shutil.move(new_archive, archive)
1081                shutil.rmtree(tempdir)
1082                result.update(changed=True)
1083            except OSError as e:
1084                module.fail_json(msg="Failed to move %s to %s" %
1085                                     (new_archive, archive),
1086                                 details=u"Error occurred while moving : %s"
1087                                         % to_text(e))
1088    else:
1089        # Perform archive from local directory
1090        git_archive(git_path, module, dest, archive, archive_fmt, archive_prefix, version)
1091        result.update(changed=True)
1092
1093
1094# ===========================================
1095
1096def main():
1097    module = AnsibleModule(
1098        argument_spec=dict(
1099            dest=dict(type='path'),
1100            repo=dict(required=True, aliases=['name']),
1101            version=dict(default='HEAD'),
1102            remote=dict(default='origin'),
1103            refspec=dict(default=None),
1104            reference=dict(default=None),
1105            force=dict(default='no', type='bool'),
1106            depth=dict(default=None, type='int'),
1107            clone=dict(default='yes', type='bool'),
1108            update=dict(default='yes', type='bool'),
1109            verify_commit=dict(default='no', type='bool'),
1110            gpg_whitelist=dict(default=[], type='list', elements='str'),
1111            accept_hostkey=dict(default='no', type='bool'),
1112            key_file=dict(default=None, type='path', required=False),
1113            ssh_opts=dict(default=None, required=False),
1114            executable=dict(default=None, type='path'),
1115            bare=dict(default='no', type='bool'),
1116            recursive=dict(default='yes', type='bool'),
1117            single_branch=dict(default=False, type='bool'),
1118            track_submodules=dict(default='no', type='bool'),
1119            umask=dict(default=None, type='raw'),
1120            archive=dict(type='path'),
1121            archive_prefix=dict(),
1122            separate_git_dir=dict(type='path'),
1123        ),
1124        mutually_exclusive=[('separate_git_dir', 'bare')],
1125        required_by={'archive_prefix': ['archive']},
1126        supports_check_mode=True
1127    )
1128
1129    dest = module.params['dest']
1130    repo = module.params['repo']
1131    version = module.params['version']
1132    remote = module.params['remote']
1133    refspec = module.params['refspec']
1134    force = module.params['force']
1135    depth = module.params['depth']
1136    update = module.params['update']
1137    allow_clone = module.params['clone']
1138    bare = module.params['bare']
1139    verify_commit = module.params['verify_commit']
1140    gpg_whitelist = module.params['gpg_whitelist']
1141    reference = module.params['reference']
1142    single_branch = module.params['single_branch']
1143    git_path = module.params['executable'] or module.get_bin_path('git', True)
1144    key_file = module.params['key_file']
1145    ssh_opts = module.params['ssh_opts']
1146    umask = module.params['umask']
1147    archive = module.params['archive']
1148    archive_prefix = module.params['archive_prefix']
1149    separate_git_dir = module.params['separate_git_dir']
1150
1151    result = dict(changed=False, warnings=list())
1152
1153    if module.params['accept_hostkey']:
1154        if ssh_opts is not None:
1155            if "-o StrictHostKeyChecking=no" not in ssh_opts:
1156                ssh_opts += " -o StrictHostKeyChecking=no"
1157        else:
1158            ssh_opts = "-o StrictHostKeyChecking=no"
1159
1160    # evaluate and set the umask before doing anything else
1161    if umask is not None:
1162        if not isinstance(umask, string_types):
1163            module.fail_json(msg="umask must be defined as a quoted octal integer")
1164        try:
1165            umask = int(umask, 8)
1166        except Exception:
1167            module.fail_json(msg="umask must be an octal integer",
1168                             details=str(sys.exc_info()[1]))
1169        os.umask(umask)
1170
1171    # Certain features such as depth require a file:/// protocol for path based urls
1172    # so force a protocol here ...
1173    if os.path.expanduser(repo).startswith('/'):
1174        repo = 'file://' + os.path.expanduser(repo)
1175
1176    # We screenscrape a huge amount of git commands so use C locale anytime we
1177    # call run_command()
1178    module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
1179
1180    if separate_git_dir:
1181        separate_git_dir = os.path.realpath(separate_git_dir)
1182
1183    gitconfig = None
1184    if not dest and allow_clone:
1185        module.fail_json(msg="the destination directory must be specified unless clone=no")
1186    elif dest:
1187        dest = os.path.abspath(dest)
1188        try:
1189            repo_path = get_repo_path(dest, bare)
1190            if separate_git_dir and os.path.exists(repo_path) and separate_git_dir != repo_path:
1191                result['changed'] = True
1192                if not module.check_mode:
1193                    relocate_repo(module, result, separate_git_dir, repo_path, dest)
1194                    repo_path = separate_git_dir
1195        except (IOError, ValueError) as err:
1196            # No repo path found
1197            """``.git`` file does not have a valid format for detached Git dir."""
1198            module.fail_json(
1199                msg='Current repo does not have a valid reference to a '
1200                'separate Git dir or it refers to the invalid path',
1201                details=to_text(err),
1202            )
1203        gitconfig = os.path.join(repo_path, 'config')
1204
1205    # create a wrapper script and export
1206    # GIT_SSH=<path> as an environment variable
1207    # for git to use the wrapper script
1208    ssh_wrapper = write_ssh_wrapper(module.tmpdir)
1209    set_git_ssh(ssh_wrapper, key_file, ssh_opts)
1210    module.add_cleanup_file(path=ssh_wrapper)
1211
1212    git_version_used = git_version(git_path, module)
1213
1214    if depth is not None and git_version_used < LooseVersion('1.9.1'):
1215        module.warn("git version is too old to fully support the depth argument. Falling back to full checkouts.")
1216        depth = None
1217
1218    recursive = module.params['recursive']
1219    track_submodules = module.params['track_submodules']
1220
1221    result.update(before=None)
1222
1223    local_mods = False
1224    if (dest and not os.path.exists(gitconfig)) or (not dest and not allow_clone):
1225        # if there is no git configuration, do a clone operation unless:
1226        # * the user requested no clone (they just want info)
1227        # * we're doing a check mode test
1228        # In those cases we do an ls-remote
1229        if module.check_mode or not allow_clone:
1230            remote_head = get_remote_head(git_path, module, dest, version, repo, bare)
1231            result.update(changed=True, after=remote_head)
1232            if module._diff:
1233                diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after'])
1234                if diff:
1235                    result['diff'] = diff
1236            module.exit_json(**result)
1237        # there's no git config, so clone
1238        clone(git_path, module, repo, dest, remote, depth, version, bare, reference,
1239              refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch)
1240    elif not update:
1241        # Just return having found a repo already in the dest path
1242        # this does no checking that the repo is the actual repo
1243        # requested.
1244        result['before'] = get_version(module, git_path, dest)
1245        result.update(after=result['before'])
1246        if archive:
1247            # Git archive is not supported by all git servers, so
1248            # we will first clone and perform git archive from local directory
1249            if module.check_mode:
1250                result.update(changed=True)
1251                module.exit_json(**result)
1252
1253            create_archive(git_path, module, dest, archive, archive_prefix, version, repo, result)
1254
1255        module.exit_json(**result)
1256    else:
1257        # else do a pull
1258        local_mods = has_local_mods(module, git_path, dest, bare)
1259        result['before'] = get_version(module, git_path, dest)
1260        if local_mods:
1261            # failure should happen regardless of check mode
1262            if not force:
1263                module.fail_json(msg="Local modifications exist in repository (force=no).", **result)
1264            # if force and in non-check mode, do a reset
1265            if not module.check_mode:
1266                reset(git_path, module, dest)
1267                result.update(changed=True, msg='Local modifications exist.')
1268
1269        # exit if already at desired sha version
1270        if module.check_mode:
1271            remote_url = get_remote_url(git_path, module, dest, remote)
1272            remote_url_changed = remote_url and remote_url != repo and unfrackgitpath(remote_url) != unfrackgitpath(repo)
1273        else:
1274            remote_url_changed = set_remote_url(git_path, module, repo, dest, remote)
1275        result.update(remote_url_changed=remote_url_changed)
1276
1277        if module.check_mode:
1278            remote_head = get_remote_head(git_path, module, dest, version, remote, bare)
1279            result.update(changed=(result['before'] != remote_head or remote_url_changed), after=remote_head)
1280            # FIXME: This diff should fail since the new remote_head is not fetched yet?!
1281            if module._diff:
1282                diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after'])
1283                if diff:
1284                    result['diff'] = diff
1285            module.exit_json(**result)
1286        else:
1287            fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, git_version_used, force=force)
1288
1289        result['after'] = get_version(module, git_path, dest)
1290
1291    # switch to version specified regardless of whether
1292    # we got new revisions from the repository
1293    if not bare:
1294        switch_version(git_path, module, dest, remote, version, verify_commit, depth, gpg_whitelist)
1295
1296    # Deal with submodules
1297    submodules_updated = False
1298    if recursive and not bare:
1299        submodules_updated = submodules_fetch(git_path, module, remote, track_submodules, dest)
1300        if submodules_updated:
1301            result.update(submodules_changed=submodules_updated)
1302
1303            if module.check_mode:
1304                result.update(changed=True, after=remote_head)
1305                module.exit_json(**result)
1306
1307            # Switch to version specified
1308            submodule_update(git_path, module, dest, track_submodules, force=force)
1309
1310    # determine if we changed anything
1311    result['after'] = get_version(module, git_path, dest)
1312
1313    if result['before'] != result['after'] or local_mods or submodules_updated or remote_url_changed:
1314        result.update(changed=True)
1315        if module._diff:
1316            diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after'])
1317            if diff:
1318                result['diff'] = diff
1319
1320    if archive:
1321        # Git archive is not supported by all git servers, so
1322        # we will first clone and perform git archive from local directory
1323        if module.check_mode:
1324            result.update(changed=True)
1325            module.exit_json(**result)
1326
1327        create_archive(git_path, module, dest, archive, archive_prefix, version, repo, result)
1328
1329    module.exit_json(**result)
1330
1331
1332if __name__ == '__main__':
1333    main()
1334