feat(show): merge the installed record with the upstream candidate

N?? and `show PKG` now print the installed record in the package
manager's own key/value layout, after one line explaining the format:

    # lines starting with > come from the upstream candidate: a differing
    value, or a field the installed record lacks

After each field whose candidate value differs comes the candidate's
line prefixed with "> "; fields only the candidate has are appended the
same way; installed-only fields stay; identical fields print once.
Multi-line values (pacman Optional Deps, dnf Description, RFC-822
continuation lines) are compared as blocks and a differing candidate
block gets "> " on every line. A package that is not installed shows the
candidate alone ("# not installed: this is the upstream candidate").

Merged where the backend gives comparable records for both sides:
- pacman: -Qi vs -Si REPO/PKG, REPO = the first repository -Sl lists
- apt: dpkg-query -s (Status, Conffiles, the long description; richer
  than apt-cache show PKG=<installed>, which gives the archive record
  when that version is still listed) vs apt-cache show --no-all-versions;
  Description-xx pairs with Description and a one-line candidate
  Description equal to the installed first line counts as identical
  (the archives ship the long text in Translation files)
- dnf: info --installed vs info --upgrades (--available lists other
  architectures first and nothing for an up-to-date package); not
  installed: --available; dnf5's differently padded keys are re-padded
zypper keeps `zypper info --requires`: rpm -qi and zypper info share
almost no keys and zypper cannot print the installed record itself.
No candidate, or not exactly one record per side, keeps the old output.

Formats verified in containers and recorded in discovery/*.md. Unit
tests replay recorded pacman, apt, dnf4 and dnf5 records; the E2E suite
checks 2?? on su-dummy-b on all 12 distros.
This commit is contained in:
bryanpedini committed 2026-10-10 14:24:39 +02:00
1 parent 296c77e319
commit 875c8294bf
30 files changed
+857 -45

No files matched your search

+30 -1
View File
@@ -99,7 +99,7 @@ Command grammar (one line, whitespace-separated tokens, each `N` is the number i
|--------|---------------------------------------------------------------------------|
| empty line | proceed: run the native upgrade with the current exclusions |
| `N?` | print three lines, none indented: `PKG: <one-line description>`, `reason: <r>` with `<r>` = `explicitly installed`, `installed as a dependency`, `unknown` (the backend cannot tell) or `not installed` (also, without asking the backend, for a row whose old version is `-`), and `required by: <a, b, ...>` - the reverse dependencies, installed packages only - or `required by: nothing` |
| `N??` | print the full native info dump (`apt-cache show` / `pacman -Qi` / `dnf info` / `zypper info --requires`) |
| `N??` | print the full native info: the installed record merged with the upstream candidate (see "Full info" below); zypper: `zypper info --requires` as it is |
| `N-` | exclude from this run (toggle; `N-` again re-includes); shown with `-` marker |
| `Nx` | purge it now, following pacman's `-Rns` principle on every backend: if any installed package still depends on it, refuse and print who (the reverse dependencies `N?` shows) - never cascade; otherwise simulate the native purge of it together with its now-unneeded dependencies, show that removal set ("This also removes: ...") and ask `y/N`. As a safety check, a simulated set containing anything that is neither the target nor in its installed dependency closure is refused too. Only a purge that ran removes rows (of every package it removed). A row whose old version is `-` (a new dependency, not installed) cannot be purged |
| `N+` | trust it: write it and its installed dependency closure to the statefile; the row leaves the list (with `--all` it stays, marked `=`). Rows that this trust hides are re-included if they were excluded |
@@ -126,6 +126,33 @@ history by itself). readline draws the prompt and the line on stderr, as bash do
stdin keeps the plain `IFS= read -r` with the prompt on stdout and each line echoed after it, so
piped runs and their logs are unchanged.
Full info (`N??`, `show PKG`). The first line is `# lines starting with > come from the upstream
candidate: a differing value, or a field the installed record lacks`; then the installed record in
its native key/value layout, as the base. After each field whose candidate value differs, the
candidate's line follows, prefixed with `> ` (same key formatting); fields only the candidate has
are appended at the end the same way; fields only the installed record has stay; identical fields
print once. A field starts on a line that begins with a non-blank character and holds a `:` (the
key is the text before it); lines beginning with a blank continue it, so multi-line values (pacman
`Optional Deps`, dnf `Description`, RFC-822 continuation lines) are compared as blocks, every line
trimmed, and a differing candidate block gets `> ` on every line. `Description-xx` (apt Translation
files) pairs with `Description`; on apt a one-line candidate `Description` equal to the first
installed line counts as identical (the archives carry only the short description, the long text
is in the Translation files). When both records align their `:` in one column but in different
ones (dnf5: 16 installed, 15 available), the candidate lines are re-padded to the installed one's.
| backend | installed record | upstream candidate |
|---------|------------------|--------------------|
| pacman | `pacman -Qi PKG` | `pacman -Si REPO/PKG`, REPO = the first repository `pacman -Sl` lists PKG in (as for the review list) |
| apt | `dpkg-query -s PKG` (Status, Conffiles, always the long description; `apt-cache show PKG=<installed>` gives the archive's record when that version is still listed: no Status/Conffiles, short description only); a `Status` of `not-installed`/`config-files` = not installed | `apt-cache show --no-all-versions PKG` |
| dnf | `dnf -q info --installed PKG` | `dnf -q info --upgrades PKG` (installed architecture only; `--available` also lists other architectures and, for an up-to-date package, other builds); not installed: `--available` |
| zypper | - | - (`rpm -qi` and `zypper info` share almost no keys - Version vs version-release, Arch, byte sizes, source names, description layout - and zypper cannot print the installed record by itself: `zypper info -r @System` is refused) |
A package that is not installed (a row whose old version is `-` is not asked) shows the candidate
alone under `# not installed: this is the upstream candidate`. Anything else - no candidate (an
up-to-date dnf package, a local-only pacman package), not exactly one record on each side (two
installed architectures), zypper - prints `<be>_full_info` unchanged. `dnf info` headings
("Installed Packages", ...) are dropped. Verified formats: `discovery/{pacman,apt,rpm}.md`.
Flags shown after a row: `[held]` = the package manager will not upgrade it on its own: on apt
a hold (`apt-mark hold`) or a package apt keeps back (the "kept back" block of
`apt-get -s --with-new-pkgs upgrade`), on pacman an `IgnorePkg` (`[ignored]` in `pacman -Qu`).
@@ -213,6 +240,8 @@ Every backend is a set of bash functions with a common prefix. All output is
# plus "#<note>" lines for pending work that is not a row
<be>_describe PKG # stdout: one-line summary
<be>_full_info PKG # stdout: native full dump (candidate version if available)
<be>_installed_record PKG # optional (pacman, apt, dnf): stdout: ONE native record of the installed package
<be>_candidate_record PKG installed|not-installed # optional, with the above: ONE native record of the candidate
<be>_rdepends PKG # stdout: installed packages requiring PKG, one per line
<be>_reason PKG # stdout: explicit | dependency | unknown | not-installed (how PKG got installed)
<be>_closure PKG # stdout: installed recursive dependency closure of PKG (excluding PKG), one per line