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

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