launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32466
[Merge] ~enriqueesanchz/launchpad:add-bugpresence into launchpad:master
Enrique Sánchez has proposed merging ~enriqueesanchz/launchpad:add-bugpresence into launchpad:master.
Commit message:
Add SVT `BugPresence` model
Add `Bug` and `BugPresence` integration
Add SVT scripts `BugPresence` integration
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/485525
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-bugpresence into launchpad:master.
diff --git a/lib/lp/bugs/adapters/bugchange.py b/lib/lp/bugs/adapters/bugchange.py
index 1532bfc..80afe09 100644
--- a/lib/lp/bugs/adapters/bugchange.py
+++ b/lib/lp/bugs/adapters/bugchange.py
@@ -19,6 +19,8 @@ __all__ = [
"BugInformationTypeChange",
"BugLocked",
"BugLockReasonSet",
+ "BugPresenceAdded",
+ "BugPresenceRemoved",
"BugTagsChange",
"BugTaskAdded",
"BugTaskAssigneeChange",
@@ -72,6 +74,8 @@ ATTACHMENT_ADDED = "attachment added"
ATTACHMENT_REMOVED = "attachment removed"
BRANCH_LINKED = "branch linked"
BRANCH_UNLINKED = "branch unlinked"
+BUG_PRESENCE_ADDED = "bug presence added"
+BUG_PRESENCE_REMOVED = "bug presence removed"
BUG_WATCH_ADDED = "bug watch added"
BUG_WATCH_REMOVED = "bug watch removed"
CHANGED_DUPLICATE_MARKER = "changed duplicate marker"
@@ -328,6 +332,58 @@ class SeriesNominated(BugChangeBase):
return None
+class BugPresenceAdded(BugChangeBase):
+ """A bug presence was added to the bug."""
+
+ def __init__(self, when, person, bug_presence):
+ super().__init__(when, person)
+ self.bug_presence = bug_presence
+
+ def getBugActivity(self):
+ """See `IBugChange`."""
+ return dict(
+ whatchanged=BUG_PRESENCE_ADDED,
+ newvalue=str(self.bug_presence.break_fix_data),
+ )
+
+ def getBugNotification(self):
+ """See `IBugChange`."""
+ return {
+ "text": "** Bug presence added: "
+ f"project: {self.bug_presence.project}"
+ f"distribution: {self.bug_presence.distribution}"
+ f"source_package_name: {self.bug_presence.source_package_name}"
+ f"gitrepository: {self.bug_presence.git_repository}"
+ f"break_fix_data: {self.bug_presence.break_fix_data}\n"
+ }
+
+
+class BugPresenceRemoved(BugChangeBase):
+ """A bug presence was removed from the bug."""
+
+ def __init__(self, when, person, bug_presence):
+ super().__init__(when, person)
+ self.bug_presence = bug_presence
+
+ def getBugActivity(self):
+ """See `IBugChange`."""
+ return dict(
+ whatchanged=BUG_PRESENCE_REMOVED,
+ newvalue=str(self.bug_presence.break_fix_data),
+ )
+
+ def getBugNotification(self):
+ """See `IBugChange`."""
+ return {
+ "text": "** Bug presence removed: "
+ f"project: {self.bug_presence.project}"
+ f"distribution: {self.bug_presence.distribution}"
+ f"source_package_name: {self.bug_presence.source_package_name}"
+ f"gitrepository: {self.bug_presence.git_repository}"
+ f"break_fix_data: {self.bug_presence.break_fix_data}\n"
+ }
+
+
class BugWatchAdded(BugChangeBase):
"""A bug watch was added to the bug."""
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 8ac1fcd..057cfdf 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -807,6 +807,7 @@
permission="launchpad.Append"
interface="lp.bugs.interfaces.bug.IBugAppend"
attributes="
+ presences
linkBranch
unlinkBranch"/>
<require
@@ -933,6 +934,42 @@
for="lp.bugs.interfaces.bugsubscription.IBugSubscription lazr.lifecycle.interfaces.IObjectModifiedEvent"
handler="lp.bugs.subscribers.bugactivity.record_bugsubscription_edited"/>
+ <!-- BugPresence -->
+
+ <class
+ class="lp.bugs.model.bugpresence.BugPresence">
+ <implements
+ interface="lp.bugs.interfaces.bugpresence.IBugPresence"/>
+ <require
+ permission="launchpad.View"
+ set_schema="lp.bugs.interfaces.bugpresence.IBugPresence"
+ attributes="
+ id
+ bug
+ project
+ distribution
+ source_package_name
+ git_repository
+ break_fix_data"/>
+ <require
+ permission="launchpad.Edit"
+ attributes="destroySelf"/>
+ </class>
+
+ <!-- BugPresenceSet -->
+
+ <class
+ class="lp.bugs.model.bugpresence.BugPresenceSet">
+ <allow
+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/>
+ </class>
+ <lp:securedutility
+ class="lp.bugs.model.bugpresence.BugPresenceSet"
+ provides="lp.bugs.interfaces.bugpresence.IBugPresenceSet">
+ <allow
+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/>
+ </lp:securedutility>
+
<!-- BugWatch -->
<class
diff --git a/lib/lp/bugs/interfaces/bug.py b/lib/lp/bugs/interfaces/bug.py
index 5135dee..390dd0a 100644
--- a/lib/lp/bugs/interfaces/bug.py
+++ b/lib/lp/bugs/interfaces/bug.py
@@ -912,6 +912,34 @@ class IBugAppend(Interface):
:is_patch: A boolean.
"""
+ def addPresence(
+ project,
+ distribution,
+ source_package_name,
+ git_repository,
+ break_fix_data,
+ user,
+ ):
+ """Add a bug presence to this bug.
+
+ :project: The project where this BugPresence is related to.
+ :distribution: The distribution where this BugPresence is related to.
+ :source_package_name: The source_package_name where this BugPresence is
+ related to.
+ :git_repository: The git_repository where this BugPresence is related
+ to.
+ :break_fix_data: The dict of commits that caused the issue (break) and
+ commits that solved it (fix).
+ :user: The user that adds this bug presence.
+ """
+
+ def removePresence(bug_presence, user):
+ """Remove a bug presence from the bug.
+
+ :bug_presence: The bug presence to be removed.
+ :user: The user that removes the bug presence.
+ """
+
def addCommentNotification(
message,
recipients=None,
diff --git a/lib/lp/bugs/interfaces/bugpresence.py b/lib/lp/bugs/interfaces/bugpresence.py
new file mode 100644
index 0000000..bbba90d
--- /dev/null
+++ b/lib/lp/bugs/interfaces/bugpresence.py
@@ -0,0 +1,48 @@
+# Copyright 2009-2025 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""BugPresence interfaces"""
+
+__all__ = [
+ "IBugPresence",
+ "IBugPresenceSet",
+]
+
+from zope.interface import Interface
+from zope.schema import Dict, Int
+
+from lp import _
+from lp.services.fields import BugField
+
+
+class IBugPresence(Interface):
+ """A single `BugPresence` database entry."""
+
+ id = Int(title=_("ID"), required=True, readonly=True)
+ bug = BugField(title=_("Bug"), readonly=True)
+ project = Int(title=_("Project"))
+ distribution = Int(title=_("Distribution"))
+ source_package_name = Int(title=_("Source Package Name"))
+ git_repository = Int(title=_("Git Repository"))
+ break_fix_data = Dict(title=_("Break-Fix"))
+
+ def destroySelf(self):
+ """Destroy this `IBugPresence` object."""
+
+
+class IBugPresenceSet(Interface):
+ """The set of `IBugPresence` objects."""
+
+ def __getitem__(id):
+ """Get a `IBugPresence` by id."""
+
+ def create(
+ id,
+ bug,
+ project,
+ distribution,
+ source_package_name,
+ git_repository,
+ break_fix_data,
+ ):
+ """Create a new `IBugPresence`."""
diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
index 989abe8..d5af37d 100644
--- a/lib/lp/bugs/model/bug.py
+++ b/lib/lp/bugs/model/bug.py
@@ -86,6 +86,8 @@ from lp.bugs.adapters.bugchange import (
BugDuplicateChange,
BugLocked,
BugLockReasonSet,
+ BugPresenceAdded,
+ BugPresenceRemoved,
BugUnlocked,
BugWatchAdded,
BugWatchRemoved,
@@ -114,6 +116,7 @@ from lp.bugs.interfaces.bugnomination import (
NominationSeriesObsoleteError,
)
from lp.bugs.interfaces.bugnotification import IBugNotificationSet
+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
from lp.bugs.interfaces.bugtask import (
UNRESOLVED_BUGTASK_STATUSES,
@@ -135,6 +138,7 @@ from lp.bugs.model.buglinktarget import ObjectLinkedEvent, ObjectUnlinkedEvent
from lp.bugs.model.bugmessage import BugMessage
from lp.bugs.model.bugnomination import BugNomination
from lp.bugs.model.bugnotification import BugNotification
+from lp.bugs.model.bugpresence import BugPresence
from lp.bugs.model.bugsubscription import BugSubscription
from lp.bugs.model.bugtarget import OfficialBugTag
from lp.bugs.model.bugtask import BugTask, bugtask_sort_key
@@ -836,6 +840,15 @@ class Bug(StormBase, InformationTypeMixin):
return sorted(tasks, key=bugtask_sort_key)
@property
+ def presences(self):
+ """See `IBug`."""
+ store = Store.of(self)
+ presences = list(
+ store.find(BugPresence, BugPresence.bug_id == self.id)
+ )
+ return presences
+
+ @property
def default_bugtask(self):
"""See `IBug`."""
from lp.registry.model.product import Product
@@ -1719,6 +1732,32 @@ class Bug(StormBase, InformationTypeMixin):
send_notifications=send_notifications,
)
+ def addPresence(
+ self,
+ project,
+ distribution,
+ source_package_name,
+ git_repository,
+ break_fix_data,
+ user,
+ ):
+ """See `IBug`."""
+ bug_presence = getUtility(IBugPresenceSet).create(
+ bug=self,
+ project=project,
+ distribution=distribution,
+ source_package_name=source_package_name,
+ git_repository=git_repository,
+ break_fix_data=break_fix_data,
+ )
+ self.addChange(BugPresenceAdded(UTC_NOW, user, bug_presence))
+ return bug_presence
+
+ def removePresence(self, bug_presence, user):
+ """See `IBug`."""
+ self.addChange(BugPresenceRemoved(UTC_NOW, user, bug_presence))
+ bug_presence.destroySelf()
+
def linkBranch(self, branch, registrant):
"""See `IBug`."""
if branch in self.linked_branches:
diff --git a/lib/lp/bugs/model/bugpresence.py b/lib/lp/bugs/model/bugpresence.py
new file mode 100644
index 0000000..32f87d3
--- /dev/null
+++ b/lib/lp/bugs/model/bugpresence.py
@@ -0,0 +1,114 @@
+# Copyright 2009-2025 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = [
+ "BugPresence",
+ "BugPresenceSet",
+]
+
+from storm.databases.postgres import JSON
+from storm.locals import Int
+from storm.references import Reference
+from storm.store import Store
+from zope.interface import implementer
+
+from lp.bugs.interfaces.bugpresence import IBugPresence, IBugPresenceSet
+from lp.services.database.interfaces import IStore
+from lp.services.database.stormbase import StormBase
+
+
+@implementer(IBugPresence)
+class BugPresence(StormBase):
+ """
+ Points in the code history of various entities like a project, a
+ distribution, or a distribution source package when something was broken
+ and/or when it was fixed.
+ """
+
+ __storm_table__ = "bugpresence"
+
+ id = Int(primary=True)
+
+ bug_id = Int(name="bug", allow_none=False)
+ bug = Reference(bug_id, "Bug.id")
+
+ project_id = Int(name="project", allow_none=True)
+ # Is this product or project: dbmodels says project but we dont have the
+ # project entity
+ project = Reference(project_id, "Product.id")
+
+ distribution_id = Int(name="distribution", allow_none=True)
+ distribution = Reference(distribution_id, "Distribution.id")
+
+ source_package_name_id = Int(name="source_package_name", allow_none=True)
+ source_package_name = Reference(
+ source_package_name_id, "SourcePackageName.id"
+ )
+
+ git_repository_id = Int(name="git_repository", allow_none=True)
+ git_repository = Reference(git_repository_id, "GitRepository.id")
+
+ _break_fix_data = JSON(name="break_fix_data", allow_none=False)
+
+ def __init__(
+ self,
+ bug,
+ project,
+ distribution,
+ source_package_name,
+ git_repository,
+ break_fix_data,
+ ):
+ super().__init__()
+ self.bug = bug
+ self.project = project
+ self.distribution = distribution
+ self.source_package_name = source_package_name
+ self.git_repository = git_repository
+ self._break_fix_data = break_fix_data
+
+ @property
+ def break_fix_data(self):
+ """See `IBugPresence`."""
+ return self._break_fix_data or {}
+
+ @break_fix_data.setter
+ def break_fix_data(self, value):
+ """See `IBugPresence`."""
+ assert value is None or isinstance(value, list)
+ self._break_fix_data = value
+
+ def destroySelf(self):
+ """See `IBugPresence`."""
+ Store.of(self).remove(self)
+
+
+@implementer(IBugPresenceSet)
+class BugPresenceSet:
+ """The set of `IBugPresence` objects."""
+
+ def __getitem__(id):
+ """See IBugPresenceSet."""
+ return IStore(BugPresence).find(BugPresence, id=id).one()
+
+ def create(
+ id,
+ bug,
+ project,
+ distribution,
+ source_package_name,
+ git_repository,
+ break_fix_data,
+ ):
+ """See IBugPresenceSet."""
+ bug_presence = BugPresence(
+ bug=bug,
+ project=project,
+ distribution=distribution,
+ source_package_name=source_package_name,
+ git_repository=git_repository,
+ break_fix_data=break_fix_data,
+ )
+
+ IStore(BugPresence).add(bug_presence)
+ return bug_presence
diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py
index a3b35dd..bf43d4d 100644
--- a/lib/lp/bugs/scripts/tests/test_uct.py
+++ b/lib/lp/bugs/scripts/tests/test_uct.py
@@ -373,6 +373,14 @@ class TestCVE(TestCaseWithFactory):
"commit/456"
),
),
+ UCTRecord.Patch(
+ patch_type="break-fix",
+ entry=(
+ "457f44363a8894135c85b7a9afd2bd8196db24ab "
+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
+ "local-CVE-2022-23222-fix"
+ ),
+ ),
],
),
UCTRecord.Package(
@@ -546,6 +554,16 @@ class TestCVE(TestCaseWithFactory):
notes=None,
),
],
+ break_fix_data=[
+ CVE.BreakFix(
+ package_name=dsp1.sourcepackagename,
+ break_="457f44363a8894135c85b7a9afd2bd8196db24ab",
+ fix=(
+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
+ "local-CVE-2022-23222-fix"
+ ),
+ ),
+ ],
global_tags={"cisa-kev"},
)
@@ -603,6 +621,43 @@ class TestCVE(TestCaseWithFactory):
),
)
+ def test_get_break_fix(self):
+ spn = self.factory.makeSourcePackageName()
+ self.assertListEqual(
+ [
+ CVE.BreakFix(
+ package_name=spn,
+ break_="d2406291483775ecddaee929231a39c70c08fda2",
+ fix="f64e67e5d3a45a4a04286c47afade4b518acd47b",
+ ),
+ CVE.BreakFix(
+ package_name=spn,
+ break_="-",
+ fix="f2ef6f7539c68c6bd6c32323d8845ee102b7c450",
+ ),
+ ],
+ list(
+ CVE.get_break_fix(
+ spn,
+ [
+ UCTRecord.Patch(
+ "break-fix",
+ "d2406291483775ecddaee929231a39c70c08fda2 "
+ "f64e67e5d3a45a4a04286c47afade4b518acd47b",
+ ),
+ UCTRecord.Patch(
+ "break-fix",
+ "- f2ef6f7539c68c6bd6c32323d8845ee102b7c450",
+ ),
+ UCTRecord.Patch(
+ "upstream", "https://github.com/repo/2 (1.2.3)"
+ ),
+ UCTRecord.Patch("other", "foo"),
+ ],
+ )
+ ),
+ )
+
class TestUCTImporterExporter(TestCaseWithFactory):
maxDiff = None
@@ -791,6 +846,16 @@ class TestUCTImporterExporter(TestCaseWithFactory):
notes=None,
),
],
+ break_fix_data=[
+ CVE.BreakFix(
+ package_name=self.ubuntu_package.sourcepackagename,
+ break_="457f44363a8894135c85b7a9afd2bd8196db24ab",
+ fix=(
+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
+ "local-CVE-2022-23222-fix"
+ ),
+ ),
+ ],
global_tags={"cisa-kev"},
)
self.importer = UCTImporter()
@@ -814,6 +879,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.checkBugTags(bug, cve)
self.checkBugAttachments(bug, cve)
+ self.checkBugPresences(bug, cve)
def checkBugTags(self, bug: Bug, cve: CVE):
tags = cve.global_tags.copy()
@@ -893,6 +959,51 @@ class TestUCTImporterExporter(TestCaseWithFactory):
for t in bug_tasks:
self.assertEqual(cve.assignee, t.assignee)
+ def checkBugPresences(self, bug: Bug, cve: CVE):
+ presences_by_pkg = {
+ presence.source_package_name: presence
+ for presence in bug.presences
+ }
+ break_fix_by_pkg = defaultdict(list)
+ for break_fix in cve.break_fix_data:
+ break_fix_by_pkg[break_fix.package_name].append(break_fix)
+
+ self.assertEqual(
+ len(bug.presences),
+ len(break_fix_by_pkg),
+ "Mismatch in presences count and break_fix count",
+ )
+
+ for package, break_fix_data in break_fix_by_pkg.items():
+ presence = presences_by_pkg.get(package)
+
+ self.assertIsNotNone(
+ presence, f"Presence for package '{package}' not found"
+ )
+
+ self.assertEqual(package, presence.source_package_name)
+ self.assertEqual(
+ len(break_fix_data),
+ len(presence.break_fix_data),
+ "Number of break_fix_data don't match for package "
+ f"'{package}'",
+ )
+
+ # Check content and its order
+ for break_fix, presence_break_fix in zip(
+ break_fix_data, presence.break_fix_data
+ ):
+ self.assertEqual(
+ break_fix.break_,
+ presence_break_fix["break"],
+ f"Break mismatch for patch in package '{package}'",
+ )
+ self.assertEqual(
+ break_fix.fix,
+ presence_break_fix["fix"],
+ f"Fix mismatch for patch in package '{package}'",
+ )
+
def checkBugAttachments(self, bug: Bug, cve: CVE):
attachments_by_url = {a.url: a for a in bug.attachments if a.url}
for patch_url in cve.patch_urls:
@@ -981,6 +1092,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.assertEqual(expected.mitigation, actual.mitigation)
self.assertListEqual(expected.cvss, actual.cvss)
self.assertListEqual(expected.patch_urls, actual.patch_urls)
+ self.assertListEqual(expected.break_fix_data, actual.break_fix_data)
self.assertEqual(expected.global_tags, actual.global_tags)
def test_create_bug(self):
@@ -993,7 +1105,8 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.assertEqual([self.lp_cve], bug.cves)
activities = list(bug.activity)
- self.assertEqual(6, len(activities))
+ # We are adding a bug presence so we add 1 activity
+ self.assertEqual(7, len(activities))
import_bug_activity = activities[-1]
self.assertEqual(self.bug_importer, import_bug_activity.person)
self.assertEqual("bug", import_bug_activity.whatchanged)
@@ -1079,6 +1192,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
),
],
patch_urls=[],
+ break_fix_data=[],
global_tags={"cisa-kev"},
)
lp_cve = self.factory.makeCVE(sequence="2022-1234")
@@ -1341,6 +1455,37 @@ class TestUCTImporterExporter(TestCaseWithFactory):
self.importer.update_bug(bug, cve, self.lp_cve)
self.checkBug(bug, cve)
+ def test_update_break_fix(self):
+ bug = self.importer.create_bug(self.cve, self.lp_cve)
+ cve = self.cve
+
+ # Add new patch URL
+ cve.break_fix_data.append(
+ CVE.BreakFix(
+ package_name=cve.distro_packages[0].package_name,
+ break_="d2406291483775ecddaee929231a39c70c08fda2",
+ fix=(
+ "f64e67e5d3a45a4a04286c47afade4b518acd47b"
+ "|cc8c837cf1b2f714dda723541c04acd1b8922d92"
+ ),
+ ),
+ )
+ cve.break_fix_data.append(
+ CVE.BreakFix(
+ package_name=cve.distro_packages[1].package_name,
+ break_="-",
+ fix="cffe487026be13eaf37ea28b783d9638ab147204",
+ ),
+ )
+ self.importer.update_bug(bug, cve, self.lp_cve)
+ self.checkBug(bug, cve)
+
+ # Remove break_fix and check if it removes from bug
+ cve.break_fix_data.pop()
+ cve.break_fix_data.pop()
+ self.importer.update_bug(bug, cve, self.lp_cve)
+ self.checkBug(bug, cve)
+
def test_update_tags(self):
bug = self.importer.create_bug(self.cve, self.lp_cve)
cve = self.cve
diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
index daceec2..209d44e 100644
--- a/lib/lp/bugs/scripts/uct/models.py
+++ b/lib/lp/bugs/scripts/uct/models.py
@@ -466,6 +466,11 @@ class CVE:
url: str
notes: Optional[str]
+ class BreakFix(NamedTuple):
+ package_name: SourcePackageName
+ break_: str
+ fix: str
+
# Example:
# https://github.com/389ds/389-ds-base/commit/123 (1.4.4)
# https://github.com/389ds/389-ds-base/commit/345
@@ -525,6 +530,7 @@ class CVE:
mitigation: str,
cvss: List[CVSS],
global_tags: Set[str],
+ break_fix_data: List[BreakFix],
patch_urls: Optional[List[PatchURL]] = None,
importance_explanation: str = "",
):
@@ -549,6 +555,7 @@ class CVE:
self.cvss = cvss
self.global_tags = global_tags
self.patch_urls: List[CVE.PatchURL] = patch_urls or []
+ self.break_fix_data: List[CVE.BreakFix] = break_fix_data or []
@classmethod
def make_from_uct_record(cls, uct_record: UCTRecord) -> "CVE":
@@ -561,6 +568,7 @@ class CVE:
distro_packages = []
series_packages = []
patch_urls = []
+ break_fix_data = []
spn_set = getUtility(ISourcePackageNameSet)
@@ -575,6 +583,10 @@ class CVE:
cls.get_patch_urls(source_package_name, uct_package.patches)
)
+ break_fix_data.extend(
+ cls.get_break_fix(source_package_name, uct_package.patches)
+ )
+
package_importance = (
cls.PRIORITY_MAP[uct_package.priority]
if uct_package.priority
@@ -697,6 +709,7 @@ class CVE:
cvss=uct_record.cvss,
global_tags=uct_record.global_tags,
patch_urls=patch_urls,
+ break_fix_data=break_fix_data,
)
def to_uct_record(self) -> UCTRecord:
@@ -797,6 +810,14 @@ class CVE:
)
)
+ for break_fix in self.break_fix_data:
+ packages_by_name[patch_url.package_name.name].patches.append(
+ UCTRecord.Patch(
+ patch_type="break-fix",
+ entry=f"{break_fix.break_} {break_fix.fix}",
+ )
+ )
+
return UCTRecord(
parent_dir=self.VULNERABILITY_STATUS_MAP_REVERSE.get(
self.status, ""
@@ -911,3 +932,27 @@ class CVE:
url=url,
notes=notes,
)
+
+ @classmethod
+ def get_break_fix(
+ cls,
+ source_package_name: SourcePackageName,
+ patches: List[UCTRecord.Patch],
+ ) -> Iterable[PatchURL]:
+ for patch in patches:
+ if patch.patch_type != "break-fix":
+ continue
+
+ if " " not in patch.entry:
+ logger.warning(
+ "Could not parse the break-fix patch entry: %s",
+ patch.entry,
+ )
+ continue
+
+ break_, fix = patch.entry.split(maxsplit=1)
+ yield cls.BreakFix(
+ package_name=source_package_name,
+ break_=break_,
+ fix=fix,
+ )
diff --git a/lib/lp/bugs/scripts/uct/uctexport.py b/lib/lp/bugs/scripts/uct/uctexport.py
index fd7df39..80462e4 100644
--- a/lib/lp/bugs/scripts/uct/uctexport.py
+++ b/lib/lp/bugs/scripts/uct/uctexport.py
@@ -239,6 +239,17 @@ class UCTExporter:
)
)
+ break_fix_data = []
+ for bugpresence in bug.presences:
+ for break_fix in bugpresence.break_fix_data:
+ break_fix_data.append(
+ CVE.BreakFix(
+ package_name=bugpresence.source_package_name,
+ break_=break_fix.get("break"),
+ fix=break_fix.get("fix"),
+ )
+ )
+
return CVE(
sequence=f"CVE-{lp_cve.sequence}",
date_made_public=vulnerability.date_made_public,
@@ -268,6 +279,7 @@ class UCTExporter:
],
global_tags=global_tags,
patch_urls=patch_urls,
+ break_fix_data=break_fix_data,
)
def _parse_bug_description(
diff --git a/lib/lp/bugs/scripts/uct/uctimport.py b/lib/lp/bugs/scripts/uct/uctimport.py
index a4abb5e..a6172fd 100644
--- a/lib/lp/bugs/scripts/uct/uctimport.py
+++ b/lib/lp/bugs/scripts/uct/uctimport.py
@@ -25,6 +25,7 @@ Three types of bug tasks are created:
status of the package in upstream.
"""
import logging
+from collections import defaultdict
from datetime import timezone
from itertools import chain
from pathlib import Path
@@ -178,6 +179,7 @@ class UCTImporter:
self._update_external_bug_urls(bug, cve.bug_urls)
self._update_patches(bug, cve.patch_urls)
+ self._update_break_fix(bug, cve.break_fix_data)
self._update_tags(bug, cve.global_tags, cve.distro_packages)
self._create_bug_tasks(
@@ -236,6 +238,7 @@ class UCTImporter:
self._assign_bug_tasks(bug, cve.assignee)
self._update_external_bug_urls(bug, cve.bug_urls)
self._update_patches(bug, cve.patch_urls)
+ self._update_break_fix(bug, cve.break_fix_data)
self._update_tags(bug, cve.global_tags, cve.distro_packages)
# Update or add new Vulnerabilities
@@ -482,6 +485,35 @@ class UCTImporter:
description=title,
)
+ def _update_break_fix(
+ self, bug: BugModel, break_fix_data: List[CVE.BreakFix]
+ ):
+ break_fix_by_pkg = defaultdict(list)
+ for break_fix in break_fix_data:
+ break_fix_by_pkg[break_fix.package_name].append(
+ {"break": break_fix.break_, "fix": break_fix.fix}
+ )
+
+ for presence in bug.presences:
+ if break_fix := break_fix_by_pkg.pop(
+ presence.source_package_name, None
+ ):
+ presence.break_fix_data = break_fix
+ else:
+ # Remove non existing
+ bug.removePresence(presence, self.bug_importer)
+
+ for package in break_fix_by_pkg:
+ # Create new presence
+ bug.addPresence(
+ project=None,
+ distribution=None,
+ source_package_name=package,
+ git_repository=None,
+ break_fix_data=break_fix_by_pkg[package],
+ user=self.bug_importer,
+ )
+
def _make_bug_description(self, cve: CVE) -> str:
"""
Some `CVE` fields can't be mapped to Launchpad models.