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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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. RepoID int64
  201. HasSha1 util.OptionalBool // useful to find draft releases which are created with existing tags
  202. }
  203. func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond {
  204. opts.RepoID = repoID
  205. return opts.ToConds()
  206. }
  207. func (opts *FindReleasesOptions) ToConds() builder.Cond {
  208. cond := builder.NewCond()
  209. cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
  210. if !opts.IncludeDrafts {
  211. cond = cond.And(builder.Eq{"is_draft": false})
  212. }
  213. if !opts.IncludeTags {
  214. cond = cond.And(builder.Eq{"is_tag": false})
  215. }
  216. if len(opts.TagNames) > 0 {
  217. cond = cond.And(builder.In("tag_name", opts.TagNames))
  218. }
  219. if !opts.IsPreRelease.IsNone() {
  220. cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()})
  221. }
  222. if !opts.IsDraft.IsNone() {
  223. cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()})
  224. }
  225. if !opts.HasSha1.IsNone() {
  226. if opts.HasSha1.IsTrue() {
  227. cond = cond.And(builder.Neq{"sha1": ""})
  228. } else {
  229. cond = cond.And(builder.Eq{"sha1": ""})
  230. }
  231. }
  232. return cond
  233. }
  234. // GetReleasesByRepoID returns a list of releases of repository.
  235. func GetReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) ([]*Release, error) {
  236. sess := db.GetEngine(ctx).
  237. Desc("created_unix", "id").
  238. Where(opts.toConds(repoID))
  239. if opts.PageSize != 0 {
  240. sess = db.SetSessionPagination(sess, &opts.ListOptions)
  241. }
  242. rels := make([]*Release, 0, opts.PageSize)
  243. return rels, sess.Find(&rels)
  244. }
  245. // GetTagNamesByRepoID returns a list of release tag names of repository.
  246. func GetTagNamesByRepoID(ctx context.Context, repoID int64) ([]string, error) {
  247. listOptions := db.ListOptions{
  248. ListAll: true,
  249. }
  250. opts := FindReleasesOptions{
  251. ListOptions: listOptions,
  252. IncludeDrafts: true,
  253. IncludeTags: true,
  254. HasSha1: util.OptionalBoolTrue,
  255. }
  256. tags := make([]string, 0)
  257. sess := db.GetEngine(ctx).
  258. Table("release").
  259. Desc("created_unix", "id").
  260. Where(opts.toConds(repoID)).
  261. Cols("tag_name")
  262. return tags, sess.Find(&tags)
  263. }
  264. // CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID.
  265. func CountReleasesByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) {
  266. return db.GetEngine(ctx).Where(opts.toConds(repoID)).Count(new(Release))
  267. }
  268. // GetLatestReleaseByRepoID returns the latest release for a repository
  269. func GetLatestReleaseByRepoID(ctx context.Context, repoID int64) (*Release, error) {
  270. cond := builder.NewCond().
  271. And(builder.Eq{"repo_id": repoID}).
  272. And(builder.Eq{"is_draft": false}).
  273. And(builder.Eq{"is_prerelease": false}).
  274. And(builder.Eq{"is_tag": false})
  275. rel := new(Release)
  276. has, err := db.GetEngine(ctx).
  277. Desc("created_unix", "id").
  278. Where(cond).
  279. Get(rel)
  280. if err != nil {
  281. return nil, err
  282. } else if !has {
  283. return nil, ErrReleaseNotExist{0, "latest"}
  284. }
  285. return rel, nil
  286. }
  287. // GetReleasesByRepoIDAndNames returns a list of releases of repository according repoID and tagNames.
  288. func GetReleasesByRepoIDAndNames(ctx context.Context, repoID int64, tagNames []string) (rels []*Release, err error) {
  289. err = db.GetEngine(ctx).
  290. In("tag_name", tagNames).
  291. Desc("created_unix").
  292. Find(&rels, Release{RepoID: repoID})
  293. return rels, err
  294. }
  295. // GetReleaseCountByRepoID returns the count of releases of repository
  296. func GetReleaseCountByRepoID(ctx context.Context, repoID int64, opts FindReleasesOptions) (int64, error) {
  297. return db.GetEngine(ctx).Where(opts.toConds(repoID)).Count(&Release{})
  298. }
  299. type releaseMetaSearch struct {
  300. ID []int64
  301. Rel []*Release
  302. }
  303. func (s releaseMetaSearch) Len() int {
  304. return len(s.ID)
  305. }
  306. func (s releaseMetaSearch) Swap(i, j int) {
  307. s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
  308. s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
  309. }
  310. func (s releaseMetaSearch) Less(i, j int) bool {
  311. return s.ID[i] < s.ID[j]
  312. }
  313. func hasDuplicateName(attaches []*Attachment) bool {
  314. attachSet := container.Set[string]{}
  315. for _, attachment := range attaches {
  316. if attachSet.Contains(attachment.Name) {
  317. return true
  318. }
  319. attachSet.Add(attachment.Name)
  320. }
  321. return false
  322. }
  323. // GetReleaseAttachments retrieves the attachments for releases
  324. func GetReleaseAttachments(ctx context.Context, rels ...*Release) (err error) {
  325. if len(rels) == 0 {
  326. return nil
  327. }
  328. // To keep this efficient as possible sort all releases by id,
  329. // select attachments by release id,
  330. // then merge join them
  331. // Sort
  332. sortedRels := releaseMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Release, len(rels))}
  333. var attachments []*Attachment
  334. for index, element := range rels {
  335. element.Attachments = []*Attachment{}
  336. sortedRels.ID[index] = element.ID
  337. sortedRels.Rel[index] = element
  338. }
  339. sort.Sort(sortedRels)
  340. // Select attachments
  341. err = db.GetEngine(ctx).
  342. Asc("release_id", "name").
  343. In("release_id", sortedRels.ID).
  344. Find(&attachments)
  345. if err != nil {
  346. return err
  347. }
  348. // merge join
  349. currentIndex := 0
  350. for _, attachment := range attachments {
  351. for sortedRels.ID[currentIndex] < attachment.ReleaseID {
  352. currentIndex++
  353. }
  354. sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment)
  355. }
  356. // Makes URL's predictable
  357. for _, release := range rels {
  358. // If we have no Repo, we don't need to execute this loop
  359. if release.Repo == nil {
  360. continue
  361. }
  362. // If the names unique, use the URL with the Name instead of the UUID
  363. if !hasDuplicateName(release.Attachments) {
  364. for _, attachment := range release.Attachments {
  365. attachment.CustomDownloadURL = release.Repo.HTMLURL() + "/releases/download/" + url.PathEscape(release.TagName) + "/" + url.PathEscape(attachment.Name)
  366. }
  367. }
  368. }
  369. return err
  370. }
  371. type releaseSorter struct {
  372. rels []*Release
  373. }
  374. func (rs *releaseSorter) Len() int {
  375. return len(rs.rels)
  376. }
  377. func (rs *releaseSorter) Less(i, j int) bool {
  378. diffNum := rs.rels[i].NumCommits - rs.rels[j].NumCommits
  379. if diffNum != 0 {
  380. return diffNum > 0
  381. }
  382. return rs.rels[i].CreatedUnix > rs.rels[j].CreatedUnix
  383. }
  384. func (rs *releaseSorter) Swap(i, j int) {
  385. rs.rels[i], rs.rels[j] = rs.rels[j], rs.rels[i]
  386. }
  387. // SortReleases sorts releases by number of commits and created time.
  388. func SortReleases(rels []*Release) {
  389. sorter := &releaseSorter{rels: rels}
  390. sort.Sort(sorter)
  391. }
  392. // DeleteReleaseByID deletes a release from database by given ID.
  393. func DeleteReleaseByID(ctx context.Context, id int64) error {
  394. _, err := db.GetEngine(ctx).ID(id).Delete(new(Release))
  395. return err
  396. }
  397. // UpdateReleasesMigrationsByType updates all migrated repositories' releases from gitServiceType to replace originalAuthorID to posterID
  398. func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.GitServiceType, originalAuthorID string, posterID int64) error {
  399. _, err := db.GetEngine(ctx).Table("release").
  400. Where("repo_id IN (SELECT id FROM repository WHERE original_service_type = ?)", gitServiceType).
  401. And("original_author_id = ?", originalAuthorID).
  402. Update(map[string]any{
  403. "publisher_id": posterID,
  404. "original_author": "",
  405. "original_author_id": 0,
  406. })
  407. return err
  408. }
  409. // PushUpdateDeleteTagsContext updates a number of delete tags with context
  410. func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []string) error {
  411. if len(tags) == 0 {
  412. return nil
  413. }
  414. lowerTags := make([]string, 0, len(tags))
  415. for _, tag := range tags {
  416. lowerTags = append(lowerTags, strings.ToLower(tag))
  417. }
  418. if _, err := db.GetEngine(ctx).
  419. Where("repo_id = ? AND is_tag = ?", repo.ID, true).
  420. In("lower_tag_name", lowerTags).
  421. Delete(new(Release)); err != nil {
  422. return fmt.Errorf("Delete: %w", err)
  423. }
  424. if _, err := db.GetEngine(ctx).
  425. Where("repo_id = ? AND is_tag = ?", repo.ID, false).
  426. In("lower_tag_name", lowerTags).
  427. Cols("is_draft", "num_commits", "sha1").
  428. Update(&Release{
  429. IsDraft: true,
  430. }); err != nil {
  431. return fmt.Errorf("Update: %w", err)
  432. }
  433. return nil
  434. }
  435. // PushUpdateDeleteTag must be called for any push actions to delete tag
  436. func PushUpdateDeleteTag(ctx context.Context, repo *Repository, tagName string) error {
  437. rel, err := GetRelease(ctx, repo.ID, tagName)
  438. if err != nil {
  439. if IsErrReleaseNotExist(err) {
  440. return nil
  441. }
  442. return fmt.Errorf("GetRelease: %w", err)
  443. }
  444. if rel.IsTag {
  445. if _, err = db.GetEngine(ctx).ID(rel.ID).Delete(new(Release)); err != nil {
  446. return fmt.Errorf("Delete: %w", err)
  447. }
  448. } else {
  449. rel.IsDraft = true
  450. rel.NumCommits = 0
  451. rel.Sha1 = ""
  452. if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
  453. return fmt.Errorf("Update: %w", err)
  454. }
  455. }
  456. return nil
  457. }
  458. // SaveOrUpdateTag must be called for any push actions to add tag
  459. func SaveOrUpdateTag(ctx context.Context, repo *Repository, newRel *Release) error {
  460. rel, err := GetRelease(ctx, repo.ID, newRel.TagName)
  461. if err != nil && !IsErrReleaseNotExist(err) {
  462. return fmt.Errorf("GetRelease: %w", err)
  463. }
  464. if rel == nil {
  465. rel = newRel
  466. if _, err = db.GetEngine(ctx).Insert(rel); err != nil {
  467. return fmt.Errorf("InsertOne: %w", err)
  468. }
  469. } else {
  470. rel.Sha1 = newRel.Sha1
  471. rel.CreatedUnix = newRel.CreatedUnix
  472. rel.NumCommits = newRel.NumCommits
  473. rel.IsDraft = false
  474. if rel.IsTag && newRel.PublisherID > 0 {
  475. rel.PublisherID = newRel.PublisherID
  476. }
  477. if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
  478. return fmt.Errorf("Update: %w", err)
  479. }
  480. }
  481. return nil
  482. }
  483. // RemapExternalUser ExternalUserRemappable interface
  484. func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error {
  485. r.OriginalAuthor = externalName
  486. r.OriginalAuthorID = externalID
  487. r.PublisherID = userID
  488. return nil
  489. }
  490. // UserID ExternalUserRemappable interface
  491. func (r *Release) GetUserID() int64 { return r.PublisherID }
  492. // ExternalName ExternalUserRemappable interface
  493. func (r *Release) GetExternalName() string { return r.OriginalAuthor }
  494. // ExternalID ExternalUserRemappable interface
  495. func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID }
  496. // InsertReleases migrates release
  497. func InsertReleases(ctx context.Context, rels ...*Release) error {
  498. ctx, committer, err := db.TxContext(ctx)
  499. if err != nil {
  500. return err
  501. }
  502. defer committer.Close()
  503. sess := db.GetEngine(ctx)
  504. for _, rel := range rels {
  505. if _, err := sess.NoAutoTime().Insert(rel); err != nil {
  506. return err
  507. }
  508. if len(rel.Attachments) > 0 {
  509. for i := range rel.Attachments {
  510. rel.Attachments[i].ReleaseID = rel.ID
  511. }
  512. if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil {
  513. return err
  514. }
  515. }
  516. }
  517. return committer.Commit()
  518. }