From e396715d7be28da70c34b1e9e191a9ecbe63e52a Mon Sep 17 00:00:00 2001 From: Laurent Coustet Date: Mon, 29 Jun 2020 21:40:54 +0200 Subject: [PATCH] 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 Co-authored-by: Sam Doran --- .../fragments/git-add-single_branch.yml | 2 + lib/ansible/modules/git.py | 51 ++++++++--- test/integration/targets/git/tasks/main.yml | 33 +++---- .../targets/git/tasks/single-branch.yml | 87 +++++++++++++++++++ test/integration/targets/git/vars/main.yml | 1 + 5 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 changelogs/fragments/git-add-single_branch.yml create mode 100644 test/integration/targets/git/tasks/single-branch.yml diff --git a/changelogs/fragments/git-add-single_branch.yml b/changelogs/fragments/git-add-single_branch.yml new file mode 100644 index 00000000000..47ae6fa7d0e --- /dev/null +++ b/changelogs/fragments/git-add-single_branch.yml @@ -0,0 +1,2 @@ +minor_changes: + - git - add ``single_branch`` parameter (https://github.com/ansible/ansible/pull/28465) diff --git a/lib/ansible/modules/git.py b/lib/ansible/modules/git.py index b47e94517bd..5332262e995 100644 --- a/lib/ansible/modules/git.py +++ b/lib/ansible/modules/git.py @@ -31,7 +31,7 @@ options: required: true version: description: - - What version of the repository to check out. This can be + - What version of the repository to check out. This can be the literal string C(HEAD), a branch name, a tag name. It can also be a I(SHA-1) hash, in which case C(refspec) needs to be specified if the given revision is not already available. @@ -69,7 +69,7 @@ options: 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 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". version_added: "1.9" force: @@ -127,6 +127,13 @@ options: default: 'yes' 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: description: - if C(yes), submodules will track the latest commit on their @@ -232,6 +239,11 @@ EXAMPLES = ''' repo: https://github.com/ansible/ansible-examples.git dest: /src/ansible-examples separate_git_dir: /src/ansible-examples.git + +# Example clone of a single branch +- git: + single_branch: yes + branch: master ''' RETURN = ''' @@ -254,7 +266,7 @@ warnings: description: List of warnings if requested features were not available due to a too old git version. returned: error 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: description: Contains the new path of .git directory if it's changed 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, - 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 ''' dest_dirname = os.path.dirname(dest) try: @@ -466,11 +478,12 @@ def clone(git_path, module, repo, dest, remote, depth, version, bare, cmd.append('--bare') else: 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 version == 'HEAD' or refspec: cmd.extend(['--depth', str(depth)]) - elif is_remote_branch(git_path, module, dest, repo, version) \ - or is_remote_tag(git_path, module, dest, repo, version): + elif is_branch_or_tag: cmd.extend(['--depth', str(depth)]) cmd.extend(['--branch', version]) else: @@ -480,12 +493,23 @@ def clone(git_path, module, repo, dest, remote, depth, version, bare, "HEAD, branches, tags or in combination with refspec.") if reference: cmd.extend(['--reference', str(reference)]) - needs_separate_git_dir_fallback = False - if separate_git_dir: - git_version_used = git_version(git_path, module) + if single_branch: 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'): # git before 1.7.5 doesn't have separate-git-dir argument, do fallback needs_separate_git_dir_fallback = True @@ -1061,6 +1085,7 @@ def main(): executable=dict(default=None, type='path'), bare=dict(default='no', type='bool'), recursive=dict(default='yes', type='bool'), + single_branch=dict(default=False, type='bool'), track_submodules=dict(default='no', type='bool'), umask=dict(default=None, type='raw'), archive=dict(type='path'), @@ -1085,6 +1110,7 @@ def main(): verify_commit = module.params['verify_commit'] gpg_whitelist = module.params['gpg_whitelist'] reference = module.params['reference'] + single_branch = module.params['single_branch'] git_path = module.params['executable'] or module.get_bin_path('git', True) key_file = module.params['key_file'] ssh_opts = module.params['ssh_opts'] @@ -1157,7 +1183,7 @@ def main(): git_version_used = git_version(git_path, module) 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 recursive = module.params['recursive'] @@ -1180,7 +1206,8 @@ def main(): result['diff'] = diff module.exit_json(**result) # 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: # Just return having found a repo already in the dest path # this does no checking that the repo is the actual repo diff --git a/test/integration/targets/git/tasks/main.yml b/test/integration/targets/git/tasks/main.yml index 9d750c5cd45..543b5842ebb 100644 --- a/test/integration/targets/git/tasks/main.yml +++ b/test/integration/targets/git/tasks/main.yml @@ -16,25 +16,26 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -- include_tasks: setup.yml -- include_tasks: setup-local-repos.yml +- import_tasks: setup.yml +- import_tasks: setup-local-repos.yml -- include_tasks: formats.yml -- include_tasks: missing_hostkey.yml -- include_tasks: no-destination.yml -- include_tasks: specific-revision.yml -- include_tasks: submodules.yml -- include_tasks: change-repo-url.yml -- include_tasks: depth.yml -- include_tasks: checkout-new-tag.yml +- import_tasks: formats.yml +- import_tasks: missing_hostkey.yml +- import_tasks: no-destination.yml +- import_tasks: specific-revision.yml +- import_tasks: submodules.yml +- import_tasks: change-repo-url.yml +- import_tasks: depth.yml +- import_tasks: single-branch.yml +- import_tasks: checkout-new-tag.yml - include_tasks: gpg-verification.yml when: - not gpg_version.stderr - gpg_version.stdout - git_version.stdout is version("2.1.0", '>=') -- include_tasks: localmods.yml -- include_tasks: reset-origin.yml -- include_tasks: ambiguous-ref.yml -- include_tasks: archive.yml -- include_tasks: separate-git-dir.yml -- include_tasks: forcefully-fetch-tag.yml +- import_tasks: localmods.yml +- import_tasks: reset-origin.yml +- import_tasks: ambiguous-ref.yml +- import_tasks: archive.yml +- import_tasks: separate-git-dir.yml +- import_tasks: forcefully-fetch-tag.yml diff --git a/test/integration/targets/git/tasks/single-branch.yml b/test/integration/targets/git/tasks/single-branch.yml new file mode 100644 index 00000000000..5cfb4d5b8f3 --- /dev/null +++ b/test/integration/targets/git/tasks/single-branch.yml @@ -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, '<') diff --git a/test/integration/targets/git/vars/main.yml b/test/integration/targets/git/vars/main.yml index a5bae5ba7f2..a1bbfca9178 100644 --- a/test/integration/targets/git/vars/main.yml +++ b/test/integration/targets/git/vars/main.yml @@ -43,6 +43,7 @@ known_host_files: - '/etc/ssh/ssh_known_hosts' git_version_supporting_depth: 1.9.1 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) # github_ssh_private_key: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa" git_gpg_testkey: |