Skip to content

Commit 6de8576

Browse files
authored
fix(add-patch): Make patch discovery work across whole spec (#18)
Makes patch-add look for highest existing patchN across all packages of the spec; ditto for patch-remove.
1 parent 11b3d80 commit 6de8576

File tree

3 files changed

+178
-48
lines changed

3 files changed

+178
-48
lines changed

internal/app/azldev/core/sources/overlays.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func ApplySpecOverlay(overlay projectconfig.ComponentOverlay, openedSpec *spec.S
173173
return fmt.Errorf("failed to add patch entry to spec:\n%w", err)
174174
}
175175
case projectconfig.ComponentOverlayRemovePatch:
176-
err := openedSpec.RemovePatchEntry(overlay.PackageName, overlay.Filename)
176+
err := openedSpec.RemovePatchEntry(overlay.Filename)
177177
if err != nil {
178178
return fmt.Errorf("failed to remove patch entry from spec:\n%w", err)
179179
}

internal/rpm/spec/edit.go

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (s *Spec) UpdateExistingTag(packageName string, tag string, value string) (
5050

5151
var updated bool
5252

53-
err = s.VisitTags(packageName, func(tagLine *TagLine, ctx *Context) error {
53+
err = s.VisitTagsPackage(packageName, func(tagLine *TagLine, ctx *Context) error {
5454
if strings.ToLower(tagLine.Tag) != tagToCompareAgainst {
5555
return nil
5656
}
@@ -100,20 +100,14 @@ func (s *Spec) RemoveTag(packageName string, tag string, value string) (err erro
100100
return nil
101101
}
102102

103-
// VisitTags iterates over all tag lines in the given package, calling the visitor function
104-
// for each one. The visitor receives the parsed [TagLine] and the mutation [Context]. This
105-
// extracts the common target-type / package / tag-type filtering that many tag-oriented
106-
// methods need.
107-
func (s *Spec) VisitTags(packageName string, visitor func(tagLine *TagLine, ctx *Context) error) error {
103+
// VisitTags iterates over all tag lines across all packages, calling the visitor function
104+
// for each one. The visitor receives the parsed [TagLine] and the mutation [Context].
105+
func (s *Spec) VisitTags(visitor func(tagLine *TagLine, ctx *Context) error) error {
108106
return s.Visit(func(ctx *Context) error {
109107
if ctx.Target.TargetType != SectionLineTarget {
110108
return nil
111109
}
112110

113-
if ctx.CurrentSection.Package != packageName {
114-
return nil
115-
}
116-
117111
if ctx.Target.Line.Parsed.GetType() != Tag {
118112
return nil
119113
}
@@ -127,13 +121,27 @@ func (s *Spec) VisitTags(packageName string, visitor func(tagLine *TagLine, ctx
127121
})
128122
}
129123

124+
// VisitTagsPackage iterates over all tag lines in the given package, calling the visitor
125+
// function for each one. The visitor receives the parsed [TagLine] and the mutation [Context].
126+
// This extracts the common target-type / package / tag-type filtering that many tag-oriented
127+
// methods need.
128+
func (s *Spec) VisitTagsPackage(packageName string, visitor func(tagLine *TagLine, ctx *Context) error) error {
129+
return s.VisitTags(func(tagLine *TagLine, ctx *Context) error {
130+
if ctx.CurrentSection.Package != packageName {
131+
return nil
132+
}
133+
134+
return visitor(tagLine, ctx)
135+
})
136+
}
137+
130138
// RemoveTagsMatching removes all tags in the given package for which the provided matcher
131139
// function returns true. The matcher receives the tag name and value as arguments. Returns
132140
// the number of tags removed. If no matching tags were found, returns 0 and no error.
133141
func (s *Spec) RemoveTagsMatching(packageName string, matcher func(tag, value string) bool) (int, error) {
134142
removed := 0
135143

136-
err := s.VisitTags(packageName, func(tagLine *TagLine, ctx *Context) error {
144+
err := s.VisitTagsPackage(packageName, func(tagLine *TagLine, ctx *Context) error {
137145
if !matcher(tagLine.Tag, tagLine.Value) {
138146
return nil
139147
}
@@ -600,7 +608,7 @@ func (s *Spec) AddPatchEntry(packageName, filename string) error {
600608
return s.AppendLinesToSection("%patchlist", "", []string{filename})
601609
}
602610

603-
highest, err := s.GetHighestPatchTagNumber(packageName)
611+
highest, err := s.GetHighestPatchTagNumber()
604612
if err != nil {
605613
return fmt.Errorf("failed to scan for existing patch tags:\n%w", err)
606614
}
@@ -610,24 +618,13 @@ func (s *Spec) AddPatchEntry(packageName, filename string) error {
610618

611619
// RemovePatchEntry removes all references to patches matching the given pattern from the spec.
612620
// The pattern is a glob pattern (supporting doublestar syntax) matched against PatchN tag values
613-
// and %patchlist entries. Returns an error if no references matched the pattern.
614-
func (s *Spec) RemovePatchEntry(packageName, pattern string) error {
615-
slog.Debug("Removing patch entry from spec", "package", packageName, "pattern", pattern)
621+
// and %patchlist entries across all packages. Returns an error if no references matched the pattern.
622+
func (s *Spec) RemovePatchEntry(pattern string) error {
623+
slog.Debug("Removing patch entry from spec", "pattern", pattern)
616624

617625
totalRemoved := 0
618626

619-
tagsRemoved, err := s.RemoveTagsMatching(packageName, func(tag, value string) bool {
620-
if _, ok := ParsePatchTagNumber(tag); !ok {
621-
return false
622-
}
623-
624-
matched, matchErr := doublestar.Match(pattern, value)
625-
if matchErr != nil {
626-
return false
627-
}
628-
629-
return matched
630-
})
627+
tagsRemoved, err := s.removePatchTagsMatching(pattern)
631628
if err != nil {
632629
return fmt.Errorf("failed to remove matching patch tags:\n%w", err)
633630
}
@@ -655,6 +652,33 @@ func (s *Spec) RemovePatchEntry(packageName, pattern string) error {
655652
return nil
656653
}
657654

655+
// removePatchTagsMatching removes all PatchN tags across all packages whose values match the
656+
// given glob pattern. Returns the number of tags removed.
657+
func (s *Spec) removePatchTagsMatching(pattern string) (int, error) {
658+
removed := 0
659+
660+
err := s.VisitTags(func(tagLine *TagLine, ctx *Context) error {
661+
if _, ok := ParsePatchTagNumber(tagLine.Tag); !ok {
662+
return nil
663+
}
664+
665+
matched, matchErr := doublestar.Match(pattern, tagLine.Value)
666+
if matchErr != nil {
667+
return fmt.Errorf("failed to match glob pattern %#q against %#q:\n%w", pattern, tagLine.Value, matchErr)
668+
}
669+
670+
if matched {
671+
ctx.RemoveLine()
672+
673+
removed++
674+
}
675+
676+
return nil
677+
})
678+
679+
return removed, err
680+
}
681+
658682
// removePatchlistEntriesMatching removes lines from the %patchlist section whose trimmed content
659683
// matches the given glob pattern. Returns the number of entries removed.
660684
func (s *Spec) removePatchlistEntriesMatching(pattern string) (int, error) {
@@ -692,13 +716,13 @@ func (s *Spec) removePatchlistEntriesMatching(pattern string) (int, error) {
692716
}
693717

694718
// GetHighestPatchTagNumber scans the spec for all PatchN tags (where N is a decimal number)
695-
// in the given package and returns the highest N found. Returns -1 if no numeric patch tags
719+
// across all packages and returns the highest N found. Returns -1 if no numeric patch tags
696720
// exist. Tags with non-numeric suffixes (e.g., macro-based names like Patch%{n}) are silently
697721
// skipped.
698-
func (s *Spec) GetHighestPatchTagNumber(packageName string) (int, error) {
722+
func (s *Spec) GetHighestPatchTagNumber() (int, error) {
699723
highest := -1
700724

701-
err := s.VisitTags(packageName, func(tagLine *TagLine, _ *Context) error {
725+
err := s.VisitTags(func(tagLine *TagLine, _ *Context) error {
702726
num, isPatchTag := ParsePatchTagNumber(tagLine.Tag)
703727
if isPatchTag && num > highest {
704728
highest = num

internal/rpm/spec/edit_test.go

Lines changed: 123 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,10 +1150,9 @@ func TestHasSection(t *testing.T) {
11501150

11511151
func TestGetHighestPatchTagNumber(t *testing.T) {
11521152
tests := []struct {
1153-
name string
1154-
input string
1155-
packageName string
1156-
expected int
1153+
name string
1154+
input string
1155+
expected int
11571156
}{
11581157
{
11591158
name: "no patch tags",
@@ -1181,16 +1180,9 @@ func TestGetHighestPatchTagNumber(t *testing.T) {
11811180
expected: 1,
11821181
},
11831182
{
1184-
name: "patches in sub-package are isolated",
1185-
input: "Name: test\nPatch0: main.patch\n\n%package devel\nPatch1: devel.patch\n",
1186-
packageName: "devel",
1187-
expected: 1,
1188-
},
1189-
{
1190-
name: "main package with sub-package present",
1191-
input: "Name: test\nPatch0: main.patch\n\n%package devel\nPatch5: devel.patch\n",
1192-
packageName: "",
1193-
expected: 0,
1183+
name: "scans across all packages",
1184+
input: "Name: test\nPatch0: main.patch\n\n%package devel\nPatch5: devel.patch\n",
1185+
expected: 5,
11941186
},
11951187
}
11961188

@@ -1199,7 +1191,7 @@ func TestGetHighestPatchTagNumber(t *testing.T) {
11991191
specFile, err := spec.OpenSpec(strings.NewReader(testCase.input))
12001192
require.NoError(t, err)
12011193

1202-
result, err := specFile.GetHighestPatchTagNumber(testCase.packageName)
1194+
result, err := specFile.GetHighestPatchTagNumber()
12031195
require.NoError(t, err)
12041196
assert.Equal(t, testCase.expected, result)
12051197
})
@@ -1281,7 +1273,6 @@ func TestRemovePatchEntry(t *testing.T) {
12811273
tests := []struct {
12821274
name string
12831275
input string
1284-
packageName string
12851276
pattern string
12861277
expectedOutput string
12871278
expectedFailure bool
@@ -1337,14 +1328,28 @@ func TestRemovePatchEntry(t *testing.T) {
13371328
pattern: "*.patch",
13381329
expectedOutput: "Name: test\n",
13391330
},
1331+
{
1332+
name: "removes matching patches across all packages",
1333+
input: "Name: test\nPatch0: CVE-001.patch\nPatch1: keep.patch\n\n%package devel\n" +
1334+
"Summary: Dev\nPatch2: CVE-002.patch\nPatch3: also-keep.patch\n",
1335+
pattern: "CVE-*.patch",
1336+
expectedOutput: "Name: test\nPatch1: keep.patch\n\n%package devel\nSummary: Dev\nPatch3: also-keep.patch\n",
1337+
},
1338+
{
1339+
name: "no match across multiple packages returns error",
1340+
input: "Name: test\nPatch0: keep.patch\n\n%package devel\nSummary: Dev\nPatch1: also-keep.patch\n",
1341+
pattern: "nonexistent.patch",
1342+
expectedFailure: true,
1343+
errorContains: "no patches matching",
1344+
},
13401345
}
13411346

13421347
for _, testCase := range tests {
13431348
t.Run(testCase.name, func(t *testing.T) {
13441349
specFile, err := spec.OpenSpec(strings.NewReader(testCase.input))
13451350
require.NoError(t, err)
13461351

1347-
err = specFile.RemovePatchEntry(testCase.packageName, testCase.pattern)
1352+
err = specFile.RemovePatchEntry(testCase.pattern)
13481353
if testCase.expectedFailure {
13491354
require.Error(t, err)
13501355

@@ -1390,3 +1395,104 @@ func TestParsePatchTagNumber(t *testing.T) {
13901395
})
13911396
}
13921397
}
1398+
1399+
func TestVisitTags(t *testing.T) {
1400+
input := `Name: main-pkg
1401+
Version: 1.0
1402+
Patch0: main.patch
1403+
1404+
%package devel
1405+
Summary: Development files
1406+
Patch1: devel.patch
1407+
1408+
%package -n other
1409+
Summary: Other package
1410+
Patch2: other.patch
1411+
`
1412+
1413+
tests := []struct {
1414+
name string
1415+
expectedTags []string
1416+
}{
1417+
{
1418+
name: "visits tags across all packages",
1419+
expectedTags: []string{"Name", "Version", "Patch0", "Summary", "Patch1", "Summary", "Patch2"},
1420+
},
1421+
}
1422+
1423+
for _, testCase := range tests {
1424+
t.Run(testCase.name, func(t *testing.T) {
1425+
sf, err := spec.OpenSpec(strings.NewReader(input))
1426+
require.NoError(t, err)
1427+
1428+
var tags []string
1429+
1430+
err = sf.VisitTags(func(tagLine *spec.TagLine, _ *spec.Context) error {
1431+
tags = append(tags, tagLine.Tag)
1432+
1433+
return nil
1434+
})
1435+
require.NoError(t, err)
1436+
assert.Equal(t, testCase.expectedTags, tags)
1437+
})
1438+
}
1439+
}
1440+
1441+
func TestVisitTagsPackage(t *testing.T) {
1442+
input := `Name: main-pkg
1443+
Version: 1.0
1444+
Patch0: main.patch
1445+
1446+
%package devel
1447+
Summary: Development files
1448+
Patch1: devel.patch
1449+
1450+
%package -n other
1451+
Summary: Other package
1452+
Patch2: other.patch
1453+
`
1454+
1455+
tests := []struct {
1456+
name string
1457+
packageName string
1458+
expectedTags []string
1459+
}{
1460+
{
1461+
name: "global package only",
1462+
packageName: "",
1463+
expectedTags: []string{"Name", "Version", "Patch0"},
1464+
},
1465+
{
1466+
name: "devel sub-package only",
1467+
packageName: "devel",
1468+
expectedTags: []string{"Summary", "Patch1"},
1469+
},
1470+
{
1471+
name: "other sub-package only",
1472+
packageName: "other",
1473+
expectedTags: []string{"Summary", "Patch2"},
1474+
},
1475+
{
1476+
name: "non-existing package returns no tags",
1477+
packageName: "nonexistent",
1478+
expectedTags: nil,
1479+
},
1480+
}
1481+
1482+
for _, testCase := range tests {
1483+
t.Run(testCase.name, func(t *testing.T) {
1484+
sf, err := spec.OpenSpec(strings.NewReader(input))
1485+
require.NoError(t, err)
1486+
1487+
var tags []string
1488+
1489+
err = sf.VisitTagsPackage(testCase.packageName, func(tagLine *spec.TagLine, _ *spec.Context) error {
1490+
tags = append(tags, tagLine.Tag)
1491+
1492+
return nil
1493+
})
1494+
require.NoError(t, err)
1495+
assert.Equal(t, testCase.expectedTags, tags)
1496+
})
1497+
}
1498+
}

0 commit comments

Comments
 (0)