diff options
Diffstat (limited to 'routers')
52 files changed, 440 insertions, 368 deletions
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index ae4ea7ea87..41e89ae567 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -701,18 +701,18 @@ func ContainerRoutes() *web.Router { r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { r.PathGroup("/*", func(g *web.RouterPathGroup) { - g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) - g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList) + g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) + g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { switch ctx.Req.Method { case http.MethodGet: - container.GetUploadBlob(ctx) + container.GetBlobsUpload(ctx) case http.MethodPatch: - container.UploadBlob(ctx) + container.PatchBlobsUpload(ctx) case http.MethodPut: - container.EndUploadBlob(ctx) + container.PutBlobsUpload(ctx) default: /* DELETE */ - container.CancelUploadBlob(ctx) + container.DeleteBlobsUpload(ctx) } }) g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) @@ -721,7 +721,7 @@ func ContainerRoutes() *web.Router { g.MatchPath("HEAD", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.HeadManifest) g.MatchPath("GET", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.GetManifest) - g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.PutManifest) g.MatchPath("DELETE", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) }) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 710c614c6e..57cb83404f 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -95,10 +95,7 @@ type SearchResultMeta struct { // https://doc.rust-lang.org/cargo/reference/registries.html#search func SearchPackages(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) perPage := ctx.FormInt("per_page") paginator := db.ListOptions{ Page: page, diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index c6c14e5cf4..3713805579 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -53,10 +53,7 @@ func ServiceIndex(ctx *context.Context) { // SearchPackages searches packages, only "q" is supported // https://packagist.org/apidoc#search-packages func SearchPackages(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) perPage := ctx.FormInt("per_page") paginator := db.ListOptions{ Page: page, diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4a2320ab76..2ea9b3839c 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -20,6 +20,8 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" + + "github.com/opencontainers/go-digest" ) // saveAsPackageBlob creates a package blob from an upload @@ -128,8 +130,8 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI pv := &packages_model.PackageVersion{ PackageID: p.ID, CreatorID: pi.Owner.ID, - Version: container_model.UploadVersion, - LowerVersion: container_model.UploadVersion, + Version: container_module.UploadVersion, + LowerVersion: container_module.UploadVersion, IsInternal: true, MetadataJSON: "null", } @@ -175,7 +177,7 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p return nil } -func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error { +func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) if err != nil { return err @@ -186,7 +188,7 @@ func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{ OwnerID: ownerID, Image: image, - Digest: digest, + Digest: string(digest), }) if err != nil { return err diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 53f1f130f8..477c3bc71a 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -231,7 +231,7 @@ func GetRepositoryList(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func InitiateUploadBlob(ctx *context.Context) { +func PostBlobsUploads(ctx *context.Context) { image := ctx.PathParam("image") mount := ctx.FormTrim("mount") @@ -313,14 +313,13 @@ func InitiateUploadBlob(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), - Range: "0-0", UploadUUID: upload.ID, Status: http.StatusAccepted, }) } -// https://docs.docker.com/registry/spec/api/#get-blob-upload -func GetUploadBlob(ctx *context.Context) { +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks +func GetBlobsUpload(ctx *context.Context) { uuid := ctx.PathParam("uuid") upload, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -333,15 +332,20 @@ func GetUploadBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ - Range: fmt.Sprintf("0-%d", upload.BytesReceived), + // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578 + respHeaders := &containerHeaders{ UploadUUID: upload.ID, Status: http.StatusNoContent, - }) + } + if upload.BytesReceived > 0 { + respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1) + } + setResponseHeaders(ctx.Resp, respHeaders) } +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func UploadBlob(ctx *context.Context) { +func PatchBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) @@ -377,16 +381,19 @@ func UploadBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + respHeaders := &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID), - Range: fmt.Sprintf("0-%d", uploader.Size()-1), UploadUUID: uploader.ID, Status: http.StatusAccepted, - }) + } + if contentRange != "" { + respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1) + } + setResponseHeaders(ctx.Resp, respHeaders) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func EndUploadBlob(ctx *context.Context) { +func PutBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") digest := ctx.FormTrim("digest") @@ -455,7 +462,7 @@ func EndUploadBlob(ctx *context.Context) { } // https://docs.docker.com/registry/spec/api/#delete-blob-upload -func CancelUploadBlob(ctx *context.Context) { +func DeleteBlobsUpload(ctx *context.Context) { uuid := ctx.PathParam("uuid") _, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -479,16 +486,15 @@ func CancelUploadBlob(ctx *context.Context) { } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { return nil, container_model.ErrContainerBlobNotExist } return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), - Digest: d, + Digest: string(d), }) } @@ -528,9 +534,8 @@ func GetBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs func DeleteBlob(ctx *context.Context) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { apiErrorDefined(ctx, errBlobUnknown) return } @@ -546,7 +551,7 @@ func DeleteBlob(ctx *context.Context) { } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests -func UploadManifest(ctx *context.Context) { +func PutManifest(ctx *context.Context) { reference := ctx.PathParam("reference") mci := &manifestCreationInfo{ @@ -602,18 +607,18 @@ func UploadManifest(ctx *context.Context) { } func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { - reference := ctx.PathParam("reference") - opts := &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), IsManifest: true, } - if digest.Digest(reference).Validate() == nil { - opts.Digest = reference + reference := ctx.PathParam("reference") + if d := digest.Digest(reference); d.Validate() == nil { + opts.Digest = string(d) } else if referencePattern.MatchString(reference) { opts.Tag = reference + opts.OnlyLead = true } else { return nil, container_model.ErrContainerBlobNotExist } @@ -730,7 +735,7 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery -func GetTagList(ctx *context.Context) { +func GetTagsList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 26faa7b024..a640bcda25 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -10,6 +10,7 @@ import ( "io" "os" "strings" + "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" @@ -22,20 +23,21 @@ import ( "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" packages_service "code.gitea.io/gitea/services/packages" + container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" oci "github.com/opencontainers/image-spec/specs-go/v1" ) -func isValidMediaType(mt string) bool { +func isMediaTypeValid(mt string) bool { return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") } -func isImageManifestMediaType(mt string) bool { +func isMediaTypeImageManifest(mt string) bool { return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") } -func isImageIndexMediaType(mt string) bool { +func isMediaTypeImageIndex(mt string) bool { return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") } @@ -64,31 +66,30 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag return "", err } - if !isValidMediaType(mci.MediaType) { + if !isMediaTypeValid(mci.MediaType) { mci.MediaType = index.MediaType - if !isValidMediaType(mci.MediaType) { + if !isMediaTypeValid(mci.MediaType) { return "", errManifestInvalid.WithMessage("MediaType not recognized") } } - if isImageManifestMediaType(mci.MediaType) { - return processImageManifest(ctx, mci, buf) - } else if isImageIndexMediaType(mci.MediaType) { - return processImageManifestIndex(ctx, mci, buf) + if isMediaTypeImageManifest(mci.MediaType) { + return processOciImageManifest(ctx, mci, buf) + } else if isMediaTypeImageIndex(mci.MediaType) { + return processOciImageIndex(ctx, mci, buf) } return "", errManifestInvalid } -func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { +func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { manifestDigest := "" err := func() error { - var manifest oci.Manifest - if err := json.NewDecoder(buf).Decode(&manifest); err != nil { + manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image) + if err != nil { return err } - - if _, err := buf.Seek(0, io.SeekStart); err != nil { + if _, err = buf.Seek(0, io.SeekStart); err != nil { return err } @@ -98,28 +99,7 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p } defer committer.Close() - configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ - OwnerID: mci.Owner.ID, - Image: mci.Image, - Digest: string(manifest.Config.Digest), - }) - if err != nil { - return err - } - - configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256)) - if err != nil { - return err - } - defer configReader.Close() - - metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader) - if err != nil { - return err - } - blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers)) - blobReferences = append(blobReferences, &blobReference{ Digest: manifest.Config.Digest, MediaType: manifest.Config.MediaType, @@ -150,13 +130,13 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p return err } - uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion) + uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_module.UploadVersion) if err != nil && err != packages_model.ErrPackageNotExist { return err } for _, ref := range blobReferences { - if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { + if _, err = createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { return err } } @@ -196,7 +176,7 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p return manifestDigest, nil } -func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { +func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { manifestDigest := "" err := func() error { @@ -221,7 +201,7 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b } for _, manifest := range index.Manifests { - if !isImageManifestMediaType(manifest.MediaType) { + if !isMediaTypeImageManifest(manifest.MediaType) { return errManifestInvalid } @@ -349,24 +329,31 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met LowerVersion: strings.ToLower(mci.Reference), MetadataJSON: string(metadataJSON), } - var pv *packages_model.PackageVersion - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + pv, err := packages_model.GetOrInsertVersion(ctx, _pv) + if err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } - - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount - - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { - log.Error("Error inserting package: %v", err) - return nil, err + if isMediaTypeImageIndex(mci.MediaType) { + if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) { + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + // keep download count on overwriting + _pv.DownloadCount = pv.DownloadCount + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } + } + } else { + err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON}) + if err != nil { + return nil, err + } } } } @@ -376,14 +363,20 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } if mci.IsTagged { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { - log.Error("Error setting package version property: %v", err) + if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { + return nil, err + } + } else { + if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil { return nil, err } } + + if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil { + return nil, err + } for _, manifest := range metadata.Manifests { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { - log.Error("Error setting package version property: %v", err) + if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { return nil, err } } @@ -400,9 +393,9 @@ type blobReference struct { IsLead bool } -func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error { +func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) (*packages_model.PackageFile, error) { if ref.File.Blob.Size != ref.ExpectedSize { - return errSizeInvalid + return nil, errSizeInvalid } if ref.Name == "" { @@ -410,20 +403,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } pf := &packages_model.PackageFile{ - VersionID: pv.ID, - BlobID: ref.File.Blob.ID, - Name: ref.Name, - LowerName: ref.Name, - IsLead: ref.IsLead, + VersionID: pv.ID, + BlobID: ref.File.Blob.ID, + Name: ref.Name, + LowerName: ref.Name, + CompositeKey: string(ref.Digest), + IsLead: ref.IsLead, } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. - return nil + return pf, nil } log.Error("Error inserting package file: %v", err) - return err + return nil, err } props := map[string]string{ @@ -433,18 +427,18 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package for name, value := range props { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil { log.Error("Error setting package file property: %v", err) - return err + return nil, err } } - // Remove the file from the blob upload version + // Remove the ref file (old file) from the blob upload version if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID { if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil { - return err + return nil, err } } - return nil + return pf, nil } func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) { @@ -471,14 +465,34 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack } manifestDigest := digestFromHashSummer(buf) - err = createFileFromBlobReference(ctx, pv, nil, &blobReference{ + pf, err := createFileFromBlobReference(ctx, pv, nil, &blobReference{ Digest: digest.Digest(manifestDigest), MediaType: mci.MediaType, - Name: container_model.ManifestFilename, + Name: container_module.ManifestFilename, File: &packages_model.PackageFileDescriptor{Blob: pb}, ExpectedSize: pb.Size, IsLead: true, }) + if err != nil { + return nil, false, "", err + } + oldManifestFiles, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: mci.Owner.ID, + PackageType: packages_model.TypeContainer, + VersionID: pv.ID, + Query: container_module.ManifestFilename, + }) + if err != nil { + return nil, false, "", err + } + for _, oldManifestFile := range oldManifestFiles { + if oldManifestFile.ID != pf.ID && oldManifestFile.IsLead { + err = packages_model.UpdateFile(ctx, &packages_model.PackageFile{ID: oldManifestFile.ID, IsLead: false}, []string{"is_lead"}) + if err != nil { + return nil, false, "", err + } + } + } return pb, !exists, manifestDigest, err } diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go index a726065ad0..801c60af13 100644 --- a/routers/api/packages/nuget/api_v2.go +++ b/routers/api/packages/nuget/api_v2.go @@ -246,21 +246,30 @@ type TypedValue[T any] struct { } type FeedEntryProperties struct { - Version string `xml:"d:Version"` - NormalizedVersion string `xml:"d:NormalizedVersion"` Authors string `xml:"d:Authors"` + Copyright string `xml:"d:Copyright,omitempty"` + Created TypedValue[time.Time] `xml:"d:Created"` Dependencies string `xml:"d:Dependencies"` Description string `xml:"d:Description"` - VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` + DevelopmentDependency TypedValue[bool] `xml:"d:DevelopmentDependency"` DownloadCount TypedValue[int64] `xml:"d:DownloadCount"` - PackageSize TypedValue[int64] `xml:"d:PackageSize"` - Created TypedValue[time.Time] `xml:"d:Created"` + ID string `xml:"d:Id"` + IconURL string `xml:"d:IconUrl,omitempty"` + Language string `xml:"d:Language,omitempty"` LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"` - Published TypedValue[time.Time] `xml:"d:Published"` + LicenseURL string `xml:"d:LicenseUrl,omitempty"` + MinClientVersion string `xml:"d:MinClientVersion,omitempty"` + NormalizedVersion string `xml:"d:NormalizedVersion"` + Owners string `xml:"d:Owners,omitempty"` + PackageSize TypedValue[int64] `xml:"d:PackageSize"` ProjectURL string `xml:"d:ProjectUrl,omitempty"` + Published TypedValue[time.Time] `xml:"d:Published"` ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"` RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"` - Title string `xml:"d:Title"` + Tags string `xml:"d:Tags,omitempty"` + Title string `xml:"d:Title,omitempty"` + Version string `xml:"d:Version"` + VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` } type FeedEntry struct { @@ -353,21 +362,30 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames Author: metadata.Authors, Content: content, Properties: &FeedEntryProperties{ - Version: pd.Version.Version, - NormalizedVersion: pd.Version.Version, Authors: metadata.Authors, + Copyright: metadata.Copyright, + Created: createdValue, Dependencies: buildDependencyString(metadata), Description: metadata.Description, - VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, + DevelopmentDependency: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.DevelopmentDependency}, DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, - PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, - Created: createdValue, + ID: pd.Package.Name, + IconURL: metadata.IconURL, + Language: metadata.Language, LastUpdated: createdValue, - Published: createdValue, + LicenseURL: metadata.LicenseURL, + MinClientVersion: metadata.MinClientVersion, + NormalizedVersion: pd.Version.Version, + Owners: metadata.Owners, + PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, ProjectURL: metadata.ProjectURL, + Published: createdValue, ReleaseNotes: metadata.ReleaseNotes, RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance}, - Title: pd.Package.Name, + Tags: metadata.Tags, + Title: metadata.Title, + Version: pd.Version.Version, + VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, }, } diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index de8c7ef3ed..cb880c8bdb 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -14,6 +14,7 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" @@ -309,7 +310,7 @@ func GetPackageInfo(ctx *context.Context) { apiError(ctx, http.StatusNotFound, nil) return } - infoContent, err := makePackageInfo(ctx, versions) + infoContent, err := makePackageInfo(ctx, versions, cache.NewEphemeralCache()) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -317,7 +318,7 @@ func GetPackageInfo(ctx *context.Context) { ctx.PlainText(http.StatusOK, infoContent) } -// GetAllPackagesVersions returns a custom text based format containing information about all versions of all rubygems. +// GetAllPackagesVersions returns a custom text-based format containing information about all versions of all rubygems. // ref: https://guides.rubygems.org/rubygems-org-compact-index-api/ func GetAllPackagesVersions(ctx *context.Context) { packages, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems) @@ -326,6 +327,7 @@ func GetAllPackagesVersions(ctx *context.Context) { return } + ephemeralCache := cache.NewEphemeralCache() out := &strings.Builder{} out.WriteString("---\n") for _, pkg := range packages { @@ -338,7 +340,7 @@ func GetAllPackagesVersions(ctx *context.Context) { continue } - info, err := makePackageInfo(ctx, versions) + info, err := makePackageInfo(ctx, versions, ephemeralCache) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -348,7 +350,14 @@ func GetAllPackagesVersions(ctx *context.Context) { _, _ = fmt.Fprintf(out, "%s ", pkg.Name) for i, v := range versions { sep := util.Iif(i == len(versions)-1, "", ",") - _, _ = fmt.Fprintf(out, "%s%s", v.Version, sep) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, v, ephemeralCache) + if errors.Is(err, util.ErrNotExist) { + continue + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + writePackageVersionForList(pd.Metadata, v.Version, sep, out) } _, _ = fmt.Fprintf(out, " %x\n", md5.Sum([]byte(info))) } @@ -356,6 +365,16 @@ func GetAllPackagesVersions(ctx *context.Context) { ctx.PlainText(http.StatusOK, out.String()) } +func writePackageVersionForList(metadata any, version, sep string, out *strings.Builder) { + if metadata, _ := metadata.(*rubygems_module.Metadata); metadata != nil && metadata.Platform != "" && metadata.Platform != "ruby" { + // VERSION_PLATFORM (see comment above in GetAllPackagesVersions) + _, _ = fmt.Fprintf(out, "%s_%s%s", version, metadata.Platform, sep) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s%s", version, sep) + } +} + func writePackageVersionRequirements(prefix string, reqs []rubygems_module.VersionRequirement, out *strings.Builder) { out.WriteString(prefix) if len(reqs) == 0 { @@ -367,11 +386,21 @@ func writePackageVersionRequirements(prefix string, reqs []rubygems_module.Versi } } -func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion) (string, error) { +func writePackageVersionForDependency(version, platform string, out *strings.Builder) { + if platform != "" && platform != "ruby" { + // VERSION-PLATFORM (see comment below in makePackageVersionDependency) + _, _ = fmt.Fprintf(out, "%s-%s ", version, platform) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s ", version) + } +} + +func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { // format: VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...] // DEPENDENCY: GEM:CONSTRAINT[&CONSTRAINT] // REQUIREMENT: KEY:VALUE (always contains "checksum") - pd, err := packages_model.GetPackageDescriptor(ctx, version) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, version, c) if err != nil { return "", err } @@ -388,8 +417,7 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. } buf := &strings.Builder{} - buf.WriteString(version.Version) - buf.WriteByte(' ') + writePackageVersionForDependency(version.Version, metadata.Platform, buf) for i, dep := range metadata.RuntimeDependencies { sep := util.Iif(i == 0, "", ",") writePackageVersionRequirements(fmt.Sprintf("%s%s:", sep, dep.Name), dep.Version, buf) @@ -404,10 +432,10 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. return buf.String(), nil } -func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion) (string, error) { +func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { ret := "---\n" for _, v := range versions { - dep, err := makePackageVersionDependency(ctx, v) + dep, err := makePackageVersionDependency(ctx, v, c) if err != nil { return "", err } diff --git a/routers/api/packages/rubygems/rubygems_test.go b/routers/api/packages/rubygems/rubygems_test.go new file mode 100644 index 0000000000..a07e12a7d3 --- /dev/null +++ b/routers/api/packages/rubygems/rubygems_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package rubygems + +import ( + "strings" + "testing" + + rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" + + "github.com/stretchr/testify/assert" +) + +func TestWritePackageVersion(t *testing.T) { + buf := &strings.Builder{} + + writePackageVersionForList(nil, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "ruby"}, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "linux"}, "1.0", " ", buf) + assert.Equal(t, "1.0_linux ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "ruby", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "os", buf) + assert.Equal(t, "1.0-os ", buf.String()) + buf.Reset() +} diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 2048c76ea0..1b58beb7b6 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -77,10 +77,7 @@ func GetIssueDependencies(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit == 0 { limit = setting.API.DefaultPagingNum @@ -328,10 +325,7 @@ func GetIssueBlocks(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit <= 1 { limit = setting.API.DefaultPagingNum diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index f2e0cad86c..c1e0b47d33 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -203,7 +203,7 @@ func Migrate(ctx *context.APIContext) { } if repo != nil { - if errDelete := repo_service.DeleteRepositoryDirectly(ctx, ctx.Doer, repo.ID); errDelete != nil { + if errDelete := repo_service.DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil { log.Error("DeleteRepository: %v", errDelete) } } diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index b6f5a3ac9e..272b395dfb 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -247,7 +247,9 @@ func CreateRelease(ctx *context.APIContext) { IsTag: false, Repo: ctx.Repo.Repository, } - if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil { + // GitHub doesn't have "tag_message", GitLab has: https://docs.gitlab.com/api/releases/#create-a-release + // It doesn't need to be the same as the "release note" + if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil { if repo_model.IsErrReleaseAlreadyExist(err) { ctx.APIError(http.StatusConflict, err) } else if release_service.IsErrProtectedTagName(err) { diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index d5840b4149..3094c1947c 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -298,10 +298,7 @@ func ListWikiPages(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit <= 1 { limit = setting.API.DefaultPagingNum @@ -434,10 +431,7 @@ func ListPageRevisions(ctx *context.APIContext) { // get commit count - wiki revisions commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( diff --git a/routers/install/install.go b/routers/install/install.go index 2962f3948f..4b3eba78b3 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strconv" "strings" "time" @@ -99,11 +100,8 @@ func Install(ctx *context.Context) { curDBType := setting.Database.Type.String() var isCurDBTypeSupported bool - for _, dbType := range setting.SupportedDatabaseTypes { - if dbType == curDBType { - isCurDBTypeSupported = true - break - } + if slices.Contains(setting.SupportedDatabaseTypes, curDBType) { + isCurDBTypeSupported = true } if !isCurDBTypeSupported { curDBType = "mysql" diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 80d554b6e3..0f6f31b884 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -177,7 +177,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { customURLMapping = nil } var scopes []string - for _, s := range strings.Split(form.Oauth2Scopes, ",") { + for s := range strings.SplitSeq(form.Oauth2Scopes, ",") { s = strings.TrimSpace(s) if s != "" { scopes = append(scopes, s) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 520f14e89f..5387d2b26f 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -61,7 +61,7 @@ func TestCache(ctx *context.Context) { func shadowPasswordKV(cfgItem, splitter string) string { fields := strings.Split(cfgItem, splitter) - for i := 0; i < len(fields); i++ { + for i := range fields { if strings.HasPrefix(fields[i], "password=") { fields[i] = "password=******" break diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index d040dbe0ba..5395529d66 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -16,13 +16,7 @@ import ( ) func MonitorDiagnosis(ctx *context.Context) { - seconds := ctx.FormInt64("seconds") - if seconds <= 1 { - seconds = 1 - } - if seconds > 300 { - seconds = 300 - } + seconds := min(max(ctx.FormInt64("seconds"), 1), 300) httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{ ContentType: "application/zip", diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index 21a8ab0d17..e9d6abbe92 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -26,10 +26,7 @@ func Notices(ctx *context.Context) { ctx.Data["PageIsAdminNotices"] = true total := system_model.CountNotices(ctx) - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) notices, err := system_model.Notices(ctx, page, setting.UI.Admin.NoticePagingNum) if err != nil { diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 5122342259..1904bfee11 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -24,10 +24,7 @@ const ( // Packages shows all packages func Packages(ctx *context.Context) { - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") sort := ctx.FormTrim("sort") diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 8c2d3276a8..2ef4a86022 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -349,10 +349,7 @@ func RegisterOpenIDPost(ctx *context.Context) { context.VerifyCaptcha(ctx, tplSignUpOID, form) } - length := setting.MinPasswordLength - if length < 256 { - length = 256 - } + length := max(setting.MinPasswordLength, 256) password, err := util.CryptoRandomString(int64(length)) if err != nil { ctx.RenderWithErr(err.Error(), tplSignUpOID, form) diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 765931a730..a22d376579 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -132,7 +132,7 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) { selectedStyle := ctx.FormString("style", badge.DefaultStyle) var badges []badge.Badge badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green")) - for r := rune(0); r < 256; r++ { + for r := range rune(256) { if unicode.IsPrint(r) { s := strings.Repeat(string(r), 15) badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green")) diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 8f6518a4fc..8bde983e30 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -5,6 +5,7 @@ package explore import ( "net/http" + "slices" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -94,11 +95,8 @@ func Code(ctx *context.Context) { loadRepoIDs := make([]int64, 0, len(searchResults)) for _, result := range searchResults { var find bool - for _, id := range loadRepoIDs { - if id == result.RepoID { - find = true - break - } + if slices.Contains(loadRepoIDs, result.RepoID) { + find = true } if !find { loadRepoIDs = append(loadRepoIDs, result.RepoID) diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index d42afafe9e..59b97c1717 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -20,7 +20,7 @@ func SSHInfo(rw http.ResponseWriter, req *http.Request) { return } rw.Header().Set("content-type", "text/json;charset=UTF-8") - _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) + _, err := rw.Write([]byte(`{"type":"agit","version":1}`)) if err != nil { log.Error("fail to write result: err: %v", err) rw.WriteHeader(http.StatusInternalServerError) diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 2cbe75989a..61022d3f09 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -28,10 +28,7 @@ func Members(ctx *context.Context) { ctx.Data["Title"] = org.FullName ctx.Data["PageIsOrgMembers"] = true - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) opts := &organization.FindOrgMembersOpts{ Doer: ctx.Doer, diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index f423e9cb36..059cce8281 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -53,10 +53,7 @@ func Projects(ctx *context.Context) { isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" keyword := ctx.FormTrim("q") - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) var projectType project_model.Type if ctx.ContextUser.IsOrganization() { diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index b01d57084a..7f219811bd 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -377,10 +377,8 @@ func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch { if !decodeNode(w.RawOn, &val) { return nil } - for _, v := range val { - if v == "workflow_dispatch" { - return &WorkflowDispatch{} - } + if slices.Contains(val, "workflow_dispatch") { + return &WorkflowDispatch{} } case yaml.MappingNode: var val map[string]yaml.Node diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5d382ebd71..dc8a90b2ae 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -45,10 +45,7 @@ func Branches(ctx *context.Context) { ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsBranches"] = true - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) pageSize := setting.Git.BranchesRangeSize kw := ctx.FormString("q") diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 9dd6988825..b3af138461 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -67,10 +67,7 @@ func Commits(ctx *context.Context) { commitsCount := ctx.Repo.CommitsCount - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) pageSize := ctx.FormInt("limit") if pageSize <= 0 { @@ -230,10 +227,7 @@ func FileHistory(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 8b99dd95da..de34a9375c 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -575,7 +575,13 @@ func PrepareCompareDiff( ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() ctx.Data["AfterCommitID"] = headCommitID - ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") + + // follow GitHub's behavior: autofill the form and expand + newPrFormTitle := ctx.FormTrim("title") + newPrFormBody := ctx.FormTrim("body") + ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != "" + ctx.Data["TitleQuery"] = newPrFormTitle + ctx.Data["BodyQuery"] = newPrFormBody if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) || headCommitID == ci.CompareInfo.BaseCommitID { diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index cbcb3a3b21..1a090c9437 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -145,10 +145,6 @@ func editFile(ctx *context.Context, isNewFile bool) { } blob := entry.Blob() - if blob.Size() >= setting.UI.MaxDisplayFileSize { - ctx.NotFound(err) - return - } buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) if err != nil { @@ -162,22 +158,37 @@ func editFile(ctx *context.Context, isNewFile bool) { defer dataRc.Close() - ctx.Data["FileSize"] = blob.Size() - - // Only some file types are editable online as text. - if !fInfo.st.IsRepresentableAsText() || fInfo.isLFSFile { - ctx.NotFound(nil) - return + if fInfo.isLFSFile { + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("GetTreePathLock", err) + return + } + if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + ctx.NotFound(nil) + return + } } - d, _ := io.ReadAll(dataRc) + ctx.Data["FileSize"] = fInfo.fileSize - buf = append(buf, d...) - if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { - log.Error("ToUTF8: %v", err) - ctx.Data["FileContent"] = string(buf) + // Only some file types are editable online as text. + if fInfo.isLFSFile { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") + } else if !fInfo.st.IsRepresentableAsText() { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") + } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") } else { - ctx.Data["FileContent"] = content + d, _ := io.ReadAll(dataRc) + + buf = append(buf, d...) + if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { + log.Error("ToUTF8: %v", err) + ctx.Data["FileContent"] = string(buf) + } else { + ctx.Data["FileContent"] = content + } } } else { // Append filename from query, or empty string to allow username the new file. @@ -277,9 +288,20 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b return } - operation := "update" + var operation string if isNewFile { operation = "create" + } else if form.Content.Has() { + // The form content only has data if the file is representable as text, is not too large and not in lfs. + operation = "update" + } else if ctx.Repo.TreePath != form.TreePath { + // If it doesn't have data, the only possible operation is a "rename" + operation = "rename" + } else { + // It should never happen, just in case + ctx.Flash.Error(ctx.Tr("error.occurred")) + ctx.HTML(http.StatusOK, tplEditFile) + return } if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ @@ -292,7 +314,7 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b Operation: operation, FromTreePath: ctx.Repo.TreePath, TreePath: form.TreePath, - ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")), + ContentReader: strings.NewReader(strings.ReplaceAll(form.Content.Value(), "\r", "")), }, }, Signoff: form.Signoff, @@ -795,7 +817,7 @@ func cleanUploadFileName(name string) string { // Rebase the filename name = util.PathJoinRel(name) // Git disallows any filenames to have a .git directory in them. - for _, part := range strings.Split(name, "/") { + for part := range strings.SplitSeq(name, "/") { if strings.ToLower(part) == ".git" { return "" } diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 79f033659b..9f5cda10c2 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -151,7 +151,7 @@ func ForkPost(ctx *context.Context) { ctx.Data["ContextUser"] = ctxUser if ctx.HasError() { - ctx.HTML(http.StatusOK, tplFork) + ctx.JSONError(ctx.GetErrMsg()) return } @@ -159,12 +159,12 @@ func ForkPost(ctx *context.Context) { traverseParentRepo := forkRepo for { if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) return } repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID) if repo != nil { - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) return } if !traverseParentRepo.IsFork { @@ -201,26 +201,26 @@ func ForkPost(ctx *context.Context) { case repo_model.IsErrReachLimitOfRepo(err): maxCreationLimit := ctxUser.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tplFork, &form) + ctx.JSONError(msg) case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) case repo_model.IsErrRepoFilesAlreadyExist(err): switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete")) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt")) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete")) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist")) } case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name)) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern)) case errors.Is(err, user_model.ErrBlockedUser): - ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form) + ctx.JSONError(ctx.Tr("repo.fork.blocked_user")) default: ctx.ServerError("ForkPost", err) } @@ -228,5 +228,5 @@ func ForkPost(ctx *context.Context) { } log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 61606f8c5f..deb3ae4f3a 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" "sync" @@ -363,12 +364,7 @@ func containsParentDirectorySeparator(v string) bool { if !strings.Contains(v, "..") { return false } - for _, ent := range strings.FieldsFunc(v, isSlashRune) { - if ent == ".." { - return true - } - } - return false + return slices.Contains(strings.FieldsFunc(v, isSlashRune), "..") } func isSlashRune(r rune) bool { return r == '/' || r == '\\' } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a4747964c6..54b7e5df2a 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -212,7 +212,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList { return nil } issueIDs := make([]int64, 0, 10) - for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") { + for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") { issueID, err := strconv.ParseInt(stringIssueID, 10, 64) if err != nil { ctx.ServerError("ParseInt", err) diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index d8863961ff..887019b146 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "html/template" + "maps" "net/http" "slices" "sort" @@ -136,9 +137,7 @@ func NewIssue(ctx *context.Context) { ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData) - for k, v := range errs { - ret.TemplateErrors[k] = v - } + maps.Copy(ret.TemplateErrors, errs) if ctx.Written() { return } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 8a26a0dcc3..dd53b1d3f1 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -38,10 +38,7 @@ func Milestones(ctx *context.Context) { isShowClosed := ctx.FormString("state") == "closed" sortType := ctx.FormString("sort") keyword := ctx.FormTrim("q") - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ ListOptions: db.ListOptions{ diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 65a340a799..d09a57c03f 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -21,10 +21,7 @@ const ( // Packages displays a list of all packages in the repository func Packages(ctx *context.Context) { - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go index ca346b7e6c..3ffd8f89c4 100644 --- a/routers/web/repo/patch.go +++ b/routers/web/repo/patch.go @@ -99,7 +99,7 @@ func NewDiffPatchPost(ctx *context.Context) { OldBranch: ctx.Repo.BranchName, NewBranch: branchName, Message: message, - Content: strings.ReplaceAll(form.Content, "\r", ""), + Content: strings.ReplaceAll(form.Content.Value(), "\r", ""), Author: gitCommitter, Committer: gitCommitter, }) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 0bf1f64d09..a57976b4ca 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -61,10 +61,7 @@ func Projects(ctx *context.Context) { isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" keyword := ctx.FormTrim("q") repo := ctx.Repo.Repository - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) ctx.Data["OpenCount"] = repo.NumOpenProjects ctx.Data["ClosedCount"] = repo.NumClosedProjects diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 8e586adde9..1592bd4ced 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -103,7 +103,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) releaseInfos := make([]*ReleaseInfo, 0, len(releases)) for _, r := range releases { if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { - r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) + r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID) if err != nil { if user_model.IsErrUserNotExist(err) { r.Publisher = user_model.NewGhostUser() diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 269ba17498..828ec08a8a 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -88,7 +88,7 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User { } var orgsAvailable []*organization.Organization - for i := 0; i < len(orgs); i++ { + for i := range orgs { if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) { orgsAvailable = append(orgsAvailable, orgs[i]) } diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index a065620b2b..bbbb99dc89 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -45,10 +45,7 @@ func LFSFiles(ctx *context.Context) { ctx.NotFound(nil) return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("LFSFiles", err) @@ -77,10 +74,7 @@ func LFSLocks(ctx *context.Context) { } ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("LFSLocks", err) diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 3df6051975..ec0ad02828 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -140,13 +140,6 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") } - // Assume file is not editable first. - if fInfo.isLFSFile { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") - } else if !isRepresentableAsText { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") - } - // read all needed attributes which will be used later // there should be no performance different between reading 2 or 4 here attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{ @@ -243,21 +236,6 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["FileContent"] = fileContent ctx.Data["LineEscapeStatus"] = statuses } - if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanEditFile"] = false - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - } else { - ctx.Data["CanEditFile"] = true - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") - } - } else if !ctx.Repo.RefFullName.IsBranch() { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") - } - } case fInfo.st.IsPDF(): ctx.Data["IsPDFFile"] = true @@ -307,17 +285,32 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanDeleteFile"] = false - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - } else { - ctx.Data["CanDeleteFile"] = true - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") - } - } else if !ctx.Repo.RefFullName.IsBranch() { + prepareToRenderButtons(ctx, lfsLock) +} + +func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { + // archived or mirror repository, the buttons should not be shown + if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() { + return + } + + // The buttons should not be shown if it's not a branch + if !ctx.Repo.RefFullName.IsBranch() { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + return + } + + if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") + return } + + // it's a lfs file and the user is not the owner of the lock + isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID + ctx.Data["CanEditFile"] = !isLFSLocked + ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file")) + ctx.Data["CanDeleteFile"] = !isLFSLocked + ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file")) } diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 459cf0a616..7af6ad450e 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -150,7 +150,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil } ctx.Data["RawFileLink"] = "" - ctx.Data["ReadmeInList"] = true + ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path ctx.Data["ReadmeExist"] = true ctx.Data["FileIsSymlink"] = readmeFile.IsLink() @@ -162,7 +162,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil defer dataRc.Close() ctx.Data["FileIsText"] = fInfo.isTextFile - ctx.Data["FileTreePath"] = path.Join(subfolder, readmeFile.Name()) + ctx.Data["FileTreePath"] = path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name()) ctx.Data["FileSize"] = fInfo.fileSize ctx.Data["IsLFSFile"] = fInfo.isLFSFile diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 41bf9f5adb..a1e10c380d 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -417,10 +417,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["CommitCount"] = commitsCount // get page - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index d0139f6613..73f9970a07 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -245,7 +245,12 @@ func TestDefaultWikiBranch(t *testing.T) { assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main")) // repo with wiki - assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"})) + assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime( + db.DefaultContext, + &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}, + "default_wiki_branch", + ), + ) ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") ctx.SetPathParam("*", "Home") diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index a642cfd66d..648f8046a4 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -108,10 +108,7 @@ func Runners(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) opts := actions_model.FindRunnerOptions{ ListOptions: db.ListOptions{ @@ -179,10 +176,7 @@ func RunnersEdit(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) runnerID := ctx.PathParamInt64("runnerid") ownerID := rCtx.OwnerID diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go index 3d1795b42c..a18dedf89c 100644 --- a/routers/web/shared/packages/packages.go +++ b/routers/web/shared/packages/packages.go @@ -8,7 +8,6 @@ import ( "net/http" "time" - "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -159,12 +158,18 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) { PackageID: p.ID, IsInternal: optional.Some(false), Sort: packages_model.SortCreatedDesc, - Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), }) if err != nil { ctx.ServerError("SearchVersions", err) return } + if pcr.KeepCount > 0 { + if pcr.KeepCount < len(pvs) { + pvs = pvs[pcr.KeepCount:] + } else { + pvs = nil + } + } for _, pv := range pvs { if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil { ctx.ServerError("ShouldBeSkipped", err) @@ -177,7 +182,6 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) { if pcr.MatchFullName { toMatch = p.LowerName + "/" + pv.LowerVersion } - if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) { continue } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f2153c6d54..20ab1405dd 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -5,6 +5,7 @@ package user import ( "net/http" + "slices" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -87,11 +88,8 @@ func CodeSearch(ctx *context.Context) { loadRepoIDs := make([]int64, 0, len(searchResults)) for _, result := range searchResults { var find bool - for _, id := range loadRepoIDs { - if id == result.RepoID { - find = true - break - } + if slices.Contains(loadRepoIDs, result.RepoID) { + find = true } if !find { loadRepoIDs = append(loadRepoIDs, result.RepoID) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 607fb62acb..b53a3daedb 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -197,7 +197,7 @@ func Milestones(ctx *context.Context) { reposQuery = reposQuery[1 : len(reposQuery)-1] // for each ID (delimiter ",") add to int to repoIDs - for _, rID := range strings.Split(reposQuery, ",") { + for rID := range strings.SplitSeq(reposQuery, ",") { // Ensure nonempty string entries if rID != "" && rID != "0" { rIDint64, err := strconv.ParseInt(rID, 10, 64) @@ -520,10 +520,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { opts.IsClosed = optional.Some(isShowClosed) // Make sure page number is at least 1. Will be posted to ctx.Data. - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) opts.Paginator = &db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 2b758ba461..610a9b8076 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -203,10 +203,7 @@ func NotificationPurgePost(ctx *context.Context) { // NotificationSubscriptions returns the list of subscribed issues func NotificationSubscriptions(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) sortType := ctx.FormString("sort") ctx.Data["SortType"] = sortType @@ -331,10 +328,7 @@ func NotificationSubscriptions(ctx *context.Context) { // NotificationWatching returns the list of watching repos func NotificationWatching(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) keyword := ctx.FormTrim("q") ctx.Data["Keyword"] = keyword diff --git a/routers/web/user/package.go b/routers/web/user/package.go index 92af6d14ef..532a28b920 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -4,6 +4,8 @@ package user import ( + gocontext "context" + "errors" "net/http" "net/url" @@ -20,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" arch_module "code.gitea.io/gitea/modules/packages/arch" + container_module "code.gitea.io/gitea/modules/packages/container" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" @@ -31,6 +34,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" packages_service "code.gitea.io/gitea/services/packages" + container_service "code.gitea.io/gitea/services/packages/container" ) const ( @@ -46,10 +50,7 @@ func ListPackages(ctx *context.Context) { ctx.ServerError("RenderUserOrgHeader", err) return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") @@ -165,6 +166,24 @@ func RedirectToLastVersion(ctx *context.Context) { ctx.Redirect(pd.VersionWebLink()) } +func viewPackageContainerImage(ctx gocontext.Context, pd *packages_model.PackageDescriptor, digest string) (*container_module.Metadata, error) { + manifestBlob, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ + OwnerID: pd.Owner.ID, + Image: pd.Package.LowerName, + Digest: digest, + }) + if err != nil { + return nil, err + } + manifestReader, err := packages_service.OpenBlobStream(manifestBlob.Blob) + if err != nil { + return nil, err + } + defer manifestReader.Close() + _, _, metadata, err := container_service.ParseManifestMetadata(ctx, manifestReader, pd.Owner.ID, pd.Package.LowerName) + return metadata, err +} + // ViewPackageVersion displays a single package version func ViewPackageVersion(ctx *context.Context) { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { @@ -172,6 +191,7 @@ func ViewPackageVersion(ctx *context.Context) { return } + versionSub := ctx.PathParam("version_sub") pd := ctx.Package.Descriptor ctx.Data["Title"] = pd.Package.Name ctx.Data["IsPackagesPage"] = true @@ -183,6 +203,9 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["PackageRegistryHost"] = registryHostURL.Host + var pvs []*packages_model.PackageVersion + pvsTotal := int64(0) + switch pd.Package.Type { case packages_model.TypeAlpine: branches := make(container.Set[string]) @@ -260,21 +283,26 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["Groups"] = util.Sorted(groups.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values()) - } - - var ( - total int64 - pvs []*packages_model.PackageVersion - ) - switch pd.Package.Type { case packages_model.TypeContainer: - pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ + imageMetadata := pd.Metadata + if versionSub != "" { + imageMetadata, err = viewPackageContainerImage(ctx, pd, versionSub) + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(nil) + return + } else if err != nil { + ctx.ServerError("viewPackageContainerImage", err) + return + } + } + ctx.Data["ContainerImageMetadata"] = imageMetadata + pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ Paginator: db.NewAbsoluteListOptions(0, 5), PackageID: pd.Package.ID, IsTagged: true, }) default: - pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ Paginator: db.NewAbsoluteListOptions(0, 5), PackageID: pd.Package.ID, IsInternal: optional.Some(false), @@ -286,7 +314,7 @@ func ViewPackageVersion(ctx *context.Context) { } ctx.Data["LatestVersions"] = pvs - ctx.Data["TotalVersionCount"] = total + ctx.Data["TotalVersionCount"] = pvsTotal ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() @@ -320,10 +348,7 @@ func ListPackageVersions(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) pagination := &db.ListOptions{ PageSize: setting.UI.PackagesPagingNum, Page: page, diff --git a/routers/web/web.go b/routers/web/web.go index 5eba29c601..a54f96ec68 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1012,6 +1012,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/versions", user.ListPackageVersions) m.Group("/{version}", func() { m.Get("", user.ViewPackageVersion) + m.Get("/{version_sub}", user.ViewPackageVersion) m.Get("/files/{fileid}", user.DownloadPackageFile) m.Group("/settings", func() { m.Get("", user.PackageSettings) |