1// Copyright 2014 The Gogs Authors. All rights reserved. 2// Copyright 2019 The Gitea Authors. All rights reserved. 3// Use of this source code is governed by a MIT-style 4// license that can be found in the LICENSE file. 5 6package models 7 8import ( 9 "context" 10 "errors" 11 "fmt" 12 "sort" 13 "strconv" 14 "strings" 15 16 "code.gitea.io/gitea/models/db" 17 repo_model "code.gitea.io/gitea/models/repo" 18 user_model "code.gitea.io/gitea/models/user" 19 "code.gitea.io/gitea/modules/structs" 20 "code.gitea.io/gitea/modules/timeutil" 21 "code.gitea.io/gitea/modules/util" 22 23 "xorm.io/builder" 24) 25 26// Release represents a release of repository. 27type Release struct { 28 ID int64 `xorm:"pk autoincr"` 29 RepoID int64 `xorm:"INDEX UNIQUE(n)"` 30 Repo *repo_model.Repository `xorm:"-"` 31 PublisherID int64 `xorm:"INDEX"` 32 Publisher *user_model.User `xorm:"-"` 33 TagName string `xorm:"INDEX UNIQUE(n)"` 34 OriginalAuthor string 35 OriginalAuthorID int64 `xorm:"index"` 36 LowerTagName string 37 Target string 38 Title string 39 Sha1 string `xorm:"VARCHAR(40)"` 40 NumCommits int64 41 NumCommitsBehind int64 `xorm:"-"` 42 Note string `xorm:"TEXT"` 43 RenderedNote string `xorm:"-"` 44 IsDraft bool `xorm:"NOT NULL DEFAULT false"` 45 IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` 46 IsTag bool `xorm:"NOT NULL DEFAULT false"` 47 Attachments []*repo_model.Attachment `xorm:"-"` 48 CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` 49} 50 51func init() { 52 db.RegisterModel(new(Release)) 53} 54 55func (r *Release) loadAttributes(e db.Engine) error { 56 var err error 57 if r.Repo == nil { 58 r.Repo, err = repo_model.GetRepositoryByID(r.RepoID) 59 if err != nil { 60 return err 61 } 62 } 63 if r.Publisher == nil { 64 r.Publisher, err = user_model.GetUserByIDEngine(e, r.PublisherID) 65 if err != nil { 66 if user_model.IsErrUserNotExist(err) { 67 r.Publisher = user_model.NewGhostUser() 68 } else { 69 return err 70 } 71 } 72 } 73 return getReleaseAttachments(e, r) 74} 75 76// LoadAttributes load repo and publisher attributes for a release 77func (r *Release) LoadAttributes() error { 78 return r.loadAttributes(db.GetEngine(db.DefaultContext)) 79} 80 81// APIURL the api url for a release. release must have attributes loaded 82func (r *Release) APIURL() string { 83 return r.Repo.APIURL() + "/releases/" + strconv.FormatInt(r.ID, 10) 84} 85 86// ZipURL the zip url for a release. release must have attributes loaded 87func (r *Release) ZipURL() string { 88 return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".zip" 89} 90 91// TarURL the tar.gz url for a release. release must have attributes loaded 92func (r *Release) TarURL() string { 93 return r.Repo.HTMLURL() + "/archive/" + util.PathEscapeSegments(r.TagName) + ".tar.gz" 94} 95 96// HTMLURL the url for a release on the web UI. release must have attributes loaded 97func (r *Release) HTMLURL() string { 98 return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName) 99} 100 101// IsReleaseExist returns true if release with given tag name already exists. 102func IsReleaseExist(repoID int64, tagName string) (bool, error) { 103 if len(tagName) == 0 { 104 return false, nil 105 } 106 107 return db.GetEngine(db.DefaultContext).Get(&Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}) 108} 109 110// InsertRelease inserts a release 111func InsertRelease(rel *Release) error { 112 _, err := db.GetEngine(db.DefaultContext).Insert(rel) 113 return err 114} 115 116// InsertReleasesContext insert releases 117func InsertReleasesContext(ctx context.Context, rels []*Release) error { 118 _, err := db.GetEngine(ctx).Insert(rels) 119 return err 120} 121 122// UpdateRelease updates all columns of a release 123func UpdateRelease(ctx context.Context, rel *Release) error { 124 _, err := db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel) 125 return err 126} 127 128// AddReleaseAttachments adds a release attachments 129func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) { 130 // Check attachments 131 attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, attachmentUUIDs) 132 if err != nil { 133 return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %v", attachmentUUIDs, err) 134 } 135 136 for i := range attachments { 137 if attachments[i].ReleaseID != 0 { 138 return errors.New("release permission denied") 139 } 140 attachments[i].ReleaseID = releaseID 141 // No assign value could be 0, so ignore AllCols(). 142 if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { 143 return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) 144 } 145 } 146 147 return 148} 149 150// GetRelease returns release by given ID. 151func GetRelease(repoID int64, tagName string) (*Release, error) { 152 isExist, err := IsReleaseExist(repoID, tagName) 153 if err != nil { 154 return nil, err 155 } else if !isExist { 156 return nil, ErrReleaseNotExist{0, tagName} 157 } 158 159 rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)} 160 _, err = db.GetEngine(db.DefaultContext).Get(rel) 161 return rel, err 162} 163 164// GetReleaseByID returns release with given ID. 165func GetReleaseByID(id int64) (*Release, error) { 166 rel := new(Release) 167 has, err := db.GetEngine(db.DefaultContext). 168 ID(id). 169 Get(rel) 170 if err != nil { 171 return nil, err 172 } else if !has { 173 return nil, ErrReleaseNotExist{id, ""} 174 } 175 176 return rel, nil 177} 178 179// FindReleasesOptions describes the conditions to Find releases 180type FindReleasesOptions struct { 181 db.ListOptions 182 IncludeDrafts bool 183 IncludeTags bool 184 IsPreRelease util.OptionalBool 185 IsDraft util.OptionalBool 186 TagNames []string 187} 188 189func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond { 190 cond := builder.NewCond() 191 cond = cond.And(builder.Eq{"repo_id": repoID}) 192 193 if !opts.IncludeDrafts { 194 cond = cond.And(builder.Eq{"is_draft": false}) 195 } 196 if !opts.IncludeTags { 197 cond = cond.And(builder.Eq{"is_tag": false}) 198 } 199 if len(opts.TagNames) > 0 { 200 cond = cond.And(builder.In("tag_name", opts.TagNames)) 201 } 202 if !opts.IsPreRelease.IsNone() { 203 cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()}) 204 } 205 if !opts.IsDraft.IsNone() { 206 cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()}) 207 } 208 return cond 209} 210 211// GetReleasesByRepoID returns a list of releases of repository. 212func GetReleasesByRepoID(repoID int64, opts FindReleasesOptions) ([]*Release, error) { 213 sess := db.GetEngine(db.DefaultContext). 214 Desc("created_unix", "id"). 215 Where(opts.toConds(repoID)) 216 217 if opts.PageSize != 0 { 218 sess = db.SetSessionPagination(sess, &opts.ListOptions) 219 } 220 221 rels := make([]*Release, 0, opts.PageSize) 222 return rels, sess.Find(&rels) 223} 224 225// CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID. 226func CountReleasesByRepoID(repoID int64, opts FindReleasesOptions) (int64, error) { 227 return db.GetEngine(db.DefaultContext).Where(opts.toConds(repoID)).Count(new(Release)) 228} 229 230// GetLatestReleaseByRepoID returns the latest release for a repository 231func GetLatestReleaseByRepoID(repoID int64) (*Release, error) { 232 cond := builder.NewCond(). 233 And(builder.Eq{"repo_id": repoID}). 234 And(builder.Eq{"is_draft": false}). 235 And(builder.Eq{"is_prerelease": false}). 236 And(builder.Eq{"is_tag": false}) 237 238 rel := new(Release) 239 has, err := db.GetEngine(db.DefaultContext). 240 Desc("created_unix", "id"). 241 Where(cond). 242 Get(rel) 243 if err != nil { 244 return nil, err 245 } else if !has { 246 return nil, ErrReleaseNotExist{0, "latest"} 247 } 248 249 return rel, nil 250} 251 252// GetReleasesByRepoIDAndNames returns a list of releases of repository according repoID and tagNames. 253func GetReleasesByRepoIDAndNames(ctx context.Context, repoID int64, tagNames []string) (rels []*Release, err error) { 254 err = db.GetEngine(ctx). 255 In("tag_name", tagNames). 256 Desc("created_unix"). 257 Find(&rels, Release{RepoID: repoID}) 258 return rels, err 259} 260 261// GetReleaseCountByRepoID returns the count of releases of repository 262func GetReleaseCountByRepoID(repoID int64, opts FindReleasesOptions) (int64, error) { 263 return db.GetEngine(db.DefaultContext).Where(opts.toConds(repoID)).Count(&Release{}) 264} 265 266type releaseMetaSearch struct { 267 ID []int64 268 Rel []*Release 269} 270 271func (s releaseMetaSearch) Len() int { 272 return len(s.ID) 273} 274 275func (s releaseMetaSearch) Swap(i, j int) { 276 s.ID[i], s.ID[j] = s.ID[j], s.ID[i] 277 s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i] 278} 279 280func (s releaseMetaSearch) Less(i, j int) bool { 281 return s.ID[i] < s.ID[j] 282} 283 284// GetReleaseAttachments retrieves the attachments for releases 285func GetReleaseAttachments(rels ...*Release) (err error) { 286 return getReleaseAttachments(db.GetEngine(db.DefaultContext), rels...) 287} 288 289func getReleaseAttachments(e db.Engine, rels ...*Release) (err error) { 290 if len(rels) == 0 { 291 return 292 } 293 294 // To keep this efficient as possible sort all releases by id, 295 // select attachments by release id, 296 // then merge join them 297 298 // Sort 299 sortedRels := releaseMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Release, len(rels))} 300 var attachments []*repo_model.Attachment 301 for index, element := range rels { 302 element.Attachments = []*repo_model.Attachment{} 303 sortedRels.ID[index] = element.ID 304 sortedRels.Rel[index] = element 305 } 306 sort.Sort(sortedRels) 307 308 // Select attachments 309 err = e. 310 Asc("release_id", "name"). 311 In("release_id", sortedRels.ID). 312 Find(&attachments, repo_model.Attachment{}) 313 if err != nil { 314 return err 315 } 316 317 // merge join 318 currentIndex := 0 319 for _, attachment := range attachments { 320 for sortedRels.ID[currentIndex] < attachment.ReleaseID { 321 currentIndex++ 322 } 323 sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment) 324 } 325 326 return 327} 328 329type releaseSorter struct { 330 rels []*Release 331} 332 333func (rs *releaseSorter) Len() int { 334 return len(rs.rels) 335} 336 337func (rs *releaseSorter) Less(i, j int) bool { 338 diffNum := rs.rels[i].NumCommits - rs.rels[j].NumCommits 339 if diffNum != 0 { 340 return diffNum > 0 341 } 342 return rs.rels[i].CreatedUnix > rs.rels[j].CreatedUnix 343} 344 345func (rs *releaseSorter) Swap(i, j int) { 346 rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i] 347} 348 349// SortReleases sorts releases by number of commits and created time. 350func SortReleases(rels []*Release) { 351 sorter := &releaseSorter{rels: rels} 352 sort.Sort(sorter) 353} 354 355// DeleteReleaseByID deletes a release from database by given ID. 356func DeleteReleaseByID(id int64) error { 357 _, err := db.GetEngine(db.DefaultContext).ID(id).Delete(new(Release)) 358 return err 359} 360 361// UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID 362func UpdateReleasesMigrationsByType(gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error { 363 _, err := db.GetEngine(db.DefaultContext).Table("release"). 364 Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType). 365 And("original_author_id = ?", originalAuthorID). 366 Update(map[string]interface{}{ 367 "publisher_id": posterID, 368 "original_author": "", 369 "original_author_id": 0, 370 }) 371 return err 372} 373 374// PushUpdateDeleteTagsContext updates a number of delete tags with context 375func PushUpdateDeleteTagsContext(ctx context.Context, repo *repo_model.Repository, tags []string) error { 376 return pushUpdateDeleteTags(db.GetEngine(ctx), repo, tags) 377} 378 379func pushUpdateDeleteTags(e db.Engine, repo *repo_model.Repository, tags []string) error { 380 if len(tags) == 0 { 381 return nil 382 } 383 lowerTags := make([]string, 0, len(tags)) 384 for _, tag := range tags { 385 lowerTags = append(lowerTags, strings.ToLower(tag)) 386 } 387 388 if _, err := e. 389 Where("repo_id = ? AND is_tag = ?", repo.ID, true). 390 In("lower_tag_name", lowerTags). 391 Delete(new(Release)); err != nil { 392 return fmt.Errorf("Delete: %v", err) 393 } 394 395 if _, err := e. 396 Where("repo_id = ? AND is_tag = ?", repo.ID, false). 397 In("lower_tag_name", lowerTags). 398 Cols("is_draft", "num_commits", "sha1"). 399 Update(&Release{ 400 IsDraft: true, 401 }); err != nil { 402 return fmt.Errorf("Update: %v", err) 403 } 404 405 return nil 406} 407 408// PushUpdateDeleteTag must be called for any push actions to delete tag 409func PushUpdateDeleteTag(repo *repo_model.Repository, tagName string) error { 410 rel, err := GetRelease(repo.ID, tagName) 411 if err != nil { 412 if IsErrReleaseNotExist(err) { 413 return nil 414 } 415 return fmt.Errorf("GetRelease: %v", err) 416 } 417 if rel.IsTag { 418 if _, err = db.GetEngine(db.DefaultContext).ID(rel.ID).Delete(new(Release)); err != nil { 419 return fmt.Errorf("Delete: %v", err) 420 } 421 } else { 422 rel.IsDraft = true 423 rel.NumCommits = 0 424 rel.Sha1 = "" 425 if _, err = db.GetEngine(db.DefaultContext).ID(rel.ID).AllCols().Update(rel); err != nil { 426 return fmt.Errorf("Update: %v", err) 427 } 428 } 429 430 return nil 431} 432 433// SaveOrUpdateTag must be called for any push actions to add tag 434func SaveOrUpdateTag(repo *repo_model.Repository, newRel *Release) error { 435 rel, err := GetRelease(repo.ID, newRel.TagName) 436 if err != nil && !IsErrReleaseNotExist(err) { 437 return fmt.Errorf("GetRelease: %v", err) 438 } 439 440 if rel == nil { 441 rel = newRel 442 if _, err = db.GetEngine(db.DefaultContext).Insert(rel); err != nil { 443 return fmt.Errorf("InsertOne: %v", err) 444 } 445 } else { 446 rel.Sha1 = newRel.Sha1 447 rel.CreatedUnix = newRel.CreatedUnix 448 rel.NumCommits = newRel.NumCommits 449 rel.IsDraft = false 450 if rel.IsTag && newRel.PublisherID > 0 { 451 rel.PublisherID = newRel.PublisherID 452 } 453 if _, err = db.GetEngine(db.DefaultContext).ID(rel.ID).AllCols().Update(rel); err != nil { 454 return fmt.Errorf("Update: %v", err) 455 } 456 } 457 return nil 458} 459