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 31 kB

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