Merge ~andrey-fedoseev/launchpad:uct-import-export-break-fix into launchpad:master

Proposed by Andrey Fedoseev
Status: Needs review
Proposed branch: ~andrey-fedoseev/launchpad:uct-import-export-break-fix
Merge into: launchpad:master
Prerequisite: ~andrey-fedoseev/launchpad:bug-presense
Diff against target: 486 lines (+260/-4)
5 files modified
lib/lp/bugs/model/bugpresence.py (+3/-0)
lib/lp/bugs/scripts/tests/test_uct.py (+102/-0)
lib/lp/bugs/scripts/uct/models.py (+93/-4)
lib/lp/bugs/scripts/uct/uctexport.py (+16/-0)
lib/lp/bugs/scripts/uct/uctimport.py (+46/-0)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+432181@code.launchpad.net

Commit message

UCT import/export: handle the `break-fix` entries

Description of the change

For each `break-fix` entry create a `BugPresence` instance linked to the default git repository of the project (if exists)

If a `break-fix` entry includes multiple git commits in the "fixed" section it means that either of them fixes the issue. So, we create multiple `BugPresence` instances, one per commit listed in the "fixed" section.

Items such as `local-CVE-2022-23222-fix` are currently ignored

To post a comment you must log in.
d62597c... by Andrey Fedoseev

UCT import/export: handle the `break-fix` entries

For each `break-fix` entry create a `BugPresence` instance linked to the default git repository of the project (if exists)

If a `break-fix` entry includes multiple git commits in the "fixed" section it means that either of them fixes the issue. So, we create multiple `BugPresence` instances, one per commit listed in the "fixed" section.

Items such as `local-CVE-2022-23222-fix` are currently ignored

Unmerged commits

d62597c... by Andrey Fedoseev

UCT import/export: handle the `break-fix` entries

For each `break-fix` entry create a `BugPresence` instance linked to the default git repository of the project (if exists)

If a `break-fix` entry includes multiple git commits in the "fixed" section it means that either of them fixes the issue. So, we create multiple `BugPresence` instances, one per commit listed in the "fixed" section.

Items such as `local-CVE-2022-23222-fix` are currently ignored

Succeeded
[SUCCEEDED] docs:0 (build)
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] mypy:0 (build)
13 of 3 results
460e925... by Andrey Fedoseev

Add `BugPresence` model

It represents a range of versions or git commits in which the bug was present.

