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