Implement external assets
This commit is contained in:
		
					parent
					
						
							
								2e234300a2
							
						
					
				
			
			
				commit
				
					
						a61e7c7a39
					
				
			
		
					 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
									
									
									
								
							| 
						 | 
				
			
			@ -13623,9 +13623,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": {
 | 
			
		||||
| 
						 | 
				
			
			@ -19001,6 +19007,14 @@
 | 
			
		|||
          "format": "int64",
 | 
			
		||||
          "x-go-name": "Size"
 | 
			
		||||
        },
 | 
			
		||||
        "type": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "enum": [
 | 
			
		||||
            "attachment",
 | 
			
		||||
            "external"
 | 
			
		||||
          ],
 | 
			
		||||
          "x-go-name": "Type"
 | 
			
		||||
        },
 | 
			
		||||
        "uuid": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "x-go-name": "UUID"
 | 
			
		||||
| 
						 | 
				
			
			@ -20979,6 +20993,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…
	
	Add table
		Add a link
		
	
		Reference in a new issue