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),
|
NewMigration("Add `normalized_federated_uri` column to `user` table", AddNormalizedFederatedURIToUser),
|
||||||
// v18 -> v19
|
// v18 -> v19
|
||||||
NewMigration("Create the `following_repo` table", CreateFollowingRepoTable),
|
NewMigration("Create the `following_repo` table", CreateFollowingRepoTable),
|
||||||
|
// v19 -> v20
|
||||||
|
NewMigration("Add external_url to attachment table", AddExternalURLColumnToAttachmentTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// 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/storage"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attachment represent a attachment of issue/comment/release.
|
// Attachment represent a attachment of issue/comment/release.
|
||||||
|
@ -31,6 +32,7 @@ type Attachment struct {
|
||||||
NoAutoTime bool `xorm:"-"`
|
NoAutoTime bool `xorm:"-"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||||
CustomDownloadURL string `xorm:"-"`
|
CustomDownloadURL string `xorm:"-"`
|
||||||
|
ExternalURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -59,6 +61,10 @@ func (a *Attachment) RelativePath() string {
|
||||||
|
|
||||||
// DownloadURL returns the download url of the attached file
|
// DownloadURL returns the download url of the attached file
|
||||||
func (a *Attachment) DownloadURL() string {
|
func (a *Attachment) DownloadURL() string {
|
||||||
|
if a.ExternalURL != "" {
|
||||||
|
return a.ExternalURL
|
||||||
|
}
|
||||||
|
|
||||||
if a.CustomDownloadURL != "" {
|
if a.CustomDownloadURL != "" {
|
||||||
return a.CustomDownloadURL
|
return a.CustomDownloadURL
|
||||||
}
|
}
|
||||||
|
@ -86,6 +92,23 @@ func (err ErrAttachmentNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
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
|
// GetAttachmentByID returns attachment by given id
|
||||||
func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) {
|
func GetAttachmentByID(ctx context.Context, id int64) (*Attachment, error) {
|
||||||
attach := &Attachment{}
|
attach := &Attachment{}
|
||||||
|
@ -221,12 +244,18 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str
|
||||||
if attach.UUID == "" {
|
if attach.UUID == "" {
|
||||||
return fmt.Errorf("attachment uuid should be not blank")
|
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)
|
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAttachment updates the given attachment in database
|
// UpdateAttachment updates the given attachment in database
|
||||||
func UpdateAttachment(ctx context.Context, atta *Attachment) error {
|
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")
|
sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count")
|
||||||
if atta.ID != 0 && atta.UUID == "" {
|
if atta.ID != 0 && atta.UUID == "" {
|
||||||
sess = sess.ID(atta.ID)
|
sess = sess.ID(atta.ID)
|
||||||
|
|
|
@ -18,10 +18,14 @@ type Attachment struct {
|
||||||
Created time.Time `json:"created_at"`
|
Created time.Time `json:"created_at"`
|
||||||
UUID string `json:"uuid"`
|
UUID string `json:"uuid"`
|
||||||
DownloadURL string `json:"browser_download_url"`
|
DownloadURL string `json:"browser_download_url"`
|
||||||
|
// Enum: attachment,external
|
||||||
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditAttachmentOptions options for editing attachments
|
// EditAttachmentOptions options for editing attachments
|
||||||
// swagger:model
|
// swagger:model
|
||||||
type EditAttachmentOptions struct {
|
type EditAttachmentOptions struct {
|
||||||
Name string `json:"name"`
|
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.releases_for = Releases for %s
|
||||||
release.tags_for = Tags for %s
|
release.tags_for = Tags for %s
|
||||||
release.system_generated = This attachment is automatically generated.
|
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.name = Branch name
|
||||||
branch.already_exists = A branch named "%s" already exists.
|
branch.already_exists = A branch named "%s" already exists.
|
||||||
|
|
|
@ -247,7 +247,7 @@ func CreateRelease(ctx *context.APIContext) {
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
Repo: ctx.Repo.Repository,
|
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) {
|
if repo_model.IsErrReleaseAlreadyExist(err) {
|
||||||
ctx.Error(http.StatusConflict, "ReleaseAlreadyExist", err)
|
ctx.Error(http.StatusConflict, "ReleaseAlreadyExist", err)
|
||||||
} else if models.IsErrProtectedTagName(err) {
|
} else if models.IsErrProtectedTagName(err) {
|
||||||
|
@ -274,7 +274,7 @@ func CreateRelease(ctx *context.APIContext) {
|
||||||
rel.Publisher = ctx.Doer
|
rel.Publisher = ctx.Doer
|
||||||
rel.Target = form.Target
|
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)
|
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -351,7 +351,7 @@ func EditRelease(ctx *context.APIContext) {
|
||||||
if form.HideArchiveLinks != nil {
|
if form.HideArchiveLinks != nil {
|
||||||
rel.HideArchiveLinks = *form.HideArchiveLinks
|
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)
|
ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,10 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
@ -179,11 +182,18 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||||
// description: name of the attachment
|
// description: name of the attachment
|
||||||
// type: string
|
// type: string
|
||||||
// required: false
|
// 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
|
// - name: attachment
|
||||||
// in: formData
|
// in: formData
|
||||||
// description: attachment to upload
|
// description: attachment to upload (this parameter is incompatible with `external_url`)
|
||||||
// type: file
|
// type: file
|
||||||
// required: false
|
// required: false
|
||||||
|
// - name: external_url
|
||||||
|
// in: formData
|
||||||
|
// description: url to external asset (this parameter is incompatible with `attachment`)
|
||||||
|
// type: string
|
||||||
|
// required: false
|
||||||
// responses:
|
// responses:
|
||||||
// "201":
|
// "201":
|
||||||
// "$ref": "#/responses/Attachment"
|
// "$ref": "#/responses/Attachment"
|
||||||
|
@ -205,31 +215,40 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get uploaded file from request
|
// Get uploaded file from request
|
||||||
|
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 isForm {
|
||||||
|
_, _, err := ctx.Req.FormFile("attachment")
|
||||||
|
hasAttachmentFile = err == nil
|
||||||
|
} else {
|
||||||
|
hasAttachmentFile = ctx.Req.Body != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAttachmentFile && hasExternalURL {
|
||||||
|
ctx.Error(http.StatusBadRequest, "DuplicateAttachment", "'attachment' and 'external_url' are mutually exclusive")
|
||||||
|
} else if hasAttachmentFile {
|
||||||
var content io.ReadCloser
|
var content io.ReadCloser
|
||||||
var filename string
|
|
||||||
var size int64 = -1
|
var size int64 = -1
|
||||||
|
|
||||||
if strings.HasPrefix(strings.ToLower(ctx.Req.Header.Get("Content-Type")), "multipart/form-data") {
|
if isForm {
|
||||||
file, header, err := ctx.Req.FormFile("attachment")
|
var header *multipart.FileHeader
|
||||||
if err != nil {
|
content, header, _ = ctx.Req.FormFile("attachment")
|
||||||
ctx.Error(http.StatusInternalServerError, "GetFile", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
content = file
|
|
||||||
size = header.Size
|
size = header.Size
|
||||||
|
defer content.Close()
|
||||||
|
if filename == "" {
|
||||||
filename = header.Filename
|
filename = header.Filename
|
||||||
if name := ctx.FormString("name"); name != "" {
|
|
||||||
filename = name
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = ctx.Req.Body
|
content = ctx.Req.Body
|
||||||
filename = ctx.FormString("name")
|
defer content.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if filename == "" {
|
if filename == "" {
|
||||||
ctx.Error(http.StatusBadRequest, "CreateReleaseAttachment", "Could not determine name of attachment.")
|
ctx.Error(http.StatusBadRequest, "MissingName", "Missing 'name' parameter")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,6 +269,42 @@ func CreateReleaseAttachment(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusCreated, convert.ToAPIAttachment(ctx.Repo.Repository, attach))
|
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
|
// EditReleaseAttachment updates the given attachment
|
||||||
|
@ -322,8 +377,21 @@ func EditReleaseAttachment(ctx *context.APIContext) {
|
||||||
attach.Name = form.Name
|
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 {
|
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))
|
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 {
|
if err := attach.IncreaseDownloadCount(ctx); err != nil {
|
||||||
ctx.ServerError("IncreaseDownloadCount", err)
|
ctx.ServerError("IncreaseDownloadCount", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -491,9 +492,44 @@ func NewReleasePost(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var attachmentUUIDs []string
|
attachmentChanges := make(container.Set[*releaseservice.AttachmentChange])
|
||||||
|
attachmentChangesByID := make(map[string]*releaseservice.AttachmentChange)
|
||||||
|
|
||||||
if setting.Attachment.Enabled {
|
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)
|
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, form.TagName)
|
||||||
|
@ -553,7 +589,7 @@ func NewReleasePost(ctx *context.Context) {
|
||||||
IsTag: false,
|
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
|
ctx.Data["Err_TagName"] = true
|
||||||
switch {
|
switch {
|
||||||
case repo_model.IsErrReleaseAlreadyExist(err):
|
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)
|
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
|
||||||
case models.IsErrProtectedTagName(err):
|
case models.IsErrProtectedTagName(err):
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
|
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:
|
default:
|
||||||
ctx.ServerError("CreateRelease", err)
|
ctx.ServerError("CreateRelease", err)
|
||||||
}
|
}
|
||||||
|
@ -583,9 +621,14 @@ func NewReleasePost(ctx *context.Context) {
|
||||||
rel.HideArchiveLinks = form.HideArchiveLinks
|
rel.HideArchiveLinks = form.HideArchiveLinks
|
||||||
rel.IsTag = false
|
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.Data["Err_TagName"] = true
|
||||||
|
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)
|
ctx.ServerError("UpdateRelease", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -667,6 +710,15 @@ func EditReleasePost(ctx *context.Context) {
|
||||||
ctx.Data["prerelease"] = rel.IsPrerelease
|
ctx.Data["prerelease"] = rel.IsPrerelease
|
||||||
ctx.Data["hide_archive_links"] = rel.HideArchiveLinks
|
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() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tplReleaseNew)
|
ctx.HTML(http.StatusOK, tplReleaseNew)
|
||||||
return
|
return
|
||||||
|
@ -674,15 +726,67 @@ func EditReleasePost(ctx *context.Context) {
|
||||||
|
|
||||||
const delPrefix = "attachment-del-"
|
const delPrefix = "attachment-del-"
|
||||||
const editPrefix = "attachment-edit-"
|
const editPrefix = "attachment-edit-"
|
||||||
var addAttachmentUUIDs, delAttachmentUUIDs []string
|
const newPrefix = "attachment-new-"
|
||||||
editAttachments := make(map[string]string) // uuid -> new name
|
const namePrefix = "name-"
|
||||||
|
const exturlPrefix = "exturl-"
|
||||||
|
attachmentChanges := make(container.Set[*releaseservice.AttachmentChange])
|
||||||
|
attachmentChangesByID := make(map[string]*releaseservice.AttachmentChange)
|
||||||
|
|
||||||
if setting.Attachment.Enabled {
|
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 {
|
for k, v := range ctx.Req.Form {
|
||||||
if strings.HasPrefix(k, delPrefix) && v[0] == "true" {
|
if strings.HasPrefix(k, delPrefix) && v[0] == "true" {
|
||||||
delAttachmentUUIDs = append(delAttachmentUUIDs, k[len(delPrefix):])
|
attachmentChanges.Add(&releaseservice.AttachmentChange{
|
||||||
} else if strings.HasPrefix(k, editPrefix) {
|
Action: "delete",
|
||||||
editAttachments[k[len(editPrefix):]] = v[0]
|
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.IsDraft = len(form.Draft) > 0
|
||||||
rel.IsPrerelease = form.Prerelease
|
rel.IsPrerelease = form.Prerelease
|
||||||
rel.HideArchiveLinks = form.HideArchiveLinks
|
rel.HideArchiveLinks = form.HideArchiveLinks
|
||||||
if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo,
|
if err = releaseservice.UpdateRelease(ctx, ctx.Doer, ctx.Repo.GitRepo, rel, false, attachmentChanges.Values()); err != nil {
|
||||||
rel, addAttachmentUUIDs, delAttachmentUUIDs, editAttachments, false); 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)
|
ctx.ServerError("UpdateRelease", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
ctx.Redirect(ctx.Repo.RepoLink + "/releases")
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/modules/validation"
|
||||||
"code.gitea.io/gitea/services/context/upload"
|
"code.gitea.io/gitea/services/context/upload"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -43,6 +44,28 @@ func NewAttachment(ctx context.Context, attach *repo_model.Attachment, file io.R
|
||||||
return attach, err
|
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
|
// 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) {
|
func UploadAttachment(ctx context.Context, file io.Reader, allowedTypes string, fileSize int64, attach *repo_model.Attachment) (*repo_model.Attachment, error) {
|
||||||
buf := make([]byte, 1024)
|
buf := make([]byte, 1024)
|
||||||
|
|
|
@ -9,6 +9,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
|
func WebAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachment) string {
|
||||||
|
if attach.ExternalURL != "" {
|
||||||
|
return attach.ExternalURL
|
||||||
|
}
|
||||||
|
|
||||||
return attach.DownloadURL()
|
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
|
// 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 {
|
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{
|
return &api.Attachment{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
Name: a.Name,
|
Name: a.Name,
|
||||||
|
@ -36,6 +46,7 @@ func toAttachment(repo *repo_model.Repository, a *repo_model.Attachment, getDown
|
||||||
Size: a.Size,
|
Size: a.Size,
|
||||||
UUID: a.UUID,
|
UUID: a.UUID,
|
||||||
DownloadURL: getDownloadURL(repo, a), // for web request json and api request json, return different download urls
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
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)
|
panic(err)
|
||||||
}
|
}
|
||||||
o.Trace("release created %d", o.forgejoRelease.ID)
|
o.Trace("release created %d", o.forgejoRelease.ID)
|
||||||
|
|
|
@ -23,9 +23,18 @@ import (
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
"code.gitea.io/gitea/services/attachment"
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
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) {
|
func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) {
|
||||||
err := rel.LoadAttributes(ctx)
|
err := rel.LoadAttributes(ctx)
|
||||||
if err != nil {
|
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.
|
// 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)
|
has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -147,7 +156,42 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU
|
||||||
return err
|
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
|
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
|
// 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
|
// 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.
|
// 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,
|
func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release, createdFromTag bool, attachmentChanges []*AttachmentChange,
|
||||||
addAttachmentUUIDs, delAttachmentUUIDs []string, editAttachments map[string]string, createdFromTag bool,
|
|
||||||
) error {
|
) error {
|
||||||
if rel.ID == 0 {
|
if rel.ID == 0 {
|
||||||
return errors.New("UpdateRelease only accepts an exist release")
|
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
|
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)
|
return fmt.Errorf("AddReleaseAttachments: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deletedUUIDs := make(container.Set[string])
|
deletedUUIDs := make(container.Set[string])
|
||||||
if len(delAttachmentUUIDs) > 0 {
|
if len(delAttachmentUUIDs) > 0 {
|
||||||
// Check attachments
|
// Check attachments
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs)
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs.Values())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", delAttachmentUUIDs, err)
|
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 {
|
if len(updateAttachmentUUIDs) > 0 {
|
||||||
updateAttachmentsList := make([]string, 0, len(editAttachments))
|
|
||||||
for k := range editAttachments {
|
|
||||||
updateAttachmentsList = append(updateAttachmentsList, k)
|
|
||||||
}
|
|
||||||
// Check attachments
|
// Check attachments
|
||||||
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentsList)
|
attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentUUIDs.Values())
|
||||||
if err != nil {
|
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 {
|
for _, attach := range attachments {
|
||||||
if attach.ReleaseID != rel.ID {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, uuid := range delAttachmentUUIDs {
|
for _, uuid := range delAttachmentUUIDs.Values() {
|
||||||
if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(uuid)); err != nil {
|
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
|
// 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.
|
// should not return error but only record the error on logs.
|
||||||
|
|
|
@ -47,7 +47,7 @@ func TestRelease_Create(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
|
|
||||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -61,7 +61,7 @@ func TestRelease_Create(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
|
|
||||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -75,7 +75,7 @@ func TestRelease_Create(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
|
|
||||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -89,7 +89,7 @@ func TestRelease_Create(t *testing.T) {
|
||||||
IsDraft: true,
|
IsDraft: true,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
|
|
||||||
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
assert.NoError(t, CreateRelease(gitRepo, &repo_model.Release{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
|
@ -103,7 +103,7 @@ func TestRelease_Create(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: true,
|
IsPrerelease: true,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
|
|
||||||
testPlayload := "testtest"
|
testPlayload := "testtest"
|
||||||
|
|
||||||
|
@ -127,7 +127,67 @@ func TestRelease_Create(t *testing.T) {
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: true,
|
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) {
|
func TestRelease_Update(t *testing.T) {
|
||||||
|
@ -153,13 +213,13 @@ func TestRelease_Update(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.1.1")
|
release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.1.1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
releaseCreatedUnix := release.CreatedUnix
|
releaseCreatedUnix := release.CreatedUnix
|
||||||
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
||||||
release.Note = "Changed note"
|
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)
|
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
||||||
|
@ -177,13 +237,13 @@ func TestRelease_Update(t *testing.T) {
|
||||||
IsDraft: true,
|
IsDraft: true,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.2.1")
|
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.2.1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
releaseCreatedUnix = release.CreatedUnix
|
releaseCreatedUnix = release.CreatedUnix
|
||||||
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
||||||
release.Title = "Changed title"
|
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)
|
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
assert.Less(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
||||||
|
@ -201,14 +261,14 @@ func TestRelease_Update(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: true,
|
IsPrerelease: true,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", []*AttachmentChange{}))
|
||||||
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.3.1")
|
release, err = repo_model.GetRelease(db.DefaultContext, repo.ID, "v1.3.1")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
releaseCreatedUnix = release.CreatedUnix
|
releaseCreatedUnix = release.CreatedUnix
|
||||||
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
time.Sleep(2 * time.Second) // sleep 2 seconds to ensure a different timestamp
|
||||||
release.Title = "Changed title"
|
release.Title = "Changed title"
|
||||||
release.Note = "Changed note"
|
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)
|
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
assert.Equal(t, int64(releaseCreatedUnix), int64(release.CreatedUnix))
|
||||||
|
@ -227,13 +287,13 @@ func TestRelease_Update(t *testing.T) {
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}
|
}
|
||||||
assert.NoError(t, CreateRelease(gitRepo, release, nil, ""))
|
assert.NoError(t, CreateRelease(gitRepo, release, "", []*AttachmentChange{}))
|
||||||
assert.Greater(t, release.ID, int64(0))
|
assert.Greater(t, release.ID, int64(0))
|
||||||
|
|
||||||
release.IsDraft = false
|
release.IsDraft = false
|
||||||
tagName := release.TagName
|
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)
|
release, err = repo_model.GetReleaseByID(db.DefaultContext, release.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, tagName, release.TagName)
|
assert.Equal(t, tagName, release.TagName)
|
||||||
|
@ -247,29 +307,79 @@ func TestRelease_Update(t *testing.T) {
|
||||||
}, strings.NewReader(samplePayload), int64(len([]byte(samplePayload))))
|
}, strings.NewReader(samplePayload), int64(len([]byte(samplePayload))))
|
||||||
assert.NoError(t, err)
|
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.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||||
assert.Len(t, release.Attachments, 1)
|
assert.Len(t, release.Attachments, 1)
|
||||||
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
||||||
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
||||||
assert.EqualValues(t, attach.Name, release.Attachments[0].Name)
|
assert.EqualValues(t, attach.Name, release.Attachments[0].Name)
|
||||||
|
assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
|
||||||
|
|
||||||
// update the attachment name
|
// update the attachment name
|
||||||
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, nil, nil, map[string]string{
|
assert.NoError(t, UpdateRelease(db.DefaultContext, user, gitRepo, release, false, []*AttachmentChange{
|
||||||
attach.UUID: "test2.txt",
|
{
|
||||||
}, false))
|
Action: "update",
|
||||||
|
Name: "test2.txt",
|
||||||
|
UUID: attach.UUID,
|
||||||
|
},
|
||||||
|
}))
|
||||||
release.Attachments = nil
|
release.Attachments = nil
|
||||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||||
assert.Len(t, release.Attachments, 1)
|
assert.Len(t, release.Attachments, 1)
|
||||||
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
assert.EqualValues(t, attach.UUID, release.Attachments[0].UUID)
|
||||||
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
assert.EqualValues(t, release.ID, release.Attachments[0].ReleaseID)
|
||||||
assert.EqualValues(t, "test2.txt", release.Attachments[0].Name)
|
assert.EqualValues(t, "test2.txt", release.Attachments[0].Name)
|
||||||
|
assert.EqualValues(t, attach.ExternalURL, release.Attachments[0].ExternalURL)
|
||||||
|
|
||||||
// delete the attachment
|
// 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
|
release.Attachments = nil
|
||||||
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
assert.NoError(t, repo_model.GetReleaseAttachments(db.DefaultContext, release))
|
||||||
assert.Empty(t, release.Attachments)
|
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) {
|
func TestRelease_createTag(t *testing.T) {
|
||||||
|
|
|
@ -72,7 +72,9 @@
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
{{if $hasArchiveLinks}}
|
{{if $hasArchiveLinks}}
|
||||||
<li>
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
|
@ -81,7 +83,9 @@
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{if $hasReleaseAttachment}}start-gap{{end}}">
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
|
@ -92,15 +96,23 @@
|
||||||
{{if $hasReleaseAttachment}}<hr>{{end}}
|
{{if $hasReleaseAttachment}}<hr>{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{range $release.Attachments}}
|
{{range $release.Attachments}}
|
||||||
|
{{if .ExternalURL}}
|
||||||
<li>
|
<li>
|
||||||
<a target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
||||||
<strong>{{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}</strong>
|
{{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>
|
</a>
|
||||||
<div>
|
<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>
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -63,15 +63,45 @@
|
||||||
{{range .attachments}}
|
{{range .attachments}}
|
||||||
<div class="field flex-text-block" id="attachment-{{.ID}}">
|
<div class="field flex-text-block" id="attachment-{{.ID}}">
|
||||||
<div class="flex-text-inline tw-flex-1">
|
<div class="flex-text-inline tw-flex-1">
|
||||||
<input name="attachment-edit-{{.UUID}}" class="attachment_edit" required value="{{.Name}}">
|
<div class="flex-text-inline tw-shrink-0" title="{{ctx.Locale.Tr "repo.release.type_attachment"}}">
|
||||||
<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
|
{{if .ExternalURL}}
|
||||||
<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>
|
{{svg "octicon-link-external" 16 "tw-mr-2"}}
|
||||||
|
{{else}}
|
||||||
|
{{svg "octicon-package" 16 "tw-mr-2"}}
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<a class="ui mini compact red button remove-rel-attach" data-id="{{.ID}}" data-uuid="{{.UUID}}">
|
<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 red button remove-rel-attach tw-ml-3" data-id="{{.ID}}" data-uuid="{{.UUID}}">
|
||||||
{{ctx.Locale.Tr "remove"}}
|
{{ctx.Locale.Tr "remove"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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}}
|
{{if .IsAttachmentEnabled}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
{{template "repo/upload" .}}
|
{{template "repo/upload" .}}
|
||||||
|
|
21
templates/swagger/v1_json.tmpl
generated
21
templates/swagger/v1_json.tmpl
generated
|
@ -13623,9 +13623,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"description": "attachment to upload",
|
"description": "attachment to upload (this parameter is incompatible with `external_url`)",
|
||||||
"name": "attachment",
|
"name": "attachment",
|
||||||
"in": "formData"
|
"in": "formData"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "url to external asset (this parameter is incompatible with `attachment`)",
|
||||||
|
"name": "external_url",
|
||||||
|
"in": "formData"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
@ -19001,6 +19007,14 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "Size"
|
"x-go-name": "Size"
|
||||||
},
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"attachment",
|
||||||
|
"external"
|
||||||
|
],
|
||||||
|
"x-go-name": "Type"
|
||||||
|
},
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "UUID"
|
"x-go-name": "UUID"
|
||||||
|
@ -20979,6 +20993,11 @@
|
||||||
"description": "EditAttachmentOptions options for editing attachments",
|
"description": "EditAttachmentOptions options for editing attachments",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"browser_download_url": {
|
||||||
|
"description": "(Can only be set if existing attachment is of external type)",
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "DownloadURL"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Name"
|
"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, "stream.bin", attachment.Name)
|
||||||
assert.EqualValues(t, 104, attachment.Size)
|
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(1), release.ArchiveDownloadCount.TarGz)
|
||||||
assert.Equal(t, int64(0), release.ArchiveDownloadCount.Zip)
|
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,
|
IsDraft: false,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: true,
|
IsTag: true,
|
||||||
}, nil, ""))
|
}, "", []*release_service.AttachmentChange{}))
|
||||||
|
|
||||||
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
|
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -111,7 +111,7 @@ func TestWebhookReleaseEvents(t *testing.T) {
|
||||||
IsDraft: false,
|
IsDraft: false,
|
||||||
IsPrerelease: false,
|
IsPrerelease: false,
|
||||||
IsTag: false,
|
IsTag: false,
|
||||||
}, nil, ""))
|
}, "", nil))
|
||||||
|
|
||||||
// check the newly created hooktasks
|
// check the newly created hooktasks
|
||||||
hookTasksLenBefore := len(hookTasks)
|
hookTasksLenBefore := len(hookTasks)
|
||||||
|
@ -125,7 +125,7 @@ func TestWebhookReleaseEvents(t *testing.T) {
|
||||||
|
|
||||||
t.Run("UpdateRelease", func(t *testing.T) {
|
t.Run("UpdateRelease", func(t *testing.T) {
|
||||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.1.1"})
|
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
|
// check the newly created hooktasks
|
||||||
hookTasksLenBefore := len(hookTasks)
|
hookTasksLenBefore := len(hookTasks)
|
||||||
|
@ -157,7 +157,7 @@ func TestWebhookReleaseEvents(t *testing.T) {
|
||||||
|
|
||||||
t.Run("UpdateRelease", func(t *testing.T) {
|
t.Run("UpdateRelease", func(t *testing.T) {
|
||||||
rel := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{RepoID: repo.ID, TagName: "v1.1.2"})
|
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
|
// check the newly created hooktasks
|
||||||
hookTasksLenBefore := len(hookTasks)
|
hookTasksLenBefore := len(hookTasks)
|
||||||
|
|
|
@ -6,7 +6,8 @@ export function initRepoRelease() {
|
||||||
el.addEventListener('click', (e) => {
|
el.addEventListener('click', (e) => {
|
||||||
const uuid = e.target.getAttribute('data-uuid');
|
const uuid = e.target.getAttribute('data-uuid');
|
||||||
const id = e.target.getAttribute('data-id');
|
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}`);
|
hideElem(`#attachment-${id}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -17,6 +18,7 @@ export function initRepoReleaseNew() {
|
||||||
|
|
||||||
initTagNameEditor();
|
initTagNameEditor();
|
||||||
initRepoReleaseEditor();
|
initRepoReleaseEditor();
|
||||||
|
initAddExternalLinkButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTagNameEditor() {
|
function initTagNameEditor() {
|
||||||
|
@ -45,9 +47,49 @@ function initTagNameEditor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function initRepoReleaseEditor() {
|
function initRepoReleaseEditor() {
|
||||||
const editor = document.querySelector('.repository.new.release .combo-markdown-editor');
|
const editor = document.querySelector(
|
||||||
|
'.repository.new.release .combo-markdown-editor',
|
||||||
|
);
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initComboMarkdownEditor(editor);
|
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