Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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