aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorScion <Filiecs2@gmail.com>2025-07-07 03:43:58 -0700
committerGitHub <noreply@github.com>2025-07-07 10:43:58 +0000
commitf88800d54505f30dbae089d446e292f176222d99 (patch)
tree0061cb770a4c0e68d034ee8490ca30b949b3131f
parentddfa2e4a3ef18cdef0f66d69af2c581ffe095f9b (diff)
downloadgitea-f88800d54505f30dbae089d446e292f176222d99.tar.gz
gitea-f88800d54505f30dbae089d446e292f176222d99.zip
Improve NuGet API Parity (#21291) (#34940)
Fixes #21291, allowing icons and other missing attributes to appear for NuGet packages from inside Visual Studio like they do with GitHub Nuget packages. Adds additional NuGet package information, particularly `IconURL`, to bring the Gitea NuGet API more in-line with GitHub's NuGet API. ref: https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource
-rw-r--r--models/packages/package_version.go8
-rw-r--r--modules/packages/nuget/metadata.go3
-rw-r--r--routers/api/packages/nuget/api_v3.go142
-rw-r--r--tests/integration/api_packages_nuget_test.go88
4 files changed, 183 insertions, 58 deletions
diff --git a/models/packages/package_version.go b/models/packages/package_version.go
index 5672e0efbf..0a478c0323 100644
--- a/models/packages/package_version.go
+++ b/models/packages/package_version.go
@@ -37,6 +37,14 @@ type PackageVersion struct {
DownloadCount int64 `xorm:"NOT NULL DEFAULT 0"`
}
+// IsPrerelease checks if the version is a prerelease version according to semantic versioning
+func (pv *PackageVersion) IsPrerelease() bool {
+ if pv == nil || pv.Version == "" {
+ return false
+ }
+ return strings.Contains(pv.Version, "-")
+}
+
// GetOrInsertVersion inserts a version. If the same version exist already ErrDuplicatePackageVersion is returned
func GetOrInsertVersion(ctx context.Context, pv *PackageVersion) (*PackageVersion, error) {
e := db.GetEngine(ctx)
diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index a122590bf1..513b4dd2b9 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -71,6 +71,7 @@ type Metadata struct {
ReleaseNotes string `json:"release_notes,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
RequireLicenseAcceptance bool `json:"require_license_acceptance"`
+ Summary string `json:"summary,omitempty"`
Tags string `json:"tags,omitempty"`
Title string `json:"title,omitempty"`
@@ -105,6 +106,7 @@ type nuspecPackage struct {
Readme string `xml:"readme"`
ReleaseNotes string `xml:"releaseNotes"`
RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
+ Summary string `xml:"summary"`
Tags string `xml:"tags"`
Title string `xml:"title"`
@@ -204,6 +206,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
ReleaseNotes: p.Metadata.ReleaseNotes,
RepositoryURL: p.Metadata.Repository.URL,
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
+ Summary: p.Metadata.Summary,
Tags: p.Metadata.Tags,
Title: p.Metadata.Title,
diff --git a/routers/api/packages/nuget/api_v3.go b/routers/api/packages/nuget/api_v3.go
index 2fe25dc0f8..3262f2d9af 100644
--- a/routers/api/packages/nuget/api_v3.go
+++ b/routers/api/packages/nuget/api_v3.go
@@ -53,15 +53,23 @@ type RegistrationIndexPageItem struct {
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
type CatalogEntry struct {
CatalogLeafURL string `json:"@id"`
- PackageContentURL string `json:"packageContent"`
+ Authors string `json:"authors"`
+ Copyright string `json:"copyright"`
+ DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
+ Description string `json:"description"`
+ IconURL string `json:"iconUrl"`
ID string `json:"id"`
+ IsPrerelease bool `json:"isPrerelease"`
+ Language string `json:"language"`
+ LicenseURL string `json:"licenseUrl"`
+ PackageContentURL string `json:"packageContent"`
+ ProjectURL string `json:"projectUrl"`
+ RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
+ Summary string `json:"summary"`
+ Tags string `json:"tags"`
Version string `json:"version"`
- Description string `json:"description"`
ReleaseNotes string `json:"releaseNotes"`
- Authors string `json:"authors"`
- RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
- ProjectURL string `json:"projectURL"`
- DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
+ Published time.Time `json:"published"`
}
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
@@ -109,15 +117,24 @@ func createRegistrationIndexPageItem(l *linkBuilder, pd *packages_model.PackageD
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
CatalogEntry: &CatalogEntry{
- CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
- PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
- ID: pd.Package.Name,
- Version: pd.Version.Version,
- Description: metadata.Description,
- ReleaseNotes: metadata.ReleaseNotes,
- Authors: metadata.Authors,
- ProjectURL: metadata.ProjectURL,
- DependencyGroups: createDependencyGroups(pd),
+ CatalogLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
+ Authors: metadata.Authors,
+ Copyright: metadata.Copyright,
+ DependencyGroups: createDependencyGroups(pd),
+ Description: metadata.Description,
+ IconURL: metadata.IconURL,
+ ID: pd.Package.Name,
+ IsPrerelease: pd.Version.IsPrerelease(),
+ Language: metadata.Language,
+ LicenseURL: metadata.LicenseURL,
+ PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ ProjectURL: metadata.ProjectURL,
+ RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
+ Summary: metadata.Summary,
+ Tags: metadata.Tags,
+ Version: pd.Version.Version,
+ ReleaseNotes: metadata.ReleaseNotes,
+ Published: pd.Version.CreatedUnix.AsLocalTime(),
},
}
}
@@ -145,22 +162,42 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe
// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
type RegistrationLeafResponse struct {
- RegistrationLeafURL string `json:"@id"`
- Type []string `json:"@type"`
- Listed bool `json:"listed"`
- PackageContentURL string `json:"packageContent"`
- Published time.Time `json:"published"`
- RegistrationIndexURL string `json:"registration"`
+ RegistrationLeafURL string `json:"@id"`
+ Type []string `json:"@type"`
+ PackageContentURL string `json:"packageContent"`
+ RegistrationIndexURL string `json:"registration"`
+ CatalogEntry CatalogEntry `json:"catalogEntry"`
}
func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *RegistrationLeafResponse {
+ registrationLeafURL := l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version)
+ packageDownloadURL := l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version)
+ metadata := pd.Metadata.(*nuget_module.Metadata)
return &RegistrationLeafResponse{
- Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
- Listed: true,
- Published: pd.Version.CreatedUnix.AsLocalTime(),
- RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
- PackageContentURL: l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version),
+ RegistrationLeafURL: registrationLeafURL,
RegistrationIndexURL: l.GetRegistrationIndexURL(pd.Package.Name),
+ PackageContentURL: packageDownloadURL,
+ Type: []string{"Package", "http://schema.nuget.org/catalog#Permalink"},
+ CatalogEntry: CatalogEntry{
+ CatalogLeafURL: registrationLeafURL,
+ Authors: metadata.Authors,
+ Copyright: metadata.Copyright,
+ DependencyGroups: createDependencyGroups(pd),
+ Description: metadata.Description,
+ IconURL: metadata.IconURL,
+ ID: pd.Package.Name,
+ IsPrerelease: pd.Version.IsPrerelease(),
+ Language: metadata.Language,
+ LicenseURL: metadata.LicenseURL,
+ PackageContentURL: packageDownloadURL,
+ ProjectURL: metadata.ProjectURL,
+ RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
+ Summary: metadata.Summary,
+ Tags: metadata.Tags,
+ Version: pd.Version.Version,
+ ReleaseNotes: metadata.ReleaseNotes,
+ Published: pd.Version.CreatedUnix.AsLocalTime(),
+ },
}
}
@@ -188,13 +225,24 @@ type SearchResultResponse struct {
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
type SearchResult struct {
- ID string `json:"id"`
- Version string `json:"version"`
- Versions []*SearchResultVersion `json:"versions"`
- Description string `json:"description"`
- Authors string `json:"authors"`
- ProjectURL string `json:"projectURL"`
- RegistrationIndexURL string `json:"registration"`
+ Authors string `json:"authors"`
+ Copyright string `json:"copyright"`
+ DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
+ Description string `json:"description"`
+ IconURL string `json:"iconUrl"`
+ ID string `json:"id"`
+ IsPrerelease bool `json:"isPrerelease"`
+ Language string `json:"language"`
+ LicenseURL string `json:"licenseUrl"`
+ ProjectURL string `json:"projectUrl"`
+ RequireLicenseAcceptance bool `json:"requireLicenseAcceptance"`
+ Summary string `json:"summary"`
+ Tags string `json:"tags"`
+ Title string `json:"title"`
+ TotalDownloads int64 `json:"totalDownloads"`
+ Version string `json:"version"`
+ Versions []*SearchResultVersion `json:"versions"`
+ RegistrationIndexURL string `json:"registration"`
}
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
@@ -230,11 +278,12 @@ func createSearchResultResponse(l *linkBuilder, totalHits int64, pds []*packages
func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor) *SearchResult {
latest := pds[0]
versions := make([]*SearchResultVersion, 0, len(pds))
+ totalDownloads := int64(0)
for _, pd := range pds {
if latest.SemVer.LessThan(pd.SemVer) {
latest = pd
}
-
+ totalDownloads += pd.Version.DownloadCount
versions = append(versions, &SearchResultVersion{
RegistrationLeafURL: l.GetRegistrationLeafURL(pd.Package.Name, pd.Version.Version),
Version: pd.Version.Version,
@@ -244,12 +293,23 @@ func createSearchResult(l *linkBuilder, pds []*packages_model.PackageDescriptor)
metadata := latest.Metadata.(*nuget_module.Metadata)
return &SearchResult{
- ID: latest.Package.Name,
- Version: latest.Version.Version,
- Versions: versions,
- Description: metadata.Description,
- Authors: metadata.Authors,
- ProjectURL: metadata.ProjectURL,
- RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
+ Authors: metadata.Authors,
+ Copyright: metadata.Copyright,
+ Description: metadata.Description,
+ DependencyGroups: createDependencyGroups(latest),
+ IconURL: metadata.IconURL,
+ ID: latest.Package.Name,
+ IsPrerelease: latest.Version.IsPrerelease(),
+ Language: metadata.Language,
+ LicenseURL: metadata.LicenseURL,
+ ProjectURL: metadata.ProjectURL,
+ RequireLicenseAcceptance: metadata.RequireLicenseAcceptance,
+ Summary: metadata.Summary,
+ Tags: metadata.Tags,
+ Title: metadata.Title,
+ TotalDownloads: totalDownloads,
+ Version: latest.Version.Version,
+ Versions: versions,
+ RegistrationIndexURL: l.GetRegistrationIndexURL(latest.Package.Name),
}
}
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
index 65b1b9845a..529b540062 100644
--- a/tests/integration/api_packages_nuget_test.go
+++ b/tests/integration/api_packages_nuget_test.go
@@ -14,6 +14,7 @@ import (
"net/http/httptest"
neturl "net/url"
"strconv"
+ "strings"
"testing"
"time"
@@ -100,6 +101,7 @@ func TestPackageNuGet(t *testing.T) {
packageVersion := "1.0.3"
packageAuthors := "KN4CK3R"
packageDescription := "Gitea Test Package"
+ isPrerelease := strings.Contains(packageVersion, "-")
symbolFilename := "test.pdb"
symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
@@ -112,11 +114,17 @@ func TestPackageNuGet(t *testing.T) {
packageOwners := "Package Owners"
packageProjectURL := "https://gitea.io"
packageReleaseNotes := "Package Release Notes"
+ summary := "This is a test package."
packageTags := "tag_1 tag_2 tag_3"
packageTitle := "Package Title"
packageDevelopmentDependency := true
packageRequireLicenseAcceptance := true
+ dependencyCount := 1
+ dependencyTargetFramework := ".NETStandard2.0"
+ dependencyID := "Microsoft.CSharp"
+ dependencyVersion := "4.5.0"
+
createNuspec := func(id, version string) string {
return `<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
@@ -133,12 +141,13 @@ func TestPackageNuGet(t *testing.T) {
<projectUrl>` + packageProjectURL + `</projectUrl>
<releaseNotes>` + packageReleaseNotes + `</releaseNotes>
<requireLicenseAcceptance>true</requireLicenseAcceptance>
+ <summary>` + summary + `</summary>
<tags>` + packageTags + `</tags>
<title>` + packageTitle + `</title>
<version>` + version + `</version>
<dependencies>
- <group targetFramework=".NETStandard2.0">
- <dependency id="Microsoft.CSharp" version="4.5.0" />
+ <group targetFramework="` + dependencyTargetFramework + `">
+ <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" />
</group>
</dependencies>
</metadata>
@@ -428,7 +437,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
assert.NoError(t, err)
- assert.Equal(t, int64(610), pb.Size)
+ assert.Equal(t, int64(633), pb.Size)
case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
assert.False(t, pf.IsLead)
@@ -440,7 +449,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
assert.NoError(t, err)
- assert.Equal(t, int64(996), pb.Size)
+ assert.Equal(t, int64(1043), pb.Size)
case symbolFilename:
assert.False(t, pf.IsLead)
@@ -747,17 +756,39 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
assert.Equal(t, indexURL, result.RegistrationIndexURL)
assert.Equal(t, 1, result.Count)
assert.Len(t, result.Pages, 1)
- assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL)
- assert.Equal(t, packageVersion, result.Pages[0].Lower)
- assert.Equal(t, packageVersion, result.Pages[0].Upper)
- assert.Equal(t, 1, result.Pages[0].Count)
- assert.Len(t, result.Pages[0].Items, 1)
- assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID)
- assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version)
- assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors)
- assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description)
- assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL)
- assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL)
+
+ page := result.Pages[0]
+ assert.Equal(t, indexURL, page.RegistrationPageURL)
+ assert.Equal(t, packageVersion, page.Lower)
+ assert.Equal(t, packageVersion, page.Upper)
+ assert.Equal(t, 1, page.Count)
+ assert.Len(t, page.Items, 1)
+
+ item := page.Items[0]
+ assert.Equal(t, packageName, item.CatalogEntry.ID)
+ assert.Equal(t, packageVersion, item.CatalogEntry.Version)
+ assert.Equal(t, packageAuthors, item.CatalogEntry.Authors)
+ assert.Equal(t, packageDescription, item.CatalogEntry.Description)
+ assert.Equal(t, leafURL, item.CatalogEntry.CatalogLeafURL)
+ assert.Equal(t, contentURL, item.CatalogEntry.PackageContentURL)
+ assert.Equal(t, packageIconURL, item.CatalogEntry.IconURL)
+ assert.Equal(t, packageLanguage, item.CatalogEntry.Language)
+ assert.Equal(t, packageLicenseURL, item.CatalogEntry.LicenseURL)
+ assert.Equal(t, packageProjectURL, item.CatalogEntry.ProjectURL)
+ assert.Equal(t, packageReleaseNotes, item.CatalogEntry.ReleaseNotes)
+ assert.Equal(t, packageRequireLicenseAcceptance, item.CatalogEntry.RequireLicenseAcceptance)
+ assert.Equal(t, packageTags, item.CatalogEntry.Tags)
+ assert.Equal(t, summary, item.CatalogEntry.Summary)
+ assert.Equal(t, isPrerelease, item.CatalogEntry.IsPrerelease)
+ assert.Len(t, item.CatalogEntry.DependencyGroups, dependencyCount)
+
+ dependencyGroup := item.CatalogEntry.DependencyGroups[0]
+ assert.Equal(t, dependencyTargetFramework, dependencyGroup.TargetFramework)
+ assert.Len(t, dependencyGroup.Dependencies, dependencyCount)
+
+ dependency := dependencyGroup.Dependencies[0]
+ assert.Equal(t, dependencyID, dependency.ID)
+ assert.Equal(t, dependencyVersion, dependency.Range)
})
t.Run("RegistrationLeaf", func(t *testing.T) {
@@ -789,7 +820,8 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
assert.Equal(t, packageTags, result.Properties.Tags)
assert.Equal(t, packageTitle, result.Properties.Title)
- assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies)
+ packageVersion := strings.Join([]string{dependencyID, dependencyVersion, dependencyTargetFramework}, ":")
+ assert.Equal(t, packageVersion, result.Properties.Dependencies)
})
t.Run("v3", func(t *testing.T) {
@@ -803,8 +835,30 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
DecodeJSON(t, resp, &result)
assert.Equal(t, leafURL, result.RegistrationLeafURL)
- assert.Equal(t, contentURL, result.PackageContentURL)
assert.Equal(t, indexURL, result.RegistrationIndexURL)
+ assert.Equal(t, packageAuthors, result.CatalogEntry.Authors)
+ assert.Equal(t, packageCopyright, result.CatalogEntry.Copyright)
+
+ dependencyGroup := result.CatalogEntry.DependencyGroups[0]
+ assert.Equal(t, dependencyTargetFramework, dependencyGroup.TargetFramework)
+ assert.Len(t, dependencyGroup.Dependencies, dependencyCount)
+
+ dependency := dependencyGroup.Dependencies[0]
+ assert.Equal(t, dependencyID, dependency.ID)
+ assert.Equal(t, dependencyVersion, dependency.Range)
+
+ assert.Equal(t, packageDescription, result.CatalogEntry.Description)
+ assert.Equal(t, packageID, result.CatalogEntry.ID)
+ assert.Equal(t, packageIconURL, result.CatalogEntry.IconURL)
+ assert.Equal(t, isPrerelease, result.CatalogEntry.IsPrerelease)
+ assert.Equal(t, packageLanguage, result.CatalogEntry.Language)
+ assert.Equal(t, packageLicenseURL, result.CatalogEntry.LicenseURL)
+ assert.Equal(t, contentURL, result.PackageContentURL)
+ assert.Equal(t, packageProjectURL, result.CatalogEntry.ProjectURL)
+ assert.Equal(t, packageRequireLicenseAcceptance, result.CatalogEntry.RequireLicenseAcceptance)
+ assert.Equal(t, summary, result.CatalogEntry.Summary)
+ assert.Equal(t, packageTags, result.CatalogEntry.Tags)
+ assert.Equal(t, packageVersion, result.CatalogEntry.Version)
})
})
})