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

+49 -4
View File
@@ -42,7 +42,7 @@ commands:
untrust PKG... remove rows for PKG (and rows whose reason is dep:PKG)
refresh recompute dependency closures for every 'user' root
info PKG description, install reason and installed dependents (as N?)
show PKG full native package information (as N??)
show PKG installed record merged with the upstream candidate (as N??)
options:
--backend=NAME force apt|dnf|pacman|zypper (default: detect pacman > apt > dnf/yum > zypper)
@@ -128,7 +128,7 @@ listed prints an error, runs nothing, and the prompt comes back.
|-------|--------|
| empty line | proceed: run the native upgrade with the current exclusions |
| `N?` | what is it: the one-line description, `reason:` how it got installed (`explicitly installed`, `installed as a dependency`, `unknown` when the package manager cannot tell, `not installed` for a new dependency) and `required by:` the packages that depend on it, or `nothing`. That list covers installed packages only |
| `N??` | full native info (`apt-cache show`, `pacman -Qi` + `-Si`, `dnf info`, `zypper info --requires`) |
| `N??` | full native info: the installed record, with the upstream candidate merged in as lines starting with `>` (see below). zypper shows `zypper info --requires` as it is |
| `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) |
@@ -142,6 +142,47 @@ Several tokens may share a line (`1? 3- 4+`); they run left to right. With `--al
trusted packages are listed too, marked with `=`. When purges leave nothing to upgrade the
session ends by itself (exit 0).
`N??` (and `show PKG`) prints the installed record in the package manager's own layout
(`dpkg-query -s`, `pacman -Qi`, `dnf info --installed`), preceded by one `#` line saying how to read
it. Right after each field whose value differs in the upstream candidate (`apt-cache show`,
`pacman -Si` from the repository pacman would use, `dnf info --upgrades`) comes the candidate's line,
prefixed with `>`; fields only the candidate has (download size, checksums, repository) follow at the
end the same way, and fields only the installed record has (install reason, conffiles,
`Required By`) stay as they are. Identical fields are printed once; multi-line values (descriptions, pacman
`Optional Deps`, apt continuation lines) are compared as blocks and, when they differ, every line of
the candidate's block gets the `>`. A package that is not installed shows the candidate alone. When
the two records cannot be paired (zypper, whose `rpm -qi` and `zypper info` share almost no keys; no
candidate; several installed architectures) the plain native output is shown. On Debian 12, with
`su-dummy-b` 1.0 installed and 2.0 available:
```
> 2??
# lines starting with > come from the upstream candidate: a differing value, or a field the installed record lacks
Package: su-dummy-b
Status: install ok installed
Priority: optional
Section: misc
Maintainer: Skeptical Updater Fixture <su-fixture@example.invalid>
Architecture: all
Version: 1.0
> Version: 2.0
Conffiles:
/etc/su-dummy/su-dummy-b.conf 607e2d3a119dfe832d937ce579ef1079
Description: dummy package su-dummy-b for skeptical-updater tests
This package only exists to exercise the package manager.
.
It ships one data file and one conffile (version 1.0).
> Description: dummy package su-dummy-b for skeptical-updater tests
> This package only exists to exercise the package manager.
> .
> It ships one data file and one conffile (version 2.0).
> Description-md5: 27e6732d8f16131f7512e1d4f1855cc3
> Filename: ./su-dummy-b_2.0_all.deb
> Size: 824
> MD5sum: 62274690c5324af5419515e9f430226b
> SHA256: e1c87480693d98770356f1dc55616dc416f7e3c3f71dc3967e94dec77bb36c16
```
Under the rows, `Note:` lines report pending work that is not a row: new dependencies or
replacements only the pacman transaction (`-Sup`) shows, packages dnf installs to obsolete
others, packages a Tumbleweed `dist-upgrade` removes. With no row but such work, the list
@@ -277,7 +318,9 @@ signal during the exit handler, the holds lock kept while a restore failed, jour
zypper configuration replayed only when no other run holds their lock, the pacman pre-check /
database install / `-Su` sequence and stale throwaway databases, statefile locking and merge,
the `mkdir` lock fallback (EPERM owner alive, no busy loop), an inaccessible state directory, the
sudo re-exec of the file itself and its loop guard), and the prompt on a real pty (`script`):
sudo re-exec of the file itself and its loop guard), the `N??` merge on recorded pacman, apt, dnf4
and dnf5 records (changed, identical, installed-only, candidate-only and multi-line fields, apt's
short candidate descriptions, dnf5's padding, the fallbacks), and the prompt on a real pty (`script`):
cursor editing, Up/Down history recall, the stashed line, the bell at both ends, no history file.
About 30 seconds.
@@ -296,7 +339,9 @@ mirror state. `su-dummy-b` is recorded as installed as a dependency on every bac
`--asdeps`, `apt-mark auto`, `dnf mark remove`, zypper's `AutoInstalled`) and `su-dummy-a` as
explicitly installed (dnf needs `dnf mark install`: it records plain `rpm` installs as
`unknown`). Then `tests/e2e/*.bats` exercise the real package manager: banner, list, `N?` (the
reason and `required by:` of both), `N??`, `N+` and the statefile, `trust`/`untrust`/`refresh`,
reason and `required by:` of both), `N??` (su-dummy-b's installed 1.0 record with `> Version`, and
`> Repository` on pacman and dnf; zypper's plain `zypper info`), `N+` and the statefile,
`trust`/`untrust`/`refresh`,
excluding `su-dummy-b` (with and without a pre-existing hold/lock/IgnorePkg/exclude on
`su-dummy-c`, which must survive byte for byte), trusting re-including an excluded
dependency, full upgrade, `Nx` (refused for su-dummy-b, which su-dummy-a needs; su-dummy-a