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

issue.go 28 kB

12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
11 years ago
11 years ago
11 years ago
12 years ago
12 years ago
12 years ago
12 years ago
11 years ago
11 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
11 years ago
12 years ago
12 years ago
12 years ago
12 years ago
11 years ago
12 years ago
12 years ago
12 years ago
11 years ago
12 years ago
11 years ago
11 years ago
11 years ago
12 years ago
11 years ago
12 years ago
11 years ago
11 years ago
11 years ago
11 years ago
12 years ago
11 years ago
12 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
12 years ago
11 years ago
11 years ago
11 years ago
12 years ago
12 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
12 years ago
11 years ago
12 years ago
11 years ago
12 years ago
11 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
12 years ago
11 years ago
11 years ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  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. "html/template"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/Unknwon/com"
  14. "github.com/go-xorm/xorm"
  15. "github.com/gogits/gogs/modules/log"
  16. "github.com/gogits/gogs/modules/setting"
  17. )
  18. var (
  19. ErrIssueNotExist = errors.New("Issue does not exist")
  20. ErrLabelNotExist = errors.New("Label does not exist")
  21. ErrMilestoneNotExist = errors.New("Milestone does not exist")
  22. ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
  23. ErrAttachmentNotExist = errors.New("Attachment does not exist")
  24. ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
  25. ErrMissingIssueNumber = errors.New("No issue number specified")
  26. )
  27. // Issue represents an issue or pull request of repository.
  28. type Issue struct {
  29. ID int64 `xorm:"pk autoincr"`
  30. RepoId int64 `xorm:"INDEX"`
  31. Index int64 // Index in one repository.
  32. Name string
  33. Repo *Repository `xorm:"-"`
  34. PosterId int64
  35. Poster *User `xorm:"-"`
  36. LabelIds string `xorm:"TEXT"`
  37. Labels []*Label `xorm:"-"`
  38. MilestoneId int64
  39. AssigneeId int64
  40. Assignee *User `xorm:"-"`
  41. IsRead bool `xorm:"-"`
  42. IsPull bool // Indicates whether is a pull request or not.
  43. IsClosed bool
  44. Content string `xorm:"TEXT"`
  45. RenderedContent string `xorm:"-"`
  46. Priority int
  47. NumComments int
  48. Deadline time.Time
  49. Created time.Time `xorm:"CREATED"`
  50. Updated time.Time `xorm:"UPDATED"`
  51. }
  52. func (i *Issue) GetPoster() (err error) {
  53. i.Poster, err = GetUserById(i.PosterId)
  54. if err == ErrUserNotExist {
  55. i.Poster = &User{Name: "FakeUser"}
  56. return nil
  57. }
  58. return err
  59. }
  60. func (i *Issue) GetLabels() error {
  61. if len(i.LabelIds) < 3 {
  62. return nil
  63. }
  64. strIds := strings.Split(strings.TrimSuffix(i.LabelIds[1:], "|"), "|$")
  65. i.Labels = make([]*Label, 0, len(strIds))
  66. for _, strId := range strIds {
  67. id, _ := com.StrTo(strId).Int64()
  68. if id > 0 {
  69. l, err := GetLabelById(id)
  70. if err != nil {
  71. if err == ErrLabelNotExist {
  72. continue
  73. }
  74. return err
  75. }
  76. i.Labels = append(i.Labels, l)
  77. }
  78. }
  79. return nil
  80. }
  81. func (i *Issue) GetAssignee() (err error) {
  82. if i.AssigneeId == 0 {
  83. return nil
  84. }
  85. i.Assignee, err = GetUserById(i.AssigneeId)
  86. if err == ErrUserNotExist {
  87. return nil
  88. }
  89. return err
  90. }
  91. func (i *Issue) Attachments() []*Attachment {
  92. a, _ := GetAttachmentsForIssue(i.ID)
  93. return a
  94. }
  95. func (i *Issue) AfterDelete() {
  96. _, err := DeleteAttachmentsByIssue(i.ID, true)
  97. if err != nil {
  98. log.Info("Could not delete files for issue #%d: %s", i.ID, err)
  99. }
  100. }
  101. // CreateIssue creates new issue for repository.
  102. func NewIssue(issue *Issue) (err error) {
  103. sess := x.NewSession()
  104. defer sessionRelease(sess)
  105. if err = sess.Begin(); err != nil {
  106. return err
  107. }
  108. if _, err = sess.Insert(issue); err != nil {
  109. return err
  110. } else if _, err = sess.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", issue.RepoId); err != nil {
  111. return err
  112. }
  113. if err = sess.Commit(); err != nil {
  114. return err
  115. }
  116. if issue.MilestoneId > 0 {
  117. // FIXES(280): Update milestone counter.
  118. return ChangeMilestoneAssign(0, issue.MilestoneId, issue)
  119. }
  120. return
  121. }
  122. // GetIssueByRef returns an Issue specified by a GFM reference.
  123. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  124. func GetIssueByRef(ref string) (issue *Issue, err error) {
  125. var issueNumber int64
  126. var repo *Repository
  127. n := strings.IndexByte(ref, byte('#'))
  128. if n == -1 {
  129. return nil, ErrMissingIssueNumber
  130. }
  131. if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil {
  132. return
  133. }
  134. if repo, err = GetRepositoryByRef(ref[:n]); err != nil {
  135. return
  136. }
  137. return GetIssueByIndex(repo.Id, issueNumber)
  138. }
  139. // GetIssueByIndex returns issue by given index in repository.
  140. func GetIssueByIndex(rid, index int64) (*Issue, error) {
  141. issue := &Issue{RepoId: rid, Index: index}
  142. has, err := x.Get(issue)
  143. if err != nil {
  144. return nil, err
  145. } else if !has {
  146. return nil, ErrIssueNotExist
  147. }
  148. return issue, nil
  149. }
  150. // GetIssueById returns an issue by ID.
  151. func GetIssueById(id int64) (*Issue, error) {
  152. issue := &Issue{ID: id}
  153. has, err := x.Get(issue)
  154. if err != nil {
  155. return nil, err
  156. } else if !has {
  157. return nil, ErrIssueNotExist
  158. }
  159. return issue, nil
  160. }
  161. // GetIssues returns a list of issues by given conditions.
  162. func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sortType string) ([]Issue, error) {
  163. sess := x.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  164. if rid > 0 {
  165. sess.Where("repo_id=?", rid).And("is_closed=?", isClosed)
  166. } else {
  167. sess.Where("is_closed=?", isClosed)
  168. }
  169. if uid > 0 {
  170. sess.And("assignee_id=?", uid)
  171. } else if pid > 0 {
  172. sess.And("poster_id=?", pid)
  173. }
  174. if mid > 0 {
  175. sess.And("milestone_id=?", mid)
  176. }
  177. if len(labelIds) > 0 {
  178. for _, label := range strings.Split(labelIds, ",") {
  179. if com.StrTo(label).MustInt() > 0 {
  180. sess.And("label_ids like ?", "'%$"+label+"|%'")
  181. }
  182. }
  183. }
  184. switch sortType {
  185. case "oldest":
  186. sess.Asc("created")
  187. case "recentupdate":
  188. sess.Desc("updated")
  189. case "leastupdate":
  190. sess.Asc("updated")
  191. case "mostcomment":
  192. sess.Desc("num_comments")
  193. case "leastcomment":
  194. sess.Asc("num_comments")
  195. case "priority":
  196. sess.Desc("priority")
  197. default:
  198. sess.Desc("created")
  199. }
  200. var issues []Issue
  201. return issues, sess.Find(&issues)
  202. }
  203. type IssueStatus int
  204. const (
  205. IS_OPEN = iota + 1
  206. IS_CLOSE
  207. )
  208. // GetIssuesByLabel returns a list of issues by given label and repository.
  209. func GetIssuesByLabel(repoID, labelID int64) ([]*Issue, error) {
  210. issues := make([]*Issue, 0, 10)
  211. return issues, x.Where("repo_id=?", repoID).And("label_ids like '%$" + com.ToStr(labelID) + "|%'").Find(&issues)
  212. }
  213. // GetIssueCountByPoster returns number of issues of repository by poster.
  214. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  215. count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  216. return count
  217. }
  218. // .___ ____ ___
  219. // | | ______ ________ __ ____ | | \______ ___________
  220. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  221. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  222. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  223. // \/ \/ \/ \/ \/
  224. // IssueUser represents an issue-user relation.
  225. type IssueUser struct {
  226. Id int64
  227. Uid int64 `xorm:"INDEX"` // User ID.
  228. IssueId int64
  229. RepoId int64 `xorm:"INDEX"`
  230. MilestoneId int64
  231. IsRead bool
  232. IsAssigned bool
  233. IsMentioned bool
  234. IsPoster bool
  235. IsClosed bool
  236. }
  237. // FIXME: organization
  238. // NewIssueUserPairs adds new issue-user pairs for new issue of repository.
  239. func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID int64) error {
  240. users, err := repo.GetCollaborators()
  241. if err != nil {
  242. return err
  243. }
  244. iu := &IssueUser{
  245. IssueId: issueID,
  246. RepoId: repo.Id,
  247. }
  248. isNeedAddPoster := true
  249. for _, u := range users {
  250. iu.Id = 0
  251. iu.Uid = u.Id
  252. iu.IsPoster = iu.Uid == posterID
  253. if isNeedAddPoster && iu.IsPoster {
  254. isNeedAddPoster = false
  255. }
  256. iu.IsAssigned = iu.Uid == assigneeID
  257. if _, err = x.Insert(iu); err != nil {
  258. return err
  259. }
  260. }
  261. if isNeedAddPoster {
  262. iu.Id = 0
  263. iu.Uid = posterID
  264. iu.IsPoster = true
  265. iu.IsAssigned = iu.Uid == assigneeID
  266. if _, err = x.Insert(iu); err != nil {
  267. return err
  268. }
  269. }
  270. // Add owner's as well.
  271. if repo.OwnerId != posterID {
  272. iu.Id = 0
  273. iu.Uid = repo.OwnerId
  274. iu.IsAssigned = iu.Uid == assigneeID
  275. if _, err = x.Insert(iu); err != nil {
  276. return err
  277. }
  278. }
  279. return nil
  280. }
  281. // PairsContains returns true when pairs list contains given issue.
  282. func PairsContains(ius []*IssueUser, issueId, uid int64) int {
  283. for i := range ius {
  284. if ius[i].IssueId == issueId &&
  285. ius[i].Uid == uid {
  286. return i
  287. }
  288. }
  289. return -1
  290. }
  291. // GetIssueUserPairs returns issue-user pairs by given repository and user.
  292. func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  293. ius := make([]*IssueUser, 0, 10)
  294. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid})
  295. return ius, err
  296. }
  297. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  298. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  299. if len(rids) == 0 {
  300. return []*IssueUser{}, nil
  301. }
  302. buf := bytes.NewBufferString("")
  303. for _, rid := range rids {
  304. buf.WriteString("repo_id=")
  305. buf.WriteString(com.ToStr(rid))
  306. buf.WriteString(" OR ")
  307. }
  308. cond := strings.TrimSuffix(buf.String(), " OR ")
  309. ius := make([]*IssueUser, 0, 10)
  310. sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  311. if len(cond) > 0 {
  312. sess.And(cond)
  313. }
  314. err := sess.Find(&ius)
  315. return ius, err
  316. }
  317. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  318. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  319. ius := make([]*IssueUser, 0, 10)
  320. sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  321. if rid > 0 {
  322. sess.And("repo_id=?", rid)
  323. }
  324. switch filterMode {
  325. case FM_ASSIGN:
  326. sess.And("is_assigned=?", true)
  327. case FM_CREATE:
  328. sess.And("is_poster=?", true)
  329. default:
  330. return ius, nil
  331. }
  332. err := sess.Find(&ius)
  333. return ius, err
  334. }
  335. // IssueStats represents issue statistic information.
  336. type IssueStats struct {
  337. OpenCount, ClosedCount int64
  338. AllCount int64
  339. AssignCount int64
  340. CreateCount int64
  341. MentionCount int64
  342. }
  343. // Filter modes.
  344. const (
  345. FM_ASSIGN = iota + 1
  346. FM_CREATE
  347. FM_MENTION
  348. )
  349. // GetIssueStats returns issue statistic information by given conditions.
  350. func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStats {
  351. stats := &IssueStats{}
  352. issue := new(Issue)
  353. tmpSess := &xorm.Session{}
  354. sess := x.Where("repo_id=?", rid)
  355. *tmpSess = *sess
  356. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue)
  357. *tmpSess = *sess
  358. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue)
  359. if isShowClosed {
  360. stats.AllCount = stats.ClosedCount
  361. } else {
  362. stats.AllCount = stats.OpenCount
  363. }
  364. if filterMode != FM_MENTION {
  365. sess = x.Where("repo_id=?", rid)
  366. switch filterMode {
  367. case FM_ASSIGN:
  368. sess.And("assignee_id=?", uid)
  369. case FM_CREATE:
  370. sess.And("poster_id=?", uid)
  371. default:
  372. goto nofilter
  373. }
  374. *tmpSess = *sess
  375. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue)
  376. *tmpSess = *sess
  377. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue)
  378. } else {
  379. sess := x.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true)
  380. *tmpSess = *sess
  381. stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssueUser))
  382. *tmpSess = *sess
  383. stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssueUser))
  384. }
  385. nofilter:
  386. stats.AssignCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue)
  387. stats.CreateCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue)
  388. stats.MentionCount, _ = x.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser))
  389. return stats
  390. }
  391. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  392. func GetUserIssueStats(uid int64, filterMode int) *IssueStats {
  393. stats := &IssueStats{}
  394. issue := new(Issue)
  395. stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue)
  396. stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue)
  397. return stats
  398. }
  399. // UpdateIssue updates information of issue.
  400. func UpdateIssue(issue *Issue) error {
  401. _, err := x.Id(issue.ID).AllCols().Update(issue)
  402. if err != nil {
  403. return err
  404. }
  405. return err
  406. }
  407. // UpdateIssueUserByStatus updates issue-user pairs by issue status.
  408. func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error {
  409. rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?"
  410. _, err := x.Exec(rawSql, isClosed, iid)
  411. return err
  412. }
  413. // UpdateIssueUserPairByAssignee updates issue-user pair for assigning.
  414. func UpdateIssueUserPairByAssignee(aid, iid int64) error {
  415. rawSql := "UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?"
  416. if _, err := x.Exec(rawSql, false, iid); err != nil {
  417. return err
  418. }
  419. // Assignee ID equals to 0 means clear assignee.
  420. if aid == 0 {
  421. return nil
  422. }
  423. rawSql = "UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?"
  424. _, err := x.Exec(rawSql, true, aid, iid)
  425. return err
  426. }
  427. // UpdateIssueUserPairByRead updates issue-user pair for reading.
  428. func UpdateIssueUserPairByRead(uid, iid int64) error {
  429. rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?"
  430. _, err := x.Exec(rawSql, true, uid, iid)
  431. return err
  432. }
  433. // UpdateIssueUserPairsByMentions updates issue-user pairs by mentioning.
  434. func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error {
  435. for _, uid := range uids {
  436. iu := &IssueUser{Uid: uid, IssueId: iid}
  437. has, err := x.Get(iu)
  438. if err != nil {
  439. return err
  440. }
  441. iu.IsMentioned = true
  442. if has {
  443. _, err = x.Id(iu.Id).AllCols().Update(iu)
  444. } else {
  445. _, err = x.Insert(iu)
  446. }
  447. if err != nil {
  448. return err
  449. }
  450. }
  451. return nil
  452. }
  453. // .____ ___. .__
  454. // | | _____ \_ |__ ____ | |
  455. // | | \__ \ | __ \_/ __ \| |
  456. // | |___ / __ \| \_\ \ ___/| |__
  457. // |_______ (____ /___ /\___ >____/
  458. // \/ \/ \/ \/
  459. // Label represents a label of repository for issues.
  460. type Label struct {
  461. ID int64 `xorm:"pk autoincr"`
  462. RepoId int64 `xorm:"INDEX"`
  463. Name string
  464. Color string `xorm:"VARCHAR(7)"`
  465. NumIssues int
  466. NumClosedIssues int
  467. NumOpenIssues int `xorm:"-"`
  468. IsChecked bool `xorm:"-"`
  469. }
  470. // CalOpenIssues calculates the open issues of label.
  471. func (m *Label) CalOpenIssues() {
  472. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  473. }
  474. // NewLabel creates new label of repository.
  475. func NewLabel(l *Label) error {
  476. _, err := x.Insert(l)
  477. return err
  478. }
  479. // GetLabelById returns a label by given ID.
  480. func GetLabelById(id int64) (*Label, error) {
  481. if id <= 0 {
  482. return nil, ErrLabelNotExist
  483. }
  484. l := &Label{ID: id}
  485. has, err := x.Get(l)
  486. if err != nil {
  487. return nil, err
  488. } else if !has {
  489. return nil, ErrLabelNotExist
  490. }
  491. return l, nil
  492. }
  493. // GetLabels returns a list of labels of given repository ID.
  494. func GetLabels(repoId int64) ([]*Label, error) {
  495. labels := make([]*Label, 0, 10)
  496. err := x.Where("repo_id=?", repoId).Find(&labels)
  497. return labels, err
  498. }
  499. // UpdateLabel updates label information.
  500. func UpdateLabel(l *Label) error {
  501. _, err := x.Id(l.ID).AllCols().Update(l)
  502. return err
  503. }
  504. // DeleteLabel delete a label of given repository.
  505. func DeleteLabel(repoID, labelID int64) error {
  506. l, err := GetLabelById(labelID)
  507. if err != nil {
  508. if err == ErrLabelNotExist {
  509. return nil
  510. }
  511. return err
  512. }
  513. issues, err := GetIssuesByLabel(repoID, labelID)
  514. if err != nil {
  515. return err
  516. }
  517. sess := x.NewSession()
  518. defer sessionRelease(sess)
  519. if err = sess.Begin(); err != nil {
  520. return err
  521. }
  522. for _, issue := range issues {
  523. issue.LabelIds = strings.Replace(issue.LabelIds, "$"+com.ToStr(labelID)+"|", "", -1)
  524. if _, err = sess.Id(issue.ID).AllCols().Update(issue); err != nil {
  525. return err
  526. }
  527. }
  528. if _, err = sess.Delete(l); err != nil {
  529. return err
  530. }
  531. return sess.Commit()
  532. }
  533. // _____ .__.__ __
  534. // / \ |__| | ____ _______/ |_ ____ ____ ____
  535. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  536. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  537. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  538. // \/ \/ \/ \/ \/
  539. // Milestone represents a milestone of repository.
  540. type Milestone struct {
  541. Id int64
  542. RepoId int64 `xorm:"INDEX"`
  543. Index int64
  544. Name string
  545. Content string `xorm:"TEXT"`
  546. RenderedContent string `xorm:"-"`
  547. IsClosed bool
  548. NumIssues int
  549. NumClosedIssues int
  550. NumOpenIssues int `xorm:"-"`
  551. Completeness int // Percentage(1-100).
  552. Deadline time.Time
  553. DeadlineString string `xorm:"-"`
  554. ClosedDate time.Time
  555. }
  556. // CalOpenIssues calculates the open issues of milestone.
  557. func (m *Milestone) CalOpenIssues() {
  558. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  559. }
  560. // NewMilestone creates new milestone of repository.
  561. func NewMilestone(m *Milestone) (err error) {
  562. sess := x.NewSession()
  563. defer sess.Close()
  564. if err = sess.Begin(); err != nil {
  565. return err
  566. }
  567. if _, err = sess.Insert(m); err != nil {
  568. sess.Rollback()
  569. return err
  570. }
  571. rawSql := "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?"
  572. if _, err = sess.Exec(rawSql, m.RepoId); err != nil {
  573. sess.Rollback()
  574. return err
  575. }
  576. return sess.Commit()
  577. }
  578. // GetMilestoneById returns the milestone by given ID.
  579. func GetMilestoneById(id int64) (*Milestone, error) {
  580. m := &Milestone{Id: id}
  581. has, err := x.Get(m)
  582. if err != nil {
  583. return nil, err
  584. } else if !has {
  585. return nil, ErrMilestoneNotExist
  586. }
  587. return m, nil
  588. }
  589. // GetMilestoneByIndex returns the milestone of given repository and index.
  590. func GetMilestoneByIndex(repoId, idx int64) (*Milestone, error) {
  591. m := &Milestone{RepoId: repoId, Index: idx}
  592. has, err := x.Get(m)
  593. if err != nil {
  594. return nil, err
  595. } else if !has {
  596. return nil, ErrMilestoneNotExist
  597. }
  598. return m, nil
  599. }
  600. // GetMilestones returns a list of milestones of given repository and status.
  601. func GetMilestones(repoId int64, isClosed bool) ([]*Milestone, error) {
  602. miles := make([]*Milestone, 0, 10)
  603. err := x.Where("repo_id=?", repoId).And("is_closed=?", isClosed).Find(&miles)
  604. return miles, err
  605. }
  606. // UpdateMilestone updates information of given milestone.
  607. func UpdateMilestone(m *Milestone) error {
  608. _, err := x.Id(m.Id).Update(m)
  609. return err
  610. }
  611. // ChangeMilestoneStatus changes the milestone open/closed status.
  612. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  613. repo, err := GetRepositoryById(m.RepoId)
  614. if err != nil {
  615. return err
  616. }
  617. sess := x.NewSession()
  618. defer sess.Close()
  619. if err = sess.Begin(); err != nil {
  620. return err
  621. }
  622. m.IsClosed = isClosed
  623. if _, err = sess.Id(m.Id).AllCols().Update(m); err != nil {
  624. sess.Rollback()
  625. return err
  626. }
  627. if isClosed {
  628. repo.NumClosedMilestones++
  629. } else {
  630. repo.NumClosedMilestones--
  631. }
  632. if _, err = sess.Id(repo.Id).Update(repo); err != nil {
  633. sess.Rollback()
  634. return err
  635. }
  636. return sess.Commit()
  637. }
  638. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress for the
  639. // milestone associated witht the given issue.
  640. func ChangeMilestoneIssueStats(issue *Issue) error {
  641. if issue.MilestoneId == 0 {
  642. return nil
  643. }
  644. m, err := GetMilestoneById(issue.MilestoneId)
  645. if err != nil {
  646. return err
  647. }
  648. if issue.IsClosed {
  649. m.NumOpenIssues--
  650. m.NumClosedIssues++
  651. } else {
  652. m.NumOpenIssues++
  653. m.NumClosedIssues--
  654. }
  655. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  656. return UpdateMilestone(m)
  657. }
  658. // ChangeMilestoneAssign changes assignment of milestone for issue.
  659. func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) {
  660. sess := x.NewSession()
  661. defer sess.Close()
  662. if err = sess.Begin(); err != nil {
  663. return err
  664. }
  665. if oldMid > 0 {
  666. m, err := GetMilestoneById(oldMid)
  667. if err != nil {
  668. return err
  669. }
  670. m.NumIssues--
  671. if issue.IsClosed {
  672. m.NumClosedIssues--
  673. }
  674. if m.NumIssues > 0 {
  675. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  676. } else {
  677. m.Completeness = 0
  678. }
  679. if _, err = sess.Id(m.Id).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil {
  680. sess.Rollback()
  681. return err
  682. }
  683. rawSql := "UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?"
  684. if _, err = sess.Exec(rawSql, issue.ID); err != nil {
  685. sess.Rollback()
  686. return err
  687. }
  688. }
  689. if mid > 0 {
  690. m, err := GetMilestoneById(mid)
  691. if err != nil {
  692. return err
  693. }
  694. m.NumIssues++
  695. if issue.IsClosed {
  696. m.NumClosedIssues++
  697. }
  698. if m.NumIssues == 0 {
  699. return ErrWrongIssueCounter
  700. }
  701. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  702. if _, err = sess.Id(m.Id).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil {
  703. sess.Rollback()
  704. return err
  705. }
  706. rawSql := "UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?"
  707. if _, err = sess.Exec(rawSql, m.Id, issue.ID); err != nil {
  708. sess.Rollback()
  709. return err
  710. }
  711. }
  712. return sess.Commit()
  713. }
  714. // DeleteMilestone deletes a milestone.
  715. func DeleteMilestone(m *Milestone) (err error) {
  716. sess := x.NewSession()
  717. defer sess.Close()
  718. if err = sess.Begin(); err != nil {
  719. return err
  720. }
  721. if _, err = sess.Delete(m); err != nil {
  722. sess.Rollback()
  723. return err
  724. }
  725. rawSql := "UPDATE `repository` SET num_milestones = num_milestones - 1 WHERE id = ?"
  726. if _, err = sess.Exec(rawSql, m.RepoId); err != nil {
  727. sess.Rollback()
  728. return err
  729. }
  730. rawSql = "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?"
  731. if _, err = sess.Exec(rawSql, m.Id); err != nil {
  732. sess.Rollback()
  733. return err
  734. }
  735. rawSql = "UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?"
  736. if _, err = sess.Exec(rawSql, m.Id); err != nil {
  737. sess.Rollback()
  738. return err
  739. }
  740. return sess.Commit()
  741. }
  742. // _________ __
  743. // \_ ___ \ ____ _____ _____ ____ _____/ |_
  744. // / \ \/ / _ \ / \ / \_/ __ \ / \ __\
  745. // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ |
  746. // \______ /\____/|__|_| /__|_| /\___ >___| /__|
  747. // \/ \/ \/ \/ \/
  748. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  749. type CommentType int
  750. const (
  751. // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0)
  752. COMMENT_TYPE_COMMENT CommentType = iota
  753. COMMENT_TYPE_REOPEN
  754. COMMENT_TYPE_CLOSE
  755. // References.
  756. COMMENT_TYPE_ISSUE
  757. // Reference from some commit (not part of a pull request)
  758. COMMENT_TYPE_COMMIT
  759. // Reference from some pull request
  760. COMMENT_TYPE_PULL
  761. )
  762. // Comment represents a comment in commit and issue page.
  763. type Comment struct {
  764. Id int64
  765. Type CommentType
  766. PosterId int64
  767. Poster *User `xorm:"-"`
  768. IssueId int64
  769. CommitId int64
  770. Line int64
  771. Content string `xorm:"TEXT"`
  772. Created time.Time `xorm:"CREATED"`
  773. }
  774. // CreateComment creates comment of issue or commit.
  775. func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) {
  776. sess := x.NewSession()
  777. defer sess.Close()
  778. if err := sess.Begin(); err != nil {
  779. return nil, err
  780. }
  781. comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId,
  782. CommitId: commitId, Line: line, Content: content}
  783. if _, err := sess.Insert(comment); err != nil {
  784. sess.Rollback()
  785. return nil, err
  786. }
  787. // Check comment type.
  788. switch cmtType {
  789. case COMMENT_TYPE_COMMENT:
  790. rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?"
  791. if _, err := sess.Exec(rawSql, issueId); err != nil {
  792. sess.Rollback()
  793. return nil, err
  794. }
  795. if len(attachments) > 0 {
  796. rawSql = "UPDATE `attachment` SET comment_id = ? WHERE id IN (?)"
  797. astrs := make([]string, 0, len(attachments))
  798. for _, a := range attachments {
  799. astrs = append(astrs, strconv.FormatInt(a, 10))
  800. }
  801. if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil {
  802. sess.Rollback()
  803. return nil, err
  804. }
  805. }
  806. case COMMENT_TYPE_REOPEN:
  807. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?"
  808. if _, err := sess.Exec(rawSql, repoId); err != nil {
  809. sess.Rollback()
  810. return nil, err
  811. }
  812. case COMMENT_TYPE_CLOSE:
  813. rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?"
  814. if _, err := sess.Exec(rawSql, repoId); err != nil {
  815. sess.Rollback()
  816. return nil, err
  817. }
  818. }
  819. return comment, sess.Commit()
  820. }
  821. // GetCommentById returns the comment with the given id
  822. func GetCommentById(commentId int64) (*Comment, error) {
  823. c := &Comment{Id: commentId}
  824. _, err := x.Get(c)
  825. return c, err
  826. }
  827. func (c *Comment) ContentHtml() template.HTML {
  828. return template.HTML(c.Content)
  829. }
  830. // GetIssueComments returns list of comment by given issue id.
  831. func GetIssueComments(issueId int64) ([]Comment, error) {
  832. comments := make([]Comment, 0, 10)
  833. err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId})
  834. return comments, err
  835. }
  836. // Attachments returns the attachments for this comment.
  837. func (c *Comment) Attachments() []*Attachment {
  838. a, _ := GetAttachmentsByComment(c.Id)
  839. return a
  840. }
  841. func (c *Comment) AfterDelete() {
  842. _, err := DeleteAttachmentsByComment(c.Id, true)
  843. if err != nil {
  844. log.Info("Could not delete files for comment %d on issue #%d: %s", c.Id, c.IssueId, err)
  845. }
  846. }
  847. type Attachment struct {
  848. Id int64
  849. IssueId int64
  850. CommentId int64
  851. Name string
  852. Path string `xorm:"TEXT"`
  853. Created time.Time `xorm:"CREATED"`
  854. }
  855. // CreateAttachment creates a new attachment inside the database and
  856. func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) {
  857. sess := x.NewSession()
  858. defer sess.Close()
  859. if err := sess.Begin(); err != nil {
  860. return nil, err
  861. }
  862. a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path}
  863. if _, err := sess.Insert(a); err != nil {
  864. sess.Rollback()
  865. return nil, err
  866. }
  867. return a, sess.Commit()
  868. }
  869. // Attachment returns the attachment by given ID.
  870. func GetAttachmentById(id int64) (*Attachment, error) {
  871. m := &Attachment{Id: id}
  872. has, err := x.Get(m)
  873. if err != nil {
  874. return nil, err
  875. }
  876. if !has {
  877. return nil, ErrAttachmentNotExist
  878. }
  879. return m, nil
  880. }
  881. func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) {
  882. attachments := make([]*Attachment, 0, 10)
  883. err := x.Where("issue_id = ?", issueId).And("comment_id = 0").Find(&attachments)
  884. return attachments, err
  885. }
  886. // GetAttachmentsByIssue returns a list of attachments for the given issue
  887. func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) {
  888. attachments := make([]*Attachment, 0, 10)
  889. err := x.Where("issue_id = ?", issueId).And("comment_id > 0").Find(&attachments)
  890. return attachments, err
  891. }
  892. // GetAttachmentsByComment returns a list of attachments for the given comment
  893. func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) {
  894. attachments := make([]*Attachment, 0, 10)
  895. err := x.Where("comment_id = ?", commentId).Find(&attachments)
  896. return attachments, err
  897. }
  898. // DeleteAttachment deletes the given attachment and optionally the associated file.
  899. func DeleteAttachment(a *Attachment, remove bool) error {
  900. _, err := DeleteAttachments([]*Attachment{a}, remove)
  901. return err
  902. }
  903. // DeleteAttachments deletes the given attachments and optionally the associated files.
  904. func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
  905. for i, a := range attachments {
  906. if remove {
  907. if err := os.Remove(a.Path); err != nil {
  908. return i, err
  909. }
  910. }
  911. if _, err := x.Delete(a.Id); err != nil {
  912. return i, err
  913. }
  914. }
  915. return len(attachments), nil
  916. }
  917. // DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
  918. func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
  919. attachments, err := GetAttachmentsByIssue(issueId)
  920. if err != nil {
  921. return 0, err
  922. }
  923. return DeleteAttachments(attachments, remove)
  924. }
  925. // DeleteAttachmentsByComment deletes all attachments associated with the given comment.
  926. func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
  927. attachments, err := GetAttachmentsByComment(commentId)
  928. if err != nil {
  929. return 0, err
  930. }
  931. return DeleteAttachments(attachments, remove)
  932. }