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 37 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
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
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419
  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. "io"
  10. "mime/multipart"
  11. "os"
  12. "path"
  13. "strings"
  14. "time"
  15. "github.com/Unknwon/com"
  16. "github.com/go-xorm/xorm"
  17. gouuid "github.com/satori/go.uuid"
  18. "github.com/gogits/gogs/modules/base"
  19. "github.com/gogits/gogs/modules/log"
  20. "github.com/gogits/gogs/modules/setting"
  21. )
  22. var (
  23. ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone")
  24. ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue")
  25. ErrMissingIssueNumber = errors.New("No issue number specified")
  26. )
  27. // Issue represents an issue or pull request of repository.
  28. type Issue struct {
  29. ID int64 `xorm:"pk autoincr"`
  30. RepoID int64 `xorm:"INDEX"`
  31. Index int64 // Index in one repository.
  32. Name string
  33. Repo *Repository `xorm:"-"`
  34. PosterID int64
  35. Poster *User `xorm:"-"`
  36. Labels []*Label `xorm:"-"`
  37. MilestoneID int64
  38. Milestone *Milestone `xorm:"-"`
  39. AssigneeID int64
  40. Assignee *User `xorm:"-"`
  41. IsRead bool `xorm:"-"`
  42. IsPull bool // Indicates whether is a pull request or not.
  43. *PullRequest `xorm:"-"`
  44. IsClosed bool
  45. Content string `xorm:"TEXT"`
  46. RenderedContent string `xorm:"-"`
  47. Priority int
  48. NumComments int
  49. Deadline time.Time `xorm:"-"`
  50. DeadlineUnix int64
  51. Created time.Time `xorm:"-"`
  52. CreatedUnix int64
  53. Updated time.Time `xorm:"-"`
  54. UpdatedUnix int64
  55. Attachments []*Attachment `xorm:"-"`
  56. Comments []*Comment `xorm:"-"`
  57. }
  58. func (i *Issue) BeforeInsert() {
  59. i.CreatedUnix = time.Now().UTC().Unix()
  60. i.UpdatedUnix = i.CreatedUnix
  61. }
  62. func (i *Issue) BeforeUpdate() {
  63. i.UpdatedUnix = time.Now().UTC().Unix()
  64. i.DeadlineUnix = i.Deadline.UTC().Unix()
  65. }
  66. func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
  67. var err error
  68. switch colName {
  69. case "id":
  70. i.Attachments, err = GetAttachmentsByIssueID(i.ID)
  71. if err != nil {
  72. log.Error(3, "GetAttachmentsByIssueID[%d]: %v", i.ID, err)
  73. }
  74. i.Comments, err = GetCommentsByIssueID(i.ID)
  75. if err != nil {
  76. log.Error(3, "GetCommentsByIssueID[%d]: %v", i.ID, err)
  77. }
  78. case "milestone_id":
  79. if i.MilestoneID == 0 {
  80. return
  81. }
  82. i.Milestone, err = GetMilestoneByID(i.MilestoneID)
  83. if err != nil {
  84. log.Error(3, "GetMilestoneById[%d]: %v", i.ID, err)
  85. }
  86. case "assignee_id":
  87. if i.AssigneeID == 0 {
  88. return
  89. }
  90. i.Assignee, err = GetUserByID(i.AssigneeID)
  91. if err != nil {
  92. log.Error(3, "GetUserByID[%d]: %v", i.ID, err)
  93. }
  94. case "deadline_unix":
  95. i.Deadline = time.Unix(i.DeadlineUnix, 0).Local()
  96. case "created_unix":
  97. i.Created = time.Unix(i.CreatedUnix, 0).Local()
  98. case "updated_unix":
  99. i.Updated = time.Unix(i.UpdatedUnix, 0).Local()
  100. }
  101. }
  102. // HashTag returns unique hash tag for issue.
  103. func (i *Issue) HashTag() string {
  104. return "issue-" + com.ToStr(i.ID)
  105. }
  106. // IsPoster returns true if given user by ID is the poster.
  107. func (i *Issue) IsPoster(uid int64) bool {
  108. return i.PosterID == uid
  109. }
  110. func (i *Issue) GetPoster() (err error) {
  111. i.Poster, err = GetUserByID(i.PosterID)
  112. if IsErrUserNotExist(err) {
  113. i.PosterID = -1
  114. i.Poster = NewFakeUser()
  115. return nil
  116. }
  117. return err
  118. }
  119. func (i *Issue) hasLabel(e Engine, labelID int64) bool {
  120. return hasIssueLabel(e, i.ID, labelID)
  121. }
  122. // HasLabel returns true if issue has been labeled by given ID.
  123. func (i *Issue) HasLabel(labelID int64) bool {
  124. return i.hasLabel(x, labelID)
  125. }
  126. func (i *Issue) addLabel(e *xorm.Session, label *Label) error {
  127. return newIssueLabel(e, i, label)
  128. }
  129. // AddLabel adds new label to issue by given ID.
  130. func (i *Issue) AddLabel(label *Label) (err error) {
  131. sess := x.NewSession()
  132. defer sessionRelease(sess)
  133. if err = sess.Begin(); err != nil {
  134. return err
  135. }
  136. if err = i.addLabel(sess, label); err != nil {
  137. return err
  138. }
  139. return sess.Commit()
  140. }
  141. func (i *Issue) getLabels(e Engine) (err error) {
  142. if len(i.Labels) > 0 {
  143. return nil
  144. }
  145. i.Labels, err = getLabelsByIssueID(e, i.ID)
  146. if err != nil {
  147. return fmt.Errorf("getLabelsByIssueID: %v", err)
  148. }
  149. return nil
  150. }
  151. // GetLabels retrieves all labels of issue and assign to corresponding field.
  152. func (i *Issue) GetLabels() error {
  153. return i.getLabels(x)
  154. }
  155. func (i *Issue) removeLabel(e *xorm.Session, label *Label) error {
  156. return deleteIssueLabel(e, i, label)
  157. }
  158. // RemoveLabel removes a label from issue by given ID.
  159. func (i *Issue) RemoveLabel(label *Label) (err error) {
  160. sess := x.NewSession()
  161. defer sessionRelease(sess)
  162. if err = sess.Begin(); err != nil {
  163. return err
  164. }
  165. if err = i.removeLabel(sess, label); err != nil {
  166. return err
  167. }
  168. return sess.Commit()
  169. }
  170. func (i *Issue) ClearLabels() (err error) {
  171. sess := x.NewSession()
  172. defer sessionRelease(sess)
  173. if err = sess.Begin(); err != nil {
  174. return err
  175. }
  176. if err = i.getLabels(sess); err != nil {
  177. return err
  178. }
  179. for idx := range i.Labels {
  180. if err = i.removeLabel(sess, i.Labels[idx]); err != nil {
  181. return err
  182. }
  183. }
  184. return sess.Commit()
  185. }
  186. func (i *Issue) GetAssignee() (err error) {
  187. if i.AssigneeID == 0 || i.Assignee != nil {
  188. return nil
  189. }
  190. i.Assignee, err = GetUserByID(i.AssigneeID)
  191. if IsErrUserNotExist(err) {
  192. return nil
  193. }
  194. return err
  195. }
  196. // ReadBy sets issue to be read by given user.
  197. func (i *Issue) ReadBy(uid int64) error {
  198. return UpdateIssueUserByRead(uid, i.ID)
  199. }
  200. func (i *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isClosed bool) (err error) {
  201. // Nothing should be performed if current status is same as target status
  202. if i.IsClosed == isClosed {
  203. return nil
  204. }
  205. i.IsClosed = isClosed
  206. if err = updateIssueCols(e, i, "is_closed"); err != nil {
  207. return err
  208. } else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil {
  209. return err
  210. }
  211. // Update issue count of labels
  212. if err = i.getLabels(e); err != nil {
  213. return err
  214. }
  215. for idx := range i.Labels {
  216. if i.IsClosed {
  217. i.Labels[idx].NumClosedIssues++
  218. } else {
  219. i.Labels[idx].NumClosedIssues--
  220. }
  221. if err = updateLabel(e, i.Labels[idx]); err != nil {
  222. return err
  223. }
  224. }
  225. // Update issue count of milestone
  226. if err = changeMilestoneIssueStats(e, i); err != nil {
  227. return err
  228. }
  229. // New action comment
  230. if _, err = createStatusComment(e, doer, repo, i); err != nil {
  231. return err
  232. }
  233. return nil
  234. }
  235. // ChangeStatus changes issue status to open or closed.
  236. func (i *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) {
  237. sess := x.NewSession()
  238. defer sessionRelease(sess)
  239. if err = sess.Begin(); err != nil {
  240. return err
  241. }
  242. if err = i.changeStatus(sess, doer, repo, isClosed); err != nil {
  243. return err
  244. }
  245. return sess.Commit()
  246. }
  247. func (i *Issue) GetPullRequest() (err error) {
  248. if i.PullRequest != nil {
  249. return nil
  250. }
  251. i.PullRequest, err = GetPullRequestByIssueID(i.ID)
  252. return err
  253. }
  254. // It's caller's responsibility to create action.
  255. func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) {
  256. if _, err = e.Insert(issue); err != nil {
  257. return err
  258. }
  259. if isPull {
  260. _, err = e.Exec("UPDATE `repository` SET num_pulls=num_pulls+1 WHERE id=?", issue.RepoID)
  261. } else {
  262. _, err = e.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID)
  263. }
  264. if err != nil {
  265. return err
  266. }
  267. if len(labelIDs) > 0 {
  268. // During the session, SQLite3 dirver cannot handle retrieve objects after update something.
  269. // So we have to get all needed labels first.
  270. labels := make([]*Label, 0, len(labelIDs))
  271. if err = e.In("id", labelIDs).Find(&labels); err != nil {
  272. return fmt.Errorf("find all labels: %v", err)
  273. }
  274. for _, label := range labels {
  275. if err = issue.addLabel(e, label); err != nil {
  276. return fmt.Errorf("addLabel: %v", err)
  277. }
  278. }
  279. }
  280. if issue.MilestoneID > 0 {
  281. if err = changeMilestoneAssign(e, 0, issue); err != nil {
  282. return err
  283. }
  284. }
  285. if err = newIssueUsers(e, repo, issue); err != nil {
  286. return err
  287. }
  288. // Check attachments.
  289. attachments := make([]*Attachment, 0, len(uuids))
  290. for _, uuid := range uuids {
  291. attach, err := getAttachmentByUUID(e, uuid)
  292. if err != nil {
  293. if IsErrAttachmentNotExist(err) {
  294. continue
  295. }
  296. return fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
  297. }
  298. attachments = append(attachments, attach)
  299. }
  300. for i := range attachments {
  301. attachments[i].IssueID = issue.ID
  302. // No assign value could be 0, so ignore AllCols().
  303. if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  304. return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
  305. }
  306. }
  307. return nil
  308. }
  309. // NewIssue creates new issue with labels for repository.
  310. func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
  311. sess := x.NewSession()
  312. defer sessionRelease(sess)
  313. if err = sess.Begin(); err != nil {
  314. return err
  315. }
  316. if err = newIssue(sess, repo, issue, labelIDs, uuids, false); err != nil {
  317. return fmt.Errorf("newIssue: %v", err)
  318. }
  319. // Notify watchers.
  320. act := &Action{
  321. ActUserID: issue.Poster.Id,
  322. ActUserName: issue.Poster.Name,
  323. ActEmail: issue.Poster.Email,
  324. OpType: ACTION_CREATE_ISSUE,
  325. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
  326. RepoID: repo.ID,
  327. RepoUserName: repo.Owner.Name,
  328. RepoName: repo.Name,
  329. IsPrivate: repo.IsPrivate,
  330. }
  331. if err = notifyWatchers(sess, act); err != nil {
  332. return err
  333. }
  334. return sess.Commit()
  335. }
  336. // GetIssueByRef returns an Issue specified by a GFM reference.
  337. // See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
  338. func GetIssueByRef(ref string) (*Issue, error) {
  339. n := strings.IndexByte(ref, byte('#'))
  340. if n == -1 {
  341. return nil, ErrMissingIssueNumber
  342. }
  343. index, err := com.StrTo(ref[n+1:]).Int64()
  344. if err != nil {
  345. return nil, err
  346. }
  347. repo, err := GetRepositoryByRef(ref[:n])
  348. if err != nil {
  349. return nil, err
  350. }
  351. issue, err := GetIssueByIndex(repo.ID, index)
  352. if err != nil {
  353. return nil, err
  354. }
  355. issue.Repo = repo
  356. return issue, nil
  357. }
  358. // GetIssueByIndex returns issue by given index in repository.
  359. func GetIssueByIndex(repoID, index int64) (*Issue, error) {
  360. issue := &Issue{
  361. RepoID: repoID,
  362. Index: index,
  363. }
  364. has, err := x.Get(issue)
  365. if err != nil {
  366. return nil, err
  367. } else if !has {
  368. return nil, ErrIssueNotExist{0, repoID, index}
  369. }
  370. return issue, nil
  371. }
  372. // GetIssueByID returns an issue by given ID.
  373. func GetIssueByID(id int64) (*Issue, error) {
  374. issue := new(Issue)
  375. has, err := x.Id(id).Get(issue)
  376. if err != nil {
  377. return nil, err
  378. } else if !has {
  379. return nil, ErrIssueNotExist{id, 0, 0}
  380. }
  381. return issue, nil
  382. }
  383. type IssuesOptions struct {
  384. UserID int64
  385. AssigneeID int64
  386. RepoID int64
  387. PosterID int64
  388. MilestoneID int64
  389. RepoIDs []int64
  390. Page int
  391. IsClosed bool
  392. IsMention bool
  393. IsPull bool
  394. Labels string
  395. SortType string
  396. }
  397. // Issues returns a list of issues by given conditions.
  398. func Issues(opts *IssuesOptions) ([]*Issue, error) {
  399. sess := x.Limit(setting.IssuePagingNum, (opts.Page-1)*setting.IssuePagingNum)
  400. if opts.RepoID > 0 {
  401. sess.Where("issue.repo_id=?", opts.RepoID).And("issue.is_closed=?", opts.IsClosed)
  402. } else if opts.RepoIDs != nil {
  403. // In case repository IDs are provided but actually no repository has issue.
  404. if len(opts.RepoIDs) == 0 {
  405. return make([]*Issue, 0), nil
  406. }
  407. sess.Where("issue.repo_id IN ("+strings.Join(base.Int64sToStrings(opts.RepoIDs), ",")+")").And("issue.is_closed=?", opts.IsClosed)
  408. } else {
  409. sess.Where("issue.is_closed=?", opts.IsClosed)
  410. }
  411. if opts.AssigneeID > 0 {
  412. sess.And("issue.assignee_id=?", opts.AssigneeID)
  413. } else if opts.PosterID > 0 {
  414. sess.And("issue.poster_id=?", opts.PosterID)
  415. }
  416. if opts.MilestoneID > 0 {
  417. sess.And("issue.milestone_id=?", opts.MilestoneID)
  418. }
  419. sess.And("issue.is_pull=?", opts.IsPull)
  420. switch opts.SortType {
  421. case "oldest":
  422. sess.Asc("created_unix")
  423. case "recentupdate":
  424. sess.Desc("updated_unix")
  425. case "leastupdate":
  426. sess.Asc("updated_unix")
  427. case "mostcomment":
  428. sess.Desc("num_comments")
  429. case "leastcomment":
  430. sess.Asc("num_comments")
  431. case "priority":
  432. sess.Desc("priority")
  433. default:
  434. sess.Desc("created_unix")
  435. }
  436. labelIDs := base.StringsToInt64s(strings.Split(opts.Labels, ","))
  437. if len(labelIDs) > 0 {
  438. validJoin := false
  439. queryStr := "issue.id=issue_label.issue_id"
  440. for _, id := range labelIDs {
  441. if id == 0 {
  442. continue
  443. }
  444. validJoin = true
  445. queryStr += " AND issue_label.label_id=" + com.ToStr(id)
  446. }
  447. if validJoin {
  448. sess.Join("INNER", "issue_label", queryStr)
  449. }
  450. }
  451. if opts.IsMention {
  452. queryStr := "issue.id=issue_user.issue_id AND issue_user.is_mentioned=1"
  453. if opts.UserID > 0 {
  454. queryStr += " AND issue_user.uid=" + com.ToStr(opts.UserID)
  455. }
  456. sess.Join("INNER", "issue_user", queryStr)
  457. }
  458. issues := make([]*Issue, 0, setting.IssuePagingNum)
  459. return issues, sess.Find(&issues)
  460. }
  461. type IssueStatus int
  462. const (
  463. IS_OPEN = iota + 1
  464. IS_CLOSE
  465. )
  466. // GetIssueCountByPoster returns number of issues of repository by poster.
  467. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 {
  468. count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue))
  469. return count
  470. }
  471. // .___ ____ ___
  472. // | | ______ ________ __ ____ | | \______ ___________
  473. // | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
  474. // | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
  475. // |___/____ >____ >____/ \___ >______//____ >\___ >__|
  476. // \/ \/ \/ \/ \/
  477. // IssueUser represents an issue-user relation.
  478. type IssueUser struct {
  479. ID int64 `xorm:"pk autoincr"`
  480. UID int64 `xorm:"INDEX"` // User ID.
  481. IssueID int64
  482. RepoID int64 `xorm:"INDEX"`
  483. MilestoneID int64
  484. IsRead bool
  485. IsAssigned bool
  486. IsMentioned bool
  487. IsPoster bool
  488. IsClosed bool
  489. }
  490. func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
  491. users, err := repo.GetAssignees()
  492. if err != nil {
  493. return err
  494. }
  495. iu := &IssueUser{
  496. IssueID: issue.ID,
  497. RepoID: repo.ID,
  498. }
  499. // Poster can be anyone.
  500. isNeedAddPoster := true
  501. for _, u := range users {
  502. iu.ID = 0
  503. iu.UID = u.Id
  504. iu.IsPoster = iu.UID == issue.PosterID
  505. if isNeedAddPoster && iu.IsPoster {
  506. isNeedAddPoster = false
  507. }
  508. iu.IsAssigned = iu.UID == issue.AssigneeID
  509. if _, err = e.Insert(iu); err != nil {
  510. return err
  511. }
  512. }
  513. if isNeedAddPoster {
  514. iu.ID = 0
  515. iu.UID = issue.PosterID
  516. iu.IsPoster = true
  517. if _, err = e.Insert(iu); err != nil {
  518. return err
  519. }
  520. }
  521. return nil
  522. }
  523. // NewIssueUsers adds new issue-user relations for new issue of repository.
  524. func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
  525. sess := x.NewSession()
  526. defer sessionRelease(sess)
  527. if err = sess.Begin(); err != nil {
  528. return err
  529. }
  530. if err = newIssueUsers(sess, repo, issue); err != nil {
  531. return err
  532. }
  533. return sess.Commit()
  534. }
  535. // PairsContains returns true when pairs list contains given issue.
  536. func PairsContains(ius []*IssueUser, issueId, uid int64) int {
  537. for i := range ius {
  538. if ius[i].IssueID == issueId &&
  539. ius[i].UID == uid {
  540. return i
  541. }
  542. }
  543. return -1
  544. }
  545. // GetIssueUsers returns issue-user pairs by given repository and user.
  546. func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
  547. ius := make([]*IssueUser, 0, 10)
  548. err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
  549. return ius, err
  550. }
  551. // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
  552. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
  553. if len(rids) == 0 {
  554. return []*IssueUser{}, nil
  555. }
  556. buf := bytes.NewBufferString("")
  557. for _, rid := range rids {
  558. buf.WriteString("repo_id=")
  559. buf.WriteString(com.ToStr(rid))
  560. buf.WriteString(" OR ")
  561. }
  562. cond := strings.TrimSuffix(buf.String(), " OR ")
  563. ius := make([]*IssueUser, 0, 10)
  564. sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed)
  565. if len(cond) > 0 {
  566. sess.And(cond)
  567. }
  568. err := sess.Find(&ius)
  569. return ius, err
  570. }
  571. // GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
  572. func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int) ([]*IssueUser, error) {
  573. ius := make([]*IssueUser, 0, 10)
  574. sess := x.Limit(20, (page-1)*20).Where("uid=?", uid).And("is_closed=?", isClosed)
  575. if rid > 0 {
  576. sess.And("repo_id=?", rid)
  577. }
  578. switch filterMode {
  579. case FM_ASSIGN:
  580. sess.And("is_assigned=?", true)
  581. case FM_CREATE:
  582. sess.And("is_poster=?", true)
  583. default:
  584. return ius, nil
  585. }
  586. err := sess.Find(&ius)
  587. return ius, err
  588. }
  589. func UpdateMentions(userNames []string, issueId int64) error {
  590. for i := range userNames {
  591. userNames[i] = strings.ToLower(userNames[i])
  592. }
  593. users := make([]*User, 0, len(userNames))
  594. if err := x.Where("lower_name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("lower_name ASC").Find(&users); err != nil {
  595. return err
  596. }
  597. ids := make([]int64, 0, len(userNames))
  598. for _, user := range users {
  599. ids = append(ids, user.Id)
  600. if !user.IsOrganization() {
  601. continue
  602. }
  603. if user.NumMembers == 0 {
  604. continue
  605. }
  606. tempIds := make([]int64, 0, user.NumMembers)
  607. orgUsers, err := GetOrgUsersByOrgId(user.Id)
  608. if err != nil {
  609. return err
  610. }
  611. for _, orgUser := range orgUsers {
  612. tempIds = append(tempIds, orgUser.ID)
  613. }
  614. ids = append(ids, tempIds...)
  615. }
  616. if err := UpdateIssueUsersByMentions(ids, issueId); err != nil {
  617. return err
  618. }
  619. return nil
  620. }
  621. // IssueStats represents issue statistic information.
  622. type IssueStats struct {
  623. OpenCount, ClosedCount int64
  624. AllCount int64
  625. AssignCount int64
  626. CreateCount int64
  627. MentionCount int64
  628. }
  629. // Filter modes.
  630. const (
  631. FM_ALL = iota
  632. FM_ASSIGN
  633. FM_CREATE
  634. FM_MENTION
  635. )
  636. func parseCountResult(results []map[string][]byte) int64 {
  637. if len(results) == 0 {
  638. return 0
  639. }
  640. for _, result := range results[0] {
  641. return com.StrTo(string(result)).MustInt64()
  642. }
  643. return 0
  644. }
  645. type IssueStatsOptions struct {
  646. RepoID int64
  647. UserID int64
  648. LabelID int64
  649. MilestoneID int64
  650. AssigneeID int64
  651. FilterMode int
  652. IsPull bool
  653. }
  654. // GetIssueStats returns issue statistic information by given conditions.
  655. func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
  656. stats := &IssueStats{}
  657. queryStr := "SELECT COUNT(*) FROM `issue` "
  658. if opts.LabelID > 0 {
  659. queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(opts.LabelID)
  660. }
  661. baseCond := " WHERE issue.repo_id=" + com.ToStr(opts.RepoID) + " AND issue.is_closed=?"
  662. if opts.MilestoneID > 0 {
  663. baseCond += " AND issue.milestone_id=" + com.ToStr(opts.MilestoneID)
  664. }
  665. if opts.AssigneeID > 0 {
  666. baseCond += " AND assignee_id=" + com.ToStr(opts.AssigneeID)
  667. }
  668. baseCond += " AND issue.is_pull=?"
  669. switch opts.FilterMode {
  670. case FM_ALL, FM_ASSIGN:
  671. results, _ := x.Query(queryStr+baseCond, false, opts.IsPull)
  672. stats.OpenCount = parseCountResult(results)
  673. results, _ = x.Query(queryStr+baseCond, true, opts.IsPull)
  674. stats.ClosedCount = parseCountResult(results)
  675. case FM_CREATE:
  676. baseCond += " AND poster_id=?"
  677. results, _ := x.Query(queryStr+baseCond, false, opts.IsPull, opts.UserID)
  678. stats.OpenCount = parseCountResult(results)
  679. results, _ = x.Query(queryStr+baseCond, true, opts.IsPull, opts.UserID)
  680. stats.ClosedCount = parseCountResult(results)
  681. case FM_MENTION:
  682. queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id"
  683. baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?"
  684. results, _ := x.Query(queryStr+baseCond, false, opts.IsPull, opts.UserID, true)
  685. stats.OpenCount = parseCountResult(results)
  686. results, _ = x.Query(queryStr+baseCond, true, opts.IsPull, opts.UserID, true)
  687. stats.ClosedCount = parseCountResult(results)
  688. }
  689. return stats
  690. }
  691. // GetUserIssueStats returns issue statistic information for dashboard by given conditions.
  692. func GetUserIssueStats(repoID, uid int64, repoIDs []int64, filterMode int, isPull bool) *IssueStats {
  693. stats := &IssueStats{}
  694. queryStr := "SELECT COUNT(*) FROM `issue` "
  695. baseCond := " WHERE issue.is_closed=?"
  696. if repoID > 0 || len(repoIDs) == 0 {
  697. baseCond += " AND issue.repo_id=" + com.ToStr(repoID)
  698. } else {
  699. baseCond += " AND issue.repo_id IN (" + strings.Join(base.Int64sToStrings(repoIDs), ",") + ")"
  700. }
  701. if isPull {
  702. baseCond += " AND issue.is_pull=1"
  703. } else {
  704. baseCond += " AND issue.is_pull=0"
  705. }
  706. results, _ := x.Query(queryStr+baseCond+" AND assignee_id=?", false, uid)
  707. stats.AssignCount = parseCountResult(results)
  708. results, _ = x.Query(queryStr+baseCond+" AND poster_id=?", false, uid)
  709. stats.CreateCount = parseCountResult(results)
  710. switch filterMode {
  711. case FM_ASSIGN:
  712. baseCond += " AND assignee_id=" + com.ToStr(uid)
  713. case FM_CREATE:
  714. baseCond += " AND poster_id=" + com.ToStr(uid)
  715. }
  716. results, _ = x.Query(queryStr+baseCond, false)
  717. stats.OpenCount = parseCountResult(results)
  718. results, _ = x.Query(queryStr+baseCond, true)
  719. stats.ClosedCount = parseCountResult(results)
  720. return stats
  721. }
  722. // GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
  723. func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen int64, numClosed int64) {
  724. queryStr := "SELECT COUNT(*) FROM `issue` "
  725. baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?"
  726. if isPull {
  727. baseCond += " AND issue.is_pull=1"
  728. } else {
  729. baseCond += " AND issue.is_pull=0"
  730. }
  731. switch filterMode {
  732. case FM_ASSIGN:
  733. baseCond += " AND assignee_id=" + com.ToStr(uid)
  734. case FM_CREATE:
  735. baseCond += " AND poster_id=" + com.ToStr(uid)
  736. }
  737. results, _ := x.Query(queryStr+baseCond, repoID, false)
  738. numOpen = parseCountResult(results)
  739. results, _ = x.Query(queryStr+baseCond, repoID, true)
  740. numClosed = parseCountResult(results)
  741. return numOpen, numClosed
  742. }
  743. func updateIssue(e Engine, issue *Issue) error {
  744. _, err := e.Id(issue.ID).AllCols().Update(issue)
  745. return err
  746. }
  747. // UpdateIssue updates all fields of given issue.
  748. func UpdateIssue(issue *Issue) error {
  749. return updateIssue(x, issue)
  750. }
  751. // updateIssueCols only updates values of specific columns for given issue.
  752. func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
  753. _, err := e.Id(issue.ID).Cols(cols...).Update(issue)
  754. return err
  755. }
  756. func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error {
  757. _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID)
  758. return err
  759. }
  760. // UpdateIssueUsersByStatus updates issue-user relations by issue status.
  761. func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error {
  762. return updateIssueUsersByStatus(x, issueID, isClosed)
  763. }
  764. func updateIssueUserByAssignee(e *xorm.Session, issue *Issue) (err error) {
  765. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issue.ID); err != nil {
  766. return err
  767. }
  768. // Assignee ID equals to 0 means clear assignee.
  769. if issue.AssigneeID > 0 {
  770. if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, issue.AssigneeID, issue.ID); err != nil {
  771. return err
  772. }
  773. }
  774. return updateIssue(e, issue)
  775. }
  776. // UpdateIssueUserByAssignee updates issue-user relation for assignee.
  777. func UpdateIssueUserByAssignee(issue *Issue) (err error) {
  778. sess := x.NewSession()
  779. defer sessionRelease(sess)
  780. if err = sess.Begin(); err != nil {
  781. return err
  782. }
  783. if err = updateIssueUserByAssignee(sess, issue); err != nil {
  784. return err
  785. }
  786. return sess.Commit()
  787. }
  788. // UpdateIssueUserByRead updates issue-user relation for reading.
  789. func UpdateIssueUserByRead(uid, issueID int64) error {
  790. _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
  791. return err
  792. }
  793. // UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
  794. func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
  795. for _, uid := range uids {
  796. iu := &IssueUser{UID: uid, IssueID: iid}
  797. has, err := x.Get(iu)
  798. if err != nil {
  799. return err
  800. }
  801. iu.IsMentioned = true
  802. if has {
  803. _, err = x.Id(iu.ID).AllCols().Update(iu)
  804. } else {
  805. _, err = x.Insert(iu)
  806. }
  807. if err != nil {
  808. return err
  809. }
  810. }
  811. return nil
  812. }
  813. // _____ .__.__ __
  814. // / \ |__| | ____ _______/ |_ ____ ____ ____
  815. // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
  816. // / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
  817. // \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
  818. // \/ \/ \/ \/ \/
  819. // Milestone represents a milestone of repository.
  820. type Milestone struct {
  821. ID int64 `xorm:"pk autoincr"`
  822. RepoID int64 `xorm:"INDEX"`
  823. Name string
  824. Content string `xorm:"TEXT"`
  825. RenderedContent string `xorm:"-"`
  826. IsClosed bool
  827. NumIssues int
  828. NumClosedIssues int
  829. NumOpenIssues int `xorm:"-"`
  830. Completeness int // Percentage(1-100).
  831. IsOverDue bool `xorm:"-"`
  832. DeadlineString string `xorm:"-"`
  833. Deadline time.Time `xorm:"-"`
  834. DeadlineUnix int64
  835. ClosedDate time.Time `xorm:"-"`
  836. ClosedDateUnix int64
  837. }
  838. func (m *Milestone) BeforeInsert() {
  839. m.DeadlineUnix = m.Deadline.UTC().Unix()
  840. }
  841. func (m *Milestone) BeforeUpdate() {
  842. if m.NumIssues > 0 {
  843. m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
  844. } else {
  845. m.Completeness = 0
  846. }
  847. m.DeadlineUnix = m.Deadline.UTC().Unix()
  848. m.ClosedDateUnix = m.ClosedDate.UTC().Unix()
  849. }
  850. func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
  851. switch colName {
  852. case "deadline_unix":
  853. m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
  854. if m.Deadline.Year() == 9999 {
  855. return
  856. }
  857. m.DeadlineString = m.Deadline.Format("2006-01-02")
  858. if time.Now().Local().After(m.Deadline) {
  859. m.IsOverDue = true
  860. }
  861. case "closed_date_unix":
  862. m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
  863. }
  864. }
  865. // CalOpenIssues calculates the open issues of milestone.
  866. func (m *Milestone) CalOpenIssues() {
  867. m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
  868. }
  869. // NewMilestone creates new milestone of repository.
  870. func NewMilestone(m *Milestone) (err error) {
  871. sess := x.NewSession()
  872. defer sessionRelease(sess)
  873. if err = sess.Begin(); err != nil {
  874. return err
  875. }
  876. if _, err = sess.Insert(m); err != nil {
  877. return err
  878. }
  879. if _, err = sess.Exec("UPDATE `repository` SET num_milestones=num_milestones+1 WHERE id=?", m.RepoID); err != nil {
  880. return err
  881. }
  882. return sess.Commit()
  883. }
  884. func getMilestoneByID(e Engine, id int64) (*Milestone, error) {
  885. m := &Milestone{ID: id}
  886. has, err := e.Get(m)
  887. if err != nil {
  888. return nil, err
  889. } else if !has {
  890. return nil, ErrMilestoneNotExist{id, 0}
  891. }
  892. return m, nil
  893. }
  894. // GetMilestoneByID returns the milestone of given ID.
  895. func GetMilestoneByID(id int64) (*Milestone, error) {
  896. return getMilestoneByID(x, id)
  897. }
  898. // GetRepoMilestoneByID returns the milestone of given ID and repository.
  899. func GetRepoMilestoneByID(repoID, milestoneID int64) (*Milestone, error) {
  900. m := &Milestone{ID: milestoneID, RepoID: repoID}
  901. has, err := x.Get(m)
  902. if err != nil {
  903. return nil, err
  904. } else if !has {
  905. return nil, ErrMilestoneNotExist{milestoneID, repoID}
  906. }
  907. return m, nil
  908. }
  909. // GetAllRepoMilestones returns all milestones of given repository.
  910. func GetAllRepoMilestones(repoID int64) ([]*Milestone, error) {
  911. miles := make([]*Milestone, 0, 10)
  912. return miles, x.Where("repo_id=?", repoID).Find(&miles)
  913. }
  914. // GetMilestones returns a list of milestones of given repository and status.
  915. func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
  916. miles := make([]*Milestone, 0, setting.IssuePagingNum)
  917. sess := x.Where("repo_id=? AND is_closed=?", repoID, isClosed)
  918. if page > 0 {
  919. sess = sess.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum)
  920. }
  921. return miles, sess.Find(&miles)
  922. }
  923. func updateMilestone(e Engine, m *Milestone) error {
  924. _, err := e.Id(m.ID).AllCols().Update(m)
  925. return err
  926. }
  927. // UpdateMilestone updates information of given milestone.
  928. func UpdateMilestone(m *Milestone) error {
  929. return updateMilestone(x, m)
  930. }
  931. func countRepoMilestones(e Engine, repoID int64) int64 {
  932. count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone))
  933. return count
  934. }
  935. // CountRepoMilestones returns number of milestones in given repository.
  936. func CountRepoMilestones(repoID int64) int64 {
  937. return countRepoMilestones(x, repoID)
  938. }
  939. func countRepoClosedMilestones(e Engine, repoID int64) int64 {
  940. closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone))
  941. return closed
  942. }
  943. // CountRepoClosedMilestones returns number of closed milestones in given repository.
  944. func CountRepoClosedMilestones(repoID int64) int64 {
  945. return countRepoClosedMilestones(x, repoID)
  946. }
  947. // MilestoneStats returns number of open and closed milestones of given repository.
  948. func MilestoneStats(repoID int64) (open int64, closed int64) {
  949. open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone))
  950. return open, CountRepoClosedMilestones(repoID)
  951. }
  952. // ChangeMilestoneStatus changes the milestone open/closed status.
  953. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
  954. repo, err := GetRepositoryByID(m.RepoID)
  955. if err != nil {
  956. return err
  957. }
  958. sess := x.NewSession()
  959. defer sessionRelease(sess)
  960. if err = sess.Begin(); err != nil {
  961. return err
  962. }
  963. m.IsClosed = isClosed
  964. if err = updateMilestone(sess, m); err != nil {
  965. return err
  966. }
  967. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  968. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  969. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  970. return err
  971. }
  972. return sess.Commit()
  973. }
  974. func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
  975. if issue.MilestoneID == 0 {
  976. return nil
  977. }
  978. m, err := getMilestoneByID(e, issue.MilestoneID)
  979. if err != nil {
  980. return err
  981. }
  982. if issue.IsClosed {
  983. m.NumOpenIssues--
  984. m.NumClosedIssues++
  985. } else {
  986. m.NumOpenIssues++
  987. m.NumClosedIssues--
  988. }
  989. return updateMilestone(e, m)
  990. }
  991. // ChangeMilestoneIssueStats updates the open/closed issues counter and progress
  992. // for the milestone associated with the given issue.
  993. func ChangeMilestoneIssueStats(issue *Issue) (err error) {
  994. sess := x.NewSession()
  995. defer sessionRelease(sess)
  996. if err = sess.Begin(); err != nil {
  997. return err
  998. }
  999. if err = changeMilestoneIssueStats(sess, issue); err != nil {
  1000. return err
  1001. }
  1002. return sess.Commit()
  1003. }
  1004. func changeMilestoneAssign(e *xorm.Session, oldMid int64, issue *Issue) error {
  1005. if oldMid > 0 {
  1006. m, err := getMilestoneByID(e, oldMid)
  1007. if err != nil {
  1008. return err
  1009. }
  1010. m.NumIssues--
  1011. if issue.IsClosed {
  1012. m.NumClosedIssues--
  1013. }
  1014. if err = updateMilestone(e, m); err != nil {
  1015. return err
  1016. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE issue_id=?", issue.ID); err != nil {
  1017. return err
  1018. }
  1019. }
  1020. if issue.MilestoneID > 0 {
  1021. m, err := getMilestoneByID(e, issue.MilestoneID)
  1022. if err != nil {
  1023. return err
  1024. }
  1025. m.NumIssues++
  1026. if issue.IsClosed {
  1027. m.NumClosedIssues++
  1028. }
  1029. if m.NumIssues == 0 {
  1030. return ErrWrongIssueCounter
  1031. }
  1032. if err = updateMilestone(e, m); err != nil {
  1033. return err
  1034. } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=? WHERE issue_id=?", m.ID, issue.ID); err != nil {
  1035. return err
  1036. }
  1037. }
  1038. return updateIssue(e, issue)
  1039. }
  1040. // ChangeMilestoneAssign changes assignment of milestone for issue.
  1041. func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) {
  1042. sess := x.NewSession()
  1043. defer sess.Close()
  1044. if err = sess.Begin(); err != nil {
  1045. return err
  1046. }
  1047. if err = changeMilestoneAssign(sess, oldMid, issue); err != nil {
  1048. return err
  1049. }
  1050. return sess.Commit()
  1051. }
  1052. // DeleteMilestoneByID deletes a milestone by given ID.
  1053. func DeleteMilestoneByID(id int64) error {
  1054. m, err := GetMilestoneByID(id)
  1055. if err != nil {
  1056. if IsErrMilestoneNotExist(err) {
  1057. return nil
  1058. }
  1059. return err
  1060. }
  1061. repo, err := GetRepositoryByID(m.RepoID)
  1062. if err != nil {
  1063. return err
  1064. }
  1065. sess := x.NewSession()
  1066. defer sessionRelease(sess)
  1067. if err = sess.Begin(); err != nil {
  1068. return err
  1069. }
  1070. if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil {
  1071. return err
  1072. }
  1073. repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
  1074. repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
  1075. if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil {
  1076. return err
  1077. }
  1078. if _, err = sess.Exec("UPDATE `issue` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  1079. return err
  1080. } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE milestone_id=?", m.ID); err != nil {
  1081. return err
  1082. }
  1083. return sess.Commit()
  1084. }
  1085. // Attachment represent a attachment of issue/comment/release.
  1086. type Attachment struct {
  1087. ID int64 `xorm:"pk autoincr"`
  1088. UUID string `xorm:"uuid UNIQUE"`
  1089. IssueID int64 `xorm:"INDEX"`
  1090. CommentID int64
  1091. ReleaseID int64 `xorm:"INDEX"`
  1092. Name string
  1093. Created time.Time `xorm:"-"`
  1094. CreatedUnix int64
  1095. }
  1096. func (a *Attachment) BeforeInsert() {
  1097. a.CreatedUnix = time.Now().UTC().Unix()
  1098. }
  1099. func (a *Attachment) AfterSet(colName string, _ xorm.Cell) {
  1100. switch colName {
  1101. case "created_unix":
  1102. a.Created = time.Unix(a.CreatedUnix, 0).Local()
  1103. }
  1104. }
  1105. // AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
  1106. func AttachmentLocalPath(uuid string) string {
  1107. return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
  1108. }
  1109. // LocalPath returns where attachment is stored in local file system.
  1110. func (attach *Attachment) LocalPath() string {
  1111. return AttachmentLocalPath(attach.UUID)
  1112. }
  1113. // NewAttachment creates a new attachment object.
  1114. func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) {
  1115. attach := &Attachment{
  1116. UUID: gouuid.NewV4().String(),
  1117. Name: name,
  1118. }
  1119. if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil {
  1120. return nil, fmt.Errorf("MkdirAll: %v", err)
  1121. }
  1122. fw, err := os.Create(attach.LocalPath())
  1123. if err != nil {
  1124. return nil, fmt.Errorf("Create: %v", err)
  1125. }
  1126. defer fw.Close()
  1127. if _, err = fw.Write(buf); err != nil {
  1128. return nil, fmt.Errorf("Write: %v", err)
  1129. } else if _, err = io.Copy(fw, file); err != nil {
  1130. return nil, fmt.Errorf("Copy: %v", err)
  1131. }
  1132. sess := x.NewSession()
  1133. defer sessionRelease(sess)
  1134. if err := sess.Begin(); err != nil {
  1135. return nil, err
  1136. }
  1137. if _, err := sess.Insert(attach); err != nil {
  1138. return nil, err
  1139. }
  1140. return attach, sess.Commit()
  1141. }
  1142. func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
  1143. attach := &Attachment{UUID: uuid}
  1144. has, err := x.Get(attach)
  1145. if err != nil {
  1146. return nil, err
  1147. } else if !has {
  1148. return nil, ErrAttachmentNotExist{0, uuid}
  1149. }
  1150. return attach, nil
  1151. }
  1152. // GetAttachmentByUUID returns attachment by given UUID.
  1153. func GetAttachmentByUUID(uuid string) (*Attachment, error) {
  1154. return getAttachmentByUUID(x, uuid)
  1155. }
  1156. // GetAttachmentsByIssueID returns all attachments for given issue by ID.
  1157. func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) {
  1158. attachments := make([]*Attachment, 0, 10)
  1159. return attachments, x.Where("issue_id=? AND comment_id=0", issueID).Find(&attachments)
  1160. }
  1161. // GetAttachmentsByCommentID returns all attachments if comment by given ID.
  1162. func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
  1163. attachments := make([]*Attachment, 0, 10)
  1164. return attachments, x.Where("comment_id=?", commentID).Find(&attachments)
  1165. }
  1166. // DeleteAttachment deletes the given attachment and optionally the associated file.
  1167. func DeleteAttachment(a *Attachment, remove bool) error {
  1168. _, err := DeleteAttachments([]*Attachment{a}, remove)
  1169. return err
  1170. }
  1171. // DeleteAttachments deletes the given attachments and optionally the associated files.
  1172. func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
  1173. for i, a := range attachments {
  1174. if remove {
  1175. if err := os.Remove(a.LocalPath()); err != nil {
  1176. return i, err
  1177. }
  1178. }
  1179. if _, err := x.Delete(a.ID); err != nil {
  1180. return i, err
  1181. }
  1182. }
  1183. return len(attachments), nil
  1184. }
  1185. // DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
  1186. func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
  1187. attachments, err := GetAttachmentsByIssueID(issueId)
  1188. if err != nil {
  1189. return 0, err
  1190. }
  1191. return DeleteAttachments(attachments, remove)
  1192. }
  1193. // DeleteAttachmentsByComment deletes all attachments associated with the given comment.
  1194. func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
  1195. attachments, err := GetAttachmentsByCommentID(commentId)
  1196. if err != nil {
  1197. return 0, err
  1198. }
  1199. return DeleteAttachments(attachments, remove)
  1200. }