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 16KB

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