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.

532 lines
14 KiB

8 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
5 years ago
  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 loadLabels(labelTemplate string) ([]string, error) {
  114. list, err := GetLabelTemplateFile(labelTemplate)
  115. if err != nil {
  116. return nil, ErrIssueLabelTemplateLoad{labelTemplate, err}
  117. }
  118. labels := make([]string, len(list))
  119. for i := 0; i < len(list); i++ {
  120. labels[i] = list[i][0]
  121. }
  122. return labels, nil
  123. }
  124. // LoadLabelsFormatted loads the labels' list of a template file as a string separated by comma
  125. func LoadLabelsFormatted(labelTemplate string) (string, error) {
  126. labels, err := loadLabels(labelTemplate)
  127. return strings.Join(labels, ", "), err
  128. }
  129. func initalizeLabels(e Engine, repoID int64, labelTemplate string) error {
  130. list, err := GetLabelTemplateFile(labelTemplate)
  131. if err != nil {
  132. return ErrIssueLabelTemplateLoad{labelTemplate, err}
  133. }
  134. labels := make([]*Label, len(list))
  135. for i := 0; i < len(list); i++ {
  136. labels[i] = &Label{
  137. RepoID: repoID,
  138. Name: list[i][0],
  139. Description: list[i][2],
  140. Color: list[i][1],
  141. }
  142. }
  143. for _, label := range labels {
  144. if err = newLabel(e, label); err != nil {
  145. return err
  146. }
  147. }
  148. return nil
  149. }
  150. // InitalizeLabels adds a label set to a repository using a template
  151. func InitalizeLabels(ctx DBContext, repoID int64, labelTemplate string) error {
  152. return initalizeLabels(ctx.e, repoID, labelTemplate)
  153. }
  154. func newLabel(e Engine, label *Label) error {
  155. _, err := e.Insert(label)
  156. return err
  157. }
  158. // NewLabel creates a new label for a repository
  159. func NewLabel(label *Label) error {
  160. return newLabel(x, label)
  161. }
  162. // NewLabels creates new labels for a repository.
  163. func NewLabels(labels ...*Label) error {
  164. sess := x.NewSession()
  165. defer sess.Close()
  166. if err := sess.Begin(); err != nil {
  167. return err
  168. }
  169. for _, label := range labels {
  170. if err := newLabel(sess, label); err != nil {
  171. return err
  172. }
  173. }
  174. return sess.Commit()
  175. }
  176. // getLabelInRepoByName returns a label by Name in given repository.
  177. // If pass repoID as 0, then ORM will ignore limitation of repository
  178. // and can return arbitrary label with any valid ID.
  179. func getLabelInRepoByName(e Engine, repoID int64, labelName string) (*Label, error) {
  180. if len(labelName) == 0 {
  181. return nil, ErrLabelNotExist{0, repoID}
  182. }
  183. l := &Label{
  184. Name: labelName,
  185. RepoID: repoID,
  186. }
  187. has, err := e.Get(l)
  188. if err != nil {
  189. return nil, err
  190. } else if !has {
  191. return nil, ErrLabelNotExist{0, l.RepoID}
  192. }
  193. return l, nil
  194. }
  195. // getLabelInRepoByID returns a label by ID in given repository.
  196. // If pass repoID as 0, then ORM will ignore limitation of repository
  197. // and can return arbitrary label with any valid ID.
  198. func getLabelInRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
  199. if labelID <= 0 {
  200. return nil, ErrLabelNotExist{labelID, repoID}
  201. }
  202. l := &Label{
  203. ID: labelID,
  204. RepoID: repoID,
  205. }
  206. has, err := e.Get(l)
  207. if err != nil {
  208. return nil, err
  209. } else if !has {
  210. return nil, ErrLabelNotExist{l.ID, l.RepoID}
  211. }
  212. return l, nil
  213. }
  214. // GetLabelByID returns a label by given ID.
  215. func GetLabelByID(id int64) (*Label, error) {
  216. return getLabelInRepoByID(x, 0, id)
  217. }
  218. // GetLabelInRepoByName returns a label by name in given repository.
  219. func GetLabelInRepoByName(repoID int64, labelName string) (*Label, error) {
  220. return getLabelInRepoByName(x, repoID, labelName)
  221. }
  222. // GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
  223. // repository.
  224. // it silently ignores label names that do not belong to the repository.
  225. func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) {
  226. labelIDs := make([]int64, 0, len(labelNames))
  227. return labelIDs, x.Table("label").
  228. Where("repo_id = ?", repoID).
  229. In("name", labelNames).
  230. Asc("name").
  231. Cols("id").
  232. Find(&labelIDs)
  233. }
  234. // GetLabelIDsInReposByNames returns a list of labelIDs by names in one of the given
  235. // repositories.
  236. // it silently ignores label names that do not belong to the repository.
  237. func GetLabelIDsInReposByNames(repoIDs []int64, labelNames []string) ([]int64, error) {
  238. labelIDs := make([]int64, 0, len(labelNames))
  239. return labelIDs, x.Table("label").
  240. In("repo_id", repoIDs).
  241. In("name", labelNames).
  242. Asc("name").
  243. Cols("id").
  244. Find(&labelIDs)
  245. }
  246. // GetLabelInRepoByID returns a label by ID in given repository.
  247. func GetLabelInRepoByID(repoID, labelID int64) (*Label, error) {
  248. return getLabelInRepoByID(x, repoID, labelID)
  249. }
  250. // GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
  251. // it silently ignores label IDs that do not belong to the repository.
  252. func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
  253. labels := make([]*Label, 0, len(labelIDs))
  254. return labels, x.
  255. Where("repo_id = ?", repoID).
  256. In("id", labelIDs).
  257. Asc("name").
  258. Find(&labels)
  259. }
  260. func getLabelsByRepoID(e Engine, repoID int64, sortType string) ([]*Label, error) {
  261. labels := make([]*Label, 0, 10)
  262. sess := e.Where("repo_id = ?", repoID)
  263. switch sortType {
  264. case "reversealphabetically":
  265. sess.Desc("name")
  266. case "leastissues":
  267. sess.Asc("num_issues")
  268. case "mostissues":
  269. sess.Desc("num_issues")
  270. default:
  271. sess.Asc("name")
  272. }
  273. return labels, sess.Find(&labels)
  274. }
  275. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  276. func GetLabelsByRepoID(repoID int64, sortType string) ([]*Label, error) {
  277. return getLabelsByRepoID(x, repoID, sortType)
  278. }
  279. func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
  280. var labels []*Label
  281. return labels, e.Where("issue_label.issue_id = ?", issueID).
  282. Join("LEFT", "issue_label", "issue_label.label_id = label.id").
  283. Asc("label.name").
  284. Find(&labels)
  285. }
  286. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  287. func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
  288. return getLabelsByIssueID(x, issueID)
  289. }
  290. func updateLabel(e Engine, l *Label) error {
  291. _, err := e.ID(l.ID).
  292. SetExpr("num_issues",
  293. builder.Select("count(*)").From("issue_label").
  294. Where(builder.Eq{"label_id": l.ID}),
  295. ).
  296. SetExpr("num_closed_issues",
  297. builder.Select("count(*)").From("issue_label").
  298. InnerJoin("issue", "issue_label.issue_id = issue.id").
  299. Where(builder.Eq{
  300. "issue_label.label_id": l.ID,
  301. "issue.is_closed": true,
  302. }),
  303. ).
  304. AllCols().Update(l)
  305. return err
  306. }
  307. // UpdateLabel updates label information.
  308. func UpdateLabel(l *Label) error {
  309. return updateLabel(x, l)
  310. }
  311. // DeleteLabel delete a label of given repository.
  312. func DeleteLabel(repoID, labelID int64) error {
  313. _, err := GetLabelInRepoByID(repoID, labelID)
  314. if err != nil {
  315. if IsErrLabelNotExist(err) {
  316. return nil
  317. }
  318. return err
  319. }
  320. sess := x.NewSession()
  321. defer sess.Close()
  322. if err = sess.Begin(); err != nil {
  323. return err
  324. }
  325. if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
  326. return err
  327. } else if _, err = sess.
  328. Where("label_id = ?", labelID).
  329. Delete(new(IssueLabel)); err != nil {
  330. return err
  331. }
  332. // Clear label id in comment table
  333. if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Update(&Comment{}); err != nil {
  334. return err
  335. }
  336. return sess.Commit()
  337. }
  338. // .___ .____ ___. .__
  339. // | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
  340. // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
  341. // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
  342. // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
  343. // \/ \/ \/ \/ \/ \/ \/
  344. // IssueLabel represents an issue-label relation.
  345. type IssueLabel struct {
  346. ID int64 `xorm:"pk autoincr"`
  347. IssueID int64 `xorm:"UNIQUE(s)"`
  348. LabelID int64 `xorm:"UNIQUE(s)"`
  349. }
  350. func hasIssueLabel(e Engine, issueID, labelID int64) bool {
  351. has, _ := e.Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
  352. return has
  353. }
  354. // HasIssueLabel returns true if issue has been labeled.
  355. func HasIssueLabel(issueID, labelID int64) bool {
  356. return hasIssueLabel(x, issueID, labelID)
  357. }
  358. func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
  359. if _, err = e.Insert(&IssueLabel{
  360. IssueID: issue.ID,
  361. LabelID: label.ID,
  362. }); err != nil {
  363. return err
  364. }
  365. if err = issue.loadRepo(e); err != nil {
  366. return
  367. }
  368. var opts = &CreateCommentOptions{
  369. Type: CommentTypeLabel,
  370. Doer: doer,
  371. Repo: issue.Repo,
  372. Issue: issue,
  373. Label: label,
  374. Content: "1",
  375. }
  376. if _, err = createComment(e, opts); err != nil {
  377. return err
  378. }
  379. return updateLabel(e, label)
  380. }
  381. // NewIssueLabel creates a new issue-label relation.
  382. func NewIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
  383. if HasIssueLabel(issue.ID, label.ID) {
  384. return nil
  385. }
  386. sess := x.NewSession()
  387. defer sess.Close()
  388. if err = sess.Begin(); err != nil {
  389. return err
  390. }
  391. if err = newIssueLabel(sess, issue, label, doer); err != nil {
  392. return err
  393. }
  394. return sess.Commit()
  395. }
  396. func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label, doer *User) (err error) {
  397. for i := range labels {
  398. if hasIssueLabel(e, issue.ID, labels[i].ID) {
  399. continue
  400. }
  401. if err = newIssueLabel(e, issue, labels[i], doer); err != nil {
  402. return fmt.Errorf("newIssueLabel: %v", err)
  403. }
  404. }
  405. return nil
  406. }
  407. // NewIssueLabels creates a list of issue-label relations.
  408. func NewIssueLabels(issue *Issue, labels []*Label, doer *User) (err error) {
  409. sess := x.NewSession()
  410. defer sess.Close()
  411. if err = sess.Begin(); err != nil {
  412. return err
  413. }
  414. if err = newIssueLabels(sess, issue, labels, doer); err != nil {
  415. return err
  416. }
  417. return sess.Commit()
  418. }
  419. func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) {
  420. if count, err := e.Delete(&IssueLabel{
  421. IssueID: issue.ID,
  422. LabelID: label.ID,
  423. }); err != nil {
  424. return err
  425. } else if count == 0 {
  426. return nil
  427. }
  428. if err = issue.loadRepo(e); err != nil {
  429. return
  430. }
  431. var opts = &CreateCommentOptions{
  432. Type: CommentTypeLabel,
  433. Doer: doer,
  434. Repo: issue.Repo,
  435. Issue: issue,
  436. Label: label,
  437. }
  438. if _, err = createComment(e, opts); err != nil {
  439. return err
  440. }
  441. return updateLabel(e, label)
  442. }
  443. // DeleteIssueLabel deletes issue-label relation.
  444. func DeleteIssueLabel(issue *Issue, label *Label, doer *User) (err error) {
  445. sess := x.NewSession()
  446. defer sess.Close()
  447. if err = sess.Begin(); err != nil {
  448. return err
  449. }
  450. if err = deleteIssueLabel(sess, issue, label, doer); err != nil {
  451. return err
  452. }
  453. return sess.Commit()
  454. }