git - add single_branch option (#28465)

In some usecases, we want to be able to clone a single branch
of a repository, without using --depth (which implies --single-branch).

* Use branch name when available
  -  update description of parameter
  - consolidate branch or tag checking for easy reuse

* Add changelog
* Use static task imports rather than dynamic includes
* Add integration tests for single_branch
* Account for older versions of git
* Minor tweak to warnings

Co-authored-by: Laurent Coustet <laurent.coustet@clarisys.fr>
Co-authored-by: Sam Doran <sdoran@redhat.com>
This commit is contained in:
Laurent Coustet 2020-06-29 21:40:54 +02:00 committed by GitHub
parent 5a28b2b86c
commit e396715d7b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 146 additions and 28 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- git - add ``single_branch`` parameter (https://github.com/ansible/ansible/pull/28465)

View file

@ -69,7 +69,7 @@ options:
If version is set to a I(SHA-1) not reachable from any branch If version is set to a I(SHA-1) not reachable from any branch
or tag, this option may be necessary to specify the ref containing or tag, this option may be necessary to specify the ref containing
the I(SHA-1). the I(SHA-1).
Uses the same syntax as the 'git fetch' command. Uses the same syntax as the C(git fetch) command.
An example value could be "refs/meta/config". An example value could be "refs/meta/config".
version_added: "1.9" version_added: "1.9"
force: force:
@ -127,6 +127,13 @@ options:
default: 'yes' default: 'yes'
version_added: "1.6" version_added: "1.6"
single_branch:
description:
- Clone only the history leading to the tip of the specified C(branch)
type: bool
default: 'no'
version_added: '2.11'
track_submodules: track_submodules:
description: description:
- if C(yes), submodules will track the latest commit on their - if C(yes), submodules will track the latest commit on their
@ -232,6 +239,11 @@ EXAMPLES = '''
repo: https://github.com/ansible/ansible-examples.git repo: https://github.com/ansible/ansible-examples.git
dest: /src/ansible-examples dest: /src/ansible-examples
separate_git_dir: /src/ansible-examples.git separate_git_dir: /src/ansible-examples.git
# Example clone of a single branch
- git:
single_branch: yes
branch: master
''' '''
RETURN = ''' RETURN = '''
@ -254,7 +266,7 @@ warnings:
description: List of warnings if requested features were not available due to a too old git version. description: List of warnings if requested features were not available due to a too old git version.
returned: error returned: error
type: str type: str
sample: Your git version is too old to fully support the depth argument. Falling back to full checkouts. sample: git version is too old to fully support the depth argument. Falling back to full checkouts.
git_dir_now: git_dir_now:
description: Contains the new path of .git directory if it's changed description: Contains the new path of .git directory if it's changed
returned: success returned: success
@ -453,7 +465,7 @@ def get_submodule_versions(git_path, module, dest, version='HEAD'):
def clone(git_path, module, repo, dest, remote, depth, version, bare, def clone(git_path, module, repo, dest, remote, depth, version, bare,
reference, refspec, verify_commit, separate_git_dir, result, gpg_whitelist): reference, refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch):
''' makes a new git repo if it does not already exist ''' ''' makes a new git repo if it does not already exist '''
dest_dirname = os.path.dirname(dest) dest_dirname = os.path.dirname(dest)
try: try:
@ -466,11 +478,12 @@ def clone(git_path, module, repo, dest, remote, depth, version, bare,
cmd.append('--bare') cmd.append('--bare')
else: else:
cmd.extend(['--origin', remote]) cmd.extend(['--origin', remote])
is_branch_or_tag = is_remote_branch(git_path, module, dest, repo, version) or is_remote_tag(git_path, module, dest, repo, version)
if depth: if depth:
if version == 'HEAD' or refspec: if version == 'HEAD' or refspec:
cmd.extend(['--depth', str(depth)]) cmd.extend(['--depth', str(depth)])
elif is_remote_branch(git_path, module, dest, repo, version) \ elif is_branch_or_tag:
or is_remote_tag(git_path, module, dest, repo, version):
cmd.extend(['--depth', str(depth)]) cmd.extend(['--depth', str(depth)])
cmd.extend(['--branch', version]) cmd.extend(['--branch', version])
else: else:
@ -480,12 +493,23 @@ def clone(git_path, module, repo, dest, remote, depth, version, bare,
"HEAD, branches, tags or in combination with refspec.") "HEAD, branches, tags or in combination with refspec.")
if reference: if reference:
cmd.extend(['--reference', str(reference)]) cmd.extend(['--reference', str(reference)])
needs_separate_git_dir_fallback = False
if separate_git_dir: if single_branch:
git_version_used = git_version(git_path, module)
if git_version_used is None: if git_version_used is None:
module.fail_json(msg='Can not find git executable at %s' % git_path) module.fail_json(msg='Cannot find git executable at %s' % git_path)
if git_version_used < LooseVersion('1.7.10'):
module.warn("git version '%s' is too old to use 'single-branch'. Ignoring." % git_version_used)
else:
cmd.append("--single-branch")
if is_branch_or_tag:
cmd.extend(['--branch', version])
needs_separate_git_dir_fallback = False
if separate_git_dir:
if git_version_used is None:
module.fail_json(msg='Cannot find git executable at %s' % git_path)
if git_version_used < LooseVersion('1.7.5'): if git_version_used < LooseVersion('1.7.5'):
# git before 1.7.5 doesn't have separate-git-dir argument, do fallback # git before 1.7.5 doesn't have separate-git-dir argument, do fallback
needs_separate_git_dir_fallback = True needs_separate_git_dir_fallback = True
@ -1061,6 +1085,7 @@ def main():
executable=dict(default=None, type='path'), executable=dict(default=None, type='path'),
bare=dict(default='no', type='bool'), bare=dict(default='no', type='bool'),
recursive=dict(default='yes', type='bool'), recursive=dict(default='yes', type='bool'),
single_branch=dict(default=False, type='bool'),
track_submodules=dict(default='no', type='bool'), track_submodules=dict(default='no', type='bool'),
umask=dict(default=None, type='raw'), umask=dict(default=None, type='raw'),
archive=dict(type='path'), archive=dict(type='path'),
@ -1085,6 +1110,7 @@ def main():
verify_commit = module.params['verify_commit'] verify_commit = module.params['verify_commit']
gpg_whitelist = module.params['gpg_whitelist'] gpg_whitelist = module.params['gpg_whitelist']
reference = module.params['reference'] reference = module.params['reference']
single_branch = module.params['single_branch']
git_path = module.params['executable'] or module.get_bin_path('git', True) git_path = module.params['executable'] or module.get_bin_path('git', True)
key_file = module.params['key_file'] key_file = module.params['key_file']
ssh_opts = module.params['ssh_opts'] ssh_opts = module.params['ssh_opts']
@ -1157,7 +1183,7 @@ def main():
git_version_used = git_version(git_path, module) git_version_used = git_version(git_path, module)
if depth is not None and git_version_used < LooseVersion('1.9.1'): if depth is not None and git_version_used < LooseVersion('1.9.1'):
result['warnings'].append("Your git version is too old to fully support the depth argument. Falling back to full checkouts.") module.warn("git version is too old to fully support the depth argument. Falling back to full checkouts.")
depth = None depth = None
recursive = module.params['recursive'] recursive = module.params['recursive']
@ -1180,7 +1206,8 @@ def main():
result['diff'] = diff result['diff'] = diff
module.exit_json(**result) module.exit_json(**result)
# there's no git config, so clone # there's no git config, so clone
clone(git_path, module, repo, dest, remote, depth, version, bare, reference, refspec, verify_commit, separate_git_dir, result, gpg_whitelist) clone(git_path, module, repo, dest, remote, depth, version, bare, reference,
refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_whitelist, single_branch)
elif not update: elif not update:
# Just return having found a repo already in the dest path # Just return having found a repo already in the dest path
# this does no checking that the repo is the actual repo # this does no checking that the repo is the actual repo

View file

@ -16,25 +16,26 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
- include_tasks: setup.yml - import_tasks: setup.yml
- include_tasks: setup-local-repos.yml - import_tasks: setup-local-repos.yml
- include_tasks: formats.yml - import_tasks: formats.yml
- include_tasks: missing_hostkey.yml - import_tasks: missing_hostkey.yml
- include_tasks: no-destination.yml - import_tasks: no-destination.yml
- include_tasks: specific-revision.yml - import_tasks: specific-revision.yml
- include_tasks: submodules.yml - import_tasks: submodules.yml
- include_tasks: change-repo-url.yml - import_tasks: change-repo-url.yml
- include_tasks: depth.yml - import_tasks: depth.yml
- include_tasks: checkout-new-tag.yml - import_tasks: single-branch.yml
- import_tasks: checkout-new-tag.yml
- include_tasks: gpg-verification.yml - include_tasks: gpg-verification.yml
when: when:
- not gpg_version.stderr - not gpg_version.stderr
- gpg_version.stdout - gpg_version.stdout
- git_version.stdout is version("2.1.0", '>=') - git_version.stdout is version("2.1.0", '>=')
- include_tasks: localmods.yml - import_tasks: localmods.yml
- include_tasks: reset-origin.yml - import_tasks: reset-origin.yml
- include_tasks: ambiguous-ref.yml - import_tasks: ambiguous-ref.yml
- include_tasks: archive.yml - import_tasks: archive.yml
- include_tasks: separate-git-dir.yml - import_tasks: separate-git-dir.yml
- include_tasks: forcefully-fetch-tag.yml - import_tasks: forcefully-fetch-tag.yml

View file

@ -0,0 +1,87 @@
# Test single_branch parameter
- name: SINGLE_BRANCH | clear checkout_dir
file:
state: absent
path: "{{ checkout_dir }}"
- name: SINGLE_BRANCH | Clone example git repo using single_branch
git:
repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
dest: '{{ checkout_dir }}'
single_branch: yes
register: single_branch_1
- name: SINGLE_BRANCH | Clone example git repo using single_branch again
git:
repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
dest: '{{ checkout_dir }}'
single_branch: yes
register: single_branch_2
- name: SINGLE_BRANCH | List revisions
command: git rev-list --all --count
args:
chdir: '{{ checkout_dir }}'
register: rev_list1
when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git >= {{ git_version_supporting_single_branch }}
assert:
that:
- single_branch_1 is changed
- single_branch_2 is not changed
when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git < {{ git_version_supporting_single_branch }}
assert:
that:
- single_branch_1 is changed
- single_branch_1.warnings | length == 1
- single_branch_2 is not changed
when: git_version.stdout is version(git_version_supporting_single_branch, '<')
- name: SINGLE_BRANCH | clear checkout_dir
file:
state: absent
path: "{{ checkout_dir }}"
- name: SINGLE_BRANCH | Clone example git repo using single_branch with version
git:
repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
dest: '{{ checkout_dir }}'
single_branch: yes
version: master
register: single_branch_3
- name: SINGLE_BRANCH | Clone example git repo using single_branch with version again
git:
repo: 'file://{{ repo_dir|expanduser }}/shallow_branches'
dest: '{{ checkout_dir }}'
single_branch: yes
version: master
register: single_branch_4
- name: SINGLE_BRANCH | List revisions
command: git rev-list --all --count
args:
chdir: '{{ checkout_dir }}'
register: rev_list2
when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git >= {{ git_version_supporting_single_branch }}
assert:
that:
- single_branch_3 is changed
- single_branch_4 is not changed
- rev_list2.stdout == '1'
when: git_version.stdout is version(git_version_supporting_single_branch, '>=')
- name: SINGLE_BRANCH | Ensure single_branch did the right thing with git < {{ git_version_supporting_single_branch }}
assert:
that:
- single_branch_3 is changed
- single_branch_3.warnings | length == 1
- single_branch_4 is not changed
when: git_version.stdout is version(git_version_supporting_single_branch, '<')

View file

@ -43,6 +43,7 @@ known_host_files:
- '/etc/ssh/ssh_known_hosts' - '/etc/ssh/ssh_known_hosts'
git_version_supporting_depth: 1.9.1 git_version_supporting_depth: 1.9.1
git_version_supporting_ls_remote: 1.7.5 git_version_supporting_ls_remote: 1.7.5
git_version_supporting_single_branch: 1.7.10
# path to a SSH private key for use with github.com (tests skipped if undefined) # path to a SSH private key for use with github.com (tests skipped if undefined)
# github_ssh_private_key: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa" # github_ssh_private_key: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa"
git_gpg_testkey: | git_gpg_testkey: |