2019-09-15 18:28:25 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package release
import (
2021-03-22 19:09:51 +03:00
"errors"
2019-09-15 18:28:25 +03:00
"fmt"
"strings"
"code.gitea.io/gitea/models"
2021-09-19 14:49:59 +03:00
"code.gitea.io/gitea/models/db"
2019-09-15 18:28:25 +03:00
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
2019-11-06 11:25:50 +03:00
"code.gitea.io/gitea/modules/notification"
2020-01-10 12:34:21 +03:00
"code.gitea.io/gitea/modules/repository"
2020-08-18 07:23:45 +03:00
"code.gitea.io/gitea/modules/storage"
2019-09-15 18:28:25 +03:00
"code.gitea.io/gitea/modules/timeutil"
)
2021-03-22 19:09:51 +03:00
func createTag ( gitRepo * git . Repository , rel * models . Release , msg string ) ( bool , error ) {
var created bool
2019-09-15 18:28:25 +03:00
// Only actual create when publish.
if ! rel . IsDraft {
if ! gitRepo . IsTagExist ( rel . TagName ) {
2021-06-25 17:28:55 +03:00
if err := rel . LoadAttributes ( ) ; err != nil {
log . Error ( "LoadAttributes: %v" , err )
return false , err
}
protectedTags , err := rel . Repo . GetProtectedTags ( )
if err != nil {
return false , fmt . Errorf ( "GetProtectedTags: %v" , err )
}
isAllowed , err := models . IsUserAllowedToControlTag ( protectedTags , rel . TagName , rel . PublisherID )
if err != nil {
return false , err
}
if ! isAllowed {
return false , models . ErrProtectedTagName {
TagName : rel . TagName ,
}
}
2019-09-15 18:28:25 +03:00
commit , err := gitRepo . GetCommit ( rel . Target )
if err != nil {
2021-08-31 04:18:15 +03:00
return false , fmt . Errorf ( "createTag::GetCommit[%v]: %v" , rel . Target , err )
2019-09-15 18:28:25 +03:00
}
// Trim '--' prefix to prevent command line argument vulnerability.
rel . TagName = strings . TrimPrefix ( rel . TagName , "--" )
2021-02-28 22:57:45 +03:00
if len ( msg ) > 0 {
if err = gitRepo . CreateAnnotatedTag ( rel . TagName , msg , commit . ID . String ( ) ) ; err != nil {
if strings . Contains ( err . Error ( ) , "is not a valid tag name" ) {
2021-03-22 19:09:51 +03:00
return false , models . ErrInvalidTagName {
2021-02-28 22:57:45 +03:00
TagName : rel . TagName ,
}
}
2021-03-22 19:09:51 +03:00
return false , err
2021-02-28 22:57:45 +03:00
}
} else if err = gitRepo . CreateTag ( rel . TagName , commit . ID . String ( ) ) ; err != nil {
2019-09-15 18:28:25 +03:00
if strings . Contains ( err . Error ( ) , "is not a valid tag name" ) {
2021-03-22 19:09:51 +03:00
return false , models . ErrInvalidTagName {
2019-09-15 18:28:25 +03:00
TagName : rel . TagName ,
}
}
2021-03-22 19:09:51 +03:00
return false , err
2019-09-15 18:28:25 +03:00
}
2021-03-22 19:09:51 +03:00
created = true
2019-09-15 18:28:25 +03:00
rel . LowerTagName = strings . ToLower ( rel . TagName )
2021-06-25 17:28:55 +03:00
2019-11-23 01:13:57 +03:00
notification . NotifyPushCommits (
2020-10-31 00:59:02 +03:00
rel . Publisher , rel . Repo ,
& repository . PushUpdateOptions {
RefFullName : git . TagPrefix + rel . TagName ,
OldCommitID : git . EmptySHA ,
NewCommitID : commit . ID . String ( ) ,
} , repository . NewPushCommits ( ) )
2019-11-23 01:13:57 +03:00
notification . NotifyCreateRef ( rel . Publisher , rel . Repo , "tag" , git . TagPrefix + rel . TagName )
2020-07-28 21:08:45 +03:00
rel . CreatedUnix = timeutil . TimeStampNow ( )
2019-09-15 18:28:25 +03:00
}
commit , err := gitRepo . GetTagCommit ( rel . TagName )
if err != nil {
2021-03-22 19:09:51 +03:00
return false , fmt . Errorf ( "GetTagCommit: %v" , err )
2019-09-15 18:28:25 +03:00
}
rel . Sha1 = commit . ID . String ( )
rel . NumCommits , err = commit . CommitsCount ( )
if err != nil {
2021-03-22 19:09:51 +03:00
return false , fmt . Errorf ( "CommitsCount: %v" , err )
2019-09-15 18:28:25 +03:00
}
2020-10-22 03:55:25 +03:00
2021-01-02 00:03:31 +03:00
if rel . PublisherID <= 0 {
u , err := models . GetUserByEmail ( commit . Author . Email )
if err == nil {
rel . PublisherID = u . ID
}
2020-10-22 03:55:25 +03:00
}
2019-09-15 18:28:25 +03:00
} else {
rel . CreatedUnix = timeutil . TimeStampNow ( )
}
2021-03-22 19:09:51 +03:00
return created , nil
2019-09-15 18:28:25 +03:00
}
// CreateRelease creates a new release of repository.
2021-02-28 22:57:45 +03:00
func CreateRelease ( gitRepo * git . Repository , rel * models . Release , attachmentUUIDs [ ] string , msg string ) error {
2019-09-15 18:28:25 +03:00
isExist , err := models . IsReleaseExist ( rel . RepoID , rel . TagName )
if err != nil {
return err
} else if isExist {
return models . ErrReleaseAlreadyExist {
TagName : rel . TagName ,
}
}
2021-03-22 19:09:51 +03:00
if _ , err = createTag ( gitRepo , rel , msg ) ; err != nil {
2019-09-15 18:28:25 +03:00
return err
}
rel . LowerTagName = strings . ToLower ( rel . TagName )
if err = models . InsertRelease ( rel ) ; err != nil {
return err
}
2021-09-23 18:45:36 +03:00
if err = models . AddReleaseAttachments ( db . DefaultContext , rel . ID , attachmentUUIDs ) ; err != nil {
2019-09-15 18:28:25 +03:00
return err
}
if ! rel . IsDraft {
2019-11-06 11:25:50 +03:00
notification . NotifyNewRelease ( rel )
2019-09-15 18:28:25 +03:00
}
return nil
}
2021-02-28 22:57:45 +03:00
// CreateNewTag creates a new repository tag
func CreateNewTag ( doer * models . User , repo * models . Repository , commit , tagName , msg string ) error {
isExist , err := models . IsReleaseExist ( repo . ID , tagName )
if err != nil {
return err
} else if isExist {
return models . ErrTagAlreadyExists {
TagName : tagName ,
}
}
gitRepo , err := git . OpenRepository ( repo . RepoPath ( ) )
if err != nil {
return err
}
defer gitRepo . Close ( )
rel := & models . Release {
RepoID : repo . ID ,
2021-06-25 17:28:55 +03:00
Repo : repo ,
2021-02-28 22:57:45 +03:00
PublisherID : doer . ID ,
2021-06-25 17:28:55 +03:00
Publisher : doer ,
2021-02-28 22:57:45 +03:00
TagName : tagName ,
Target : commit ,
IsDraft : false ,
IsPrerelease : false ,
IsTag : true ,
}
2021-03-22 19:09:51 +03:00
if _ , err = createTag ( gitRepo , rel , msg ) ; err != nil {
2021-02-28 22:57:45 +03:00
return err
}
if err = models . InsertRelease ( rel ) ; err != nil {
return err
}
return err
}
2021-03-22 19:09:51 +03:00
// UpdateRelease updates information, attachments of a release and will create tag if it's not a draft and tag not exist.
// 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 ( doer * models . User , gitRepo * git . Repository , rel * models . Release ,
addAttachmentUUIDs , delAttachmentUUIDs [ ] string , editAttachments map [ string ] string ) ( err error ) {
if rel . ID == 0 {
return errors . New ( "UpdateRelease only accepts an exist release" )
}
isCreated , err := createTag ( gitRepo , rel , "" )
if err != nil {
2019-09-15 18:28:25 +03:00
return err
}
rel . LowerTagName = strings . ToLower ( rel . TagName )
2021-09-19 14:49:59 +03:00
ctx , committer , err := db . TxContext ( )
2021-03-22 19:09:51 +03:00
if err != nil {
return err
}
2021-09-19 14:49:59 +03:00
defer committer . Close ( )
2021-03-22 19:09:51 +03:00
if err = models . UpdateRelease ( ctx , rel ) ; err != nil {
2019-09-15 18:28:25 +03:00
return err
}
2021-03-22 19:09:51 +03:00
if err = models . AddReleaseAttachments ( ctx , rel . ID , addAttachmentUUIDs ) ; err != nil {
return fmt . Errorf ( "AddReleaseAttachments: %v" , err )
}
var deletedUUIDsMap = make ( map [ string ] bool )
if len ( delAttachmentUUIDs ) > 0 {
// Check attachments
attachments , err := models . GetAttachmentsByUUIDs ( ctx , delAttachmentUUIDs )
if err != nil {
return fmt . Errorf ( "GetAttachmentsByUUIDs [uuids: %v]: %v" , delAttachmentUUIDs , err )
}
for _ , attach := range attachments {
if attach . ReleaseID != rel . ID {
return errors . New ( "delete attachement of release permission denied" )
}
deletedUUIDsMap [ attach . UUID ] = true
}
if _ , err := models . DeleteAttachments ( ctx , attachments , false ) ; err != nil {
return fmt . Errorf ( "DeleteAttachments [uuids: %v]: %v" , delAttachmentUUIDs , err )
}
}
if len ( editAttachments ) > 0 {
var updateAttachmentsList = make ( [ ] string , 0 , len ( editAttachments ) )
for k := range editAttachments {
updateAttachmentsList = append ( updateAttachmentsList , k )
}
// Check attachments
attachments , err := models . GetAttachmentsByUUIDs ( ctx , updateAttachmentsList )
if err != nil {
return fmt . Errorf ( "GetAttachmentsByUUIDs [uuids: %v]: %v" , updateAttachmentsList , err )
}
for _ , attach := range attachments {
if attach . ReleaseID != rel . ID {
return errors . New ( "update attachement of release permission denied" )
}
}
for uuid , newName := range editAttachments {
if ! deletedUUIDsMap [ uuid ] {
if err = models . UpdateAttachmentByUUID ( ctx , & models . Attachment {
UUID : uuid ,
Name : newName ,
} , "name" ) ; err != nil {
return err
}
}
}
}
2021-09-19 14:49:59 +03:00
if err = committer . Commit ( ) ; err != nil {
2021-03-22 19:09:51 +03:00
return
}
for _ , uuid := range delAttachmentUUIDs {
if err := storage . Attachments . Delete ( models . 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.
// users have to delete this attachments manually or we should have a
// synchronize between database attachment table and attachment storage
log . Error ( "delete attachment[uuid: %s] failed: %v" , uuid , err )
}
2019-09-24 09:55:07 +03:00
}
2021-03-22 19:09:51 +03:00
if ! isCreated {
2020-07-29 22:20:54 +03:00
notification . NotifyUpdateRelease ( doer , rel )
return
}
if ! rel . IsDraft {
notification . NotifyNewRelease ( rel )
}
2019-09-15 18:28:25 +03:00
return err
}
2019-09-30 19:10:00 +03:00
// DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
func DeleteReleaseByID ( id int64 , doer * models . User , delTag bool ) error {
rel , err := models . GetReleaseByID ( id )
if err != nil {
return fmt . Errorf ( "GetReleaseByID: %v" , err )
}
repo , err := models . GetRepositoryByID ( rel . RepoID )
if err != nil {
return fmt . Errorf ( "GetRepositoryByID: %v" , err )
}
if delTag {
2019-11-30 17:40:22 +03:00
if stdout , err := git . NewCommand ( "tag" , "-d" , rel . TagName ) .
SetDescription ( fmt . Sprintf ( "DeleteReleaseByID (git tag -d): %d" , rel . ID ) ) .
RunInDir ( repo . RepoPath ( ) ) ; err != nil && ! strings . Contains ( err . Error ( ) , "not found" ) {
log . Error ( "DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v" , rel . ID , repo , stdout , err )
return fmt . Errorf ( "git tag -d: %v" , err )
2019-09-30 19:10:00 +03:00
}
2020-12-16 19:00:46 +03:00
notification . NotifyPushCommits (
doer , repo ,
& repository . PushUpdateOptions {
RefFullName : git . TagPrefix + rel . TagName ,
OldCommitID : rel . Sha1 ,
NewCommitID : git . EmptySHA ,
} , repository . NewPushCommits ( ) )
notification . NotifyDeleteRef ( doer , repo , "tag" , git . TagPrefix + rel . TagName )
2019-09-30 19:10:00 +03:00
if err := models . DeleteReleaseByID ( id ) ; err != nil {
return fmt . Errorf ( "DeleteReleaseByID: %v" , err )
}
} else {
rel . IsTag = true
2021-09-23 18:45:36 +03:00
if err = models . UpdateRelease ( db . DefaultContext , rel ) ; err != nil {
2019-09-30 19:10:00 +03:00
return fmt . Errorf ( "Update: %v" , err )
}
}
rel . Repo = repo
if err = rel . LoadAttributes ( ) ; err != nil {
return fmt . Errorf ( "LoadAttributes: %v" , err )
}
if err := models . DeleteAttachmentsByRelease ( rel . ID ) ; err != nil {
return fmt . Errorf ( "DeleteAttachments: %v" , err )
}
for i := range rel . Attachments {
attachment := rel . Attachments [ i ]
2020-08-18 07:23:45 +03:00
if err := storage . Attachments . Delete ( attachment . RelativePath ( ) ) ; err != nil {
2019-09-30 19:10:00 +03:00
log . Error ( "Delete attachment %s of release %s failed: %v" , attachment . UUID , rel . ID , err )
}
}
2019-11-06 11:25:50 +03:00
notification . NotifyDeleteRelease ( doer , rel )
2019-09-30 19:10:00 +03:00
return nil
}