6 Commits
7 changed files with 535 additions and 89 deletions

No files matched your search

+16 -4
View File
@@ -59,7 +59,7 @@ options:
none (default: paru, else yay, when installed; other backends ignore it) none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none --no-aur same as --aur-helper=none
-h, --help this help -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), Exit status: `0` ok, `1` usage or backend error, `2` aborted (end of input),
@@ -130,8 +130,13 @@ 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 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 stable for the whole session (rows that leave the list leave a gap). Every token
of a line is validated first: a line with an unknown token or a number that is not of a line is checked for syntax first: an unknown token rejects the line. A number
listed prints an error, runs nothing, and the prompt comes back. 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 | | token | action |
|-------|--------| |-------|--------|
@@ -141,13 +146,20 @@ listed prints an error, runs nothing, and the prompt comes back.
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with a `-` marker | | `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 | | `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+` | 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) | | `*+` / `*-` | `+` / `-` 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 | | `l` | redraw the list |
| `q` | quit without upgrading (exit 0) | | `q` | quit without upgrading (exit 0) |
| `h`, `?` | help | | `h`, `?` | help |
| end of input | abort without upgrading (exit 2) | | end of input | abort without upgrading (exit 2) |
Several tokens may share a line (`1? 3- 4+`); they run left to right. With `--all` 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`
trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the
session ends by itself (exit 0). session ends by itself (exit 0).
+10 -2
View File
@@ -125,6 +125,14 @@ Refreshing package lists... done.
``` ```
Command grammar (one line, whitespace-separated tokens, each `N` is the number in brackets): 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.
| token | action | | token | action |
|--------|---------------------------------------------------------------------------| |--------|---------------------------------------------------------------------------|
@@ -153,8 +161,8 @@ On AUR rows `N?`, `N+` and `Nx` act through pacman like on any installed package
`pacman -Qi` with `<helper> -Sia PKG`; `N-` excludes the row through the helper's `--ignore`. `pacman -Qi` with `<helper> -Sia PKG`; `N-` excludes the row through the helper's `--ignore`.
Numbers are stable for the whole session (removed rows leave a gap, never renumber). Numbers are stable for the whole session (removed rows leave a gap, never renumber).
All tokens of a line are validated first: an unknown token or a number that is not listed Syntax is validated first; an unknown token rejects the line. Numbers that are not listed
prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without print an error and are skipped while the remaining commands run. EOF on stdin aborts without
upgrading (exit 2) so a piped run can never upgrade by accident unless it sends an empty upgrading (exit 2) so a piped run can never upgrade by accident unless it sends an empty
line. When purges leave no row at all, the session ends ("Nothing left to upgrade.", exit 0). line. When purges leave no row at all, the session ends ("Nothing left to upgrade.", exit 0).
+257 -70
View File
@@ -6,7 +6,7 @@
# and zypper: list what would be upgraded, interrogate every package, remember the # and zypper: list what would be upgraded, interrogate every package, remember the
# ones you trust, then run the native upgrade (with per-run exclusions). # ones you trust, then run the native upgrade (with per-run exclusions).
# #
# Version: 0.1.0 # Version: 0.6.0
# #
# Specification: docs/DESIGN.md. Single file, bash >= 4.3, no runtime dependency # 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 # 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). # never read from or written to a file (no HISTFILE; `history -r`/`-w`/`-a` are never used).
unset HISTFILE unset HISTFILE
readonly SU_VERSION=0.1.0 readonly SU_VERSION=0.6.0
readonly SU_PROG=skeptical-updater readonly SU_PROG=skeptical-updater
readonly SU_STATE_HEADER='# skeptical-updater trusted packages. Columns: backend package reason added(UTC ISO-8601)' 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' 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) none (default: paru, else yay, when installed; other backends ignore it)
--no-aur same as --aur-helper=none --no-aur same as --aur-helper=none
-h, --help this help -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): interactive commands (one line, whitespace-separated tokens, N = number in brackets):
<empty line> proceed: run the native upgrade with the current exclusions <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 that depend on it - installed packages only
N?? full native info: the installed record; lines starting with '*' come N?? full native info: the installed record; lines starting with '*' come
from the upstream candidate (differing values, fields it adds) 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 N- exclude from this run (toggle); shown with a '-' marker
Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused Nx purge it now with its now-unneeded dependencies (shown, asks y/N); refused
when other installed packages depend on it, like pacman -Rns 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 it forever (with its installed dependency closure)
N++ trust the topmost installed dependents and their dependency closures
*+ / *- apply + / - to every listed package *+ / *- apply + / - to every listed package
a show packages trusted during this session again
l redraw the list l redraw the list
q quit without upgrading (exit 0) q quit without upgrading (exit 0)
h, ? this help h, ? this help
@@ -138,12 +142,16 @@ session_help() {
<enter> upgrade now, skipping the rows marked '-' <enter> upgrade now, skipping the rows marked '-'
N? what is it: description, install reason + installed packages that need it N? what is it: description, install reason + installed packages that need it
N?? full native info: installed record, '*' lines from the upstream candidate 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) N- exclude N from this run (toggle)
Nx purge N now: lists every package the purge removes, then asks y/N. 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 x refuses when other installed packages depend on the target, like
pacman -Rns; purge the dependents first 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 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) *+ *- 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) l redraw the list (a '*' after the number marks an AUR package)
q quit without upgrading q quit without upgrading
h ? this help h ? this help
@@ -197,7 +205,7 @@ parse_args() {
usage usage
exit 0 exit 0
;; ;;
-V | --version) -v | --version)
printf '%s %s\n' "$SU_PROG" "$SU_VERSION" printf '%s %s\n' "$SU_PROG" "$SU_VERSION"
exit 0 exit 0
;; ;;
@@ -970,20 +978,21 @@ apt_closure() {
# is system-wide (it also takes pre-existing orphans, e.g. old kernels: verified), so the # 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. # orphans that were already autoremovable before are subtracted and the rest named explicitly.
apt_purge_plan() { apt_purge_plan() {
local pkg=$1 pre sim w name rest local pre sim w name rest
local -A before=() local -A before=() requested=()
local -a args=("$pkg") local -a args=("$@")
for name in "$@"; do requested[$name]=1; done
pre=$(apt-get -s autoremove </dev/null 2>/dev/null) || pre='' pre=$(apt-get -s autoremove </dev/null 2>/dev/null) || pre=''
while read -r w name rest; do while read -r w name rest; do
[[ $w == Remv || $w == Purg ]] && before[$name]=1 [[ $w == Remv || $w == Purg ]] && before[$name]=1
done <<<"$pre" 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 printf '%s\n' "$sim" >&2
return 1 return 1
} }
while read -r w name rest; do while read -r w name rest; do
[[ $w == Remv || $w == Purg ]] || continue [[ $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" done <<<"$sim"
sim=$(apt-get -s purge "${args[@]}" </dev/null 2>&1) || { sim=$(apt-get -s purge "${args[@]}" </dev/null 2>&1) || {
printf '%s\n' "$sim" >&2 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. # On refusal pacman explains on stdout (":: removing X breaks dependency ..."): show all of it.
pacman_purge_plan() { pacman_purge_plan() {
local out rc=0 l 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 if ((rc)); then
printf '%s\n' "$out" >&2 printf '%s\n' "$out" >&2
return "$rc" return "$rc"
@@ -1906,7 +1915,7 @@ dnf_purge_plan() {
local out line in=0 wrapped=0 local out line in=0 wrapped=0
local -a f=() 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)$' 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 != *'Transaction Summary'* ]]; then
if [[ $out == *'Nothing to do'* ]]; then return 0; fi if [[ $out == *'Nothing to do'* ]]; then return 0; fi
printf '%s\n' "$out" >&2 printf '%s\n' "$out" >&2
@@ -2089,7 +2098,7 @@ zypper_reason() {
} }
zypper_purge_plan() { zypper_purge_plan() {
local xml rc=0 sec name rest 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 if ((rc)); then
[[ $rc != 104 ]] || say "zypper: no installed package provides '$1' (exit 104)" >&2 [[ $rc != 104 ]] || say "zypper: no installed package provides '$1' (exit 104)" >&2
((rc == 104)) || printf '%s\n' "$xml" >&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=() 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_IDX=() # number -> row index
N_MAX=0 N_MAX=0
SESSION_SHOW_TRUSTED=0
declare -A SESSION_TRUSTED=()
load_rows() { load_rows() {
local out rc=0 line name old new repo flags i n=0 w aur shown 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 row_visible() { # row_visible IDX
local i=$1 local i=$1
((R_GONE[i] == 0 && R_NUM[i] > 0)) || return 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() { print_list() {
@@ -2291,6 +2302,8 @@ print_list() {
done done
if ((opt_all)); then if ((opt_all)); then
say "$total upgradable, $trusted trusted (marked with =), $review to review:" 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 else
say "$total upgradable, $trusted trusted (hidden, use --all to show), $review to review:" say "$total upgradable, $trusted trusted (hidden, use --all to show), $review to review:"
fi 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]} 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); # 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. # otherwise the package goes with the dependencies it alone needed, as the y/N prompt lists.
session_purge() { session_purge() {
local i=$1 name=${R_NAME[$1]} rc=0 out line p prompt rd d local i name rd out rc=0 p prompt idx dep changed
local -a plan=() others=() extra=() local -a targets=() target_names=() total_plan=() total_others=() extra=() rdeps=()
local -A still=() gone=() mine=() 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 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." 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 fi
rd=$(be rdepends "$name") || rd='' rd=$(be rdepends "$name") || rd=''
rd=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ') reverse[$name]=$(printf '%s\n' "$rd" | sed '/^$/d' | tr '\n' ' ')
rd=${rd% } done
if [[ -n $rd ]]; then # 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" say "not purged: $name is required by: ${rd// /, } - purge the dependents first"
return 0 rejected[$name]=1
changed=1
break
fi 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 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 return 0
fi fi
while IFS= read -r p; do while IFS= read -r p; do
[[ -n $p && -z ${gone[$p]+x} ]] || continue [[ -n $p ]] || continue
total_plan+=("$p")
gone[$p]=1 gone[$p]=1
plan+=("$p") [[ -n ${purge_selected[$p]+x} && -z ${rejected[$p]+x} ]] || total_others+=("$p")
[[ $p == "$name" ]] || others+=("$p") [[ -n ${allowed[$p]+x} && -z ${rejected[$p]+x} ]] || extra+=("$p")
done <<<"$out" done <<<"$out"
if ((${#plan[@]} == 0)); then if ((${#total_plan[@]} == 0)); then
say "$name is not installed: nothing to purge." say "${target_names[*]} is not installed: nothing to purge."
return 0 return 0
fi fi
# belt and braces: the plan may only hold the target and its own (installed) dependencies; for name in "${target_names[@]}"; do
# anything else means a dependent the reverse-dependency query missed (e.g. through a virtual) if [[ -z ${gone[$name]+x} ]]; then
if ((${#others[@]})); then err "cannot purge $name: $(be label) did not include it in the plan; nothing was removed"
closure_of "$name" return 0
for d in ${CLOSURE[@]+"${CLOSURE[@]}"}; do mine[$d]=1; done fi
for p in "${others[@]}"; do [[ -n ${mine[$p]+x} ]] || extra+=("$p"); done done
# belt and braces: no package outside the targets and their dependencies may be removed.
if ((${#extra[@]})); then 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 return 0
fi fi
fi prompt="Purge ${target_names[*]} now?"
prompt="Purge $name now?" ((${#total_others[@]} == 0)) || prompt+=" This also removes: $(join_by ', ' "${total_others[@]}")"
((${#others[@]} == 0)) || prompt+=" This also removes: $(join_by ', ' "${others[@]}")"
read_line "$prompt [y/N] " read_line "$prompt [y/N] "
case $REPLY in case $REPLY in
y | Y | yes | YES | Yes) ;; y | Y | yes | YES | Yes) ;;
*) *)
say "not purged: $name" say "not purged: ${target_names[*]}"
return 0 return 0
;; ;;
esac esac
be purge "${plan[@]}" || rc=$? rc=0
be purge "${total_plan[@]}" || rc=$?
if ((rc)); then 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 return 0
fi fi
say "purged: ${plan[*]}" say "purged: ${total_plan[*]}"
for ((i = 0; i < ${#R_NAME[@]}; i++)); do for ((i = 0; i < ${#R_NAME[@]}; i++)); do
if [[ -n ${gone[${R_NAME[i]}]+x} ]]; then if [[ -n ${gone[${R_NAME[i]}]+x} ]]; then
R_GONE[i]=1 R_GONE[i]=1
@@ -2712,15 +2762,42 @@ session_purge() {
} }
session_trust() { # session_trust IDX... session_trust() { # session_trust IDX...
local i now local i j d now covered
now=$(now_utc) local -a selected=() roots=()
local -A seen=() reach=()
for i in "$@"; do for i in "$@"; do
[[ -z ${seen[$i]+x} ]] || continue
seen[$i]=1
selected+=("$i")
done
for i in "${selected[@]}"; do
closure_of "${R_NAME[i]}"
for d in ${CLOSURE[@]+"${CLOSURE[@]}"}; do reach["$i,$d"]=1; done
done
for i in "${selected[@]}"; do
covered=0
for j in "${selected[@]}"; do
[[ $i != "$j" && -n ${reach[$j,${R_NAME[i]}]+x} ]] || continue
if [[ -z ${reach[$i,${R_NAME[j]}]+x} ]] || ((j < i)); then covered=1; break; fi
done
((covered)) || roots+=("$i")
done
now=$(now_utc)
for i in "${roots[@]}"; do
trust_pkg "${R_NAME[i]}" "$now" trust_pkg "${R_NAME[i]}" "$now"
R_EXCL[i]=0 R_EXCL[i]=0
done 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 # a row this trust hides must not stay excluded behind the user's back
for ((i = 0; i < ${#R_NAME[@]}; i++)); do 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 R_EXCL[i]=0
say " (${R_NAME[i]} is trusted now: no longer excluded from this run)" say " (${R_NAME[i]} is trusted now: no longer excluded from this run)"
fi fi
@@ -2728,6 +2805,62 @@ session_trust() { # session_trust IDX...
state_save 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() { session_toggle_all() {
local i all=1 local i all=1
local -a idx=() local -a idx=()
@@ -2747,26 +2880,33 @@ session_toggle_all() {
SESSION_PROCEED=0 SESSION_PROCEED=0
UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own UPGRADE_FAILED_MSG= # set by <be>_upgrade when the failing step is not the package manager's own
# session_line LINE: validate every token first (a typo never executes half a line), then run them 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() { session_line() {
local tok n op i local tok n op i name
local -a toks=() local -a toks=() batch=() purge_batch=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$' 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 read -r -a toks <<<"$1" || true
if ((${#toks[@]} == 0)); then if ((${#toks[@]} == 0)); then
SESSION_PROCEED=1 SESSION_PROCEED=1
return 0 return 0
fi fi
for tok in "${toks[@]}"; do for tok in "${toks[@]}"; do
if [[ $tok =~ $re ]]; then if [[ ! $tok =~ $re && ! $tok =~ $name_re ]]; then
n=$((10#${BASH_REMATCH[1]}))
if ! num_row "$n"; then
err "no package [$n] in the list (l to redraw)"
return 0
fi
else
case $tok in case $tok in
'*+' | '*-' | l | q | h | '?') ;; '*+' | '*-' | a | l | q | h | '?') ;;
*) *)
err "unknown command '$tok' (h for help)" err "unknown command '$tok' (h for help)"
return 0 return 0
@@ -2774,16 +2914,49 @@ session_line() {
esac esac
fi fi
done done
for ((n = 1; n <= N_MAX; n++)); do
if num_row "$n"; then
initial[$n]=$REPLY
named[${R_NAME[REPLY]}]=$REPLY
fi
done
for tok in "${toks[@]}"; do 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 if [[ $tok =~ $re ]]; then
n=$((10#${BASH_REMATCH[1]})) n=$((10#${BASH_REMATCH[1]}))
op=${BASH_REMATCH[2]} op=${BASH_REMATCH[2]}
if ! num_row "$n"; then if [[ -z ${initial[$n]+x} ]] || ((R_GONE[${initial[$n]:-0}])); then
err "package [$n] has left the list; ignoring the rest of the line" err "no package [$n] in the list (l to redraw)"
return 0 continue
fi
i=${initial[$n]}
if [[ $op == + ]]; then
batch+=("$i")
continue
fi
if [[ $op == x ]]; then
purge_batch+=("$i")
continue
fi fi
i=$REPLY
case $op in case $op in
'++') session_trust_dependents "$i" ;;
'?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;; '?') print_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
'??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;; '??') print_full_info "${R_NAME[i]}" "${R_OLD[i]}" || true ;;
-) -)
@@ -2795,15 +2968,20 @@ session_line() {
say "excluded from this run: ${R_NAME[i]}" say "excluded from this run: ${R_NAME[i]}"
fi fi
;; ;;
x)
session_purge "$i"
if (($(rows_left) == 0 && ${#R_NOTES[@]} == 0)); then
say "Nothing left to upgrade."
exit 0
fi
;;
+) session_trust "$i" ;;
esac 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 else
case $tok in case $tok in
'*+') '*+')
@@ -2814,6 +2992,13 @@ session_line() {
if ((${#idx[@]})); then session_trust "${idx[@]}"; else say "nothing to trust"; fi if ((${#idx[@]})); then session_trust "${idx[@]}"; else say "nothing to trust"; fi
;; ;;
'*-') session_toggle_all ;; '*-') 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 ;; l) print_list ;;
q) q)
say "Quit: nothing was upgraded." say "Quit: nothing was upgraded."
@@ -2823,6 +3008,8 @@ session_line() {
esac esac
fi fi
done done
if ((${#batch[@]})); then session_trust "${batch[@]}"; fi
session_flush_purges purge_batch
} }
cmd_interactive() { 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.' refute_output --partial 'skeptical-updater.'
[[ ! -e $STATE/zypper-private ]] [[ ! -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]' assert_line 'usage: skeptical-updater [options] [command]'
} }
@test "--version and -V print the version" { @test "--version and -v print the version" {
run skup --version run skup --version
assert_success assert_success
assert_output 'skeptical-updater 0.1.0' assert_output --regexp '^skeptical-updater [0-9]+\.[0-9]+\.[0-9]+(\+(alpha|beta|rc)[.-]?[0-9]+)?$'
run skup -V local version=$output
assert_output 'skeptical-updater 0.1.0' run skup -v
assert_success
assert_output "$version"
} }
@test "an unknown option is a usage error (exit 1)" { @test "an unknown option is a usage error (exit 1)" {
+6 -7
View File
@@ -137,21 +137,20 @@ case $op in
-Rs) -Rs)
((print)) || exit 98 ((print)) || exit 98
tg=${targets[0]} 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 "checking dependencies..."
echo ":: removing $tg breaks dependency '$tg' required by su-dummy-a" echo ":: removing $tg breaks dependency '$tg' required by su-dummy-a"
echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2 echo "error: failed to prepare transaction (could not satisfy dependencies)" >&2
exit 1 exit 1
fi fi
for tg in "${targets[@]}"; do
case $tg in 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-a) printf '%s\n' su-dummy-a su-dummy-b ;;
su-dummy-c | glib2 | expat | ca-certificates-mozilla | pfetch | su-dummy-aur) echo "$tg" ;; # (the last two: foreign, AUR) 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 ;;
echo "error: target not found: $tg" >&2
exit 1
;;
esac esac
for r in ${FAKE_PACMAN_RS_EXTRA:-}; do [[ ${r%%:*} == "$tg" ]] && echo "${r#*:}"; done for r in ${FAKE_PACMAN_RS_EXTRA:-}; do [[ ${r%%:*} == "$tg" ]] && echo "${r#*:}"; done
done
;; ;;
-Rns) -Rns)
rc=$(fake_rc FAKE_PACMAN_RNS_RC 0) rc=$(fake_rc FAKE_PACMAN_RNS_RC 0)
+230 -2
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' 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" { @test "N?? on a new dependency (old version -) never looks for an installed record" {
FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq' FAKE_APT_SIM=real run skup --no-refresh <<<$'9??\nq'
assert_success assert_success
@@ -292,11 +340,49 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc
run skup --no-refresh <<<$'*+\nl\nq' run skup --no-refresh <<<$'*+\nl\nq'
assert_success assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)' assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line 'trusted su-dummy-b'
assert_line 'trusted su-dummy-c' assert_line 'trusted su-dummy-c'
assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:' assert_line '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:'
run cut -f2,3 "$STATE_FILE" run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-b${T}user" assert_line "su-dummy-b${T}dep:su-dummy-a"
}
@test "consecutive trust commands keep dependencies under their selected root in either order" {
for line in '1+ 2+ 3+' '2+ 3+ 1+'; do
rm -rf "$STATE"
run skup --no-refresh <<<"$line"$'\nq'
assert_success
assert_line 'trusted su-dummy-a (+1 dependency: su-dummy-b)'
assert_line 'trusted su-dummy-c'
refute_output --partial 'no package ['
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
assert_line "su-dummy-c${T}user"
done
}
@test "invalid numbers do not stop other commands on the line" {
run skup --no-refresh <<<$'9+ 2- 0? 3+\nq'
assert_success
assert_output --partial 'no package [9] in the list'
assert_output --partial 'no package [0] in the list'
assert_line 'excluded from this run: su-dummy-b'
assert_line 'trusted su-dummy-c'
}
@test "a package removed by a purge does not stop later commands" {
FAKE_APT_AUTO_ORPHANS=su-dummy-a:su-dummy-b run skup --no-refresh <<<$'1x 2? 3-\ny\nq'
assert_success
assert_output --partial 'no package [2] in the list'
assert_line 'excluded from this run: su-dummy-c'
}
@test "trusting a dependency and its root with --all keeps the dependency implicit" {
run skup --no-refresh --all <<<$'2+ 1+\nq'
assert_success
run cut -f2,3 "$STATE_FILE"
assert_line "su-dummy-a${T}user"
assert_line "su-dummy-b${T}dep:su-dummy-a"
} }
@test "*- excludes every listed package; *- again re-includes them" { @test "*- excludes every listed package; *- again re-includes them" {
@@ -371,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 ' [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' 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'
}