git: fix track_submodules using hardcoded master branch (#86692)

The git module hardcodes 'master' when comparing submodule versions
with track_submodules=yes. This fails for repositories that use a
different default branch (e.g. 'main').

Read the branch from .gitmodules for each submodule, falling back to
the remote HEAD if no branch is configured. This respects the
submodule's configured branch and works regardless of the default
branch name.

Fixes #77691


Signed-off-by: olegnazarov23 <olegnazarov23@users.noreply.github.com>
Co-authored-by: Abhijeet Kasurde <Akasurde@redhat.com>
pull/86923/head
Oleg Nazarov 1 month ago committed by GitHub
parent a1ba752ed3
commit 44aa5b8936
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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).

@ -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:

@ -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

@ -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 }}"
Loading…
Cancel
Save