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 49 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
10 years ago
10 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
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
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 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
10 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
10 years ago
10 years ago
10 years ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868
  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. "io"
  11. "mime/multipart"
  12. "os"
  13. "path"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "github.com/Unknwon/com"
  18. "github.com/go-xorm/xorm"
  19. gouuid "github.com/satori/go.uuid"
  20. "github.com/gogits/gogs/modules/base"
  21. "github.com/gogits/gogs/modules/log"
  22. "github.com/gogits/gogs/modules/setting"
  23. )
  24. var (
  25. ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
  26. ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
  27. ErrMissingIssueNumber = errors.New("No issue number specified")
  28. )
  29. // Issue represents an issue or pull request of repository.
  30. type Issue struct {
  31. ID int64 `xorm:"pk autoincr"`
  32. RepoID int64 `xorm:"INDEX"`
  33. Index int64 // Index in one repository.
  34. Name string
  35. Repo *Repository `xorm:"-"`
  36. PosterID int64
  37. Poster *User `xorm:"-"`
  38. Labels []*Label `xorm:"-"`
  39. MilestoneID int64
  40. Milestone *Milestone `xorm:"-"`
  41. AssigneeID int64
  42. Assignee *User `xorm:"-"`
  43. IsRead bool `xorm:"-"`
  44. IsPull bool // Indicates whether is a pull request or not.
  45. *PullRequest `xorm:"-"`
  46. IsClosed bool
  47. Content string `xorm:"TEXT"`
  48. RenderedContent string `xorm:"-"`
  49. Priority int
  50. NumComments int
  51. Deadline time.Time
  52. Created time.Time `xorm:"CREATED"`
  53. Updated time.Time `xorm:"UPDATED"`
  54. Attachments []*Attachment `xorm:"-"`
  55. Comments []*Comment `xorm:"-"`
  56. }
  57. func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
  58. var err error
  59. switch colName {
  60. case "id":
  61. i.Attachments, err = GetAttachmentsByIssueID(i.ID)
  62. if err != nil {
  63. log.Error(3, "GetAttachmentsByIssueID[%d]: %v", i.ID, err)
  64. }
  65. i.Comments, err = GetCommentsByIssueID(i.ID)
  66. if err != nil {
  67. log.Error(3, "GetCommentsByIssueID[%d]: %v", i.ID, err)
  68. }
  69. case "milestone_id":
  70. if i.MilestoneID == 0 {
  71. return
  72. }
  73. i.Milestone, err = GetMilestoneByID(i.MilestoneID)
  74. if err != nil {
  75. log.Error(3, "GetMilestoneById[%d]: %v", i.ID, err)
  76. }
  77. case "assignee_id":
  78. if i.AssigneeID == 0 {
  79. return
  80. }
  81. i.Assignee, err = GetUserByID(i.AssigneeID)
  82. if err != nil {
  83. log.Error(3, "GetUserByID[%d]: %v", i.ID, err)
  84. }
  85. case "created":
  86. i.Created = regulateTimeZone(i.Created)
  87. }
  88. }
  89. // HashTag returns unique hash tag for issue.
  90. func (i *Issue) HashTag() string {
  91. return "issue-" + com.ToStr(i.ID)
  92. }
  93. // IsPoster returns true if given user by ID is the poster.
  94. func (i *Issue) IsPoster(uid int64) bool {
  95. return i.PosterID == uid
  96. }
  97. func (i *Issue) GetPoster() (err error) {
  98. i.Poster, err = GetUserByID(i.PosterID)
  99. if IsErrUserNotExist(err) {
  100. i.PosterID = -1
  101. i.Poster = NewFakeUser()
  102. return nil
  103. }
  104. return err
  105. }
  106. func (i *Issue) hasLabel(e Engine, labelID int64) bool {
  107. return hasIssueLabel(e, i.ID, labelID)
  108. }
  109. // HasLabel returns true if issue has been labeled by given ID.
  110. func (i *Issue) HasLabel(labelID int64) bool {
  111. return i.hasLabel(x, labelID)
  112. }
  113. func (i *Issue) addLabel(e *xorm.Session, label *Label) error {
  114. return newIssueLabel(e, i, label)
  115. }
  116. // AddLabel adds new label to issue by given ID.
  117. func (i *Issue) AddLabel(label *Label) (err error) {
  118. sess := x.NewSession()
  119. defer sessionRelease(sess)
  120. if err = sess.Begin(); err != nil {
  121. return err
  122. }
  123. if err = i.addLabel(sess, label); err != nil {
  124. return err
  125. }
  126. return sess.Commit()
  127. }
  128. func (i *Issue) getLabels(e Engine) (err error) {
  129. if len(i.Labels) > 0 {
  130. return nil
  131. }
  132. i.Labels, err = getLabelsByIssueID(e, i.ID)
  133. if err != nil {
  134. return fmt.Errorf("getLabelsByIssueID: %v", err)
  135. }
  136. return nil
  137. }
  138. // GetLabels retrieves all labels of issue and assign to corresponding field.
  139. func (i *Issue) GetLabels() error {
  140. return i.getLabels(x)
  141. }
  142. func (i *Issue) removeLabel(e *xorm.Session, label *Label) error {
  143. return deleteIssueLabel(e, i, label)
  144. }
  145. // RemoveLabel removes a label from issue by given ID.
  146. func (i *Issue) RemoveLabel(label *Label) (err error) {
  147. sess := x.NewSession()
  148. defer sessionRelease(sess)
  149. if err = sess.Begin(); err != nil {
  150. return err
  151. }
  152. if err = i.removeLabel(sess, label); err != nil {
  153. return err
  154. }
  155. return sess.Commit()
  156. }
  157. func (i *Issue) ClearLabels() (err error) {
  158. sess := x.NewSession()
  159. defer sessionRelease(sess)
  160. if err = sess.Begin(); err != nil {
  161. return err
  162. }
  163. if err = i.getLabels(sess); err != nil {
  164. return err
  165. }
  166. for idx := range i.Labels {
  167. if err = i.removeLabel(sess, i.Labels[idx]); err != nil {
  168. return err
  169. }
  170. }
  171. return sess.Commit()
  172. }
  173. func (i *Issue) GetAssignee() (err error) {
  174. if i.AssigneeID == 0 || i.Assignee != nil {
  175. return nil
  176. }
  177. i.Assignee, err = GetUserByID(i.AssigneeID)
  178. if IsErrUserNotExist(err) {
  179. return nil
  180. }
  181. return err
  182. }
  183. // ReadBy sets issue to be read by given user.
  184. func (i *Issue) ReadBy(uid int64) error {
  185. return UpdateIssueUserByRead(uid, i.ID)
  186. }
  187. func (i *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isClosed bool) (err error) {
  188. if i.IsClosed == isClosed {
  189. return nil
  190. }
  191. i.IsClosed = isClosed
  192. if err = updateIssueCols(e, i, "is_closed"); err != nil {
  193. return err
  194. } else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil {
  195. return err
  196. }
  197. // Update labels.
  198. if err = i.getLabels(e); err != nil {
  199. return err
  200. }
  201. for idx := range i.Labels {
  202. if i.IsClosed {
  203. i.Labels[idx].NumClosedIssues++
  204. } else {
  205. i.Labels[idx].NumClosedIssues--
  206. }
  207. if err = updateLabel(e, i.Labels[idx]); err != nil {
  208. return err
  209. }
  210. }
  211. // Update milestone.
  212. if err = changeMilestoneIssueStats(e, i); err != nil {
  213. return err
  214. }
  215. // New action comment.
  216. if _, err = createStatusComment(e, doer, repo, i); err != nil {
  217. return err
  218. }
  219. return nil
  220. }
  221. // ChangeStatus changes issue status to open/closed.
  222. func (i *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) {
  223. sess := x.NewSession()
  224. defer sessionRelease(sess)
  225. if err = sess.Begin(); err != nil {
  226. return err
  227. }
  228. if err = i.changeStatus(sess, doer, repo, isClosed); err != nil {
  229. return err
  230. }
  231. return sess.Commit()
  232. }
  233. func (i *Issue) GetPullRequest() (err error) {
  234. if i.PullRequest != nil {
  235. return nil
  236. }
  237. i.PullRequest, err = GetPullRequestByIssueID(i.ID)
  238. return err
  239. }
  240. // It's caller's responsibility to create action.
  241. func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) {
  242. if _, err = e.Insert(issue); err != nil {
  243. return err
  244. }
  245. if isPull {
  246. _, err = e.Exec("UPDATE `repository` SET num_pulls=num_pulls+1 WHERE id=?", issue.RepoID)
  247. } else {
  248. _, err = e.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID)
  249. }
  250. if err != nil {
  251. return err
  252. }
  253. var label *Label
  254. for _, id := range labelIDs {
  255. if id == 0 {
  256. continue
  257. }
  258. label, err = getLabelByID(e, id)
  259. if err != nil {
  260. return err
  261. }
  262. if err = issue.addLabel(e, label); err != nil {
  263. return fmt.Errorf("addLabel: %v", err)
  264. }
  265. }
  266. if issue.MilestoneID > 0 {
  267. if err = changeMilestoneAssign(e, 0, issue); err != nil {
  268. return err
  269. }
  270. }
  271. if err = newIssueUsers(e, repo, issue); err != nil {
  272. return err
  273. }
  274. // Check attachments.
  275. attachments := make([]*Attachment, 0, len(uuids))
  276. for _, uuid := range uuids {
  277. attach, err := getAttachmentByUUID(e, uuid)
  278. if err != nil {
  279. if IsErrAttachmentNotExist(err) {
  280. continue
  281. }
  282. return fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
  283. }
  284. attachments = append(attachments, attach)
  285. }
  286. for i := range attachments {
  287. attachments[i].IssueID = issue.ID
  288. // No assign value could be 0, so ignore AllCols().
  289. if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  290. return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
  291. }
  292. }
  293. return nil
  294. }
  295. // NewIssue creates new issue with labels for repository.
  296. func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  297. sess := x.NewSession()
  298. defer sessionRelease(sess)
  299. if err = sess.Begin(); err != nil {
  300. return err
  301. }
  302. if err = newIssue(sess, repo, issue, labelIDs, uuids, false); err != nil {
  303. return fmt.Errorf("newIssue: %v", err)
  304. }
  305. // Notify watchers.
  306. act := &Action{
  307. ActUserID: issue.Poster.Id,
  308. ActUserName: issue.Poster.Name,
  309. ActEmail: issue.Poster.Email,
  310. OpType: ACTION_CREATE_ISSUE,
  311. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
  312. RepoID: repo.ID,
  313. RepoUserName: repo.Owner.Name,
  314. RepoName: repo.Name,
  315. IsPrivate: repo.IsPrivate,
  316. }
  317. if err = notifyWatchers(sess, act); err != nil {
  318. return err
  319. }
  320. return sess.Commit()
  321. }
  322. // GetIssueByRef returns an Issue specified by a GFM reference.
  323. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  324. func GetIssueByRef(ref string) (*Issue, error) {
  325. n := strings.IndexByte(ref, byte('#'))
  326. if n == -1 {
  327. return nil, ErrMissingIssueNumber
  328. }
  329. index, err := com.StrTo(ref[n+1:]).Int64()
  330. if err != nil {
  331. return nil, err
  332. }
  333. repo, err := GetRepositoryByRef(ref[:n])
  334. if err != nil {
  335. return nil, err
  336. }
  337. issue, err := GetIssueByIndex(repo.ID, index)
  338. if err != nil {
  339. return nil, err
  340. }
  341. issue.Repo = repo
  342. return issue, nil
  343. }
  344. // GetIssueByIndex returns issue by given index in repository.
  345. func GetIssueByIndex(repoID, index int64) (*Issue, error) {
  346. issue := &Issue{
  347. RepoID: repoID,
  348. Index: index,
  349. }
  350. has, err := x.Get(issue)
  351. if err != nil {
  352. return nil, err
  353. } else if !has {
  354. return nil, ErrIssueNotExist{0, repoID, index}
  355. }
  356. return issue, nil
  357. }
  358. // GetIssueByID returns an issue by given ID.
  359. func GetIssueByID(id int64) (*Issue, error) {
  360. issue := new(Issue)
  361. has, err := x.Id(id).Get(issue)
  362. if err != nil {
  363. return nil, err
  364. } else if !has {
  365. return nil, ErrIssueNotExist{id, 0, 0}
  366. }
  367. return issue, nil
  368. }
  369. type IssuesOptions struct {
  370. UserID int64
  371. AssigneeID int64
  372. RepoID int64
  373. PosterID int64
  374. MilestoneID int64
  375. RepoIDs []int64
  376. Page int
  377. IsClosed bool
  378. IsMention bool
  379. IsPull bool
  380. Labels string
  381. SortType string
  382. }
  383. // Issues returns a list of issues by given conditions.
  384. func Issues(opts *IssuesOptions) ([]*Issue, error) {
  385. sess := x.Limit(setting.IssuePagingNum, (opts.Page-1)*setting.IssuePagingNum)
  386. if opts.RepoID > 0 {
  387. sess.Where("issue.repo_id=?", opts.RepoID).And("issue.is_closed=?", opts.IsClosed)
  388. } else if opts.RepoIDs != nil {
  389. // In case repository IDs are provided but actually no repository has issue.
  390. if len(opts.RepoIDs) == 0 {
  391. return make([]*Issue, 0), nil
  392. }
  393. sess.Where("issue.repo_id IN ("+strings.Join(base.Int64sToStrings(opts.RepoIDs), ",")+")").And("issue.is_closed=?", opts.IsClosed)
  394. } else {
  395. sess.Where("issue.is_closed=?", opts.IsClosed)
  396. }
  397. if opts.AssigneeID > 0 {
  398. sess.And("issue.assignee_id=?", opts.AssigneeID)
  399. } else if opts.PosterID > 0 {
  400. sess.And("issue.poster_id=?", opts.PosterID)
  401. }
  402. if opts.MilestoneID > 0 {
  403. sess.And("issue.milestone_id=?", opts.MilestoneID)
  404. }
  405. sess.And("issue.is_pull=?", opts.IsPull)
  406. switch opts.SortType {
  407. case "oldest":
  408. sess.Asc("created")
  409. case "recentupdate":
  410. sess.Desc("updated")
  411. case "leastupdate":
  412. sess.Asc("updated")
  413. case "mostcomment":
  414. sess.Desc("num_comments")
  415. case "leastcomment":
  416. sess.Asc("num_comments")
  417. case "priority":
  418. sess.Desc("priority")
  419. default:
  420. sess.Desc("created")
  421. }
  422. labelIDs := base.StringsToInt64s(strings.Split(opts.Labels, ","))
  423. if len(labelIDs) > 0 {
  424. validJoin := false
  425. queryStr := "issue.id=issue_label.issue_id"
  426. for _, id := range labelIDs {
  427. if id == 0 {
  428. continue
  429. }
  430. validJoin = true
  431. queryStr += " AND issue_label.label_id=" + com.ToStr(id)
  432. }
  433. if validJoin {
  434. sess.Join("INNER", "issue_label", queryStr)
  435. }
  436. }
  437. if opts.IsMention {
  438. queryStr := "issue.id=issue_user.issue_id AND issue_user.is_mentioned=1"
  439. if opts.UserID > 0 {
  440. queryStr += " AND issue_user.uid=" + com.ToStr(opts.UserID)
  441. }
  442. sess.Join("INNER", "issue_user", queryStr)
  443. }
  444. issues := make([]*Issue, 0, setting.IssuePagingNum)
  445. return issues, sess.Find(&issues)
  446. }
  447. type IssueStatus int
  448. const (
  449. IS_OPEN = iota + 1
  450. IS_CLOSE
  451. )
  452. // GetIssueCountByPoster returns number of issues of repository by poster.
  453. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  454. count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  455. return count
  456. }
  457. // .___ ____ ___
  458. // | | ______ ________ __ ____ | | \______ ___________
  459. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  460. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  461. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  462. // \/ \/ \/ \/ \/
  463. // IssueUser represents an issue-user relation.
  464. type IssueUser struct {
  465. ID int64 `xorm:"pk autoincr"`
  466. UID int64 `xorm:"INDEX"` // User ID.
  467. IssueID int64
  468. RepoID int64 `xorm:"INDEX"`
  469. MilestoneID int64
  470. IsRead bool
  471. IsAssigned bool
  472. IsMentioned bool
  473. IsPoster bool
  474. IsClosed bool
  475. }
  476. func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
  477. users, err := repo.GetAssignees()
  478. if err != nil {
  479. return err
  480. }
  481. iu := &IssueUser{
  482. IssueID: issue.ID,
  483. RepoID: repo.ID,
  484. }
  485. // Poster can be anyone.
  486. isNeedAddPoster := true
  487. for _, u := range users {
  488. iu.ID = 0
  489. iu.UID = u.Id
  490. iu.IsPoster = iu.UID == issue.PosterID
  491. if isNeedAddPoster && iu.IsPoster {
  492. isNeedAddPoster = false
  493. }
  494. iu.IsAssigned = iu.UID == issue.AssigneeID
  495. if _, err = e.Insert(iu); err != nil {
  496. return err
  497. }
  498. }
  499. if isNeedAddPoster {
  500. iu.ID = 0
  501. iu.UID = issue.PosterID
  502. iu.IsPoster = true
  503. if _, err = e.Insert(iu); err != nil {
  504. return err
  505. }
  506. }
  507. return nil
  508. }
  509. // NewIssueUsers adds new issue-user relations for new issue of repository.
  510. func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
  511. sess := x.NewSession()
  512. defer sessionRelease(sess)
  513. if err = sess.Begin(); err != nil {
  514. return err
  515. }
  516. if err = newIssueUsers(sess, repo, issue); err != nil {
  517. return err
  518. }
  519. return sess.Commit()
  520. }
  521. // PairsContains returns true when pairs list contains given issue.
  522. func PairsContains(ius []*IssueUser, issueId, uid int64) int {
  523. for i := range ius {
  524. if ius[i].IssueID == issueId &&
  525. ius[i].UID == uid {
  526. return i
  527. }
  528. }
  529. return -1
  530. }
  531. // GetIssueUsers returns issue-user pairs by given repository and user.
  532. func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  533. ius := make([]*IssueUser, 0, 10)
  534. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
  535. return ius, err
  536. }
  537. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  538. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  539. if len(rids) == 0 {
  540. return []*IssueUser{}, nil
  541. }
  542. buf := bytes.NewBufferString("")
  543. for _, rid := range rids {
  544. buf.WriteString("repo_id=")
  545. buf.WriteString(com.ToStr(rid))
  546. buf.WriteString(" OR ")
  547. }
  548. cond := strings.TrimSuffix(buf.String(), " OR ")
  549. ius := make([]*IssueUser, 0, 10)
  550. sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  551. if len(cond) > 0 {
  552. sess.And(cond)
  553. }
  554. err := sess.Find(&ius)
  555. return ius, err
  556. }
  557. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  558. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  559. ius := make([]*IssueUser, 0, 10)
  560. sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  561. if rid > 0 {
  562. sess.And("repo_id=?", rid)
  563. }
  564. switch filterMode {
  565. case FM_ASSIGN:
  566. sess.And("is_assigned=?", true)
  567. case FM_CREATE:
  568. sess.And("is_poster=?", true)
  569. default:
  570. return ius, nil
  571. }
  572. err := sess.Find(&ius)
  573. return ius, err
  574. }
  575. func UpdateMentions(userNames []string, issueId int64) error {
  576. for i := range userNames {
  577. userNames[i] = strings.ToLower(userNames[i])
  578. }
  579. users := make([]*User, 0, len(userNames))
  580. if err := x.Where("lower_name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("lower_name ASC").Find(&users); err != nil {
  581. return err
  582. }
  583. ids := make([]int64, 0, len(userNames))
  584. for _, user := range users {
  585. ids = append(ids, user.Id)
  586. if !user.IsOrganization() {
  587. continue
  588. }
  589. if user.NumMembers == 0 {
  590. continue
  591. }
  592. tempIds := make([]int64, 0, user.NumMembers)
  593. orgUsers, err := GetOrgUsersByOrgId(user.Id)
  594. if err != nil {
  595. return err
  596. }
  597. for _, orgUser := range orgUsers {
  598. tempIds = append(tempIds, orgUser.ID)
  599. }
  600. ids = append(ids, tempIds...)
  601. }
  602. if err := UpdateIssueUsersByMentions(ids, issueId); err != nil {
  603. return err
  604. }
  605. return nil
  606. }
  607. // IssueStats represents issue statistic information.
  608. type IssueStats struct {
  609. OpenCount, ClosedCount int64
  610. AllCount int64
  611. AssignCount int64
  612. CreateCount int64
  613. MentionCount int64
  614. }
  615. // Filter modes.
  616. const (
  617. FM_ALL = iota
  618. FM_ASSIGN
  619. FM_CREATE
  620. FM_MENTION
  621. )
  622. func parseCountResult(results []map[string][]byte) int64 {
  623. if len(results) == 0 {
  624. return 0
  625. }
  626. for _, result := range results[0] {
  627. return com.StrTo(string(result)).MustInt64()
  628. }
  629. return 0
  630. }
  631. type IssueStatsOptions struct {
  632. RepoID int64
  633. UserID int64
  634. LabelID int64
  635. MilestoneID int64
  636. AssigneeID int64
  637. FilterMode int
  638. IsPull bool
  639. }
  640. // GetIssueStats returns issue statistic information by given conditions.
  641. func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
  642. stats := &IssueStats{}
  643. queryStr := "SELECT COUNT(*) FROM `issue` "
  644. if opts.LabelID > 0 {
  645. queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(opts.LabelID)
  646. }
  647. baseCond := " WHERE issue.repo_id=" + com.ToStr(opts.RepoID) + " AND issue.is_closed=?"
  648. if opts.MilestoneID > 0 {
  649. baseCond += " AND issue.milestone_id=" + com.ToStr(opts.MilestoneID)
  650. }
  651. if opts.AssigneeID > 0 {
  652. baseCond += " AND assignee_id=" + com.ToStr(opts.AssigneeID)
  653. }
  654. baseCond += " AND issue.is_pull=?"
  655. switch opts.FilterMode {
  656. case FM_ALL, FM_ASSIGN:
  657. results, _ := x.Query(queryStr+baseCond, false, opts.IsPull)
  658. stats.OpenCount = parseCountResult(results)
  659. results, _ = x.Query(queryStr+baseCond, true, opts.IsPull)
  660. stats.ClosedCount = parseCountResult(results)
  661. case FM_CREATE:
  662. baseCond += " AND poster_id=?"
  663. results, _ := x.Query(queryStr+baseCond, false, opts.IsPull, opts.UserID)
  664. stats.OpenCount = parseCountResult(results)
  665. results, _ = x.Query(queryStr+baseCond, true, opts.IsPull, opts.UserID)
  666. stats.ClosedCount = parseCountResult(results)
  667. case FM_MENTION:
  668. queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id"
  669. baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?"
  670. results, _ := x.Query(queryStr+baseCond, false, opts.IsPull, opts.UserID, true)
  671. stats.OpenCount = parseCountResult(results)
  672. results, _ = x.Query(queryStr+baseCond, true, opts.IsPull, opts.UserID, true)
  673. stats.ClosedCount = parseCountResult(results)
  674. }
  675. return stats
  676. }
  677. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  678. func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPull bool) *IssueStats {
  679. stats := &IssueStats{}
  680. queryStr := "SELECT COUNT(*) FROM `issue` "
  681. baseCond := " WHERE issue.is_closed=?"
  682. if repoID > 0 || len(repoIDs) == 0 {
  683. baseCond += " AND issue.repo_id=" + com.ToStr(repoID)
  684. } else {
  685. baseCond += " AND issue.repo_id IN (" + strings.Join(base.Int64sToStrings(repoIDs), ",") + ")"
  686. }
  687. if isPull {
  688. baseCond += " AND issue.is_pull=1"
  689. } else {
  690. baseCond += " AND issue.is_pull=0"
  691. }
  692. results, _ := x.Query(queryStr+baseCond+" AND assignee_id=?", false, uid)
  693. stats.AssignCount = parseCountResult(results)
  694. results, _ = x.Query(queryStr+baseCond+" AND poster_id=?", false, uid)
  695. stats.CreateCount = parseCountResult(results)
  696. switch filterMode {
  697. case FM_ASSIGN:
  698. baseCond += " AND assignee_id=" + com.ToStr(uid)
  699. case FM_CREATE:
  700. baseCond += " AND poster_id=" + com.ToStr(uid)
  701. }
  702. results, _ = x.Query(queryStr+baseCond, false)
  703. stats.OpenCount = parseCountResult(results)
  704. results, _ = x.Query(queryStr+baseCond, true)
  705. stats.ClosedCount = parseCountResult(results)
  706. return stats
  707. }
  708. // GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
  709. func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen int64, numClosed int64) {
  710. queryStr := "SELECT COUNT(*) FROM `issue` "
  711. baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?"
  712. if isPull {
  713. baseCond += " AND issue.is_pull=1"
  714. } else {
  715. baseCond += " AND issue.is_pull=0"
  716. }
  717. switch filterMode {
  718. case FM_ASSIGN:
  719. baseCond += " AND assignee_id=" + com.ToStr(uid)
  720. case FM_CREATE:
  721. baseCond += " AND poster_id=" + com.ToStr(uid)
  722. }
  723. results, _ := x.Query(queryStr+baseCond, repoID, false)
  724. numOpen = parseCountResult(results)
  725. results, _ = x.Query(queryStr+baseCond, repoID, true)
  726. numClosed = parseCountResult(results)
  727. return numOpen, numClosed
  728. }
  729. func updateIssue(e Engine, issue *Issue) error {
  730. _, err := e.Id(issue.ID).AllCols().Update(issue)
  731. return err
  732. }
  733. // UpdateIssue updates all fields of given issue.
  734. func UpdateIssue(issue *Issue) error {
  735. return updateIssue(x, issue)
  736. }
  737. // updateIssueCols updates specific fields of given issue.
  738. func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
  739. _, err := e.Id(issue.ID).Cols(cols...).Update(issue)
  740. return err
  741. }
  742. func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error {
  743. _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID)
  744. return err
  745. }
  746. // UpdateIssueUsersByStatus updates issue-user relations by issue status.
  747. func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error {
  748. return updateIssueUsersByStatus(x, issueID, isClosed)
  749. }
  750. func updateIssueUserByAssignee(e *xorm.Session, issue *Issue) (err error) {
  751. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issue.ID); err != nil {
  752. return err
  753. }
  754. // Assignee ID equals to 0 means clear assignee.
  755. if issue.AssigneeID > 0 {
  756. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, issue.AssigneeID, issue.ID); err != nil {
  757. return err
  758. }
  759. }
  760. return updateIssue(e, issue)
  761. }
  762. // UpdateIssueUserByAssignee updates issue-user relation for assignee.
  763. func UpdateIssueUserByAssignee(issue *Issue) (err error) {
  764. sess := x.NewSession()
  765. defer sessionRelease(sess)
  766. if err = sess.Begin(); err != nil {
  767. return err
  768. }
  769. if err = updateIssueUserByAssignee(sess, issue); err != nil {
  770. return err
  771. }
  772. return sess.Commit()
  773. }
  774. // UpdateIssueUserByRead updates issue-user relation for reading.
  775. func UpdateIssueUserByRead(uid, issueID int64) error {
  776. _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
  777. return err
  778. }
  779. // UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
  780. func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
  781. for _, uid := range uids {
  782. iu := &IssueUser{UID: uid, IssueID: iid}
  783. has, err := x.Get(iu)
  784. if err != nil {
  785. return err
  786. }
  787. iu.IsMentioned = true
  788. if has {
  789. _, err = x.Id(iu.ID).AllCols().Update(iu)
  790. } else {
  791. _, err = x.Insert(iu)
  792. }
  793. if err != nil {
  794. return err
  795. }
  796. }
  797. return nil
  798. }
  799. // .____ ___. .__
  800. // | | _____ \_ |__ ____ | |
  801. // | | \__ \ | __ \_/ __ \| |
  802. // | |___ / __ \| \_\ \ ___/| |__
  803. // |_______ (____ /___ /\___ >____/
  804. // \/ \/ \/ \/
  805. // Label represents a label of repository for issues.
  806. type Label struct {
  807. ID int64 `xorm:"pk autoincr"`
  808. RepoID int64 `xorm:"INDEX"`
  809. Name string
  810. Color string `xorm:"VARCHAR(7)"`
  811. NumIssues int
  812. NumClosedIssues int
  813. NumOpenIssues int `xorm:"-"`
  814. IsChecked bool `xorm:"-"`
  815. }
  816. // CalOpenIssues calculates the open issues of label.
  817. func (m *Label) CalOpenIssues() {
  818. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  819. }
  820. // ForegroundColor calculates the text color for labels based
  821. // on their background color
  822. func (l *Label) ForegroundColor() template.CSS {
  823. if strings.HasPrefix(l.Color, "#") {
  824. if color, err := strconv.ParseUint(l.Color[1:], 16, 64); err == nil {
  825. r := float32(0xFF & (color >> 16))
  826. g := float32(0xFF & (color >> 8))
  827. b := float32(0xFF & color)
  828. luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
  829. if luminance < 0.5 {
  830. return template.CSS("rgba(255,255,255,.8)")
  831. }
  832. }
  833. }
  834. // default to black
  835. return template.CSS("rgba(0,0,0,.8)")
  836. }
  837. // NewLabel creates new label of repository.
  838. func NewLabel(l *Label) error {
  839. _, err := x.Insert(l)
  840. return err
  841. }
  842. func getLabelByID(e Engine, id int64) (*Label, error) {
  843. if id <= 0 {
  844. return nil, ErrLabelNotExist{id}
  845. }
  846. l := &Label{ID: id}
  847. has, err := x.Get(l)
  848. if err != nil {
  849. return nil, err
  850. } else if !has {
  851. return nil, ErrLabelNotExist{l.ID}
  852. }
  853. return l, nil
  854. }
  855. // GetLabelByID returns a label by given ID.
  856. func GetLabelByID(id int64) (*Label, error) {
  857. return getLabelByID(x, id)
  858. }
  859. // GetLabelsByRepoID returns all labels that belong to given repository by ID.
  860. func GetLabelsByRepoID(repoID int64) ([]*Label, error) {
  861. labels := make([]*Label, 0, 10)
  862. return labels, x.Where("repo_id=?", repoID).Find(&labels)
  863. }
  864. func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
  865. issueLabels, err := getIssueLabels(e, issueID)
  866. if err != nil {
  867. return nil, fmt.Errorf("getIssueLabels: %v", err)
  868. }
  869. var label *Label
  870. labels := make([]*Label, 0, len(issueLabels))
  871. for idx := range issueLabels {
  872. label, err = getLabelByID(e, issueLabels[idx].LabelID)
  873. if err != nil && !IsErrLabelNotExist(err) {
  874. return nil, fmt.Errorf("getLabelByID: %v", err)
  875. }
  876. labels = append(labels, label)
  877. }
  878. return labels, nil
  879. }
  880. // GetLabelsByIssueID returns all labels that belong to given issue by ID.
  881. func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
  882. return getLabelsByIssueID(x, issueID)
  883. }
  884. func updateLabel(e Engine, l *Label) error {
  885. _, err := e.Id(l.ID).AllCols().Update(l)
  886. return err
  887. }
  888. // UpdateLabel updates label information.
  889. func UpdateLabel(l *Label) error {
  890. return updateLabel(x, l)
  891. }
  892. // DeleteLabel delete a label of given repository.
  893. func DeleteLabel(repoID, labelID int64) error {
  894. l, err := GetLabelByID(labelID)
  895. if err != nil {
  896. if IsErrLabelNotExist(err) {
  897. return nil
  898. }
  899. return err
  900. }
  901. sess := x.NewSession()
  902. defer sessionRelease(sess)
  903. if err = sess.Begin(); err != nil {
  904. return err
  905. }
  906. if _, err = x.Where("label_id=?", labelID).Delete(new(IssueLabel)); err != nil {
  907. return err
  908. } else if _, err = sess.Delete(l); err != nil {
  909. return err
  910. }
  911. return sess.Commit()
  912. }
  913. // .___ .____ ___. .__
  914. // | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
  915. // | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
  916. // | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
  917. // |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
  918. // \/ \/ \/ \/ \/ \/ \/
  919. // IssueLabel represetns an issue-lable relation.
  920. type IssueLabel struct {
  921. ID int64 `xorm:"pk autoincr"`
  922. IssueID int64 `xorm:"UNIQUE(s)"`
  923. LabelID int64 `xorm:"UNIQUE(s)"`
  924. }
  925. func hasIssueLabel(e Engine, issueID, labelID int64) bool {
  926. has, _ := e.Where("issue_id=? AND label_id=?", issueID, labelID).Get(new(IssueLabel))
  927. return has
  928. }
  929. // HasIssueLabel returns true if issue has been labeled.
  930. func HasIssueLabel(issueID, labelID int64) bool {
  931. return hasIssueLabel(x, issueID, labelID)
  932. }
  933. func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
  934. if _, err = e.Insert(&IssueLabel{
  935. IssueID: issue.ID,
  936. LabelID: label.ID,
  937. }); err != nil {
  938. return err
  939. }
  940. label.NumIssues++
  941. if issue.IsClosed {
  942. label.NumClosedIssues++
  943. }
  944. return updateLabel(e, label)
  945. }
  946. // NewIssueLabel creates a new issue-label relation.
  947. func NewIssueLabel(issue *Issue, label *Label) (err error) {
  948. sess := x.NewSession()
  949. defer sessionRelease(sess)
  950. if err = sess.Begin(); err != nil {
  951. return err
  952. }
  953. if err = newIssueLabel(sess, issue, label); err != nil {
  954. return err
  955. }
  956. return sess.Commit()
  957. }
  958. func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) {
  959. issueLabels := make([]*IssueLabel, 0, 10)
  960. return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels)
  961. }
  962. // GetIssueLabels returns all issue-label relations of given issue by ID.
  963. func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
  964. return getIssueLabels(x, issueID)
  965. }
  966. func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
  967. if _, err = e.Delete(&IssueLabel{
  968. IssueID: issue.ID,
  969. LabelID: label.ID,
  970. }); err != nil {
  971. return err
  972. }
  973. label.NumIssues--
  974. if issue.IsClosed {
  975. label.NumClosedIssues--
  976. }
  977. return updateLabel(e, label)
  978. }
  979. // DeleteIssueLabel deletes issue-label relation.
  980. func DeleteIssueLabel(issue *Issue, label *Label) (err error) {
  981. sess := x.NewSession()
  982. defer sessionRelease(sess)
  983. if err = sess.Begin(); err != nil {
  984. return err
  985. }
  986. if err = deleteIssueLabel(sess, issue, label); err != nil {
  987. return err
  988. }
  989. return sess.Commit()
  990. }
  991. // _____ .__.__ __
  992. // / \ |__| | ____ _______/ |_ ____ ____ ____
  993. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  994. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  995. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  996. // \/ \/ \/ \/ \/
  997. // Milestone represents a milestone of repository.
  998. type Milestone struct {
  999. ID int64 `xorm:"pk autoincr"`
  1000. RepoID int64 `xorm:"INDEX"`
  1001. Name string
  1002. Content string `xorm:"TEXT"`
  1003. RenderedContent string `xorm:"-"`
  1004. IsClosed bool
  1005. NumIssues int
  1006. NumClosedIssues int
  1007. NumOpenIssues int `xorm:"-"`
  1008. Completeness int // Percentage(1-100).
  1009. Deadline time.Time
  1010. DeadlineString string `xorm:"-"`
  1011. IsOverDue bool `xorm:"-"`
  1012. ClosedDate time.Time
  1013. }
  1014. func (m *Milestone) BeforeUpdate() {
  1015. if m.NumIssues > 0 {
  1016. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  1017. } else {
  1018. m.Completeness = 0
  1019. }
  1020. }
  1021. func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
  1022. if colName == "deadline" {
  1023. if m.Deadline.Year() == 9999 {
  1024. return
  1025. }
  1026. m.Deadline = regulateTimeZone(m.Deadline)
  1027. m.DeadlineString = m.Deadline.Format("2006-01-02")
  1028. if time.Now().After(m.Deadline) {
  1029. m.IsOverDue = true
  1030. }
  1031. }
  1032. }
  1033. // CalOpenIssues calculates the open issues of milestone.
  1034. func (m *Milestone) CalOpenIssues() {
  1035. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  1036. }
  1037. // NewMilestone creates new milestone of repository.
  1038. func NewMilestone(m *Milestone) (err error) {
  1039. sess := x.NewSession()
  1040. defer sessionRelease(sess)
  1041. if err = sess.Begin(); err != nil {
  1042. return err
  1043. }
  1044. if _, err = sess.Insert(m); err != nil {
  1045. return err
  1046. }
  1047. if _, err = sess.Exec("UPDATE `repository` SET num_milestones=num_milestones+1 WHERE id=?", m.RepoID); err != nil {
  1048. return err
  1049. }
  1050. return sess.Commit()
  1051. }
  1052. func getMilestoneByID(e Engine, id int64) (*Milestone, error) {
  1053. m := &Milestone{ID: id}
  1054. has, err := e.Get(m)
  1055. if err != nil {
  1056. return nil, err
  1057. } else if !has {
  1058. return nil, ErrMilestoneNotExist{id, 0}
  1059. }
  1060. return m, nil
  1061. }
  1062. // GetMilestoneByID returns the milestone of given ID.
  1063. func GetMilestoneByID(id int64) (*Milestone, error) {
  1064. return getMilestoneByID(x, id)
  1065. }
  1066. // GetRepoMilestoneByID returns the milestone of given ID and repository.
  1067. func GetRepoMilestoneByID(repoID, milestoneID int64) (*Milestone, error) {
  1068. m := &Milestone{ID: milestoneID, RepoID: repoID}
  1069. has, err := x.Get(m)
  1070. if err != nil {
  1071. return nil, err
  1072. } else if !has {
  1073. return nil, ErrMilestoneNotExist{milestoneID, repoID}
  1074. }
  1075. return m, nil
  1076. }
  1077. // GetAllRepoMilestones returns all milestones of given repository.
  1078. func GetAllRepoMilestones(repoID int64) ([]*Milestone, error) {
  1079. miles := make([]*Milestone, 0, 10)
  1080. return miles, x.Where("repo_id=?", repoID).Find(&miles)
  1081. }
  1082. // GetMilestones returns a list of milestones of given repository and status.
  1083. func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
  1084. miles := make([]*Milestone, 0, setting.IssuePagingNum)
  1085. sess := x.Where("repo_id=? AND is_closed=?", repoID, isClosed)
  1086. if page > 0 {
  1087. sess = sess.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  1088. }
  1089. return miles, sess.Find(&miles)
  1090. }
  1091. func updateMilestone(e Engine, m *Milestone) error {
  1092. _, err := e.Id(m.ID).AllCols().Update(m)
  1093. return err
  1094. }
  1095. // UpdateMilestone updates information of given milestone.
  1096. func UpdateMilestone(m *Milestone) error {
  1097. return updateMilestone(x, m)
  1098. }
  1099. func countRepoMilestones(e Engine, repoID int64) int64 {
  1100. count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone))
  1101. return count
  1102. }
  1103. // CountRepoMilestones returns number of milestones in given repository.
  1104. func CountRepoMilestones(repoID int64) int64 {
  1105. return countRepoMilestones(x, repoID)
  1106. }
  1107. func countRepoClosedMilestones(e Engine, repoID int64) int64 {
  1108. closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone))
  1109. return closed
  1110. }
  1111. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  1112. func CountRepoClosedMilestones(repoID int64) int64 {
  1113. return countRepoClosedMilestones(x, repoID)
  1114. }
  1115. // MilestoneStats returns number of open and closed milestones of given repository.
  1116. func MilestoneStats(repoID int64) (open int64, closed int64) {
  1117. open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone))
  1118. return open, CountRepoClosedMilestones(repoID)
  1119. }
  1120. // ChangeMilestoneStatus changes the milestone open/closed status.
  1121. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  1122. repo, err := GetRepositoryByID(m.RepoID)
  1123. if err != nil {
  1124. return err
  1125. }
  1126. sess := x.NewSession()
  1127. defer sessionRelease(sess)
  1128. if err = sess.Begin(); err != nil {
  1129. return err
  1130. }
  1131. m.IsClosed = isClosed
  1132. if err = updateMilestone(sess, m); err != nil {
  1133. return err
  1134. }
  1135. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  1136. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  1137. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  1138. return err
  1139. }
  1140. return sess.Commit()
  1141. }
  1142. func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
  1143. if issue.MilestoneID == 0 {
  1144. return nil
  1145. }
  1146. m, err := getMilestoneByID(e, issue.MilestoneID)
  1147. if err != nil {
  1148. return err
  1149. }
  1150. if issue.IsClosed {
  1151. m.NumOpenIssues--
  1152. m.NumClosedIssues++
  1153. } else {
  1154. m.NumOpenIssues++
  1155. m.NumClosedIssues--
  1156. }
  1157. return updateMilestone(e, m)
  1158. }
  1159. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  1160. // for the milestone associated with the given issue.
  1161. func ChangeMilestoneIssueStats(issue *Issue) (err error) {
  1162. sess := x.NewSession()
  1163. defer sessionRelease(sess)
  1164. if err = sess.Begin(); err != nil {
  1165. return err
  1166. }
  1167. if err = changeMilestoneIssueStats(sess, issue); err != nil {
  1168. return err
  1169. }
  1170. return sess.Commit()
  1171. }
  1172. func changeMilestoneAssign(e *xorm.Session, oldMid int64, issue *Issue) error {
  1173. if oldMid > 0 {
  1174. m, err := getMilestoneByID(e, oldMid)
  1175. if err != nil {
  1176. return err
  1177. }
  1178. m.NumIssues--
  1179. if issue.IsClosed {
  1180. m.NumClosedIssues--
  1181. }
  1182. if err = updateMilestone(e, m); err != nil {
  1183. return err
  1184. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE issue_id=?", issue.ID); err != nil {
  1185. return err
  1186. }
  1187. }
  1188. if issue.MilestoneID > 0 {
  1189. m, err := getMilestoneByID(e, issue.MilestoneID)
  1190. if err != nil {
  1191. return err
  1192. }
  1193. m.NumIssues++
  1194. if issue.IsClosed {
  1195. m.NumClosedIssues++
  1196. }
  1197. if m.NumIssues == 0 {
  1198. return ErrWrongIssueCounter
  1199. }
  1200. if err = updateMilestone(e, m); err != nil {
  1201. return err
  1202. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=? WHERE issue_id=?", m.ID, issue.ID); err != nil {
  1203. return err
  1204. }
  1205. }
  1206. return updateIssue(e, issue)
  1207. }
  1208. // ChangeMilestoneAssign changes assignment of milestone for issue.
  1209. func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) {
  1210. sess := x.NewSession()
  1211. defer sess.Close()
  1212. if err = sess.Begin(); err != nil {
  1213. return err
  1214. }
  1215. if err = changeMilestoneAssign(sess, oldMid, issue); err != nil {
  1216. return err
  1217. }
  1218. return sess.Commit()
  1219. }
  1220. // DeleteMilestoneByID deletes a milestone by given ID.
  1221. func DeleteMilestoneByID(id int64) error {
  1222. m, err := GetMilestoneByID(id)
  1223. if err != nil {
  1224. if IsErrMilestoneNotExist(err) {
  1225. return nil
  1226. }
  1227. return err
  1228. }
  1229. repo, err := GetRepositoryByID(m.RepoID)
  1230. if err != nil {
  1231. return err
  1232. }
  1233. sess := x.NewSession()
  1234. defer sessionRelease(sess)
  1235. if err = sess.Begin(); err != nil {
  1236. return err
  1237. }
  1238. if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
  1239. return err
  1240. }
  1241. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  1242. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  1243. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  1244. return err
  1245. }
  1246. if _, err = sess.Exec("UPDATE `issue` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  1247. return err
  1248. } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  1249. return err
  1250. }
  1251. return sess.Commit()
  1252. }
  1253. // _________ __
  1254. // \_ ___ \ ____ _____ _____ ____ _____/ |_
  1255. // / \ \/ / _ \ / \ / \_/ __ \ / \ __\
  1256. // \ \___( <_> ) Y Y \ Y Y \ ___/| | \ |
  1257. // \______ /\____/|__|_| /__|_| /\___ >___| /__|
  1258. // \/ \/ \/ \/ \/
  1259. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  1260. type CommentType int
  1261. const (
  1262. // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0)
  1263. COMMENT_TYPE_COMMENT CommentType = iota
  1264. COMMENT_TYPE_REOPEN
  1265. COMMENT_TYPE_CLOSE
  1266. // References.
  1267. COMMENT_TYPE_ISSUE_REF
  1268. // Reference from a commit (not part of a pull request)
  1269. COMMENT_TYPE_COMMIT_REF
  1270. // Reference from a comment
  1271. COMMENT_TYPE_COMMENT_REF
  1272. // Reference from a pull request
  1273. COMMENT_TYPE_PULL_REF
  1274. )
  1275. type CommentTag int
  1276. const (
  1277. COMMENT_TAG_NONE CommentTag = iota
  1278. COMMENT_TAG_POSTER
  1279. COMMENT_TAG_ADMIN
  1280. COMMENT_TAG_OWNER
  1281. )
  1282. // Comment represents a comment in commit and issue page.
  1283. type Comment struct {
  1284. ID int64 `xorm:"pk autoincr"`
  1285. Type CommentType
  1286. PosterID int64
  1287. Poster *User `xorm:"-"`
  1288. IssueID int64 `xorm:"INDEX"`
  1289. CommitID int64
  1290. Line int64
  1291. Content string `xorm:"TEXT"`
  1292. RenderedContent string `xorm:"-"`
  1293. Created time.Time `xorm:"CREATED"`
  1294. // Reference issue in commit message
  1295. CommitSHA string `xorm:"VARCHAR(40)"`
  1296. Attachments []*Attachment `xorm:"-"`
  1297. // For view issue page.
  1298. ShowTag CommentTag `xorm:"-"`
  1299. }
  1300. func (c *Comment) AfterSet(colName string, _ xorm.Cell) {
  1301. var err error
  1302. switch colName {
  1303. case "id":
  1304. c.Attachments, err = GetAttachmentsByCommentID(c.ID)
  1305. if err != nil {
  1306. log.Error(3, "GetAttachmentsByCommentID[%d]: %v", c.ID, err)
  1307. }
  1308. case "poster_id":
  1309. c.Poster, err = GetUserByID(c.PosterID)
  1310. if err != nil {
  1311. if IsErrUserNotExist(err) {
  1312. c.PosterID = -1
  1313. c.Poster = NewFakeUser()
  1314. } else {
  1315. log.Error(3, "GetUserByID[%d]: %v", c.ID, err)
  1316. }
  1317. }
  1318. case "created":
  1319. c.Created = regulateTimeZone(c.Created)
  1320. }
  1321. }
  1322. func (c *Comment) AfterDelete() {
  1323. _, err := DeleteAttachmentsByComment(c.ID, true)
  1324. if err != nil {
  1325. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  1326. }
  1327. }
  1328. // HashTag returns unique hash tag for comment.
  1329. func (c *Comment) HashTag() string {
  1330. return "issuecomment-" + com.ToStr(c.ID)
  1331. }
  1332. // EventTag returns unique event hash tag for comment.
  1333. func (c *Comment) EventTag() string {
  1334. return "event-" + com.ToStr(c.ID)
  1335. }
  1336. func createComment(e *xorm.Session, u *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content, commitSHA string, uuids []string) (_ *Comment, err error) {
  1337. comment := &Comment{
  1338. PosterID: u.Id,
  1339. Type: cmtType,
  1340. IssueID: issue.ID,
  1341. CommitID: commitID,
  1342. Line: line,
  1343. Content: content,
  1344. CommitSHA: commitSHA,
  1345. }
  1346. if _, err = e.Insert(comment); err != nil {
  1347. return nil, err
  1348. }
  1349. // Compose comment action, could be plain comment, close or reopen issue.
  1350. // This object will be used to notify watchers in the end of function.
  1351. act := &Action{
  1352. ActUserID: u.Id,
  1353. ActUserName: u.Name,
  1354. ActEmail: u.Email,
  1355. Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]),
  1356. RepoID: repo.ID,
  1357. RepoUserName: repo.Owner.Name,
  1358. RepoName: repo.Name,
  1359. IsPrivate: repo.IsPrivate,
  1360. }
  1361. // Check comment type.
  1362. switch cmtType {
  1363. case COMMENT_TYPE_COMMENT:
  1364. act.OpType = ACTION_COMMENT_ISSUE
  1365. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", issue.ID); err != nil {
  1366. return nil, err
  1367. }
  1368. // Check attachments.
  1369. attachments := make([]*Attachment, 0, len(uuids))
  1370. for _, uuid := range uuids {
  1371. attach, err := getAttachmentByUUID(e, uuid)
  1372. if err != nil {
  1373. if IsErrAttachmentNotExist(err) {
  1374. continue
  1375. }
  1376. return nil, fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
  1377. }
  1378. attachments = append(attachments, attach)
  1379. }
  1380. for i := range attachments {
  1381. attachments[i].IssueID = issue.ID
  1382. attachments[i].CommentID = comment.ID
  1383. // No assign value could be 0, so ignore AllCols().
  1384. if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  1385. return nil, fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
  1386. }
  1387. }
  1388. case COMMENT_TYPE_REOPEN:
  1389. act.OpType = ACTION_REOPEN_ISSUE
  1390. if issue.IsPull {
  1391. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", repo.ID)
  1392. } else {
  1393. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", repo.ID)
  1394. }
  1395. if err != nil {
  1396. return nil, err
  1397. }
  1398. case COMMENT_TYPE_CLOSE:
  1399. act.OpType = ACTION_CLOSE_ISSUE
  1400. if issue.IsPull {
  1401. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", repo.ID)
  1402. } else {
  1403. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", repo.ID)
  1404. }
  1405. if err != nil {
  1406. return nil, err
  1407. }
  1408. }
  1409. // Notify watchers for whatever action comes in.
  1410. if err = notifyWatchers(e, act); err != nil {
  1411. return nil, fmt.Errorf("notifyWatchers: %v", err)
  1412. }
  1413. return comment, nil
  1414. }
  1415. func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
  1416. cmtType := COMMENT_TYPE_CLOSE
  1417. if !issue.IsClosed {
  1418. cmtType = COMMENT_TYPE_REOPEN
  1419. }
  1420. return createComment(e, doer, repo, issue, 0, 0, cmtType, "", "", nil)
  1421. }
  1422. // CreateComment creates comment of issue or commit.
  1423. func CreateComment(doer *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content, commitSHA string, attachments []string) (comment *Comment, err error) {
  1424. sess := x.NewSession()
  1425. defer sessionRelease(sess)
  1426. if err = sess.Begin(); err != nil {
  1427. return nil, err
  1428. }
  1429. comment, err = createComment(sess, doer, repo, issue, commitID, line, cmtType, content, commitSHA, attachments)
  1430. if err != nil {
  1431. return nil, err
  1432. }
  1433. return comment, sess.Commit()
  1434. }
  1435. // CreateIssueComment creates a plain issue comment.
  1436. func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
  1437. return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, "", attachments)
  1438. }
  1439. // CreateRefComment creates a commit reference comment to issue.
  1440. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  1441. if len(commitSHA) == 0 {
  1442. return fmt.Errorf("cannot create reference with empty commit SHA")
  1443. }
  1444. // Check if same reference from same commit has already existed.
  1445. has, err := x.Get(&Comment{
  1446. Type: COMMENT_TYPE_COMMIT_REF,
  1447. IssueID: issue.ID,
  1448. CommitSHA: commitSHA,
  1449. })
  1450. if err != nil {
  1451. return fmt.Errorf("check reference comment: %v", err)
  1452. } else if has {
  1453. return nil
  1454. }
  1455. _, err = CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMIT_REF, content, commitSHA, nil)
  1456. return err
  1457. }
  1458. // GetCommentByID returns the comment by given ID.
  1459. func GetCommentByID(id int64) (*Comment, error) {
  1460. c := new(Comment)
  1461. has, err := x.Id(id).Get(c)
  1462. if err != nil {
  1463. return nil, err
  1464. } else if !has {
  1465. return nil, ErrCommentNotExist{id}
  1466. }
  1467. return c, nil
  1468. }
  1469. // GetCommentsByIssueID returns all comments of issue by given ID.
  1470. func GetCommentsByIssueID(issueID int64) ([]*Comment, error) {
  1471. comments := make([]*Comment, 0, 10)
  1472. return comments, x.Where("issue_id=?", issueID).Asc("created").Find(&comments)
  1473. }
  1474. // UpdateComment updates information of comment.
  1475. func UpdateComment(c *Comment) error {
  1476. _, err := x.Id(c.ID).AllCols().Update(c)
  1477. return err
  1478. }
  1479. // Attachment represent a attachment of issue/comment/release.
  1480. type Attachment struct {
  1481. ID int64 `xorm:"pk autoincr"`
  1482. UUID string `xorm:"uuid UNIQUE"`
  1483. IssueID int64 `xorm:"INDEX"`
  1484. CommentID int64
  1485. ReleaseID int64 `xorm:"INDEX"`
  1486. Name string
  1487. Created time.Time `xorm:"CREATED"`
  1488. }
  1489. // AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
  1490. func AttachmentLocalPath(uuid string) string {
  1491. return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
  1492. }
  1493. // LocalPath returns where attachment is stored in local file system.
  1494. func (attach *Attachment) LocalPath() string {
  1495. return AttachmentLocalPath(attach.UUID)
  1496. }
  1497. // NewAttachment creates a new attachment object.
  1498. func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) {
  1499. attach := &Attachment{
  1500. UUID: gouuid.NewV4().String(),
  1501. Name: name,
  1502. }
  1503. if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil {
  1504. return nil, fmt.Errorf("MkdirAll: %v", err)
  1505. }
  1506. fw, err := os.Create(attach.LocalPath())
  1507. if err != nil {
  1508. return nil, fmt.Errorf("Create: %v", err)
  1509. }
  1510. defer fw.Close()
  1511. if _, err = fw.Write(buf); err != nil {
  1512. return nil, fmt.Errorf("Write: %v", err)
  1513. } else if _, err = io.Copy(fw, file); err != nil {
  1514. return nil, fmt.Errorf("Copy: %v", err)
  1515. }
  1516. sess := x.NewSession()
  1517. defer sessionRelease(sess)
  1518. if err := sess.Begin(); err != nil {
  1519. return nil, err
  1520. }
  1521. if _, err := sess.Insert(attach); err != nil {
  1522. return nil, err
  1523. }
  1524. return attach, sess.Commit()
  1525. }
  1526. func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
  1527. attach := &Attachment{UUID: uuid}
  1528. has, err := x.Get(attach)
  1529. if err != nil {
  1530. return nil, err
  1531. } else if !has {
  1532. return nil, ErrAttachmentNotExist{0, uuid}
  1533. }
  1534. return attach, nil
  1535. }
  1536. // GetAttachmentByUUID returns attachment by given UUID.
  1537. func GetAttachmentByUUID(uuid string) (*Attachment, error) {
  1538. return getAttachmentByUUID(x, uuid)
  1539. }
  1540. // GetAttachmentsByIssueID returns all attachments for given issue by ID.
  1541. func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) {
  1542. attachments := make([]*Attachment, 0, 10)
  1543. return attachments, x.Where("issue_id=? AND comment_id=0", issueID).Find(&attachments)
  1544. }
  1545. // GetAttachmentsByCommentID returns all attachments if comment by given ID.
  1546. func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
  1547. attachments := make([]*Attachment, 0, 10)
  1548. return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
  1549. }
  1550. // DeleteAttachment deletes the given attachment and optionally the associated file.
  1551. func DeleteAttachment(a *Attachment, remove bool) error {
  1552. _, err := DeleteAttachments([]*Attachment{a}, remove)
  1553. return err
  1554. }
  1555. // DeleteAttachments deletes the given attachments and optionally the associated files.
  1556. func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
  1557. for i, a := range attachments {
  1558. if remove {
  1559. if err := os.Remove(a.LocalPath()); err != nil {
  1560. return i, err
  1561. }
  1562. }
  1563. if _, err := x.Delete(a.ID); err != nil {
  1564. return i, err
  1565. }
  1566. }
  1567. return len(attachments), nil
  1568. }
  1569. // DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
  1570. func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
  1571. attachments, err := GetAttachmentsByIssueID(issueId)
  1572. if err != nil {
  1573. return 0, err
  1574. }
  1575. return DeleteAttachments(attachments, remove)
  1576. }
  1577. // DeleteAttachmentsByComment deletes all attachments associated with the given comment.
  1578. func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
  1579. attachments, err := GetAttachmentsByCommentID(commentId)
  1580. if err != nil {
  1581. return 0, err
  1582. }
  1583. return DeleteAttachments(attachments, remove)
  1584. }