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.

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