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 }