code.gitea.io/gitea@v1.21.7/services/release/release.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package release
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models"
    13  	"code.gitea.io/gitea/models/db"
    14  	git_model "code.gitea.io/gitea/models/git"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/container"
    18  	"code.gitea.io/gitea/modules/git"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/repository"
    21  	"code.gitea.io/gitea/modules/storage"
    22  	"code.gitea.io/gitea/modules/timeutil"
    23  	"code.gitea.io/gitea/modules/util"
    24  	notify_service "code.gitea.io/gitea/services/notify"
    25  )
    26  
    27  func createTag(ctx context.Context, gitRepo *git.Repository, rel *repo_model.Release, msg string) (bool, error) {
    28  	var created bool
    29  	// Only actual create when publish.
    30  	if !rel.IsDraft {
    31  		if !gitRepo.IsTagExist(rel.TagName) {
    32  			if err := rel.LoadAttributes(ctx); err != nil {
    33  				log.Error("LoadAttributes: %v", err)
    34  				return false, err
    35  			}
    36  
    37  			protectedTags, err := git_model.GetProtectedTags(ctx, rel.Repo.ID)
    38  			if err != nil {
    39  				return false, fmt.Errorf("GetProtectedTags: %w", err)
    40  			}
    41  
    42  			// Trim '--' prefix to prevent command line argument vulnerability.
    43  			rel.TagName = strings.TrimPrefix(rel.TagName, "--")
    44  			isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
    45  			if err != nil {
    46  				return false, err
    47  			}
    48  			if !isAllowed {
    49  				return false, models.ErrProtectedTagName{
    50  					TagName: rel.TagName,
    51  				}
    52  			}
    53  
    54  			commit, err := gitRepo.GetCommit(rel.Target)
    55  			if err != nil {
    56  				return false, fmt.Errorf("createTag::GetCommit[%v]: %w", rel.Target, err)
    57  			}
    58  
    59  			if len(msg) > 0 {
    60  				if err = gitRepo.CreateAnnotatedTag(rel.TagName, msg, commit.ID.String()); err != nil {
    61  					if strings.Contains(err.Error(), "is not a valid tag name") {
    62  						return false, models.ErrInvalidTagName{
    63  							TagName: rel.TagName,
    64  						}
    65  					}
    66  					return false, err
    67  				}
    68  			} else if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil {
    69  				if strings.Contains(err.Error(), "is not a valid tag name") {
    70  					return false, models.ErrInvalidTagName{
    71  						TagName: rel.TagName,
    72  					}
    73  				}
    74  				return false, err
    75  			}
    76  			created = true
    77  			rel.LowerTagName = strings.ToLower(rel.TagName)
    78  
    79  			commits := repository.NewPushCommits()
    80  			commits.HeadCommit = repository.CommitToPushCommit(commit)
    81  			commits.CompareURL = rel.Repo.ComposeCompareURL(git.EmptySHA, commit.ID.String())
    82  
    83  			refFullName := git.RefNameFromTag(rel.TagName)
    84  			notify_service.PushCommits(
    85  				ctx, rel.Publisher, rel.Repo,
    86  				&repository.PushUpdateOptions{
    87  					RefFullName: refFullName,
    88  					OldCommitID: git.EmptySHA,
    89  					NewCommitID: commit.ID.String(),
    90  				}, commits)
    91  			notify_service.CreateRef(ctx, rel.Publisher, rel.Repo, refFullName, commit.ID.String())
    92  			rel.CreatedUnix = timeutil.TimeStampNow()
    93  		}
    94  		commit, err := gitRepo.GetTagCommit(rel.TagName)
    95  		if err != nil {
    96  			return false, fmt.Errorf("GetTagCommit: %w", err)
    97  		}
    98  
    99  		rel.Sha1 = commit.ID.String()
   100  		rel.NumCommits, err = commit.CommitsCount()
   101  		if err != nil {
   102  			return false, fmt.Errorf("CommitsCount: %w", err)
   103  		}
   104  
   105  		if rel.PublisherID <= 0 {
   106  			u, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
   107  			if err == nil {
   108  				rel.PublisherID = u.ID
   109  			}
   110  		}
   111  	} else {
   112  		rel.CreatedUnix = timeutil.TimeStampNow()
   113  	}
   114  	return created, nil
   115  }
   116  
   117  // CreateRelease creates a new release of repository.
   118  func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentUUIDs []string, msg string) error {
   119  	has, err := repo_model.IsReleaseExist(gitRepo.Ctx, rel.RepoID, rel.TagName)
   120  	if err != nil {
   121  		return err
   122  	} else if has {
   123  		return repo_model.ErrReleaseAlreadyExist{
   124  			TagName: rel.TagName,
   125  		}
   126  	}
   127  
   128  	if _, err = createTag(gitRepo.Ctx, gitRepo, rel, msg); err != nil {
   129  		return err
   130  	}
   131  
   132  	rel.LowerTagName = strings.ToLower(rel.TagName)
   133  	if err = db.Insert(gitRepo.Ctx, rel); err != nil {
   134  		return err
   135  	}
   136  
   137  	if err = repo_model.AddReleaseAttachments(gitRepo.Ctx, rel.ID, attachmentUUIDs); err != nil {
   138  		return err
   139  	}
   140  
   141  	if !rel.IsDraft {
   142  		notify_service.NewRelease(gitRepo.Ctx, rel)
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  // CreateNewTag creates a new repository tag
   149  func CreateNewTag(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commit, tagName, msg string) error {
   150  	has, err := repo_model.IsReleaseExist(ctx, repo.ID, tagName)
   151  	if err != nil {
   152  		return err
   153  	} else if has {
   154  		return models.ErrTagAlreadyExists{
   155  			TagName: tagName,
   156  		}
   157  	}
   158  
   159  	gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
   160  	if err != nil {
   161  		return err
   162  	}
   163  	defer closer.Close()
   164  
   165  	rel := &repo_model.Release{
   166  		RepoID:       repo.ID,
   167  		Repo:         repo,
   168  		PublisherID:  doer.ID,
   169  		Publisher:    doer,
   170  		TagName:      tagName,
   171  		Target:       commit,
   172  		IsDraft:      false,
   173  		IsPrerelease: false,
   174  		IsTag:        true,
   175  	}
   176  
   177  	if _, err = createTag(ctx, gitRepo, rel, msg); err != nil {
   178  		return err
   179  	}
   180  
   181  	return db.Insert(ctx, rel)
   182  }
   183  
   184  // UpdateRelease updates information, attachments of a release and will create tag if it's not a draft and tag not exist.
   185  // addAttachmentUUIDs accept a slice of new created attachments' uuids which will be reassigned release_id as the created release
   186  // delAttachmentUUIDs accept a slice of attachments' uuids which will be deleted from the release
   187  // editAttachments accept a map of attachment uuid to new attachment name which will be updated with attachments.
   188  func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, rel *repo_model.Release,
   189  	addAttachmentUUIDs, delAttachmentUUIDs []string, editAttachments map[string]string,
   190  ) error {
   191  	if rel.ID == 0 {
   192  		return errors.New("UpdateRelease only accepts an exist release")
   193  	}
   194  	isCreated, err := createTag(gitRepo.Ctx, gitRepo, rel, "")
   195  	if err != nil {
   196  		return err
   197  	}
   198  	rel.LowerTagName = strings.ToLower(rel.TagName)
   199  
   200  	ctx, committer, err := db.TxContext(ctx)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	defer committer.Close()
   205  
   206  	if err = repo_model.UpdateRelease(ctx, rel); err != nil {
   207  		return err
   208  	}
   209  
   210  	if err = repo_model.AddReleaseAttachments(ctx, rel.ID, addAttachmentUUIDs); err != nil {
   211  		return fmt.Errorf("AddReleaseAttachments: %w", err)
   212  	}
   213  
   214  	deletedUUIDs := make(container.Set[string])
   215  	if len(delAttachmentUUIDs) > 0 {
   216  		// Check attachments
   217  		attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, delAttachmentUUIDs)
   218  		if err != nil {
   219  			return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", delAttachmentUUIDs, err)
   220  		}
   221  		for _, attach := range attachments {
   222  			if attach.ReleaseID != rel.ID {
   223  				return util.SilentWrap{
   224  					Message: "delete attachment of release permission denied",
   225  					Err:     util.ErrPermissionDenied,
   226  				}
   227  			}
   228  			deletedUUIDs.Add(attach.UUID)
   229  		}
   230  
   231  		if _, err := repo_model.DeleteAttachments(ctx, attachments, true); err != nil {
   232  			return fmt.Errorf("DeleteAttachments [uuids: %v]: %w", delAttachmentUUIDs, err)
   233  		}
   234  	}
   235  
   236  	if len(editAttachments) > 0 {
   237  		updateAttachmentsList := make([]string, 0, len(editAttachments))
   238  		for k := range editAttachments {
   239  			updateAttachmentsList = append(updateAttachmentsList, k)
   240  		}
   241  		// Check attachments
   242  		attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, updateAttachmentsList)
   243  		if err != nil {
   244  			return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", updateAttachmentsList, err)
   245  		}
   246  		for _, attach := range attachments {
   247  			if attach.ReleaseID != rel.ID {
   248  				return util.SilentWrap{
   249  					Message: "update attachment of release permission denied",
   250  					Err:     util.ErrPermissionDenied,
   251  				}
   252  			}
   253  		}
   254  
   255  		for uuid, newName := range editAttachments {
   256  			if !deletedUUIDs.Contains(uuid) {
   257  				if err = repo_model.UpdateAttachmentByUUID(ctx, &repo_model.Attachment{
   258  					UUID: uuid,
   259  					Name: newName,
   260  				}, "name"); err != nil {
   261  					return err
   262  				}
   263  			}
   264  		}
   265  	}
   266  
   267  	if err := committer.Commit(); err != nil {
   268  		return err
   269  	}
   270  
   271  	for _, uuid := range delAttachmentUUIDs {
   272  		if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(uuid)); err != nil {
   273  			// Even delete files failed, but the attachments has been removed from database, so we
   274  			// should not return error but only record the error on logs.
   275  			// users have to delete this attachments manually or we should have a
   276  			// synchronize between database attachment table and attachment storage
   277  			log.Error("delete attachment[uuid: %s] failed: %v", uuid, err)
   278  		}
   279  	}
   280  
   281  	if !rel.IsDraft {
   282  		if !isCreated {
   283  			notify_service.UpdateRelease(gitRepo.Ctx, doer, rel)
   284  			return nil
   285  		}
   286  		notify_service.NewRelease(gitRepo.Ctx, rel)
   287  	}
   288  	return nil
   289  }
   290  
   291  // DeleteReleaseByID deletes a release and corresponding Git tag by given ID.
   292  func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *repo_model.Release, doer *user_model.User, delTag bool) error {
   293  	if delTag {
   294  		protectedTags, err := git_model.GetProtectedTags(ctx, rel.RepoID)
   295  		if err != nil {
   296  			return fmt.Errorf("GetProtectedTags: %w", err)
   297  		}
   298  		isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
   299  		if err != nil {
   300  			return err
   301  		}
   302  		if !isAllowed {
   303  			return models.ErrProtectedTagName{
   304  				TagName: rel.TagName,
   305  			}
   306  		}
   307  
   308  		if stdout, _, err := git.NewCommand(ctx, "tag", "-d").AddDashesAndList(rel.TagName).
   309  			SetDescription(fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID)).
   310  			RunStdString(&git.RunOpts{Dir: repo.RepoPath()}); err != nil && !strings.Contains(err.Error(), "not found") {
   311  			log.Error("DeleteReleaseByID (git tag -d): %d in %v Failed:\nStdout: %s\nError: %v", rel.ID, repo, stdout, err)
   312  			return fmt.Errorf("git tag -d: %w", err)
   313  		}
   314  
   315  		refName := git.RefNameFromTag(rel.TagName)
   316  		notify_service.PushCommits(
   317  			ctx, doer, repo,
   318  			&repository.PushUpdateOptions{
   319  				RefFullName: refName,
   320  				OldCommitID: rel.Sha1,
   321  				NewCommitID: git.EmptySHA,
   322  			}, repository.NewPushCommits())
   323  		notify_service.DeleteRef(ctx, doer, repo, refName)
   324  
   325  		if err := repo_model.DeleteReleaseByID(ctx, rel.ID); err != nil {
   326  			return fmt.Errorf("DeleteReleaseByID: %w", err)
   327  		}
   328  	} else {
   329  		rel.IsTag = true
   330  
   331  		if err := repo_model.UpdateRelease(ctx, rel); err != nil {
   332  			return fmt.Errorf("Update: %w", err)
   333  		}
   334  	}
   335  
   336  	rel.Repo = repo
   337  	if err := rel.LoadAttributes(ctx); err != nil {
   338  		return fmt.Errorf("LoadAttributes: %w", err)
   339  	}
   340  
   341  	if err := repo_model.DeleteAttachmentsByRelease(ctx, rel.ID); err != nil {
   342  		return fmt.Errorf("DeleteAttachments: %w", err)
   343  	}
   344  
   345  	for i := range rel.Attachments {
   346  		attachment := rel.Attachments[i]
   347  		if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil {
   348  			log.Error("Delete attachment %s of release %s failed: %v", attachment.UUID, rel.ID, err)
   349  		}
   350  	}
   351  
   352  	if !rel.IsDraft {
   353  		notify_service.DeleteRelease(ctx, doer, rel)
   354  	}
   355  	return nil
   356  }