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.

issue_label.go 13KB

7 years ago
Allow cross-repository dependencies on issues (#7901) * in progress changes for #7405, added ability to add cross-repo dependencies * removed unused repolink var * fixed query that was breaking ci tests; fixed check in issue dependency add so that the id of the issue and dependency is checked rather than the indexes * reverted removal of string in local files becasue these are done via crowdin, not updated manually * removed 'Select("issue.*")' from getBlockedByDependencies and getBlockingDependencies based on comments in PR review * changed getBlockedByDependencies and getBlockingDependencies to use a more xorm-like query, also updated the sidebar as a result * simplified the getBlockingDependencies and getBlockedByDependencies methods; changed the sidebar to show the dependencies in a different format where you can see the name of the repository * made some changes to the issue view in the dependencies (issue name on top, repo full name on separate line). Change view of issue in the dependency search results (also showing the full repo name on separate line) * replace call to FindUserAccessibleRepoIDs with SearchRepositoryByName. The former was hardcoded to use isPrivate = false on the repo search, but this code needed it to be true. The SearchRepositoryByName method is used more in the code including on the user's dashboard * some more tweaks to the layout of the issues when showing dependencies and in the search box when you add new dependencies * added Name to the RepositoryMeta struct * updated swagger doc * fixed total count for link header on SearchIssues * fixed indentation * fixed aligment of remove icon on dependencies in issue sidebar * removed unnecessary nil check (unnecessary because issue.loadRepo is called prior to this block) * reverting .css change, somehow missed or forgot that less is used * updated less file and generated css; updated sidebar template with styles to line up delete and issue index * added ordering to the blocked by/depends on queries * fixed sorting in issue dependency search and the depends on/blocks views to show issues from the current repo first, then by created date descending; added a "all cross repository dependencies" setting to allow this feature to be turned off, if turned off, the issue dependency search will work the way it did before (restricted to the current repository) * re-applied my swagger changes after merge * fixed split string condition in issue search * changed ALLOW_CROSS_REPOSITORY_DEPENDENCIES description to sound more global than just the issue dependency search; returning 400 in the cross repo issue search api method if not enabled; fixed bug where the issue count did not respect the state parameter * when adding a dependency to an issue, added a check to make sure the issue and dependency are in the same repo if cross repo dependencies is not enabled * updated sortIssuesSession call in PullRequests, another commit moved this method from pull.go to pull_list.go so I had to re-apply my change here * fixed incorrect setting of user id parameter in search repos call
4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "fmt"
  7. "html/template"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. api "code.gitea.io/gitea/modules/structs"
  12. "xorm.io/builder"
  13. "xorm.io/xorm"
  14. )
  15. var labelColorPattern = regexp.MustCompile("#([a-fA-F0-9]{6})")
  16. // GetLabelTemplateFile loads the label template file by given name,
  17. // then parses and returns a list of name-color pairs and optionally description.
  18. func GetLabelTemplateFile(name string) ([][3]string, error) {
  19. data, err := getRepoInitFile("label", name)
  20. if err != nil {
  21. return nil, fmt.Errorf("getRepoInitFile: %v", err)
  22. }
  23. lines := strings.Split(string(data), "\n")
  24. list := make([][3]string, 0, len(lines))
  25. for i := 0; i < len(lines); i++ {
  26. line := strings.TrimSpace(lines[i])
  27. if len(line) == 0 {
  28. continue
  29. }
  30. parts := strings.SplitN(line, ";", 2)
  31. fields := strings.SplitN(parts[0], " ", 2)
  32. if len(fields) != 2 {
  33. return nil, fmt.Errorf("line is malformed: %s", line)
  34. }
  35. if !labelColorPattern.MatchString(fields[0]) {
  36. return nil, fmt.Errorf("bad HTML color code in line: %s", line)
  37. }
  38. var description string
  39. if len(parts) > 1 {
  40. description = strings.TrimSpace(parts[1])
  41. }
  42. fields[1] = strings.TrimSpace(fields[1])
  43. list = append(list, [3]string{fields[1], fields[0], description})
  44. }
  45. return list, nil
  46. }
  47. // Label represents a label of repository for issues.
  48. type Label struct {
  49. ID int64 `xorm:"pk autoincr"`
  50. RepoID int64 `xorm:"INDEX"`
  51. Name string
  52. Description string
  53. Color string `xorm:"VARCHAR(7)"`
  54. NumIssues int
  55. NumClosedIssues int
  56. NumOpenIssues int `xorm:"-"`
  57. IsChecked bool `xorm:"-"`
  58. QueryString string `xorm:"-"`
  59. IsSelected bool `xorm:"-"`
  60. IsExcluded bool `xorm:"-"`
  61. }
  62. // APIFormat converts a Label to the api.Label format
  63. func (label *Label) APIFormat() *api.Label {
  64. return &api.Label{
  65. ID: label.ID,
  66. Name: label.Name,
  67. Color: strings.TrimLeft(label.Color, "#"),
  68. Description: label.Description,
  69. }
  70. }
  71. // CalOpenIssues calculates the open issues of label.
  72. func (label *Label) CalOpenIssues() {
  73. label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
  74. }
  75. // LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
  76. func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) {
  77. var labelQuerySlice []string
  78. labelSelected := false
  79. labelID := strconv.FormatInt(label.ID, 10)
  80. for _, s := range currentSelectedLabels {
  81. if s == label.ID {
  82. labelSelected = true
  83. } else if -s == label.ID {
  84. labelSelected = true
  85. label.IsExcluded = true
  86. } else if s != 0 {
  87. labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
  88. }
  89. }
  90. if !labelSelected {
  91. labelQuerySlice = append(labelQuerySlice, labelID)
  92. }
  93. label.IsSelected = labelSelected
  94. label.QueryString = strings.Join(labelQuerySlice, ",")
  95. }
  96. // ForegroundColor calculates the text color for labels based
  97. // on their background color.
  98. func (label *Label) ForegroundColor() template.CSS {
  99. if strings.HasPrefix(label.Color, "#") {
  100. if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
  101. r := float32(0xFF & (color >> 16))
  102. g := float32(0xFF & (color >> 8))
  103. b := float32(0xFF & color)
  104. luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
  105. if luminance < 0.66 {
  106. return template.CSS("#fff")
  107. }
  108. }
  109. }
  110. // default to black
  111. return template.CSS("#000")
  112. }
  113. func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
  114. list, err := GetLabelTemplateFile(labelTemplate)
  115. if err != nil {
  116. return ErrIssueLabelTemplateLoad{labelTemplate, err}
  117. }
  118. labels := make([]*Label, len(list))
  119. for i := 0; i < len(list); i++ {
  120. labels[i] = &Label{
  121. RepoID: repoID,
  122. Name: list[i][0],
  123. Description: list[i][2],
  124. Color: list[i][1],
  125. }
  126. }
  127. for _, label := range labels {
  128. if err = newLabel(e, label); err != nil {
  129. return err
  130. }
  131. }
  132. return nil
  133. }
  134. // InitalizeLabels adds a label set to a repository using a template
  135. func InitalizeLabels(repoID int64, labelTemplate string) error {
  136. return initalizeLabels(x, repoID, labelTemplate)
  137. }
  138. func newLabel(e Engine, label *Label) error {
  139. _, err := e.Insert(label)
  140. return err
  141. }
  142. // NewLabel creates a new label for a repository
  143. func NewLabel(label *Label) error {
  144. return newLabel(x, label)
  145. }
  146. // NewLabels creates new labels for a repository.
  147. func NewLabels(labels ...*Label) error {
  148. sess := x.NewSession()
  149. defer sess.Close()
  150. if err := sess.Begin(); err != nil {
  151. return err
  152. }
  153. for _, label := range labels {
  154. if err := newLabel(sess, label); err != nil {
  155. return err
  156. }
  157. }
  158. return sess.Commit()
  159. }
  160. // getLabelInRepoByName returns a label by Name in given repository.
  161. // If pass repoID as 0, then ORM will ignore limitation of repository
  162. // and can return arbitrary label with any valid ID.
  163. func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, error) {
  164. if len(labelName) == 0 {
  165. return nil, ErrLabelNotExist{0, repoID}
  166. }
  167. l := &Label{
  168. Name: labelName,
  169. RepoID: repoID,
  170. }
  171. has, err := e.Get(l)
  172. if err != nil {
  173. return nil, err
  174. } else if !has {
  175. return nil, ErrLabelNotExist{0, l.RepoID}
  176. }
  177. return l, nil
  178. }
  179. // getLabelInRepoByID returns a label by ID in given repository.
  180. // If pass repoID as 0, then ORM will ignore limitation of repository
  181. // and can return arbitrary label with any valid ID.
  182. func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
  183. if labelID <= 0 {
  184. return nil, ErrLabelNotExist{labelID, repoID}
  185. }
  186. l := &Label{
  187. ID: labelID,
  188. RepoID: repoID,
  189. }
  190. has, err := e.Get(l)
  191. if err != nil {
  192. return nil, err
  193. } else if !has {
  194. return nil, ErrLabelNotExist{l.ID, l.RepoID}
  195. }
  196. return l, nil
  197. }
  198. // GetLabelByID returns a label by given ID.
  199. func GetLabelByID(id int64) (*Label, error) {
  200. return getLabelInRepoByID(x, 0, id)
  201. }
  202. // GetLabelInRepoByName returns a label by name in given repository.
  203. func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) {
  204. return getLabelInRepoByName(x, repoID, labelName)
  205. }
  206. // GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
  207. // repository.
  208. // it silently ignores label names that do not belong to the repository.
  209. func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) {
  210. labelIDs := make([]int64, 0, len(labelNames))
  211. return labelIDs, x.Table("label").
  212. Where("repo_id = ?", repoID).
  213. In("name", labelNames).
  214. Asc("name").
  215. Cols("id").
  216. Find(&labelIDs)
  217. }
  218. // GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given
  219. // repositories.
  220. // it silently ignores label names that do not belong to the repository.
  221. func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) {
  222. labelIDs := make([]int64, 0, len(labelNames))
  223. return labelIDs, x.Table("label").
  224. In("repo_id", repoIDs).
  225. In("name", labelNames).
  226. Asc("name").
  227. Cols("id").
  228. Find(&labelIDs)
  229. }
  230. // GetLabelInRepoByID returns a label by ID in given repository.
  231. func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) {
  232. return getLabelInRepoByID(x, repoID, labelID)
  233. }
  234. // GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
  235. // it silently ignores label IDs that do not belong to the repository.
  236. func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
  237. labels := make([]*Label, 0, len(labelIDs))
  238. return labels, x.
  239. Where("repo_id = ?", repoID).
  240. In("id", labelIDs).
  241. Asc("name").
  242. Find(&labels)
  243. }
  244. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  245. func GetLabelsByRepoID(repoID int64, sortType string) ([]*Label, error) {
  246. labels := make([]*Label, 0, 10)
  247. sess := x.Where("repo_id = ?", repoID)
  248. switch sortType {
  249. case "reversealphabetically":
  250. sess.Desc("name")
  251. case "leastissues":
  252. sess.Asc("num_issues")
  253. case "mostissues":
  254. sess.Desc("num_issues")
  255. default:
  256. sess.Asc("name")
  257. }
  258. return labels, sess.Find(&labels)
  259. }
  260. func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
  261. var labels []*Label
  262. return labels, e.Where("issue_label.issue_id = ?", issueID).
  263. Join("LEFT", "issue_label", "issue_label.label_id = label.id").
  264. Asc("label.name").
  265. Find(&labels)
  266. }
  267. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  268. func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
  269. return getLabelsByIssueID(x, issueID)
  270. }
  271. func updateLabel(e Engine, l *Label) error {
  272. _, err := e.ID(l.ID).
  273. SetExpr("num_issues",
  274. builder.Select("count(*)").From("issue_label").
  275. Where(builder.Eq{"label_id": l.ID}),
  276. ).
  277. SetExpr("num_closed_issues",
  278. builder.Select("count(*)").From("issue_label").
  279. InnerJoin("issue", "issue_label.issue_id = issue.id").
  280. Where(builder.Eq{
  281. "issue_label.label_id": l.ID,
  282. "issue.is_closed": true,
  283. }),
  284. ).
  285. AllCols().Update(l)
  286. return err
  287. }
  288. // UpdateLabel updates label information.
  289. func UpdateLabel(l *Label) error {
  290. return updateLabel(x, l)
  291. }
  292. // DeleteLabel delete a label of given repository.
  293. func DeleteLabel(repoID, labelID int64) error {
  294. _, err := GetLabelInRepoByID(repoID, labelID)
  295. if err != nil {
  296. if IsErrLabelNotExist(err) {
  297. return nil
  298. }
  299. return err
  300. }
  301. sess := x.NewSession()
  302. defer sess.Close()
  303. if err = sess.Begin(); err != nil {
  304. return err
  305. }
  306. if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
  307. return err
  308. } else if _, err = sess.
  309. Where("label_id = ?", labelID).
  310. Delete(new(IssueLabel)); err != nil {
  311. return err
  312. }
  313. // Clear label id in comment table
  314. if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Update(&Comment{}); err != nil {
  315. return err
  316. }
  317. return sess.Commit()
  318. }
  319. // .___ .____ ___. .__
  320. // | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
  321. // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
  322. // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
  323. // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
  324. // \/ \/ \/ \/ \/ \/ \/
  325. // IssueLabel represents an issue-label relation.
  326. type IssueLabel struct {
  327. ID int64 `xorm:"pk autoincr"`
  328. IssueID int64 `xorm:"UNIQUE(s)"`
  329. LabelID int64 `xorm:"UNIQUE(s)"`
  330. }
  331. func hasIssueLabel(e Engine, issueID, labelID int64) bool {
  332. has, _ := e.Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
  333. return has
  334. }
  335. // HasIssueLabel returns true if issue has been labeled.
  336. func HasIssueLabel(issueID, labelID int64) bool {
  337. return hasIssueLabel(x, issueID, labelID)
  338. }
  339. func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
  340. if _, err = e.Insert(&IssueLabel{
  341. IssueID: issue.ID,
  342. LabelID: label.ID,
  343. }); err != nil {
  344. return err
  345. }
  346. if err = issue.loadRepo(e); err != nil {
  347. return
  348. }
  349. if _, err = createLabelComment(e, doer, issue.Repo, issue, label, true); err != nil {
  350. return err
  351. }
  352. return updateLabel(e, label)
  353. }
  354. // NewIssueLabel creates a new issue-label relation.
  355. func NewIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
  356. if HasIssueLabel(issue.ID, label.ID) {
  357. return nil
  358. }
  359. sess := x.NewSession()
  360. defer sess.Close()
  361. if err = sess.Begin(); err != nil {
  362. return err
  363. }
  364. if err = newIssueLabel(sess, issue, label, doer); err != nil {
  365. return err
  366. }
  367. return sess.Commit()
  368. }
  369. func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label, doer *User) (err error) {
  370. for i := range labels {
  371. if hasIssueLabel(e, issue.ID, labels[i].ID) {
  372. continue
  373. }
  374. if err = newIssueLabel(e, issue, labels[i], doer); err != nil {
  375. return fmt.Errorf("newIssueLabel: %v", err)
  376. }
  377. }
  378. return nil
  379. }
  380. // NewIssueLabels creates a list of issue-label relations.
  381. func NewIssueLabels(issue *Issue, labels []*Label, doer *User) (err error) {
  382. sess := x.NewSession()
  383. defer sess.Close()
  384. if err = sess.Begin(); err != nil {
  385. return err
  386. }
  387. if err = newIssueLabels(sess, issue, labels, doer); err != nil {
  388. return err
  389. }
  390. return sess.Commit()
  391. }
  392. func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
  393. if count, err := e.Delete(&IssueLabel{
  394. IssueID: issue.ID,
  395. LabelID: label.ID,
  396. }); err != nil {
  397. return err
  398. } else if count == 0 {
  399. return nil
  400. }
  401. if err = issue.loadRepo(e); err != nil {
  402. return
  403. }
  404. if _, err = createLabelComment(e, doer, issue.Repo, issue, label, false); err != nil {
  405. return err
  406. }
  407. return updateLabel(e, label)
  408. }
  409. // DeleteIssueLabel deletes issue-label relation.
  410. func DeleteIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
  411. sess := x.NewSession()
  412. defer sess.Close()
  413. if err = sess.Begin(); err != nil {
  414. return err
  415. }
  416. if err = deleteIssueLabel(sess, issue, label, doer); err != nil {
  417. return err
  418. }
  419. return sess.Commit()
  420. }