mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-23 06:43:34 +01:00
Merge pull request 'Implement external release assets' (#1445) from maltejur/forgejo:forgejo-external-attachments into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/1445 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
94933470cd
22 changed files with 826 additions and 119 deletions
|
@ -74,6 +74,8 @@ var migrations = []*Migration{
|
|||
NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser),
|
||||
// v18 -> v19
|
||||
NewMigration("Create the `following_repo` table", CreateFollowingRepoTable),
|
||||
// v19 -> v20
|
||||
NewMigration("Add external_url to attachment table", AddExternalURLColumnToAttachmentTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
14
models/forgejo_migrations/v19.go
Normal file
14
models/forgejo_migrations/v19.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgejo_migrations //nolint:revive
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddExternalURLColumnToAttachmentTable(x *xorm.Engine) error {
|
||||
type Attachment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ExternalURL string
|
||||
}
|
||||
return x.Sync(new(Attachment))
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
// Attachment represent a attachment of issue/comment/release.
|
||||
|
@ -31,6 +32,7 @@ type Attachment struct {
|
|||
NoAutoTime bool `xorm:"-"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
CustomDownloadURL string `xorm:"-"`
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -59,6 +61,10 @@ func (a *Attachment) RelativePath() string {
|
|||
|
||||
// DownloadURL returns the download url of the attached file
|
||||
func (a *Attachment) DownloadURL() string {
|
||||
if a.ExternalURL != "" {
|
||||
return a.ExternalURL
|
||||
}
|
||||
|
||||
if a.CustomDownloadURL != "" {
|
||||
return a.CustomDownloadURL
|
||||
}
|
||||
|
@ -86,6 +92,23 @@ func (err ErrAttachmentNotExist) Unwrap() error {
|
|||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
type ErrInvalidExternalURL struct {
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
func IsErrInvalidExternalURL(err error) bool {
|
||||
_, ok := err.(ErrInvalidExternalURL)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrInvalidExternalURL) Error() string {
|
||||
return fmt.Sprintf("invalid external URL: '%s'", err.ExternalURL)
|
||||
}
|
||||
|
||||
func (err ErrInvalidExternalURL) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// GetAttachmentByID returns attachment by given id
|
||||
func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) {
|
||||
attach := &Attachment{}
|
||||
|
@ -221,12 +244,18 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str
|
|||
if attach.UUID == "" {
|
||||
return fmt.Errorf("attachment uuid should be not blank")
|
||||
}
|
||||
if attach.ExternalURL != "" && !validation.IsValidExternalURL(attach.ExternalURL) {
|
||||
return ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
|
||||
}
|
||||
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAttachment updates the given attachment in database
|
||||
func UpdateAttachment(ctx context.Context, atta *Attachment) error {
|
||||
if atta.ExternalURL != "" && !validation.IsValidExternalURL(atta.ExternalURL) {
|
||||
return ErrInvalidExternalURL{ExternalURL: atta.ExternalURL}
|
||||
}
|
||||
sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count")
|
||||
if atta.ID != 0 && atta.UUID == "" {
|
||||
sess = sess.ID(atta.ID)
|
||||
|
|
|
@ -18,10 +18,14 @@ type Attachment struct {
|
|||
Created time.Time `json:"created_at"`
|
||||
UUID string `json:"uuid"`
|
||||
DownloadURL string `json:"browser_download_url"`
|
||||
// Enum: attachment,external
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// EditAttachmentOptions options for editing attachments
|
||||
// swagger:model
|
||||
type EditAttachmentOptions struct {
|
||||
Name string `json:"name"`
|
||||
// (Can only be set if existing attachment is of external type)
|
||||
DownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
|
|
@ -2721,6 +2721,12 @@ release.add_tag = Create tag
|
|||
release.releases_for = Releases for %s
|
||||
release.tags_for = Tags for %s
|
||||
release.system_generated = This attachment is automatically generated.
|
||||
release.type_attachment = Attachment
|
||||
release.type_external_asset = External Asset
|
||||
release.asset_name = Asset Name
|
||||
release.asset_external_url = External URL
|
||||
release.add_external_asset = Add External Asset
|
||||
release.invalid_external_url = Invalid External URL: "%s"
|
||||
|
||||
branch.name = Branch name
|
||||
branch.already_exists = A branch named "%s" already exists.
|
||||
|
|
|
@ -247,7 +247,7 @@ func CreateRelease(ctx *context.APIContext) {
|
|||
IsTag: false,
|
||||
Repo: ctx.Repo.Repository,
|
||||
}
|
||||
if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil {
|
||||
if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, "", nil); err != nil {
|
||||
if repo_model.IsErrReleaseAlreadyExist(err) {
|
||||
ctx.Error(http.StatusConflict, "ReleaseAlreadyExist", err)
|
||||
} else if models.IsErrProtectedTagName(err) {
|
||||
|
@ -274,7 +274,7 @@ func CreateRelease(ctx *context.APIContext) {
|
|||
rel.Publisher = ctx.Doer
|
||||
rel.Target = form.Target
|
||||
|
||||
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil, true); err != nil {
|
||||
if err = release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, true, nil); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
|
||||
return
|
||||
}
|
||||
|
@ -351,7 +351,7 @@ func EditRelease(ctx *context.APIContext) {
|
|||
if form.HideArchiveLinks != nil {
|
||||
rel.HideArchiveLinks = *form.HideArchiveLinks
|
||||
}
|
||||
if err := release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, nil, nil, nil, false); err != nil {
|
||||
if err := release_service.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, false, nil); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -5,7 +5,10 @@ package repo
|
|||
|
||||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -179,11 +182,18 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
|||
// description: name of the attachment
|
||||
// type: string
|
||||
// required: false
|
||||
// # There is no good way to specify "either 'attachment' or 'external_url' is required" with OpenAPI
|
||||
// # https://github.com/OAI/OpenAPI-Specification/issues/256
|
||||
// - name: attachment
|
||||
// in: formData
|
||||
// description: attachment to upload
|
||||
// description: attachment to upload (this parameter is incompatible with `external_url`)
|
||||
// type: file
|
||||
// required: false
|
||||
// - name: external_url
|
||||
// in: formData
|
||||
// description: url to external asset (this parameter is incompatible with `attachment`)
|
||||
// type: string
|
||||
// required: false
|
||||
// responses:
|
||||
// "201":
|
||||
// "$ref": "#/responses/Attachment"
|
||||
|
@ -205,51 +215,96 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
// Get uploaded file from request
|
||||
var content io.ReadCloser
|
||||
var filename string
|
||||
var size int64 = -1
|
||||
var isForm, hasAttachmentFile, hasExternalURL bool
|
||||
externalURL := ctx.FormString("external_url")
|
||||
hasExternalURL = externalURL != ""
|
||||
filename := ctx.FormString("name")
|
||||
isForm = strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data")
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
|
||||
file, header, err := ctx.Req.FormFile("attachment")
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetFile", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content = file
|
||||
size = header.Size
|
||||
filename = header.Filename
|
||||
if name := ctx.FormString("name"); name != "" {
|
||||
filename = name
|
||||
}
|
||||
if isForm {
|
||||
_, _, err := ctx.Req.FormFile("attachment")
|
||||
hasAttachmentFile = err == nil
|
||||
} else {
|
||||
content = ctx.Req.Body
|
||||
filename = ctx.FormString("name")
|
||||
hasAttachmentFile = ctx.Req.Body != nil
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
|
||||
return
|
||||
}
|
||||
if hasAttachmentFile && hasExternalURL {
|
||||
ctx.Error(http.StatusBadRequest, "DuplicateAttachment", "'attachment' and 'external_url' are mutually exclusive")
|
||||
} else if hasAttachmentFile {
|
||||
var content io.ReadCloser
|
||||
var size int64 = -1
|
||||
|
||||
// Create a new attachment and save the file
|
||||
attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
ReleaseID: releaseID,
|
||||
})
|
||||
if err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.Error(http.StatusBadRequest, "DetectContentType", err)
|
||||
if isForm {
|
||||
var header *multipart.FileHeader
|
||||
content, header, _ = ctx.Req.FormFile("attachment")
|
||||
size = header.Size
|
||||
defer content.Close()
|
||||
if filename == "" {
|
||||
filename = header.Filename
|
||||
}
|
||||
} else {
|
||||
content = ctx.Req.Body
|
||||
defer content.Close()
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
ctx.Error(http.StatusBadRequest, "MissingName", "Missing 'name' parameter")
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "NewAttachment", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
// Create a new attachment and save the file
|
||||
attach, err := attachment.UploadAttachment(ctx, content, setting.Repository.Release.AllowedTypes, size, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
ReleaseID: releaseID,
|
||||
})
|
||||
if err != nil {
|
||||
if upload.IsErrFileTypeForbidden(err) {
|
||||
ctx.Error(http.StatusBadRequest, "DetectContentType", err)
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "NewAttachment", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
} else if hasExternalURL {
|
||||
url, err := url.Parse(externalURL)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusBadRequest, "InvalidExternalURL", err)
|
||||
return
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
filename = path.Base(url.Path)
|
||||
|
||||
if filename == "." {
|
||||
// Url path is empty
|
||||
filename = url.Host
|
||||
}
|
||||
}
|
||||
|
||||
attach, err := attachment.NewExternalAttachment(ctx, &repo_model.Attachment{
|
||||
Name: filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
ReleaseID: releaseID,
|
||||
ExternalURL: url.String(),
|
||||
})
|
||||
if err != nil {
|
||||
if repo_model.IsErrInvalidExternalURL(err) {
|
||||
ctx.Error(http.StatusBadRequest, "NewExternalAttachment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "NewExternalAttachment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
} else {
|
||||
ctx.Error(http.StatusBadRequest, "MissingAttachment", "One of 'attachment' or 'external_url' is required")
|
||||
}
|
||||
}
|
||||
|
||||
// EditReleaseAttachment updates the given attachment
|
||||
|
@ -322,8 +377,21 @@ func EditReleaseAttachment(ctx *context.APIContext) {
|
|||
attach.Name = form.Name
|
||||
}
|
||||
|
||||
if form.DownloadURL != "" {
|
||||
if attach.ExternalURL == "" {
|
||||
ctx.Error(http.StatusBadRequest, "EditAttachment", "existing attachment is not external")
|
||||
return
|
||||
}
|
||||
attach.ExternalURL = form.DownloadURL
|
||||
}
|
||||
|
||||
if err := repo_model.UpdateAttachment(ctx, attach); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", attach)
|
||||
if repo_model.IsErrInvalidExternalURL(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UpdateAttachment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAttachment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
||||
}
|
||||
|
|
|
@ -122,6 +122,11 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
|||
}
|
||||
}
|
||||
|
||||
if attach.ExternalURL != "" {
|
||||
ctx.Redirect(attach.ExternalURL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := attach.IncreaseDownloadCount(ctx); err != nil {
|
||||
ctx.ServerError("IncreaseDownloadCount", err)
|
||||
return
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -491,9 +492,44 @@ func NewReleasePost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
var attachmentUUIDs []string
|
||||
attachmentChanges := make(container.Set[*releaseservice.AttachmentChange])
|
||||
attachmentChangesByID := make(map[string]*releaseservice.AttachmentChange)
|
||||
|
||||
if setting.Attachment.Enabled {
|
||||
attachmentUUIDs = form.Files
|
||||
for _, uuid := range form.Files {
|
||||
attachmentChanges.Add(&releaseservice.AttachmentChange{
|
||||
Action: "add",
|
||||
Type: "attachment",
|
||||
UUID: uuid,
|
||||
})
|
||||
}
|
||||
|
||||
const namePrefix = "attachment-new-name-"
|
||||
const exturlPrefix = "attachment-new-exturl-"
|
||||
for k, v := range ctx.Req.Form {
|
||||
isNewName := strings.HasPrefix(k, namePrefix)
|
||||
isNewExturl := strings.HasPrefix(k, exturlPrefix)
|
||||
if isNewName || isNewExturl {
|
||||
var id string
|
||||
if isNewName {
|
||||
id = k[len(namePrefix):]
|
||||
} else if isNewExturl {
|
||||
id = k[len(exturlPrefix):]
|
||||
}
|
||||
if _, ok := attachmentChangesByID[id]; !ok {
|
||||
attachmentChangesByID[id] = &releaseservice.AttachmentChange{
|
||||
Action: "add",
|
||||
Type: "external",
|
||||
}
|
||||
attachmentChanges.Add(attachmentChangesByID[id])
|
||||
}
|
||||
if isNewName {
|
||||
attachmentChangesByID[id].Name = v[0]
|
||||
} else if isNewExturl {
|
||||
attachmentChangesByID[id].ExternalURL = v[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
|
||||
|
@ -553,7 +589,7 @@ func NewReleasePost(ctx *context.Context) {
|
|||
IsTag: false,
|
||||
}
|
||||
|
||||
if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, attachmentUUIDs, msg); err != nil {
|
||||
if err = releaseservice.CreateRelease(ctx.Repo.GitRepo, rel, msg, attachmentChanges.Values()); err != nil {
|
||||
ctx.Data["Err_TagName"] = true
|
||||
switch {
|
||||
case repo_model.IsErrReleaseAlreadyExist(err):
|
||||
|
@ -562,6 +598,8 @@ func NewReleasePost(ctx *context.Context) {
|
|||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
|
||||
case models.IsErrProtectedTagName(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
|
||||
case repo_model.IsErrInvalidExternalURL(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.invalid_external_url", err.(repo_model.ErrInvalidExternalURL).ExternalURL), tplReleaseNew, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateRelease", err)
|
||||
}
|
||||
|
@ -583,9 +621,14 @@ func NewReleasePost(ctx *context.Context) {
|
|||
rel.HideArchiveLinks = form.HideArchiveLinks
|
||||
rel.IsTag = false
|
||||
|
||||
if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, attachmentUUIDs, nil, nil, true); err != nil {
|
||||
if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, true, attachmentChanges.Values()); err != nil {
|
||||
ctx.Data["Err_TagName"] = true
|
||||
ctx.ServerError("UpdateRelease", err)
|
||||
switch {
|
||||
case repo_model.IsErrInvalidExternalURL(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.invalid_external_url", err.(repo_model.ErrInvalidExternalURL).ExternalURL), tplReleaseNew, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateRelease", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -667,6 +710,15 @@ func EditReleasePost(ctx *context.Context) {
|
|||
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||
ctx.Data["hide_archive_links"] = rel.HideArchiveLinks
|
||||
|
||||
rel.Repo = ctx.Repo.Repository
|
||||
if err := rel.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
// TODO: If an error occurs, do not forget the attachment edits the user made
|
||||
// when displaying the error message.
|
||||
ctx.Data["attachments"] = rel.Attachments
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplReleaseNew)
|
||||
return
|
||||
|
@ -674,15 +726,67 @@ func EditReleasePost(ctx *context.Context) {
|
|||
|
||||
const delPrefix = "attachment-del-"
|
||||
const editPrefix = "attachment-edit-"
|
||||
var addAttachmentUUIDs, delAttachmentUUIDs []string
|
||||
editAttachments := make(map[string]string) // uuid -> new name
|
||||
const newPrefix = "attachment-new-"
|
||||
const namePrefix = "name-"
|
||||
const exturlPrefix = "exturl-"
|
||||
attachmentChanges := make(container.Set[*releaseservice.AttachmentChange])
|
||||
attachmentChangesByID := make(map[string]*releaseservice.AttachmentChange)
|
||||
|
||||
if setting.Attachment.Enabled {
|
||||
addAttachmentUUIDs = form.Files
|
||||
for _, uuid := range form.Files {
|
||||
attachmentChanges.Add(&releaseservice.AttachmentChange{
|
||||
Action: "add",
|
||||
Type: "attachment",
|
||||
UUID: uuid,
|
||||
})
|
||||
}
|
||||
|
||||
for k, v := range ctx.Req.Form {
|
||||
if strings.HasPrefix(k, delPrefix) && v[0] == "true" {
|
||||
delAttachmentUUIDs = append(delAttachmentUUIDs, k[len(delPrefix):])
|
||||
} else if strings.HasPrefix(k, editPrefix) {
|
||||
editAttachments[k[len(editPrefix):]] = v[0]
|
||||
attachmentChanges.Add(&releaseservice.AttachmentChange{
|
||||
Action: "delete",
|
||||
UUID: k[len(delPrefix):],
|
||||
})
|
||||
} else {
|
||||
isUpdatedName := strings.HasPrefix(k, editPrefix+namePrefix)
|
||||
isUpdatedExturl := strings.HasPrefix(k, editPrefix+exturlPrefix)
|
||||
isNewName := strings.HasPrefix(k, newPrefix+namePrefix)
|
||||
isNewExturl := strings.HasPrefix(k, newPrefix+exturlPrefix)
|
||||
|
||||
if isUpdatedName || isUpdatedExturl || isNewName || isNewExturl {
|
||||
var uuid string
|
||||
|
||||
if isUpdatedName {
|
||||
uuid = k[len(editPrefix+namePrefix):]
|
||||
} else if isUpdatedExturl {
|
||||
uuid = k[len(editPrefix+exturlPrefix):]
|
||||
} else if isNewName {
|
||||
uuid = k[len(newPrefix+namePrefix):]
|
||||
} else if isNewExturl {
|
||||
uuid = k[len(newPrefix+exturlPrefix):]
|
||||
}
|
||||
|
||||
if _, ok := attachmentChangesByID[uuid]; !ok {
|
||||
attachmentChangesByID[uuid] = &releaseservice.AttachmentChange{
|
||||
Type: "attachment",
|
||||
UUID: uuid,
|
||||
}
|
||||
attachmentChanges.Add(attachmentChangesByID[uuid])
|
||||
}
|
||||
|
||||
if isUpdatedName || isUpdatedExturl {
|
||||
attachmentChangesByID[uuid].Action = "update"
|
||||
} else if isNewName || isNewExturl {
|
||||
attachmentChangesByID[uuid].Action = "add"
|
||||
}
|
||||
|
||||
if isUpdatedName || isNewName {
|
||||
attachmentChangesByID[uuid].Name = v[0]
|
||||
} else if isUpdatedExturl || isNewExturl {
|
||||
attachmentChangesByID[uuid].ExternalURL = v[0]
|
||||
attachmentChangesByID[uuid].Type = "external"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -692,9 +796,13 @@ func EditReleasePost(ctx *context.Context) {
|
|||
rel.IsDraft = len(form.Draft) > 0
|
||||
rel.IsPrerelease = form.Prerelease
|
||||
rel.HideArchiveLinks = form.HideArchiveLinks
|
||||
if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo,
|
||||
rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments, false); err != nil {
|
||||
ctx.ServerError("UpdateRelease", err)
|
||||
if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, false, attachmentChanges.Values()); err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrInvalidExternalURL(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.invalid_external_url", err.(repo_model.ErrInvalidExternalURL).ExternalURL), tplReleaseNew, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateRelease", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
@ -43,6 +44,28 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
|
|||
return attach, err
|
||||
}
|
||||
|
||||
func NewExternalAttachment(ctx context.Context, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
|
||||
if attach.RepoID == 0 {
|
||||
return nil, fmt.Errorf("attachment %s should belong to a repository", attach.Name)
|
||||
}
|
||||
if attach.ExternalURL == "" {
|
||||
return nil, fmt.Errorf("attachment %s should have a external url", attach.Name)
|
||||
}
|
||||
if !validation.IsValidExternalURL(attach.ExternalURL) {
|
||||
return nil, repo_model.ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
|
||||
}
|
||||
|
||||
attach.UUID = uuid.New().String()
|
||||
|
||||
eng := db.GetEngine(ctx)
|
||||
if attach.NoAutoTime {
|
||||
eng.NoAutoTime()
|
||||
}
|
||||
_, err := eng.Insert(attach)
|
||||
|
||||
return attach, err
|
||||
}
|
||||
|
||||
// UploadAttachment upload new attachment into storage and update database
|
||||
func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
|
||||
buf := make([]byte, 1024)
|
||||
|
|
|
@ -9,6 +9,10 @@ import (
|
|||
)
|
||||
|
||||
func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
|
||||
if attach.ExternalURL != "" {
|
||||
return attach.ExternalURL
|
||||
}
|
||||
|
||||
return attach.DownloadURL()
|
||||
}
|
||||
|
||||
|
@ -28,6 +32,12 @@ func ToAPIAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api
|
|||
|
||||
// toAttachment converts models.Attachment to api.Attachment for API usage
|
||||
func toAttachment(repo *repo_model.Repository, a *repo_model.Attachment, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Attachment {
|
||||
var typeName string
|
||||
if a.ExternalURL != "" {
|
||||
typeName = "external"
|
||||
} else {
|
||||
typeName = "attachment"
|
||||
}
|
||||
return &api.Attachment{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
|
@ -36,6 +46,7 @@ func toAttachment(repo *repo_model.Repository, a *repo_model.Attachment, getDown
|
|||
Size: a.Size,
|
||||
UUID: a.UUID,
|
||||
DownloadURL: getDownloadURL(repo, a), // for web request json and api request json, return different download urls
|
||||
Type: typeName,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ func (o *release) Put(ctx context.Context) generic.NodeID {
|
|||
panic(err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
if err := release_service.CreateRelease(gitRepo, o.forgejoRelease, nil, ""); err != nil {
|
||||
if err := release_service.CreateRelease(gitRepo, o.forgejoRelease, "", nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
o.Trace("release created %d", o.forgejoRelease.ID)
|
||||
|
|
|
@ -23,9 +23,18 @@ import (
|
|||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
type AttachmentChange struct {
|
||||
Action string // "add", "delete", "update
|
||||
Type string // "attachment", "external"
|
||||
UUID string
|
||||
Name string
|
||||
ExternalURL string
|
||||
}
|
||||
|
||||
func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) {
|
||||
err := rel.LoadAttributes(ctx)
|
||||
if err != nil {
|
||||
|
@ -128,7 +137,7 @@ func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Rel
|
|||
}
|
||||
|
||||
// CreateRelease creates a new release of repository.
|
||||
func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error {
|
||||
func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, msg string, attachmentChanges []*AttachmentChange) error {
|
||||
has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -147,7 +156,42 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
|||
return err
|
||||
}
|
||||
|
||||
if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil {
|
||||
addAttachmentUUIDs := make(container.Set[string])
|
||||
|
||||
for _, attachmentChange := range attachmentChanges {
|
||||
if attachmentChange.Action != "add" {
|
||||
return fmt.Errorf("can only create new attachments when creating release")
|
||||
}
|
||||
switch attachmentChange.Type {
|
||||
case "attachment":
|
||||
if attachmentChange.UUID == "" {
|
||||
return fmt.Errorf("new attachment should have a uuid")
|
||||
}
|
||||
addAttachmentUUIDs.Add(attachmentChange.UUID)
|
||||
case "external":
|
||||
if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" {
|
||||
return fmt.Errorf("new external attachment should have a name and external url")
|
||||
}
|
||||
|
||||
_, err = attachment.NewExternalAttachment(gitRepo.Ctx, &repo_model.Attachment{
|
||||
Name: attachmentChange.Name,
|
||||
UploaderID: rel.PublisherID,
|
||||
RepoID: rel.RepoID,
|
||||
ReleaseID: rel.ID,
|
||||
ExternalURL: attachmentChange.ExternalURL,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if attachmentChange.Type == "" {
|
||||
return fmt.Errorf("missing attachment type")
|
||||
}
|
||||
return fmt.Errorf("unknown attachment type: '%q'", attachmentChange.Type)
|
||||
}
|
||||
}
|
||||
|
||||
if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, addAttachmentUUIDs.Values()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -198,8 +242,7 @@ func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.R
|
|||
// addAttachmentUUIDs accept a slice of new created attachments' uuids which will be reassigned release_id as the created release
|
||||
// delAttachmentUUIDs accept a slice of attachments' uuids which will be deleted from the release
|
||||
// editAttachments accept a map of attachment uuid to new attachment name which will be updated with attachments.
|
||||
func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release,
|
||||
addAttachmentUUIDs, delAttachmentUUIDs []string, editAttachments map[string]string, createdFromTag bool,
|
||||
func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release, createdFromTag bool, attachmentChanges []*AttachmentChange,
|
||||
) error {
|
||||
if rel.ID == 0 {
|
||||
return errors.New("UpdateRelease only accepts an exist release")
|
||||
|
@ -220,14 +263,64 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
|
|||
return err
|
||||
}
|
||||
|
||||
if err = repo_model.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs); err != nil {
|
||||
addAttachmentUUIDs := make(container.Set[string])
|
||||
delAttachmentUUIDs := make(container.Set[string])
|
||||
updateAttachmentUUIDs := make(container.Set[string])
|
||||
updateAttachments := make(container.Set[*AttachmentChange])
|
||||
|
||||
for _, attachmentChange := range attachmentChanges {
|
||||
switch attachmentChange.Action {
|
||||
case "add":
|
||||
switch attachmentChange.Type {
|
||||
case "attachment":
|
||||
if attachmentChange.UUID == "" {
|
||||
return fmt.Errorf("new attachment should have a uuid (%s)}", attachmentChange.Name)
|
||||
}
|
||||
addAttachmentUUIDs.Add(attachmentChange.UUID)
|
||||
case "external":
|
||||
if attachmentChange.Name == "" || attachmentChange.ExternalURL == "" {
|
||||
return fmt.Errorf("new external attachment should have a name and external url")
|
||||
}
|
||||
_, err := attachment.NewExternalAttachment(ctx, &repo_model.Attachment{
|
||||
Name: attachmentChange.Name,
|
||||
UploaderID: doer.ID,
|
||||
RepoID: rel.RepoID,
|
||||
ReleaseID: rel.ID,
|
||||
ExternalURL: attachmentChange.ExternalURL,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
if attachmentChange.Type == "" {
|
||||
return fmt.Errorf("missing attachment type")
|
||||
}
|
||||
return fmt.Errorf("unknown attachment type: %q", attachmentChange.Type)
|
||||
}
|
||||
case "delete":
|
||||
if attachmentChange.UUID == "" {
|
||||
return fmt.Errorf("attachment deletion should have a uuid")
|
||||
}
|
||||
delAttachmentUUIDs.Add(attachmentChange.UUID)
|
||||
case "update":
|
||||
updateAttachmentUUIDs.Add(attachmentChange.UUID)
|
||||
updateAttachments.Add(attachmentChange)
|
||||
default:
|
||||
if attachmentChange.Action == "" {
|
||||
return fmt.Errorf("missing attachment action")
|
||||
}
|
||||
return fmt.Errorf("unknown attachment action: %q", attachmentChange.Action)
|
||||
}
|
||||
}
|
||||
|
||||
if err = repo_model.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs.Values()); err != nil {
|
||||
return fmt.Errorf("AddReleaseAttachments: %w", err)
|
||||
}
|
||||
|
||||
deletedUUIDs := make(container.Set[string])
|
||||
if len(delAttachmentUUIDs) > 0 {
|
||||
// Check attachments
|
||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs)
|
||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs.Values())
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", delAttachmentUUIDs, err)
|
||||
}
|
||||
|
@ -246,15 +339,11 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
|
|||
}
|
||||
}
|
||||
|
||||
if len(editAttachments) > 0 {
|
||||
updateAttachmentsList := make([]string, 0, len(editAttachments))
|
||||
for k := range editAttachments {
|
||||
updateAttachmentsList = append(updateAttachmentsList, k)
|
||||
}
|
||||
if len(updateAttachmentUUIDs) > 0 {
|
||||
// Check attachments
|
||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentsList)
|
||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentUUIDs.Values())
|
||||
if err != nil {
|
||||
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", updateAttachmentsList, err)
|
||||
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", updateAttachmentUUIDs, err)
|
||||
}
|
||||
for _, attach := range attachments {
|
||||
if attach.ReleaseID != rel.ID {
|
||||
|
@ -264,15 +353,16 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for uuid, newName := range editAttachments {
|
||||
if !deletedUUIDs.Contains(uuid) {
|
||||
if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
|
||||
UUID: uuid,
|
||||
Name: newName,
|
||||
}, "name"); err != nil {
|
||||
return err
|
||||
}
|
||||
for attachmentChange := range updateAttachments {
|
||||
if !deletedUUIDs.Contains(attachmentChange.UUID) {
|
||||
if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
|
||||
UUID: attachmentChange.UUID,
|
||||
Name: attachmentChange.Name,
|
||||
ExternalURL: attachmentChange.ExternalURL,
|
||||
}, "name", "external_url"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +371,7 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo
|
|||
return err
|
||||
}
|
||||
|
||||
for _, uuid := range delAttachmentUUIDs {
|
||||
for _, uuid := range delAttachmentUUIDs.Values() {
|
||||
if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(uuid)); err != nil {
|
||||
// Even delete files failed, but the attachments has been removed from database, so we
|
||||
// should not return error but only record the error on logs.
|
||||
|
|
|
@ -47,7 +47,7 @@ func TestRelease_Create(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||
RepoID: repo.ID,
|
||||
|
@ -61,7 +61,7 @@ func TestRelease_Create(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||
RepoID: repo.ID,
|
||||
|
@ -75,7 +75,7 @@ func TestRelease_Create(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||
RepoID: repo.ID,
|
||||
|
@ -89,7 +89,7 @@ func TestRelease_Create(t *testing.T) {
|
|||
IsDraft: true,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
|
||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||
RepoID: repo.ID,
|
||||
|
@ -103,7 +103,7 @@ func TestRelease_Create(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: true,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
|
||||
testPlayload := "testtest"
|
||||
|
||||
|
@ -127,7 +127,67 @@ func TestRelease_Create(t *testing.T) {
|
|||
IsPrerelease: false,
|
||||
IsTag: true,
|
||||
}
|
||||
assert.NoError(t, CreateRelease(gitRepo, &release, []string{attach.UUID}, "test"))
|
||||
assert.NoError(t, CreateRelease(gitRepo, &release, "test", []*AttachmentChange{
|
||||
{
|
||||
Action: "add",
|
||||
Type: "attachment",
|
||||
UUID: attach.UUID,
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, &release))
|
||||
assert.Len(t, release.Attachments, 1)
|
||||
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
||||
assert.EqualValues(t, attach.Name, release.Attachments[0].Name)
|
||||
assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
|
||||
|
||||
release = repo_model.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.6",
|
||||
Target: "65f1bf2",
|
||||
Title: "v0.1.6 is released",
|
||||
Note: "v0.1.6 is released",
|
||||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: true,
|
||||
}
|
||||
assert.NoError(t, CreateRelease(gitRepo, &release, "", []*AttachmentChange{
|
||||
{
|
||||
Action: "add",
|
||||
Type: "external",
|
||||
Name: "test",
|
||||
ExternalURL: "https://forgejo.org/",
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, &release))
|
||||
assert.Len(t, release.Attachments, 1)
|
||||
assert.EqualValues(t, "test", release.Attachments[0].Name)
|
||||
assert.EqualValues(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
|
||||
|
||||
release = repo_model.Release{
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
PublisherID: user.ID,
|
||||
Publisher: user,
|
||||
TagName: "v0.1.7",
|
||||
Target: "65f1bf2",
|
||||
Title: "v0.1.7 is released",
|
||||
Note: "v0.1.7 is released",
|
||||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: true,
|
||||
}
|
||||
assert.Error(t, CreateRelease(gitRepo, &repo_model.Release{}, "", []*AttachmentChange{
|
||||
{
|
||||
Action: "add",
|
||||
Type: "external",
|
||||
Name: "Click me",
|
||||
// Invalid URL (API URL of current instance), this should result in an error
|
||||
ExternalURL: "https://try.gitea.io/api/v1/user/follow",
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestRelease_Update(t *testing.T) {
|
||||
|
@ -153,13 +213,13 @@ func TestRelease_Update(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.1.1")
|
||||
assert.NoError(t, err)
|
||||
releaseCreatedUnix := release.CreatedUnix
|
||||
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
||||
release.Note = "Changed note"
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, nil, nil, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
|
||||
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
||||
|
@ -177,13 +237,13 @@ func TestRelease_Update(t *testing.T) {
|
|||
IsDraft: true,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.2.1")
|
||||
assert.NoError(t, err)
|
||||
releaseCreatedUnix = release.CreatedUnix
|
||||
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
||||
release.Title = "Changed title"
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, nil, nil, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
|
||||
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
||||
|
@ -201,14 +261,14 @@ func TestRelease_Update(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: true,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", []*AttachmentChange{}))
|
||||
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.3.1")
|
||||
assert.NoError(t, err)
|
||||
releaseCreatedUnix = release.CreatedUnix
|
||||
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
||||
release.Title = "Changed title"
|
||||
release.Note = "Changed note"
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, nil, nil, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
|
||||
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
||||
|
@ -227,13 +287,13 @@ func TestRelease_Update(t *testing.T) {
|
|||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}
|
||||
assert.NoError(t, CreateRelease(gitRepo, release, nil, ""))
|
||||
assert.NoError(t, CreateRelease(gitRepo, release, "", []*AttachmentChange{}))
|
||||
assert.Greater(t, release.ID, int64(0))
|
||||
|
||||
release.IsDraft = false
|
||||
tagName := release.TagName
|
||||
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, nil, nil, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{}))
|
||||
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tagName, release.TagName)
|
||||
|
@ -247,29 +307,79 @@ func TestRelease_Update(t *testing.T) {
|
|||
}, strings.NewReader(samplePayload), int64(len([]byte(samplePayload))))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, []string{attach.UUID}, nil, nil, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
|
||||
{
|
||||
Action: "add",
|
||||
Type: "attachment",
|
||||
UUID: attach.UUID,
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||
assert.Len(t, release.Attachments, 1)
|
||||
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
||||
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
||||
assert.EqualValues(t, attach.Name, release.Attachments[0].Name)
|
||||
assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
|
||||
|
||||
// update the attachment name
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, nil, map[string]string{
|
||||
attach.UUID: "test2.txt",
|
||||
}, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
|
||||
{
|
||||
Action: "update",
|
||||
Name: "test2.txt",
|
||||
UUID: attach.UUID,
|
||||
},
|
||||
}))
|
||||
release.Attachments = nil
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||
assert.Len(t, release.Attachments, 1)
|
||||
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
||||
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
||||
assert.EqualValues(t, "test2.txt", release.Attachments[0].Name)
|
||||
assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
|
||||
|
||||
// delete the attachment
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, []string{attach.UUID}, nil, false))
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
|
||||
{
|
||||
Action: "delete",
|
||||
UUID: attach.UUID,
|
||||
},
|
||||
}))
|
||||
release.Attachments = nil
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||
assert.Empty(t, release.Attachments)
|
||||
|
||||
// Add new external attachment
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
|
||||
{
|
||||
Action: "add",
|
||||
Type: "external",
|
||||
Name: "test",
|
||||
ExternalURL: "https://forgejo.org/",
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||
assert.Len(t, release.Attachments, 1)
|
||||
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
||||
assert.EqualValues(t, "test", release.Attachments[0].Name)
|
||||
assert.EqualValues(t, "https://forgejo.org/", release.Attachments[0].ExternalURL)
|
||||
externalAttachmentUUID := release.Attachments[0].UUID
|
||||
|
||||
// update the attachment name
|
||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
|
||||
{
|
||||
Action: "update",
|
||||
Name: "test2",
|
||||
UUID: externalAttachmentUUID,
|
||||
ExternalURL: "https://about.gitea.com/",
|
||||
},
|
||||
}))
|
||||
release.Attachments = nil
|
||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||
assert.Len(t, release.Attachments, 1)
|
||||
assert.EqualValues(t, externalAttachmentUUID, release.Attachments[0].UUID)
|
||||
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
||||
assert.EqualValues(t, "test2", release.Attachments[0].Name)
|
||||
assert.EqualValues(t, "https://about.gitea.com/", release.Attachments[0].ExternalURL)
|
||||
}
|
||||
|
||||
func TestRelease_createTag(t *testing.T) {
|
||||
|
|
|
@ -72,7 +72,9 @@
|
|||
<ul class="list">
|
||||
{{if $hasArchiveLinks}}
|
||||
<li>
|
||||
<a class="archive-link tw-flex-1" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong></a>
|
||||
<a class="archive-link tw-flex-1 flex-text-inline tw-font-bold" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow">
|
||||
{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)
|
||||
</a>
|
||||
<div class="tw-mr-1">
|
||||
<span class="text grey">{{ctx.Locale.TrN .Release.ArchiveDownloadCount.Zip "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .Release.ArchiveDownloadCount.Zip)}}</span>
|
||||
</div>
|
||||
|
@ -81,7 +83,9 @@
|
|||
</span>
|
||||
</li>
|
||||
<li class="{{if $hasReleaseAttachment}}start-gap{{end}}">
|
||||
<a class="archive-link tw-flex-1" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong></a>
|
||||
<a class="archive-link tw-flex-1 flex-text-inline tw-font-bold" href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">
|
||||
{{svg "octicon-file-zip" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)
|
||||
</a>
|
||||
<div class="tw-mr-1">
|
||||
<span class="text grey">{{ctx.Locale.TrN .Release.ArchiveDownloadCount.TarGz "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .Release.ArchiveDownloadCount.TarGz)}}</span>
|
||||
</div>
|
||||
|
@ -92,14 +96,22 @@
|
|||
{{if $hasReleaseAttachment}}<hr>{{end}}
|
||||
{{end}}
|
||||
{{range $release.Attachments}}
|
||||
<li>
|
||||
<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
||||
<strong>{{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}</strong>
|
||||
</a>
|
||||
<div>
|
||||
<span class="text grey">{{ctx.Locale.TrN .DownloadCount "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .DownloadCount)}} · {{.Size | ctx.Locale.TrSize}}</span>
|
||||
</div>
|
||||
</li>
|
||||
{{if .ExternalURL}}
|
||||
<li>
|
||||
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
||||
{{svg "octicon-link-external" 16 "tw-mr-1"}}{{.Name}}
|
||||
</a>
|
||||
</li>
|
||||
{{else}}
|
||||
<li>
|
||||
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
||||
{{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}
|
||||
</a>
|
||||
<div>
|
||||
<span class="text grey">{{ctx.Locale.TrN .DownloadCount "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .DownloadCount)}} · {{.Size | ctx.Locale.TrSize}}</span>
|
||||
</div>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</details>
|
||||
|
|
|
@ -63,15 +63,45 @@
|
|||
{{range .attachments}}
|
||||
<div class="field flex-text-block" id="attachment-{{.ID}}">
|
||||
<div class="flex-text-inline tw-flex-1">
|
||||
<input name="attachment-edit-{{.UUID}}" class="attachment_edit" required value="{{.Name}}">
|
||||
<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
|
||||
<span class="ui text grey tw-whitespace-nowrap">{{ctx.Locale.TrN .DownloadCount "repo.release.download_count_one" "repo.release.download_count_few" (ctx.Locale.PrettyNumber .DownloadCount)}} · {{.Size | ctx.Locale.TrSize}}</span>
|
||||
<div class="flex-text-inline tw-shrink-0" title="{{ctx.Locale.Tr "repo.release.type_attachment"}}">
|
||||
{{if .ExternalURL}}
|
||||
{{svg "octicon-link-external" 16 "tw-mr-2"}}
|
||||
{{else}}
|
||||
{{svg "octicon-package" 16 "tw-mr-2"}}
|
||||
{{end}}
|
||||
</div>
|
||||
<input name="attachment-edit-name-{{.UUID}}" placeholder="{{ctx.Locale.Tr "repo.release.asset_name"}}" class="attachment_edit" required value="{{.Name}}">
|
||||
<input name="attachment-del-{{.UUID}}" type="hidden"
|
||||
value="false">
|
||||
{{if .ExternalURL}}
|
||||
<input name="attachment-edit-exturl-{{.UUID}}" placeholder="{{ctx.Locale.Tr "repo.release.asset_external_url"}}" class="attachment_edit" required value="{{.ExternalURL}}">
|
||||
{{else}}
|
||||
<span class="ui text grey tw-whitespace-nowrap tw-ml-auto tw-pl-3">{{ctx.Locale.TrN
|
||||
.DownloadCount "repo.release.download_count_one"
|
||||
"repo.release.download_count_few" (ctx.Locale.PrettyNumber
|
||||
.DownloadCount)}} · {{.Size | ctx.Locale.TrSize}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<a class="ui mini compact red button remove-rel-attach" data-id="{{.ID}}" data-uuid="{{.UUID}}">
|
||||
<a class="ui mini red button remove-rel-attach tw-ml-3" data-id="{{.ID}}" data-uuid="{{.UUID}}">
|
||||
{{ctx.Locale.Tr "remove"}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field flex-text-block tw-hidden" id="attachment-template">
|
||||
<div class="flex-text-inline tw-flex-1">
|
||||
<div class="flex-text-inline tw-shrink-0" title="{{ctx.Locale.Tr "repo.release.type_external_asset"}}">
|
||||
{{svg "octicon-link-external" 16 "tw-mr-2"}}
|
||||
</div>
|
||||
<input name="attachment-template-new-name" placeholder="{{ctx.Locale.Tr "repo.release.asset_name"}}" class="attachment_edit">
|
||||
<input name="attachment-template-new-exturl" placeholder="{{ctx.Locale.Tr "repo.release.asset_external_url"}}" class="attachment_edit">
|
||||
</div>
|
||||
<a class="ui mini red button remove-rel-attach tw-ml-3">
|
||||
{{ctx.Locale.Tr "remove"}}
|
||||
</a>
|
||||
</div>
|
||||
<a class="ui mini button tw-float-right tw-mb-4 tw-mt-2" id="add-external-link">
|
||||
{{ctx.Locale.Tr "repo.release.add_external_asset"}}
|
||||
</a>
|
||||
{{if .IsAttachmentEnabled}}
|
||||
<div class="field">
|
||||
{{template "repo/upload" .}}
|
||||
|
|
21
templates/swagger/v1_json.tmpl
generated
21
templates/swagger/v1_json.tmpl
generated
|
@ -13632,9 +13632,15 @@
|
|||
},
|
||||
{
|
||||
"type": "file",
|
||||
"description": "attachment to upload",
|
||||
"description": "attachment to upload (this parameter is incompatible with `external_url`)",
|
||||
"name": "attachment",
|
||||
"in": "formData"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "url to external asset (this parameter is incompatible with `attachment`)",
|
||||
"name": "external_url",
|
||||
"in": "formData"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -19010,6 +19016,14 @@
|
|||
"format": "int64",
|
||||
"x-go-name": "Size"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"attachment",
|
||||
"external"
|
||||
],
|
||||
"x-go-name": "Type"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"x-go-name": "UUID"
|
||||
|
@ -20988,6 +21002,11 @@
|
|||
"description": "EditAttachmentOptions options for editing attachments",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"browser_download_url": {
|
||||
"description": "(Can only be set if existing attachment is of external type)",
|
||||
"type": "string",
|
||||
"x-go-name": "DownloadURL"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"x-go-name": "Name"
|
||||
|
|
67
tests/e2e/release.test.e2e.js
Normal file
67
tests/e2e/release.test.e2e.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
// @ts-check
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login_user, save_visual, load_logged_in_context} from './utils_e2e.js';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
test.describe.configure({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
test('External Release Attachments', async ({browser, isMobile}, workerInfo) => {
|
||||
test.skip(isMobile);
|
||||
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
// Click "New Release"
|
||||
await page.goto('/user2/repo2/releases');
|
||||
await page.click('.button.small.primary');
|
||||
|
||||
// Fill out form and create new release
|
||||
await page.fill('input[name=tag_name]', '2.0');
|
||||
await page.fill('input[name=title]', '2.0');
|
||||
await page.click('#add-external-link');
|
||||
await page.click('#add-external-link');
|
||||
await page.fill('input[name=attachment-new-name-2]', 'Test');
|
||||
await page.fill('input[name=attachment-new-exturl-2]', 'https://forgejo.org/');
|
||||
await page.click('.remove-rel-attach');
|
||||
save_visual(page);
|
||||
await page.click('.button.small.primary');
|
||||
|
||||
// Validate release page and click edit
|
||||
await expect(page.locator('.download[open] li')).toHaveCount(3);
|
||||
await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test');
|
||||
await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://forgejo.org/');
|
||||
save_visual(page);
|
||||
await page.locator('.octicon-pencil').first().click();
|
||||
|
||||
// Validate edit page and edit the release
|
||||
await expect(page.locator('.attachment_edit:visible')).toHaveCount(2);
|
||||
await expect(page.locator('.attachment_edit:visible').nth(0)).toHaveValue('Test');
|
||||
await expect(page.locator('.attachment_edit:visible').nth(1)).toHaveValue('https://forgejo.org/');
|
||||
await page.locator('.attachment_edit:visible').nth(0).fill('Test2');
|
||||
await page.locator('.attachment_edit:visible').nth(1).fill('https://gitea.io/');
|
||||
await page.click('#add-external-link');
|
||||
await expect(page.locator('.attachment_edit:visible')).toHaveCount(4);
|
||||
await page.locator('.attachment_edit:visible').nth(2).fill('Test3');
|
||||
await page.locator('.attachment_edit:visible').nth(3).fill('https://gitea.com/');
|
||||
save_visual(page);
|
||||
await page.click('.button.small.primary');
|
||||
|
||||
// Validate release page and click edit
|
||||
await expect(page.locator('.download[open] li')).toHaveCount(4);
|
||||
await expect(page.locator('.download[open] li:nth-of-type(3)')).toContainText('Test2');
|
||||
await expect(page.locator('.download[open] li:nth-of-type(3) a')).toHaveAttribute('href', 'https://gitea.io/');
|
||||
await expect(page.locator('.download[open] li:nth-of-type(4)')).toContainText('Test3');
|
||||
await expect(page.locator('.download[open] li:nth-of-type(4) a')).toHaveAttribute('href', 'https://gitea.com/');
|
||||
save_visual(page);
|
||||
await page.locator('.octicon-pencil').first().click();
|
||||
|
||||
// Delete release
|
||||
await page.click('.delete-button');
|
||||
await page.click('.button.ok');
|
||||
});
|
|
@ -347,6 +347,7 @@ func TestAPIUploadAssetRelease(t *testing.T) {
|
|||
|
||||
assert.EqualValues(t, "stream.bin", attachment.Name)
|
||||
assert.EqualValues(t, 104, attachment.Size)
|
||||
assert.EqualValues(t, "attachment", attachment.Type)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -385,3 +386,69 @@ func TestAPIGetReleaseArchiveDownloadCount(t *testing.T) {
|
|||
assert.Equal(t, int64(1), release.ArchiveDownloadCount.TarGz)
|
||||
assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip)
|
||||
}
|
||||
|
||||
func TestAPIExternalAssetRelease(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, owner.LowerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
r := createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test")
|
||||
|
||||
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=https%%3A%%2F%%2Fforgejo.org%%2F", owner.Name, repo.Name, r.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var attachment *api.Attachment
|
||||
DecodeJSON(t, resp, &attachment)
|
||||
|
||||
assert.EqualValues(t, "test-asset", attachment.Name)
|
||||
assert.EqualValues(t, 0, attachment.Size)
|
||||
assert.EqualValues(t, "https://forgejo.org/", attachment.DownloadURL)
|
||||
assert.EqualValues(t, "external", attachment.Type)
|
||||
}
|
||||
|
||||
func TestAPIDuplicateAssetRelease(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, owner.LowerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
r := createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test")
|
||||
|
||||
filename := "image.png"
|
||||
buff := generateImg()
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.Copy(part, &buff)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=https%%3A%%2F%%2Fforgejo.org%%2F", owner.Name, repo.Name, r.ID), body).
|
||||
AddTokenAuth(token)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAPIMissingAssetRelease(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, owner.LowerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
r := createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test")
|
||||
|
||||
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset", owner.Name, repo.Name, r.ID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
}
|
||||
|
|
|
@ -78,7 +78,7 @@ func TestMirrorPull(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: true,
|
||||
}, nil, ""))
|
||||
}, "", []*release_service.AttachmentChange{}))
|
||||
|
||||
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -111,7 +111,7 @@ func TestWebhookReleaseEvents(t *testing.T) {
|
|||
IsDraft: false,
|
||||
IsPrerelease: false,
|
||||
IsTag: false,
|
||||
}, nil, ""))
|
||||
}, "", nil))
|
||||
|
||||
// check the newly created hooktasks
|
||||
hookTasksLenBefore := len(hookTasks)
|
||||
|
@ -125,7 +125,7 @@ func TestWebhookReleaseEvents(t *testing.T) {
|
|||
|
||||
t.Run("UpdateRelease", func(t *testing.T) {
|
||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.1.1"})
|
||||
assert.NoError(t, release.UpdateRelease(db.DefaultContext, user, gitRepo, rel, nil, nil, nil, false))
|
||||
assert.NoError(t, release.UpdateRelease(db.DefaultContext, user, gitRepo, rel, false, nil))
|
||||
|
||||
// check the newly created hooktasks
|
||||
hookTasksLenBefore := len(hookTasks)
|
||||
|
@ -157,7 +157,7 @@ func TestWebhookReleaseEvents(t *testing.T) {
|
|||
|
||||
t.Run("UpdateRelease", func(t *testing.T) {
|
||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.1.2"})
|
||||
assert.NoError(t, release.UpdateRelease(db.DefaultContext, user, gitRepo, rel, nil, nil, nil, true))
|
||||
assert.NoError(t, release.UpdateRelease(db.DefaultContext, user, gitRepo, rel, true, nil))
|
||||
|
||||
// check the newly created hooktasks
|
||||
hookTasksLenBefore := len(hookTasks)
|
||||
|
|
|
@ -6,7 +6,8 @@ export function initRepoRelease() {
|
|||
el.addEventListener('click', (e) => {
|
||||
const uuid = e.target.getAttribute('data-uuid');
|
||||
const id = e.target.getAttribute('data-id');
|
||||
document.querySelector(`input[name='attachment-del-${uuid}']`).value = 'true';
|
||||
document.querySelector(`input[name='attachment-del-${uuid}']`).value =
|
||||
'true';
|
||||
hideElem(`#attachment-${id}`);
|
||||
});
|
||||
}
|
||||
|
@ -17,6 +18,7 @@ export function initRepoReleaseNew() {
|
|||
|
||||
initTagNameEditor();
|
||||
initRepoReleaseEditor();
|
||||
initAddExternalLinkButton();
|
||||
}
|
||||
|
||||
function initTagNameEditor() {
|
||||
|
@ -45,9 +47,49 @@ function initTagNameEditor() {
|
|||
}
|
||||
|
||||
function initRepoReleaseEditor() {
|
||||
const editor = document.querySelector('.repository.new.release .combo-markdown-editor');
|
||||
const editor = document.querySelector(
|
||||
'.repository.new.release .combo-markdown-editor',
|
||||
);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
initComboMarkdownEditor(editor);
|
||||
}
|
||||
|
||||
let newAttachmentCount = 0;
|
||||
|
||||
function initAddExternalLinkButton() {
|
||||
const addExternalLinkButton = document.getElementById('add-external-link');
|
||||
if (!addExternalLinkButton) return;
|
||||
|
||||
addExternalLinkButton.addEventListener('click', () => {
|
||||
newAttachmentCount += 1;
|
||||
const attachmentTemplate = document.getElementById('attachment-template');
|
||||
|
||||
const newAttachment = attachmentTemplate.cloneNode(true);
|
||||
newAttachment.id = `attachment-N${newAttachmentCount}`;
|
||||
newAttachment.classList.remove('tw-hidden');
|
||||
|
||||
const attachmentName = newAttachment.querySelector(
|
||||
'input[name="attachment-template-new-name"]',
|
||||
);
|
||||
attachmentName.name = `attachment-new-name-${newAttachmentCount}`;
|
||||
attachmentName.required = true;
|
||||
|
||||
const attachmentExtUrl = newAttachment.querySelector(
|
||||
'input[name="attachment-template-new-exturl"]',
|
||||
);
|
||||
attachmentExtUrl.name = `attachment-new-exturl-${newAttachmentCount}`;
|
||||
attachmentExtUrl.required = true;
|
||||
|
||||
const attachmentDel = newAttachment.querySelector('.remove-rel-attach');
|
||||
attachmentDel.addEventListener('click', () => {
|
||||
newAttachment.remove();
|
||||
});
|
||||
|
||||
attachmentTemplate.parentNode.insertBefore(
|
||||
newAttachment,
|
||||
attachmentTemplate,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue