diff --git a/changelogs/fragments/77691-git-track-submodules-branch.yml b/changelogs/fragments/77691-git-track-submodules-branch.yml new file mode 100644 index 00000000000..589397d101d --- /dev/null +++ b/changelogs/fragments/77691-git-track-submodules-branch.yml @@ -0,0 +1,4 @@ +bugfixes: + - git - use the branch configured in ``.gitmodules`` or the remote HEAD instead + of hardcoding ``master`` when ``track_submodules=yes`` + (https://github.com/ansible/ansible/issues/77691). diff --git a/lib/ansible/modules/git.py b/lib/ansible/modules/git.py index 5ad0e77b388..e3a45996a30 100644 --- a/lib/ansible/modules/git.py +++ b/lib/ansible/modules/git.py @@ -168,9 +168,11 @@ options: track_submodules: description: - - If V(true), submodules will track the latest commit on their - master branch (or other branch specified in C(.gitmodules)). If - V(false), submodules will be kept at the revision specified by the + - If V(true), submodules will track the latest commit on the branch specified in C(.gitmodules). + If no branch is specified in C(.gitmodules), it will use the remote HEAD. + - Currently, the value of remote is defaulted to C(origin). + Specifying the remote for the submodules is not supported. + - If V(false), submodules will be kept at the revision specified by the main project. This is equivalent to specifying the C(--remote) flag to git submodule update. type: bool @@ -565,6 +567,28 @@ def get_submodule_versions(git_path, module, dest, version='HEAD'): return submodules +def get_submodule_branch(git_path, module, dest, submodule): + """Get the configured branch for a submodule from .gitmodules. + + Falls back to the remote HEAD if no branch is configured. + """ + cmd = [git_path, 'config', '-f', '.gitmodules', + f'submodule.{submodule}.branch'] + rc, out, _err = module.run_command(cmd, cwd=dest) + if rc == 0 and out.strip(): + return out.strip() + + # No branch configured in .gitmodules, use remote HEAD + submodule_path = os.path.join(dest, submodule) + cmd = [git_path, 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'] + rc, out, _err = module.run_command(cmd, cwd=submodule_path) + if rc == 0 and out.strip(): + # Returns e.g. "origin/main", strip the remote prefix + return out.strip().split('/', 1)[-1] + + return 'HEAD' + + def clone(git_path, module, repo, dest, remote, depth, version, bare, reference, refspec, git_version_used, verify_commit, separate_git_dir, result, gpg_allowlist, single_branch): """ makes a new git repo if it does not already exist """ @@ -974,10 +998,19 @@ def submodules_fetch(git_path, module, remote, track_submodules, dest): module.fail_json(msg="Failed to fetch submodules: %s" % out + err) if track_submodules: - # Compare against submodule HEAD - # FIXME: determine this from .gitmodules - version = 'master' - after = get_submodule_versions(git_path, module, dest, '%s/%s' % (remote, version)) + # Compare each submodule against its configured remote branch + after = {} + for submodule in begin: + branch = get_submodule_branch(git_path, module, dest, submodule) + version_ref = f'{remote}/{branch}' if branch != 'HEAD' else 'HEAD' + submodule_path = os.path.join(dest, submodule) + cmd = [git_path, 'rev-parse', version_ref] + (rc, out, err) = module.run_command(cmd, cwd=submodule_path) + if rc != 0: + module.fail_json( + msg='Unable to determine hash of submodule %s at %s' % (submodule, version_ref), + stdout=out, stderr=err, rc=rc) + after[submodule] = out.strip() if begin != after: changed = True else: diff --git a/test/integration/targets/git/tasks/main.yml b/test/integration/targets/git/tasks/main.yml index 6d36869dd5c..10fd6b8c33c 100644 --- a/test/integration/targets/git/tasks/main.yml +++ b/test/integration/targets/git/tasks/main.yml @@ -33,6 +33,7 @@ - import_tasks: specific-revision.yml - import_tasks: specific-rev-diff-check.yml - import_tasks: submodules.yml + - import_tasks: track-submodules-branch.yml - import_tasks: change-repo-url.yml - import_tasks: depth.yml - import_tasks: single-branch.yml diff --git a/test/integration/targets/git/tasks/track-submodules-branch.yml b/test/integration/targets/git/tasks/track-submodules-branch.yml new file mode 100644 index 00000000000..891dfed6b42 --- /dev/null +++ b/test/integration/targets/git/tasks/track-submodules-branch.yml @@ -0,0 +1,272 @@ +# +# Tests for track_submodules with non-master default branches +# +# Verifies that get_submodule_branch() correctly reads the branch from +# .gitmodules and falls back to remote HEAD, instead of hardcoding 'master'. +# + +# --- Setup --- + +- name: TRACK-SUBMODULES-BRANCH | allow local file transport for submodule clones + shell: git config --global protocol.file.allow always + +- name: TRACK-SUBMODULES-BRANCH | create local repo dirs + file: + path: "{{ item }}" + state: directory + loop: + - "{{ repo_dir }}/track_sub_remote" + - "{{ repo_dir }}/track_sub_parent" + +- name: TRACK-SUBMODULES-BRANCH | create a submodule repo with 'main' as default branch + shell: | + set -eEu + git init + echo "initial" > file1.txt + git add file1.txt + git commit -m "initial commit on main" + args: + chdir: "{{ repo_dir }}/track_sub_remote" + +- name: TRACK-SUBMODULES-BRANCH | create parent repo with submodule pointing to 'main' + shell: | + set -eEu + git init + echo "parent" > README.md + git add README.md + git commit -m "parent initial" + + git submodule add "{{ repo_dir }}/track_sub_remote" sub1 + git commit -m "add submodule sub1" + args: + chdir: "{{ repo_dir }}/track_sub_parent" + +- name: TRACK-SUBMODULES-BRANCH | set explicit branch in .gitmodules + shell: | + set -eEu + git config -f .gitmodules submodule.sub1.branch main + git add .gitmodules + git commit -m "set submodule branch to main" + args: + chdir: "{{ repo_dir }}/track_sub_parent" + +# --- Test 1: track_submodules detects updates on configured branch (main) --- + +- name: TRACK-SUBMODULES-BRANCH | clean checkout dir + file: + state: absent + path: "{{ checkout_dir }}" + +- name: TRACK-SUBMODULES-BRANCH | clone parent with recursive submodules + git: + repo: "{{ repo_dir }}/track_sub_parent" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + +- name: TRACK-SUBMODULES-BRANCH | record initial state (should not be changed) + git: + repo: "{{ repo_dir }}/track_sub_parent" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + register: track_initial + +- name: TRACK-SUBMODULES-BRANCH | assert no change on first re-run + assert: + that: + - not track_initial.changed + +- name: TRACK-SUBMODULES-BRANCH | push a new commit to the submodule remote + shell: | + set -eEu + echo "update" > file2.txt + git add file2.txt + git commit -m "second commit on main" + args: + chdir: "{{ repo_dir }}/track_sub_remote" + +- name: TRACK-SUBMODULES-BRANCH | re-run with track_submodules to detect remote update + git: + repo: "{{ repo_dir }}/track_sub_parent" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + register: track_after_update + +- name: TRACK-SUBMODULES-BRANCH | assert change detected after submodule remote was updated + assert: + that: + - track_after_update.changed + +# --- Test 2: track_submodules works with remote HEAD fallback (no branch in .gitmodules) --- + +- name: TRACK-SUBMODULES-BRANCH | create dirs for fallback test + file: + path: "{{ item }}" + state: directory + loop: + - "{{ repo_dir }}/track_sub_remote_fb" + - "{{ repo_dir }}/track_sub_parent_fb" + +- name: TRACK-SUBMODULES-BRANCH | create submodule repo for fallback test + shell: | + set -eEu + git init + echo "initial" > file1.txt + git add file1.txt + git commit -m "initial commit" + args: + chdir: "{{ repo_dir }}/track_sub_remote_fb" + +- name: TRACK-SUBMODULES-BRANCH | create parent repo without explicit branch in .gitmodules + shell: | + set -eEu + git init + echo "parent" > README.md + git add README.md + git commit -m "parent initial" + + git submodule add "{{ repo_dir }}/track_sub_remote_fb" sub_fb + git commit -m "add submodule sub_fb (no explicit branch)" + args: + chdir: "{{ repo_dir }}/track_sub_parent_fb" + +- name: TRACK-SUBMODULES-BRANCH | clean checkout dir for fallback test + file: + state: absent + path: "{{ checkout_dir }}" + +- name: TRACK-SUBMODULES-BRANCH | clone parent (fallback test) + git: + repo: "{{ repo_dir }}/track_sub_parent_fb" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + +- name: TRACK-SUBMODULES-BRANCH | push a new commit to fallback submodule remote + shell: | + set -eEu + echo "new content" > file2.txt + git add file2.txt + git commit -m "second commit" + args: + chdir: "{{ repo_dir }}/track_sub_remote_fb" + +- name: TRACK-SUBMODULES-BRANCH | re-run track_submodules with fallback branch detection + git: + repo: "{{ repo_dir }}/track_sub_parent_fb" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + register: track_fallback_update + +- name: TRACK-SUBMODULES-BRANCH | assert change detected with fallback branch + assert: + that: + - track_fallback_update.changed + +# --- Test 3: two submodules with different default branches --- + +- name: TRACK-SUBMODULES-BRANCH | create dirs for multi-branch test + file: + path: "{{ item }}" + state: directory + loop: + - "{{ repo_dir }}/track_sub_remote_master" + - "{{ repo_dir }}/track_sub_remote_main" + - "{{ repo_dir }}/track_sub_parent_multi" + +- name: TRACK-SUBMODULES-BRANCH | create submodule repo with 'develop' branch + shell: | + set -eEu + git init + echo "initial" > file1.txt + git add file1.txt + git commit -m "initial on main" + git checkout -b develop + echo "dev" > file2.txt + git add file2.txt + git commit -m "initial on develop" + args: + chdir: "{{ repo_dir }}/track_sub_remote_master" + +- name: TRACK-SUBMODULES-BRANCH | create submodule repo using 'main' + shell: | + set -eEu + git init + echo "initial" > file1.txt + git add file1.txt + git commit -m "initial on main" + args: + chdir: "{{ repo_dir }}/track_sub_remote_main" + +- name: TRACK-SUBMODULES-BRANCH | create parent with two submodules on different branches + shell: | + set -eEu + git init + echo "parent" > README.md + git add README.md + git commit -m "parent initial" + + git submodule add -b develop "{{ repo_dir }}/track_sub_remote_master" sub_develop + git submodule add "{{ repo_dir }}/track_sub_remote_main" sub_main + git config -f .gitmodules submodule.sub_main.branch main + git add .gitmodules + git commit -m "add two submodules with different branches" + args: + chdir: "{{ repo_dir }}/track_sub_parent_multi" + +- name: TRACK-SUBMODULES-BRANCH | clean checkout dir for multi-branch test + file: + state: absent + path: "{{ checkout_dir }}" + +- name: TRACK-SUBMODULES-BRANCH | clone parent with two submodules + git: + repo: "{{ repo_dir }}/track_sub_parent_multi" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + +- name: TRACK-SUBMODULES-BRANCH | re-run should show no changes + git: + repo: "{{ repo_dir }}/track_sub_parent_multi" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + register: track_multi_initial + +- name: TRACK-SUBMODULES-BRANCH | assert no change on re-run with two submodules + assert: + that: + - not track_multi_initial.changed + +- name: TRACK-SUBMODULES-BRANCH | push update to only the 'main' submodule + shell: | + set -eEu + echo "update" > file2.txt + git add file2.txt + git commit -m "second commit on main" + args: + chdir: "{{ repo_dir }}/track_sub_remote_main" + +- name: TRACK-SUBMODULES-BRANCH | re-run should detect change in sub_main only + git: + repo: "{{ repo_dir }}/track_sub_parent_multi" + dest: "{{ checkout_dir }}" + recursive: yes + track_submodules: yes + register: track_multi_update + +- name: TRACK-SUBMODULES-BRANCH | assert change detected with mixed-branch submodules + assert: + that: + - track_multi_update.changed + +# --- Cleanup --- + +- name: TRACK-SUBMODULES-BRANCH | clean checkout dir + file: + state: absent + path: "{{ checkout_dir }}"