This commit is contained in:
David Schroeder
2026-07-25 16:47:09 -05:00
parent b324d31273
commit bd005495ce
5 changed files with 250 additions and 51 deletions
+4 -1
View File
@@ -19,10 +19,13 @@ updates are installed only when explicitly selected or requested with
The required iDSSYS Defaults repository is handled separately: it is The required iDSSYS Defaults repository is handled separately: it is
automatically refreshed before launch when its last successful check is more automatically refreshed before launch when its last successful check is more
than four hours old. If the remote is unavailable, the installed copy is used. than four hours old. If the remote is unavailable, the installed copy is used.
Updates are fast-forward-only. Local file changes, local-only commits, and
diverged histories are preserved and reported instead of being overwritten.
The Utilities menu can safely switch TA-ProxMenu between branches published on The Utilities menu can safely switch TA-ProxMenu between branches published on
its Git origin. Branch switching is refused when the installed repository has its Git origin. Branch switching is refused when the installed repository has
local changes. local changes or when the destination branch has commits that would be
discarded.
## Direct actions ## Direct actions
+1 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}" action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu' FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.7.25-29' VERS='2026.7.25-30'
noupdate=' ' noupdate=' '
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Shared non-destructive Git update helpers for TA-ProxMenu.
TAPM_GIT_WORKTREE_DIRTY() {
local repository="$1"
[[ -n "$(git -C "$repository" status --porcelain --untracked-files=normal 2>/dev/null)" ]]
}
TAPM_GIT_FETCH_BRANCH() {
local repository="$1"
local branch="$2"
local timeout_seconds="${3:-30}"
timeout "$timeout_seconds" git -C "$repository" fetch --prune origin \
"+refs/heads/${branch}:refs/remotes/origin/${branch}"
}
TAPM_GIT_RELATION() {
local repository="$1"
local local_ref="$2"
local remote_ref="$3"
local local_commit
local remote_commit
local_commit="$(git -C "$repository" rev-parse --verify "${local_ref}^{commit}" 2>/dev/null)" ||
return 1
remote_commit="$(git -C "$repository" rev-parse --verify "${remote_ref}^{commit}" 2>/dev/null)" ||
return 1
if [[ "$local_commit" == "$remote_commit" ]]; then
printf 'current\n'
elif git -C "$repository" merge-base --is-ancestor "$local_commit" "$remote_commit"; then
printf 'behind\n'
elif git -C "$repository" merge-base --is-ancestor "$remote_commit" "$local_commit"; then
printf 'ahead\n'
else
printf 'diverged\n'
fi
}
TAPM_GIT_BRANCH_STATE() {
local repository="$1"
local branch="$2"
local current_branch
[[ -d "${repository}/.git" ]] || {
printf 'unavailable\n'
return 1
}
current_branch="$(git -C "$repository" branch --show-current 2>/dev/null)"
if [[ -z "$current_branch" ]]; then
printf 'detached\n'
return 0
fi
if [[ "$current_branch" != "$branch" ]]; then
printf 'wrong-branch\n'
return 0
fi
if TAPM_GIT_WORKTREE_DIRTY "$repository"; then
printf 'dirty\n'
return 0
fi
TAPM_GIT_RELATION "$repository" HEAD "refs/remotes/origin/${branch}" || {
printf 'unavailable\n'
return 1
}
}
TAPM_GIT_FAST_FORWARD() {
local repository="$1"
local branch="$2"
git -C "$repository" merge --ff-only "refs/remotes/origin/${branch}"
}
+78 -19
View File
@@ -5,6 +5,7 @@
[ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc [ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc
source /opt/idssys/defaults/default.inc source /opt/idssys/defaults/default.inc
source /opt/idssys/ta-proxmenu/defaults.inc source /opt/idssys/ta-proxmenu/defaults.inc
source /opt/idssys/ta-proxmenu/inc/git-update.inc
ACTION_REQUESTED=0 ACTION_REQUESTED=0
[[ -n "${action:-}" ]] && ACTION_REQUESTED=1 [[ -n "${action:-}" ]] && ACTION_REQUESTED=1
@@ -744,16 +745,18 @@ UPDATE_CHECK_WORKER() {
WRITE_UPDATE_CACHE "unavailable" "$branch" "$local_commit" "" WRITE_UPDATE_CACHE "unavailable" "$branch" "$local_commit" ""
return return
fi fi
if TAPM_GIT_WORKTREE_DIRTY "$FOLDER"; then
remote_commit="$(git -C "$FOLDER" ls-remote origin "refs/heads/${branch}" 2>/dev/null | cut -f1)" WRITE_UPDATE_CACHE "dirty" "$branch" "$local_commit" ""
if [[ -z "$remote_commit" ]]; then return
status="unavailable"
elif [[ "$local_commit" == "$remote_commit" ]]; then
status="current"
else
status="available"
fi fi
if ! TAPM_GIT_FETCH_BRANCH "$FOLDER" "$branch" 30 >/dev/null 2>&1; then
WRITE_UPDATE_CACHE "unavailable" "$branch" "$local_commit" ""
return
fi
remote_commit="$(git -C "$FOLDER" rev-parse "refs/remotes/origin/${branch}" 2>/dev/null)"
status="$(TAPM_GIT_BRANCH_STATE "$FOLDER" "$branch")"
WRITE_UPDATE_CACHE "$status" "$branch" "$local_commit" "$remote_commit" WRITE_UPDATE_CACHE "$status" "$branch" "$local_commit" "$remote_commit"
} }
@@ -774,12 +777,14 @@ LOAD_UPDATE_STATUS() {
local cached_status local cached_status
local current_branch local current_branch
local current_commit local current_commit
local current_dirty=0
local now local now
UPDATE_STATUS='unknown' UPDATE_STATUS='unknown'
UPDATE_REMOTE_COMMIT='' UPDATE_REMOTE_COMMIT=''
current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)" current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
current_commit="$(git -C "$FOLDER" rev-parse HEAD 2>/dev/null)" current_commit="$(git -C "$FOLDER" rev-parse HEAD 2>/dev/null)"
TAPM_GIT_WORKTREE_DIRTY "$FOLDER" && current_dirty=1
now="$(date +%s)" now="$(date +%s)"
if [[ -r "$UPDATE_CACHE_FILE" ]]; then if [[ -r "$UPDATE_CACHE_FILE" ]]; then
@@ -789,7 +794,9 @@ LOAD_UPDATE_STATUS() {
(( now - checked_at < UPDATE_CACHE_SECONDS )) && (( now - checked_at < UPDATE_CACHE_SECONDS )) &&
[[ "$cached_branch" == "$current_branch" ]] && [[ "$cached_branch" == "$current_branch" ]] &&
[[ "$cached_local" == "$current_commit" ]] && [[ "$cached_local" == "$current_commit" ]] &&
[[ "$cached_status" =~ ^(current|available|unavailable)$ ]]; then [[ "$cached_status" =~ ^(current|behind|ahead|diverged|dirty|unavailable)$ ]] &&
{ [[ "$cached_status" == "dirty" && "$current_dirty" -eq 1 ]] ||
[[ "$cached_status" != "dirty" && "$current_dirty" -eq 0 ]]; }; then
UPDATE_STATUS="$cached_status" UPDATE_STATUS="$cached_status"
UPDATE_REMOTE_COMMIT="$cached_remote" UPDATE_REMOTE_COMMIT="$cached_remote"
return return
@@ -818,12 +825,21 @@ FORCE_UPDATE_CHECK() {
echo -e "${idsCL[Default]}" echo -e "${idsCL[Default]}"
case "$UPDATE_STATUS" in case "$UPDATE_STATUS" in
available) behind)
echo -e "${idsCL[LightYellow]}An update is available for the current branch.${idsCL[Default]}" echo -e "${idsCL[LightYellow]}An update is available for the current branch.${idsCL[Default]}"
;; ;;
current) current)
echo -e "${idsCL[Green]}TA-ProxMenu is current.${idsCL[Default]}" echo -e "${idsCL[Green]}TA-ProxMenu is current.${idsCL[Default]}"
;; ;;
ahead)
echo -e "${idsCL[LightYellow]}The current branch has local commits not on origin.${idsCL[Default]}"
;;
diverged)
echo -e "${idsCL[LightRed]}The current branch has diverged from origin; automatic update is disabled.${idsCL[Default]}"
;;
dirty)
echo -e "${idsCL[LightYellow]}The TA-ProxMenu repository has local file changes.${idsCL[Default]}"
;;
*) *)
echo -e "${idsCL[Red]}The update status could not be determined.${idsCL[Default]}" echo -e "${idsCL[Red]}The update status could not be determined.${idsCL[Default]}"
;; ;;
@@ -839,9 +855,18 @@ MENU_HEADER() {
LOAD_UPDATE_STATUS LOAD_UPDATE_STATUS
case "$UPDATE_STATUS" in case "$UPDATE_STATUS" in
available) behind)
version_display="${idsCL[LightYellow]}${VERS} ** UPDATE AVAILABLE **${idsCL[Default]}" version_display="${idsCL[LightYellow]}${VERS} ** UPDATE AVAILABLE **${idsCL[Default]}"
;; ;;
ahead)
version_display="${idsCL[LightYellow]}${VERS} ** LOCAL COMMITS **${idsCL[Default]}"
;;
diverged)
version_display="${idsCL[LightRed]}${VERS} ** BRANCH DIVERGED **${idsCL[Default]}"
;;
dirty)
version_display="${idsCL[LightYellow]}${VERS} ** LOCAL CHANGES **${idsCL[Default]}"
;;
checking) checking)
version_display="${idsCL[LightCyan]}${VERS} (checking for updates)${idsCL[Default]}" version_display="${idsCL[LightCyan]}${VERS} (checking for updates)${idsCL[Default]}"
;; ;;
@@ -1114,6 +1139,7 @@ SWITCH_SCRIPT_BRANCH() {
local target_branch="$1" local target_branch="$1"
local current_branch local current_branch
local choice local choice
local target_state=''
current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)" current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
if [[ "$target_branch" == "$current_branch" ]]; then if [[ "$target_branch" == "$current_branch" ]]; then
@@ -1136,14 +1162,36 @@ SWITCH_SCRIPT_BRANCH() {
echo echo
[[ "$choice" =~ ^[Yy]$ ]] || return [[ "$choice" =~ ^[Yy]$ ]] || return
if ! timeout 30 git -C "$FOLDER" fetch origin \ if ! TAPM_GIT_FETCH_BRANCH "$FOLDER" "$target_branch" 30; then
"refs/heads/${target_branch}:refs/remotes/origin/${target_branch}"; then
echo -e "${idsCL[Red]}Failed to fetch '${target_branch}' from origin.${idsCL[Default]}" echo -e "${idsCL[Red]}Failed to fetch '${target_branch}' from origin.${idsCL[Default]}"
ENTER2CONTINUE ENTER2CONTINUE
return return
fi fi
if git -C "$FOLDER" show-ref --verify --quiet "refs/heads/${target_branch}"; then if git -C "$FOLDER" show-ref --verify --quiet "refs/heads/${target_branch}"; then
target_state="$(
TAPM_GIT_RELATION "$FOLDER" "refs/heads/${target_branch}" \
"refs/remotes/origin/${target_branch}"
)" || {
echo -e "${idsCL[Red]}Could not compare local and remote '${target_branch}'.${idsCL[Default]}"
ENTER2CONTINUE
return
}
case "$target_state" in
ahead)
echo -e "${idsCL[LightYellow]}The local '${target_branch}' branch has commits not on origin.${idsCL[Default]}"
echo "Branch switching was refused to preserve those commits."
ENTER2CONTINUE
return
;;
diverged)
echo -e "${idsCL[LightRed]}The local '${target_branch}' branch has diverged from origin.${idsCL[Default]}"
echo "Branch switching was refused; manual Git review is required."
ENTER2CONTINUE
return
;;
esac
git -C "$FOLDER" switch "$target_branch" || { git -C "$FOLDER" switch "$target_branch" || {
ENTER2CONTINUE ENTER2CONTINUE
return return
@@ -1156,10 +1204,12 @@ SWITCH_SCRIPT_BRANCH() {
} }
fi fi
if ! git -C "$FOLDER" reset --hard "origin/${target_branch}"; then if [[ "$target_state" == "behind" ]]; then
echo -e "${idsCL[Red]}Failed to synchronize '${target_branch}' with origin.${idsCL[Default]}" if ! TAPM_GIT_FAST_FORWARD "$FOLDER" "$target_branch"; then
ENTER2CONTINUE echo -e "${idsCL[Red]}Failed to fast-forward '${target_branch}'; no commits were discarded.${idsCL[Default]}"
return ENTER2CONTINUE
return
fi
fi fi
rm -f "$UPDATE_CACHE_FILE" rm -f "$UPDATE_CACHE_FILE"
@@ -1233,12 +1283,21 @@ UTILITIES_MENU() {
while true; do while true; do
LOAD_UPDATE_STATUS LOAD_UPDATE_STATUS
case "$UPDATE_STATUS" in case "$UPDATE_STATUS" in
available) behind)
labels=("Install available update") labels=("Install available update")
;; ;;
current) current)
labels=("TA-ProxMenu is current") labels=("TA-ProxMenu is current")
;; ;;
ahead)
labels=("Local branch is ahead of origin")
;;
diverged)
labels=("Local branch has diverged from origin")
;;
dirty)
labels=("Local repository has uncommitted changes")
;;
checking) checking)
labels=("Update check in progress") labels=("Update check in progress")
;; ;;
@@ -1255,7 +1314,7 @@ UTILITIES_MENU() {
SELECT_MENU "Utilities" labels values SELECT_MENU "Utilities" labels values
case "$MENU_SELECTION" in case "$MENU_SELECTION" in
install_update) install_update)
if [[ "$UPDATE_STATUS" != "available" ]]; then if [[ "$UPDATE_STATUS" != "behind" ]]; then
echo -e "\n${idsCL[LightCyan]}No available update is currently detected.${idsCL[Default]}" echo -e "\n${idsCL[LightCyan]}No available update is currently detected.${idsCL[Default]}"
ENTER2CONTINUE ENTER2CONTINUE
continue continue
+90 -30
View File
@@ -6,11 +6,13 @@ DEFAULTS_CACHE_DIR='/var/cache/ta-proxmenu'
DEFAULTS_CHECK_FILE="${DEFAULTS_CACHE_DIR}/defaults-last-check" DEFAULTS_CHECK_FILE="${DEFAULTS_CACHE_DIR}/defaults-last-check"
DEFAULTS_CHECK_SECONDS=14400 DEFAULTS_CHECK_SECONDS=14400
source /opt/idssys/ta-proxmenu/inc/git-update.inc
AUTO_UPDATE_DEFAULTS() { AUTO_UPDATE_DEFAULTS() {
local checked_at=0 local checked_at=0
local local_commit local current_branch
local now local now
local remote_commit local state
mkdir -p "$DEFAULTS_CACHE_DIR" 2>/dev/null || true mkdir -p "$DEFAULTS_CACHE_DIR" 2>/dev/null || true
now="$(date +%s)" now="$(date +%s)"
@@ -41,22 +43,46 @@ AUTO_UPDATE_DEFAULTS() {
return return
fi fi
else else
if ! timeout 20 git -C "$DEFAULTS_REPOSITORY" fetch origin master \ current_branch="$(git -C "$DEFAULTS_REPOSITORY" branch --show-current 2>/dev/null)"
if [[ "$current_branch" != "master" ]]; then
echo "WARNING: iDSSYS Defaults is not on master; automatic update skipped." >&2
date +%s >"$DEFAULTS_CHECK_FILE"
return
fi
if TAPM_GIT_WORKTREE_DIRTY "$DEFAULTS_REPOSITORY"; then
echo "WARNING: iDSSYS Defaults has local changes; automatic update skipped." >&2
date +%s >"$DEFAULTS_CHECK_FILE"
return
fi
if ! TAPM_GIT_FETCH_BRANCH "$DEFAULTS_REPOSITORY" master 20 \
>/dev/null 2>&1; then >/dev/null 2>&1; then
echo "WARNING: Unable to check iDSSYS Defaults; using the installed copy." >&2 echo "WARNING: Unable to check iDSSYS Defaults; using the installed copy." >&2
return return
fi fi
local_commit="$(git -C "$DEFAULTS_REPOSITORY" rev-parse HEAD)" state="$(TAPM_GIT_BRANCH_STATE "$DEFAULTS_REPOSITORY" master)"
remote_commit="$(git -C "$DEFAULTS_REPOSITORY" rev-parse origin/master)" case "$state" in
if [[ "$local_commit" != "$remote_commit" ]]; then behind)
echo "Updating required iDSSYS Defaults..." echo "Updating required iDSSYS Defaults..."
if ! git -C "$DEFAULTS_REPOSITORY" reset --hard origin/master \ if ! TAPM_GIT_FAST_FORWARD "$DEFAULTS_REPOSITORY" master \
>/dev/null 2>&1; then >/dev/null 2>&1; then
echo "WARNING: Unable to update iDSSYS Defaults; using the installed copy." >&2
return
fi
;;
current)
;;
ahead)
echo "WARNING: iDSSYS Defaults has local commits not on origin; automatic update skipped." >&2
;;
diverged)
echo "WARNING: iDSSYS Defaults has diverged from origin; automatic update skipped." >&2
;;
*)
echo "WARNING: Unable to update iDSSYS Defaults; using the installed copy." >&2 echo "WARNING: Unable to update iDSSYS Defaults; using the installed copy." >&2
return return
fi ;;
fi esac
fi fi
date +%s >"$DEFAULTS_CHECK_FILE" date +%s >"$DEFAULTS_CHECK_FILE"
@@ -72,36 +98,67 @@ UPDATE_REPOSITORY() {
local repository="$1" local repository="$1"
local branch="$2" local branch="$2"
local label="$3" local label="$3"
local local_commit local current_branch
local remote_commit local state
cd "$repository" || return 1 if [[ ! -d "${repository}/.git" ]]; then
local_commit="$(git rev-parse HEAD 2>/dev/null)" || return 1 echo -e "${idsCL[Red]}${label} is not a Git repository.${idsCL[Default]}"
remote_commit="$(git ls-remote origin "refs/heads/${branch}" | cut -f1)"
if [ -z "$remote_commit" ]; then
echo -e "${idsCL[Red]}Could not find branch '${branch}' for ${label}${idsCL[Default]}"
return 1 return 1
fi fi
if [ "$local_commit" = "$remote_commit" ]; then current_branch="$(git -C "$repository" branch --show-current 2>/dev/null)"
echo -e "${idsCL[Green]}${label} is current (${branch})${idsCL[Default]}" if [[ -z "$current_branch" ]]; then
return 0 echo -e "${idsCL[Red]}${label} is in a detached HEAD state; update skipped.${idsCL[Default]}"
return 1
fi
if [[ "$current_branch" != "$branch" ]]; then
echo -e "${idsCL[Red]}${label} is on '${current_branch}', not '${branch}'; update skipped.${idsCL[Default]}"
return 1
fi
if TAPM_GIT_WORKTREE_DIRTY "$repository"; then
echo -e "${idsCL[LightYellow]}${label} has local changes; update skipped to preserve them.${idsCL[Default]}"
git -C "$repository" status --short
return 1
fi fi
echo -en "${idsCL[LightCyan]}Updating ${label} (${branch})...${idsCL[Default]}" if ! TAPM_GIT_FETCH_BRANCH "$repository" "$branch" 30 >/dev/null 2>&1; then
if git fetch origin "$branch" >/dev/null 2>&1 && echo -e "${idsCL[Red]}Could not fetch branch '${branch}' for ${label}.${idsCL[Default]}"
git reset --hard "origin/${branch}" >/dev/null 2>&1; then return 1
echo -e " ${idsCL[Green]}Done${idsCL[Default]}"
return 0
fi fi
echo -e " ${idsCL[Red]}Failed${idsCL[Default]}" state="$(TAPM_GIT_BRANCH_STATE "$repository" "$branch")"
return 1 case "$state" in
current)
echo -e "${idsCL[Green]}${label} is current (${branch}).${idsCL[Default]}"
return 0
;;
behind)
echo -en "${idsCL[LightCyan]}Updating ${label} (${branch})...${idsCL[Default]}"
if TAPM_GIT_FAST_FORWARD "$repository" "$branch" >/dev/null 2>&1; then
echo -e " ${idsCL[Green]}Done${idsCL[Default]}"
return 0
fi
echo -e " ${idsCL[Red]}Failed; local files were preserved.${idsCL[Default]}"
return 1
;;
ahead)
echo -e "${idsCL[LightYellow]}${label} is ahead of origin; its local commits were preserved.${idsCL[Default]}"
return 1
;;
diverged)
echo -e "${idsCL[LightYellow]}${label} has diverged from origin; automatic update was refused.${idsCL[Default]}"
return 1
;;
*)
echo -e "${idsCL[Red]}Could not determine the update state for ${label}.${idsCL[Default]}"
return 1
;;
esac
} }
INSTALL_UPDATES() { INSTALL_UPDATES() {
local current_branch local current_branch
local defaults_warning=0
local update_failed=0 local update_failed=0
echo -e "${idsCL[LightCyan]}Checking for updates...${idsCL[Default]}" echo -e "${idsCL[LightCyan]}Checking for updates...${idsCL[Default]}"
@@ -112,7 +169,7 @@ INSTALL_UPDATES() {
fi fi
UPDATE_REPOSITORY /opt/idssys/defaults master "iDSSYS Defaults" || UPDATE_REPOSITORY /opt/idssys/defaults master "iDSSYS Defaults" ||
update_failed=1 defaults_warning=1
current_branch="$(git -C /opt/idssys/ta-proxmenu branch --show-current)" current_branch="$(git -C /opt/idssys/ta-proxmenu branch --show-current)"
if [ -z "$current_branch" ]; then if [ -z "$current_branch" ]; then
@@ -128,6 +185,9 @@ INSTALL_UPDATES() {
if (( update_failed == 0 )); then if (( update_failed == 0 )); then
source /opt/idssys/ta-proxmenu/defaults.inc source /opt/idssys/ta-proxmenu/defaults.inc
echo -e "\n${idsCL[Green]}Update check complete. Installed version: ${VERS}${idsCL[Default]}" echo -e "\n${idsCL[Green]}Update check complete. Installed version: ${VERS}${idsCL[Default]}"
if (( defaults_warning == 1 )); then
echo -e "${idsCL[LightYellow]}TA-ProxMenu was processed, but iDSSYS Defaults requires attention.${idsCL[Default]}"
fi
return 0 return 0
fi fi