issue.go 33 KB

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