This commit is contained in:
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
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.
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
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
+1 -1
View File
@@ -3,7 +3,7 @@
action="${1:-}"
FOLDER='/opt/idssys/ta-proxmenu'
VERS='2026.7.25-29'
VERS='2026.7.25-30'
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}"
}
+76 -17
View File
@@ -5,6 +5,7 @@
[ "${2:-}" != "q" ] && source /opt/idssys/defaults/colors.inc
source /opt/idssys/defaults/default.inc
source /opt/idssys/ta-proxmenu/defaults.inc
source /opt/idssys/ta-proxmenu/inc/git-update.inc
ACTION_REQUESTED=0
[[ -n "${action:-}" ]] && ACTION_REQUESTED=1
@@ -744,16 +745,18 @@ UPDATE_CHECK_WORKER() {
WRITE_UPDATE_CACHE "unavailable" "$branch" "$local_commit" ""
return
fi
remote_commit="$(git -C "$FOLDER" ls-remote origin "refs/heads/${branch}" 2>/dev/null | cut -f1)"
if [[ -z "$remote_commit" ]]; then
status="unavailable"
elif [[ "$local_commit" == "$remote_commit" ]]; then
status="current"
else
status="available"
if TAPM_GIT_WORKTREE_DIRTY "$FOLDER"; then
WRITE_UPDATE_CACHE "dirty" "$branch" "$local_commit" ""
return
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"
}
@@ -774,12 +777,14 @@ LOAD_UPDATE_STATUS() {
local cached_status
local current_branch
local current_commit
local current_dirty=0
local now
UPDATE_STATUS='unknown'
UPDATE_REMOTE_COMMIT=''
current_branch="$(git -C "$FOLDER" branch --show-current 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)"
if [[ -r "$UPDATE_CACHE_FILE" ]]; then
@@ -789,7 +794,9 @@ LOAD_UPDATE_STATUS() {
(( now - checked_at < UPDATE_CACHE_SECONDS )) &&
[[ "$cached_branch" == "$current_branch" ]] &&
[[ "$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_REMOTE_COMMIT="$cached_remote"
return
@@ -818,12 +825,21 @@ FORCE_UPDATE_CHECK() {
echo -e "${idsCL[Default]}"
case "$UPDATE_STATUS" in
available)
behind)
echo -e "${idsCL[LightYellow]}An update is available for the current branch.${idsCL[Default]}"
;;
current)
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]}"
;;
@@ -839,9 +855,18 @@ MENU_HEADER() {
LOAD_UPDATE_STATUS
case "$UPDATE_STATUS" in
available)
behind)
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)
version_display="${idsCL[LightCyan]}${VERS} (checking for updates)${idsCL[Default]}"
;;
@@ -1114,6 +1139,7 @@ SWITCH_SCRIPT_BRANCH() {
local target_branch="$1"
local current_branch
local choice
local target_state=''
current_branch="$(git -C "$FOLDER" branch --show-current 2>/dev/null)"
if [[ "$target_branch" == "$current_branch" ]]; then
@@ -1136,14 +1162,36 @@ SWITCH_SCRIPT_BRANCH() {
echo
[[ "$choice" =~ ^[Yy]$ ]] || return
if ! timeout 30 git -C "$FOLDER" fetch origin \
"refs/heads/${target_branch}:refs/remotes/origin/${target_branch}"; then
if ! TAPM_GIT_FETCH_BRANCH "$FOLDER" "$target_branch" 30; then
echo -e "${idsCL[Red]}Failed to fetch '${target_branch}' from origin.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
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" || {
ENTER2CONTINUE
return
@@ -1156,11 +1204,13 @@ SWITCH_SCRIPT_BRANCH() {
}
fi
if ! git -C "$FOLDER" reset --hard "origin/${target_branch}"; then
echo -e "${idsCL[Red]}Failed to synchronize '${target_branch}' with origin.${idsCL[Default]}"
if [[ "$target_state" == "behind" ]]; then
if ! TAPM_GIT_FAST_FORWARD "$FOLDER" "$target_branch"; then
echo -e "${idsCL[Red]}Failed to fast-forward '${target_branch}'; no commits were discarded.${idsCL[Default]}"
ENTER2CONTINUE
return
fi
fi
rm -f "$UPDATE_CACHE_FILE"
echo -e "\n${idsCL[Green]}Now using TA-ProxMenu branch '${target_branch}'.${idsCL[Default]}"
@@ -1233,12 +1283,21 @@ UTILITIES_MENU() {
while true; do
LOAD_UPDATE_STATUS
case "$UPDATE_STATUS" in
available)
behind)
labels=("Install available update")
;;
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)
labels=("Update check in progress")
;;
@@ -1255,7 +1314,7 @@ UTILITIES_MENU() {
SELECT_MENU "Utilities" labels values
case "$MENU_SELECTION" in
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]}"
ENTER2CONTINUE
continue
+84 -24
View File
@@ -6,11 +6,13 @@ DEFAULTS_CACHE_DIR='/var/cache/ta-proxmenu'
DEFAULTS_CHECK_FILE="${DEFAULTS_CACHE_DIR}/defaults-last-check"
DEFAULTS_CHECK_SECONDS=14400
source /opt/idssys/ta-proxmenu/inc/git-update.inc
AUTO_UPDATE_DEFAULTS() {
local checked_at=0
local local_commit
local current_branch
local now
local remote_commit
local state
mkdir -p "$DEFAULTS_CACHE_DIR" 2>/dev/null || true
now="$(date +%s)"
@@ -41,22 +43,46 @@ AUTO_UPDATE_DEFAULTS() {
return
fi
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
echo "WARNING: Unable to check iDSSYS Defaults; using the installed copy." >&2
return
fi
local_commit="$(git -C "$DEFAULTS_REPOSITORY" rev-parse HEAD)"
remote_commit="$(git -C "$DEFAULTS_REPOSITORY" rev-parse origin/master)"
if [[ "$local_commit" != "$remote_commit" ]]; then
state="$(TAPM_GIT_BRANCH_STATE "$DEFAULTS_REPOSITORY" master)"
case "$state" in
behind)
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
echo "WARNING: Unable to update iDSSYS Defaults; using the installed copy." >&2
return
fi
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
return
;;
esac
fi
date +%s >"$DEFAULTS_CHECK_FILE"
@@ -72,36 +98,67 @@ UPDATE_REPOSITORY() {
local repository="$1"
local branch="$2"
local label="$3"
local local_commit
local remote_commit
local current_branch
local state
cd "$repository" || return 1
local_commit="$(git rev-parse HEAD 2>/dev/null)" || return 1
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]}"
if [[ ! -d "${repository}/.git" ]]; then
echo -e "${idsCL[Red]}${label} is not a Git repository.${idsCL[Default]}"
return 1
fi
if [ "$local_commit" = "$remote_commit" ]; then
echo -e "${idsCL[Green]}${label} is current (${branch})${idsCL[Default]}"
return 0
current_branch="$(git -C "$repository" branch --show-current 2>/dev/null)"
if [[ -z "$current_branch" ]]; then
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
if ! TAPM_GIT_FETCH_BRANCH "$repository" "$branch" 30 >/dev/null 2>&1; then
echo -e "${idsCL[Red]}Could not fetch branch '${branch}' for ${label}.${idsCL[Default]}"
return 1
fi
state="$(TAPM_GIT_BRANCH_STATE "$repository" "$branch")"
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 git fetch origin "$branch" >/dev/null 2>&1 &&
git reset --hard "origin/${branch}" >/dev/null 2>&1; then
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${idsCL[Default]}"
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() {
local current_branch
local defaults_warning=0
local update_failed=0
echo -e "${idsCL[LightCyan]}Checking for updates...${idsCL[Default]}"
@@ -112,7 +169,7 @@ INSTALL_UPDATES() {
fi
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)"
if [ -z "$current_branch" ]; then
@@ -128,6 +185,9 @@ INSTALL_UPDATES() {
if (( update_failed == 0 )); then
source /opt/idssys/ta-proxmenu/defaults.inc
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
fi