Repository: ansible/ansible

Issue:
Git module fails to update if separated work tree
##### ISSUE TYPE

Bug Report

##### COMPONENT NAME

git module

##### ANSIBLE VERSION

1.9.2

##### SUMMARY

From @giacmir on 2015-10-08T13:11:51Z
I have a deploy scenario where I have worktree and git-dir separated. if I try to update using the git module it fails with this error

```
FAILED >> {
    "cmd": "/usr/bin/git clone --origin origin --branch master '' /path/to/worktree", 
    "failed": true, 
    "msg": "fatal: destination path '/path/to/worktree' already exists and is not an empty directory.", 
    "rc": 128, 
    "stderr": "fatal: destination path '//path/to/worktree' already exists and is not an empty directory.\n", 
    "stdout": ""
}
```

Same happens if I put the git dir as dest.
Ansible is version 1.9.2 and git 1.7.1

Copied from original issue: ansible/ansible-modules-core#2226





Discussion:
From @robynbergeron on 2015-10-08T13:11:51Z

[module: source_control/git.py]

From @robynbergeron on 2015-10-08T13:11:51Z

Hi @giacmir -- thanks for submitting this bug report. 

@ansible/core team -- pinging you because you are the module owners. Thanks in advance for taking a look at this!

cc @robinro
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Confirm this. git module should support git deploy with separated git tree. It's --separate-git-dir option for git-clone.
@iprok This issue has not seen any activity since 2015. Implementing this might be fairly straight forward, by just passing `--seperate-git-dir` to all git commands. Would you be interesting in implementing this and opening a PR? I could review it and help along.
@robinro, I work together with @iprok and we decided to work on a solution for this issue. I'll have a look at it first and I'll ask you questions if I have any. Thanks!
@robinro 

Here is what I came up with for detecting separate git dir setups:
```diff
diff --git a/lib/ansible/modules/source_control/git.py b/lib/ansible/modules/source_control/git.py
index d703451..9f85236 100644
--- a/lib/ansible/modules/source_control/git.py
+++ b/lib/ansible/modules/source_control/git.py
@@ -1012,6 +1012,7 @@ def main():
     # call run_command()
     module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
 
+    separate_git_dir = None
     gitconfig = None
     if not dest and allow_clone:
         module.fail_json(msg="the destination directory must be specified unless clone=no")
@@ -1019,6 +1020,13 @@ def main():
         dest = os.path.abspath(dest)
         if bare:
             gitconfig = os.path.join(dest, 'config')
+        elif os.path.isfile(os.path.join(dest, '.git')):
+            with open(os.path.join(dest, '.git'), 'r') as gitfile:
+                data = gitfile.read()
+                match = re.match('gitdir: (.+)$', data)
+                if match:
+                    separate_git_dir = match.group(1)
+                    gitconfig = os.path.join(separate_git_dir, 'config')
         else:
             gitconfig = os.path.join(dest, '.git', 'config')
```

Now question: is there a way to avoid modifying every git-interface function arguments list and calls in the `/lib/ansible/modules/source_control/git.py` file in order to add `"--separate-git-dir=%s" % separate_git_dir` to git command calls?
Oh, probably I can try setting $GIT_DIR environment variable

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
@@ -595,6 +595,30 @@ def is_not_a_branch(git_path, module, dest):
     return False
 
 
+def get_repo_path(dest, bare):
+    if bare:
+        repo_path = dest
+    else:
+        repo_path = os.path.join(dest, '.git')
+    # 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
+    # submodule structure).
+    if os.path.isfile(repo_path):
+        with open(repo_path, 'r') as gitfile:
+            data = gitfile.read()
+        ref_prefix, gitdir = data.rstrip().split('gitdir: ', 1)
+        if ref_prefix:
+            raise ValueError('.git file has invalid git dir reference format')
+
+        # There is a possibility 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)
+        if not os.path.isdir(repo_path):
+            raise ValueError('%s is not a directory' % repo_path)
+    return repo_path
+
+
 def get_head_branch(git_path, module, dest, remote, bare=False):
     '''
     Determine what branch HEAD is associated with.  This is partly
@@ -603,31 +627,16 @@ def get_head_branch(git_path, module, dest, remote, bare=False):
     associated with.  In the case of a detached HEAD, this will look
     up the branch in .git/refs/remotes/<remote>/HEAD.
     '''
