fix(sync): never reset a submodule with local work (branch/dirty)
Phase-3 post-rebase reconcile ran 'git submodule update --init --recursive' unconditionally, force-detaching every submodule to the parent's pinned gitlink and discarding any feature branch, commits, or uncommitted edits inside it. The Phase-1a init guard did not cover this path. New submodule_update_safe() advances ONLY submodules in the pristine pinned state (clean, detached HEAD) and skips any on a branch or with uncommitted changes, so in-progress submodule work survives a parent sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user