mirror of
https://gitlab.com/khumba/nvd.git
synced 2024-11-23 13:21:46 +01:00
Add --sort option (issue #17).
This commit is contained in:
parent
20bcbda4b5
commit
b2cf56efff
3 changed files with 205 additions and 17 deletions
|
@ -2,6 +2,9 @@
|
|||
|
||||
## 0.2.4 (unreleased)
|
||||
|
||||
- Added `--sort` option for controlling the order packages are listed in, for
|
||||
issue #17.
|
||||
|
||||
- In the "Version changes" section of a diff, highlight the portions of version
|
||||
numbers that have changed, i.e., everything after the part common to all
|
||||
versions (issue #17). This makes it easier to spot whether packages have had
|
||||
|
|
198
src/nvd
198
src/nvd
|
@ -43,11 +43,12 @@ import os.path
|
|||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from functools import total_ordering
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import cmp_to_key, total_ordering
|
||||
from pathlib import Path
|
||||
from signal import SIGPIPE, SIG_DFL, signal
|
||||
from subprocess import PIPE
|
||||
from typing import Optional, Union
|
||||
from typing import Any, Iterable, Optional, Union
|
||||
|
||||
signal(SIGPIPE, SIG_DFL) # Python handles SIGPIPE improperly by default.
|
||||
|
||||
|
@ -283,9 +284,11 @@ class PackageSet:
|
|||
def all_store_paths(self):
|
||||
return iter(self._store_paths)
|
||||
|
||||
def get_pname_packages(self, pname: str) -> list[Package]:
|
||||
return self._packages_by_pname.get(pname, [])
|
||||
|
||||
def get_pname_versions(self, pname: str) -> list[Version]:
|
||||
pkgs = self._packages_by_pname.get(pname, [])
|
||||
return [pkg.version() for pkg in pkgs]
|
||||
return [pkg.version() for pkg in self.get_pname_packages(pname)]
|
||||
|
||||
class PackageSetPair:
|
||||
def __init__(
|
||||
|
@ -343,6 +346,121 @@ class PackageSetPair:
|
|||
self._right_set.contains_pname(pname)
|
||||
return in_left_set != in_right_set
|
||||
|
||||
def cmp(a, b) -> int:
|
||||
return (a > b) - (a < b)
|
||||
|
||||
class PackageListEntry:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
left_packages: list[Package],
|
||||
right_packages: Optional[list[Package]],
|
||||
) -> None:
|
||||
assert left_packages or right_packages, "No packages given."
|
||||
|
||||
self._left_packages = left_packages
|
||||
self._right_packages = right_packages
|
||||
|
||||
def get_pname(self) -> str:
|
||||
if self._right_packages:
|
||||
return self._right_packages[0].pname()
|
||||
else:
|
||||
return self._left_packages[0].pname()
|
||||
|
||||
def get_left_packages(self) -> list[Package]:
|
||||
return self._left_packages
|
||||
|
||||
def get_right_packages(self) -> Optional[list[Package]]:
|
||||
return self._right_packages
|
||||
|
||||
class PackageListEntryComparator:
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cmp_packages(self, a: PackageListEntry, b: PackageListEntry) -> int:
|
||||
pass
|
||||
|
||||
def reversed(self) -> "PackageListEntryComparator":
|
||||
return PackageListEntryReverseComparator(self)
|
||||
|
||||
class PackageListEntryReverseComparator(PackageListEntryComparator):
|
||||
def __init__(self, delegate: PackageListEntryComparator):
|
||||
self._delegate = delegate
|
||||
|
||||
def name(self) -> str:
|
||||
return "-" + self._delegate.name()
|
||||
|
||||
def cmp_packages(self, a: PackageListEntry, b: PackageListEntry) -> int:
|
||||
return self._delegate.cmp_packages(b, a)
|
||||
|
||||
def reversed(self) -> PackageListEntryComparator:
|
||||
return self._delegate
|
||||
|
||||
class PackageListEntryNameComparator(PackageListEntryComparator):
|
||||
def name(self) -> str:
|
||||
return "name"
|
||||
|
||||
def cmp_packages(self, a: PackageListEntry, b: PackageListEntry) -> int:
|
||||
return cmp(
|
||||
a.get_left_packages()[0].pname(),
|
||||
b.get_left_packages()[0].pname(),
|
||||
)
|
||||
|
||||
ALL_SORTS: dict[str, PackageListEntryComparator] = {
|
||||
sk.name(): sk
|
||||
for sk in (
|
||||
# The first one listed here is the default.
|
||||
PackageListEntryNameComparator(),
|
||||
)
|
||||
}
|
||||
|
||||
DEFAULT_SORT: PackageListEntryComparator = next(iter(ALL_SORTS.values()))
|
||||
|
||||
class PackageListEntryCombinedComparator(PackageListEntryComparator):
|
||||
def __init__(self, comparators: Iterable[PackageListEntryComparator]):
|
||||
self._comparators = list(comparators)
|
||||
|
||||
@staticmethod
|
||||
def from_str(val: Optional[str]):
|
||||
if not val:
|
||||
val = DEFAULT_SORT.name()
|
||||
|
||||
comparators: list[PackageListEntryComparator] = []
|
||||
|
||||
specs = val.split(",")
|
||||
for spec in specs:
|
||||
reverse = spec.startswith("-")
|
||||
if reverse:
|
||||
spec = spec[1:]
|
||||
|
||||
comparator = ALL_SORTS.get(spec, None)
|
||||
if comparator is None:
|
||||
sys.stderr.write(f"nvd: Error, unknown sort key '{spec}'.\n")
|
||||
sys.exit(1)
|
||||
|
||||
if reverse:
|
||||
comparator = comparator.reversed()
|
||||
|
||||
comparators.append(comparator)
|
||||
|
||||
return PackageListEntryCombinedComparator(comparators)
|
||||
|
||||
def name(self) -> str:
|
||||
return ",".join(c.name() for c in self._comparators)
|
||||
|
||||
def cmp_packages(self, a: PackageListEntry, b: PackageListEntry) -> int:
|
||||
for comparator in self._comparators:
|
||||
result = comparator.cmp_packages(a, b)
|
||||
if result != 0:
|
||||
return result
|
||||
|
||||
return 0
|
||||
|
||||
def reversed(self) -> PackageListEntryComparator:
|
||||
return PackageListEntryCombinedComparator(c.reversed() for c in self._comparators)
|
||||
|
||||
def parse_pname_version(path: str) -> tuple[str, Optional[str]]:
|
||||
base_path = str(StorePath(path).to_base_path().path())
|
||||
|
||||
|
@ -398,6 +516,7 @@ def render_versions(
|
|||
def print_package_list(
|
||||
*,
|
||||
pnames: list[str],
|
||||
sort_comparator: Optional[PackageListEntryComparator],
|
||||
selected_sets: PackageSetPair,
|
||||
left_package_set: PackageSet,
|
||||
right_package_set: Optional[PackageSet] = None,
|
||||
|
@ -412,6 +531,26 @@ def print_package_list(
|
|||
print("No packages to display.")
|
||||
return
|
||||
|
||||
# If we've been asked to sort, then apply the sort to the pnames list, since
|
||||
# we use that list's order to print things.
|
||||
if sort_comparator is not None:
|
||||
entries_to_sort: list[PackageListEntry] = [
|
||||
PackageListEntry(
|
||||
left_packages=left_package_set.get_pname_packages(pname),
|
||||
right_packages=(
|
||||
right_package_set.get_pname_packages(pname)
|
||||
if right_package_set is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
for pname in pnames
|
||||
]
|
||||
|
||||
entries_to_sort.sort(key=cmp_to_key(sort_comparator.cmp_packages))
|
||||
|
||||
pnames = [e.get_pname() for e in entries_to_sort]
|
||||
del entries_to_sort
|
||||
|
||||
count = 1
|
||||
count_width = len(str(len(pnames)))
|
||||
count_format_str = "#{:0" + str(count_width) + "d}"
|
||||
|
@ -614,7 +753,13 @@ def query_nix_version() -> Version:
|
|||
raise RuntimeError(
|
||||
"Could not determine Nix version from 'nix-store --version' output: {output!r}")
|
||||
|
||||
def run_list(*, root, only_selected, name_patterns):
|
||||
def run_list(
|
||||
*,
|
||||
root,
|
||||
only_selected,
|
||||
name_patterns,
|
||||
sort_comparator: PackageListEntryComparator,
|
||||
):
|
||||
path = Path(root)
|
||||
|
||||
if not path.exists():
|
||||
|
@ -648,6 +793,7 @@ def run_list(*, root, only_selected, name_patterns):
|
|||
left_package_set=closure_set,
|
||||
right_package_set=None,
|
||||
fixed_install_state=INST_INSTALLED,
|
||||
sort_comparator=sort_comparator,
|
||||
)
|
||||
|
||||
def run_diff(
|
||||
|
@ -655,6 +801,7 @@ def run_diff(
|
|||
root1,
|
||||
root2,
|
||||
no_version_suffix_highlight: bool,
|
||||
sort_comparator: PackageListEntryComparator,
|
||||
):
|
||||
left_path = Path(root1)
|
||||
right_path = Path(root2)
|
||||
|
@ -691,9 +838,9 @@ def run_diff(
|
|||
left_package_names = set(left_closure_set.all_pnames())
|
||||
right_package_names = set(right_closure_set.all_pnames())
|
||||
|
||||
common_package_names = sorted(left_package_names & right_package_names)
|
||||
left_only_package_names = sorted(left_package_names - right_package_names)
|
||||
right_only_package_names = sorted(right_package_names - left_package_names)
|
||||
common_package_names = left_package_names & right_package_names
|
||||
left_only_package_names = left_package_names - right_package_names
|
||||
right_only_package_names = right_package_names - left_package_names
|
||||
|
||||
# Announce version changes.
|
||||
package_names_with_changed_versions = []
|
||||
|
@ -716,6 +863,7 @@ def run_diff(
|
|||
left_package_set=left_closure_set,
|
||||
right_package_set=right_closure_set,
|
||||
no_version_suffix_highlight=no_version_suffix_highlight,
|
||||
sort_comparator=sort_comparator,
|
||||
)
|
||||
|
||||
# Announce specific changes for packages whose versions haven't changed.
|
||||
|
@ -727,6 +875,7 @@ def run_diff(
|
|||
selected_sets=selected_sets,
|
||||
left_package_set=left_closure_set,
|
||||
fixed_install_state=INST_CHANGED,
|
||||
sort_comparator=sort_comparator,
|
||||
)
|
||||
|
||||
# Announce added packages.
|
||||
|
@ -738,6 +887,7 @@ def run_diff(
|
|||
selected_sets=selected_sets,
|
||||
left_package_set=right_closure_set, # Yes, this is correct.
|
||||
fixed_install_state=INST_ADDED,
|
||||
sort_comparator=sort_comparator,
|
||||
)
|
||||
|
||||
# Announce removed packages.
|
||||
|
@ -749,6 +899,7 @@ def run_diff(
|
|||
selected_sets=selected_sets,
|
||||
left_package_set=left_closure_set,
|
||||
fixed_install_state=INST_REMOVED,
|
||||
sort_comparator=sort_comparator,
|
||||
)
|
||||
|
||||
if not any_changes_displayed:
|
||||
|
@ -778,6 +929,13 @@ def run_diff(
|
|||
)
|
||||
|
||||
def parse_args():
|
||||
def add_sort_arg(p: argparse.ArgumentParser) -> None:
|
||||
p.add_argument(
|
||||
"--sort",
|
||||
default=DEFAULT_SORT.name(),
|
||||
help="Sort order for displayed packages.",
|
||||
)
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Nix/NixOS package version diff tool",
|
||||
epilog="See the nvd(1) manual page for more information.",
|
||||
|
@ -804,6 +962,7 @@ def parse_args():
|
|||
diff_parser = subparsers.add_parser(
|
||||
"diff",
|
||||
help="Diff two Nix store paths and their closures.")
|
||||
add_sort_arg(diff_parser)
|
||||
diff_parser.add_argument(
|
||||
"--no-version-suffix-highlight",
|
||||
action="store_true",
|
||||
|
@ -818,6 +977,7 @@ def parse_args():
|
|||
list_parser = subparsers.add_parser(
|
||||
"list",
|
||||
help="List packages in the closure of a Nix store path.")
|
||||
add_sort_arg(list_parser)
|
||||
list_parser.add_argument(
|
||||
"-r", "--root",
|
||||
default="/run/current-system",
|
||||
|
@ -845,15 +1005,15 @@ def main():
|
|||
global INST_DOWNGRADED
|
||||
global INST_CHANGED
|
||||
|
||||
args = parse_args()
|
||||
action = args.action
|
||||
NIX_BIN_DIR = args.nix_bin_dir or None
|
||||
args = vars(parse_args())
|
||||
action = args["action"]
|
||||
NIX_BIN_DIR = args["nix_bin_dir"] or None
|
||||
USE_COLOUR = \
|
||||
args.color == "always" \
|
||||
or (args.color == "auto" and sys.stdout.isatty() and not os.getenv('NO_COLOR'))
|
||||
del args.action
|
||||
del args.nix_bin_dir
|
||||
del args.color
|
||||
args["color"] == "always" \
|
||||
or (args["color"] == "auto" and sys.stdout.isatty() and not os.getenv('NO_COLOR'))
|
||||
del args["action"]
|
||||
del args["nix_bin_dir"]
|
||||
del args["color"]
|
||||
|
||||
if USE_COLOUR:
|
||||
INST_ADDED = sgr(SGR_FG + SGR_BRIGHT + SGR_GREEN) + INST_ADDED
|
||||
|
@ -862,6 +1022,10 @@ def main():
|
|||
INST_DOWNGRADED = sgr(SGR_FG + SGR_BRIGHT + SGR_YELLOW) + INST_DOWNGRADED
|
||||
INST_CHANGED = sgr(SGR_FG + SGR_BRIGHT + SGR_MAGENTA) + INST_CHANGED
|
||||
|
||||
if "sort" in args:
|
||||
args["sort_comparator"] = PackageListEntryCombinedComparator.from_str(args["sort"])
|
||||
del args["sort"]
|
||||
|
||||
if action is None:
|
||||
print("nvd: Subcommand required, see 'nvd --help'.")
|
||||
sys.exit(1)
|
||||
|
@ -874,7 +1038,7 @@ def main():
|
|||
{
|
||||
"list": run_list,
|
||||
"diff": run_diff,
|
||||
}[action](**vars(args))
|
||||
}[action](**args)
|
||||
|
||||
# Importing this module is kinda broken because of the constant initializations
|
||||
# at the top.
|
||||
|
|
21
src/nvd.1
21
src/nvd.1
|
@ -6,8 +6,15 @@ nvd \- Nix/NixOS package version diff tool
|
|||
.B nvd [ -h | --help ]
|
||||
.P
|
||||
.B nvd [ GLOBAL OPTIONS ] diff
|
||||
.RS
|
||||
.B [ --sort
|
||||
.I sort-order
|
||||
.B ]
|
||||
.br
|
||||
.I root1
|
||||
.br
|
||||
.I root2
|
||||
.RE
|
||||
.P
|
||||
.B nvd [ GLOBAL OPTIONS ] list
|
||||
.RS
|
||||
|
@ -17,6 +24,10 @@ nvd \- Nix/NixOS package version diff tool
|
|||
.br
|
||||
.B [ -s|--selected ]
|
||||
.br
|
||||
.B [ --sort
|
||||
.I sort-order
|
||||
.B ]
|
||||
.br
|
||||
.B [
|
||||
.I name-pattern
|
||||
.B ]*
|
||||
|
@ -130,6 +141,16 @@ etc. binaries to use. If provided, all invocations of Nix binaries will use
|
|||
this directory. If empty or not provided, then invocations of Nix binaries will
|
||||
use regular path lookup. This is the default.
|
||||
.TP
|
||||
--sort=<sort-order>
|
||||
Specifies the order to use to sort package lists. The argument is a
|
||||
comma-separted list of keywords indicating which properties to sort on. The
|
||||
first keyword given is the primary sort, and subsequent keywords are used to
|
||||
break ties, in order. Each keyword may be preceeded by a hyphen
|
||||
.B \-
|
||||
to reverse the regular sort order of the keyword. The default order is
|
||||
.B name,
|
||||
which sorts by package name. Currently only this keyword is supported.
|
||||
.TP
|
||||
-r|--root <store-path>
|
||||
For the
|
||||
.B list
|
||||
|
|
Loading…
Reference in a new issue