diff --git a/.claude/scripts/sync.sh b/.claude/scripts/sync.sh index 7bef1506..f63779cc 100755 --- a/.claude/scripts/sync.sh +++ b/.claude/scripts/sync.sh @@ -145,6 +145,50 @@ resolve_submodule_collisions() { [ "$moved" -eq 1 ] && return 0 || return 1 } +# Advance submodules to the parent's pinned gitlinks after a parent pull, WITHOUT +# ever clobbering a submodule that has local work. The pristine/pinned state of a +# submodule is a CLEAN, DETACHED HEAD; a developer actively working in a submodule +# checks out a BRANCH and/or has uncommitted edits. Those we skip entirely so an +# in-progress feature inside a submodule survives a parent sync. +# +# (Friction 2026-06-22: an unguarded `git submodule update --init --recursive` +# after the parent rebase repeatedly reset guru-rmm to its pinned commit — detaching +# HEAD and discarding a pushed-elsewhere feature branch + commits mid-build. The +# Phase-1a init guard at the top did not cover this post-rebase reconcile path.) +# +# Protected-submodule notices go to stderr; git's own output goes to stdout so the +# caller can capture it for failure reporting. Returns git's rc (0 if nothing to do). +submodule_update_safe() { + [ -f ".gitmodules" ] || return 0 + local p safe=() prot=() + while read -r p; do + [ -n "$p" ] || continue + if [ -e "$p/.git" ]; then + # On a branch (not the pristine detached pin) -> someone is working here. + if git -C "$p" symbolic-ref -q HEAD >/dev/null 2>&1; then + prot+=("$p [branch $(git -C "$p" rev-parse --abbrev-ref HEAD 2>/dev/null)]") + continue + fi + # Uncommitted changes -> protect. + if [ -n "$(git -C "$p" status --porcelain 2>/dev/null)" ]; then + prot+=("$p [uncommitted changes]") + continue + fi + fi + # Clean detached HEAD, or not yet populated (fresh clone) -> safe to update. + safe+=("$p") + done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null | awk '{print $2}') + + local item + for item in "${prot[@]}"; do + echo -e "${YELLOW}[INFO]${NC} sync: leaving submodule with local work untouched: ${item}" >&2 + done + + [ "${#safe[@]}" -eq 0 ] && return 0 + git submodule update --init --recursive --quiet -- "${safe[@]}" 2>&1 + return $? +} + # Machine + timestamp if [ -n "$COMPUTERNAME" ]; then MACHINE="$COMPUTERNAME" @@ -522,14 +566,14 @@ if [ "$INCOMING_COUNT" -gt 0 ]; then # raw "fatal: Unable to checkout" reads alarmingly right before a clean resolve. # Only surface the raw output if the resolve+retry below also fails. set +e - SUB_OUT=$(git submodule update --init --recursive --quiet 2>&1) + SUB_OUT=$(submodule_update_safe) SUB_RC=$? set -e if [ "$SUB_RC" -ne 0 ]; then # Move any untracked files colliding with the incoming commit aside, retry once. if resolve_submodule_collisions; then set +e - SUB_OUT=$(git submodule update --init --recursive --quiet 2>&1) + SUB_OUT=$(submodule_update_safe) SUB_RC=$? set -e fi