Skip to content

Commit 835d155

Browse files
NagyViktNagyVikt
andauthored
feat(gx): auto-sync submodule working dirs on gx setup for monorepo shops (#557)
When `<repoRoot>/.gitmodules` exists, `gx setup` now: - Sets `pull.recurseSubmodules=true` so `git pull` auto-updates submodule working dirs (only when unset; pre-existing values are preserved). - Sets `fetch.recurseSubmodules=on-demand` so submodule commits are fetched as parent pointers move. - Runs `git submodule update --init --recursive` once to snap working dirs to the parent index — fixes the screenshot case where the parent is `0↓ 2↑` (pointer bumps already in the index, working dir stale). `submodule.recurse` is intentionally NOT touched — push behavior stays default, avoiding cascade with the Codex external-remote approval block (prior incident S715/S716). Repos without `.gitmodules` are untouched (helper returns `[]` early). Files: - src/git/index.js — add `ensureSubmoduleAutoSync` helper. - src/cli/main.js — call it from `runSetupBootstrapInternal`. Smoke-tested via dry-run against `medusa-shops/compastor`: - would-set git config pull.recurseSubmodules - would-set git config fetch.recurseSubmodules - would-sync git submodule update --init --recursive Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 3c3aac4 commit 835d155

4 files changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-11
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# submodule-auto-sync-on-setup (T1)
2+
3+
Branch: `agent/claude/submodule-auto-sync-on-setup-2026-05-11-13-32`
4+
5+
## Why
6+
7+
Monorepo shops (`medusa-shops/lifted`, `compastor`, `koronakert`, `teherguminet`, …) pin `apps/backend` and `apps/storefront` as independent submodule repos. When an agent commits inside a submodule (in a separate `.omc/agent-worktrees/...` lane) and the parent PR merges, the user's primary checkout never updates the submodule working dir until they manually run `git submodule update --init --recursive`. Symptom: `pnpm storefront:dev` shows stale code, looking like the agent did nothing.
8+
9+
`submodule.recurse=true` would fix this but also affects `push` and cascades into the Codex external-remote approval block (S715/S716). Targeted pair is safer and pull-only.
10+
11+
## Change
12+
13+
In `runSetupBootstrapInternal` (src/cli/main.js), after `ensureSetupProtectedBranches`, call a new `ensureSubmoduleAutoSync(repoRoot, dryRun)` helper (src/git/index.js).
14+
15+
The helper:
16+
1. Returns `[]` immediately if `<repoRoot>/.gitmodules` is absent — non-monorepo repos are untouched.
17+
2. For each of `pull.recurseSubmodules=true` and `fetch.recurseSubmodules=on-demand`: only writes when the key is unset locally. Pre-existing values are preserved (the user may have a reason).
18+
3. Runs `git submodule update --init --recursive` once to snap working dirs to the parent index — this is the part that fixes the screenshot case where the parent is `0↓ 2↑` (pointer bumps already in the index, working dir stale).
19+
4. Operations show up in the normal `Setup/install` output as `would-set` / `set` / `unchanged` / `synced`.
20+
21+
`submodule.recurse` is intentionally not touched — push behavior stays default.
22+
23+
## Files
24+
25+
- `src/git/index.js` — add `ensureSubmoduleAutoSync` + `SUBMODULE_AUTO_SYNC_CONFIGS`, export.
26+
- `src/cli/main.js` — import + call from `runSetupBootstrapInternal`.
27+
28+
## Verification
29+
30+
Dry-run against `/home/deadpool/Documents/medusa-shops/compastor`:
31+
32+
```
33+
- would-set git config pull.recurseSubmodules (true (auto-update submodule working dirs on `git pull`))
34+
- would-set git config fetch.recurseSubmodules (on-demand (fetch submodule commits as parent pointers move))
35+
- would-sync git submodule update --init --recursive (snap submodule working dirs to parent index)
36+
```
37+
38+
Idempotency: `readGitConfig` returns `''` only when truly unset, so any pre-existing value (including `false`) is preserved and surfaced as `unchanged`.
39+
40+
## Follow-ups (not in this PR)
41+
42+
- Phase B (T1): standalone `gx submodule sync` verb that fetches each submodule, bumps the parent pointer to the tracked branch's remote tip, commits.
43+
- Phase C (T2): workspace-aware `gx branch finish` — fans out child PRs, bumps parent pointer, opens parent PR with dependency.
44+
45+
## Cleanup
46+
47+
- [x] Run dry-run smoke test on compastor — three new ops appear correctly.
48+
- [ ] Run: `gx branch finish --branch agent/claude/submodule-auto-sync-on-setup-2026-05-11-13-32 --base main --via-pr --wait-for-merge --cleanup`
49+
- [ ] Record PR URL + `MERGED` state in the completion handoff.
50+
- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`).

src/cli/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const {
8989
readConfiguredProtectedBranches,
9090
readProtectedBranches,
9191
ensureSetupProtectedBranches,
92+
ensureSubmoduleAutoSync,
9293
writeProtectedBranches,
9394
readGitConfig,
9495
resolveBaseBranch,
@@ -383,6 +384,9 @@ function runSetupBootstrapInternal(options) {
383384
installPayload.operations.push(
384385
ensureSetupProtectedBranches(installPayload.repoRoot, Boolean(options.dryRun)),
385386
);
387+
installPayload.operations.push(
388+
...ensureSubmoduleAutoSync(installPayload.repoRoot, Boolean(options.dryRun)),
389+
);
386390

387391
let parentWorkspace = null;
388392
if (options.parentWorkspaceView) {

src/git/index.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,70 @@ function writeProtectedBranches(repoRoot, branches) {
319319
gitRun(repoRoot, ['config', GIT_PROTECTED_BRANCHES_KEY, branches.join(' ')]);
320320
}
321321

322+
const SUBMODULE_AUTO_SYNC_CONFIGS = [
323+
{
324+
key: 'pull.recurseSubmodules',
325+
value: 'true',
326+
note: 'auto-update submodule working dirs on `git pull`',
327+
},
328+
{
329+
key: 'fetch.recurseSubmodules',
330+
value: 'on-demand',
331+
note: 'fetch submodule commits as parent pointers move',
332+
},
333+
];
334+
335+
function ensureSubmoduleAutoSync(repoRoot, dryRun) {
336+
const gitmodulesPath = path.join(repoRoot, '.gitmodules');
337+
if (!fs.existsSync(gitmodulesPath)) {
338+
return [];
339+
}
340+
341+
const operations = [];
342+
for (const { key, value, note } of SUBMODULE_AUTO_SYNC_CONFIGS) {
343+
const existing = readGitConfig(repoRoot, key);
344+
if (existing) {
345+
operations.push({
346+
status: 'unchanged',
347+
file: `git config ${key}`,
348+
note: `respected pre-existing value: ${existing}`,
349+
});
350+
continue;
351+
}
352+
if (!dryRun) {
353+
gitRun(repoRoot, ['config', key, value]);
354+
}
355+
operations.push({
356+
status: dryRun ? 'would-set' : 'set',
357+
file: `git config ${key}`,
358+
note: `${value} (${note})`,
359+
});
360+
}
361+
362+
if (dryRun) {
363+
operations.push({
364+
status: 'would-sync',
365+
file: 'git submodule update --init --recursive',
366+
note: 'snap submodule working dirs to parent index',
367+
});
368+
return operations;
369+
}
370+
371+
const result = gitRun(
372+
repoRoot,
373+
['submodule', 'update', '--init', '--recursive'],
374+
{ allowFailure: true },
375+
);
376+
operations.push({
377+
status: result.status === 0 ? 'synced' : 'failed',
378+
file: 'git submodule update --init --recursive',
379+
note: result.status === 0
380+
? 'submodule working dirs snapped to parent index'
381+
: `failed: ${(result.stderr || '').trim().split('\n')[0] || 'unknown'}`,
382+
});
383+
return operations;
384+
}
385+
322386
function readGitConfig(repoRoot, key) {
323387
const result = gitRun(repoRoot, ['config', '--get', key], { allowFailure: true });
324388
if (result.status !== 0) {
@@ -697,6 +761,7 @@ module.exports = {
697761
hasSignificantWorkingTreeChanges,
698762
readProtectedBranches,
699763
ensureSetupProtectedBranches,
764+
ensureSubmoduleAutoSync,
700765
writeProtectedBranches,
701766
readGitConfig,
702767
resolveBaseBranch,

0 commit comments

Comments
 (0)