mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-18 23:47:50 +00:00
[v16.0/forgejo] fix: Update swift package registry to use the metadata schema from the official Swift docs (#13208)
**Backport:** #12200
(cherry picked from commit 939e5e8b0a)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12200
Reviewed-by: limiting-factor <limiting-factor@noreply.codeberg.org>
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Benedek Kozma <cyberbeni@gmail.com>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/13208
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
This commit is contained in:
parent
fb0ab1e5fa
commit
9d2c7d9e96
7 changed files with 87 additions and 150 deletions
|
|
@ -197,7 +197,7 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
|||
case TypeRubyGems:
|
||||
metadata = &rubygems.Metadata{}
|
||||
case TypeSwift:
|
||||
metadata = &swift.Metadata{}
|
||||
metadata = &swift.Package{}
|
||||
case TypeVagrant:
|
||||
metadata = &vagrant.Metadata{}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"forgejo.org/modules/json"
|
||||
"forgejo.org/modules/util"
|
||||
"forgejo.org/modules/validation"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
|
@ -37,18 +36,8 @@ const (
|
|||
|
||||
// Package represents a Swift package
|
||||
type Package struct {
|
||||
RepositoryURLs []string
|
||||
Metadata *Metadata
|
||||
}
|
||||
|
||||
// Metadata represents the metadata of a Swift package
|
||||
type Metadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Author Person `json:"author"`
|
||||
Manifests map[string]*Manifest `json:"manifests,omitempty"`
|
||||
Manifests map[string]*Manifest
|
||||
Metadata *PackageRelease
|
||||
}
|
||||
|
||||
// Manifest represents a Package.swift file
|
||||
|
|
@ -57,54 +46,31 @@ type Manifest struct {
|
|||
ToolsVersion string `json:"tools_version,omitempty"`
|
||||
}
|
||||
|
||||
// https://schema.org/SoftwareSourceCode
|
||||
type SoftwareSourceCode struct {
|
||||
Context []string `json:"@context"`
|
||||
Type string `json:"@type"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"`
|
||||
CodeRepository string `json:"codeRepository,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
Author Person `json:"author"`
|
||||
ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"`
|
||||
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
|
||||
// https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/registryserverspecification/#PackageRelease-type
|
||||
type PackageRelease struct {
|
||||
Author *Author `json:"author,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
LicenseURL string `json:"licenseURL,omitempty"`
|
||||
OriginalPublicationTime string `json:"originalPublicationTime,omitempty"`
|
||||
ReadmeURL string `json:"readmeURL,omitempty"`
|
||||
RepositoryURLs []string `json:"repositoryURLs,omitempty"`
|
||||
}
|
||||
|
||||
// https://schema.org/ProgrammingLanguage
|
||||
type ProgrammingLanguage struct {
|
||||
Type string `json:"@type"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
// https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/registryserverspecification/#Author-type
|
||||
type Author struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// https://schema.org/Person
|
||||
type Person struct {
|
||||
Type string `json:"@type,omitempty"`
|
||||
GivenName string `json:"givenName,omitempty"`
|
||||
MiddleName string `json:"middleName,omitempty"`
|
||||
FamilyName string `json:"familyName,omitempty"`
|
||||
}
|
||||
|
||||
func (p Person) String() string {
|
||||
var sb strings.Builder
|
||||
if p.GivenName != "" {
|
||||
sb.WriteString(p.GivenName)
|
||||
}
|
||||
if p.MiddleName != "" {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
sb.WriteString(p.MiddleName)
|
||||
}
|
||||
if p.FamilyName != "" {
|
||||
if sb.Len() > 0 {
|
||||
sb.WriteRune(' ')
|
||||
}
|
||||
sb.WriteString(p.FamilyName)
|
||||
}
|
||||
return sb.String()
|
||||
// https://docs.swift.org/swiftpm/documentation/packagemanagerdocs/registryserverspecification/#Organization-type
|
||||
type Organization struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// ParsePackage parses the Swift package upload
|
||||
|
|
@ -115,9 +81,8 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
|
|||
}
|
||||
|
||||
p := &Package{
|
||||
Metadata: &Metadata{
|
||||
Manifests: make(map[string]*Manifest),
|
||||
},
|
||||
Manifests: make(map[string]*Manifest),
|
||||
Metadata: &PackageRelease{},
|
||||
}
|
||||
|
||||
for _, file := range zr.File {
|
||||
|
|
@ -168,34 +133,27 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) {
|
|||
manifest.ToolsVersion = TrimmedVersionString(v)
|
||||
}
|
||||
|
||||
p.Metadata.Manifests[swiftVersion] = manifest
|
||||
p.Manifests[swiftVersion] = manifest
|
||||
}
|
||||
|
||||
if _, found := p.Metadata.Manifests[""]; !found {
|
||||
if _, found := p.Manifests[""]; !found {
|
||||
return nil, ErrMissingManifestFile
|
||||
}
|
||||
|
||||
if mr != nil {
|
||||
var ssc *SoftwareSourceCode
|
||||
if err := json.NewDecoder(mr).Decode(&ssc); err != nil {
|
||||
var pr *PackageRelease
|
||||
if err := json.NewDecoder(mr).Decode(&pr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.Metadata.Description = ssc.Description
|
||||
p.Metadata.Keywords = ssc.Keywords
|
||||
p.Metadata.License = ssc.License
|
||||
p.Metadata.Author = Person{
|
||||
GivenName: ssc.Author.GivenName,
|
||||
MiddleName: ssc.Author.MiddleName,
|
||||
FamilyName: ssc.Author.FamilyName,
|
||||
if pr.Author != nil {
|
||||
if pr.Author.Name == "" {
|
||||
return nil, util.NewInvalidArgumentErrorf("if metadata.author exists, its name can't be empty")
|
||||
}
|
||||
if pr.Author.Organization != nil && pr.Author.Organization.Name == "" {
|
||||
return nil, util.NewInvalidArgumentErrorf("if metadata.author.organization exists, its name can't be empty")
|
||||
}
|
||||
}
|
||||
|
||||
p.Metadata.RepositoryURL = ssc.CodeRepository
|
||||
if !validation.IsValidURL(p.Metadata.RepositoryURL) {
|
||||
p.Metadata.RepositoryURL = ""
|
||||
}
|
||||
|
||||
p.RepositoryURLs = ssc.RepositoryURLs
|
||||
p.Metadata = pr
|
||||
}
|
||||
|
||||
return p, nil
|
||||
|
|
|
|||
|
|
@ -9,18 +9,19 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
packageName = "gitea"
|
||||
packageVersion = "1.0.1"
|
||||
packageDescription = "Package Description"
|
||||
packageRepositoryURL = "https://gitea.io/gitea/gitea"
|
||||
packageAuthor = "KN4CK3R"
|
||||
packageLicense = "MIT"
|
||||
packageEmail = "example@example.com"
|
||||
packageLicense = "https://opensource.org/license/mit"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
|
|
@ -67,12 +68,12 @@ func TestParsePackage(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, p.Metadata)
|
||||
assert.Empty(t, p.RepositoryURLs)
|
||||
assert.Len(t, p.Metadata.Manifests, 2)
|
||||
m := p.Metadata.Manifests[""]
|
||||
assert.Empty(t, p.Metadata.RepositoryURLs)
|
||||
assert.Len(t, p.Manifests, 2)
|
||||
m := p.Manifests[""]
|
||||
assert.Equal(t, "5.7", m.ToolsVersion)
|
||||
assert.Equal(t, content1, m.Content)
|
||||
m = p.Metadata.Manifests["5.5"]
|
||||
m = p.Manifests["5.5"]
|
||||
assert.Equal(t, "5.6", m.ToolsVersion)
|
||||
assert.Equal(t, content2, m.Content)
|
||||
})
|
||||
|
|
@ -85,22 +86,34 @@ func TestParsePackage(t *testing.T) {
|
|||
p, err := ParsePackage(
|
||||
data,
|
||||
data.Size(),
|
||||
strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||
strings.NewReader(`{"description":"`+packageDescription+`","licenseURL":"`+packageLicense+`","author":{"name":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||
)
|
||||
assert.NotNil(t, p)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, p.Metadata)
|
||||
assert.Len(t, p.Metadata.Manifests, 1)
|
||||
m := p.Metadata.Manifests[""]
|
||||
assert.Len(t, p.Manifests, 1)
|
||||
m := p.Manifests[""]
|
||||
assert.Equal(t, "5.7", m.ToolsVersion)
|
||||
|
||||
assert.Equal(t, packageDescription, p.Metadata.Description)
|
||||
assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords)
|
||||
assert.Equal(t, packageLicense, p.Metadata.License)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName)
|
||||
assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL)
|
||||
assert.ElementsMatch(t, []string{packageRepositoryURL}, p.RepositoryURLs)
|
||||
assert.Equal(t, packageLicense, p.Metadata.LicenseURL)
|
||||
assert.Equal(t, packageAuthor, p.Metadata.Author.Name)
|
||||
assert.ElementsMatch(t, []string{packageRepositoryURL}, p.Metadata.RepositoryURLs)
|
||||
})
|
||||
|
||||
t.Run("WithInvalidMetadata", func(t *testing.T) {
|
||||
data := createArchive(map[string][]byte{
|
||||
"Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"),
|
||||
})
|
||||
|
||||
p, err := ParsePackage(
|
||||
data,
|
||||
data.Size(),
|
||||
strings.NewReader(`{"description":"`+packageDescription+`","licenseURL":"`+packageLicense+`","author":{"email":"`+packageEmail+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`),
|
||||
)
|
||||
assert.Nil(t, p)
|
||||
require.ErrorIs(t, err, util.ErrInvalidArgument)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -174,10 +174,10 @@ type Resource struct {
|
|||
}
|
||||
|
||||
type PackageVersionMetadataResponse struct {
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Resources []Resource `json:"resources"`
|
||||
Metadata *swift_module.SoftwareSourceCode `json:"metadata"`
|
||||
ID string `json:"id"`
|
||||
Version string `json:"version"`
|
||||
Resources []Resource `json:"resources"`
|
||||
Metadata *swift_module.PackageRelease `json:"metadata"`
|
||||
}
|
||||
|
||||
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-2
|
||||
|
|
@ -200,7 +200,7 @@ func PackageVersionMetadata(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
||||
metadata := pd.Metadata.(*swift_module.Package)
|
||||
|
||||
setResponseHeaders(ctx.Resp, &headers{})
|
||||
|
||||
|
|
@ -214,27 +214,7 @@ func PackageVersionMetadata(ctx *context.Context) {
|
|||
Checksum: pd.Files[0].Blob.HashSHA256,
|
||||
},
|
||||
},
|
||||
Metadata: &swift_module.SoftwareSourceCode{
|
||||
Context: []string{"http://schema.org/"},
|
||||
Type: "SoftwareSourceCode",
|
||||
Name: pd.PackageProperties.GetByName(swift_module.PropertyName),
|
||||
Version: pd.Version.Version,
|
||||
Description: metadata.Description,
|
||||
Keywords: metadata.Keywords,
|
||||
CodeRepository: metadata.RepositoryURL,
|
||||
License: metadata.License,
|
||||
ProgrammingLanguage: swift_module.ProgrammingLanguage{
|
||||
Type: "ComputerLanguage",
|
||||
Name: "Swift",
|
||||
URL: "https://swift.org",
|
||||
},
|
||||
Author: swift_module.Person{
|
||||
Type: "Person",
|
||||
GivenName: metadata.Author.GivenName,
|
||||
MiddleName: metadata.Author.MiddleName,
|
||||
FamilyName: metadata.Author.FamilyName,
|
||||
},
|
||||
},
|
||||
Metadata: metadata.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +247,7 @@ func DownloadManifest(ctx *context.Context) {
|
|||
swiftVersion = swift_module.TrimmedVersionString(v)
|
||||
}
|
||||
}
|
||||
m, ok := pd.Metadata.(*swift_module.Metadata).Manifests[swiftVersion]
|
||||
m, ok := pd.Metadata.(*swift_module.Package).Manifests[swiftVersion]
|
||||
if !ok {
|
||||
setResponseHeaders(ctx.Resp, &headers{
|
||||
Status: http.StatusSeeOther,
|
||||
|
|
@ -370,7 +350,7 @@ func UploadPackageFile(ctx *context.Context) {
|
|||
},
|
||||
SemverCompatible: true,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: pck.Metadata,
|
||||
Metadata: pck,
|
||||
PackageProperties: map[string]string{
|
||||
swift_module.PropertyScope: packageScope,
|
||||
swift_module.PropertyName: packageName,
|
||||
|
|
@ -397,7 +377,7 @@ func UploadPackageFile(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
for _, url := range pck.RepositoryURLs {
|
||||
for _, url := range pck.Metadata.RepositoryURLs {
|
||||
_, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, swift_module.PropertyRepositoryURL, url)
|
||||
if err != nil {
|
||||
log.Error("InsertProperty failed: %v", err)
|
||||
|
|
|
|||
|
|
@ -22,19 +22,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{if .PackageDescriptor.Metadata.Description}}
|
||||
{{if .PackageDescriptor.Metadata.Metadata.Description}}
|
||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .PackageDescriptor.Metadata.Keywords}}
|
||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.keywords"}}</h4>
|
||||
<div class="ui attached segment">
|
||||
{{range .PackageDescriptor.Metadata.Keywords}}
|
||||
{{.}}
|
||||
{{end}}
|
||||
{{.PackageDescriptor.Metadata.Metadata.Description}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{{if eq .PackageDescriptor.Package.Type "swift"}}
|
||||
{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Metadata.Author.Name}}</div>{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ func TestPackageSwift(t *testing.T) {
|
|||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
`{"description":"`+packageDescription+`","author":{"name":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
|
||||
|
|
@ -159,9 +159,9 @@ func TestPackageSwift(t *testing.T) {
|
|||
assert.NotNil(t, pd.SemVer)
|
||||
assert.Equal(t, packageID, pd.Package.Name)
|
||||
assert.Equal(t, packageVersion, pd.Version.Version)
|
||||
assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
||||
assert.Equal(t, packageDescription, metadata.Description)
|
||||
assert.IsType(t, &swift_module.Package{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*swift_module.Package)
|
||||
assert.Equal(t, packageDescription, metadata.Metadata.Description)
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
|
|
@ -235,7 +235,7 @@ func TestPackageSwift(t *testing.T) {
|
|||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
`{"description":"`+packageDescription+`","author":{"name":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeSwift)
|
||||
|
|
@ -247,9 +247,9 @@ func TestPackageSwift(t *testing.T) {
|
|||
assert.NotNil(t, pd.SemVer)
|
||||
assert.Equal(t, packageID, pd.Package.Name)
|
||||
assert.Equal(t, packageVersion2, pd.Version.Version)
|
||||
assert.IsType(t, &swift_module.Metadata{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*swift_module.Metadata)
|
||||
assert.Equal(t, packageDescription, metadata.Description)
|
||||
assert.IsType(t, &swift_module.Package{}, pd.Metadata)
|
||||
metadata := pd.Metadata.(*swift_module.Package)
|
||||
assert.Equal(t, packageDescription, metadata.Metadata.Description)
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
|
|
@ -350,12 +350,8 @@ func TestPackageSwift(t *testing.T) {
|
|||
assert.Equal(t, "source-archive", result.Resources[0].Name)
|
||||
assert.Equal(t, "application/zip", result.Resources[0].Type)
|
||||
assert.Equal(t, pd.Files[0].Blob.HashSHA256, result.Resources[0].Checksum)
|
||||
assert.Equal(t, "SoftwareSourceCode", result.Metadata.Type)
|
||||
assert.Equal(t, packageName, result.Metadata.Name)
|
||||
assert.Equal(t, packageVersion, result.Metadata.Version)
|
||||
assert.Equal(t, packageDescription, result.Metadata.Description)
|
||||
assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
|
||||
assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
|
||||
assert.Equal(t, packageAuthor, result.Metadata.Author.Name)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)).
|
||||
AddBasicAuth(user.Name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue