diff --git a/.claude/scripts/sync.sh b/.claude/scripts/sync.sh index d54c86e..12a5bb6 100755 --- a/.claude/scripts/sync.sh +++ b/.claude/scripts/sync.sh @@ -109,6 +109,42 @@ BOTID return 0 } +# When the parent's gitlinks point a submodule at a commit that TRACKS a file the +# submodule currently holds as UNTRACKED, `git submodule update` aborts with +# "untracked working tree files would be overwritten by checkout". Non-fatal, but it +# leaves the pointer stale and recurs every sync (seen with guru-rmm +# docs/RMM_THOUGHTS.md). Resolve NON-DESTRUCTIVELY: for each submodule, find untracked +# files that collide with the target gitlink commit's tree and rename them aside +# (content preserved) so checkout can proceed. Returns 0 if it moved anything (caller +# should retry the update), 1 if there was nothing to do. +resolve_submodule_collisions() { + [ -f ".gitmodules" ] || return 1 + local moved=0 subpath target untracked stamp dest + local subpaths=() + stamp=$(date -u "+%Y%m%dT%H%M%SZ") + while read -r p; do [ -n "$p" ] && subpaths+=("$p"); done \ + < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null | awk '{print $2}') + for subpath in "${subpaths[@]}"; do + [ -e "$subpath/.git" ] || continue + # Target gitlink commit the parent now expects for this submodule. + target=$(git ls-tree HEAD -- "$subpath" 2>/dev/null | awk '$2=="commit"{print $3}') + [ -n "$target" ] || continue + # Need the target commit present locally to know which paths it tracks. + git -C "$subpath" cat-file -e "$target" 2>/dev/null || continue + # Untracked files in the submodule that the target commit also tracks = the + # exact set that would block checkout. Move only those aside. + while IFS= read -r -d '' untracked; do + git -C "$subpath" cat-file -e "${target}:${untracked}" 2>/dev/null || continue + dest="${untracked}.synced-aside-${stamp}" + if mv "$subpath/$untracked" "$subpath/$dest" 2>/dev/null; then + echo -e "${YELLOW}[WARNING]${NC} ${subpath}: untracked '${untracked}' collided with incoming commit; preserved as '${dest}' (reconcile manually if it held local edits)." + moved=1 + fi + done < <(git -C "$subpath" ls-files --others --exclude-standard -z 2>/dev/null) + done + [ "$moved" -eq 1 ] && return 0 || return 1 +} + # Machine + timestamp if [ -n "$COMPUTERNAME" ]; then MACHINE="$COMPUTERNAME" @@ -477,7 +513,19 @@ if [ "$INCOMING_COUNT" -gt 0 ]; then SUB_RC=${PIPESTATUS[0]} set -e if [ "$SUB_RC" -ne 0 ]; then - echo -e "${YELLOW}[WARNING]${NC} One or more submodules failed to update — likely a transient dead ref. Parent repo is current; investigate the submodule manually if a pointer is wrong." + # Most common recurring cause: an untracked file in a submodule colliding + # with the incoming gitlink commit. Resolve non-destructively and retry once. + if resolve_submodule_collisions; then + set +e + git submodule update --init --recursive --quiet 2>&1 | grep -v '^$' || true + SUB_RC=${PIPESTATUS[0]} + set -e + fi + if [ "$SUB_RC" -ne 0 ]; then + echo -e "${YELLOW}[WARNING]${NC} One or more submodules failed to update — likely a transient dead ref. Parent repo is current; investigate the submodule manually if a pointer is wrong." + else + echo -e "${GREEN}[OK]${NC} Submodules reconciled (set aside colliding untracked file(s))." + fi fi fi