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

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