feat: resolve dependencies in batch trust commands

This commit is contained in:
bryanpedini committed 2026-10-30 21:07:05 +01:00
1 parent 3af159e09a
commit 33ebf89748
4 files changed
+91 -25

No files matched your search

+6 -3
View File
@@ -130,8 +130,8 @@ 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. that is not listed prints an error and the remaining commands still run.
| token | action | | token | action |
|-------|--------| |-------|--------|
@@ -147,7 +147,10 @@ listed prints an error, runs nothing, and the prompt comes back.
| `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
`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).
+5 -2
View File
@@ -125,6 +125,9 @@ 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.
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 +156,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).
+40 -18
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.2.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.2.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'
@@ -2712,9 +2712,28 @@ 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
@@ -2747,10 +2766,10 @@ 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_line() { session_line() {
local tok n op i local tok n op i
local -a toks=() local -a toks=() batch=()
local -A initial=()
local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$' local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$'
read -r -a toks <<<"$1" || true read -r -a toks <<<"$1" || true
if ((${#toks[@]} == 0)); then if ((${#toks[@]} == 0)); then
@@ -2758,13 +2777,7 @@ session_line() {
return 0 return 0
fi fi
for tok in "${toks[@]}"; do for tok in "${toks[@]}"; do
if [[ $tok =~ $re ]]; then if [[ ! $tok =~ $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 | '?') ;; '*+' | '*-' | l | q | h | '?') ;;
*) *)
@@ -2774,15 +2787,23 @@ session_line() {
esac esac
fi fi
done done
for ((n = 1; n <= N_MAX; n++)); do
if num_row "$n"; then initial[$n]=$REPLY; fi
done
for tok in "${toks[@]}"; do for tok in "${toks[@]}"; do
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 fi
i=$REPLY i=${initial[$n]}
if [[ $op == + ]]; then
batch+=("$i")
continue
fi
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $op in case $op in
'?') 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 ;;
@@ -2802,9 +2823,9 @@ session_line() {
exit 0 exit 0
fi fi
;; ;;
+) session_trust "$i" ;;
esac esac
else else
if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi
case $tok in case $tok in
'*+') '*+')
local -a idx=() local -a idx=()
@@ -2823,6 +2844,7 @@ session_line() {
esac esac
fi fi
done done
if ((${#batch[@]})); then session_trust "${batch[@]}"; fi
} }
cmd_interactive() { cmd_interactive() {
+40 -2
View File
@@ -292,11 +292,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" {