1
0

issue_label.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. package database
  2. import (
  3. "fmt"
  4. "html/template"
  5. "strconv"
  6. "strings"
  7. "github.com/cockroachdb/errors"
  8. api "github.com/gogs/go-gogs-client"
  9. "gorm.io/gorm"
  10. "gogs.io/gogs/internal/errutil"
  11. "gogs.io/gogs/internal/lazyregexp"
  12. )
  13. var labelColorPattern = lazyregexp.New("#([a-fA-F0-9]{6})")
  14. // GetLabelTemplateFile loads the label template file by given name,
  15. // then parses and returns a list of name-color pairs.
  16. func GetLabelTemplateFile(name string) ([][2]string, error) {
  17. data, err := getRepoInitFile("label", name)
  18. if err != nil {
  19. return nil, errors.Newf("getRepoInitFile: %v", err)
  20. }
  21. lines := strings.Split(string(data), "\n")
  22. list := make([][2]string, 0, len(lines))
  23. for i := 0; i < len(lines); i++ {
  24. line := strings.TrimSpace(lines[i])
  25. if line == "" {
  26. continue
  27. }
  28. fields := strings.SplitN(line, " ", 2)
  29. if len(fields) != 2 {
  30. return nil, errors.Newf("line is malformed: %s", line)
  31. }
  32. if !labelColorPattern.MatchString(fields[0]) {
  33. return nil, errors.Newf("bad HTML color code in line: %s", line)
  34. }
  35. fields[1] = strings.TrimSpace(fields[1])
  36. list = append(list, [2]string{fields[1], fields[0]})
  37. }
  38. return list, nil
  39. }
  40. // Label represents a label of repository for issues.
  41. type Label struct {
  42. ID int64
  43. RepoID int64 `gorm:"index"`
  44. Name string
  45. Color string `gorm:"type:varchar(7)"`
  46. NumIssues int
  47. NumClosedIssues int
  48. NumOpenIssues int `gorm:"-" json:"-"`
  49. IsChecked bool `gorm:"-" json:"-"`
  50. }
  51. func (l *Label) APIFormat() *api.Label {
  52. return &api.Label{
  53. ID: l.ID,
  54. Name: l.Name,
  55. Color: strings.TrimLeft(l.Color, "#"),
  56. }
  57. }
  58. // CalOpenIssues calculates the open issues of label.
  59. func (l *Label) CalOpenIssues() {
  60. l.NumOpenIssues = l.NumIssues - l.NumClosedIssues
  61. }
  62. // ForegroundColor calculates the text color for labels based
  63. // on their background color.
  64. func (l *Label) ForegroundColor() template.CSS {
  65. if strings.HasPrefix(l.Color, "#") {
  66. if color, err := strconv.ParseUint(l.Color[1:], 16, 64); err == nil {
  67. r := float32(0xFF & (color >> 16))
  68. g := float32(0xFF & (color >> 8))
  69. b := float32(0xFF & color)
  70. luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
  71. if luminance < 0.66 {
  72. return template.CSS("#fff")
  73. }
  74. }
  75. }
  76. // default to black
  77. return template.CSS("#000")
  78. }
  79. // NewLabels creates new label(s) for a repository.
  80. func NewLabels(labels ...*Label) error {
  81. return db.Create(labels).Error
  82. }
  83. var _ errutil.NotFound = (*ErrLabelNotExist)(nil)
  84. type ErrLabelNotExist struct {
  85. args map[string]any
  86. }
  87. func IsErrLabelNotExist(err error) bool {
  88. _, ok := err.(ErrLabelNotExist)
  89. return ok
  90. }
  91. func (err ErrLabelNotExist) Error() string {
  92. return fmt.Sprintf("label does not exist: %v", err.args)
  93. }
  94. func (ErrLabelNotExist) NotFound() bool {
  95. return true
  96. }
  97. // getLabelOfRepoByName returns a label by Name in given repository.
  98. // If pass repoID as 0, then ORM will ignore limitation of repository
  99. // and can return arbitrary label with any valid ID.
  100. func getLabelOfRepoByName(tx *gorm.DB, repoID int64, labelName string) (*Label, error) {
  101. if len(labelName) <= 0 {
  102. return nil, ErrLabelNotExist{args: map[string]any{"repoID": repoID}}
  103. }
  104. l := &Label{}
  105. query := tx.Where("name = ?", labelName)
  106. if repoID > 0 {
  107. query = query.Where("repo_id = ?", repoID)
  108. }
  109. err := query.First(l).Error
  110. if err != nil {
  111. if errors.Is(err, gorm.ErrRecordNotFound) {
  112. return nil, ErrLabelNotExist{args: map[string]any{"repoID": repoID}}
  113. }
  114. return nil, err
  115. }
  116. return l, nil
  117. }
  118. // getLabelInRepoByID returns a label by ID in given repository.
  119. // If pass repoID as 0, then ORM will ignore limitation of repository
  120. // and can return arbitrary label with any valid ID.
  121. func getLabelOfRepoByID(tx *gorm.DB, repoID, labelID int64) (*Label, error) {
  122. if labelID <= 0 {
  123. return nil, ErrLabelNotExist{args: map[string]any{"repoID": repoID, "labelID": labelID}}
  124. }
  125. l := &Label{}
  126. query := tx.Where("id = ?", labelID)
  127. if repoID > 0 {
  128. query = query.Where("repo_id = ?", repoID)
  129. }
  130. err := query.First(l).Error
  131. if err != nil {
  132. if errors.Is(err, gorm.ErrRecordNotFound) {
  133. return nil, ErrLabelNotExist{args: map[string]any{"repoID": repoID, "labelID": labelID}}
  134. }
  135. return nil, err
  136. }
  137. return l, nil
  138. }
  139. // GetLabelByID returns a label by given ID.
  140. func GetLabelByID(id int64) (*Label, error) {
  141. return getLabelOfRepoByID(db, 0, id)
  142. }
  143. // GetLabelOfRepoByID returns a label by ID in given repository.
  144. func GetLabelOfRepoByID(repoID, labelID int64) (*Label, error) {
  145. return getLabelOfRepoByID(db, repoID, labelID)
  146. }
  147. // GetLabelOfRepoByName returns a label by name in given repository.
  148. func GetLabelOfRepoByName(repoID int64, labelName string) (*Label, error) {
  149. return getLabelOfRepoByName(db, repoID, labelName)
  150. }
  151. // GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
  152. // it silently ignores label IDs that are not belong to the repository.
  153. func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
  154. labels := make([]*Label, 0, len(labelIDs))
  155. return labels, db.Where("repo_id = ? AND id IN ?", repoID, labelIDs).Order("name ASC").Find(&labels).Error
  156. }
  157. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  158. func GetLabelsByRepoID(repoID int64) ([]*Label, error) {
  159. labels := make([]*Label, 0, 10)
  160. return labels, db.Where("repo_id = ?", repoID).Order("name ASC").Find(&labels).Error
  161. }
  162. func getLabelsByIssueID(tx *gorm.DB, issueID int64) ([]*Label, error) {
  163. issueLabels, err := getIssueLabels(tx, issueID)
  164. if err != nil {
  165. return nil, errors.Newf("getIssueLabels: %v", err)
  166. } else if len(issueLabels) == 0 {
  167. return []*Label{}, nil
  168. }
  169. labelIDs := make([]int64, len(issueLabels))
  170. for i := range issueLabels {
  171. labelIDs[i] = issueLabels[i].LabelID
  172. }
  173. labels := make([]*Label, 0, len(labelIDs))
  174. return labels, tx.Where("id > 0 AND id IN ?", labelIDs).Order("name ASC").Find(&labels).Error
  175. }
  176. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  177. func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
  178. return getLabelsByIssueID(db, issueID)
  179. }
  180. func updateLabel(tx *gorm.DB, l *Label) error {
  181. return tx.Model(l).Where("id = ?", l.ID).Updates(l).Error
  182. }
  183. // UpdateLabel updates label information.
  184. func UpdateLabel(l *Label) error {
  185. return updateLabel(db, l)
  186. }
  187. // DeleteLabel delete a label of given repository.
  188. func DeleteLabel(repoID, labelID int64) error {
  189. _, err := GetLabelOfRepoByID(repoID, labelID)
  190. if err != nil {
  191. if IsErrLabelNotExist(err) {
  192. return nil
  193. }
  194. return err
  195. }
  196. return db.Transaction(func(tx *gorm.DB) error {
  197. if err := tx.Where("id = ?", labelID).Delete(new(Label)).Error; err != nil {
  198. return err
  199. }
  200. if err := tx.Where("label_id = ?", labelID).Delete(new(IssueLabel)).Error; err != nil {
  201. return err
  202. }
  203. return nil
  204. })
  205. }
  206. // .___ .____ ___. .__
  207. // | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
  208. // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
  209. // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
  210. // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
  211. // \/ \/ \/ \/ \/ \/ \/
  212. // IssueLabel represents an issue-lable relation.
  213. type IssueLabel struct {
  214. ID int64
  215. IssueID int64 `gorm:"uniqueIndex:issue_label_unique"`
  216. LabelID int64 `gorm:"uniqueIndex:issue_label_unique"`
  217. }
  218. func hasIssueLabel(tx *gorm.DB, issueID, labelID int64) bool {
  219. var count int64
  220. tx.Model(new(IssueLabel)).Where("issue_id = ? AND label_id = ?", issueID, labelID).Count(&count)
  221. return count > 0
  222. }
  223. // HasIssueLabel returns true if issue has been labeled.
  224. func HasIssueLabel(issueID, labelID int64) bool {
  225. return hasIssueLabel(db, issueID, labelID)
  226. }
  227. func newIssueLabel(tx *gorm.DB, issue *Issue, label *Label) (err error) {
  228. if err = tx.Create(&IssueLabel{
  229. IssueID: issue.ID,
  230. LabelID: label.ID,
  231. }).Error; err != nil {
  232. return err
  233. }
  234. label.NumIssues++
  235. if issue.IsClosed {
  236. label.NumClosedIssues++
  237. }
  238. if err = updateLabel(tx, label); err != nil {
  239. return errors.Newf("updateLabel: %v", err)
  240. }
  241. issue.Labels = append(issue.Labels, label)
  242. return nil
  243. }
  244. // NewIssueLabel creates a new issue-label relation.
  245. func NewIssueLabel(issue *Issue, label *Label) (err error) {
  246. if HasIssueLabel(issue.ID, label.ID) {
  247. return nil
  248. }
  249. return db.Transaction(func(tx *gorm.DB) error {
  250. return newIssueLabel(tx, issue, label)
  251. })
  252. }
  253. func newIssueLabels(tx *gorm.DB, issue *Issue, labels []*Label) (err error) {
  254. for i := range labels {
  255. if hasIssueLabel(tx, issue.ID, labels[i].ID) {
  256. continue
  257. }
  258. if err = newIssueLabel(tx, issue, labels[i]); err != nil {
  259. return errors.Newf("newIssueLabel: %v", err)
  260. }
  261. }
  262. return nil
  263. }
  264. // NewIssueLabels creates a list of issue-label relations.
  265. func NewIssueLabels(issue *Issue, labels []*Label) (err error) {
  266. return db.Transaction(func(tx *gorm.DB) error {
  267. return newIssueLabels(tx, issue, labels)
  268. })
  269. }
  270. func getIssueLabels(tx *gorm.DB, issueID int64) ([]*IssueLabel, error) {
  271. issueLabels := make([]*IssueLabel, 0, 10)
  272. return issueLabels, tx.Where("issue_id = ?", issueID).Order("label_id ASC").Find(&issueLabels).Error
  273. }
  274. // GetIssueLabels returns all issue-label relations of given issue by ID.
  275. func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
  276. return getIssueLabels(db, issueID)
  277. }
  278. func deleteIssueLabel(tx *gorm.DB, issue *Issue, label *Label) (err error) {
  279. if err = tx.Where("issue_id = ? AND label_id = ?", issue.ID, label.ID).Delete(&IssueLabel{}).Error; err != nil {
  280. return err
  281. }
  282. label.NumIssues--
  283. if issue.IsClosed {
  284. label.NumClosedIssues--
  285. }
  286. if err = updateLabel(tx, label); err != nil {
  287. return errors.Newf("updateLabel: %v", err)
  288. }
  289. for i := range issue.Labels {
  290. if issue.Labels[i].ID == label.ID {
  291. issue.Labels = append(issue.Labels[:i], issue.Labels[i+1:]...)
  292. break
  293. }
  294. }
  295. return nil
  296. }
  297. // DeleteIssueLabel deletes issue-label relation.
  298. func DeleteIssueLabel(issue *Issue, label *Label) (err error) {
  299. return db.Transaction(func(tx *gorm.DB) error {
  300. return deleteIssueLabel(tx, issue, label)
  301. })
  302. }