diff --git a/library/source_control/git b/library/source_control/git index 213cccb0c7a..39e7ac60cbb 100644 --- a/library/source_control/git +++ b/library/source_control/git @@ -43,6 +43,12 @@ options: - What version of the repository to check out. This can be the full 40-character I(SHA-1) hash, the literal string C(HEAD), a branch name, or a tag name. + reference: + required: false + default: null + version_added: "1.4" + description: + - Reference repository (see "git clone --reference ...") remote: required: false default: "origin" @@ -81,6 +87,14 @@ options: description: - Path to git executable to use. If not supplied, the normal mechanism for resolving binary paths will be used. + bare: + required: false + default: "no" + choices: [ "yes", "no" ] + version_added: "1.4" + description: + - if C(yes), repository will be created as a bare repo, otherwise + it will be a standard repo with a workspace. notes: - "If the task seems to be hanging, first verify remote host is in C(known_hosts). SSH will prompt user to authorize the first contact with a remote host. To avoid this prompt, @@ -104,15 +118,14 @@ EXAMPLES = ''' import re import tempfile -def get_version(git_path, dest): +def get_version(git_path, dest, ref="HEAD"): ''' samples the version of the git repo ''' os.chdir(dest) - cmd = "%s show" % (git_path,) - sha = os.popen(cmd).read().split("\n") - sha = sha[0].split()[1] + cmd = "%s rev-parse %s" % (git_path, ref) + sha = os.popen(cmd).read().rstrip("\n") return sha -def clone(git_path, module, repo, dest, remote, depth, version): +def clone(git_path, module, repo, dest, remote, depth, version, bare, reference): ''' makes a new git repo if it does not already exist ''' dest_dirname = os.path.dirname(dest) try: @@ -120,16 +133,28 @@ def clone(git_path, module, repo, dest, remote, depth, version): except: pass os.chdir(dest_dirname) - cmd = [ git_path, 'clone', '-o', remote, '--recursive' ] - if is_remote_branch(git_path, module, dest, repo, version) \ - or is_remote_tag(git_path, module, dest, repo, version): - cmd.extend([ '--branch', version ]) + cmd = [ git_path, 'clone' ] + if bare: + cmd.append('--bare') + else: + cmd.extend([ '--origin', remote, '--recursive' ]) + if is_remote_branch(git_path, module, dest, repo, version) \ + or is_remote_tag(git_path, module, dest, repo, version): + cmd.extend([ '--branch', version ]) if depth: cmd.extend([ '--depth', str(depth) ]) + if reference: + cmd.extend([ '--reference', str(reference) ]) cmd.extend([ repo, dest ]) - return module.run_command(cmd, check_rc=True) - -def has_local_mods(git_path, dest): + module.run_command(cmd, check_rc=True) + if bare: + os.chdir(dest) + if remote != 'origin': + module.run_command([git_path, 'remote', 'add', remote, repo], check_rc=True) + +def has_local_mods(git_path, dest, bare): + if bare: + return False os.chdir(dest) cmd = "%s status -s" % (git_path,) lines = os.popen(cmd).read().splitlines() @@ -146,7 +171,7 @@ def reset(git_path, module, dest): cmd = "%s reset --hard HEAD" % (git_path,) return module.run_command(cmd, check_rc=True) -def get_remote_head(git_path, module, dest, version, remote): +def get_remote_head(git_path, module, dest, version, remote, bare): cloning = False if remote == module.params['repo']: cloning = True @@ -157,7 +182,7 @@ def get_remote_head(git_path, module, dest, version, remote): # cloning the repo, just get the remote's HEAD version cmd = '%s ls-remote %s -h HEAD' % (git_path, remote) else: - head_branch = get_head_branch(git_path, module, dest, remote) + head_branch = get_head_branch(git_path, module, dest, remote, bare) cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, head_branch) elif is_remote_branch(git_path, module, dest, remote, version): cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) @@ -217,7 +242,7 @@ def is_not_a_branch(git_path, module, dest): return True return False -def get_head_branch(git_path, module, dest, remote): +def get_head_branch(git_path, module, dest, remote, bare=False): ''' Determine what branch HEAD is associated with. This is partly taken from lib/ansible/utils/__init__.py. It finds the correct @@ -225,7 +250,10 @@ def get_head_branch(git_path, module, dest, remote): associated with. In the case of a detached HEAD, this will look up the branch in .git/refs/remotes//HEAD. ''' - repo_path = os.path.join(dest, '.git') + 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: @@ -248,14 +276,20 @@ def get_head_branch(git_path, module, dest, remote): f.close() return branch -def fetch(git_path, module, repo, dest, version, remote): +def fetch(git_path, module, repo, dest, version, remote, bare): ''' updates repo from remote sources ''' os.chdir(dest) - (rc, out1, err1) = module.run_command("%s fetch %s" % (git_path, remote)) + if bare: + (rc, out1, err1) = module.run_command([git_path, 'fetch', remote, '+refs/heads/*:refs/heads/*']) + else: + (rc, out1, err1) = module.run_command("%s fetch %s" % (git_path, remote)) if rc != 0: module.fail_json(msg="Failed to download remote objects and refs") - (rc, out2, err2) = module.run_command("%s fetch --tags %s" % (git_path, remote)) + if bare: + (rc, out2, err2) = module.run_command([git_path, 'fetch', remote, '+refs/tags/*:refs/tags/*']) + else: + (rc, out2, err2) = module.run_command("%s fetch --tags %s" % (git_path, remote)) if rc != 0: module.fail_json(msg="Failed to download remote objects and refs") (rc, out3, err3) = submodule_update(git_path, module, dest) @@ -314,24 +348,31 @@ def main(): repo=dict(required=True, aliases=['name']), version=dict(default='HEAD'), remote=dict(default='origin'), + reference=dict(default=None), force=dict(default='yes', type='bool'), depth=dict(default=None, type='int'), update=dict(default='yes', type='bool'), executable=dict(default=None), + bare=dict(default='no', type='bool'), ), supports_check_mode=True ) - dest = os.path.abspath(os.path.expanduser(module.params['dest'])) - repo = module.params['repo'] - version = module.params['version'] - remote = module.params['remote'] - force = module.params['force'] - depth = module.params['depth'] - update = module.params['update'] - git_path = module.params['executable'] or module.get_bin_path('git', True) - - gitconfig = os.path.join(dest, '.git', 'config') + dest = os.path.abspath(os.path.expanduser(module.params['dest'])) + repo = module.params['repo'] + version = module.params['version'] + remote = module.params['remote'] + force = module.params['force'] + depth = module.params['depth'] + update = module.params['update'] + bare = module.params['bare'] + reference = module.params['reference'] + git_path = module.params['executable'] or module.get_bin_path('git', True) + + if bare: + gitconfig = os.path.join(dest, 'config') + else: + gitconfig = os.path.join(dest, '.git', 'config') rc, out, err, status = (0, None, None, None) @@ -343,7 +384,7 @@ def main(): if module.check_mode: remote_head = get_remote_head(git_path, module, dest, version, repo) module.exit_json(changed=True, before=before, after=remote_head) - clone(git_path, module, repo, dest, remote, depth, version) + clone(git_path, module, repo, dest, remote, depth, version, bare, reference) elif not update: # Just return having found a repo already in the dest path # this does no checking that the repo is the actual repo @@ -352,9 +393,8 @@ def main(): module.exit_json(changed=False, before=before, after=before) else: # else do a pull - local_mods = has_local_mods(git_path, dest) + local_mods = has_local_mods(git_path, dest, bare) before = get_version(git_path, dest) - remote_head = get_remote_head(git_path, module, dest, version, remote) if local_mods: # failure should happen regardless of check mode if not force: @@ -363,6 +403,7 @@ def main(): if not module.check_mode: reset(git_path, module, dest) # exit if already at desired sha version + remote_head = get_remote_head(git_path, module, dest, version, remote, bare) if before == remote_head: if local_mods: module.exit_json(changed=True, before=before, after=remote_head, @@ -371,11 +412,12 @@ def main(): module.exit_json(changed=False, before=before, after=remote_head) if module.check_mode: module.exit_json(changed=True, before=before, after=remote_head) - fetch(git_path, module, repo, dest, version, remote) + fetch(git_path, module, repo, dest, version, remote, bare) # switch to version specified regardless of whether # we cloned or pulled - switch_version(git_path, module, dest, remote, version) + if not bare: + switch_version(git_path, module, dest, remote, version) # determine if we changed anything after = get_version(git_path, dest) diff --git a/test/TestRunner.py b/test/TestRunner.py index f991d02bd3c..3ae95a071f7 100644 --- a/test/TestRunner.py +++ b/test/TestRunner.py @@ -170,10 +170,13 @@ class TestRunner(unittest.TestCase): def test_git(self): self._run('file', ['path=/tmp/gitdemo', 'state=absent']) self._run('file', ['path=/tmp/gd', 'state=absent']) + self._run('file', ['path=/tmp/gdbare', 'state=absent']) + self._run('file', ['path=/tmp/gdreference', 'state=absent']) + self._run('file', ['path=/tmp/gdreftest', 'state=absent']) self._run('command', ['git init gitdemo', 'chdir=/tmp']) self._run('command', ['touch a', 'chdir=/tmp/gitdemo']) self._run('command', ['git add *', 'chdir=/tmp/gitdemo']) - self._run('command', ['git commit -m "test commit 2"', 'chdir=/tmp/gitdemo']) + self._run('command', ['git commit -m "test commit 1"', 'chdir=/tmp/gitdemo']) self._run('command', ['touch b', 'chdir=/tmp/gitdemo']) self._run('command', ['git add *', 'chdir=/tmp/gitdemo']) self._run('command', ['git commit -m "test commit 2"', 'chdir=/tmp/gitdemo']) @@ -186,6 +189,28 @@ class TestRunner(unittest.TestCase): # test the force option when set result = self._run('git', ["repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gd", "force=yes"]) assert result['changed'] + # test the bare option + result = self._run('git', ["repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gdbare", "bare=yes", "remote=test"]) + assert result['changed'] + # test a no-op fetch + result = self._run('git', ["repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gdbare", "bare=yes"]) + assert not result['changed'] + # test whether fetch is working for bare repos + self._run('command', ['touch c', 'chdir=/tmp/gitdemo']) + self._run('command', ['git add *', 'chdir=/tmp/gitdemo']) + self._run('command', ['git commit -m "test commit 3"', 'chdir=/tmp/gitdemo']) + result = self._run('git', ["repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gdbare", "bare=yes"]) + assert result['changed'] + # test reference repos + result = self._run('git', ["repo=\"file:///tmp/gdbare\"", "dest=/tmp/gdreference", "bare=yes"]) + assert result['changed'] + result = self._run('git', ["repo=\"file:///tmp/gitdemo\"", "dest=/tmp/gdreftest", "reference=/tmp/gdreference/"]) + assert result['changed'] + assert os.path.isfile('/tmp/gdreftest/a') + result = self._run('command', ['ls', 'chdir=/tmp/gdreference/objects/pack']) + assert result['stdout'] != '' + result = self._run('command', ['ls', 'chdir=/tmp/gdreftest/.git/objects/pack']) + assert result['stdout'] == '' def test_file(self): filedemo = tempfile.mkstemp()[1]