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.

1621 lines
41 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. // Copyright 2014 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. "bytes"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "mime/multipart"
  11. "os"
  12. "path"
  13. "strconv"
  14. "strings"
  15. "time"
  16. "github.com/Unknwon/com"
  17. "github.com/go-xorm/xorm"
  18. "github.com/gogits/gogs/modules/base"
  19. "github.com/gogits/gogs/modules/log"
  20. "github.com/gogits/gogs/modules/setting"
  21. gouuid "github.com/gogits/gogs/modules/uuid"
  22. )
  23. var (
  24. ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
  25. ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
  26. ErrMissingIssueNumber = errors.New("No issue number specified")
  27. )
  28. // Issue represents an issue or pull request of repository.
  29. type Issue struct {
  30. ID int64 `xorm:"pk autoincr"`
  31. RepoID int64 `xorm:"INDEX"`
  32. Index int64 // Index in one repository.
  33. Name string
  34. Repo *Repository `xorm:"-"`
  35. PosterID int64
  36. Poster *User `xorm:"-"`
  37. Labels []*Label `xorm:"-"`
  38. MilestoneID int64
  39. Milestone *Milestone `xorm:"-"`
  40. AssigneeID int64
  41. Assignee *User `xorm:"-"`
  42. IsRead bool `xorm:"-"`
  43. IsPull bool // Indicates whether is a pull request or not.
  44. IsClosed bool
  45. Content string `xorm:"TEXT"`
  46. RenderedContent string `xorm:"-"`
  47. Priority int
  48. NumComments int
  49. Deadline time.Time
  50. Created time.Time `xorm:"CREATED"`
  51. Updated time.Time `xorm:"UPDATED"`
  52. Attachments []*Attachment `xorm:"-"`
  53. Comments []*Comment `xorm:"-"`
  54. }
  55. // HashTag returns unique hash tag for issue.
  56. func (i *Issue) HashTag() string {
  57. return "issue-" + com.ToStr(i.ID)
  58. }
  59. func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
  60. var err error
  61. switch colName {
  62. case "id":
  63. i.Attachments, err = GetAttachmentsByIssueID(i.ID)
  64. if err != nil {
  65. log.Error(3, "GetAttachmentsByIssueID[%d]: %v", i.ID, err)
  66. }
  67. i.Comments, err = GetCommentsByIssueID(i.ID)
  68. if err != nil {
  69. log.Error(3, "GetCommentsByIssueID[%d]: %v", i.ID, err)
  70. }
  71. case "milestone_id":
  72. if i.MilestoneID == 0 {
  73. return
  74. }
  75. i.Milestone, err = GetMilestoneByID(i.MilestoneID)
  76. if err != nil {
  77. log.Error(3, "GetMilestoneById[%d]: %v", i.ID, err)
  78. }
  79. case "assignee_id":
  80. if i.AssigneeID == 0 {
  81. return
  82. }
  83. i.Assignee, err = GetUserByID(i.AssigneeID)
  84. if err != nil {
  85. log.Error(3, "GetUserByID[%d]: %v", i.ID, err)
  86. }
  87. }
  88. }
  89. // IsPoster returns true if given user by ID is the poster.
  90. func (i *Issue) IsPoster(uid int64) bool {
  91. return i.PosterID == uid
  92. }
  93. func (i *Issue) GetPoster() (err error) {
  94. i.Poster, err = GetUserByID(i.PosterID)
  95. if IsErrUserNotExist(err) {
  96. i.PosterID = -1
  97. i.Poster = NewFakeUser()
  98. return nil
  99. }
  100. return err
  101. }
  102. func (i *Issue) hasLabel(e Engine, labelID int64) bool {
  103. return hasIssueLabel(e, i.ID, labelID)
  104. }
  105. // HasLabel returns true if issue has been labeled by given ID.
  106. func (i *Issue) HasLabel(labelID int64) bool {
  107. return i.hasLabel(x, labelID)
  108. }
  109. func (i *Issue) addLabel(e *xorm.Session, label *Label) error {
  110. return newIssueLabel(e, i, label)
  111. }
  112. // AddLabel adds new label to issue by given ID.
  113. func (i *Issue) AddLabel(label *Label) (err error) {
  114. sess := x.NewSession()
  115. defer sessionRelease(sess)
  116. if err = sess.Begin(); err != nil {
  117. return err
  118. }
  119. if err = i.addLabel(sess, label); err != nil {
  120. return err
  121. }
  122. return sess.Commit()
  123. }
  124. func (i *Issue) getLabels(e Engine) (err error) {
  125. if len(i.Labels) > 0 {
  126. return nil
  127. }
  128. i.Labels, err = getLabelsByIssueID(e, i.ID)
  129. if err != nil {
  130. return fmt.Errorf("getLabelsByIssueID: %v", err)
  131. }
  132. return nil
  133. }
  134. // GetLabels retrieves all labels of issue and assign to corresponding field.
  135. func (i *Issue) GetLabels() error {
  136. return i.getLabels(x)
  137. }
  138. func (i *Issue) removeLabel(e *xorm.Session, label *Label) error {
  139. return deleteIssueLabel(e, i, label)
  140. }
  141. // RemoveLabel removes a label from issue by given ID.
  142. func (i *Issue) RemoveLabel(label *Label) (err error) {
  143. sess := x.NewSession()
  144. defer sessionRelease(sess)
  145. if err = sess.Begin(); err != nil {
  146. return err
  147. }
  148. if err = i.removeLabel(sess, label); err != nil {
  149. return err
  150. }
  151. return sess.Commit()
  152. }
  153. func (i *Issue) ClearLabels() (err error) {
  154. sess := x.NewSession()
  155. defer sessionRelease(sess)
  156. if err = sess.Begin(); err != nil {
  157. return err
  158. }
  159. if err = i.getLabels(sess); err != nil {
  160. return err
  161. }
  162. for idx := range i.Labels {
  163. if err = i.removeLabel(sess, i.Labels[idx]); err != nil {
  164. return err
  165. }
  166. }
  167. return sess.Commit()
  168. }
  169. func (i *Issue) GetAssignee() (err error) {
  170. if i.AssigneeID == 0 || i.Assignee != nil {
  171. return nil
  172. }
  173. i.Assignee, err = GetUserByID(i.AssigneeID)
  174. if IsErrUserNotExist(err) {
  175. return nil
  176. }
  177. return err
  178. }
  179. // ReadBy sets issue to be read by given user.
  180. func (i *Issue) ReadBy(uid int64) error {
  181. return UpdateIssueUserByRead(uid, i.ID)
  182. }
  183. func (i *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err error) {
  184. if i.IsClosed == isClosed {
  185. return nil
  186. }
  187. i.IsClosed = isClosed
  188. if err = updateIssue(e, i); err != nil {
  189. return err
  190. } else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil {
  191. return err
  192. }
  193. // Update labels.
  194. if err = i.getLabels(e); err != nil {
  195. return err
  196. }
  197. for idx := range i.Labels {
  198. if i.IsClosed {
  199. i.Labels[idx].NumClosedIssues++
  200. } else {
  201. i.Labels[idx].NumClosedIssues--
  202. }
  203. if err = updateLabel(e, i.Labels[idx]); err != nil {
  204. return err
  205. }
  206. }
  207. // Update milestone.
  208. if err = changeMilestoneIssueStats(e, i); err != nil {
  209. return err
  210. }
  211. // New action comment.
  212. if _, err = createStatusComment(e, doer, i.Repo, i); err != nil {
  213. return err
  214. }
  215. return nil
  216. }
  217. // ChangeStatus changes issue status to open/closed.
  218. func (i *Issue) ChangeStatus(doer *User, isClosed bool) (err error) {
  219. sess := x.NewSession()
  220. defer sessionRelease(sess)
  221. if err = sess.Begin(); err != nil {
  222. return err
  223. }
  224. if err = i.changeStatus(sess, doer, isClosed); err != nil {
  225. return err
  226. }
  227. return sess.Commit()
  228. }
  229. // CreateIssue creates new issue with labels for repository.
  230. func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  231. // Check attachments.
  232. attachments := make([]*Attachment, 0, len(uuids))
  233. for _, uuid := range uuids {
  234. attach, err := GetAttachmentByUUID(uuid)
  235. if err != nil {
  236. if IsErrAttachmentNotExist(err) {
  237. continue
  238. }
  239. return fmt.Errorf("GetAttachmentByUUID[%s]: %v", uuid, err)
  240. }
  241. attachments = append(attachments, attach)
  242. }
  243. sess := x.NewSession()
  244. defer sessionRelease(sess)
  245. if err = sess.Begin(); err != nil {
  246. return err
  247. }
  248. if _, err = sess.Insert(issue); err != nil {
  249. return err
  250. } else if _, err = sess.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil {
  251. return err
  252. }
  253. var label *Label
  254. for _, id := range labelIDs {
  255. if id == 0 {
  256. continue
  257. }
  258. label, err = getLabelByID(sess, id)
  259. if err != nil {
  260. return err
  261. }
  262. if err = issue.addLabel(sess, label); err != nil {
  263. return fmt.Errorf("addLabel: %v", err)
  264. }
  265. }
  266. if issue.MilestoneID > 0 {
  267. if err = changeMilestoneAssign(sess, 0, issue); err != nil {
  268. return err
  269. }
  270. }
  271. if err = newIssueUsers(sess, repo, issue); err != nil {
  272. return err
  273. }
  274. for i := range attachments {
  275. attachments[i].IssueID = issue.ID
  276. // No assign value could be 0, so ignore AllCols().
  277. if _, err = sess.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  278. return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
  279. }
  280. }
  281. // Notify watchers.
  282. act := &Action{
  283. ActUserID: issue.Poster.Id,
  284. ActUserName: issue.Poster.Name,
  285. ActEmail: issue.Poster.Email,
  286. OpType: CREATE_ISSUE,
  287. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
  288. RepoID: repo.ID,
  289. RepoUserName: repo.Owner.Name,
  290. RepoName: repo.Name,
  291. IsPrivate: repo.IsPrivate,
  292. }
  293. if err = notifyWatchers(sess, act); err != nil {
  294. return err
  295. }
  296. return sess.Commit()
  297. }
  298. // GetIssueByRef returns an Issue specified by a GFM reference.
  299. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  300. func GetIssueByRef(ref string) (issue *Issue, err error) {
  301. var issueNumber int64
  302. var repo *Repository
  303. n := strings.IndexByte(ref, byte('#'))
  304. if n == -1 {
  305. return nil, ErrMissingIssueNumber
  306. }
  307. if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil {
  308. return
  309. }
  310. if repo, err = GetRepositoryByRef(ref[:n]); err != nil {
  311. return
  312. }
  313. return GetIssueByIndex(repo.ID, issueNumber)
  314. }
  315. // GetIssueByIndex returns issue by given index in repository.
  316. func GetIssueByIndex(repoID, index int64) (*Issue, error) {
  317. issue := &Issue{
  318. RepoID: repoID,
  319. Index: index,
  320. }
  321. has, err := x.Get(issue)
  322. if err != nil {
  323. return nil, err
  324. } else if !has {
  325. return nil, ErrIssueNotExist{0, repoID, index}
  326. }
  327. return issue, nil
  328. }
  329. // GetIssueByID returns an issue by given ID.
  330. func GetIssueByID(id int64) (*Issue, error) {
  331. issue := new(Issue)
  332. has, err := x.Id(id).Get(issue)
  333. if err != nil {
  334. return nil, err
  335. } else if !has {
  336. return nil, ErrIssueNotExist{id, 0, 0}
  337. }
  338. return issue, nil
  339. }
  340. // Issues returns a list of issues by given conditions.
  341. func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labels, sortType string) ([]*Issue, error) {
  342. sess := x.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  343. if repoID > 0 {
  344. sess.Where("issue.repo_id=?", repoID).And("issue.is_closed=?", isClosed)
  345. } else {
  346. sess.Where("issue.is_closed=?", isClosed)
  347. }
  348. if assigneeID > 0 {
  349. sess.And("issue.assignee_id=?", assigneeID)
  350. } else if posterID > 0 {
  351. sess.And("issue.poster_id=?", posterID)
  352. }
  353. if milestoneID > 0 {
  354. sess.And("issue.milestone_id=?", milestoneID)
  355. }
  356. switch sortType {
  357. case "oldest":
  358. sess.Asc("created")
  359. case "recentupdate":
  360. sess.Desc("updated")
  361. case "leastupdate":
  362. sess.Asc("updated")
  363. case "mostcomment":
  364. sess.Desc("num_comments")
  365. case "leastcomment":
  366. sess.Asc("num_comments")
  367. case "priority":
  368. sess.Desc("priority")
  369. default:
  370. sess.Desc("created")
  371. }
  372. labelIDs := base.StringsToInt64s(strings.Split(labels, ","))
  373. if len(labelIDs) > 0 {
  374. validJoin := false
  375. queryStr := "issue.id=issue_label.issue_id"
  376. for _, id := range labelIDs {
  377. if id == 0 {
  378. continue
  379. }
  380. validJoin = true
  381. queryStr += " AND issue_label.label_id=" + com.ToStr(id)
  382. }
  383. if validJoin {
  384. sess.Join("INNER", "issue_label", queryStr)
  385. }
  386. }
  387. if isMention {
  388. queryStr := "issue.id=issue_user.issue_id AND issue_user.is_mentioned=1"
  389. if uid > 0 {
  390. queryStr += " AND issue_user.uid=" + com.ToStr(uid)
  391. }
  392. sess.Join("INNER", "issue_user", queryStr)
  393. }
  394. issues := make([]*Issue, 0, setting.IssuePagingNum)
  395. return issues, sess.Find(&issues)
  396. }
  397. type IssueStatus int
  398. const (
  399. IS_OPEN = iota + 1
  400. IS_CLOSE
  401. )
  402. // GetIssueCountByPoster returns number of issues of repository by poster.
  403. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  404. count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  405. return count
  406. }
  407. // .___ ____ ___
  408. // | | ______ ________ __ ____ | | \______ ___________
  409. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  410. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  411. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  412. // \/ \/ \/ \/ \/
  413. // IssueUser represents an issue-user relation.
  414. type IssueUser struct {
  415. ID int64 `xorm:"pk autoincr"`
  416. UID int64 `xorm:"uid INDEX"` // User ID.
  417. IssueID int64
  418. RepoID int64 `xorm:"INDEX"`
  419. MilestoneID int64
  420. IsRead bool
  421. IsAssigned bool
  422. IsMentioned bool
  423. IsPoster bool
  424. IsClosed bool
  425. }
  426. func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
  427. users, err := repo.GetAssignees()
  428. if err != nil {
  429. return err
  430. }
  431. iu := &IssueUser{
  432. IssueID: issue.ID,
  433. RepoID: repo.ID,
  434. }
  435. // Poster can be anyone.
  436. isNeedAddPoster := true
  437. for _, u := range users {
  438. iu.ID = 0
  439. iu.UID = u.Id
  440. iu.IsPoster = iu.UID == issue.PosterID
  441. if isNeedAddPoster && iu.IsPoster {
  442. isNeedAddPoster = false
  443. }
  444. iu.IsAssigned = iu.UID == issue.AssigneeID
  445. if _, err = e.Insert(iu); err != nil {
  446. return err
  447. }
  448. }
  449. if isNeedAddPoster {
  450. iu.ID = 0
  451. iu.UID = issue.PosterID
  452. iu.IsPoster = true
  453. if _, err = e.Insert(iu); err != nil {
  454. return err
  455. }
  456. }
  457. return nil
  458. }
  459. // NewIssueUsers adds new issue-user relations for new issue of repository.
  460. func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
  461. sess := x.NewSession()
  462. defer sessionRelease(sess)
  463. if err = sess.Begin(); err != nil {
  464. return err
  465. }
  466. if err = newIssueUsers(sess, repo, issue); err != nil {
  467. return err
  468. }
  469. return sess.Commit()
  470. }
  471. // PairsContains returns true when pairs list contains given issue.
  472. func PairsContains(ius []*IssueUser, issueId, uid int64) int {
  473. for i := range ius {
  474. if ius[i].IssueID == issueId &&
  475. ius[i].UID == uid {
  476. return i
  477. }
  478. }
  479. return -1
  480. }
  481. // GetIssueUsers returns issue-user pairs by given repository and user.
  482. func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  483. ius := make([]*IssueUser, 0, 10)
  484. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
  485. return ius, err
  486. }
  487. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  488. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  489. if len(rids) == 0 {
  490. return []*IssueUser{}, nil
  491. }
  492. buf := bytes.NewBufferString("")
  493. for _, rid := range rids {
  494. buf.WriteString("repo_id=")
  495. buf.WriteString(com.ToStr(rid))
  496. buf.WriteString(" OR ")
  497. }
  498. cond := strings.TrimSuffix(buf.String(), " OR ")
  499. ius := make([]*IssueUser, 0, 10)
  500. sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  501. if len(cond) > 0 {
  502. sess.And(cond)
  503. }
  504. err := sess.Find(&ius)
  505. return ius, err
  506. }
  507. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  508. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  509. ius := make([]*IssueUser, 0, 10)
  510. sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  511. if rid > 0 {
  512. sess.And("repo_id=?", rid)
  513. }
  514. switch filterMode {
  515. case FM_ASSIGN:
  516. sess.And("is_assigned=?", true)
  517. case FM_CREATE:
  518. sess.And("is_poster=?", true)
  519. default:
  520. return ius, nil
  521. }
  522. err := sess.Find(&ius)
  523. return ius, err
  524. }
  525. // IssueStats represents issue statistic information.
  526. type IssueStats struct {
  527. OpenCount, ClosedCount int64
  528. AllCount int64
  529. AssignCount int64
  530. CreateCount int64
  531. MentionCount int64
  532. }
  533. // Filter modes.
  534. const (
  535. FM_ALL = iota
  536. FM_ASSIGN
  537. FM_CREATE
  538. FM_MENTION
  539. )
  540. func parseCountResult(results []map[string][]byte) int64 {
  541. if len(results) == 0 {
  542. return 0
  543. }
  544. for _, result := range results[0] {
  545. return com.StrTo(string(result)).MustInt64()
  546. }
  547. return 0
  548. }
  549. // GetIssueStats returns issue statistic information by given conditions.
  550. func GetIssueStats(repoID, uid, labelID, milestoneID, assigneeID int64, isShowClosed bool, filterMode int) *IssueStats {
  551. stats := &IssueStats{}
  552. // issue := new(Issue)
  553. queryStr := "SELECT COUNT(*) FROM `issue` "
  554. if labelID > 0 {
  555. queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(labelID)
  556. }
  557. baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?"
  558. if milestoneID > 0 {
  559. baseCond += " AND issue.milestone_id=" + com.ToStr(milestoneID)
  560. }
  561. if assigneeID > 0 {
  562. baseCond += " AND assignee_id=" + com.ToStr(assigneeID)
  563. }
  564. switch filterMode {
  565. case FM_ALL, FM_ASSIGN:
  566. resutls, _ := x.Query(queryStr+baseCond, repoID, false)
  567. stats.OpenCount = parseCountResult(resutls)
  568. resutls, _ = x.Query(queryStr+baseCond, repoID, true)
  569. stats.ClosedCount = parseCountResult(resutls)
  570. case FM_CREATE:
  571. baseCond += " AND poster_id=?"
  572. resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid)
  573. stats.OpenCount = parseCountResult(resutls)
  574. resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid)
  575. stats.ClosedCount = parseCountResult(resutls)
  576. case FM_MENTION:
  577. queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id"
  578. baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?"
  579. resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid, true)
  580. stats.OpenCount = parseCountResult(resutls)
  581. resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid, true)
  582. stats.ClosedCount = parseCountResult(resutls)
  583. }
  584. return stats
  585. }
  586. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  587. func GetUserIssueStats(uid int64, filterMode int) *IssueStats {
  588. stats := &IssueStats{}
  589. issue := new(Issue)
  590. stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue)
  591. stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue)
  592. return stats
  593. }
  594. func updateIssue(e Engine, issue *Issue) error {
  595. _, err := e.Id(issue.ID).AllCols().Update(issue)
  596. return err
  597. }
  598. // UpdateIssue updates information of issue.
  599. func UpdateIssue(issue *Issue) error {
  600. return updateIssue(x, issue)
  601. }
  602. func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error {
  603. _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID)
  604. return err
  605. }
  606. // UpdateIssueUsersByStatus updates issue-user relations by issue status.
  607. func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error {
  608. return updateIssueUsersByStatus(x, issueID, isClosed)
  609. }
  610. func updateIssueUserByAssignee(e *xorm.Session, issue *Issue) (err error) {
  611. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issue.ID); err != nil {
  612. return err
  613. }
  614. // Assignee ID equals to 0 means clear assignee.
  615. if issue.AssigneeID > 0 {
  616. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, issue.AssigneeID, issue.ID); err != nil {
  617. return err
  618. }
  619. }
  620. return updateIssue(e, issue)
  621. }
  622. // UpdateIssueUserByAssignee updates issue-user relation for assignee.
  623. func UpdateIssueUserByAssignee(issue *Issue) (err error) {
  624. sess := x.NewSession()
  625. defer sessionRelease(sess)
  626. if err = sess.Begin(); err != nil {
  627. return err
  628. }
  629. if err = updateIssueUserByAssignee(sess, issue); err != nil {
  630. return err
  631. }
  632. return sess.Commit()
  633. }
  634. // UpdateIssueUserByRead updates issue-user relation for reading.
  635. func UpdateIssueUserByRead(uid, issueID int64) error {
  636. _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
  637. return err
  638. }
  639. // UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
  640. func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
  641. for _, uid := range uids {
  642. iu := &IssueUser{UID: uid, IssueID: iid}
  643. has, err := x.Get(iu)
  644. if err != nil {
  645. return err
  646. }
  647. iu.IsMentioned = true
  648. if has {
  649. _, err = x.Id(iu.ID).AllCols().Update(iu)
  650. } else {
  651. _, err = x.Insert(iu)
  652. }
  653. if err != nil {
  654. return err
  655. }
  656. }
  657. return nil
  658. }
  659. // .____ ___. .__
  660. // | | _____ \_ |__ ____ | |
  661. // | | \__ \ | __ \_/ __ \| |
  662. // | |___ / __ \| \_\ \ ___/| |__
  663. // |_______ (____ /___ /\___ >____/
  664. // \/ \/ \/ \/
  665. // Label represents a label of repository for issues.
  666. type Label struct {
  667. ID int64 `xorm:"pk autoincr"`
  668. RepoID int64 `xorm:"INDEX"`
  669. Name string
  670. Color string `xorm:"VARCHAR(7)"`
  671. NumIssues int
  672. NumClosedIssues int
  673. NumOpenIssues int `xorm:"-"`
  674. IsChecked bool `xorm:"-"`
  675. }
  676. // CalOpenIssues calculates the open issues of label.
  677. func (m *Label) CalOpenIssues() {
  678. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  679. }
  680. // NewLabel creates new label of repository.
  681. func NewLabel(l *Label) error {
  682. _, err := x.Insert(l)
  683. return err
  684. }
  685. func getLabelByID(e Engine, id int64) (*Label, error) {
  686. if id <= 0 {
  687. return nil, ErrLabelNotExist{id}
  688. }
  689. l := &Label{ID: id}
  690. has, err := x.Get(l)
  691. if err != nil {
  692. return nil, err
  693. } else if !has {
  694. return nil, ErrLabelNotExist{l.ID}
  695. }
  696. return l, nil
  697. }
  698. // GetLabelByID returns a label by given ID.
  699. func GetLabelByID(id int64) (*Label, error) {
  700. return getLabelByID(x, id)
  701. }
  702. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  703. func GetLabelsByRepoID(repoID int64) ([]*Label, error) {
  704. labels := make([]*Label, 0, 10)
  705. return labels, x.Where("repo_id=?", repoID).Find(&labels)
  706. }
  707. func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
  708. issueLabels, err := getIssueLabels(e, issueID)
  709. if err != nil {
  710. return nil, fmt.Errorf("getIssueLabels: %v", err)
  711. }
  712. var label *Label
  713. labels := make([]*Label, 0, len(issueLabels))
  714. for idx := range issueLabels {
  715. label, err = getLabelByID(e, issueLabels[idx].LabelID)
  716. if err != nil && !IsErrLabelNotExist(err) {
  717. return nil, fmt.Errorf("getLabelByID: %v", err)
  718. }
  719. labels = append(labels, label)
  720. }
  721. return labels, nil
  722. }
  723. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  724. func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
  725. return getLabelsByIssueID(x, issueID)
  726. }
  727. func updateLabel(e Engine, l *Label) error {
  728. _, err := e.Id(l.ID).AllCols().Update(l)
  729. return err
  730. }
  731. // UpdateLabel updates label information.
  732. func UpdateLabel(l *Label) error {
  733. return updateLabel(x, l)
  734. }
  735. // DeleteLabel delete a label of given repository.
  736. func DeleteLabel(repoID, labelID int64) error {
  737. l, err := GetLabelByID(labelID)
  738. if err != nil {
  739. if IsErrLabelNotExist(err) {
  740. return nil
  741. }
  742. return err
  743. }
  744. sess := x.NewSession()
  745. defer sessionRelease(sess)
  746. if err = sess.Begin(); err != nil {
  747. return err
  748. }
  749. if _, err = x.Where("label_id=?", labelID).Delete(new(IssueLabel)); err != nil {
  750. return err
  751. } else if _, err = sess.Delete(l); err != nil {
  752. return err
  753. }
  754. return sess.Commit()
  755. }
  756. // .___ .____ ___. .__
  757. // | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
  758. // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
  759. // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
  760. // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
  761. // \/ \/ \/ \/ \/ \/ \/
  762. // IssueLabel represetns an issue-lable relation.
  763. type IssueLabel struct {
  764. ID int64 `xorm:"pk autoincr"`
  765. IssueID int64 `xorm:"UNIQUE(s)"`
  766. LabelID int64 `xorm:"UNIQUE(s)"`
  767. }
  768. func hasIssueLabel(e Engine, issueID, labelID int64) bool {
  769. has, _ := e.Where("issue_id=? AND label_id=?", issueID, labelID).Get(new(IssueLabel))
  770. return has
  771. }
  772. // HasIssueLabel returns true if issue has been labeled.
  773. func HasIssueLabel(issueID, labelID int64) bool {
  774. return hasIssueLabel(x, issueID, labelID)
  775. }
  776. func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
  777. if _, err = e.Insert(&IssueLabel{
  778. IssueID: issue.ID,
  779. LabelID: label.ID,
  780. }); err != nil {
  781. return err
  782. }
  783. label.NumIssues++
  784. if issue.IsClosed {
  785. label.NumClosedIssues++
  786. }
  787. return updateLabel(e, label)
  788. }
  789. // NewIssueLabel creates a new issue-label relation.
  790. func NewIssueLabel(issue *Issue, label *Label) (err error) {
  791. sess := x.NewSession()
  792. defer sessionRelease(sess)
  793. if err = sess.Begin(); err != nil {
  794. return err
  795. }
  796. if err = newIssueLabel(sess, issue, label); err != nil {
  797. return err
  798. }
  799. return sess.Commit()
  800. }
  801. func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) {
  802. issueLabels := make([]*IssueLabel, 0, 10)
  803. return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels)
  804. }
  805. // GetIssueLabels returns all issue-label relations of given issue by ID.
  806. func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
  807. return getIssueLabels(x, issueID)
  808. }
  809. func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
  810. if _, err = e.Delete(&IssueLabel{
  811. IssueID: issue.ID,
  812. LabelID: label.ID,
  813. }); err != nil {
  814. return err
  815. }
  816. label.NumIssues--
  817. if issue.IsClosed {
  818. label.NumClosedIssues--
  819. }
  820. return updateLabel(e, label)
  821. }
  822. // DeleteIssueLabel deletes issue-label relation.
  823. func DeleteIssueLabel(issue *Issue, label *Label) (err error) {
  824. sess := x.NewSession()
  825. defer sessionRelease(sess)
  826. if err = sess.Begin(); err != nil {
  827. return err
  828. }
  829. if err = deleteIssueLabel(sess, issue, label); err != nil {
  830. return err
  831. }
  832. return sess.Commit()
  833. }
  834. // _____ .__.__ __
  835. // / \ |__| | ____ _______/ |_ ____ ____ ____
  836. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  837. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  838. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  839. // \/ \/ \/ \/ \/
  840. // Milestone represents a milestone of repository.
  841. type Milestone struct {
  842. ID int64 `xorm:"pk autoincr"`
  843. RepoID int64 `xorm:"INDEX"`
  844. Name string
  845. Content string `xorm:"TEXT"`
  846. RenderedContent string `xorm:"-"`
  847. IsClosed bool
  848. NumIssues int
  849. NumClosedIssues int
  850. NumOpenIssues int `xorm:"-"`
  851. Completeness int // Percentage(1-100).
  852. Deadline time.Time
  853. DeadlineString string `xorm:"-"`
  854. IsOverDue bool `xorm:"-"`
  855. ClosedDate time.Time
  856. }
  857. func (m *Milestone) BeforeUpdate() {
  858. if m.NumIssues > 0 {
  859. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  860. } else {
  861. m.Completeness = 0
  862. }
  863. }
  864. func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
  865. if colName == "deadline" {
  866. if m.Deadline.Year() == 9999 {
  867. return
  868. }
  869. m.DeadlineString = m.Deadline.Format("2006-01-02")
  870. if time.Now().After(m.Deadline) {
  871. m.IsOverDue = true
  872. }
  873. }
  874. }
  875. // CalOpenIssues calculates the open issues of milestone.
  876. func (m *Milestone) CalOpenIssues() {
  877. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  878. }
  879. // NewMilestone creates new milestone of repository.
  880. func NewMilestone(m *Milestone) (err error) {
  881. sess := x.NewSession()
  882. defer sessionRelease(sess)
  883. if err = sess.Begin(); err != nil {
  884. return err
  885. }
  886. if _, err = sess.Insert(m); err != nil {
  887. return err
  888. }
  889. if _, err = sess.Exec("UPDATE `repository` SET num_milestones=num_milestones+1 WHERE id=?", m.RepoID); err != nil {
  890. return err
  891. }
  892. return sess.Commit()
  893. }
  894. func getMilestoneByID(e Engine, id int64) (*Milestone, error) {
  895. m := &Milestone{ID: id}
  896. has, err := e.Get(m)
  897. if err != nil {
  898. return nil, err
  899. } else if !has {
  900. return nil, ErrMilestoneNotExist{id, 0}
  901. }
  902. return m, nil
  903. }
  904. // GetMilestoneByID returns the milestone of given ID.
  905. func GetMilestoneByID(id int64) (*Milestone, error) {
  906. return getMilestoneByID(x, id)
  907. }
  908. // GetRepoMilestoneByID returns the milestone of given ID and repository.
  909. func GetRepoMilestoneByID(repoID, milestoneID int64) (*Milestone, error) {
  910. m := &Milestone{ID: milestoneID, RepoID: repoID}
  911. has, err := x.Get(m)
  912. if err != nil {
  913. return nil, err
  914. } else if !has {
  915. return nil, ErrMilestoneNotExist{milestoneID, repoID}
  916. }
  917. return m, nil
  918. }
  919. // GetAllRepoMilestones returns all milestones of given repository.
  920. func GetAllRepoMilestones(repoID int64) ([]*Milestone, error) {
  921. miles := make([]*Milestone, 0, 10)
  922. return miles, x.Where("repo_id=?", repoID).Find(&miles)
  923. }
  924. // GetMilestones returns a list of milestones of given repository and status.
  925. func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
  926. miles := make([]*Milestone, 0, setting.IssuePagingNum)
  927. sess := x.Where("repo_id=? AND is_closed=?", repoID, isClosed)
  928. if page > 0 {
  929. sess = sess.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  930. }
  931. return miles, sess.Find(&miles)
  932. }
  933. func updateMilestone(e Engine, m *Milestone) error {
  934. _, err := e.Id(m.ID).AllCols().Update(m)
  935. return err
  936. }
  937. // UpdateMilestone updates information of given milestone.
  938. func UpdateMilestone(m *Milestone) error {
  939. return updateMilestone(x, m)
  940. }
  941. func countRepoMilestones(e Engine, repoID int64) int64 {
  942. count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone))
  943. return count
  944. }
  945. // CountRepoMilestones returns number of milestones in given repository.
  946. func CountRepoMilestones(repoID int64) int64 {
  947. return countRepoMilestones(x, repoID)
  948. }
  949. func countRepoClosedMilestones(e Engine, repoID int64) int64 {
  950. closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone))
  951. return closed
  952. }
  953. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  954. func CountRepoClosedMilestones(repoID int64) int64 {
  955. return countRepoClosedMilestones(x, repoID)
  956. }
  957. // MilestoneStats returns number of open and closed milestones of given repository.
  958. func MilestoneStats(repoID int64) (open int64, closed int64) {
  959. open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone))
  960. return open, CountRepoClosedMilestones(repoID)
  961. }
  962. // ChangeMilestoneStatus changes the milestone open/closed status.
  963. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  964. repo, err := GetRepositoryByID(m.RepoID)
  965. if err != nil {
  966. return err
  967. }
  968. sess := x.NewSession()
  969. defer sessionRelease(sess)
  970. if err = sess.Begin(); err != nil {
  971. return err
  972. }
  973. m.IsClosed = isClosed
  974. if err = updateMilestone(sess, m); err != nil {
  975. return err
  976. }
  977. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  978. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  979. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  980. return err
  981. }
  982. return sess.Commit()
  983. }
  984. func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
  985. if issue.MilestoneID == 0 {
  986. return nil
  987. }
  988. m, err := getMilestoneByID(e, issue.MilestoneID)
  989. if err != nil {
  990. return err
  991. }
  992. if issue.IsClosed {
  993. m.NumOpenIssues--
  994. m.NumClosedIssues++
  995. } else {
  996. m.NumOpenIssues++
  997. m.NumClosedIssues--
  998. }
  999. return updateMilestone(e, m)
  1000. }
  1001. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  1002. // for the milestone associated witht the given issue.
  1003. func ChangeMilestoneIssueStats(issue *Issue) (err error) {
  1004. sess := x.NewSession()
  1005. defer sessionRelease(sess)
  1006. if err = sess.Begin(); err != nil {
  1007. return err
  1008. }
  1009. if err = changeMilestoneIssueStats(sess, issue); err != nil {
  1010. return err
  1011. }
  1012. return sess.Commit()
  1013. }
  1014. func changeMilestoneAssign(e *xorm.Session, oldMid int64, issue *Issue) error {
  1015. if oldMid > 0 {
  1016. m, err := getMilestoneByID(e, oldMid)
  1017. if err != nil {
  1018. return err
  1019. }
  1020. m.NumIssues--
  1021. if issue.IsClosed {
  1022. m.NumClosedIssues--
  1023. }
  1024. if err = updateMilestone(e, m); err != nil {
  1025. return err
  1026. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE issue_id=?", issue.ID); err != nil {
  1027. return err
  1028. }
  1029. }
  1030. if issue.MilestoneID > 0 {
  1031. m, err := getMilestoneByID(e, issue.MilestoneID)
  1032. if err != nil {
  1033. return err
  1034. }
  1035. m.NumIssues++
  1036. if issue.IsClosed {
  1037. m.NumClosedIssues++
  1038. }
  1039. if m.NumIssues == 0 {
  1040. return ErrWrongIssueCounter
  1041. }
  1042. if err = updateMilestone(e, m); err != nil {
  1043. return err
  1044. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=? WHERE issue_id=?", m.ID, issue.ID); err != nil {
  1045. return err
  1046. }
  1047. }
  1048. return updateIssue(e, issue)
  1049. }
  1050. // ChangeMilestoneAssign changes assignment of milestone for issue.
  1051. func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) {
  1052. sess := x.NewSession()
  1053. defer sess.Close()
  1054. if err = sess.Begin(); err != nil {
  1055. return err
  1056. }
  1057. if err = changeMilestoneAssign(sess, oldMid, issue); err != nil {
  1058. return err
  1059. }
  1060. return sess.Commit()
  1061. }
  1062. // DeleteMilestoneByID deletes a milestone by given ID.
  1063. func DeleteMilestoneByID(mid int64) error {
  1064. m, err := GetMilestoneByID(mid)
  1065. if err != nil {
  1066. if IsErrMilestoneNotExist(err) {
  1067. return nil
  1068. }
  1069. return err
  1070. }
  1071. repo, err := GetRepositoryByID(m.RepoID)
  1072. if err != nil {
  1073. return err
  1074. }
  1075. sess := x.NewSession()
  1076. defer sessionRelease(sess)
  1077. if err = sess.Begin(); err != nil {
  1078. return err
  1079. }
  1080. if _, err = sess.Id(m.ID).Delete(m); err != nil {
  1081. return err
  1082. }
  1083. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  1084. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  1085. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  1086. return err
  1087. }
  1088. if _, err = sess.Exec("UPDATE `issue` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  1089. return err
  1090. } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  1091. return err
  1092. }
  1093. return sess.Commit()
  1094. }
  1095. // _________ __
  1096. // \_ ___ \ ____ _____ _____ ____ _____/ |_
  1097. // / \ \/ / _ \ / \ / \_/ __ \ / \ __\
  1098. // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ |
  1099. // \______ /\____/|__|_| /__|_| /\___ >___| /__|
  1100. // \/ \/ \/ \/ \/
  1101. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  1102. type CommentType int
  1103. const (
  1104. // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0)
  1105. COMMENT_TYPE_COMMENT CommentType = iota
  1106. COMMENT_TYPE_REOPEN
  1107. COMMENT_TYPE_CLOSE
  1108. // References.
  1109. COMMENT_TYPE_ISSUE_REF
  1110. // Reference from a commit (not part of a pull request)
  1111. COMMENT_TYPE_COMMIT_REF
  1112. // Reference from a comment
  1113. COMMENT_TYPE_COMMENT_REF
  1114. // Reference from a pull request
  1115. COMMENT_TYPE_PULL_REF
  1116. )
  1117. type CommentTag int
  1118. const (
  1119. COMMENT_TAG_NONE CommentTag = iota
  1120. COMMENT_TAG_POSTER
  1121. COMMENT_TAG_ADMIN
  1122. COMMENT_TAG_OWNER
  1123. )
  1124. // Comment represents a comment in commit and issue page.
  1125. type Comment struct {
  1126. ID int64 `xorm:"pk autoincr"`
  1127. Type CommentType
  1128. PosterID int64
  1129. Poster *User `xorm:"-"`
  1130. IssueID int64 `xorm:"INDEX"`
  1131. CommitID int64
  1132. Line int64
  1133. Content string `xorm:"TEXT"`
  1134. RenderedContent string `xorm:"-"`
  1135. Created time.Time `xorm:"CREATED"`
  1136. Attachments []*Attachment `xorm:"-"`
  1137. // For view issue page.
  1138. ShowTag CommentTag `xorm:"-"`
  1139. }
  1140. // HashTag returns unique hash tag for comment.
  1141. func (c *Comment) HashTag() string {
  1142. return "issuecomment-" + com.ToStr(c.ID)
  1143. }
  1144. // EventTag returns unique event hash tag for comment.
  1145. func (c *Comment) EventTag() string {
  1146. return "event-" + com.ToStr(c.ID)
  1147. }
  1148. func (c *Comment) AfterSet(colName string, _ xorm.Cell) {
  1149. var err error
  1150. switch colName {
  1151. case "id":
  1152. c.Attachments, err = GetAttachmentsByCommentID(c.ID)
  1153. if err != nil {
  1154. log.Error(3, "GetAttachmentsByCommentID[%d]: %v", c.ID, err)
  1155. }
  1156. case "poster_id":
  1157. c.Poster, err = GetUserByID(c.PosterID)
  1158. if err != nil {
  1159. if IsErrUserNotExist(err) {
  1160. c.PosterID = -1
  1161. c.Poster = NewFakeUser()
  1162. } else {
  1163. log.Error(3, "GetUserByID[%d]: %v", c.ID, err)
  1164. }
  1165. }
  1166. }
  1167. }
  1168. func createComment(e *xorm.Session, u *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content string, uuids []string) (_ *Comment, err error) {
  1169. comment := &Comment{
  1170. PosterID: u.Id,
  1171. Type: cmtType,
  1172. IssueID: issue.ID,
  1173. CommitID: commitID,
  1174. Line: line,
  1175. Content: content,
  1176. }
  1177. if _, err = e.Insert(comment); err != nil {
  1178. return nil, err
  1179. }
  1180. // Check comment type.
  1181. switch cmtType {
  1182. case COMMENT_TYPE_COMMENT:
  1183. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", issue.ID); err != nil {
  1184. return nil, err
  1185. }
  1186. // Check attachments.
  1187. attachments := make([]*Attachment, 0, len(uuids))
  1188. for _, uuid := range uuids {
  1189. attach, err := getAttachmentByUUID(e, uuid)
  1190. if err != nil {
  1191. if IsErrAttachmentNotExist(err) {
  1192. continue
  1193. }
  1194. return nil, fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
  1195. }
  1196. attachments = append(attachments, attach)
  1197. }
  1198. for i := range attachments {
  1199. attachments[i].IssueID = issue.ID
  1200. attachments[i].CommentID = comment.ID
  1201. // No assign value could be 0, so ignore AllCols().
  1202. if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  1203. return nil, fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
  1204. }
  1205. }
  1206. // Notify watchers.
  1207. act := &Action{
  1208. ActUserID: u.Id,
  1209. ActUserName: u.LowerName,
  1210. ActEmail: u.Email,
  1211. OpType: COMMENT_ISSUE,
  1212. Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]),
  1213. RepoID: repo.ID,
  1214. RepoUserName: repo.Owner.LowerName,
  1215. RepoName: repo.LowerName,
  1216. IsPrivate: repo.IsPrivate,
  1217. }
  1218. if err = notifyWatchers(e, act); err != nil {
  1219. return nil, err
  1220. }
  1221. case COMMENT_TYPE_REOPEN:
  1222. if _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", repo.ID); err != nil {
  1223. return nil, err
  1224. }
  1225. case COMMENT_TYPE_CLOSE:
  1226. if _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", repo.ID); err != nil {
  1227. return nil, err
  1228. }
  1229. }
  1230. return comment, nil
  1231. }
  1232. func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
  1233. cmtType := COMMENT_TYPE_CLOSE
  1234. if !issue.IsClosed {
  1235. cmtType = COMMENT_TYPE_REOPEN
  1236. }
  1237. return createComment(e, doer, repo, issue, 0, 0, cmtType, "", nil)
  1238. }
  1239. // CreateComment creates comment of issue or commit.
  1240. func CreateComment(doer *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content string, attachments []string) (comment *Comment, err error) {
  1241. sess := x.NewSession()
  1242. defer sessionRelease(sess)
  1243. if err = sess.Begin(); err != nil {
  1244. return nil, err
  1245. }
  1246. comment, err = createComment(sess, doer, repo, issue, commitID, line, cmtType, content, attachments)
  1247. if err != nil {
  1248. return nil, err
  1249. }
  1250. return comment, sess.Commit()
  1251. }
  1252. // CreateIssueComment creates a plain issue comment.
  1253. func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
  1254. return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, attachments)
  1255. }
  1256. // GetCommentById returns the comment with the given id
  1257. func GetCommentById(id int64) (*Comment, error) {
  1258. c := new(Comment)
  1259. _, err := x.Id(id).Get(c)
  1260. return c, err
  1261. }
  1262. // GetCommentsByIssueID returns all comments of issue by given ID.
  1263. func GetCommentsByIssueID(issueID int64) ([]*Comment, error) {
  1264. comments := make([]*Comment, 0, 10)
  1265. return comments, x.Where("issue_id=?", issueID).Asc("created").Find(&comments)
  1266. }
  1267. func (c *Comment) AfterDelete() {
  1268. _, err := DeleteAttachmentsByComment(c.ID, true)
  1269. if err != nil {
  1270. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  1271. }
  1272. }
  1273. // Attachment represent a attachment of issue/comment/release.
  1274. type Attachment struct {
  1275. ID int64 `xorm:"pk autoincr"`
  1276. UUID string `xorm:"uuid UNIQUE"`
  1277. IssueID int64 `xorm:"INDEX"`
  1278. CommentID int64
  1279. ReleaseID int64 `xorm:"INDEX"`
  1280. Name string
  1281. Created time.Time `xorm:"CREATED"`
  1282. }
  1283. // AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
  1284. func AttachmentLocalPath(uuid string) string {
  1285. return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
  1286. }
  1287. // LocalPath returns where attachment is stored in local file system.
  1288. func (attach *Attachment) LocalPath() string {
  1289. return AttachmentLocalPath(attach.UUID)
  1290. }
  1291. // NewAttachment creates a new attachment object.
  1292. func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) {
  1293. attach := &Attachment{
  1294. UUID: gouuid.NewV4().String(),
  1295. Name: name,
  1296. }
  1297. if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil {
  1298. return nil, fmt.Errorf("MkdirAll: %v", err)
  1299. }
  1300. fw, err := os.Create(attach.LocalPath())
  1301. if err != nil {
  1302. return nil, fmt.Errorf("Create: %v", err)
  1303. }
  1304. defer fw.Close()
  1305. if _, err = fw.Write(buf); err != nil {
  1306. return nil, fmt.Errorf("Write: %v", err)
  1307. } else if _, err = io.Copy(fw, file); err != nil {
  1308. return nil, fmt.Errorf("Copy: %v", err)
  1309. }
  1310. sess := x.NewSession()
  1311. defer sessionRelease(sess)
  1312. if err := sess.Begin(); err != nil {
  1313. return nil, err
  1314. }
  1315. if _, err := sess.Insert(attach); err != nil {
  1316. return nil, err
  1317. }
  1318. return attach, sess.Commit()
  1319. }
  1320. func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
  1321. attach := &Attachment{UUID: uuid}
  1322. has, err := x.Get(attach)
  1323. if err != nil {
  1324. return nil, err
  1325. } else if !has {
  1326. return nil, ErrAttachmentNotExist{0, uuid}
  1327. }
  1328. return attach, nil
  1329. }
  1330. // GetAttachmentByUUID returns attachment by given UUID.
  1331. func GetAttachmentByUUID(uuid string) (*Attachment, error) {
  1332. return getAttachmentByUUID(x, uuid)
  1333. }
  1334. // GetAttachmentsByIssueID returns all attachments for given issue by ID.
  1335. func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) {
  1336. attachments := make([]*Attachment, 0, 10)
  1337. return attachments, x.Where("issue_id=? AND comment_id=0", issueID).Find(&attachments)
  1338. }
  1339. // GetAttachmentsByCommentID returns all attachments if comment by given ID.
  1340. func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
  1341. attachments := make([]*Attachment, 0, 10)
  1342. return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
  1343. }
  1344. // DeleteAttachment deletes the given attachment and optionally the associated file.
  1345. func DeleteAttachment(a *Attachment, remove bool) error {
  1346. _, err := DeleteAttachments([]*Attachment{a}, remove)
  1347. return err
  1348. }
  1349. // DeleteAttachments deletes the given attachments and optionally the associated files.
  1350. func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
  1351. for i, a := range attachments {
  1352. if remove {
  1353. if err := os.Remove(a.LocalPath()); err != nil {
  1354. return i, err
  1355. }
  1356. }
  1357. if _, err := x.Delete(a.ID); err != nil {
  1358. return i, err
  1359. }
  1360. }
  1361. return len(attachments), nil
  1362. }
  1363. // DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
  1364. func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
  1365. attachments, err := GetAttachmentsByIssueID(issueId)
  1366. if err != nil {
  1367. return 0, err
  1368. }
  1369. return DeleteAttachments(attachments, remove)
  1370. }
  1371. // DeleteAttachmentsByComment deletes all attachments associated with the given comment.
  1372. func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
  1373. attachments, err := GetAttachmentsByCommentID(commentId)
  1374. if err != nil {
  1375. return 0, err
  1376. }
  1377. return DeleteAttachments(attachments, remove)
  1378. }