diff options
Diffstat (limited to '.local')
| -rwxr-xr-x | .local/bin/zypper-wassup | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/.local/bin/zypper-wassup b/.local/bin/zypper-wassup new file mode 100755 index 0000000..10e4826 --- /dev/null +++ b/.local/bin/zypper-wassup @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +from collections import defaultdict, namedtuple +from enum import IntEnum +import re +from subprocess import run + + +Package = namedtuple('Package', ('package', 'old', 'new')) + +ZYPPER_PATTERN = re.compile( + r' +\| +'.join(( + '^v', + '[^|]+', + '(?P<package>[^ ]+)', + '(?P<old>[^ ]+)', + '(?P<new>[^ ]+)' + )), + re.MULTILINE +) + + +def execute(command): + return run(command, check=True, text=True, capture_output=True).stdout + + +def zypper_list_updates(): + zypp_output = execute(('zypper', 'list-updates')) + + return tuple( + Package(**match.groupdict()) + for match in ZYPPER_PATTERN.finditer(zypp_output) + ) + + +def source_package_name(package): + # Using http://ftp.rpm.org/max-rpm/ch-rpm-file-format.html to make + # a few assumptions, e.g. versions can't contain hyphens. + pattern = r'\.'.join(( + '-'.join(('(?P<name>.+)', '(?P<version>[^-]+)', '(?P<release>[^-]+)')), + r'(?:no)?src', + r'rpm' + )) + match = re.fullmatch(pattern, package) + if match is None: + print(pattern) + print(package) + return match.group('name') + + +def sort_by_source_package(packages): + sorted_packages = defaultdict(list) + + print() + + for p in packages: + source_pkgs = execute( + ('rpm', '--query', '--queryformat', r'%{SOURCERPM}\n', p.package) + ) + + # Some packages, e.g. kernel-default and kernel-devel, may be + # provided by multiple version of a source package. Assume + # the last one is one we want. + + *_, last_source_pkg = source_pkgs.splitlines() + name = source_package_name(last_source_pkg) + + sorted_packages[name].append(p) + + return sorted_packages + + +class Sgr(IntEnum): + RESET = 0 + BOLD = 1 + RED_FG = 31 + GREEN_FG = 32 + + +def colorize(text, *params): + prefix = '\N{ESCAPE}[' + suffix = 'm' + reset = f'{prefix}{Sgr.RESET}{suffix}' + param_list = ';'.join(map(format, params)) + + return f'{prefix}{param_list}{suffix}{text}{reset}' + + +def highlight_diff(old, new): + for i, (o, n) in enumerate(zip(old, new)): + if o != n: + break + + old = old[:i]+colorize(old[i:], Sgr.BOLD, Sgr.RED_FG) + new = new[:i]+colorize(new[i:], Sgr.BOLD, Sgr.GREEN_FG) + + return old, new + + +def padding(string, width): + # Python's str.format does not skip over control sequences when + # computing how long a string is. Compute padding manually before + # adding these sequences + return ' '*(width-len(string)) + + +COLUMN = ' │ ' + + +def print_header(widths, name): + if len(name) > widths['package']: + name = name[:widths['package']-1]+'…' + + print(COLUMN.join(( + colorize(name, Sgr.BOLD)+padding(name, widths['package']), + ' '*widths['old'], + ' '*widths['new'], + ))) + + +def print_footer(i, n, widths): + if i < n: + print('─┼─'.join('─'*widths[f] for f in Package._fields)) + + +def main(): + print('Querying zypper list-updates… ', end='', flush=True) + packages = zypper_list_updates() + print(f'{len(packages)} updates.') + + widths = { + field: max(len(p._asdict()[field]) for p in packages) + for field in Package._fields + } + + print('Sorting by source package… ', end='', flush=True) + packages = sort_by_source_package(packages) + print('Done') + + for i, src in enumerate(sorted(packages), 1): + print_header(widths, src) + + for pkg, old, new in sorted(packages[src]): + old_padding = padding(old, widths['old']) + new_padding = padding(new, widths['new']) + + old, new = highlight_diff(old, new) + + print(COLUMN.join(( + f'{pkg:<{widths["package"]}}', + old+old_padding, + new+new_padding + ))) + + print_footer(i, len(packages), widths) + + +if __name__ == '__main__': + main() |
