updates bug fix

This commit is contained in:
2026-10-13 15:36:59 +02:00
parent 76bc2c7ade
commit b5be992d82
3 changed files with 153 additions and 58 deletions
+139 -55
View File
@@ -40,14 +40,49 @@ export BASE_URL="https://git.geoffray-levasseur.org/fatalerrors/profile"
# UPDT_DEFAULT_BRANCH (if set in profile.conf) is already available.
load_conf "updates"
# ------------------------------------------------------------------------------
# Check a branch name. It ends up in URLs and in git command lines, so it is
# restricted to plain ref characters, without a leading "-" or "..".
# Usage: _updt_valid_branch <name>
_updt_valid_branch()
{
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ && "$1" != *..* ]]
}
if ! _updt_valid_branch "${UPDT_DEFAULT_BRANCH:-master}"; then
disp W "Invalid UPDT_DEFAULT_BRANCH '${UPDT_DEFAULT_BRANCH}', using master."
UPDT_DEFAULT_BRANCH=master
fi
export UPDT_DEFAULT_BRANCH="${UPDT_DEFAULT_BRANCH:-master}"
export UPDT_URL="$BASE_URL/raw/branch/$UPDT_DEFAULT_BRANCH"
export ARCH_URL="$BASE_URL/archive/$UPDT_DEFAULT_BRANCH.tar.gz"
# ------------------------------------------------------------------------------
# Tell whether the published version $2 is an update for the installed one $1.
# Versions look like MAJOR.MINOR.PATCH[-REVISION[_STAGE_N]]. The base version
# (before the first "-" or "_") decides, compared in version order; with the same
# base, any difference (a new revision, a release candidate becoming final) is
# an update. Returns 0 when it is, 1 otherwise (equal, or the installed version
# is the more recent one, as on a development branch).
# Usage: _updt_is_newer <installed> <published>
_updt_is_newer()
{
local cur="$1" new="$2" cur_base new_base
[[ "$cur" == "$new" ]] && return 1
cur_base="${cur%%[-_]*}"
new_base="${new%%[-_]*}"
[[ "$cur_base" == "$new_base" ]] && return 0
[[ "$(printf '%s\n%s\n' "$cur_base" "$new_base" | sort -V | tail -n 1)" == "$new_base" ]]
}
# ------------------------------------------------------------------------------
# Check whether a newer profile version is available
# Usage: check_updates [-q]
# If -q is specified, the function will operate in quiet mode (internal use only)
# If -q is specified, the function will operate in quiet mode (internal use
# only): only the "update available" notice is displayed, errors are silent.
# Return status: 0 up to date, 1 update available, 2 invalid option,
# 4 temporary file error, 5 version unavailable or invalid.
check_updates()
{
local quiet=0 result=5 PARSED
@@ -59,9 +94,8 @@ check_updates()
disp E "Invalid options, use \"check_updates --help\" to display usage."
return 2
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
@@ -87,8 +121,8 @@ check_updates()
(( quiet != 1 )) && disp I "Checking for updates..."
vfile=$(mktemp /tmp/profile_version.XXXXXX) || {
disp E "Failed to create a temporary file."
vfile=$(mktemp "${TMPDIR:-/tmp}/profile_version.XXXXXX") || {
(( quiet != 1 )) && disp E "Failed to create a temporary file."
return 4
}
@@ -103,20 +137,30 @@ check_updates()
return 5
}
if [[ -s $vfile ]]; then
lastver=$(<"$vfile")
if [[ "$lastver" != "$PROFVERSION" ]]; then
disp I "Installed: $PROFVERSION. Available: $lastver."
(( quiet != 1 )) && disp I "You should upgrade when possible."
result=1
else
(( quiet != 1 )) && disp I "Your version is up-to-date."
result=0
fi
rm -f "$vfile"
# First line only, and only a plausible version: the text comes from the
# network and is displayed by disp, which interprets backslash escapes (a
# crafted file, or a captive portal page, could otherwise inject terminal
# control sequences at every shell startup).
IFS= read -r lastver < "$vfile" || true
rm -f "$vfile"
lastver="${lastver%$'\r'}"
lastver="${lastver#"${lastver%%[![:space:]]*}"}"
lastver="${lastver%"${lastver##*[![:space:]]}"}"
if [[ ! "$lastver" =~ ^[0-9]+(\.[0-9]+)*([-_.+][0-9A-Za-z._+-]+)?$ ]]; then
(( quiet != 1 )) && disp E "The downloaded version file is empty or invalid; unable to continue."
return 5
fi
if [[ "$lastver" == "${PROFVERSION:-}" ]]; then
(( quiet != 1 )) && disp I "Your version is up-to-date."
result=0
elif _updt_is_newer "${PROFVERSION:-}" "$lastver"; then
disp I "Installed: ${PROFVERSION:-unknown}. Available: $lastver."
(( quiet != 1 )) && disp I "You should upgrade when possible."
result=1
else
rm -f "$vfile"
disp E "Temporary file is unreadable; unable to continue."
(( quiet != 1 )) && disp I "Installed version (${PROFVERSION:-}) is more recent than the published one ($lastver)."
result=0
fi
return $result
@@ -128,6 +172,9 @@ export -f check_updates
# ------------------------------------------------------------------------------
# Apply the available profile upgrade
# Usage: profile_upgrade [options]
# Return status: 0 done (or nothing to do), 1 refused/bad context, 2 git or
# option error, 3 git/install error, 4 local archive or force error, 5
# temporary directory, 6 download, 7 extraction, 8 archive content, 9 copy.
profile_upgrade()
{
local PARSED
@@ -141,9 +188,8 @@ profile_upgrade()
disp E "Invalid options, use \"profile_upgrade --help\" to display usage."
return 2
fi
local -a _pargs=()
readarray -td '' _pargs < <(printf '%s' "$PARSED" | xargs printf '%s\0')
set -- "${_pargs[@]}"
# Restore the getopt-normalised (shell-quoted) argument list
eval set -- "$PARSED"
while true; do
case "$1" in
-h|--help)
@@ -157,7 +203,7 @@ profile_upgrade()
printf "\t-g, --switch-to-git\tReplace current install with a fresh Git clone\n"
printf "\t-n, --dry-run\t\tDisplay what would be done without changing anything\n"
printf "\t-F, --force\t\tDiscard local changes before upgrading\n\n"
printf "If the profile is installed from Git, the upgrade uses 'git pull'.\n"
printf "If the profile is installed from Git, the upgrade uses 'git pull --ff-only'.\n"
printf "Otherwise, it downloads or applies an archive and refreshes the files.\n"
return 0
;;
@@ -197,6 +243,13 @@ profile_upgrade()
esac
done
# Target branch: explicit -b flag > config default > master
local target_branch="${branch:-${UPDT_DEFAULT_BRANCH:-master}}"
if ! _updt_valid_branch "$target_branch"; then
disp E "Invalid branch name '$target_branch'."
return 1
fi
if (( ! use_archive && ! switch_to_git )); then
check_updates -q
check_rc=$?
@@ -224,17 +277,18 @@ profile_upgrade()
disp E "Git is required to switch this install to a Git clone."
return 3
}
local backup_dir="${MYPATH}.pre-git.$$.bak"
if (( dry_run )); then
disp I "[dry-run] rm -rf \"$MYPATH\"/.git"
disp I "[dry-run] mv \"$MYPATH\" \"$backup_dir\""
disp I "[dry-run] git clone $BASE_URL \"$MYPATH\""
[[ -n "$branch" ]] && disp I "[dry-run] git -C \"$MYPATH\" checkout $branch"
disp I "[dry-run] copy profile.conf and custom themes from \"$backup_dir\""
return 0
fi
if [[ -d $MYPATH/.git ]]; then
disp W "Git repository already present; no switch is needed."
else
local backup_dir="${MYPATH}.pre-git.$$.bak"
mv "$MYPATH" "$backup_dir" || {
disp E "Failed to move current install out of the way."
return 3
@@ -247,8 +301,18 @@ profile_upgrade()
[[ -n "$branch" ]] && (
cd "$MYPATH" && git checkout "$branch"
) || true
# The personal files are not in the repository (profile.conf is
# git-ignored): without this the new install would start without
# the user's configuration, which would stay in the backup.
# Existing files are never overwritten.
[[ -f "$backup_dir/profile.conf" ]] && \
cp -an "$backup_dir/profile.conf" "$MYPATH/profile.conf"
if [[ -d "$backup_dir/profile.d/themes" && -d "$MYPATH/profile.d/themes" ]]; then
cp -an "$backup_dir/profile.d/themes/." "$MYPATH/profile.d/themes/"
fi
disp I "Switched installation to Git source."
disp I "Previous install kept in $backup_dir."
disp I "Previous install kept in $backup_dir (profile.conf and custom themes were copied)."
return 0
fi
fi
@@ -259,23 +323,18 @@ profile_upgrade()
disp E "Git is required for this upgrade but is not available."
return 3
}
pushd "$MYPATH" >/dev/null || {
disp E "Failed to change directory to $MYPATH."
return 3
}
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
# git -C instead of cd: no directory change to undo on each exit path
local -a G=(git -C "$MYPATH")
"${G[@]}" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
disp E "Install directory is not a valid Git working tree."
popd >/dev/null || return 1
return 3
}
# Resolve the target branch: explicit -b flag > config default > master
local target_branch="${branch:-${UPDT_DEFAULT_BRANCH:-master}}"
branch="$target_branch"
# Detect current local branch and warn+switch when it differs
local current_branch
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
current_branch=$("${G[@]}" rev-parse --abbrev-ref HEAD 2>/dev/null)
if [[ -n "$current_branch" && "$current_branch" != "$branch" ]]; then
disp W "Branch mismatch: currently on '$current_branch', configured branch is '$branch'."
if (( dry_run )); then
@@ -283,21 +342,20 @@ profile_upgrade()
disp I "[dry-run] git checkout $branch"
disp W "[dry-run] Branch would be changed from '$current_branch' to '$branch'."
else
git fetch origin "$branch" || {
"${G[@]}" fetch origin "$branch" || {
disp E "Git fetch failed for branch $branch."
popd >/dev/null || return 1
return 2
}
git checkout "$branch" || {
"${G[@]}" checkout "$branch" || {
disp E "Git checkout failed for branch $branch."
popd >/dev/null || return 1
return 2
}
disp W "Branch has been changed from '$current_branch' to '$branch'."
fi
fi
if ! git diff --quiet || ! git diff --cached --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then
# status --porcelain: staged, unstaged and untracked (not ignored) files
if [[ -n $("${G[@]}" status --porcelain 2>/dev/null) ]]; then
if (( force_git )); then
disp W "Force mode: local Git changes and untracked files will be lost."
if (( dry_run )); then
@@ -305,19 +363,16 @@ profile_upgrade()
disp I "[dry-run] git reset --hard HEAD"
disp I "[dry-run] git clean -fd"
else
git fetch --all --prune || {
"${G[@]}" fetch --all --prune || {
disp E "Git fetch failed, upgrade not applied."
popd >/dev/null || return 1
return 4
}
git reset --hard HEAD || {
"${G[@]}" reset --hard HEAD || {
disp E "Git reset failed, upgrade not applied."
popd >/dev/null || return 1
return 4
}
git clean -fd || {
"${G[@]}" clean -fd || {
disp E "Git clean failed, upgrade not applied."
popd >/dev/null || return 1
return 4
}
fi
@@ -328,16 +383,16 @@ profile_upgrade()
fi
fi
if (( dry_run )); then
disp I "[dry-run] git pull origin $branch"
disp I "[dry-run] git pull --ff-only origin $branch"
else
git pull origin "$branch" || {
disp E "Git pull failed, upgrade not applied."
popd >/dev/null || return 1
# --ff-only: never create a merge commit nor leave conflict markers
# in the installed scripts; local commits that diverge are reported
"${G[@]}" pull --ff-only origin "$branch" || {
disp E "Git pull failed, upgrade not applied (see the git message above: local changes in the way, or local commits that diverge from origin/$branch and must be rebased or dropped by hand)."
return 2
}
disp I "Successfully upgraded using git."
disp I "Successfully upgraded using git. Please log out and log in again."
fi
popd >/dev/null || return 1
else
if (( use_archive )); then
[[ -r "$archive_file" ]] || {
@@ -384,7 +439,7 @@ profile_upgrade()
if (( dry_run )); then
disp I "[dry-run] dwl \"$ARCH_URL\" \"$archive\""
else
dwl "$ARCH_URL" "$archive" || {
dwl -t 300 "$ARCH_URL" "$archive" || {
disp E "Failed to download archive."
rm -rf "$tmpdir"
return 6
@@ -393,10 +448,25 @@ profile_upgrade()
fi
if (( dry_run )); then
disp I "[dry-run] tar -xzf \"$archive\" -C \"$tmpdir\""
disp I "[dry-run] tar -xf \"$archive\" -C \"$tmpdir\""
disp I "[dry-run] cp -a <extracted_profile>/. \"$MYPATH\"/"
else
tar -xzf "$archive" -C "$tmpdir" || {
# An archive is refused if one of its members is an absolute path or
# climbs out of the extraction directory with "..".
if ! tar -tf "$archive" >"$tmpdir/list" 2>/dev/null; then
disp E "Archive extraction failed (unreadable archive)."
rm -rf "$tmpdir"
return 7
fi
if grep -qE '(^/|(^|/)\.\.(/|$))' "$tmpdir/list"; then
disp E "Unsafe archive (absolute or '..' path); upgrade refused."
rm -rf "$tmpdir"
return 7
fi
rm -f "$tmpdir/list"
# --no-same-owner: the files belong to the user running the upgrade
tar --no-same-owner -xf "$archive" -C "$tmpdir" || {
disp E "Archive extraction failed."
rm -rf "$tmpdir"
return 7
@@ -409,6 +479,16 @@ profile_upgrade()
return 8
fi
# cp only adds and replaces: a module removed from the new version
# stays in profile.d and keeps being loaded. They are listed after
# the copy (never deleted: they may be the user's own modules).
local f
local -a stale=()
for f in "$MYPATH"/profile.d/*.sh "$MYPATH"/profile.d/bash-completion/*.sh; do
[[ -e "$f" ]] || continue
[[ -e "$extracted_root/${f#"$MYPATH"/}" ]] || stale+=("${f#"$MYPATH"/}")
done
disp I "Installing new version..."
cp -a "$extracted_root"/. "$MYPATH"/ || {
disp E "Failed to copy new files into $MYPATH."
@@ -416,10 +496,14 @@ profile_upgrade()
return 9
}
if (( ${#stale[@]} )); then
disp W "These files are not part of the new version and are still loaded; remove them if they are not yours: ${stale[*]}"
fi
disp I "Upgrade complete. Please log out and log in again."
rm -rf "$tmpdir"
fi
fi
return 0
}
export -f profile_upgrade
# ------------------------------------------------------------------------------