gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "time"
  9. "code.gitea.io/gitea/models/db"
  10. user_model "code.gitea.io/gitea/models/user"
  11. "code.gitea.io/gitea/modules/optional"
  12. "code.gitea.io/gitea/modules/setting"
  13. "code.gitea.io/gitea/modules/util"
  14. "xorm.io/builder"
  15. "xorm.io/xorm"
  16. )
  17. // TrackedTime represents a time that was spent for a specific issue.
  18. type TrackedTime struct {
  19. ID int64 `xorm:"pk autoincr"`
  20. IssueID int64 `xorm:"INDEX"`
  21. Issue *Issue `xorm:"-"`
  22. UserID int64 `xorm:"INDEX"`
  23. User *user_model.User `xorm:"-"`
  24. Created time.Time `xorm:"-"`
  25. CreatedUnix int64 `xorm:"created"`
  26. Time int64 `xorm:"NOT NULL"`
  27. Deleted bool `xorm:"NOT NULL DEFAULT false"`
  28. }
  29. func init() {
  30. db.RegisterModel(new(TrackedTime))
  31. }
  32. // TrackedTimeList is a List of TrackedTime's
  33. type TrackedTimeList []*TrackedTime
  34. // AfterLoad is invoked from XORM after setting the values of all fields of this object.
  35. func (t *TrackedTime) AfterLoad() {
  36. t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation)
  37. }
  38. // LoadAttributes load Issue, User
  39. func (t *TrackedTime) LoadAttributes(ctx context.Context) (err error) {
  40. // Load the issue
  41. if t.Issue == nil {
  42. t.Issue, err = GetIssueByID(ctx, t.IssueID)
  43. if err != nil && !errors.Is(err, util.ErrNotExist) {
  44. return err
  45. }
  46. }
  47. // Now load the repo for the issue (which we may have just loaded)
  48. if t.Issue != nil {
  49. err = t.Issue.LoadRepo(ctx)
  50. if err != nil && !errors.Is(err, util.ErrNotExist) {
  51. return err
  52. }
  53. }
  54. // Load the user
  55. if t.User == nil {
  56. t.User, err = user_model.GetUserByID(ctx, t.UserID)
  57. if err != nil {
  58. if !errors.Is(err, util.ErrNotExist) {
  59. return err
  60. }
  61. t.User = user_model.NewGhostUser()
  62. }
  63. }
  64. return nil
  65. }
  66. // LoadAttributes load Issue, User
  67. func (tl TrackedTimeList) LoadAttributes(ctx context.Context) error {
  68. for _, t := range tl {
  69. if err := t.LoadAttributes(ctx); err != nil {
  70. return err
  71. }
  72. }
  73. return nil
  74. }
  75. // FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
  76. type FindTrackedTimesOptions struct {
  77. db.ListOptions
  78. IssueID int64
  79. UserID int64
  80. RepositoryID int64
  81. MilestoneID int64
  82. CreatedAfterUnix int64
  83. CreatedBeforeUnix int64
  84. }
  85. // toCond will convert each condition into a xorm-Cond
  86. func (opts *FindTrackedTimesOptions) ToConds() builder.Cond {
  87. cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false})
  88. if opts.IssueID != 0 {
  89. cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
  90. }
  91. if opts.UserID != 0 {
  92. cond = cond.And(builder.Eq{"user_id": opts.UserID})
  93. }
  94. if opts.RepositoryID != 0 {
  95. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
  96. }
  97. if opts.MilestoneID != 0 {
  98. cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
  99. }
  100. if opts.CreatedAfterUnix != 0 {
  101. cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix})
  102. }
  103. if opts.CreatedBeforeUnix != 0 {
  104. cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix})
  105. }
  106. return cond
  107. }
  108. func (opts *FindTrackedTimesOptions) ToJoins() []db.JoinFunc {
  109. if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
  110. return []db.JoinFunc{
  111. func(e db.Engine) error {
  112. e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
  113. return nil
  114. },
  115. }
  116. }
  117. return nil
  118. }
  119. // toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required
  120. func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
  121. sess := e
  122. if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
  123. sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
  124. }
  125. sess = sess.Where(opts.ToConds())
  126. if opts.Page > 0 {
  127. sess = db.SetSessionPagination(sess, opts)
  128. }
  129. return sess
  130. }
  131. // GetTrackedTimes returns all tracked times that fit to the given options.
  132. func GetTrackedTimes(ctx context.Context, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) {
  133. err = options.toSession(db.GetEngine(ctx)).Find(&trackedTimes)
  134. return trackedTimes, err
  135. }
  136. // CountTrackedTimes returns count of tracked times that fit to the given options.
  137. func CountTrackedTimes(ctx context.Context, opts *FindTrackedTimesOptions) (int64, error) {
  138. sess := db.GetEngine(ctx).Where(opts.ToConds())
  139. if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
  140. sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
  141. }
  142. return sess.Count(&TrackedTime{})
  143. }
  144. // GetTrackedSeconds return sum of seconds
  145. func GetTrackedSeconds(ctx context.Context, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) {
  146. return opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
  147. }
  148. // AddTime will add the given time (in seconds) to the issue
  149. func AddTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
  150. return db.WithTx2(ctx, func(ctx context.Context) (*TrackedTime, error) {
  151. t, err := addTime(ctx, user, issue, amount, created)
  152. if err != nil {
  153. return nil, err
  154. }
  155. if err := issue.LoadRepo(ctx); err != nil {
  156. return nil, err
  157. }
  158. if _, err := CreateComment(ctx, &CreateCommentOptions{
  159. Issue: issue,
  160. Repo: issue.Repo,
  161. Doer: user,
  162. // Content before v1.21 did store the formatted string instead of seconds,
  163. // so use "|" as delimiter to mark the new format
  164. Content: fmt.Sprintf("|%d", amount),
  165. Type: CommentTypeAddTimeManual,
  166. TimeID: t.ID,
  167. }); err != nil {
  168. return nil, err
  169. }
  170. return t, nil
  171. })
  172. }
  173. func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
  174. if created.IsZero() {
  175. created = time.Now()
  176. }
  177. tt := &TrackedTime{
  178. IssueID: issue.ID,
  179. UserID: user.ID,
  180. Time: amount,
  181. Created: created,
  182. }
  183. return tt, db.Insert(ctx, tt)
  184. }
  185. // TotalTimesForEachUser returns the spent time in seconds for each user by an issue
  186. func TotalTimesForEachUser(ctx context.Context, options *FindTrackedTimesOptions) (map[*user_model.User]int64, error) {
  187. trackedTimes, err := GetTrackedTimes(ctx, options)
  188. if err != nil {
  189. return nil, err
  190. }
  191. // Adding total time per user ID
  192. totalTimesByUser := make(map[int64]int64)
  193. for _, t := range trackedTimes {
  194. totalTimesByUser[t.UserID] += t.Time
  195. }
  196. totalTimes := make(map[*user_model.User]int64)
  197. // Fetching User and making time human readable
  198. for userID, total := range totalTimesByUser {
  199. user, err := user_model.GetUserByID(ctx, userID)
  200. if err != nil {
  201. if user_model.IsErrUserNotExist(err) {
  202. continue
  203. }
  204. return nil, err
  205. }
  206. totalTimes[user] = total
  207. }
  208. return totalTimes, nil
  209. }
  210. // DeleteIssueUserTimes deletes times for issue
  211. func DeleteIssueUserTimes(ctx context.Context, issue *Issue, user *user_model.User) error {
  212. return db.WithTx(ctx, func(ctx context.Context) error {
  213. opts := FindTrackedTimesOptions{
  214. IssueID: issue.ID,
  215. UserID: user.ID,
  216. }
  217. removedTime, err := deleteTimes(ctx, opts)
  218. if err != nil {
  219. return err
  220. }
  221. if removedTime == 0 {
  222. return db.ErrNotExist{Resource: "tracked_time"}
  223. }
  224. if err := issue.LoadRepo(ctx); err != nil {
  225. return err
  226. }
  227. _, err = CreateComment(ctx, &CreateCommentOptions{
  228. Issue: issue,
  229. Repo: issue.Repo,
  230. Doer: user,
  231. // Content before v1.21 did store the formatted string instead of seconds,
  232. // so use "|" as delimiter to mark the new format
  233. Content: fmt.Sprintf("|%d", removedTime),
  234. Type: CommentTypeDeleteTimeManual,
  235. })
  236. return err
  237. })
  238. }
  239. // DeleteTime delete a specific Time
  240. func DeleteTime(ctx context.Context, t *TrackedTime) error {
  241. return db.WithTx(ctx, func(ctx context.Context) error {
  242. if err := t.LoadAttributes(ctx); err != nil {
  243. return err
  244. }
  245. if err := deleteTime(ctx, t); err != nil {
  246. return err
  247. }
  248. _, err := CreateComment(ctx, &CreateCommentOptions{
  249. Issue: t.Issue,
  250. Repo: t.Issue.Repo,
  251. Doer: t.User,
  252. // Content before v1.21 did store the formatted string instead of seconds,
  253. // so use "|" as delimiter to mark the new format
  254. Content: fmt.Sprintf("|%d", t.Time),
  255. Type: CommentTypeDeleteTimeManual,
  256. })
  257. return err
  258. })
  259. }
  260. func deleteTimes(ctx context.Context, opts FindTrackedTimesOptions) (removedTime int64, err error) {
  261. removedTime, err = GetTrackedSeconds(ctx, opts)
  262. if err != nil || removedTime == 0 {
  263. return removedTime, err
  264. }
  265. _, err = opts.toSession(db.GetEngine(ctx)).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true})
  266. return removedTime, err
  267. }
  268. func deleteTime(ctx context.Context, t *TrackedTime) error {
  269. if t.Deleted {
  270. return db.ErrNotExist{Resource: "tracked_time", ID: t.ID}
  271. }
  272. t.Deleted = true
  273. _, err := db.GetEngine(ctx).ID(t.ID).Cols("deleted").Update(t)
  274. return err
  275. }
  276. // GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
  277. func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
  278. time := new(TrackedTime)
  279. has, err := db.GetEngine(ctx).ID(id).Get(time)
  280. if err != nil {
  281. return nil, err
  282. } else if !has {
  283. return nil, db.ErrNotExist{Resource: "tracked_time", ID: id}
  284. }
  285. return time, nil
  286. }
  287. // GetIssueTotalTrackedTime returns the total tracked time for issues by given conditions.
  288. func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool]) (int64, error) {
  289. if len(opts.IssueIDs) <= MaxQueryParameters {
  290. return getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs)
  291. }
  292. // If too long a list of IDs is provided,
  293. // we get the statistics in smaller chunks and get accumulates
  294. var accum int64
  295. for i := 0; i < len(opts.IssueIDs); {
  296. chunk := min(i+MaxQueryParameters, len(opts.IssueIDs))
  297. time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk])
  298. if err != nil {
  299. return 0, err
  300. }
  301. accum += time
  302. i = chunk
  303. }
  304. return accum, nil
  305. }
  306. func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isClosed optional.Option[bool], issueIDs []int64) (int64, error) {
  307. sumSession := func(opts *IssuesOptions, issueIDs []int64) *xorm.Session {
  308. sess := db.GetEngine(ctx).
  309. Table("tracked_time").
  310. Where("tracked_time.deleted = ?", false).
  311. Join("INNER", "issue", "tracked_time.issue_id = issue.id")
  312. return applyIssuesOptions(sess, opts, issueIDs)
  313. }
  314. type trackedTime struct {
  315. Time int64
  316. }
  317. session := sumSession(opts, issueIDs)
  318. if isClosed.Has() {
  319. session = session.And("issue.is_closed = ?", isClosed.Value())
  320. }
  321. return session.SumInt(new(trackedTime), "tracked_time.time")
  322. }