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

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"net/url"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	user_model "code.gitea.io/gitea/models/user"
    17  	"code.gitea.io/gitea/modules/container"
    18  	"code.gitea.io/gitea/modules/structs"
    19  	"code.gitea.io/gitea/modules/timeutil"
    20  	"code.gitea.io/gitea/modules/util"
    21  
    22  	"xorm.io/builder"
    23  )
    24  
    25  // ErrReleaseAlreadyExist represents a "ReleaseAlreadyExist" kind of error.
    26  type ErrReleaseAlreadyExist struct {
    27  	TagName string
    28  }
    29  
    30  // IsErrReleaseAlreadyExist checks if an error is a ErrReleaseAlreadyExist.
    31  func IsErrReleaseAlreadyExist(err error) bool {
    32  	_, ok := err.(ErrReleaseAlreadyExist)
    33  	return ok
    34  }
    35  
    36  func (err ErrReleaseAlreadyExist) Error() string {
    37  	return fmt.Sprintf("release tag already exist [tag_name: %s]", err.TagName)
    38  }
    39  
    40  func (err ErrReleaseAlreadyExist) Unwrap() error {
    41  	return util.ErrAlreadyExist
    42  }
    43  
    44  // ErrReleaseNotExist represents a "ReleaseNotExist" kind of error.
    45  type ErrReleaseNotExist struct {
    46  	ID      int64
    47  	TagName string
    48  }
    49  
    50  // IsErrReleaseNotExist checks if an error is a ErrReleaseNotExist.
    51  func IsErrReleaseNotExist(err error) bool {
    52  	_, ok := err.(ErrReleaseNotExist)
    53  	return ok
    54  }
    55  
    56  func (err ErrReleaseNotExist) Error() string {
    57  	return fmt.Sprintf("release tag does not exist [id: %d, tag_name: %s]", err.ID, err.TagName)
    58  }
    59  
    60  func (err ErrReleaseNotExist) Unwrap() error {
    61  	return util.ErrNotExist
    62  }
    63  
    64  // Release represents a release of repository.
    65  type Release struct {
    66  	ID               int64            `xorm:"pk autoincr"`
    67  	RepoID           int64            `xorm:"INDEX UNIQUE(n)"`
    68  	Repo             *Repository      `xorm:"-"`
    69  	PublisherID      int64            `xorm:"INDEX"`
    70  	Publisher        *user_model.User `xorm:"-"`
    71  	TagName          string           `xorm:"INDEX UNIQUE(n)"`
    72  	OriginalAuthor   string
    73  	OriginalAuthorID int64 `xorm:"index"`
    74  	LowerTagName     string
    75  	Target           string
    76  	TargetBehind     string `xorm:"-"` // to handle non-existing or empty target
    77  	Title            string
    78  	Sha1             string `xorm:"VARCHAR(40)"`
    79  	NumCommits       int64
    80  	NumCommitsBehind int64              `xorm:"-"`
    81  	Note             string             `xorm:"TEXT"`
    82  	RenderedNote     string             `xorm:"-"`
    83  	IsDraft          bool               `xorm:"NOT NULL DEFAULT false"`
    84  	IsPrerelease     bool               `xorm:"NOT NULL DEFAULT false"`
    85  	IsTag            bool               `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
    86  	Attachments      []*Attachment      `xorm:"-"`
    87  	CreatedUnix      timeutil.TimeStamp `xorm:"INDEX"`
    88  }
    89  
    90  func init() {
    91  	db.RegisterModel(new(Release))
    92  }
    93  
    94  // LoadAttributes load repo and publisher attributes for a release
    95  func (r *Release) LoadAttributes(ctx context.Context) error {
    96  	var err error
    97  	if r.Repo == nil {
    98  		r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
    99  		if err != nil {
   100  			return err
   101  		}
   102  	}
   103  	if r.Publisher == nil {
   104  		r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
   105  		if err != nil {
   106  			if user_model.IsErrUserNotExist(err) {
   107  				r.Publisher = user_model.NewGhostUser()
   108  			} else {
   109  				return err
   110  			}
   111  		}
   112  	}
   113  	return GetReleaseAttachments(ctx, r)
   114  }
   115  
   116  // APIURL the api url for a release. release must have attributes loaded
   117  func (r *Release) APIURL() string {
   118  	return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10)
   119  }
   120  
   121  // ZipURL the zip url for a release. release must have attributes loaded
   122  func (r *Release) ZipURL() string {
   123  	return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".zip"
   124  }
   125  
   126  // TarURL the tar.gz url for a release. release must have attributes loaded
   127  func (r *Release) TarURL() string {
   128  	return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".tar.gz"
   129  }
   130  
   131  // HTMLURL the url for a release on the web UI. release must have attributes loaded
   132  func (r *Release) HTMLURL() string {
   133  	return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
   134  }
   135  
   136  // APIUploadURL the api url to upload assets to a release. release must have attributes loaded
   137  func (r *Release) APIUploadURL() string {
   138  	return r.APIURL() + "/assets"
   139  }
   140  
   141  // Link the relative url for a release on the web UI. release must have attributes loaded
   142  func (r *Release) Link() string {
   143  	return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
   144  }
   145  
   146  // IsReleaseExist returns true if release with given tag name already exists.
   147  func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, error) {
   148  	if len(tagName) == 0 {
   149  		return false, nil
   150  	}
   151  
   152  	return db.GetEngine(ctx).Exist(&Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)})
   153  }
   154  
   155  // UpdateRelease updates all columns of a release
   156  func UpdateRelease(ctx context.Context, rel *Release) error {
   157  	_, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel)
   158  	return err
   159  }
   160  
   161  // AddReleaseAttachments adds a release attachments
   162  func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
   163  	// Check attachments
   164  	attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
   165  	if err != nil {
   166  		return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %w", attachmentUUIDs, err)
   167  	}
   168  
   169  	for i := range attachments {
   170  		if attachments[i].ReleaseID != 0 {
   171  			return util.NewPermissionDeniedErrorf("release permission denied")
   172  		}
   173  		attachments[i].ReleaseID = releaseID
   174  		// No assign value could be 0, so ignore AllCols().
   175  		if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
   176  			return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
   177  		}
   178  	}
   179  
   180  	return err
   181  }
   182  
   183  // GetRelease returns release by given ID.
   184  func GetRelease(ctx context.Context, repoID int64, tagName string) (*Release, error) {
   185  	rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
   186  	has, err := db.GetEngine(ctx).Get(rel)
   187  	if err != nil {
   188  		return nil, err
   189  	} else if !has {
   190  		return nil, ErrReleaseNotExist{0, tagName}
   191  	}
   192  	return rel, nil
   193  }
   194  
   195  // GetReleaseByID returns release with given ID.
   196  func GetReleaseByID(ctx context.Context, id int64) (*Release, error) {
   197  	rel := new(Release)
   198  	has, err := db.GetEngine(ctx).
   199  		ID(id).
   200  		Get(rel)
   201  	if err != nil {
   202  		return nil, err
   203  	} else if !has {
   204  		return nil, ErrReleaseNotExist{id, ""}
   205  	}
   206  
   207  	return rel, nil
   208  }
   209  
   210  // GetReleaseForRepoByID returns release with given ID.
   211  func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, error) {
   212  	rel := new(Release)
   213  	has, err := db.GetEngine(ctx).
   214  		Where("id=? AND repo_id=?", id, repoID).
   215  		Get(rel)
   216  	if err != nil {
   217  		return nil, err
   218  	} else if !has {
   219  		return nil, ErrReleaseNotExist{id, ""}
   220  	}
   221  
   222  	return rel, nil
   223  }
   224  
   225  // FindReleasesOptions describes the conditions to Find releases
   226  type FindReleasesOptions struct {
   227  	db.ListOptions
   228  	IncludeDrafts bool
   229  	IncludeTags   bool
   230  	IsPreRelease  util.OptionalBool
   231  	IsDraft       util.OptionalBool
   232  	TagNames      []string
   233  	RepoID        int64
   234  	HasSha1       util.OptionalBool // useful to find draft releases which are created with existing tags
   235  }
   236  
   237  func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
   238  	opts.RepoID = repoID
   239  	return opts.ToConds()
   240  }
   241  
   242  func (opts *FindReleasesOptions) ToConds() builder.Cond {
   243  	cond := builder.NewCond()
   244  	cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
   245  
   246  	if !opts.IncludeDrafts {
   247  		cond = cond.And(builder.Eq{"is_draft": false})
   248  	}
   249  	if !opts.IncludeTags {
   250  		cond = cond.And(builder.Eq{"is_tag": false})
   251  	}
   252  	if len(opts.TagNames) > 0 {
   253  		cond = cond.And(builder.In("tag_name", opts.TagNames))
   254  	}
   255  	if !opts.IsPreRelease.IsNone() {
   256  		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()})
   257  	}
   258  	if !opts.IsDraft.IsNone() {
   259  		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()})
   260  	}
   261  	if !opts.HasSha1.IsNone() {
   262  		if opts.HasSha1.IsTrue() {
   263  			cond = cond.And(builder.Neq{"sha1": ""})
   264  		} else {
   265  			cond = cond.And(builder.Eq{"sha1": ""})
   266  		}
   267  	}
   268  	return cond
   269  }
   270  
   271  // GetReleasesByRepoID returns a list of releases of repository.
   272  func GetReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) ([]*Release, error) {
   273  	sess := db.GetEngine(ctx).
   274  		Desc("created_unix", "id").
   275  		Where(opts.toConds(repoID))
   276  
   277  	if opts.PageSize != 0 {
   278  		sess = db.SetSessionPagination(sess, &opts.ListOptions)
   279  	}
   280  
   281  	rels := make([]*Release, 0, opts.PageSize)
   282  	return rels, sess.Find(&rels)
   283  }
   284  
   285  // GetTagNamesByRepoID returns a list of release tag names of repository.
   286  func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
   287  	listOptions := db.ListOptions{
   288  		ListAll: true,
   289  	}
   290  	opts := FindReleasesOptions{
   291  		ListOptions:   listOptions,
   292  		IncludeDrafts: true,
   293  		IncludeTags:   true,
   294  		HasSha1:       util.OptionalBoolTrue,
   295  	}
   296  
   297  	tags := make([]string, 0)
   298  	sess := db.GetEngine(ctx).
   299  		Table("release").
   300  		Desc("created_unix", "id").
   301  		Where(opts.toConds(repoID)).
   302  		Cols("tag_name")
   303  
   304  	return tags, sess.Find(&tags)
   305  }
   306  
   307  // CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID.
   308  func CountReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) {
   309  	return db.GetEngine(ctx).Where(opts.toConds(repoID)).Count(new(Release))
   310  }
   311  
   312  // GetLatestReleaseByRepoID returns the latest release for a repository
   313  func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
   314  	cond := builder.NewCond().
   315  		And(builder.Eq{"repo_id": repoID}).
   316  		And(builder.Eq{"is_draft": false}).
   317  		And(builder.Eq{"is_prerelease": false}).
   318  		And(builder.Eq{"is_tag": false})
   319  
   320  	rel := new(Release)
   321  	has, err := db.GetEngine(ctx).
   322  		Desc("created_unix", "id").
   323  		Where(cond).
   324  		Get(rel)
   325  	if err != nil {
   326  		return nil, err
   327  	} else if !has {
   328  		return nil, ErrReleaseNotExist{0, "latest"}
   329  	}
   330  
   331  	return rel, nil
   332  }
   333  
   334  // GetReleasesByRepoIDAndNames returns a list of releases of repository according repoID and tagNames.
   335  func GetReleasesByRepoIDAndNames(ctx context.Context, repoID int64, tagNames []string) (rels []*Release, err error) {
   336  	err = db.GetEngine(ctx).
   337  		In("tag_name", tagNames).
   338  		Desc("created_unix").
   339  		Find(&rels, Release{RepoID: repoID})
   340  	return rels, err
   341  }
   342  
   343  // GetReleaseCountByRepoID returns the count of releases of repository
   344  func GetReleaseCountByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) {
   345  	return db.GetEngine(ctx).Where(opts.toConds(repoID)).Count(&Release{})
   346  }
   347  
   348  type releaseMetaSearch struct {
   349  	ID  []int64
   350  	Rel []*Release
   351  }
   352  
   353  func (s releaseMetaSearch) Len() int {
   354  	return len(s.ID)
   355  }
   356  
   357  func (s releaseMetaSearch) Swap(i, j int) {
   358  	s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
   359  	s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
   360  }
   361  
   362  func (s releaseMetaSearch) Less(i, j int) bool {
   363  	return s.ID[i] < s.ID[j]
   364  }
   365  
   366  func hasDuplicateName(attaches []*Attachment) bool {
   367  	attachSet := container.Set[string]{}
   368  	for _, attachment := range attaches {
   369  		if attachSet.Contains(attachment.Name) {
   370  			return true
   371  		}
   372  		attachSet.Add(attachment.Name)
   373  	}
   374  	return false
   375  }
   376  
   377  // GetReleaseAttachments retrieves the attachments for releases
   378  func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
   379  	if len(rels) == 0 {
   380  		return nil
   381  	}
   382  
   383  	// To keep this efficient as possible sort all releases by id,
   384  	//    select attachments by release id,
   385  	//    then merge join them
   386  
   387  	// Sort
   388  	sortedRels := releaseMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Release, len(rels))}
   389  	var attachments []*Attachment
   390  	for index, element := range rels {
   391  		element.Attachments = []*Attachment{}
   392  		sortedRels.ID[index] = element.ID
   393  		sortedRels.Rel[index] = element
   394  	}
   395  	sort.Sort(sortedRels)
   396  
   397  	// Select attachments
   398  	err = db.GetEngine(ctx).
   399  		Asc("release_id", "name").
   400  		In("release_id", sortedRels.ID).
   401  		Find(&attachments)
   402  	if err != nil {
   403  		return err
   404  	}
   405  
   406  	// merge join
   407  	currentIndex := 0
   408  	for _, attachment := range attachments {
   409  		for sortedRels.ID[currentIndex] < attachment.ReleaseID {
   410  			currentIndex++
   411  		}
   412  		sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment)
   413  	}
   414  
   415  	// Makes URL's predictable
   416  	for _, release := range rels {
   417  		// If we have no Repo, we don't need to execute this loop
   418  		if release.Repo == nil {
   419  			continue
   420  		}
   421  
   422  		// If the names unique, use the URL with the Name instead of the UUID
   423  		if !hasDuplicateName(release.Attachments) {
   424  			for _, attachment := range release.Attachments {
   425  				attachment.CustomDownloadURL = release.Repo.HTMLURL() + "/releases/download/" + url.PathEscape(release.TagName) + "/" + url.PathEscape(attachment.Name)
   426  			}
   427  		}
   428  	}
   429  
   430  	return err
   431  }
   432  
   433  type releaseSorter struct {
   434  	rels []*Release
   435  }
   436  
   437  func (rs *releaseSorter) Len() int {
   438  	return len(rs.rels)
   439  }
   440  
   441  func (rs *releaseSorter) Less(i, j int) bool {
   442  	diffNum := rs.rels[i].NumCommits - rs.rels[j].NumCommits
   443  	if diffNum != 0 {
   444  		return diffNum > 0
   445  	}
   446  	return rs.rels[i].CreatedUnix > rs.rels[j].CreatedUnix
   447  }
   448  
   449  func (rs *releaseSorter) Swap(i, j int) {
   450  	rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i]
   451  }
   452  
   453  // SortReleases sorts releases by number of commits and created time.
   454  func SortReleases(rels []*Release) {
   455  	sorter := &releaseSorter{rels: rels}
   456  	sort.Sort(sorter)
   457  }
   458  
   459  // DeleteReleaseByID deletes a release from database by given ID.
   460  func DeleteReleaseByID(ctx context.Context, id int64) error {
   461  	_, err := db.GetEngine(ctx).ID(id).Delete(new(Release))
   462  	return err
   463  }
   464  
   465  // UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID
   466  func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error {
   467  	_, err := db.GetEngine(ctx).Table("release").
   468  		Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
   469  		And("original_author_id = ?", originalAuthorID).
   470  		Update(map[string]any{
   471  			"publisher_id":       posterID,
   472  			"original_author":    "",
   473  			"original_author_id": 0,
   474  		})
   475  	return err
   476  }
   477  
   478  // PushUpdateDeleteTagsContext updates a number of delete tags with context
   479  func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []string) error {
   480  	if len(tags) == 0 {
   481  		return nil
   482  	}
   483  	lowerTags := make([]string, 0, len(tags))
   484  	for _, tag := range tags {
   485  		lowerTags = append(lowerTags, strings.ToLower(tag))
   486  	}
   487  
   488  	if _, err := db.GetEngine(ctx).
   489  		Where("repo_id = ? AND is_tag = ?", repo.ID, true).
   490  		In("lower_tag_name", lowerTags).
   491  		Delete(new(Release)); err != nil {
   492  		return fmt.Errorf("Delete: %w", err)
   493  	}
   494  
   495  	if _, err := db.GetEngine(ctx).
   496  		Where("repo_id = ? AND is_tag = ?", repo.ID, false).
   497  		In("lower_tag_name", lowerTags).
   498  		Cols("is_draft", "num_commits", "sha1").
   499  		Update(&Release{
   500  			IsDraft: true,
   501  		}); err != nil {
   502  		return fmt.Errorf("Update: %w", err)
   503  	}
   504  
   505  	return nil
   506  }
   507  
   508  // PushUpdateDeleteTag must be called for any push actions to delete tag
   509  func PushUpdateDeleteTag(ctx context.Context, repo *Repository, tagName string) error {
   510  	rel, err := GetRelease(ctx, repo.ID, tagName)
   511  	if err != nil {
   512  		if IsErrReleaseNotExist(err) {
   513  			return nil
   514  		}
   515  		return fmt.Errorf("GetRelease: %w", err)
   516  	}
   517  	if rel.IsTag {
   518  		if _, err = db.GetEngine(ctx).ID(rel.ID).Delete(new(Release)); err != nil {
   519  			return fmt.Errorf("Delete: %w", err)
   520  		}
   521  	} else {
   522  		rel.IsDraft = true
   523  		rel.NumCommits = 0
   524  		rel.Sha1 = ""
   525  		if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
   526  			return fmt.Errorf("Update: %w", err)
   527  		}
   528  	}
   529  
   530  	return nil
   531  }
   532  
   533  // SaveOrUpdateTag must be called for any push actions to add tag
   534  func SaveOrUpdateTag(ctx context.Context, repo *Repository, newRel *Release) error {
   535  	rel, err := GetRelease(ctx, repo.ID, newRel.TagName)
   536  	if err != nil && !IsErrReleaseNotExist(err) {
   537  		return fmt.Errorf("GetRelease: %w", err)
   538  	}
   539  
   540  	if rel == nil {
   541  		rel = newRel
   542  		if _, err = db.GetEngine(ctx).Insert(rel); err != nil {
   543  			return fmt.Errorf("InsertOne: %w", err)
   544  		}
   545  	} else {
   546  		rel.Sha1 = newRel.Sha1
   547  		rel.CreatedUnix = newRel.CreatedUnix
   548  		rel.NumCommits = newRel.NumCommits
   549  		rel.IsDraft = false
   550  		if rel.IsTag && newRel.PublisherID > 0 {
   551  			rel.PublisherID = newRel.PublisherID
   552  		}
   553  		if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
   554  			return fmt.Errorf("Update: %w", err)
   555  		}
   556  	}
   557  	return nil
   558  }
   559  
   560  // RemapExternalUser ExternalUserRemappable interface
   561  func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error {
   562  	r.OriginalAuthor = externalName
   563  	r.OriginalAuthorID = externalID
   564  	r.PublisherID = userID
   565  	return nil
   566  }
   567  
   568  // UserID ExternalUserRemappable interface
   569  func (r *Release) GetUserID() int64 { return r.PublisherID }
   570  
   571  // ExternalName ExternalUserRemappable interface
   572  func (r *Release) GetExternalName() string { return r.OriginalAuthor }
   573  
   574  // ExternalID ExternalUserRemappable interface
   575  func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID }
   576  
   577  // InsertReleases migrates release
   578  func InsertReleases(ctx context.Context, rels ...*Release) error {
   579  	ctx, committer, err := db.TxContext(ctx)
   580  	if err != nil {
   581  		return err
   582  	}
   583  	defer committer.Close()
   584  	sess := db.GetEngine(ctx)
   585  
   586  	for _, rel := range rels {
   587  		if _, err := sess.NoAutoTime().Insert(rel); err != nil {
   588  			return err
   589  		}
   590  
   591  		if len(rel.Attachments) > 0 {
   592  			for i := range rel.Attachments {
   593  				rel.Attachments[i].ReleaseID = rel.ID
   594  			}
   595  
   596  			if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
   597  				return err
   598  			}
   599  		}
   600  	}
   601  
   602  	return committer.Commit()
   603  }