Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

issue_label.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "fmt"
  7. "sort"
  8. "code.gitea.io/gitea/models/db"
  9. access_model "code.gitea.io/gitea/models/perm/access"
  10. user_model "code.gitea.io/gitea/models/user"
  11. "xorm.io/builder"
  12. )
  13. // IssueLabel represents an issue-label relation.
  14. type IssueLabel struct {
  15. ID int64 `xorm:"pk autoincr"`
  16. IssueID int64 `xorm:"UNIQUE(s)"`
  17. LabelID int64 `xorm:"UNIQUE(s)"`
  18. }
  19. // HasIssueLabel returns true if issue has been labeled.
  20. func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool {
  21. has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
  22. return has
  23. }
  24. // newIssueLabel this function creates a new label it does not check if the label is valid for the issue
  25. // YOU MUST CHECK THIS BEFORE THIS FUNCTION
  26. func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
  27. if err = db.Insert(ctx, &IssueLabel{
  28. IssueID: issue.ID,
  29. LabelID: label.ID,
  30. }); err != nil {
  31. return err
  32. }
  33. if err = issue.LoadRepo(ctx); err != nil {
  34. return err
  35. }
  36. opts := &CreateCommentOptions{
  37. Type: CommentTypeLabel,
  38. Doer: doer,
  39. Repo: issue.Repo,
  40. Issue: issue,
  41. Label: label,
  42. Content: "1",
  43. }
  44. if _, err = CreateComment(ctx, opts); err != nil {
  45. return err
  46. }
  47. issue.Labels = append(issue.Labels, label)
  48. return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
  49. }
  50. // Remove all issue labels in the given exclusive scope
  51. func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
  52. scope := label.ExclusiveScope()
  53. if scope == "" {
  54. return nil
  55. }
  56. var toRemove []*Label
  57. for _, issueLabel := range issue.Labels {
  58. if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope {
  59. toRemove = append(toRemove, issueLabel)
  60. }
  61. }
  62. for _, issueLabel := range toRemove {
  63. if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil {
  64. return err
  65. }
  66. }
  67. return nil
  68. }
  69. // NewIssueLabel creates a new issue-label relation.
  70. func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) {
  71. if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) {
  72. return nil
  73. }
  74. ctx, committer, err := db.TxContext(db.DefaultContext)
  75. if err != nil {
  76. return err
  77. }
  78. defer committer.Close()
  79. if err = issue.LoadRepo(ctx); err != nil {
  80. return err
  81. }
  82. // Do NOT add invalid labels
  83. if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID {
  84. return nil
  85. }
  86. if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil {
  87. return nil
  88. }
  89. if err = newIssueLabel(ctx, issue, label, doer); err != nil {
  90. return err
  91. }
  92. issue.Labels = nil
  93. if err = issue.LoadLabels(ctx); err != nil {
  94. return err
  95. }
  96. return committer.Commit()
  97. }
  98. // newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
  99. func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
  100. if err = issue.LoadRepo(ctx); err != nil {
  101. return err
  102. }
  103. if err = issue.LoadLabels(ctx); err != nil {
  104. return err
  105. }
  106. for _, l := range labels {
  107. // Don't add already present labels and invalid labels
  108. if HasIssueLabel(ctx, issue.ID, l.ID) ||
  109. (l.RepoID != issue.RepoID && l.OrgID != issue.Repo.OwnerID) {
  110. continue
  111. }
  112. if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, l, doer); err != nil {
  113. return err
  114. }
  115. if err = newIssueLabel(ctx, issue, l, doer); err != nil {
  116. return fmt.Errorf("newIssueLabel: %w", err)
  117. }
  118. }
  119. return nil
  120. }
  121. // NewIssueLabels creates a list of issue-label relations.
  122. func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
  123. ctx, committer, err := db.TxContext(db.DefaultContext)
  124. if err != nil {
  125. return err
  126. }
  127. defer committer.Close()
  128. if err = newIssueLabels(ctx, issue, labels, doer); err != nil {
  129. return err
  130. }
  131. issue.Labels = nil
  132. if err = issue.LoadLabels(ctx); err != nil {
  133. return err
  134. }
  135. return committer.Commit()
  136. }
  137. func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
  138. if count, err := db.DeleteByBean(ctx, &IssueLabel{
  139. IssueID: issue.ID,
  140. LabelID: label.ID,
  141. }); err != nil {
  142. return err
  143. } else if count == 0 {
  144. return nil
  145. }
  146. if err = issue.LoadRepo(ctx); err != nil {
  147. return err
  148. }
  149. opts := &CreateCommentOptions{
  150. Type: CommentTypeLabel,
  151. Doer: doer,
  152. Repo: issue.Repo,
  153. Issue: issue,
  154. Label: label,
  155. }
  156. if _, err = CreateComment(ctx, opts); err != nil {
  157. return err
  158. }
  159. return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
  160. }
  161. // DeleteIssueLabel deletes issue-label relation.
  162. func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error {
  163. if err := deleteIssueLabel(ctx, issue, label, doer); err != nil {
  164. return err
  165. }
  166. issue.Labels = nil
  167. return issue.LoadLabels(ctx)
  168. }
  169. // DeleteLabelsByRepoID deletes labels of some repository
  170. func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error {
  171. deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID})
  172. if _, err := db.GetEngine(ctx).In("label_id", deleteCond).
  173. Delete(&IssueLabel{}); err != nil {
  174. return err
  175. }
  176. _, err := db.DeleteByBean(ctx, &Label{RepoID: repoID})
  177. return err
  178. }
  179. // CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
  180. func CountOrphanedLabels(ctx context.Context) (int64, error) {
  181. noref, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count()
  182. if err != nil {
  183. return 0, err
  184. }
  185. norepo, err := db.GetEngine(ctx).Table("label").
  186. Where(builder.And(
  187. builder.Gt{"repo_id": 0},
  188. builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
  189. )).
  190. Count()
  191. if err != nil {
  192. return 0, err
  193. }
  194. noorg, err := db.GetEngine(ctx).Table("label").
  195. Where(builder.And(
  196. builder.Gt{"org_id": 0},
  197. builder.NotIn("org_id", builder.Select("id").From("`user`")),
  198. )).
  199. Count()
  200. if err != nil {
  201. return 0, err
  202. }
  203. return noref + norepo + noorg, nil
  204. }
  205. // DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
  206. func DeleteOrphanedLabels(ctx context.Context) error {
  207. // delete labels with no reference
  208. if _, err := db.GetEngine(ctx).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
  209. return err
  210. }
  211. // delete labels with none existing repos
  212. if _, err := db.GetEngine(ctx).
  213. Where(builder.And(
  214. builder.Gt{"repo_id": 0},
  215. builder.NotIn("repo_id", builder.Select("id").From("`repository`")),
  216. )).
  217. Delete(Label{}); err != nil {
  218. return err
  219. }
  220. // delete labels with none existing orgs
  221. if _, err := db.GetEngine(ctx).
  222. Where(builder.And(
  223. builder.Gt{"org_id": 0},
  224. builder.NotIn("org_id", builder.Select("id").From("`user`")),
  225. )).
  226. Delete(Label{}); err != nil {
  227. return err
  228. }
  229. return nil
  230. }
  231. // CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
  232. func CountOrphanedIssueLabels(ctx context.Context) (int64, error) {
  233. return db.GetEngine(ctx).Table("issue_label").
  234. NotIn("label_id", builder.Select("id").From("label")).
  235. Count()
  236. }
  237. // DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
  238. func DeleteOrphanedIssueLabels(ctx context.Context) error {
  239. _, err := db.GetEngine(ctx).
  240. NotIn("label_id", builder.Select("id").From("label")).
  241. Delete(IssueLabel{})
  242. return err
  243. }
  244. // CountIssueLabelWithOutsideLabels count label comments with outside label
  245. func CountIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
  246. return db.GetEngine(ctx).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")).
  247. Table("issue_label").
  248. Join("inner", "label", "issue_label.label_id = label.id ").
  249. Join("inner", "issue", "issue.id = issue_label.issue_id ").
  250. Join("inner", "repository", "issue.repo_id = repository.id").
  251. Count(new(IssueLabel))
  252. }
  253. // FixIssueLabelWithOutsideLabels fix label comments with outside label
  254. func FixIssueLabelWithOutsideLabels(ctx context.Context) (int64, error) {
  255. res, err := db.GetEngine(ctx).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
  256. SELECT il_too.id FROM (
  257. SELECT il_too_too.id
  258. FROM issue_label AS il_too_too
  259. INNER JOIN label ON il_too_too.label_id = label.id
  260. INNER JOIN issue on issue.id = il_too_too.issue_id
  261. INNER JOIN repository on repository.id = issue.repo_id
  262. WHERE
  263. (label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
  264. ) AS il_too )`)
  265. if err != nil {
  266. return 0, err
  267. }
  268. return res.RowsAffected()
  269. }
  270. // LoadLabels loads labels
  271. func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
  272. if issue.Labels == nil && issue.ID != 0 {
  273. issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
  274. if err != nil {
  275. return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
  276. }
  277. }
  278. return nil
  279. }
  280. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  281. func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) {
  282. var labels []*Label
  283. return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID).
  284. Join("LEFT", "issue_label", "issue_label.label_id = label.id").
  285. Asc("label.name").
  286. Find(&labels)
  287. }
  288. func clearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) (err error) {
  289. if err = issue.LoadLabels(ctx); err != nil {
  290. return fmt.Errorf("getLabels: %w", err)
  291. }
  292. for i := range issue.Labels {
  293. if err = deleteIssueLabel(ctx, issue, issue.Labels[i], doer); err != nil {
  294. return fmt.Errorf("removeLabel: %w", err)
  295. }
  296. }
  297. return nil
  298. }
  299. // ClearIssueLabels removes all issue labels as the given user.
  300. // Triggers appropriate WebHooks, if any.
  301. func ClearIssueLabels(issue *Issue, doer *user_model.User) (err error) {
  302. ctx, committer, err := db.TxContext(db.DefaultContext)
  303. if err != nil {
  304. return err
  305. }
  306. defer committer.Close()
  307. if err := issue.LoadRepo(ctx); err != nil {
  308. return err
  309. } else if err = issue.LoadPullRequest(ctx); err != nil {
  310. return err
  311. }
  312. perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
  313. if err != nil {
  314. return err
  315. }
  316. if !perm.CanWriteIssuesOrPulls(issue.IsPull) {
  317. return ErrRepoLabelNotExist{}
  318. }
  319. if err = clearIssueLabels(ctx, issue, doer); err != nil {
  320. return err
  321. }
  322. if err = committer.Commit(); err != nil {
  323. return fmt.Errorf("Commit: %w", err)
  324. }
  325. return nil
  326. }
  327. type labelSorter []*Label
  328. func (ts labelSorter) Len() int {
  329. return len([]*Label(ts))
  330. }
  331. func (ts labelSorter) Less(i, j int) bool {
  332. return []*Label(ts)[i].ID < []*Label(ts)[j].ID
  333. }
  334. func (ts labelSorter) Swap(i, j int) {
  335. []*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
  336. }
  337. // Ensure only one label of a given scope exists, with labels at the end of the
  338. // array getting preference over earlier ones.
  339. func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
  340. validLabels := make([]*Label, 0, len(labels))
  341. for i, label := range labels {
  342. scope := label.ExclusiveScope()
  343. if scope != "" {
  344. foundOther := false
  345. for _, otherLabel := range labels[i+1:] {
  346. if otherLabel.ExclusiveScope() == scope {
  347. foundOther = true
  348. break
  349. }
  350. }
  351. if foundOther {
  352. continue
  353. }
  354. }
  355. validLabels = append(validLabels, label)
  356. }
  357. return validLabels
  358. }
  359. // ReplaceIssueLabels removes all current labels and add new labels to the issue.
  360. // Triggers appropriate WebHooks, if any.
  361. func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
  362. ctx, committer, err := db.TxContext(db.DefaultContext)
  363. if err != nil {
  364. return err
  365. }
  366. defer committer.Close()
  367. if err = issue.LoadRepo(ctx); err != nil {
  368. return err
  369. }
  370. if err = issue.LoadLabels(ctx); err != nil {
  371. return err
  372. }
  373. labels = RemoveDuplicateExclusiveLabels(labels)
  374. sort.Sort(labelSorter(labels))
  375. sort.Sort(labelSorter(issue.Labels))
  376. var toAdd, toRemove []*Label
  377. addIndex, removeIndex := 0, 0
  378. for addIndex < len(labels) && removeIndex < len(issue.Labels) {
  379. addLabel := labels[addIndex]
  380. removeLabel := issue.Labels[removeIndex]
  381. if addLabel.ID == removeLabel.ID {
  382. // Silently drop invalid labels
  383. if removeLabel.RepoID != issue.RepoID && removeLabel.OrgID != issue.Repo.OwnerID {
  384. toRemove = append(toRemove, removeLabel)
  385. }
  386. addIndex++
  387. removeIndex++
  388. } else if addLabel.ID < removeLabel.ID {
  389. // Only add if the label is valid
  390. if addLabel.RepoID == issue.RepoID || addLabel.OrgID == issue.Repo.OwnerID {
  391. toAdd = append(toAdd, addLabel)
  392. }
  393. addIndex++
  394. } else {
  395. toRemove = append(toRemove, removeLabel)
  396. removeIndex++
  397. }
  398. }
  399. toAdd = append(toAdd, labels[addIndex:]...)
  400. toRemove = append(toRemove, issue.Labels[removeIndex:]...)
  401. if len(toAdd) > 0 {
  402. if err = newIssueLabels(ctx, issue, toAdd, doer); err != nil {
  403. return fmt.Errorf("addLabels: %w", err)
  404. }
  405. }
  406. for _, l := range toRemove {
  407. if err = deleteIssueLabel(ctx, issue, l, doer); err != nil {
  408. return fmt.Errorf("removeLabel: %w", err)
  409. }
  410. }
  411. issue.Labels = nil
  412. if err = issue.LoadLabels(ctx); err != nil {
  413. return err
  414. }
  415. return committer.Commit()
  416. }