You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

release.go 17KB

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