From 33ebf89748349be106142c3e7f7699fa55ce3a45 Mon Sep 17 00:00:00 2001 From: Bryan Joshua Pedini Date: Fri, 30 Oct 2026 21:07:05 +0100 Subject: [PATCH] feat: resolve dependencies in batch trust commands --- README.md | 9 ++++-- docs/DESIGN.md | 7 +++-- skeptical-updater | 58 +++++++++++++++++++++++++------------ tests/unit/interactive.bats | 42 +++++++++++++++++++++++++-- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 8b267fa..e903f53 100644 --- a/README.md +++ b/README.md @@ -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 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 -listed prints an error, runs nothing, and the prompt comes back. +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. | token | action | |-------|--------| @@ -147,7 +147,10 @@ listed prints an error, runs nothing, and the prompt comes back. | `h`, `?` | help | | 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 session ends by itself (exit 0). diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 4a59f78..6505d56 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -125,6 +125,9 @@ 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. +Consecutive `N+` commands and `*+` trust the selected dependency roots, recording +other selected packages in their closures as dependencies regardless of input order. | token | action | |--------|---------------------------------------------------------------------------| @@ -153,8 +156,8 @@ On AUR rows `N?`, `N+` and `Nx` act through pacman like on any installed package `pacman -Qi` with ` -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). -All tokens of a line are validated first: an unknown token or a number that is not listed -prints an error, runs nothing of that line and re-prompts. EOF on stdin aborts without +Syntax is validated first; an unknown token rejects the line. Numbers that are not listed +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 line. When purges leave no row at all, the session ends ("Nothing left to upgrade.", exit 0). diff --git a/skeptical-updater b/skeptical-updater index fe715ff..81a2eb3 100755 --- a/skeptical-updater +++ b/skeptical-updater @@ -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.1.0 +# Version: 0.2.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.1.0 +readonly SU_VERSION=0.2.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=' 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... - local i now - now=$(now_utc) + local i j d now covered + local -a selected=() roots=() + local -A seen=() reach=() 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" R_EXCL[i]=0 done @@ -2747,10 +2766,10 @@ session_toggle_all() { SESSION_PROCEED=0 UPGRADE_FAILED_MSG= # set by _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() { local tok n op i - local -a toks=() + local -a toks=() batch=() + local -A initial=() local re='^([0-9]{1,6})(\?\?|\?|-|x|\+)$' read -r -a toks <<<"$1" || true if ((${#toks[@]} == 0)); then @@ -2758,13 +2777,7 @@ session_line() { return 0 fi for tok in "${toks[@]}"; do - 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 + if [[ ! $tok =~ $re ]]; then case $tok in '*+' | '*-' | l | q | h | '?') ;; *) @@ -2774,15 +2787,23 @@ session_line() { esac fi done + for ((n = 1; n <= N_MAX; n++)); do + if num_row "$n"; then initial[$n]=$REPLY; fi + done for tok in "${toks[@]}"; do if [[ $tok =~ $re ]]; then n=$((10#${BASH_REMATCH[1]})) op=${BASH_REMATCH[2]} - if ! num_row "$n"; then - err "package [$n] has left the list; ignoring the rest of the line" - return 0 + if [[ -z ${initial[$n]+x} ]] || ((R_GONE[${initial[$n]:-0}])); then + err "no package [$n] in the list (l to redraw)" + continue fi - i=$REPLY + i=${initial[$n]} + if [[ $op == + ]]; then + batch+=("$i") + continue + fi + if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi case $op in '?') print_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 fi ;; - +) session_trust "$i" ;; esac else + if ((${#batch[@]})); then session_trust "${batch[@]}"; batch=(); fi case $tok in '*+') local -a idx=() @@ -2823,6 +2844,7 @@ session_line() { esac fi done + if ((${#batch[@]})); then session_trust "${batch[@]}"; fi } cmd_interactive() { diff --git a/tests/unit/interactive.bats b/tests/unit/interactive.bats index 19e842c..463cc62 100644 --- a/tests/unit/interactive.bats +++ b/tests/unit/interactive.bats @@ -292,11 +292,49 @@ UPGRADE='apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--forc run skup --no-refresh <<<$'*+\nl\nq' assert_success 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 '3 upgradable, 3 trusted (hidden, use --all to show), 0 to review:' 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" {