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