You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue.go 36KB

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