Repository: ansible/ansible

Issue:
Archive option for git module 
##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
git

##### ANSIBLE VERSION
```
ansible 2.2.1.0
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
It would be nice to also have, next to the `bare` option, a `archive` option. When using this option, Ansible will run the [git-archive](https://git-scm.com/docs/git-archive) command and produce a archive (tar or zip) file.

##### STEPS TO REPRODUCE
```yaml
# Example git archive from Ansible Playbooks
- git:
    repo: git://foosball.example.org/path/to/repo.git
    dest: /srv/repo-release-0.22.zip
    version: release-0.22
    archive: yes
    update: no

# Archive with latest commits
- git:
    repo: git://foosball.example.org/path/to/repo.git
    dest: /srv/repo-nightly.tar.gz
    archive: yes
```
##### EXPECTED RESULTS
Ansible creates the files `/srv/repo-release-0.22.zip` and `/srv/repo-nightly.tar.gz` by running `git archive`.


Discussion:
@s-hertel I would like to work on this issue. Can you please guide me ?
The repo could also be a local bare repo, so pulling from the remote is not needed every time:
```yaml
# Archive with latest commits from local bare repo
- git:
    repo: /path/to/bare/repo.git
    dest: /srv/repo-nightly.tar.gz
    archive: yes
```
@Akasurde Cool, that would be very helpful. I haven't worked a whole lot with the git module but I'm happy to test it or try to help. When you make a pr for it feel welcome to ping me on it. I'd suggest first adding the `archive` option to the [documentation](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/source_control/git.py#L36) and to the [module parameters](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/source_control/git.py#L882), and then work on adding a function for the git-archive command (which you'll want to run with AnsibleModule's [run_command()](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/basic.py#L2263) like git.py is already employing in [various](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/source_control/git.py#L314) [places](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/source_control/git.py#L444)). Feel free to ask specific questions and I'll do my best to answer or find the answer.

Resolving patch:
diff --git a/lib/ansible/modules/source_control/git.py b/lib/ansible/modules/source_control/git.py
--- a/lib/ansible/modules/source_control/git.py
+++ b/lib/ansible/modules/source_control/git.py
@@ -186,6 +186,15 @@
               to be installed. The commit MUST be signed and the public key MUST
               be trusted in the GPG trustdb.
 
+    archive:
+        required: false
+        version_added: "2.4"
+        description:
+            - Specify archive file path with extension. If specified, creates an
+              archive file of the specified format containing the tree structure
+              for the source tree.
+              Allowed archive formats ["zip", "tar.gz", "tar", "tgz"]
+
 requirements:
     - git>=1.7.1 (the command line tool)
 
@@ -228,6 +237,13 @@
     repo: https://github.com/ansible/ansible-examples.git
     dest: /src/ansible-examples
     refspec: '+refs/pull/*:refs/heads/*'
+
+# Example Create git archive from repo
+- git:
+    repo: https://github.com/ansible/ansible-examples.git
+    dest: /src/ansible-examples
+    archive: /tmp/ansible-examples.zip
+
 '''
 
 RETURN = '''
@@ -253,15 +269,18 @@
     sample: Your git version is too old to fully support the depth argument. Falling back to full checkouts.
 '''
 
+import filecmp
 import os
 import re
 import shlex
 import stat
 import sys
+import shutil
 import tempfile
 from distutils.version import LooseVersion
 
 from ansible.module_utils.basic import AnsibleModule, get_module_path
+from ansible.module_utils.basic import get_exception
 from ansible.module_utils.known_hosts import add_git_host_key
 from ansible.module_utils.six import b, string_types
 from ansible.module_utils._text import to_native
@@ -901,6 +920,68 @@ def git_version(git_path, module):
     return LooseVersion(rematch.groups()[0])
 
 
+def git_archive(git_path, module, dest, archive, archive_fmt, version):
+    """ Create git archive in given source directory """
+    cmd = "%s archive --format=%s --output=%s %s" \
+          % (git_path, archive_fmt, archive, version)
+    (rc, out, err) = module.run_command(cmd, cwd=dest)
+    if rc != 0:
+        module.fail_json(msg="Failed to perform archive operation",
+                         details="Git archive command failed to create "
+                                 "archive %s using %s directory."
+                                 "Error: %s" % (archive, dest, err))
+    return rc, out, err
+
+
+def create_archive(git_path, module, dest, archive, version, repo, result):
+    """ Helper function for creating archive using git_archive """
+    all_archive_fmt = {'.zip': 'zip', '.gz': 'tar.gz', '.tar': 'tar',
+                       '.tgz': 'tgz'}
+    _, archive_ext = os.path.splitext(archive)
+    archive_fmt = all_archive_fmt.get(archive_ext, None)
+    if archive_fmt is None:
+        module.fail_json(msg="Unable to get file extension from "
+                             "archive file name : %s" % archive,
+                         details="Please specify archive as filename with "
+                                 "extension. File extension can be one "
+                                 "of ['tar', 'tar.gz', 'zip', 'tgz']")
+
+    repo_name = repo.split("/")[-1].replace(".git", "")
+
+    if os.path.exists(archive):
+        # If git archive file exists, then compare it with new git archive file.
+        # if match, do nothing
+        # if does not match, then replace existing with temp archive file.
+        tempdir = tempfile.mkdtemp()
+        new_archive_dest = os.path.join(tempdir, repo_name)
+        new_archive = new_archive_dest + '.' + archive_fmt
+        git_archive(git_path, module, dest, new_archive, archive_fmt, version)
+
+        # filecmp is supposed to be efficient than md5sum checksum
+        if filecmp.cmp(new_archive, archive):
+            result.update(changed=False)
+            # Cleanup before exiting
+            try:
+                shutil.remove(tempdir)
+            except OSError:
+                pass
+        else:
+            try:
+                shutil.move(new_archive, archive)
+                shutil.remove(tempdir)
+                result.update(changed=True)
+            except OSError:
+                exception = get_exception()
+                module.fail_json(msg="Failed to move %s to %s" %
+                                     (new_archive, archive),
+                                 details="Error occured while moving : %s"
+                                         % exception)
+    else:
+        # Perform archive from local directory
+        git_archive(git_path, module, dest, archive, archive_fmt, version)
+        result.update(changed=True)
+
+
 # ===========================================
 
 def main():
@@ -925,6 +1006,7 @@ def main():
             recursive=dict(default='yes', type='bool'),
             track_submodules=dict(default='no', type='bool'),
             umask=dict(default=None, type='raw'),
+            archive=dict(type='path'),
         ),
         supports_check_mode=True
     )
@@ -945,6 +1027,7 @@ def main():
     key_file = module.params['key_file']
     ssh_opts = module.params['ssh_opts']
     umask = module.params['umask']
+    archive = module.params['archive']
 
     result = dict(changed=False, warnings=list())
 
@@ -1003,6 +1086,7 @@ def main():
     track_submodules = module.params['track_submodules']
 
     result.update(before=None)
+
     local_mods = False
     need_fetch = True
     if (dest and not os.path.exists(gitconfig)) or (not dest and not allow_clone):
@@ -1092,6 +1176,15 @@ def main():
             if diff:
                 result['diff'] = diff
 
+    if archive:
+        # Git archive is not supported by all git servers, so
+        # we will first clone and perform git archive from local directory
+        if module.check_mode:
+            result.update(changed=True)
+            module.exit_json(**result)
+
+        create_archive(git_path, module, dest, archive, version, repo, result)
+
     # cleanup the wrapper script
     if ssh_wrapper:
         try:


Test patch: