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)
|
## 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
|
- 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
|
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
|
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 re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
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 pathlib import Path
|
||||||
from signal import SIGPIPE, SIG_DFL, signal
|
from signal import SIGPIPE, SIG_DFL, signal
|
||||||
from subprocess import PIPE
|
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.
|
signal(SIGPIPE, SIG_DFL) # Python handles SIGPIPE improperly by default.
|
||||||
|
|
||||||
|
@ -283,9 +284,11 @@ class PackageSet:
|
||||||
def all_store_paths(self):
|
def all_store_paths(self):
|
||||||
return iter(self._store_paths)
|
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]:
|
def get_pname_versions(self, pname: str) -> list[Version]:
|
||||||
pkgs = self._packages_by_pname.get(pname, [])
|
return [pkg.version() for pkg in self.get_pname_packages(pname)]
|
||||||
return [pkg.version() for pkg in pkgs]
|
|
||||||
|
|
||||||
class PackageSetPair:
|
class PackageSetPair:
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -343,6 +346,121 @@ class PackageSetPair:
|
||||||
self._right_set.contains_pname(pname)
|
self._right_set.contains_pname(pname)
|
||||||
return in_left_set != in_right_set
|
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]]:
|
def parse_pname_version(path: str) -> tuple[str, Optional[str]]:
|
||||||
base_path = str(StorePath(path).to_base_path().path())
|
base_path = str(StorePath(path).to_base_path().path())
|
||||||
|
|
||||||
|
@ -398,6 +516,7 @@ def render_versions(
|
||||||
def print_package_list(
|
def print_package_list(
|
||||||
*,
|
*,
|
||||||
pnames: list[str],
|
pnames: list[str],
|
||||||
|
sort_comparator: Optional[PackageListEntryComparator],
|
||||||
selected_sets: PackageSetPair,
|
selected_sets: PackageSetPair,
|
||||||
left_package_set: PackageSet,
|
left_package_set: PackageSet,
|
||||||
right_package_set: Optional[PackageSet] = None,
|
right_package_set: Optional[PackageSet] = None,
|
||||||
|
@ -412,6 +531,26 @@ def print_package_list(
|
||||||
print("No packages to display.")
|
print("No packages to display.")
|
||||||
return
|
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 = 1
|
||||||
count_width = len(str(len(pnames)))
|
count_width = len(str(len(pnames)))
|
||||||
count_format_str = "#{:0" + str(count_width) + "d}"
|
count_format_str = "#{:0" + str(count_width) + "d}"
|
||||||
|
@ -614,7 +753,13 @@ def query_nix_version() -> Version:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Could not determine Nix version from 'nix-store --version' output: {output!r}")
|
"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)
|
path = Path(root)
|
||||||
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
@ -648,6 +793,7 @@ def run_list(*, root, only_selected, name_patterns):
|
||||||
left_package_set=closure_set,
|
left_package_set=closure_set,
|
||||||
right_package_set=None,
|
right_package_set=None,
|
||||||
fixed_install_state=INST_INSTALLED,
|
fixed_install_state=INST_INSTALLED,
|
||||||
|
sort_comparator=sort_comparator,
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_diff(
|
def run_diff(
|
||||||
|
@ -655,6 +801,7 @@ def run_diff(
|
||||||
root1,
|
root1,
|
||||||
root2,
|
root2,
|
||||||
no_version_suffix_highlight: bool,
|
no_version_suffix_highlight: bool,
|
||||||
|
sort_comparator: PackageListEntryComparator,
|
||||||
):
|
):
|
||||||
left_path = Path(root1)
|
left_path = Path(root1)
|
||||||
right_path = Path(root2)
|
right_path = Path(root2)
|
||||||
|
@ -691,9 +838,9 @@ def run_diff(
|
||||||
left_package_names = set(left_closure_set.all_pnames())
|
left_package_names = set(left_closure_set.all_pnames())
|
||||||
right_package_names = set(right_closure_set.all_pnames())
|
right_package_names = set(right_closure_set.all_pnames())
|
||||||
|
|
||||||
common_package_names = sorted(left_package_names & right_package_names)
|
common_package_names = left_package_names & right_package_names
|
||||||
left_only_package_names = sorted(left_package_names - right_package_names)
|
left_only_package_names = left_package_names - right_package_names
|
||||||
right_only_package_names = sorted(right_package_names - left_package_names)
|
right_only_package_names = right_package_names - left_package_names
|
||||||
|
|
||||||
# Announce version changes.
|
# Announce version changes.
|
||||||
package_names_with_changed_versions = []
|
package_names_with_changed_versions = []
|
||||||
|
@ -716,6 +863,7 @@ def run_diff(
|
||||||
left_package_set=left_closure_set,
|
left_package_set=left_closure_set,
|
||||||
right_package_set=right_closure_set,
|
right_package_set=right_closure_set,
|
||||||
no_version_suffix_highlight=no_version_suffix_highlight,
|
no_version_suffix_highlight=no_version_suffix_highlight,
|
||||||
|
sort_comparator=sort_comparator,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Announce specific changes for packages whose versions haven't changed.
|
# Announce specific changes for packages whose versions haven't changed.
|
||||||
|
@ -727,6 +875,7 @@ def run_diff(
|
||||||
selected_sets=selected_sets,
|
selected_sets=selected_sets,
|
||||||
left_package_set=left_closure_set,
|
left_package_set=left_closure_set,
|
||||||
fixed_install_state=INST_CHANGED,
|
fixed_install_state=INST_CHANGED,
|
||||||
|
sort_comparator=sort_comparator,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Announce added packages.
|
# Announce added packages.
|
||||||
|
@ -738,6 +887,7 @@ def run_diff(
|
||||||
selected_sets=selected_sets,
|
selected_sets=selected_sets,
|
||||||
left_package_set=right_closure_set, # Yes, this is correct.
|
left_package_set=right_closure_set, # Yes, this is correct.
|
||||||
fixed_install_state=INST_ADDED,
|
fixed_install_state=INST_ADDED,
|
||||||
|
sort_comparator=sort_comparator,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Announce removed packages.
|
# Announce removed packages.
|
||||||
|
@ -749,6 +899,7 @@ def run_diff(
|
||||||
selected_sets=selected_sets,
|
selected_sets=selected_sets,
|
||||||
left_package_set=left_closure_set,
|
left_package_set=left_closure_set,
|
||||||
fixed_install_state=INST_REMOVED,
|
fixed_install_state=INST_REMOVED,
|
||||||
|
sort_comparator=sort_comparator,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not any_changes_displayed:
|
if not any_changes_displayed:
|
||||||
|
@ -778,6 +929,13 @@ def run_diff(
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_args():
|
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(
|
parser = argparse.ArgumentParser(
|
||||||
description="Nix/NixOS package version diff tool",
|
description="Nix/NixOS package version diff tool",
|
||||||
epilog="See the nvd(1) manual page for more information.",
|
epilog="See the nvd(1) manual page for more information.",
|
||||||
|
@ -804,6 +962,7 @@ def parse_args():
|
||||||
diff_parser = subparsers.add_parser(
|
diff_parser = subparsers.add_parser(
|
||||||
"diff",
|
"diff",
|
||||||
help="Diff two Nix store paths and their closures.")
|
help="Diff two Nix store paths and their closures.")
|
||||||
|
add_sort_arg(diff_parser)
|
||||||
diff_parser.add_argument(
|
diff_parser.add_argument(
|
||||||
"--no-version-suffix-highlight",
|
"--no-version-suffix-highlight",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
@ -818,6 +977,7 @@ def parse_args():
|
||||||
list_parser = subparsers.add_parser(
|
list_parser = subparsers.add_parser(
|
||||||
"list",
|
"list",
|
||||||
help="List packages in the closure of a Nix store path.")
|
help="List packages in the closure of a Nix store path.")
|
||||||
|
add_sort_arg(list_parser)
|
||||||
list_parser.add_argument(
|
list_parser.add_argument(
|
||||||
"-r", "--root",
|
"-r", "--root",
|
||||||
default="/run/current-system",
|
default="/run/current-system",
|
||||||
|
@ -845,15 +1005,15 @@ def main():
|
||||||
global INST_DOWNGRADED
|
global INST_DOWNGRADED
|
||||||
global INST_CHANGED
|
global INST_CHANGED
|
||||||
|
|
||||||
args = parse_args()
|
args = vars(parse_args())
|
||||||
action = args.action
|
action = args["action"]
|
||||||
NIX_BIN_DIR = args.nix_bin_dir or None
|
NIX_BIN_DIR = args["nix_bin_dir"] or None
|
||||||
USE_COLOUR = \
|
USE_COLOUR = \
|
||||||
args.color == "always" \
|
args["color"] == "always" \
|
||||||
or (args.color == "auto" and sys.stdout.isatty() and not os.getenv('NO_COLOR'))
|
or (args["color"] == "auto" and sys.stdout.isatty() and not os.getenv('NO_COLOR'))
|
||||||
del args.action
|
del args["action"]
|
||||||
del args.nix_bin_dir
|
del args["nix_bin_dir"]
|
||||||
del args.color
|
del args["color"]
|
||||||
|
|
||||||
if USE_COLOUR:
|
if USE_COLOUR:
|
||||||
INST_ADDED = sgr(SGR_FG + SGR_BRIGHT + SGR_GREEN) + INST_ADDED
|
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_DOWNGRADED = sgr(SGR_FG + SGR_BRIGHT + SGR_YELLOW) + INST_DOWNGRADED
|
||||||
INST_CHANGED = sgr(SGR_FG + SGR_BRIGHT + SGR_MAGENTA) + INST_CHANGED
|
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:
|
if action is None:
|
||||||
print("nvd: Subcommand required, see 'nvd --help'.")
|
print("nvd: Subcommand required, see 'nvd --help'.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -874,7 +1038,7 @@ def main():
|
||||||
{
|
{
|
||||||
"list": run_list,
|
"list": run_list,
|
||||||
"diff": run_diff,
|
"diff": run_diff,
|
||||||
}[action](**vars(args))
|
}[action](**args)
|
||||||
|
|
||||||
# Importing this module is kinda broken because of the constant initializations
|
# Importing this module is kinda broken because of the constant initializations
|
||||||
# at the top.
|
# 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 ]
|
.B nvd [ -h | --help ]
|
||||||
.P
|
.P
|
||||||
.B nvd [ GLOBAL OPTIONS ] diff
|
.B nvd [ GLOBAL OPTIONS ] diff
|
||||||
|
.RS
|
||||||
|
.B [ --sort
|
||||||
|
.I sort-order
|
||||||
|
.B ]
|
||||||
|
.br
|
||||||
.I root1
|
.I root1
|
||||||
|
.br
|
||||||
.I root2
|
.I root2
|
||||||
|
.RE
|
||||||
.P
|
.P
|
||||||
.B nvd [ GLOBAL OPTIONS ] list
|
.B nvd [ GLOBAL OPTIONS ] list
|
||||||
.RS
|
.RS
|
||||||
|
@ -17,6 +24,10 @@ nvd \- Nix/NixOS package version diff tool
|
||||||
.br
|
.br
|
||||||
.B [ -s|--selected ]
|
.B [ -s|--selected ]
|
||||||
.br
|
.br
|
||||||
|
.B [ --sort
|
||||||
|
.I sort-order
|
||||||
|
.B ]
|
||||||
|
.br
|
||||||
.B [
|
.B [
|
||||||
.I name-pattern
|
.I name-pattern
|
||||||
.B ]*
|
.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
|
this directory. If empty or not provided, then invocations of Nix binaries will
|
||||||
use regular path lookup. This is the default.
|
use regular path lookup. This is the default.
|
||||||
.TP
|
.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>
|
-r|--root <store-path>
|
||||||
For the
|
For the
|
||||||
.B list
|
.B list
|
||||||
|
|
Loading…
Reference in a new issue