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.

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