gitea源码

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