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
|
[ "$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
|
# Machine + timestamp
|
||||||
if [ -n "$COMPUTERNAME" ]; then
|
if [ -n "$COMPUTERNAME" ]; then
|
||||||
MACHINE="$COMPUTERNAME"
|
MACHINE="$COMPUTERNAME"
|
||||||
@@ -522,14 +566,14 @@ if [ "$INCOMING_COUNT" -gt 0 ]; then
|
|||||||
# raw "fatal: Unable to checkout" reads alarmingly right before a clean resolve.
|
# raw "fatal: Unable to checkout" reads alarmingly right before a clean resolve.
|
||||||
# Only surface the raw output if the resolve+retry below also fails.
|
# Only surface the raw output if the resolve+retry below also fails.
|
||||||
set +e
|
set +e
|
||||||
SUB_OUT=$(git submodule update --init --recursive --quiet 2>&1)
|
SUB_OUT=$(submodule_update_safe)
|
||||||
SUB_RC=$?
|
SUB_RC=$?
|
||||||
set -e
|
set -e
|
||||||
if [ "$SUB_RC" -ne 0 ]; then
|
if [ "$SUB_RC" -ne 0 ]; then
|
||||||
# Move any untracked files colliding with the incoming commit aside, retry once.
|
# Move any untracked files colliding with the incoming commit aside, retry once.
|
||||||
if resolve_submodule_collisions; then
|
if resolve_submodule_collisions; then
|
||||||
set +e
|
set +e
|
||||||
SUB_OUT=$(git submodule update --init --recursive --quiet 2>&1)
|
SUB_OUT=$(submodule_update_safe)
|
||||||
SUB_RC=$?
|
SUB_RC=$?
|
||||||
set -e
|
set -e
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user