5 Commits
7 changed files with 453 additions and 73 deletions

No files matched your search

+11 -2
View File
@@ -59,7 +59,7 @@ options:
none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none
-h, --help this help
-V, --version print the version
-v, --version print the version
```
Exit status: `0` ok, `1` usage or backend error, `2` aborted (end of input),
@@ -131,7 +131,12 @@ package (pacman backend with paru or yay, see below) and a space on every other
One line, whitespace-separated tokens; `N` is the number in brackets. Numbers are
stable for the whole session (rows that leave the list leave a gap). Every token
of a line is checked for syntax first: an unknown token rejects the line. A number
that is not listed prints an error and the remaining commands still run.
or package name that is not listed prints an error and the remaining commands still run.
For `?` and `??`, a listed package name can replace the number, for example
`su-dummy-a?` or `su-dummy-a??`. A digits-only reference is interpreted as a number.
Names are restricted to these information commands to avoid unnecessary parsing
complexity: package names may contain `-` or end in `x`, which conflicts with the
other command suffixes.
| token | action |
|-------|--------|
@@ -141,13 +146,17 @@ that is not listed prints an error and the remaining commands still run.
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker |
| `Nx` | purge now, with its now-unneeded dependencies: the purge is simulated first and the prompt lists everything it removes (`Purge su-dummy-a now? This also removes: su-dummy-b [y/N]`); the answer is read from the same stdin; the rows of every removed package leave the list. `x` refuses when other installed packages depend on the target, like `pacman -Rns`; purge the dependents first (`not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first`). Rows with old version `-` (new dependencies, not installed) cannot be purged |
| `N+` | trust forever: write it and its installed dependency closure to the statefile; the row leaves the list. Rows this hides are re-included if they were excluded (a trusted package is upgraded) |
| `N++` | follow installed dependents recursively and trust each topmost package and its installed dependencies, even when a topmost package is not in the update list |
| `*+` / `*-` | `+` / `-` for every listed package (`*-` is an all-or-nothing toggle: when all are excluded it re-includes them) |
| `a` | show packages trusted during this session again, marked with `=`; packages already trusted before the session remain hidden |
| `l` | redraw the list |
| `q` | quit without upgrading (exit 0) |
| `h`, `?` | help |
| end of input | abort without upgrading (exit 2) |
Several tokens may share a line (`1? 3- 4+`); they run left to right. Consecutive
`Nx` commands such as `5x 30x 41x` are simulated and confirmed together. Another
command ends the group; a dependent can be included with its dependency. Consecutive
`N+` commands are processed together: selected packages required by another selected
package are recorded as dependencies, regardless of their order. `*+` works the same
way. With `--all`
+5
View File
@@ -126,6 +126,11 @@ Refreshing package lists... done.
Command grammar (one line, whitespace-separated tokens, each `N` is the number in brackets):
Unknown tokens reject the line; nonexistent numbers report an error and are skipped.
For `?` and `??`, the exact name of a listed package may replace `N`; a digits-only
reference is treated as a number. Missing or no longer listed names are reported and
skipped. Other commands accept only numbers: package names can contain `-` or end in
`x`, and limiting name references to information commands avoids unnecessary parsing
complexity.
Consecutive `N+` commands and `*+` trust the selected dependency roots, recording
other selected packages in their closures as dependencies regardless of input order.
+225 -60
View File
@@ -6,7 +6,7 @@
# and zypper: list what would be upgraded, interrogate every package, remember the
# ones you trust, then run the native upgrade (with per-run exclusions).
#
# Version: 0.2.0
# Version: 0.6.0
#
# Specification: docs/DESIGN.md. Single file, bash >= 4.3, no runtime dependency
# beyond the distro's package manager and coreutils/sed/grep (awk only on the apt
@@ -23,7 +23,7 @@ export LC_ALL=C DEBIAN_FRONTEND=noninteractive
# never read from or written to a file (no HISTFILE; `history -r`/`-w`/`-a` are never used).
unset HISTFILE
readonly SU_VERSION=0.2.0
readonly SU_VERSION=0.6.0
readonly SU_PROG=skeptical-updater
readonly SU_STATE_HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)'
readonly SU_PROMPT_HELP='<enter> upgrade | N? what is it | N?? full info | N- exclude this run | Nx purge now | N+ trust forever'
@@ -105,7 +105,7 @@ options:
none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none
-h, --help this help
-V, --version print the version
-v, --version print the version
interactive commands (one line, whitespace-separated tokens, N = number in brackets):
<empty line> proceed: run the native upgrade with the current exclusions
@@ -114,11 +114,15 @@ interactive commands (one line, whitespace-separated tokens, N = number in brack
that depend on it - installed packages only
N?? full native info: the installed record; lines starting with '*' come
from the upstream candidate (differing values, fields it adds)
For N? and N??, a listed package name may be used instead of N.
N- exclude from this run (toggle); shown with a '-' marker
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
when other installed packages depend on it, like pacman -Rns
consecutive Nx commands are purged together after one confirmation
N+ trust it forever (with its installed dependency closure)
N++ trust the topmost installed dependents and their dependency closures
*+ / *- apply + / - to every listed package
a show packages trusted during this session again
l redraw the list
q quit without upgrading (exit 0)
h, ? this help
@@ -138,12 +142,16 @@ session_help() {
<enter> upgrade now, skipping the rows marked '-'
N? what is it: description, install reason + installed packages that need it
N?? full native info: installed record, '*' lines from the upstream candidate
A listed package name may replace N for ? and ?? (e.g. okular?).
N- exclude N from this run (toggle)
Nx purge N now: lists every package the purge removes, then asks y/N.
x refuses when other installed packages depend on the target, like
pacman -Rns; purge the dependents first
consecutive Nx commands are purged together after one confirmation
N+ trust N (and its installed dependencies) forever
N++ trust the topmost installed dependents of N and their dependencies
*+ *- trust / exclude every listed package (*- again re-includes them)
a show packages trusted during this session again
l redraw the list (a '*' after the number marks an AUR package)
q quit without upgrading
h ? this help
@@ -197,7 +205,7 @@ parse_args() {
usage
exit 0
;;
-V | --version)
-v | --version)
printf '%s %s\n' "$SU_PROG" "$SU_VERSION"
exit 0
;;
@@ -970,20 +978,21 @@ apt_closure() {
# is system-wide (it also takes pre-existing orphans, e.g. old kernels: verified), so the
# orphans that were already autoremovable before are subtracted and the rest named explicitly.
apt_purge_plan() {
local pkg=$1 pre sim w name rest
local -A before=()
local -a args=("$pkg")
local pre sim w name rest
local -A before=() requested=()
local -a args=("$@")
for name in "$@"; do requested[$name]=1; done
pre=$(apt-get -s autoremove </dev/null 2>/dev/null) || pre=''
while read -r w name rest; do
[[ $w == Remv || $w == Purg ]] && before[$name]=1
done <<<"$pre"
sim=$(apt-get -s purge --auto-remove "$pkg" </dev/null 2>&1) || {
sim=$(apt-get -s purge --auto-remove "$@" </dev/null 2>&1) || {
printf '%s\n' "$sim" >&2
return 1
}
while read -r w name rest; do
[[ $w == Remv || $w == Purg ]] || continue
[[ -z ${before[$name]+x} && $name != "$pkg" ]] && args+=("$name")
[[ -z ${before[$name]+x} && -z ${requested[$name]+x} ]] && args+=("$name")
done <<<"$sim"
sim=$(apt-get -s purge "${args[@]}" </dev/null 2>&1) || {
printf '%s\n' "$sim" >&2
@@ -1505,7 +1514,7 @@ pacman_closure() {
# On refusal pacman explains on stdout (":: removing X breaks dependency ..."): show all of it.
pacman_purge_plan() {
local out rc=0 l
out=$(COLUMNS=0 pacman -Rs --print --print-format '%n' -- "$1" </dev/null 2>&1) || rc=$?
out=$(COLUMNS=0 pacman -Rs --print --print-format '%n' -- "$@" </dev/null 2>&1) || rc=$?
if ((rc)); then
printf '%s\n' "$out" >&2
return "$rc"
@@ -1906,7 +1915,7 @@ dnf_purge_plan() {
local out line in=0 wrapped=0
local -a f=()
local arch_re='^(noarch|x86_64|x86_64_v[0-9]|aarch64|i[3-6]86|ppc64le|ppc64|s390x|armv7hl|armv7l|riscv64|loongarch64|src)$'
out=$("$DNF" --assumeno remove "$1" </dev/null 2>&1) || true
out=$("$DNF" --assumeno remove "$@" </dev/null 2>&1) || true
if [[ $out != *'Transaction Summary'* ]]; then
if [[ $out == *'Nothing to do'* ]]; then return 0; fi
printf '%s\n' "$out" >&2
@@ -2089,7 +2098,7 @@ zypper_reason() {
}
zypper_purge_plan() {
local xml rc=0 sec name rest
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$1" </dev/null 2>&1) || rc=$?
xml=$(zypper --non-interactive --no-refresh --xmlout remove --dry-run --clean-deps "$@" </dev/null 2>&1) || rc=$?
if ((rc)); then
[[ $rc != 104 ]] || say "zypper: no installed package provides '$1' (exit 104)" >&2
((rc == 104)) || printf '%s\n' "$xml" >&2
@@ -2227,6 +2236,8 @@ maybe_refresh() { if ((opt_refresh)); then be refresh || true; fi; }
R_NAME=() R_OLD=() R_NEW=() R_REPO=() R_FLAGS=() R_AUR=() R_NUM=() R_GONE=() R_EXCL=() R_NOTES=()
N_IDX=() # number -> row index
N_MAX=0
SESSION_SHOW_TRUSTED=0
declare -A SESSION_TRUSTED=()
load_rows() {
local out rc=0 line name old new repo flags i n=0 w aur shown
@@ -2274,7 +2285,7 @@ rows_left() { # number of rows not purged
row_visible() { # row_visible IDX
local i=$1
((R_GONE[i] == 0 && R_NUM[i] > 0)) || return 1
((opt_all)) || ! is_trusted "${R_NAME[i]}"
((opt_all)) || ! is_trusted "${R_NAME[i]}" || { ((SESSION_SHOW_TRUSTED)) && [[ -n ${SESSION_TRUSTED[${R_NAME[i]}]+x} ]]; }
}
print_list() {
@@ -2291,6 +2302,8 @@ print_list() {
done
if ((opt_all)); then
say "$total upgradable, $trusted trusted (marked with =), $review to review:"
elif ((SESSION_SHOW_TRUSTED)); then
say "$total upgradable, $trusted trusted (session trusted marked with =), $review to review:"
else
say "$total upgradable, $trusted trusted (hidden, use --all to show), $review to review:"
fi
@@ -2629,66 +2642,103 @@ num_row() { # num_row N -> $REPLY = row index of a listed row, rc 1 if N is not
REPLY=${N_IDX[n]}
}
# session_purge IDX: pacman's -Rns principle on every backend - a package that other installed
# session_purge IDX...: pacman's -Rns principle on every backend - a package that other installed
# packages still depend on is never purged (they are listed, as N? does; nothing cascades);
# otherwise the package goes with the dependencies it alone needed, as the y/N prompt lists.
session_purge() {
local i=$1 name=${R_NAME[$1]} rc=0 out line p prompt rd d
local -a plan=() others=() extra=()
local -A still=() gone=() mine=()
local i name rd out rc=0 p prompt idx dep changed
local -a targets=() target_names=() total_plan=() total_others=() extra=() rdeps=()
local -A purge_selected=() rejected=() reverse=() gone=() allowed=() still=()
local line
for idx in "$@"; do purge_selected[${R_NAME[idx]}]=$idx; done
mapfile -t targets < <(printf '%s\n' "${purge_selected[@]}" | sort -n)
for i in "${targets[@]}"; do
name=${R_NAME[i]}
if [[ ${R_OLD[i]} == - ]]; then
say "$name is not installed (the upgrade would install it as a new dependency): nothing to purge; use $((R_NUM[i]))- to exclude it from this run."
return 0
rejected[$name]=1
continue
fi
rd=$(be rdepends "$name") || rd=''
rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
rd=${rd% }
if [[ -n $rd ]]; then
reverse[$name]=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
done
# A refused dependent also blocks its dependencies, including through a chain.
changed=1
while ((changed)); do
changed=0
for i in "${targets[@]}"; do
name=${R_NAME[i]}
[[ -z ${rejected[$name]+x} ]] || continue
read -r -a rdeps <<<"${reverse[$name]}"
for dep in "${rdeps[@]}"; do
if [[ -z ${purge_selected[$dep]+x} || -n ${rejected[$dep]+x} ]]; then
rd=${reverse[$name]% }
say "not purged: $name is required by: ${rd// /, } - purge the dependents first"
return 0
rejected[$name]=1
changed=1
break
fi
out=$(be purge_plan "$name" | sort -u) || rc=$?
done
done
done
for i in "${targets[@]}"; do
name=${R_NAME[i]}
[[ -z ${rejected[$name]+x} ]] || continue
target_names+=("$name")
allowed[$name]=1
closure_of "$name"
for dep in ${CLOSURE[@]+"${CLOSURE[@]}"}; do allowed[$dep]=1; done
done
((${#target_names[@]})) || return 0
out=$(be purge_plan "${target_names[@]}" | sort -u) || rc=$?
if ((rc)); then
err "cannot purge $name: $(be label) refuses (see above); nothing was removed"
err "cannot purge ${target_names[*]}: $(be label) refuses (see above); nothing was removed"
return 0
fi
while IFS= read -r p; do
[[ -n $p && -z ${gone[$p]+x} ]] || continue
[[ -n $p ]] || continue
total_plan+=("$p")
gone[$p]=1
plan+=("$p")
[[ $p == "$name" ]] || others+=("$p")
[[ -n ${purge_selected[$p]+x} && -z ${rejected[$p]+x} ]] || total_others+=("$p")
[[ -n ${allowed[$p]+x} && -z ${rejected[$p]+x} ]] || extra+=("$p")
done <<<"$out"
if ((${#plan[@]} == 0)); then
say "$name is not installed: nothing to purge."
if ((${#total_plan[@]} == 0)); then
say "${target_names[*]} is not installed: nothing to purge."
return 0
fi
# belt and braces: the plan may only hold the target and its own (installed) dependencies;
# anything else means a dependent the reverse-dependency query missed (e.g. through a virtual)
if ((${#others[@]})); then
closure_of "$name"
for d in ${CLOSURE[@]+"${CLOSURE[@]}"}; do mine[$d]=1; done
for p in "${others[@]}"; do [[ -n ${mine[$p]+x} ]] || extra+=("$p"); done
for name in "${target_names[@]}"; do
if [[ -z ${gone[$name]+x} ]]; then
err "cannot purge $name: $(be label) did not include it in the plan; nothing was removed"
return 0
fi
done
# belt and braces: no package outside the targets and their dependencies may be removed.
if ((${#extra[@]})); then
err "cannot purge $name: $(be label) would also remove $(join_by ', ' "${extra[@]}"), which $name does not depend on (dependents of it?); nothing was removed"
err "cannot purge ${target_names[*]}: $(be label) would also remove $(join_by ', ' "${extra[@]}"), which ${target_names[*]} does not depend on (dependents of it?); nothing was removed"
return 0
fi
fi
prompt="Purge $name now?"
((${#others[@]} == 0)) || prompt+=" This also removes: $(join_by ', ' "${others[@]}")"
prompt="Purge ${target_names[*]} now?"
((${#total_others[@]} == 0)) || prompt+=" This also removes: $(join_by ', ' "${total_others[@]}")"
read_line "$prompt [y/N] "
case $REPLY in
y | Y | yes | YES | Yes) ;;
*)
say "not purged: $name"
say "not purged: ${target_names[*]}"
return 0
;;
esac
be purge "${plan[@]}" || rc=$?
rc=0
be purge "${total_plan[@]}" || rc=$?
if ((rc)); then
err "purge of $name failed ($(be label) exited $rc); the row stays in the list"
if ((${#target_names[@]} == 1)); then
err "purge of ${target_names[0]} failed ($(be label) exited $rc); the row stays in the list"
else
err "purge of ${target_names[*]} failed ($(be label) exited $rc); the rows stay in the list"
fi
return 0
fi
say "purged: ${plan[*]}"
say "purged: ${total_plan[*]}"
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
if [[ -n ${gone[${R_NAME[i]}]+x} ]]; then
R_GONE[i]=1
@@ -2737,9 +2787,17 @@ session_trust() { # session_trust IDX...
trust_pkg "${R_NAME[i]}" "$now"
R_EXCL[i]=0
done
session_finish_trust
}
session_finish_trust() {
local i
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
if ((R_NUM[i] > 0 && !R_GONE[i])) && is_trusted "${R_NAME[i]}"; then SESSION_TRUSTED[${R_NAME[i]}]=1; fi
done
# a row this trust hides must not stay excluded behind the user's back
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
if ((R_EXCL[i] && !R_GONE[i] && !opt_all)) && is_trusted "${R_NAME[i]}"; then
if ((R_EXCL[i] && !R_GONE[i])) && is_trusted "${R_NAME[i]}" && ! row_visible "$i"; then
R_EXCL[i]=0
say " (${R_NAME[i]} is trusted now: no longer excluded from this run)"
fi
@@ -2747,6 +2805,62 @@ session_trust() { # session_trust IDX...
state_save
}
session_trust_dependents() {
local i=$1 node dep out root other candidate now head blocked
local -a queue=() nodes=() roots=() walk=() deps=()
local -A seen=() edges=() reach=() visited=()
if [[ ${R_OLD[i]} == - ]]; then
say "${R_NAME[i]} is not installed: cannot find installed dependents."
return 0
fi
queue=("${R_NAME[i]}")
seen[${R_NAME[i]}]=1
for ((head = 0; head < ${#queue[@]}; head++)); do
node=${queue[head]}
nodes+=("$node")
if ! out=$(be rdepends "$node"); then
err "cannot find installed dependents of $node; nothing was trusted"
return 0
fi
while IFS= read -r dep; do
[[ -n $dep ]] || continue
edges[$node]+=" $dep"
if [[ -z ${seen[$dep]+x} ]]; then seen[$dep]=1; queue+=("$dep"); fi
done <<<"$out"
done
for root in "${nodes[@]}"; do
walk=("$root")
visited=(["$root"]=1)
for ((head = 0; head < ${#walk[@]}; head++)); do
node=${walk[head]}
read -r -a deps <<<"${edges[$node]-}"
for dep in "${deps[@]}"; do
[[ -z ${visited[$dep]+x} ]] || continue
visited[$dep]=1
walk+=("$dep")
done
done
for node in "${walk[@]}"; do reach["$root,$node"]=1; done
done
mapfile -t nodes < <(printf '%s\n' "${nodes[@]}" | sort -u)
for root in "${nodes[@]}"; do
blocked=0
for other in "${nodes[@]}"; do
[[ $root != "$other" && -n ${reach[$root,$other]+x} ]] || continue
if [[ -z ${reach[$other,$root]+x} || $other < $root ]]; then blocked=1; break; fi
done
((blocked)) || roots+=("$root")
done
now=$(now_utc)
for candidate in "${roots[@]}"; do
trust_pkg "$candidate" "$now"
for ((i = 0; i < ${#R_NAME[@]}; i++)); do
[[ ${R_NAME[i]} != "$candidate" ]] || R_EXCL[i]=0
done
done
session_finish_trust
}
session_toggle_all() {
local i all=1
local -a idx=()
@@ -2766,20 +2880,33 @@ session_toggle_all() {
SESSION_PROCEED=0
UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own
session_flush_purges() {
local -n pb=$1
if ((${#pb[@]})); then
session_purge "${pb[@]}"
pb=()
if (($(rows_left) == 0 && ${#R_NOTES[@]} == 0)); then
say "Nothing left to upgrade."
exit 0
fi
fi
}
session_line() {
local tok n op i
local -a toks=() batch=()
local -A initial=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$'
local tok n op i name
local -a toks=() batch=() purge_batch=()
local -A initial=() named=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+\+|\+)$'
local name_re='^([A-Za-z0-9_+@][A-Za-z0-9_+.@:~-]*)(\?\?|\?)$'
read -r -a toks <<<"$1" || true
if ((${#toks[@]} == 0)); then
SESSION_PROCEED=1
return 0
fi
for tok in "${toks[@]}"; do
if [[ ! $tok =~ $re ]]; then
if [[ ! $tok =~ $re && ! $tok =~ $name_re ]]; then
case $tok in
'*+' | '*-' | l | q | h | '?') ;;
'*+' | '*-' | a | l | q | h | '?') ;;
*)
err "unknown command '$tok' (h for help)"
return 0
@@ -2788,9 +2915,30 @@ session_line() {
fi
done
for ((n = 1; n <= N_MAX; n++)); do
if num_row "$n"; then initial[$n]=$REPLY; fi
if num_row "$n"; then
initial[$n]=$REPLY
named[${R_NAME[REPLY]}]=$REPLY
fi
done
for tok in "${toks[@]}"; do
if [[ $tok =~ $re ]]; then
op=${BASH_REMATCH[2]}
if [[ $op == x ]]; then
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
elif [[ $op == + ]]; then
session_flush_purges purge_batch
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
session_flush_purges purge_batch
fi
elif [[ $tok =~ $name_re ]]; then
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
session_flush_purges purge_batch
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
session_flush_purges purge_batch
fi
if [[ $tok =~ $re ]]; then
n=$((10#${BASH_REMATCH[1]}))
op=${BASH_REMATCH[2]}
@@ -2803,8 +2951,12 @@ session_line() {
batch+=("$i")
continue
fi
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
if [[ $op == x ]]; then
purge_batch+=("$i")
continue
fi
case $op in
'++') session_trust_dependents "$i" ;;
'?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
'??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
-)
@@ -2816,16 +2968,21 @@ session_line() {
say "excluded from this run: ${R_NAME[i]}"
fi
;;
x)
session_purge "$i"
if (($(rows_left) == 0 && ${#R_NOTES[@]} == 0)); then
say "Nothing left to upgrade."
exit 0
fi
;;
esac
elif [[ $tok =~ $name_re ]]; then
name=${BASH_REMATCH[1]}
op=${BASH_REMATCH[2]}
if [[ -z ${named[$name]+x} ]] || ((R_GONE[${named[$name]:-0}])); then
err "no package '$name' in the list (l to redraw)"
continue
fi
i=${named[$name]}
if [[ $op == '?' ]]; then
print_info "${R_NAME[i]}" "${R_OLD[i]}" || true
else
print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true
fi
else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $tok in
'*+')
local -a idx=()
@@ -2835,6 +2992,13 @@ session_line() {
if ((${#idx[@]})); then session_trust "${idx[@]}"; else say "nothing to trust"; fi
;;
'*-') session_toggle_all ;;
a)
SESSION_SHOW_TRUSTED=1
for ((n = 1; n <= N_MAX; n++)); do
if num_row "$n"; then initial[$n]=$REPLY; named[${R_NAME[REPLY]}]=$REPLY; fi
done
print_list
;;
l) print_list ;;
q)
say "Quit: nothing was upgraded."
@@ -2845,6 +3009,7 @@ session_line() {
fi
done
if ((${#batch[@]})); then session_trust "${batch[@]}"; fi
session_flush_purges purge_batch
}
cmd_interactive() {
+10
View File
@@ -785,3 +785,13 @@ zypp_conf() { # a "real" zypp.conf + locks file with one user lock, in the test
refute_output --partial 'skeptical-updater.'
[[ ! -e $STATE/zypper-private ]]
}
@test "pacman: dependent and dependency are simulated and purged together" {
use_fakes pacman
run skup --no-refresh <<<$'2x 1x\ny\nq'
assert_success
assert_line 'Purge su-dummy-a su-dummy-b now? [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b'
log_has 'pacman -Rs --print --print-format %n -- su-dummy-a su-dummy-b'
log_has 'pacman -Rns --noconfirm -- su-dummy-a su-dummy-b'
}
+6 -4
View File
@@ -24,12 +24,14 @@ setup() { common_setup; }
assert_line 'usage: skeptical-updater [options] [command]'
}
@test "--version and -V print the version" {
@test "--version and -v print the version" {
run skup --version
assert_success
assert_output 'skeptical-updater 0.1.0'
run skup -V
assert_output 'skeptical-updater 0.1.0'
assert_output --regexp '^skeptical-updater [0-9]+\.[0-9]+\.[0-9]+(\+(alpha|beta|rc)[.-]?[0-9]+)?$'
local version=$output
run skup -v
assert_success
assert_output "$version"
}
@test "an unknown option is a usage error (exit 1)" {
+6 -7
View File
@@ -137,21 +137,20 @@ case $op in
-Rs)
((print)) || exit 98
tg=${targets[0]}
if [[ -n ${FAKE_PACMAN_RS_REFUSE:-} || $tg == su-dummy-b ]]; then
if [[ -n ${FAKE_PACMAN_RS_REFUSE:-} || " ${targets[*]} " == *' su-dummy-b '* && " ${targets[*]} " != *' su-dummy-a '* ]]; then
echo "checking dependencies..."
echo ":: removing $tg breaks dependency '$tg' required by su-dummy-a"
echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2
exit 1
fi
for tg in "${targets[@]}"; do
case $tg in
su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;; # b was installed as a dependency of a
su-dummy-c | glib2 | expat | ca-certificates-mozilla | pfetch | su-dummy-aur) echo "$tg" ;; # (the last two: foreign, AUR)
*)
echo "error: target not found: $tg" >&2
exit 1
;;
su-dummy-a) printf '%s\n' su-dummy-a su-dummy-b ;;
su-dummy-b | su-dummy-c | glib2 | expat | ca-certificates-mozilla | pfetch | su-dummy-aur) echo "$tg" ;;
*) echo "error: target not found: $tg" >&2; exit 1 ;;
esac
for r in ${FAKE_PACMAN_RS_EXTRA:-}; do [[ ${r%%:*} == "$tg" ]] && echo "${r#*:}"; done
done
;;
-Rns)
rc=$(fake_rc FAKE_PACMAN_RNS_RC 0)
+190
View File
@@ -101,6 +101,54 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
log_has 'apt-cache show --no-all-versions su-dummy-a'
}
@test "listed package names support both information commands alongside numeric commands" {
run skup --no-refresh <<<$'su-dummy-a? 2? su-dummy-b?? 3-\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
assert_line ' Package: su-dummy-b'
assert_line 'excluded from this run: su-dummy-c'
log_has 'dpkg-query -s -- su-dummy-b'
}
@test "unknown or hidden package names report an error and do not stop later commands" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<$'missing? su-dummy-a?? 2?\nq'
assert_success
assert_output --partial "no package 'missing' in the list"
assert_output --partial "no package 'su-dummy-a' in the list"
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
}
@test "names work for trusted rows shown with --all and new dependencies" {
write_state "$HEADER" "apt${T}su-dummy-a${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh --all <<<$'su-dummy-a?\nq'
assert_success
assert_line 'su-dummy-a: dummy package su-dummy-a for skeptical-updater tests'
FAKE_APT_SIM=real run skup --no-refresh <<<$'su-dummy-newdep? su-dummy-newdep??\nq'
assert_success
assert_line 'reason: not installed'
log_lacks '^dpkg-query -s -- su-dummy-newdep'
log_has 'apt-cache show --no-all-versions su-dummy-newdep'
}
@test "a package removed during a line cannot be queried by name" {
run skup --no-refresh <<<$'3x su-dummy-c? 2?\ny\nq'
assert_success
assert_output --partial "no package 'su-dummy-c' in the list"
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
}
@test "package names are not accepted for trust, exclude or purge" {
for token in su-dummy-a+ su-dummy-a- su-dummy-ax; do
run skup --no-refresh <<<"$token"$'\nq'
assert_success
assert_output --partial "unknown command '$token'"
[[ ! -e $STATE_FILE ]]
log_lacks '^apt-get -y purge'
done
}
@test "N?? on a new dependency (old version -) never looks for an installed record" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq'
assert_success
@@ -409,3 +457,145 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
assert_line ' [3] su-dummy-c 1.0 -> 2.0 file:/srv/su-repo ./ [held]'
assert_line 'su-dummy-c: dummy package su-dummy-c for skeptical-updater tests'
}
@test "Nx: consecutive purges use one transaction including orphans" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 3x\ny\nq'
assert_success
assert_line 'Purge su-dummy-a su-dummy-c now? This also removes: su-dummy-b [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b su-dummy-c'
assert_line 'Nothing left to upgrade.'
log_has 'apt-get -y purge su-dummy-a su-dummy-b su-dummy-c'
}
@test "Nx: other commands flush the batch before resolving removed rows" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 2? 3x\ny\ny\nq'
assert_success
assert_line 'purged: su-dummy-a su-dummy-b'
assert_output --partial 'no package [2] in the list'
assert_line 'Purge su-dummy-c now? [y/N] y'
assert_line 'purged: su-dummy-c'
}
@test "Nx: a trust command cannot act on a removed row" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 2+\ny\nq'
assert_success
assert_output --partial 'no package [2] in the list'
assert_line 'purged: su-dummy-a su-dummy-b'
}
@test "Nx: duplicate targets are purged once" {
run skup --no-refresh <<<$'3x 3x\ny\nq'
assert_success
assert_line 'Purge su-dummy-c now? [y/N] y'
assert_line 'purged: su-dummy-c'
log_has 'apt-get -y purge su-dummy-c'
}
@test "Nx: a refused target does not prevent other purges" {
run skup --no-refresh <<<$'2x 3x\ny\nl\nq'
assert_success
assert_line 'not purged: su-dummy-b is required by: su-dummy-a - purge the dependents first'
assert_line 'purged: su-dummy-c'
log_has 'apt-get -y purge su-dummy-c'
}
@test "Nx: invalid numbers do not interrupt a batch" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 9x 3x\ny\nq'
assert_success
assert_output --partial 'no package [9] in the list'
assert_line 'purged: su-dummy-a su-dummy-b su-dummy-c'
log_has 'apt-get -y purge su-dummy-a su-dummy-b su-dummy-c'
}
@test "Nx: rejecting the prompt purges nothing" {
run skup --no-refresh <<<$'1x 3x\nn\nl\nq'
assert_success
assert_line 'not purged: su-dummy-a su-dummy-c'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
log_lacks '^apt-get -y purge'
}
@test "Nx: failed transaction keeps all rows" {
FAKE_APT_PURGE_RC=100 run skup --no-refresh <<<$'1x 3x\nyes\nl\nq'
assert_success
assert_output --partial 'purge of su-dummy-a su-dummy-c failed (apt exited 100); the rows stay in the list'
assert_line '3 upgradable, 0 trusted (hidden, use --all to show), 3 to review:'
}
@test "Nx: a dependent and dependency are planned together" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'2x 1x\ny\nq'
assert_success
assert_line 'Purge su-dummy-a su-dummy-b now? [y/N] y'
assert_line 'purged: su-dummy-a su-dummy-b'
log_has 'apt-get -y purge su-dummy-a su-dummy-b'
}
@test "Nx: unexpected removals never leak into another target's purge" {
FAKE_APT_SIM_EXTRA=su-dummy-a:su-dummy-c run skup --no-refresh <<<$'1x 2x\ny\nq'
assert_success
assert_output --partial 'would also remove su-dummy-c'
log_lacks '^apt-get -y purge'
}
@test "a shows session trusted rows without exposing previously trusted rows" {
write_state "$HEADER" "apt${T}su-dummy-c${T}user${T}2026-01-01T00:00:00Z"
run skup --no-refresh <<<$'1+\na\n2?\nq'
assert_success
assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
assert_line '=[2] su-dummy-b 1.0 -> 2.0 su-fixture:su-repo'
refute_output --partial '=[3] su-dummy-c'
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
}
@test "a works on the same line as trust and keeps removed rows hidden" {
run skup --no-refresh <<<$'3x\ny\n1+ a 2?\nq'
assert_success
assert_line 'purged: su-dummy-c'
assert_line '=[1] su-dummy-a 1.0 -> 2.0 su-fixture:su-repo'
assert_line 'su-dummy-b: dummy package su-dummy-b for skeptical-updater tests'
refute_output --partial '=[3] su-dummy-c'
}
@test "N++ trusts the topmost installed dependent and its dependencies" {
run skup --no-refresh <<<$'2++\nq'
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
}
@test "N++ trusts all topmost branches including packages outside the list" {
local fixture_data="$BATS_TEST_TMPDIR/data"
cp -R "$FAKE_DATA" "$fixture_data"
printf 'su-dummy-b\nReverse Depends:\n su-dummy-a\n su-dummy-c\n' >"$fixture_data/apt/rdepends/su-dummy-b"
printf 'su-dummy-a\nReverse Depends:\n su-dummy-w\n' >"$fixture_data/apt/rdepends/su-dummy-a"
sed -i 's/su-virtual | su-dummy-alt/su-dummy-a/; s/^ii \(.*su-dummy-c\)\t\t\t\t/ii \1\t\tsu-dummy-b\t/' "$fixture_data/apt/dpkg-status.tsv"
FAKE_DATA="$fixture_data" run skup --no-refresh <<<$'2++\nq'
assert_success
assert_output --partial 'trusted su-dummy-c'
assert_output --partial 'trusted su-dummy-w'
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-c${T}user"
assert_line "su-dummy-w${T}user"
assert_line "su-dummy-a${T}dep:su-dummy-w"
}
@test "N++ terminates on a dependency cycle and chooses one root" {
local fixture_data="$BATS_TEST_TMPDIR/data"
cp -R "$FAKE_DATA" "$fixture_data"
printf 'su-dummy-a\nReverse Depends:\n su-dummy-b\n' >"$fixture_data/apt/rdepends/su-dummy-a"
sed -i 's/^ii \(.*su-dummy-b\)\t\t\t\t/ii \1\t\tsu-dummy-a\t/' "$fixture_data/apt/dpkg-status.tsv"
FAKE_DATA="$fixture_data" run skup --no-refresh <<<$'2++\nq'
assert_success
assert_output --partial 'trusted su-dummy-a'
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
}
@test "N++ trusts the package itself when it has no installed dependents" {
run skup --no-refresh <<<$'3++\nq'
assert_success
assert_line 'trusted su-dummy-c'
}