Skip to content

Commit ab7899e

Browse files
committed
cmd-diff: Add new ability to generate rpm diffs from commitmeta.json
This adds two new arguments for generating rpm-ostree db diff compatible output by just leveraging the information in the commitmeta.json files in each build's directory. This is a much more lightweight way to get diffs rather than having to download and import the entire ociarchive, which we are now having to do in the build-with-buildah world where the rpm information isn't available to us in the OSTree commit object like it was in the past. This was assisted by Google/Gemini and got me 75% of the way there. The prompt provided: ``` Look at the files old-commitmeta.json and new-commitmeta.json and generate a two functions in src/cmd-diff that will produce similar diff output. The first function will produce human readable diff output and should be in the form as shown in the human-diff.txt file. The second function should produce JSON output and be in the format as shown in the json-diff.txt file. The commitmeta.json files will exist in the build directory for a build (so when diffing two builds you'll need to look at the commitmeta.json for the first build and then the commitmeta.json for the second build and then produce the diff output based on the differences in those two files. ``` Assisted-By: <google/gemini-2.5-pro>
1 parent 4005faf commit ab7899e

File tree

1 file changed

+158
-1
lines changed

1 file changed

+158
-1
lines changed

src/cmd-diff

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import argparse
44
import os
55
import shutil
6+
import json
67
import subprocess
78
import sys
89
import tempfile
910
import time
11+
import rpm
1012
from multiprocessing import Process
1113

1214
from dataclasses import dataclass
@@ -24,11 +26,79 @@ class DiffBuildTarget:
2426
id: str
2527
dir: str
2628
meta: dict
29+
commitmeta: dict
2730

2831
@staticmethod
2932
def from_build(builds, build, arch):
3033
return DiffBuildTarget(build, builds.get_build_dir(build, arch),
31-
builds.get_build_meta(build, arch))
34+
builds.get_build_meta(build, arch),
35+
builds.get_build_commitmeta(build, arch))
36+
37+
38+
class PackageDiffType(IntEnum):
39+
ADD = 0
40+
REMOVE = 1
41+
UPGRADE = 2
42+
DOWNGRADE = 3
43+
44+
45+
@dataclass
46+
class Package:
47+
name: str
48+
epoch: int
49+
v: str # Version
50+
r: str # Release
51+
a: str # Architecture
52+
53+
def evr(self):
54+
epoch_string = "" if self.epoch == "0" else f"{self.epoch}:"
55+
return f"{epoch_string}{self.v}-{self.r}"
56+
57+
# Compare versions
58+
# rc > 0 -> Newer; rc == 0 -> Same; rc < 0 -> Older
59+
@staticmethod
60+
def vercmp(from_pkg, to_pkg):
61+
return rpm.labelCompare(
62+
(to_pkg.epoch, to_pkg.v, to_pkg.r),
63+
(from_pkg.epoch, from_pkg.v, from_pkg.r)
64+
)
65+
66+
# Return a data sctructure representing a diff entry from the
67+
# rpm-ostree db diff --json output.
68+
# "pkgdiff" : [
69+
# [
70+
# "NetworkManager",
71+
# 2,
72+
# {
73+
# "PreviousPackage" : [
74+
# "NetworkManager",
75+
# "1:1.52.0-5.el9_6",
76+
# "x86_64"
77+
# ],
78+
# "NewPackage" : [
79+
# "NetworkManager",
80+
# "1:1.52.0-7.el9_6",
81+
# "x86_64"
82+
# ]
83+
# }
84+
# ],
85+
@staticmethod
86+
def to_json_diff_entry(difftype: PackageDiffType, from_pkg, to_pkg):
87+
name = from_pkg.name if from_pkg else to_pkg.name
88+
change = ""
89+
match difftype:
90+
case PackageDiffType.ADD:
91+
change = {"NewPackage": [to_pkg.name, to_pkg.evr(), to_pkg.a]}
92+
case PackageDiffType.REMOVE:
93+
change = {"PreviousPackage": [from_pkg.name, from_pkg.evr(), from_pkg.a]}
94+
case PackageDiffType.UPGRADE | PackageDiffType.DOWNGRADE:
95+
change = {
96+
"PreviousPackage": [from_pkg.name, from_pkg.evr(), from_pkg.a],
97+
"NewPackage": [to_pkg.name, to_pkg.evr(), to_pkg.a]
98+
}
99+
case _:
100+
raise
101+
return [name, difftype, change]
32102

33103

34104
class OSTreeImport(IntEnum):
@@ -167,6 +237,89 @@ def diff_rpms_rpm_ostree(diff_from, diff_to, json):
167237
runcmd(cmd)
168238

169239