Succeeded
[SUCCEEDED] docs:0 (build)
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] mypy:0 (build)
13 of 3 results

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/bugs/model/bugpresence.py b/lib/lp/bugs/model/bugpresence.py
2index b21371e..4ca8128 100644
3--- a/lib/lp/bugs/model/bugpresence.py
4+++ b/lib/lp/bugs/model/bugpresence.py
5@@ -20,6 +20,9 @@ class BugPresence(StormBase):
6 """See `IBugPresence`."""
7
8 __storm_table__ = "BugPresence"
9+ __storm_order__ = [
10+ "id",
11+ ]
12
13 id = Int(primary=True)
14
15diff --git a/lib/lp/bugs/scripts/tests/test_uct.py b/lib/lp/bugs/scripts/tests/test_uct.py
16index 9813d86..3ce76b7 100644
17--- a/lib/lp/bugs/scripts/tests/test_uct.py
18+++ b/lib/lp/bugs/scripts/tests/test_uct.py
19@@ -6,10 +6,12 @@ from typing import List
20
21 from pytz import UTC
22 from zope.component import getUtility
23+from zope.security.proxy import removeSecurityProxy
24
25 from lp.app.enums import InformationType
26 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
27 from lp.bugs.enums import VulnerabilityStatus
28+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
29 from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus
30 from lp.bugs.model.bug import Bug
31 from lp.bugs.model.bugtask import BugTask
32@@ -274,6 +276,20 @@ class TestCVE(TestCaseWithFactory):
33 tags=set(),
34 patches=[
35 UCTRecord.Patch(
36+ patch_type="break-fix",
37+ entry=(
38+ "457f44363a8894135c85b7a9afd2bd8196db24ab "
39+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841|"
40+ "4969c06a0d83c9c3dc50b8efcdc8eeedfce896f6"
41+ ),
42+ ),
43+ UCTRecord.Patch(
44+ patch_type="break-fix",
45+ entry=(
46+ "- c25b2ae136039ffa820c26138ed4a5e5f3ab3841"
47+ ),
48+ ),
49+ UCTRecord.Patch(
50 patch_type="upstream",
51 entry=(
52 "https://github.com/389ds/389-ds-base/"
53@@ -416,6 +432,30 @@ class TestCVE(TestCaseWithFactory):
54 importance=None,
55 status=BugTaskStatus.FIXRELEASED,
56 status_explanation="reason 4",
57+ break_fixes=[
58+ CVE.BreakFix(
59+ broken_git_commit_sha1=(
60+ "457f44363a8894135c85b7a9afd2bd8196db24ab"
61+ ),
62+ fixed_git_commit_sha1=(
63+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841"
64+ ),
65+ ),
66+ CVE.BreakFix(
67+ broken_git_commit_sha1=(
68+ "457f44363a8894135c85b7a9afd2bd8196db24ab"
69+ ),
70+ fixed_git_commit_sha1=(
71+ "4969c06a0d83c9c3dc50b8efcdc8eeedfce896f6"
72+ ),
73+ ),
74+ CVE.BreakFix(
75+ broken_git_commit_sha1=None,
76+ fixed_git_commit_sha1=(
77+ "c25b2ae136039ffa820c26138ed4a5e5f3ab3841"
78+ ),
79+ ),
80+ ],
81 ),
82 CVE.UpstreamPackage(
83 target=product_2,
84@@ -423,6 +463,7 @@ class TestCVE(TestCaseWithFactory):
85 importance=None,
86 status=BugTaskStatus.FIXRELEASED,
87 status_explanation="",
88+ break_fixes=[],
89 ),
90 ],
91 importance=BugTaskImportance.CRITICAL,
92@@ -564,6 +605,13 @@ class TestUCTImporterExporter(TestCaseWithFactory):
93 distroseries=self.esm_current_series,
94 ).productseries.product
95
96+ self.product_1_git_repo = self.factory.makeGitRepository(
97+ target=self.product_1, target_default=True
98+ )
99+ self.product_2_git_repo = self.factory.makeGitRepository(
100+ target=self.product_2, target_default=True
101+ )
102+
103 for series in (
104 self.ubuntu_supported_series,
105 self.ubuntu_current_series,
106@@ -661,6 +709,24 @@ class TestUCTImporterExporter(TestCaseWithFactory):
107 importance=BugTaskImportance.HIGH,
108 status=BugTaskStatus.FIXRELEASED,
109 status_explanation="fix released",
110+ break_fixes=[
111+ CVE.BreakFix(
112+ broken_git_commit_sha1=(
113+ "054623105728b06852f077299e2bf1bf3d5f2b0b"
114+ ),
115+ fixed_git_commit_sha1=(
116+ "db7bee653859ef7179be933e7d1384644f795f26"
117+ ),
118+ ),
119+ CVE.BreakFix(
120+ broken_git_commit_sha1=(
121+ "054623105728b06852f077299e2bf1bf3d5f2b0b"
122+ ),
123+ fixed_git_commit_sha1=(
124+ "6e61dc9da0b7a0d91d57c2e20b5ea4fd2d4e7e53"
125+ ),
126+ ),
127+ ],
128 ),
129 CVE.UpstreamPackage(
130 target=self.product_2,
131@@ -668,6 +734,20 @@ class TestUCTImporterExporter(TestCaseWithFactory):
132 importance=BugTaskImportance.LOW,
133 status=BugTaskStatus.WONTFIX,
134 status_explanation="ignored",
135+ break_fixes=[
136+ CVE.BreakFix(
137+ broken_git_commit_sha1=None,
138+ fixed_git_commit_sha1=(
139+ "6e61dc9da0b7a0d91d57c2e20b5ea4fd2d4e7e53"
140+ ),
141+ ),
142+ CVE.BreakFix(
143+ broken_git_commit_sha1=(
144+ "4e9b4a6883dd97aff53ae3b08eb900716a5469dc"
145+ ),
146+ fixed_git_commit_sha1=None,
147+ ),
148+ ],
149 ),
150 ],
151 importance=BugTaskImportance.MEDIUM,
152@@ -724,6 +804,7 @@ class TestUCTImporterExporter(TestCaseWithFactory):
153 self.assertEqual(sorted(cve.bug_urls), sorted(w.url for w in watches))
154
155 self.checkBugAttachments(bug, cve)
156+ self.checkBugPresence(bug, cve)
157
158 def checkBugTasks(self, bug: Bug, cve: CVE):
159 bug_tasks = bug.bugtasks # type: List[BugTask]
160@@ -799,6 +880,27 @@ class TestUCTImporterExporter(TestCaseWithFactory):
161 )
162 self.assertEqual(expected_title, attachment.title)
163
164+ def checkBugPresence(self, bug: Bug, cve: CVE):
165+ expected_break_fixes = set()
166+ for upstream_package in cve.upstream_packages:
167+ for break_fix in upstream_package.break_fixes:
168+ expected_break_fixes.add(
169+ (upstream_package.target.name, break_fix)
170+ )
171+ actual_break_fixes = set()
172+ for bp in getUtility(IBugPresenceSet).getByBug(bug):
173+ project = removeSecurityProxy(bp).git_repository.project
174+ actual_break_fixes.add(
175+ (
176+ project.name,
177+ CVE.BreakFix(
178+ broken_git_commit_sha1=bp.broken_git_commit_sha1,
179+ fixed_git_commit_sha1=bp.fixed_git_commit_sha1,
180+ ),
181+ )
182+ )
183+ self.assertSetEqual(expected_break_fixes, actual_break_fixes)
184+
185 def checkVulnerabilities(self, bug: Bug, cve: CVE):
186 vulnerabilities = bug.vulnerabilities
187
188diff --git a/lib/lp/bugs/scripts/uct/models.py b/lib/lp/bugs/scripts/uct/models.py
189index fd77ea5..2540eda 100644
190--- a/lib/lp/bugs/scripts/uct/models.py
191+++ b/lib/lp/bugs/scripts/uct/models.py
192@@ -450,6 +450,14 @@ class CVE:
193 ),
194 )
195
196+ BreakFix = NamedTuple(
197+ "BreakFix",
198+ (
199+ ("broken_git_commit_sha1", Optional[str]),
200+ ("fixed_git_commit_sha1", Optional[str]),
201+ ),
202+ )
203+
204 UpstreamPackage = NamedTuple(
205 "UpstreamPackage",
206 (
207@@ -458,6 +466,7 @@ class CVE:
208 ("importance", Optional[BugTaskImportance]),
209 ("status", BugTaskStatus),
210 ("status_explanation", str),
211+ ("break_fixes", List[BreakFix]),
212 ),
213 )
214
215@@ -475,6 +484,7 @@ class CVE:
216 # https://github.com/389ds/389-ds-base/commit/123 (1.4.4)
217 # https://github.com/389ds/389-ds-base/commit/345
218 PATCH_URL_RE = re.compile(r"^(?P<url>.+?)(\s+\((?P<notes>.+)\))?$")
219+ GIT_HASH_RE = re.compile(r"^[a-f0-9]{40}$")
220
221 PRIORITY_MAP = {
222 UCTRecord.Priority.CRITICAL: BugTaskImportance.CRITICAL,
223@@ -563,6 +573,7 @@ class CVE:
224 distro_packages = []
225 series_packages = []
226 patch_urls = []
227+ break_fixes = {} # type: Dict[SourcePackageName, List[CVE.BreakFix]]
228
229 spn_set = getUtility(ISourcePackageNameSet)
230
231@@ -577,6 +588,10 @@ class CVE:
232 cls.get_patch_urls(source_package_name, uct_package.patches)
233 )
234
235+ break_fixes[source_package_name] = cls.get_break_fixes(
236+ uct_package.patches
237+ )
238+
239 package_importance = (
240 cls.PRIORITY_MAP[uct_package.priority]
241 if uct_package.priority
242@@ -665,6 +680,7 @@ class CVE:
243 ),
244 status=cls.BUG_TASK_STATUS_MAP[upstream_status.status],
245 status_explanation=upstream_status.reason,
246+ break_fixes=break_fixes[source_package_name],
247 )
248 )
249
250@@ -786,15 +802,32 @@ class CVE:
251 patches=[],
252 )
253
254+ grouped_break_fixes = OrderedDict()
255+ for break_fix in upstream_package.break_fixes:
256+ if break_fix.broken_git_commit_sha1 not in grouped_break_fixes:
257+ grouped_break_fixes[break_fix.broken_git_commit_sha1] = []
258+ if break_fix.fixed_git_commit_sha1:
259+ grouped_break_fixes[
260+ break_fix.broken_git_commit_sha1
261+ ].append(break_fix.fixed_git_commit_sha1)
262+
263+ for break_commit, fixed_commits in grouped_break_fixes.items():
264+ entry = "{} {}".format(
265+ break_commit or "-",
266+ "|".join(fixed_commits) or "-",
267+ )
268+ packages_by_name[
269+ upstream_package.package_name.name
270+ ].patches.append(
271+ UCTRecord.Patch(patch_type="break-fix", entry=entry)
272+ )
273+
274 for patch_url in self.patch_urls:
275 entry = patch_url.url
276 if patch_url.notes:
277 entry = "{} ({})".format(entry, patch_url.notes)
278 packages_by_name[patch_url.package_name.name].patches.append(
279- UCTRecord.Patch(
280- patch_type=patch_url.type,
281- entry=entry,
282- )
283+ UCTRecord.Patch(patch_type=patch_url.type, entry=entry)
284 )
285
286 return UCTRecord(
287@@ -909,3 +942,59 @@ class CVE:
288 url=url,
289 notes=notes,
290 )
291+
292+ @classmethod
293+ def get_break_fixes(
294+ cls,
295+ patches: List[UCTRecord.Patch],
296+ ) -> List[BreakFix]:
297+
298+ break_fixes = []
299+
300+ for patch in patches:
301+ if patch.patch_type != "break-fix":
302+ continue
303+
304+ try:
305+ broken_entry, joint_fixed_entries = patch.entry.split()
306+ except ValueError:
307+ logger.error(
308+ "Could not parse break-fix entry: %s", patch.entry
309+ )
310+ continue
311+
312+ if broken_entry == "-":
313+ broken_entry = None
314+ elif not cls.GIT_HASH_RE.match(broken_entry):
315+ logger.error(
316+ "broken entry doesn't look like git commit SHA1: %s",
317+ broken_entry,
318+ )
319+ continue
320+
321+ if joint_fixed_entries == "-":
322+ if broken_entry:
323+ break_fixes.append(
324+ cls.BreakFix(
325+ broken_git_commit_sha1=broken_entry,
326+ fixed_git_commit_sha1=None,
327+ )
328+ )
329+ continue
330+
331+ for fixed_entry in joint_fixed_entries.split("|"):
332+ # TODO: handle the with fixed_entry="local-CVE-2021-20177"
333+ if cls.GIT_HASH_RE.match(fixed_entry):
334+ break_fixes.append(
335+ cls.BreakFix(
336+ broken_git_commit_sha1=broken_entry,
337+ fixed_git_commit_sha1=fixed_entry,
338+ )
339+ )
340+ else:
341+ logger.error(
342+ "fixed entry doesn't look like git commit " "SHA1: %s",
343+ fixed_entry,
344+ )
345+
346+ return break_fixes
347diff --git a/lib/lp/bugs/scripts/uct/uctexport.py b/lib/lp/bugs/scripts/uct/uctexport.py
348index 8a1dd27..ff1a9c5 100644
349--- a/lib/lp/bugs/scripts/uct/uctexport.py
350+++ b/lib/lp/bugs/scripts/uct/uctexport.py
351@@ -12,6 +12,7 @@ from zope.security.proxy import removeSecurityProxy
352
353 from lp.bugs.interfaces.bug import IBugSet
354 from lp.bugs.interfaces.bugattachment import BugAttachmentType
355+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
356 from lp.bugs.model.bug import Bug as BugModel
357 from lp.bugs.model.bugtask import BugTask
358 from lp.bugs.model.cve import Cve as CveModel
359@@ -173,6 +174,13 @@ class UCTExporter:
360 )
361 )
362
363+ bug_presence_by_product = defaultdict(list)
364+ for bp in getUtility(IBugPresenceSet).getByBug(bug):
365+ if bp.git_repository:
366+ bug_presence_by_product[
367+ removeSecurityProxy(bp).git_repository.project
368+ ].append(bp)
369+
370 upstream_packages = []
371 for bug_task in bug_tasks:
372 target = removeSecurityProxy(bug_task.target)
373@@ -187,6 +195,7 @@ class UCTExporter:
374 package_name = package_name_by_product[target]
375 up_importance = bug_task.importance
376 package_importance = package_importances.get(target.name)
377+ bug_presences = bug_presence_by_product.get(target, [])
378 upstream_packages.append(
379 CVE.UpstreamPackage(
380 target=target,
381@@ -198,6 +207,13 @@ class UCTExporter:
382 ),
383 status=bug_task.status,
384 status_explanation=bug_task.status_explanation,
385+ break_fixes=[
386+ CVE.BreakFix(
387+ broken_git_commit_sha1=bp.broken_git_commit_sha1,
388+ fixed_git_commit_sha1=bp.fixed_git_commit_sha1,
389+ )
390+ for bp in bug_presences
391+ ],
392 )
393 )
394
395diff --git a/lib/lp/bugs/scripts/uct/uctimport.py b/lib/lp/bugs/scripts/uct/uctimport.py
396index 5bc8411..607dd2e 100644
397--- a/lib/lp/bugs/scripts/uct/uctimport.py
398+++ b/lib/lp/bugs/scripts/uct/uctimport.py
399@@ -25,6 +25,7 @@ Three types of bug tags are created:
400 status of the package in upstream.
401 """
402 import logging
403+from collections import defaultdict
404 from datetime import timezone
405 from itertools import chain
406 from pathlib import Path
407@@ -39,6 +40,7 @@ from lp.app.interfaces.launchpad import ILaunchpadCelebrities
408 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
409 from lp.bugs.interfaces.bugactivity import IBugActivitySet
410 from lp.bugs.interfaces.bugattachment import BugAttachmentType
411+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
412 from lp.bugs.interfaces.bugtask import BugTaskImportance, IBugTaskSet
413 from lp.bugs.interfaces.bugwatch import IBugWatchSet
414 from lp.bugs.interfaces.cve import ICveSet
415@@ -48,6 +50,7 @@ from lp.bugs.model.bugtask import BugTask
416 from lp.bugs.model.cve import Cve as CveModel
417 from lp.bugs.model.vulnerability import Vulnerability
418 from lp.bugs.scripts.uct.models import CVE, UCTRecord
419+from lp.code.interfaces.gitrepository import IGitRepositorySet
420 from lp.registry.model.distribution import Distribution
421 from lp.registry.model.person import Person
422 from lp.services.database.constants import UTC_NOW
423@@ -168,6 +171,7 @@ class UCTImporter:
424
425 self._update_external_bug_urls(bug, cve.bug_urls)
426 self._update_patches(bug, cve.patch_urls)
427+ self._update_bug_presence(bug, cve.upstream_packages)
428
429 self._create_bug_tasks(
430 bug,
431@@ -225,6 +229,7 @@ class UCTImporter:
432 self._assign_bug_tasks(bug, cve.assignee)
433 self._update_external_bug_urls(bug, cve.bug_urls)
434 self._update_patches(bug, cve.patch_urls)
435+ self._update_bug_presence(bug, cve.upstream_packages)
436
437 # Update or add new Vulnerabilities
438 vulnerabilities_by_distro = {
439@@ -455,6 +460,47 @@ class UCTImporter:
440 description=title,
441 )
442
443+ def _update_bug_presence(
444+ self, bug: BugModel, upstream_packages: List[CVE.UpstreamPackage]
445+ ):
446+ bug_presence_set = getUtility(IBugPresenceSet)
447+ bug_presences_by_target = defaultdict(list)
448+ for bp in bug_presence_set.getByBug(bug):
449+ bug_presences_by_target[bp.target].append(bp)
450+ for upstream_package in upstream_packages:
451+ if not upstream_package.break_fixes:
452+ continue
453+ git_repo = getUtility(IGitRepositorySet).getDefaultRepository(
454+ upstream_package.target
455+ )
456+
457+ if not git_repo:
458+ logger.error(
459+ "Could not find a git repository for %s "
460+ "to save bug presence information",
461+ upstream_package.target.name,
462+ )
463+ continue
464+
465+ existing_break_fixes = set()
466+ for bp in bug_presences_by_target.get(git_repo, []):
467+ existing_break_fixes.add(
468+ CVE.BreakFix(
469+ broken_git_commit_sha1=bp.broken_git_commit_sha1,
470+ fixed_git_commit_sha1=bp.fixed_git_commit_sha1,
471+ )
472+ )
473+
474+ for break_fix in upstream_package.break_fixes:
475+ if break_fix is existing_break_fixes:
476+ continue
477+ bug_presence_set.new(
478+ bug=bug,
479+ target=git_repo,
480+ broken_git_commit_sha1=break_fix.broken_git_commit_sha1,
481+ fixed_git_commit_sha1=break_fix.fixed_git_commit_sha1,
482+ )
483+
484 def _make_bug_description(self, cve: CVE) -> str:
485 """
486 Some `CVE` fields can't be mapped to Launchpad models.

Subscribers

People subscribed via source and target branches

to status/vote changes: