Some NuGet package enhancements (#30280)

Fixes #30265

1. Read second type of dependencies
2. Render `Description` and `ReleaseNotes`

old:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/abac057c-11cd-4d25-b196-01ff899d948e)

new:

![grafik](https://github.com/go-gitea/gitea/assets/1666336/35302273-740c-481a-a031-1f80d2d7d336)

The NuGet spec does not specify what kind of text can be stored in the
description but we can best guess markdown. The official NuGet registry
just [converts the newlines to html
lines](https://www.nuget.org/packages/rb.Firefox#readme-body-tab).

3. Extract and render the readme. This is the new and better place to
store larger text than in the description. The content is markdown.

![grafik](https://github.com/go-gitea/gitea/assets/1666336/f442264e-3735-4b55-92c4-3b89a8ebafb0)

---------

Co-authored-by: Benjamin Heemann <benjamin.heemann@raith.de>
This commit is contained in:
KN4CK3R 2024-04-07 18:46:59 +02:00 committed by GitHub
parent 36887ed392
commit 8498e67309
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 32 deletions

View File

@ -58,6 +58,7 @@ type Package struct {
type Metadata struct { type Metadata struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
ReleaseNotes string `json:"release_notes,omitempty"` ReleaseNotes string `json:"release_notes,omitempty"`
Readme string `json:"readme,omitempty"`
Authors string `json:"authors,omitempty"` Authors string `json:"authors,omitempty"`
ProjectURL string `json:"project_url,omitempty"` ProjectURL string `json:"project_url,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"` RepositoryURL string `json:"repository_url,omitempty"`
@ -71,6 +72,7 @@ type Dependency struct {
Version string `json:"version"` Version string `json:"version"`
} }
// https://learn.microsoft.com/en-us/nuget/reference/nuspec
type nuspecPackage struct { type nuspecPackage struct {
Metadata struct { Metadata struct {
ID string `xml:"id"` ID string `xml:"id"`
@ -80,6 +82,7 @@ type nuspecPackage struct {
ProjectURL string `xml:"projectUrl"` ProjectURL string `xml:"projectUrl"`
Description string `xml:"description"` Description string `xml:"description"`
ReleaseNotes string `xml:"releaseNotes"` ReleaseNotes string `xml:"releaseNotes"`
Readme string `xml:"readme"`
PackageTypes struct { PackageTypes struct {
PackageType []struct { PackageType []struct {
Name string `xml:"name,attr"` Name string `xml:"name,attr"`
@ -89,6 +92,11 @@ type nuspecPackage struct {
URL string `xml:"url,attr"` URL string `xml:"url,attr"`
} `xml:"repository"` } `xml:"repository"`
Dependencies struct { Dependencies struct {
Dependency []struct {
ID string `xml:"id,attr"`
Version string `xml:"version,attr"`
Exclude string `xml:"exclude,attr"`
} `xml:"dependency"`
Group []struct { Group []struct {
TargetFramework string `xml:"targetFramework,attr"` TargetFramework string `xml:"targetFramework,attr"`
Dependency []struct { Dependency []struct {
@ -122,14 +130,14 @@ func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) {
} }
defer f.Close() defer f.Close()
return ParseNuspecMetaData(f) return ParseNuspecMetaData(archive, f)
} }
} }
return nil, ErrMissingNuspecFile return nil, ErrMissingNuspecFile
} }
// ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package // ParseNuspecMetaData parses a Nuspec file to retrieve the metadata of a Nuget package
func ParseNuspecMetaData(r io.Reader) (*Package, error) { func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
var p nuspecPackage var p nuspecPackage
if err := xml.NewDecoder(r).Decode(&p); err != nil { if err := xml.NewDecoder(r).Decode(&p); err != nil {
return nil, err return nil, err
@ -166,6 +174,28 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
Dependencies: make(map[string][]Dependency), Dependencies: make(map[string][]Dependency),
} }
if p.Metadata.Readme != "" {
f, err := archive.Open(p.Metadata.Readme)
if err == nil {
buf, _ := io.ReadAll(f)
m.Readme = string(buf)
_ = f.Close()
}
}
if len(p.Metadata.Dependencies.Dependency) > 0 {
deps := make([]Dependency, 0, len(p.Metadata.Dependencies.Dependency))
for _, dep := range p.Metadata.Dependencies.Dependency {
if dep.ID == "" || dep.Version == "" {
continue
}
deps = append(deps, Dependency{
ID: dep.ID,
Version: dep.Version,
})
}
m.Dependencies[""] = deps
}
for _, group := range p.Metadata.Dependencies.Group { for _, group := range p.Metadata.Dependencies.Group {
deps := make([]Dependency, 0, len(group.Dependency)) deps := make([]Dependency, 0, len(group.Dependency))
for _, dep := range group.Dependency { for _, dep := range group.Dependency {

View File

@ -6,7 +6,6 @@ package nuget
import ( import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -19,6 +18,7 @@ const (
projectURL = "https://gitea.io" projectURL = "https://gitea.io"
description = "Package Description" description = "Package Description"
releaseNotes = "Package Release Notes" releaseNotes = "Package Release Notes"
readme = "Readme"
repositoryURL = "https://gitea.io/gitea/gitea" repositoryURL = "https://gitea.io/gitea/gitea"
targetFramework = ".NETStandard2.1" targetFramework = ".NETStandard2.1"
dependencyID = "System.Text.Json" dependencyID = "System.Text.Json"
@ -36,6 +36,7 @@ const nuspecContent = `<?xml version="1.0" encoding="utf-8"?>
<description>` + description + `</description> <description>` + description + `</description>
<releaseNotes>` + releaseNotes + `</releaseNotes> <releaseNotes>` + releaseNotes + `</releaseNotes>
<repository url="` + repositoryURL + `" /> <repository url="` + repositoryURL + `" />
<readme>README.md</readme>
<dependencies> <dependencies>
<group targetFramework="` + targetFramework + `"> <group targetFramework="` + targetFramework + `">
<dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" /> <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" />
@ -60,17 +61,19 @@ const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?>
</package>` </package>`
func TestParsePackageMetaData(t *testing.T) { func TestParsePackageMetaData(t *testing.T) {
createArchive := func(name, content string) []byte { createArchive := func(files map[string]string) []byte {
var buf bytes.Buffer var buf bytes.Buffer
archive := zip.NewWriter(&buf) archive := zip.NewWriter(&buf)
w, _ := archive.Create(name) for name, content := range files {
w.Write([]byte(content)) w, _ := archive.Create(name)
w.Write([]byte(content))
}
archive.Close() archive.Close()
return buf.Bytes() return buf.Bytes()
} }
t.Run("MissingNuspecFile", func(t *testing.T) { t.Run("MissingNuspecFile", func(t *testing.T) {
data := createArchive("dummy.txt", "") data := createArchive(map[string]string{"dummy.txt": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np) assert.Nil(t, np)
@ -78,7 +81,7 @@ func TestParsePackageMetaData(t *testing.T) {
}) })
t.Run("MissingNuspecFileInRoot", func(t *testing.T) { t.Run("MissingNuspecFileInRoot", func(t *testing.T) {
data := createArchive("sub/package.nuspec", "") data := createArchive(map[string]string{"sub/package.nuspec": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np) assert.Nil(t, np)
@ -86,7 +89,7 @@ func TestParsePackageMetaData(t *testing.T) {
}) })
t.Run("InvalidNuspecFile", func(t *testing.T) { t.Run("InvalidNuspecFile", func(t *testing.T) {
data := createArchive("package.nuspec", "") data := createArchive(map[string]string{"package.nuspec": ""})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np) assert.Nil(t, np)
@ -94,10 +97,10 @@ func TestParsePackageMetaData(t *testing.T) {
}) })
t.Run("InvalidPackageId", func(t *testing.T) { t.Run("InvalidPackageId", func(t *testing.T) {
data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?> data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata></metadata> <metadata></metadata>
</package>`) </package>`})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np) assert.Nil(t, np)
@ -105,30 +108,34 @@ func TestParsePackageMetaData(t *testing.T) {
}) })
t.Run("InvalidPackageVersion", func(t *testing.T) { t.Run("InvalidPackageVersion", func(t *testing.T) {
data := createArchive("package.nuspec", `<?xml version="1.0" encoding="utf-8"?> data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata> <metadata>
<id>`+id+`</id> <id>` + id + `</id>
</metadata> </metadata>
</package>`) </package>`})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, np) assert.Nil(t, np)
assert.ErrorIs(t, err, ErrNuspecInvalidVersion) assert.ErrorIs(t, err, ErrNuspecInvalidVersion)
}) })
t.Run("Valid", func(t *testing.T) { t.Run("MissingReadme", func(t *testing.T) {
data := createArchive("package.nuspec", nuspecContent) data := createArchive(map[string]string{"package.nuspec": nuspecContent})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data))) np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, np) assert.NotNil(t, np)
assert.Empty(t, np.Metadata.Readme)
}) })
}
func TestParseNuspecMetaData(t *testing.T) {
t.Run("Dependency Package", func(t *testing.T) { t.Run("Dependency Package", func(t *testing.T) {
np, err := ParseNuspecMetaData(strings.NewReader(nuspecContent)) data := createArchive(map[string]string{
"package.nuspec": nuspecContent,
"README.md": readme,
})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, np) assert.NotNil(t, np)
assert.Equal(t, DependencyPackage, np.PackageType) assert.Equal(t, DependencyPackage, np.PackageType)
@ -139,6 +146,7 @@ func TestParseNuspecMetaData(t *testing.T) {
assert.Equal(t, projectURL, np.Metadata.ProjectURL) assert.Equal(t, projectURL, np.Metadata.ProjectURL)
assert.Equal(t, description, np.Metadata.Description) assert.Equal(t, description, np.Metadata.Description)
assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes) assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
assert.Equal(t, readme, np.Metadata.Readme)
assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL) assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
assert.Len(t, np.Metadata.Dependencies, 1) assert.Len(t, np.Metadata.Dependencies, 1)
assert.Contains(t, np.Metadata.Dependencies, targetFramework) assert.Contains(t, np.Metadata.Dependencies, targetFramework)
@ -148,13 +156,15 @@ func TestParseNuspecMetaData(t *testing.T) {
assert.Equal(t, dependencyVersion, deps[0].Version) assert.Equal(t, dependencyVersion, deps[0].Version)
t.Run("NormalizedVersion", func(t *testing.T) { t.Run("NormalizedVersion", func(t *testing.T) {
np, err := ParseNuspecMetaData(strings.NewReader(`<?xml version="1.0" encoding="utf-8"?> data := createArchive(map[string]string{"package.nuspec": `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata> <metadata>
<id>test</id> <id>test</id>
<version>1.04.5.2.5-rc.1+metadata</version> <version>1.04.5.2.5-rc.1+metadata</version>
</metadata> </metadata>
</package>`)) </package>`})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, np) assert.NotNil(t, np)
assert.Equal(t, "1.4.5.2-rc.1", np.Version) assert.Equal(t, "1.4.5.2-rc.1", np.Version)
@ -162,7 +172,9 @@ func TestParseNuspecMetaData(t *testing.T) {
}) })
t.Run("Symbols Package", func(t *testing.T) { t.Run("Symbols Package", func(t *testing.T) {
np, err := ParseNuspecMetaData(strings.NewReader(symbolsNuspecContent)) data := createArchive(map[string]string{"package.nuspec": symbolsNuspecContent})
np, err := ParsePackageMetaData(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, np) assert.NotNil(t, np)
assert.Equal(t, SymbolsPackage, np.PackageType) assert.Equal(t, SymbolsPackage, np.PackageType)

View File

@ -16,12 +16,11 @@
</div> </div>
</div> </div>
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes}} {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4> <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
<div class="ui attached segment"> {{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Description}}</div>{{end}}
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}} {{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}}
{{if .PackageDescriptor.Metadata.ReleaseNotes}}{{.PackageDescriptor.Metadata.ReleaseNotes}}{{end}} {{if .PackageDescriptor.Metadata.ReleaseNotes}}<div class="ui attached segment">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.ReleaseNotes}}</div>{{end}}
</div>
{{end}} {{end}}
{{if .PackageDescriptor.Metadata.Dependencies}} {{if .PackageDescriptor.Metadata.Dependencies}}