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