-    if bare:
-        repo_path = dest
-    else:
-        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:
-            git_conf = open(repo_path, 'rb')
-            for line in git_conf:
-                config_val = line.split(b(':'), 1)
-                if config_val[0].strip() == b('gitdir'):
-                    gitdir = to_native(config_val[1].strip(), errors='surrogate_or_strict')
-                    break
-            else:
-                # No repo path found
-                return ''
-
-            # There is a possibility 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):
-            # No repo path found
-            return ''
+    try:
+        repo_path = get_repo_path(dest, bare)
+    except (IOError, ValueError) as err:
+        # No repo path found
+        """``.git`` file does not have a valid format for detached Git dir."""
+        module.fail_json(
+            msg='Current repo does not have a valid reference to a '
+            'separate Git dir or it refers to the invalid path',
+            details=str(err),
+        )
     # 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
@@ -1017,10 +1026,17 @@ def main():
         module.fail_json(msg="the destination directory must be specified unless clone=no")
     elif dest:
         dest = os.path.abspath(dest)
-        if bare:
-            gitconfig = os.path.join(dest, 'config')
-        else:
-            gitconfig = os.path.join(dest, '.git', 'config')
+        try:
+            repo_path = get_repo_path(dest, bare)
+        except (IOError, ValueError) as err:
+            # No repo path found
+            """``.git`` file does not have a valid format for detached Git dir."""
+            module.fail_json(
+                msg='Current repo does not have a valid reference to a '
+                'separate Git dir or it refers to the invalid path',
+                details=str(err),
+            )
+        gitconfig = os.path.join(repo_path, 'config')
 
     # create a wrapper script and export
     # GIT_SSH=<path> as an environment variable


Test patch:
diff --git a/test/integration/targets/git/tasks/main.yml b/test/integration/targets/git/tasks/main.yml
--- a/test/integration/targets/git/tasks/main.yml
+++ b/test/integration/targets/git/tasks/main.yml
@@ -36,3 +36,6 @@
 - include_tasks: reset-origin.yml
 - include_tasks: ambiguous-ref.yml
 - include_tasks: archive.yml
+- include_tasks: separate-git-dir.yml
+  when:
+    - git_version.stdout is version("1.7.5", '>=')
diff --git a/test/integration/targets/git/tasks/separate-git-dir.yml b/test/integration/targets/git/tasks/separate-git-dir.yml
new file mode 100644
--- /dev/null
+++ b/test/integration/targets/git/tasks/separate-git-dir.yml
@@ -0,0 +1,64 @@
+# test code for repositories with separate git dir updating
+# see https://github.com/ansible/ansible/pull/38016
+# see https://github.com/ansible/ansible/issues/30034
+
+- name: SEPARATE-GIT-DIR | clear checkout_dir
+  file:
+    state: absent
+    path: '{{ checkout_dir }}'
+
+- name: create a tempdir for separate git dir
+  local_action: shell mktemp -du
+  register: tempdir
+
+- name: SEPARATE-GIT-DIR | clone with a separate git dir
+  command: git clone {{ repo_format1 }} {{ checkout_dir }} --separate-git-dir={{ tempdir.stdout }}
+
+- name: SEPARATE-GIT-DIR | update repo the usual way
+  git:
+    repo: "{{ repo_format1 }}"
+    dest: "{{ checkout_dir }}"
+
+- name: SEPARATE-GIT-DIR | set git dir to non-existent dir
+  shell: "echo gitdir: /dev/null/non-existent-dir > .git"
+  args:
+    chdir: "{{ checkout_dir }}"
+
+- name: SEPARATE-GIT-DIR | update repo the usual way
+  git:
+    repo: "{{ repo_format1 }}"
+    dest: "{{ checkout_dir }}"
+  ignore_errors: yes
+  register: result
+
+- name: SEPARATE-GIT-DIR | check update has failed
+  assert:
+    that:
+      - result is failed
+
+- name: SEPARATE-GIT-DIR | set .git file to bad format
+  shell: "echo some text  gitdir: {{ checkout_dir }} > .git"
+  args:
+    chdir: "{{ checkout_dir }}"
+
+- name: SEPARATE-GIT-DIR | update repo the usual way
+  git:
+    repo: "{{ repo_format1 }}"
+    dest: "{{ checkout_dir }}"
+  ignore_errors: yes
+  register: result
+
+- name: SEPARATE-GIT-DIR | check update has failed
+  assert:
+    that:
+      - result is failed
+
+- name: SEPARATE-GIT-DIR | clear separate git dir
+  file:
+    state: absent
+    path: "{{ tempdir.stdout }}"
+
+- name: SEPARATE-GIT-DIR | clear checkout_dir
+  file:
+    state: absent
+    path: '{{ checkout_dir }}'