Suppose an automation job successfully selects superproject commit B, records a zero exit status, and proceeds under the assumption that every source directory now represents B. Inside an already initialized submodule, however, engine.txt can still contain the payload from child commit A. Nothing in the successful parent checkout alone proves that the child working tree moved.
That distinction matters because a submodule is a repository with its own history. The superproject records an expected child commit through a gitlink, while the initialized child has its own HEAD and working tree. Git's checkout documentation explicitly says that --no-recurse-submodules leaves submodule working trees unupdated, whereas --recurse-submodules updates active submodules and can refuse when local child modifications would be overwritten.
The useful operational decision is therefore narrower than “did checkout succeed?” Identify the independently approved parent, inspect the child commit recorded in that parent's tree, inspect any separately staged gitlink, read the actual child HEAD, and verify the tracked child content and pending changes. Then classify the owned checkout as accepted, repairable, or on hold before a later build consumes it.
This article uses a supplied nine-case local experiment prepared on November 3, 2026 with Git 2.51.1 and CPython 3.12.14. No build, deployment, archive export, network fetch, or organizational release approval is performed.
Establish the approved parent and child revision
Begin with a declared source-selection contract, not with whatever commit happens to be checked out. In this experiment, parent A and parent B are readable labels for two separately authored superproject commits. Child A and child B are independently authored commits in the submodule repository. The A labels do not imply that parent A and child A have the same object ID; they are commits in different repositories.
The contract first identifies the approved parent commit. Then it identifies the child commit that the tree of that approved parent records at modules/engine. Git's submodule guide describes exactly this relationship: the submodule has its own history, and a superproject gitlink contains the object name of the child commit that the superproject expects at that path.
That expected parent identity must come from an owner or another independently controlled release-selection input. Deriving “expected parent” from the current HEAD would make an accidentally selected commit approve itself. The fixture consequently checks parent_head == parent_commits[label] separately before evaluating its broader matches_contract expression.
For readers who need the underlying commit-and-staging model rather than a submodule-specific discussion, use Refonte Learning's Git version-control foundations. The concern here starts after those fundamentals: whether an initialized child repository actually materializes the revision authorized by the selected parent.
The rolling Git manuals examined on November 3, 2026 are documentation context, not evidence that this behavior was introduced recently. The checkout page shows its last manual update at Git 2.55.0 on June 29, 2026; the submodule guide shows 2.52.0 on November 17, 2025. The observed client remains the supplied Git 2.51.1 baseline.
Separate the committed gitlink, index and child HEAD
Four identities answer four different questions.
Evidence | Fixture command | Question answered |
Parent HEAD | git rev-parse HEAD | Which superproject commit is selected? |
Committed gitlink | git ls-tree HEAD -- modules/engine | Which child commit does that parent tree record? |
Indexed gitlink | git ls-files --stage -- modules/engine | Which child commit is currently staged? |
Child HEAD | git rev-parse HEAD in the child | Which child commit is actually checked out? |
git ls-tree inspects a named committed tree and reports mode, object type, object identity, and pathname. Its population is affected by the named tree, invocation context, and path argument. In this controlled fixture, the expected row has mode 160000, object type commit, and the fixed path modules/engine.
git ls-files --stage is deliberately different: the manual says --stage exposes indexed mode bits, object name, and stage number. The fixture accepts exactly one stage-zero 160000 entry for this known path. Stage zero narrows this experiment away from unresolved index stages; it is not a general conflict parser.
This separation is the submodule equivalent of asking which population a repository command actually inspected. Refonte Learning's article on why a clean diff is only a bounded input check treats ordinary tracked, staged, untracked, and ignored files as a separate validation problem. Here the population under test is the relationship among a parent tree, its index entry, and another repository's materialized revision.
Read the child repository rather than a cached target
The fixture refuses to read child HEAD unless modules/engine/.git is a file. That precondition matters because running a Git command from a directory that is not actually an initialized child can resolve some repository context other than the one the operator intended.
The child command also uses the child directory explicitly. This is the direct readback. By contrast, git submodule status --cached reports the commit recorded by the containing superproject rather than reading the currently checked-out child revision. The ordinary git submodule status form reports the checked-out child and prefixes + when that revision does not match the index target.
That cached value is valuable evidence of intended selection. It is not evidence that the child's worktree reached that selection. The experiment intentionally excludes alternate old-form layouts, arbitrary path parsing, uninitialized children, and nested submodules.
Define the controlled repository boundary
The laboratory contains one local child source repository and one superproject. The child source receives commits A and B with literal tracked payloads engine-A\n and engine-B\n. The superproject then receives two commits: parent A records the child A gitlink, and parent B records child B. All objects needed by the nine cases already exist locally.
The submodule guide describes the basic initialized form as a child Git directory, child working directory, gitlink, and .gitmodules mapping, while also noting that other forms exist. This article qualifies only the initialized form exercised by the fixture.
Both synthetic repositories set submodule.recurse=false, while each tested checkout makes recursion explicit with either --recurse-submodules or --no-recurse-submodules. That makes the relevant selection choice visible in the captured command log rather than relying on an inherited default. Git documents command-line recursion as a higher-precedence control and says checkout recursion applies to active submodules.
A submodule should also not be confused with a linked worktree. A child repository has its own history. Linked worktrees are multiple working directories associated with one repository and can share administrative resources; that different ownership model is examined in shared configuration in linked worktrees.
Limit local transport permission to the authored setup
The one protocol.file.allow=always setting appears only on the git submodule add command that clones the synthetic, locally authored source. It is command-scoped. It is not a recommendation to modify global protocol policy or to trust an unreviewed .gitmodules URL.
The fixture also uses an empty global Git configuration, disables system configuration, supplies an empty initialization template, sets core.hooksPath=/dev/null for its Git invocations, and constructs an allowlisted subprocess environment. Those controls reduce unrelated inherited influences in this experiment, but they are not an operating-system security sandbox and do not establish containment against a malicious host.
Temporary repositories are deleted on normal completion. That cleanup is appropriate because every repository and data file in the fixture is authored for the experiment. It says nothing about how an operator should clean an existing repository containing another person's work.
Run the complete nine-case experiment
Save the source below as cloud_submodule_fixture.py outside an existing repository. It requires Python 3, a compatible Git executable, a Unix-like host supporting os.getuid, and writable temporary storage. Run the commands, capture the fixture's exit status immediately, then inspect the JSON rather than treating the shell status as the entire acceptance record.
python --version
git --version
python cloud_submodule_fixture.py > cloud_submodule_results.json
run_status=$?
printf 'fixture exit=%s\n' "$run_status"The reference source is:
"""Owned local Git submodule revision experiment; no network or existing repo."""
import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
PAYLOAD = {'A': 'engine-A\n', 'B': 'engine-B\n'}
SUB = 'modules/engine'
def require(ok, message):
if not ok:
raise RuntimeError('HOLD: ' + message)
def run(report):
executable = shutil.which('git')
require(executable is not None, 'Git executable required')
with tempfile.TemporaryDirectory(prefix='owned-submodule-') as name:
root = Path(name)
empty = root / 'empty-config'
empty.write_text('')
template = root / 'empty-template'
template.mkdir()
env = {'PATH': os.defpath, 'LC_ALL': 'C', 'LANG': 'C',
'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': str(empty),
'GIT_ATTR_NOSYSTEM': '1', 'GIT_TERMINAL_PROMPT': '0',
'GIT_AUTHOR_NAME': 'Owned fixture', 'GIT_COMMITTER_NAME': 'Owned fixture',
'GIT_AUTHOR_EMAIL': '[email protected]',
'GIT_COMMITTER_EMAIL': '[email protected]',
'GIT_AUTHOR_DATE': '2026-11-03T00:00:00+00:00',
'GIT_COMMITTER_DATE': '2026-11-03T00:00:00+00:00'}
def git(where, args, allowed=(0,)):
p = subprocess.run([executable, '-c', 'core.hooksPath=/dev/null', args],
cwd=where, env=env, stdin=subprocess.DEVNULL,
capture_output=True, text=True, timeout=20)
row = {'cwd': str(where), 'argv': p.args, 'returncode': p.returncode,
'stdout': p.stdout, 'stderr': p.stderr}
report['commands'].append(row)
require(p.returncode in allowed, 'unexpected Git exit: ' + repr(args))
return row
def output(where, args):
return git(where, args)['stdout'].strip()
report['git_version'] = output(root, '--version')
report['git_executable'] = executable
report['git_executable_sha256'] = hashlib.sha256(Path(executable).read_bytes()).hexdigest()
source, parent = root / 'engine-source', root / 'superproject'
for repo in (source, parent):
repo.mkdir()
git(repo, 'init', '-q', '--object-format=sha1', '--initial-branch=main',
'--template=' + str(template))
git(repo, 'config', 'core.autocrlf', 'false')
git(repo, 'config', 'submodule.recurse', 'false')
child_commits = {}
for label in ('A', 'B'):
(source / 'engine.txt').write_text(PAYLOAD[label])
git(source, 'add', '--', 'engine.txt')
git(source, 'commit', '-q', '-m', 'Engine ' + label)
child_commits[label] = output(source, 'rev-parse', 'HEAD')
(parent / 'README.txt').write_text('Owned superproject\n')
# File transport is enabled only for this authored local source clone.
git(parent, '-c', 'protocol.file.allow=always', 'submodule', 'add',
str(source), SUB)
child = parent / SUB
parent_commits = {}
for label in ('A', 'B'):
git(child, 'checkout', '-q', '--detach', child_commits[label])
git(parent, 'add', '--', 'README.txt', '.gitmodules', SUB)
git(parent, 'commit', '-q', '-m', 'Record engine ' + label)
parent_commits[label] = output(parent, 'rev-parse', 'HEAD')
report['authored_commits'] = {'parent': parent_commits, 'child': child_commits}
def snapshot():
require((child / '.git').is_file(), 'initialized owned submodule required')
head = output(parent, 'rev-parse', 'HEAD')
tree = output(parent, 'ls-tree', 'HEAD', '--', SUB).split()
index = output(parent, 'ls-files', '--stage', '--', SUB).split()
require(len(tree) == 4 and tree[:2] == ['160000', 'commit'] and tree[3] == SUB,
'single gitlink tree entry')
require(len(index) == 4 and index[0] == '160000' and index[2:] == ['0', SUB],
'single stage-zero gitlink index entry')
return {'parent_head': head, 'tree_gitlink': tree[2], 'index_gitlink': index[1],
'child_head': output(child, 'rev-parse', 'HEAD'),
'child_file': (child / 'engine.txt').read_text(),
'child_status': output(child, 'status', '--porcelain=v1', '--untracked-files=all'),
'parent_status': output(parent, 'status', '--porcelain=v1', '--ignore-submodules=none'),
'hidden_status': output(parent, 'status', '--porcelain=v1', '--ignore-submodules=all'),
'submodule_status': git(parent, 'submodule', 'status', '--', SUB)['stdout'],
'cached_status': git(parent, 'submodule', 'status', '--cached', '--', SUB)['stdout']}
def record(identity, label, expected_child, expected_index, expected_text, accepted, decision):
state = snapshot()
require(state['parent_head'] == parent_commits[label], identity + ' parent HEAD')
require(state['tree_gitlink'] == child_commits[label], identity + ' committed gitlink')
require(state['child_head'] == child_commits[expected_child], identity + ' child HEAD')
require(state['index_gitlink'] == child_commits[expected_index], identity + ' index gitlink')
require(state['child_file'] == expected_text, identity + ' literal file oracle')
matches = (state['tree_gitlink'] == state['index_gitlink'] == state['child_head']
and state['child_file'] == PAYLOAD[label]
and state['child_status'] == state['parent_status'] == '')
require(matches == accepted, identity + ' gate result')
report['cases'].append({'case': identity, 'state': state,
'matches_contract': matches, 'decision': decision, 'status': 'PASS'})
return state
git(parent, 'checkout', '-q', '--detach', '--recurse-submodules', parent_commits['A'])
record('G01', 'A', 'A', 'A', PAYLOAD['A'], True, 'ACCEPT_BASELINE')
checkout = git(parent, 'checkout', '-q', '--detach', '--no-recurse-submodules', parent_commits['B'])
record('G02', 'B', 'A', 'B', PAYLOAD['A'], False, 'REPAIR_SUBMODULE_REVISION')
state = record('G03', 'B', 'A', 'B', PAYLOAD['A'], False, 'HOLD_HIDDEN_MISMATCH')
require(checkout['returncode'] == 0 and state['hidden_status'] == '', 'G03 misleading green view')
require(state['submodule_status'].startswith('+') and child_commits['B'] in state['cached_status'],
'G03 actual versus cached status')
git(parent, 'submodule', 'update', '--checkout', '--no-fetch', '--', SUB)
record('G04', 'B', 'B', 'B', PAYLOAD['B'], True, 'ACCEPT_EXPLICIT_UPDATE')
detached = git(child, 'symbolic-ref', '-q', 'HEAD', allowed=(1,))
require(detached['stdout'] == '', 'G04 detached child HEAD')
git(parent, 'checkout', '-q', '--detach', '--recurse-submodules', parent_commits['A'])
record('G05', 'A', 'A', 'A', PAYLOAD['A'], True, 'ACCEPT_RECURSIVE_CHECKOUT')
(child / 'engine.txt').write_text('owned-local-edit\n')
git(parent, 'submodule', 'update', '--checkout', '--no-fetch', '--', SUB)
dirty = record('G06', 'A', 'A', 'A', 'owned-local-edit\n', False, 'HOLD_LOCAL_EDIT')
blocked = git(parent, 'checkout', '-q', '--detach', '--recurse-submodules', parent_commits['B'],
allowed=(1,))
after = record('G07', 'A', 'A', 'A', 'owned-local-edit\n', False, 'HOLD_CHECKOUT_REFUSED')
require(after == dirty and blocked['stderr'] != '', 'G07 bounded state preservation')
# Undo only this fixture's known edit; never repair an existing checkout this way.
(child / 'engine.txt').write_text(PAYLOAD['A'])
git(child, 'checkout', '-q', '--detach', child_commits['B'])
git(parent, 'add', '--', SUB)
git(child, 'checkout', '-q', '--detach', child_commits['A'])
require(output(child, 'rev-parse', 'HEAD') == child_commits['A'], 'G08 starts at child A')
git(parent, 'submodule', 'update', '--checkout', '--no-fetch', '--', SUB)
record('G08', 'A', 'B', 'B', PAYLOAD['B'], False, 'HOLD_STAGED_GITLINK')
# Restore only the gitlink staging change authored immediately above.
git(parent, 'restore', '--staged', '--', SUB)
git(parent, 'submodule', 'update', '--checkout', '--no-fetch', '--', SUB)
record('G09', 'A', 'A', 'A', PAYLOAD['A'], True, 'ACCEPT_RECONCILED_HEAD')
require([c['case'] for c in report['cases']] == ['G%02d' % i for i in range(1, 10)],
'complete case inventory')
require(not root.exists(), 'normal temporary cleanup')
report.update(status='PASS_9_CASES', normal_temporary_cleanup_completed=True)
if name == '__main__':
report = {'status': 'HOLD', 'started_utc': datetime.now(timezone.utc).isoformat(),
'python': platform.python_version(), 'platform': platform.platform(), 'uid': os.getuid(),
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'cases': [], 'commands': []}
try:
run(report)
except Exception as error:
report.update(error=type(error).__name__, detail=str(error))
report['finished_utc'] = datetime.now(timezone.utc).isoformat()
print(json.dumps(report, indent=2))
sys.exit(0 if report['status'] == 'PASS_9_CASES' else 1)The supplied preparation report recorded PASS_9_CASES on November 3, 2026, beginning at 07:50:32.707570 UTC and ending at 07:50:35.049135 UTC. Its recorded environment was Git 2.51.1, CPython 3.12.14, Linux-6.18.57-x86_64-with-glibc2.39, UID 0, with /usr/local/bin/git as the executable. The supplied source digest was 28208f3c2a9735c3e66092276dc6c7c404f2a30137856f0155602d2d7fd43688; the reported executable digest was 2dae8066ef4d6a926561b4b0eaca7b458d4f5b56bb83760656e6baa7fc3e974f.
Those values identify the preparation evidence. A digest identifies bytes; it does not attest that particular bytes were executed. No independent rerun is claimed here. The rolling online manuals were examined separately, and no claim depends on directly reading a version-specific Git 2.51.1 online manual.
The fixture's oracle is independent of observed state. PAYLOAD fixes literal A/B text before the cases run; child_commits and parent_commits capture authored identities; record() then checks parent identity, committed gitlink, index gitlink, child HEAD, and literal text against those expectations. Every Git invocation stores its arguments, directory, stdout, stderr, and return code. Expected refusal is represented explicitly through allowed=(1,), rather than being discarded as an incidental error.
The final inventory must contain G01 through G09 in order. PASS_9_CASES therefore means all nine expected observations, including deliberately rejected states, were reproduced by the supplied preparation run. It does not mean all nine checkout states are acceptable.
Generated object IDs are not portable expected values. In particular, .gitmodules records the temporary absolute source path, so parent commit IDs may differ across separate runs. Compare relationships and authored labels, not hashes copied from another JSON report.
Reproduce the successful nonrecursive mismatch
G01 establishes the accepted baseline: parent A, committed gitlink A, index gitlink A, child HEAD A, literal engine-A\n, and no tracked child or parent status finding.
G02 then runs:
git checkout -q --detach --no-recurse-submodules <parent-B>The parent checkout succeeds. Parent HEAD is B. Parent B's committed tree records child B, and its freshly selected index also records B. The initialized child, however, remains on A and continues to supply engine-A\n.
That is exactly the behavior documented for explicit no-recursion: Git says that when --no-recurse-submodules is used, submodule working trees are not updated. A successful parent checkout and a Git checkout submodule not updated condition are therefore compatible observations.
This is a clean revision mismatch, not a dirty-child case. The child file matches child A's authored content; it has not been locally edited. The bounded decision is REPAIR_SUBMODULE_REVISION because the intended B target is known, the tree and index agree on B, and the owned child is clean. The repair still needs explicit execution and readback; checkout's zero status did not perform either.
Show how a status view can conceal the mismatch
G03 makes no mutation. It observes the G02 state through two deliberately selected status views.
With:
git status --porcelain=v1 --ignore-submodules=nonethe parent reports the child mismatch. Git documents none as considering a submodule modified when its HEAD differs from the superproject's recorded commit, as well as when relevant child worktree changes exist.
With:
git status --porcelain=v1 --ignore-submodules=allthe fixture receives an empty report. Git documents all as hiding all submodule changes. The significant statement is not “status is green”; it is “this specifically restricted status view emitted no entry.”
The manual describes all as the default value for the --ignore-submodules option when that option is supplied without a value. This article does not generalize that sentence into a claim about every ordinary git status invocation, because the fixture does not rely on an implicit status policy.
Compare actual and cached submodule status
G03 also compares:
git submodule status -- modules/engine
git submodule status --cached -- modules/engineIn the supplied observation, ordinary submodule status begins with +, meaning the currently checked-out child commit does not match the containing index target. The child identity shown is A. The cached form reports B.
That interpretation follows the submodule manual: ordinary status reports the currently checked-out commit and prefixes + on mismatch with the containing repository's index, while --cached reports the recorded target instead.
The prefix and identifier answer different questions. Do not parse the cached B as if it were a readback of the child. The fixture retains raw stdout in its command records because prefixes and whitespace can matter even though its convenience snapshot helper strips surrounding whitespace.
Update the clean child and verify the result
G04 starts from the clean B/B/A state and issues:
git submodule update --checkout --no-fetch -- modules/engineAll required objects are already local. --no-fetch therefore makes the no-network boundary explicit; Git documents it as preventing update from fetching new objects. --checkout selects the checkout update procedure, which the manual describes as checking out the recorded submodule commit on detached HEAD.
The supplied observation then reads back parent B, committed gitlink B, index gitlink B, child HEAD B, engine-B\n, empty child status, and empty parent status under --ignore-submodules=none. A separate git symbolic-ref -q HEAD inside the child returns status 1 with no stdout, confirming detached child HEAD in this case.
G05 provides the complementary control:
git checkout -q --detach --recurse-submodules <parent-A>The resulting accepted state is A/A/A with engine-A\n. Git documents recursive checkout as updating active submodules according to the recorded commit and detaching child HEAD.
Neither accepted result means “use this flag and skip verification.” Both G04 and G05 are accepted because the operation is followed by the same parent, tree, index, child, content, and status readback. The experiment does not demonstrate --init, missing-object acquisition, nested recursion, or alternate update procedures.
Hold local edits even when every commit ID agrees
G06 begins in the accepted A/A/A state and replaces engine.txt with the fixture-owned literal:
owned-local-editincluding its terminal newline. Parent HEAD remains A. The committed gitlink remains A. The index gitlink remains A. Child HEAD remains A. Revision identity is perfectly aligned, yet the tracked child file no longer equals the authored A payload.
The fixture then runs the same-commit command:
git submodule update --checkout --no-fetch -- modules/engineIt returns successfully and leaves the local edit in place in the supplied observation. This is consistent with the documented distinction around forced update: the manual says --force makes checkout happen even when the index target already matches the checked-out child, and can throw away local changes under the checkout procedure. The fixture intentionally does not use --force.
The important conclusion is bounded to the observation: ordinary same-target update did not sanitize this working tree. Matching commit IDs therefore do not prove an unchanged tracked file, and a successful diagnostic command does not authorize destruction of pending work.
The decision is HOLD_LOCAL_EDIT. Ownership of the local change must be resolved independently of revision alignment.
Bound what the checkout refusal proves
G07 next attempts recursive checkout of parent B while that tracked child edit remains. Git's checkout manual says recursive checkout fails when local submodule modifications would be overwritten unless force is used. The supplied G07 command returns status 1 with nonempty stderr.
The fixture compares its entire enumerated snapshot before and after that one refusal. The compared fields are parent HEAD, committed gitlink, index gitlink, child HEAD, child file text, child status, parent status under none, hidden parent status under all, ordinary submodule status, and cached submodule status. They match the G06 snapshot.
That is useful bounded preservation evidence. It does not prove that every administrative byte stayed unchanged, that Git provides transactional atomicity across multiple children, or that an interrupted operation would leave the same evidence. The real operational decision remains HOLD_CHECKOUT_REFUSED: pending work is preserved while its owner decides how it may be reconciled.
Prove that update can follow the index instead of HEAD
G08 is the experiment's most important comparison beyond the original nonrecursive mismatch.
Parent HEAD remains A, so the committed tree still records child A. The fixture first removes only its own known owned-local-edit\n by rewriting the previously authored A literal. It then checks out child B and stages the submodule path:
git add -- modules/engineAt that point the superproject tree still says A, while the index says B. This is exactly the kind of separation discussed in Refonte Learning's article on the difference between index and committed state: staged state and committed state can describe different candidates. The present case is a gitlink, not an ignore-rule repair.
Git's concise submodule documentation often uses the phrase “commit recorded in the superproject” for update behavior, while elsewhere on the same page it explicitly describes the “commit specified in the index of the containing repository” when discussing checkout-update forcing. The G08 observation is therefore especially useful operational evidence for Git 2.51.1: the staged gitlink must be inspected separately rather than assuming parent HEAD alone controls the update target.
Start the child at A before requesting the indexed target
After staging B, the fixture deliberately returns the child to A and verifies that git rev-parse HEAD equals the authored child-A commit. This matters causally. Without that control, an update performed while the child was already at B could not demonstrate which target Git selected.
Only then does it run:
git submodule update --checkout --no-fetch -- modules/engineThe supplied observation moves the child from A to B. The resulting state is:
parent HEAD: A
committed gitlink: A
index gitlink: B
child HEAD: B
tracked text: engine-B\nThat transition demonstrates that, in this fixture and observed Git 2.51.1 environment, update followed the staged/indexed B target even though the selected parent tree still recorded A.
The operation's success does not approve B. A staged gitlink can represent intentional future work, an incomplete change, or something requiring review. Under this article's declared committed-A contract, the mismatch is HOLD_STAGED_GITLINK.
This is the practical staged submodule commit mismatch: do not infer “child should be A” merely from parent HEAD A, and do not infer “B is approved” merely because git submodule update materialized B. The staged target and committed target are separate approval questions.
Reconcile only the staging change the fixture owns
G09 resolves the specific staging change created immediately in G08:
git restore --staged -- modules/engine
git submodule update --checkout --no-fetch -- modules/engineThe first command restores the index entry for the submodule path from HEAD, taking the staged gitlink from B back to A. That operation changes the containing index; it does not itself move the child from B.
The second command is therefore necessary in this experiment. After it runs, the readback shows parent A, committed gitlink A, index gitlink A, child HEAD A, and engine-A\n, with the relevant statuses empty. The decision is ACCEPT_RECONCILED_HEAD.
The authorization boundary is critical. The fixture can perform that restoration because it knows exactly which staging change it created moments earlier and which literal child data it authored. Its comments explicitly warn against turning the same operation into a generic repair for an existing checkout.
In a real repository, determine four things before mutation: who owns the staged gitlink change, whether the staged target is intended, whether the child contains pending work, and what preservation obligations apply. If those answers are unresolved, the correct decision is HOLD.
Do not convert this playbook into an automatic --force, reset --hard, or clean recipe. Those commands would answer a different question (how to overwrite state) before the repository and work owners have decided whether overwriting is permitted. This experiment tests neither stash-based preservation nor remote recovery, so neither should be presented as a validated procedure here.
Reconcile every case with the declared contract
The complete ledger keeps diagnostic success separate from checkout acceptance. “Parent A/B” means the separately authored parent identity whose committed tree records child A/B. Every displayed text literal includes its terminal newline.
Case | Committed target / index / child | Tracked text | Bounded decision |
G01 | A / A / A | engine-A\n | ACCEPT_BASELINE |
G02 | B / B / A | engine-A\n | REPAIR_SUBMODULE_REVISION |
G03 | B / B / A | engine-A\n; mismatch hidden only by selected status view | HOLD_HIDDEN_MISMATCH |
G04 | B / B / B | engine-B\n | ACCEPT_EXPLICIT_UPDATE |
G05 | A / A / A | engine-A\n | ACCEPT_RECURSIVE_CHECKOUT |
G06 | A / A / A | owned-local-edit\n | HOLD_LOCAL_EDIT |
G07 | A / A / A | owned-local-edit\n; checkout refused | HOLD_CHECKOUT_REFUSED |
G08 | A / B / B | engine-B\n after A-to-B update | HOLD_STAGED_GITLINK |
G09 | A / A / A | engine-A\n after authorized fixture reconciliation | ACCEPT_RECONCILED_HEAD |
Only G01, G04, G05, and G09 satisfy the authored bounded contract. G02 is deliberately diagnosable and repairable, but it is not accepted before repair. G03 through G08 demonstrate various reasons that successful commands, matching identifiers, or intentionally restricted reports are insufficient.
PASS_9_CASES means the fixture's nine expected observations all passed. It includes successful verification of expected rejections. That status is not equivalent to a release approval.
Acceptance also depends on the independently selected parent identity, not only the fixture's matches_contract boolean. An incomplete case inventory, unexpected Git return code, contradictory parent or child identities, missing JSON, or malformed report is incomplete evidence rather than an invitation to infer a favorable result.
Turn evidence into an owned operational decision
A practical gate can reduce the nine cases to four evidence patterns without erasing ownership.
Evidence | Bounded action |
Approved parent is known; tree and index agree; clean child is at the wrong revision | REPAIR_SUBMODULE_REVISION, then rerun all readback |
Parent/tree/index/child IDs agree, but tracked child content is locally modified | HOLD for work-owner preservation decision |
Committed gitlink and staged gitlink disagree | HOLD until the intended target is reconciled |
Required evidence is missing, unsupported, contradictory, or command behavior is unexpected | HOLD |
Acceptance requires more than a Git submodule gitlink validation equality check. The expected parent must be independently approved; its committed gitlink must be correct; the staged gitlink must not introduce an unauthorized target; the child HEAD must match; known tracked content must match the declared oracle; and relevant status evidence must show no pending tracked work within the qualified scope.
The fixture's ASCII/LF readback is intentionally narrow. Reading engine.txt as a Python text string validates these known literals; it is not a general binary-integrity mechanism.
Keep approval separate from the test result
Assign ownership explicitly. Repository ownership supplies the approved parent and decides whether a staged gitlink is intended. Work ownership decides what may happen to pending child edits. Automation ownership defines the exact Git invocations, executable, environment controls, and captured evidence. Review ownership examines the complete ledger.
One person or team can hold several roles. The logical separation still matters: automation cannot discover a mismatch and then redefine the expected target to match the observation merely to produce green output.
Similarly, labels such as ACCEPT_BASELINE are synthetic case decisions inside this controlled contract. They are not evidence that an organization approved a deployment, artifact, or release.
Retain evidence and requalify the checkout before handoff
Retain the JSON report, the exact executed source or its independently verified identity, complete command records, full generated parent and child object IDs, Git and Python versions, executable identity where required, timestamps, and the exclusions under which the result was interpreted.
The supplied preparation record says normal temporary cleanup completed. That means the fixture deleted its owned temporary repositories after normal execution while retaining their observations in JSON. It is not a crash journal. If execution stops before valid JSON is produced, a partial directory or malformed report should be treated as incomplete evidence rather than reconstructed into a pass.
The experiment also does not qualify ordinary-user permission behavior because the supplied run used UID 0. It does not test concurrent writers, hostile repository containment, ignored-file inventories, nested children, inactive or uninitialized submodules, missing-object fetch, network Git, filters, LFS, sparse checkout, index flags, or arbitrary filenames.
Downstream artifact selection is another contract. Refonte Learning's article on release archive selection as a separate check demonstrates that tracked source presence does not by itself determine what enters a Git archive. This submodule experiment produces no archive and certifies no compiled output.
Requalify after changing the Git executable, relevant wrapper arguments, approved parent, staged target, child work, or configuration that can affect the tested commands. The rolling manuals examined here show different documentation-update versions (checkout 2.55.0, submodule 2.54.0, submodules guide 2.52.0, status 2.53.0, ls-tree 2.42.0, and ls-files 2.55.0), so those pages must not be mistaken for a direct execution record of Git 2.51.1.
Finally, this is a point-in-time checkout qualification. It does not create an immutable interval between checking and a later consumer. Handoff ownership must prevent or account for later writers rather than replacing that requirement with a bit-for-bit reproducibility claim.
Develop practical DevOps repository judgement
The durable skill is learning to separate identities that happen to coincide in the happy path. Parent HEAD identifies the selected superproject commit. Its tree gitlink identifies the committed child target. The containing index can stage a different gitlink. The initialized child has an actual HEAD. Its tracked worktree can still contain pending edits even when every commit ID agrees.
Refonte Learning's DevOps Engineering program lists Git and GitHub, Linux fundamentals and scripting, CI/CD, Docker and Kubernetes, Terraform, AWS/Azure/GCP, monitoring and logging, and a capstone among its published foundations. The page states a three-month period and 12–14 hours per week, names an educational mentor, and describes virtual internship opportunities while also using “Potential Internship” wording. Admission states that applicants must be working toward a bachelor's or higher-level degree; successful completion is associated with a Training Certificate and a Certificate of Internship.
The published page does not establish that this specific submodule fixture or advanced gitlink workflow is part of the syllabus, so no such claim is made here.
For the operational decision demonstrated above, the rule is complete: select the approved parent independently, reconcile committed and staged targets, read the actual child repository and tracked state, and accept the checkout only when those observations satisfy the declared contract.
