summaryrefslogtreecommitdiff
path: root/.local
diff options
context:
space:
mode:
Diffstat (limited to '.local')
-rwxr-xr-x.local/bin/zypper-wassup159
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()