240+
def diff_rpms_commitmeta_json(diff_from, diff_to):
241+
diff_rpms_commitmeta(diff_from, diff_to, json_output=True)
242+
243+
244+
def diff_rpms_commitmeta_no_json(diff_from, diff_to):
245+
diff_rpms_commitmeta(diff_from, diff_to, json_output=False)
246+
247+
248+
def diff_rpms_commitmeta(diff_from, diff_to, json_output=False):
249+
"""
250+
Diff the commitmeta.json files between two builds.
251+
"""
252+
from_meta = diff_from.meta
253+
to_meta = diff_to.meta
254+
from_commitmeta = diff_from.commitmeta
255+
to_commitmeta = diff_to.commitmeta
256+
257+
# Gather info about upgraded, downgraded, added, removed RPMS
258+
from_rpms = {pkg[0]: Package(*pkg) for pkg in from_commitmeta.get('rpmostree.rpmdb.pkglist', [])}
259+
to_rpms = {pkg[0]: Package(*pkg) for pkg in to_commitmeta.get('rpmostree.rpmdb.pkglist', [])}
260+
261+
added_rpms = set(to_rpms.keys()) - set(from_rpms.keys())
262+
removed_rpms = set(from_rpms.keys()) - set(to_rpms.keys())
263+
common_rpms = set(from_rpms.keys()) & set(to_rpms.keys())
264+
265+
upgraded_rpms = []
266+
downgraded_rpms = []
267+
for name in common_rpms:
268+
from_pkg = from_rpms[name]
269+
to_pkg = to_rpms[name]
270+
rc = Package.vercmp(from_pkg, to_pkg)
271+
if rc > 0:
272+
upgraded_rpms.append((name, from_pkg, to_pkg))
273+
elif rc == 0:
274+
pass # They are equal versions
275+
elif rc < 0:
276+
downgraded_rpms.append((name, from_pkg, to_pkg))
277+
278+
if json_output:
279+
diff = {
280+
"ostree-commit-from": from_meta.get('ostree-commit'),
281+
"ostree-commit-to": to_meta.get('ostree-commit'),
282+
"pkgdiff": [],
283+
"advisories": []
284+
}
285+
for name in sorted(added_rpms):
286+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.ADD, from_pkg=None, to_pkg=to_rpms[name]))
287+
for name in sorted(removed_rpms):
288+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.REMOVE, from_pkg=from_rpms[name], to_pkg=None))
289+
for name, from_pkg, to_pkg in sorted(upgraded_rpms):
290+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.UPGRADE, from_pkg=from_pkg, to_pkg=to_pkg))
291+
for name, from_pkg, to_pkg in sorted(downgraded_rpms):
292+
diff['pkgdiff'].append(Package.to_json_diff_entry(PackageDiffType.DOWNGRADE, from_pkg=from_pkg, to_pkg=to_pkg))
293+
294+
# Only add advisory info for the JSON output
295+
from_advisories = {adv[0]: adv for adv in from_commitmeta.get('rpmostree.advisories', [])}
296+
to_advisories = {adv[0]: adv for adv in to_commitmeta.get('rpmostree.advisories', [])}
297+
added_advisories = set(to_advisories.keys()) - set(from_advisories.keys())
298+
for name in added_advisories:
299+
diff['advisories'].append(to_advisories[name])
300+
# Dump the JSON to stdout
301+
print(json.dumps(diff, indent=2))
302+
else:
303+
print(f"ostree diff commit from: {diff_from.id} ({from_meta.get('ostree-commit')})")
304+
print(f"ostree diff commit to: {diff_to.id} ({to_meta.get('ostree-commit')})")
305+
if upgraded_rpms:
306+
print("Upgraded:")
307+
for name, from_pkg, to_pkg in sorted(upgraded_rpms):
308+
print(f" {name} {from_pkg.evr()} -> {to_pkg.evr()}")
309+
if downgraded_rpms:
310+
print("Downgraded:")
311+
for name, from_pkg, to_pkg in sorted(downgraded_rpms):
312+
print(f" {name} {from_pkg.evr()} -> {to_pkg.evr()}")
313+
if removed_rpms:
314+
print("Removed:")
315+
for name in sorted(removed_rpms):
316+
print(f" {name}-{from_rpms[name].evr()}")
317+
if added_rpms:
318+
print("Added:")
319+
for name in sorted(added_rpms):
320+
print(f" {name}-{to_rpms[name].evr()}")
321+
322+
170323
def diff_ostree_ls(diff_from, diff_to):
171324
commit_from = diff_from.meta['ostree-commit']
172325
commit_to = diff_to.meta['ostree-commit']
@@ -476,6 +629,10 @@ DIFFERS = [
476629
needs_ostree=OSTreeImport.FULL, function=diff_rpms_rpm_ostree_no_json),
477630
Differ("rpms-json", "Diff RPMs, output JSON",
478631
needs_ostree=OSTreeImport.FULL, function=diff_rpms_rpm_ostree_json),
632+
Differ("rpms-commitmeta", "Diff rpms from commitmeta.json",
633+
needs_ostree=OSTreeImport.NO, function=diff_rpms_commitmeta_no_json),
634+
Differ("rpms-json-commitmeta", "Diff RPMS & Advisories from commitmeta.json, output JSON",
635+
needs_ostree=OSTreeImport.NO, function=diff_rpms_commitmeta_json),
479636
Differ("source-control", "Diff config and COSA input commits",
480637
needs_ostree=OSTreeImport.NO, function=diff_source_control),
481638
Differ("ostree-ls", "Diff OSTree contents using 'ostree diff'",

0 commit comments

Comments
 (0)