gitea源码

release.go 15KB

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