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.

label.go 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2020 The Gitea Authors.
  3. // SPDX-License-Identifier: MIT
  4. package issues
  5. import (
  6. "context"
  7. "fmt"
  8. "strconv"
  9. "strings"
  10. "code.gitea.io/gitea/models/db"
  11. "code.gitea.io/gitea/modules/label"
  12. "code.gitea.io/gitea/modules/timeutil"
  13. "code.gitea.io/gitea/modules/util"
  14. "xorm.io/builder"
  15. )
  16. // ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
  17. type ErrRepoLabelNotExist struct {
  18. LabelID int64
  19. RepoID int64
  20. }
  21. // IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
  22. func IsErrRepoLabelNotExist(err error) bool {
  23. _, ok := err.(ErrRepoLabelNotExist)
  24. return ok
  25. }
  26. func (err ErrRepoLabelNotExist) Error() string {
  27. return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
  28. }
  29. func (err ErrRepoLabelNotExist) Unwrap() error {
  30. return util.ErrNotExist
  31. }
  32. // ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
  33. type ErrOrgLabelNotExist struct {
  34. LabelID int64
  35. OrgID int64
  36. }
  37. // IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
  38. func IsErrOrgLabelNotExist(err error) bool {
  39. _, ok := err.(ErrOrgLabelNotExist)
  40. return ok
  41. }
  42. func (err ErrOrgLabelNotExist) Error() string {
  43. return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
  44. }
  45. func (err ErrOrgLabelNotExist) Unwrap() error {
  46. return util.ErrNotExist
  47. }
  48. // ErrLabelNotExist represents a "LabelNotExist" kind of error.
  49. type ErrLabelNotExist struct {
  50. LabelID int64
  51. }
  52. // IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
  53. func IsErrLabelNotExist(err error) bool {
  54. _, ok := err.(ErrLabelNotExist)
  55. return ok
  56. }
  57. func (err ErrLabelNotExist) Error() string {
  58. return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
  59. }
  60. func (err ErrLabelNotExist) Unwrap() error {
  61. return util.ErrNotExist
  62. }
  63. // Label represents a label of repository for issues.
  64. type Label struct {
  65. ID int64 `xorm:"pk autoincr"`
  66. RepoID int64 `xorm:"INDEX"`
  67. OrgID int64 `xorm:"INDEX"`
  68. Name string
  69. Exclusive bool
  70. Description string
  71. Color string `xorm:"VARCHAR(7)"`
  72. NumIssues int
  73. NumClosedIssues int
  74. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  75. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  76. NumOpenIssues int `xorm:"-"`
  77. NumOpenRepoIssues int64 `xorm:"-"`
  78. IsChecked bool `xorm:"-"`
  79. QueryString string `xorm:"-"`
  80. IsSelected bool `xorm:"-"`
  81. IsExcluded bool `xorm:"-"`
  82. ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"`
  83. }
  84. func init() {
  85. db.RegisterModel(new(Label))
  86. db.RegisterModel(new(IssueLabel))
  87. }
  88. // CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
  89. func (l *Label) CalOpenIssues() {
  90. l.NumOpenIssues = l.NumIssues - l.NumClosedIssues
  91. }
  92. // SetArchived set the label as archived
  93. func (l *Label) SetArchived(isArchived bool) {
  94. if !isArchived {
  95. l.ArchivedUnix = timeutil.TimeStamp(0)
  96. } else if isArchived && l.ArchivedUnix.IsZero() {
  97. // Only change the date when it is newly archived.
  98. l.ArchivedUnix = timeutil.TimeStampNow()
  99. }
  100. }
  101. // CalOpenOrgIssues calculates the open issues of a label for a specific repo
  102. func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) {
  103. counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{
  104. RepoIDs: []int64{repoID},
  105. LabelIDs: []int64{labelID},
  106. IsClosed: util.OptionalBoolFalse,
  107. })
  108. for _, count := range counts {
  109. l.NumOpenRepoIssues += count
  110. }
  111. }
  112. // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
  113. func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) {
  114. var labelQuerySlice []string
  115. labelSelected := false
  116. labelID := strconv.FormatInt(l.ID, 10)
  117. labelScope := l.ExclusiveScope()
  118. for i, s := range currentSelectedLabels {
  119. if s == l.ID {
  120. labelSelected = true
  121. } else if -s == l.ID {
  122. labelSelected = true
  123. l.IsExcluded = true
  124. } else if s != 0 {
  125. // Exclude other labels in the same scope from selection
  126. if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
  127. labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
  128. }
  129. }
  130. }
  131. if !labelSelected {
  132. labelQuerySlice = append(labelQuerySlice, labelID)
  133. }
  134. l.IsSelected = labelSelected
  135. l.QueryString = strings.Join(labelQuerySlice, ",")
  136. }
  137. // BelongsToOrg returns true if label is an organization label
  138. func (l *Label) BelongsToOrg() bool {
  139. return l.OrgID > 0
  140. }
  141. // IsArchived returns true if label is an archived
  142. func (l *Label) IsArchived() bool {
  143. return l.ArchivedUnix > 0
  144. }
  145. // BelongsToRepo returns true if label is a repository label
  146. func (l *Label) BelongsToRepo() bool {
  147. return l.RepoID > 0
  148. }
  149. // Return scope substring of label name, or empty string if none exists
  150. func (l *Label) ExclusiveScope() string {
  151. if !l.Exclusive {
  152. return ""
  153. }
  154. lastIndex := strings.LastIndex(l.Name, "/")
  155. if lastIndex == -1 || lastIndex == 0 || lastIndex == len(l.Name)-1 {
  156. return ""
  157. }
  158. return l.Name[:lastIndex]
  159. }
  160. // NewLabel creates a new label
  161. func NewLabel(ctx context.Context, l *Label) error {
  162. color, err := label.NormalizeColor(l.Color)
  163. if err != nil {
  164. return err
  165. }
  166. l.Color = color
  167. return db.Insert(ctx, l)
  168. }
  169. // NewLabels creates new labels
  170. func NewLabels(ctx context.Context, labels ...*Label) error {
  171. ctx, committer, err := db.TxContext(ctx)
  172. if err != nil {
  173. return err
  174. }
  175. defer committer.Close()
  176. for _, l := range labels {
  177. color, err := label.NormalizeColor(l.Color)
  178. if err != nil {
  179. return err
  180. }
  181. l.Color = color
  182. if err := db.Insert(ctx, l); err != nil {
  183. return err
  184. }
  185. }
  186. return committer.Commit()
  187. }
  188. // UpdateLabel updates label information.
  189. func UpdateLabel(ctx context.Context, l *Label) error {
  190. color, err := label.NormalizeColor(l.Color)
  191. if err != nil {
  192. return err
  193. }
  194. l.Color = color
  195. return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
  196. }
  197. // DeleteLabel delete a label
  198. func DeleteLabel(ctx context.Context, id, labelID int64) error {
  199. l, err := GetLabelByID(ctx, labelID)
  200. if err != nil {
  201. if IsErrLabelNotExist(err) {
  202. return nil
  203. }
  204. return err
  205. }
  206. ctx, committer, err := db.TxContext(ctx)
  207. if err != nil {
  208. return err
  209. }
  210. defer committer.Close()
  211. sess := db.GetEngine(ctx)
  212. if l.BelongsToOrg() && l.OrgID != id {
  213. return nil
  214. }
  215. if l.BelongsToRepo() && l.RepoID != id {
  216. return nil
  217. }
  218. if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
  219. return err
  220. } else if _, err = sess.
  221. Where("label_id = ?", labelID).
  222. Delete(new(IssueLabel)); err != nil {
  223. return err
  224. }
  225. // delete comments about now deleted label_id
  226. if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil {
  227. return err
  228. }
  229. return committer.Commit()
  230. }
  231. // GetLabelByID returns a label by given ID.
  232. func GetLabelByID(ctx context.Context, labelID int64) (*Label, error) {
  233. if labelID <= 0 {
  234. return nil, ErrLabelNotExist{labelID}
  235. }
  236. l := &Label{}
  237. has, err := db.GetEngine(ctx).ID(labelID).Get(l)
  238. if err != nil {
  239. return nil, err
  240. } else if !has {
  241. return nil, ErrLabelNotExist{l.ID}
  242. }
  243. return l, nil
  244. }
  245. // GetLabelsByIDs returns a list of labels by IDs
  246. func GetLabelsByIDs(ctx context.Context, labelIDs []int64, cols ...string) ([]*Label, error) {
  247. labels := make([]*Label, 0, len(labelIDs))
  248. return labels, db.GetEngine(ctx).Table("label").
  249. In("id", labelIDs).
  250. Asc("name").
  251. Cols(cols...).
  252. Find(&labels)
  253. }
  254. // GetLabelInRepoByName returns a label by name in given repository.
  255. func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) {
  256. if len(labelName) == 0 || repoID <= 0 {
  257. return nil, ErrRepoLabelNotExist{0, repoID}
  258. }
  259. l := &Label{
  260. Name: labelName,
  261. RepoID: repoID,
  262. }
  263. has, err := db.GetByBean(ctx, l)
  264. if err != nil {
  265. return nil, err
  266. } else if !has {
  267. return nil, ErrRepoLabelNotExist{0, l.RepoID}
  268. }
  269. return l, nil
  270. }
  271. // GetLabelInRepoByID returns a label by ID in given repository.
  272. func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) {
  273. if labelID <= 0 || repoID <= 0 {
  274. return nil, ErrRepoLabelNotExist{labelID, repoID}
  275. }
  276. l := &Label{
  277. ID: labelID,
  278. RepoID: repoID,
  279. }
  280. has, err := db.GetByBean(ctx, l)
  281. if err != nil {
  282. return nil, err
  283. } else if !has {
  284. return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
  285. }
  286. return l, nil
  287. }
  288. // GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
  289. // repository.
  290. // it silently ignores label names that do not belong to the repository.
  291. func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []string) ([]int64, error) {
  292. labelIDs := make([]int64, 0, len(labelNames))
  293. return labelIDs, db.GetEngine(ctx).Table("label").
  294. Where("repo_id = ?", repoID).
  295. In("name", labelNames).
  296. Asc("name").
  297. Cols("id").
  298. Find(&labelIDs)
  299. }
  300. // BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
  301. func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
  302. return builder.Select("issue_label.issue_id").
  303. From("issue_label").
  304. InnerJoin("label", "label.id = issue_label.label_id").
  305. Where(
  306. builder.In("label.name", labelNames),
  307. ).
  308. GroupBy("issue_label.issue_id")
  309. }
  310. // GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
  311. // it silently ignores label IDs that do not belong to the repository.
  312. func GetLabelsInRepoByIDs(ctx context.Context, repoID int64, labelIDs []int64) ([]*Label, error) {
  313. labels := make([]*Label, 0, len(labelIDs))
  314. return labels, db.GetEngine(ctx).
  315. Where("repo_id = ?", repoID).
  316. In("id", labelIDs).
  317. Asc("name").
  318. Find(&labels)
  319. }
  320. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  321. func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) {
  322. if repoID <= 0 {
  323. return nil, ErrRepoLabelNotExist{0, repoID}
  324. }
  325. labels := make([]*Label, 0, 10)
  326. sess := db.GetEngine(ctx).Where("repo_id = ?", repoID)
  327. switch sortType {
  328. case "reversealphabetically":
  329. sess.Desc("name")
  330. case "leastissues":
  331. sess.Asc("num_issues")
  332. case "mostissues":
  333. sess.Desc("num_issues")
  334. default:
  335. sess.Asc("name")
  336. }
  337. if listOptions.Page != 0 {
  338. sess = db.SetSessionPagination(sess, &listOptions)
  339. }
  340. return labels, sess.Find(&labels)
  341. }
  342. // CountLabelsByRepoID count number of all labels that belong to given repository by ID.
  343. func CountLabelsByRepoID(ctx context.Context, repoID int64) (int64, error) {
  344. return db.GetEngine(ctx).Where("repo_id = ?", repoID).Count(&Label{})
  345. }
  346. // GetLabelInOrgByName returns a label by name in given organization.
  347. func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) {
  348. if len(labelName) == 0 || orgID <= 0 {
  349. return nil, ErrOrgLabelNotExist{0, orgID}
  350. }
  351. l := &Label{
  352. Name: labelName,
  353. OrgID: orgID,
  354. }
  355. has, err := db.GetByBean(ctx, l)
  356. if err != nil {
  357. return nil, err
  358. } else if !has {
  359. return nil, ErrOrgLabelNotExist{0, l.OrgID}
  360. }
  361. return l, nil
  362. }
  363. // GetLabelInOrgByID returns a label by ID in given organization.
  364. func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error) {
  365. if labelID <= 0 || orgID <= 0 {
  366. return nil, ErrOrgLabelNotExist{labelID, orgID}
  367. }
  368. l := &Label{
  369. ID: labelID,
  370. OrgID: orgID,
  371. }
  372. has, err := db.GetByBean(ctx, l)
  373. if err != nil {
  374. return nil, err
  375. } else if !has {
  376. return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
  377. }
  378. return l, nil
  379. }
  380. // GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given
  381. // organization.
  382. func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) {
  383. if orgID <= 0 {
  384. return nil, ErrOrgLabelNotExist{0, orgID}
  385. }
  386. labelIDs := make([]int64, 0, len(labelNames))
  387. return labelIDs, db.GetEngine(ctx).Table("label").
  388. Where("org_id = ?", orgID).
  389. In("name", labelNames).
  390. Asc("name").
  391. Cols("id").
  392. Find(&labelIDs)
  393. }
  394. // GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
  395. // it silently ignores label IDs that do not belong to the organization.
  396. func GetLabelsInOrgByIDs(ctx context.Context, orgID int64, labelIDs []int64) ([]*Label, error) {
  397. labels := make([]*Label, 0, len(labelIDs))
  398. return labels, db.GetEngine(ctx).
  399. Where("org_id = ?", orgID).
  400. In("id", labelIDs).
  401. Asc("name").
  402. Find(&labels)
  403. }
  404. // GetLabelsByOrgID returns all labels that belong to given organization by ID.
  405. func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) {
  406. if orgID <= 0 {
  407. return nil, ErrOrgLabelNotExist{0, orgID}
  408. }
  409. labels := make([]*Label, 0, 10)
  410. sess := db.GetEngine(ctx).Where("org_id = ?", orgID)
  411. switch sortType {
  412. case "reversealphabetically":
  413. sess.Desc("name")
  414. case "leastissues":
  415. sess.Asc("num_issues")
  416. case "mostissues":
  417. sess.Desc("num_issues")
  418. default:
  419. sess.Asc("name")
  420. }
  421. if listOptions.Page != 0 {
  422. sess = db.SetSessionPagination(sess, &listOptions)
  423. }
  424. return labels, sess.Find(&labels)
  425. }
  426. // GetLabelIDsByNames returns a list of labelIDs by names.
  427. // It doesn't filter them by repo or org, so it could return labels belonging to different repos/orgs.
  428. // It's used for filtering issues via indexer, otherwise it would be useless.
  429. // Since it could return labels with the same name, so the length of returned ids could be more than the length of names.
  430. func GetLabelIDsByNames(ctx context.Context, labelNames []string) ([]int64, error) {
  431. labelIDs := make([]int64, 0, len(labelNames))
  432. return labelIDs, db.GetEngine(ctx).Table("label").
  433. In("name", labelNames).
  434. Cols("id").
  435. Find(&labelIDs)
  436. }
  437. // CountLabelsByOrgID count all labels that belong to given organization by ID.
  438. func CountLabelsByOrgID(ctx context.Context, orgID int64) (int64, error) {
  439. return db.GetEngine(ctx).Where("org_id = ?", orgID).Count(&Label{})
  440. }
  441. func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
  442. _, err := db.GetEngine(ctx).ID(l.ID).
  443. SetExpr("num_issues",
  444. builder.Select("count(*)").From("issue_label").
  445. Where(builder.Eq{"label_id": l.ID}),
  446. ).
  447. SetExpr("num_closed_issues",
  448. builder.Select("count(*)").From("issue_label").
  449. InnerJoin("issue", "issue_label.issue_id = issue.id").
  450. Where(builder.Eq{
  451. "issue_label.label_id": l.ID,
  452. "issue.is_closed": true,
  453. }),
  454. ).
  455. Cols(cols...).Update(l)
  456. return err
  457. }