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 }