You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

351 lines
10 KiB

  1. // Copyright 2017 The Gitea 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. "fmt"
  7. "sort"
  8. "time"
  9. "code.gitea.io/gitea/modules/git"
  10. "xorm.io/xorm"
  11. )
  12. // ActivityAuthorData represents statistical git commit count data
  13. type ActivityAuthorData struct {
  14. Name string `json:"name"`
  15. Login string `json:"login"`
  16. AvatarLink string `json:"avatar_link"`
  17. Commits int64 `json:"commits"`
  18. }
  19. // ActivityStats represets issue and pull request information.
  20. type ActivityStats struct {
  21. OpenedPRs PullRequestList
  22. OpenedPRAuthorCount int64
  23. MergedPRs PullRequestList
  24. MergedPRAuthorCount int64
  25. OpenedIssues IssueList
  26. OpenedIssueAuthorCount int64
  27. ClosedIssues IssueList
  28. ClosedIssueAuthorCount int64
  29. UnresolvedIssues IssueList
  30. PublishedReleases []*Release
  31. PublishedReleaseAuthorCount int64
  32. Code *git.CodeActivityStats
  33. }
  34. // GetActivityStats return stats for repository at given time range
  35. func GetActivityStats(repo *Repository, timeFrom time.Time, releases, issues, prs, code bool) (*ActivityStats, error) {
  36. stats := &ActivityStats{Code: &git.CodeActivityStats{}}
  37. if releases {
  38. if err := stats.FillReleases(repo.ID, timeFrom); err != nil {
  39. return nil, fmt.Errorf("FillReleases: %v", err)
  40. }
  41. }
  42. if prs {
  43. if err := stats.FillPullRequests(repo.ID, timeFrom); err != nil {
  44. return nil, fmt.Errorf("FillPullRequests: %v", err)
  45. }
  46. }
  47. if issues {
  48. if err := stats.FillIssues(repo.ID, timeFrom); err != nil {
  49. return nil, fmt.Errorf("FillIssues: %v", err)
  50. }
  51. }
  52. if err := stats.FillUnresolvedIssues(repo.ID, timeFrom, issues, prs); err != nil {
  53. return nil, fmt.Errorf("FillUnresolvedIssues: %v", err)
  54. }
  55. if code {
  56. gitRepo, err := git.OpenRepository(repo.RepoPath())
  57. if err != nil {
  58. return nil, fmt.Errorf("OpenRepository: %v", err)
  59. }
  60. defer gitRepo.Close()
  61. code, err := gitRepo.GetCodeActivityStats(timeFrom, repo.DefaultBranch)
  62. if err != nil {
  63. return nil, fmt.Errorf("FillFromGit: %v", err)
  64. }
  65. stats.Code = code
  66. }
  67. return stats, nil
  68. }
  69. // GetActivityStatsTopAuthors returns top author stats for git commits for all branches
  70. func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) {
  71. gitRepo, err := git.OpenRepository(repo.RepoPath())
  72. if err != nil {
  73. return nil, fmt.Errorf("OpenRepository: %v", err)
  74. }
  75. defer gitRepo.Close()
  76. code, err := gitRepo.GetCodeActivityStats(timeFrom, "")
  77. if err != nil {
  78. return nil, fmt.Errorf("FillFromGit: %v", err)
  79. }
  80. if code.Authors == nil {
  81. return nil, nil
  82. }
  83. users := make(map[int64]*ActivityAuthorData)
  84. for k, v := range code.Authors {
  85. if len(k) == 0 {
  86. continue
  87. }
  88. u, err := GetUserByEmail(k)
  89. if u == nil || IsErrUserNotExist(err) {
  90. continue
  91. }
  92. if err != nil {
  93. return nil, err
  94. }
  95. if user, ok := users[u.ID]; !ok {
  96. users[u.ID] = &ActivityAuthorData{
  97. Name: u.DisplayName(),
  98. Login: u.LowerName,
  99. AvatarLink: u.AvatarLink(),
  100. Commits: v,
  101. }
  102. } else {
  103. user.Commits += v
  104. }
  105. }
  106. v := make([]*ActivityAuthorData, 0)
  107. for _, u := range users {
  108. v = append(v, u)
  109. }
  110. sort.Slice(v, func(i, j int) bool {
  111. return v[i].Commits < v[j].Commits
  112. })
  113. cnt := count
  114. if cnt > len(v) {
  115. cnt = len(v)
  116. }
  117. return v[:cnt], nil
  118. }
  119. // ActivePRCount returns total active pull request count
  120. func (stats *ActivityStats) ActivePRCount() int {
  121. return stats.OpenedPRCount() + stats.MergedPRCount()
  122. }
  123. // OpenedPRCount returns opened pull request count
  124. func (stats *ActivityStats) OpenedPRCount() int {
  125. return len(stats.OpenedPRs)
  126. }
  127. // OpenedPRPerc returns opened pull request percents from total active
  128. func (stats *ActivityStats) OpenedPRPerc() int {
  129. return int(float32(stats.OpenedPRCount()) / float32(stats.ActivePRCount()) * 100.0)
  130. }
  131. // MergedPRCount returns merged pull request count
  132. func (stats *ActivityStats) MergedPRCount() int {
  133. return len(stats.MergedPRs)
  134. }
  135. // MergedPRPerc returns merged pull request percent from total active
  136. func (stats *ActivityStats) MergedPRPerc() int {
  137. return int(float32(stats.MergedPRCount()) / float32(stats.ActivePRCount()) * 100.0)
  138. }
  139. // ActiveIssueCount returns total active issue count
  140. func (stats *ActivityStats) ActiveIssueCount() int {
  141. return stats.OpenedIssueCount() + stats.ClosedIssueCount()
  142. }
  143. // OpenedIssueCount returns open issue count
  144. func (stats *ActivityStats) OpenedIssueCount() int {
  145. return len(stats.OpenedIssues)
  146. }
  147. // OpenedIssuePerc returns open issue count percent from total active
  148. func (stats *ActivityStats) OpenedIssuePerc() int {
  149. return int(float32(stats.OpenedIssueCount()) / float32(stats.ActiveIssueCount()) * 100.0)
  150. }
  151. // ClosedIssueCount returns closed issue count
  152. func (stats *ActivityStats) ClosedIssueCount() int {
  153. return len(stats.ClosedIssues)
  154. }
  155. // ClosedIssuePerc returns closed issue count percent from total active
  156. func (stats *ActivityStats) ClosedIssuePerc() int {
  157. return int(float32(stats.ClosedIssueCount()) / float32(stats.ActiveIssueCount()) * 100.0)
  158. }
  159. // UnresolvedIssueCount returns unresolved issue and pull request count
  160. func (stats *ActivityStats) UnresolvedIssueCount() int {
  161. return len(stats.UnresolvedIssues)
  162. }
  163. // PublishedReleaseCount returns published release count
  164. func (stats *ActivityStats) PublishedReleaseCount() int {
  165. return len(stats.PublishedReleases)
  166. }
  167. // FillPullRequests returns pull request information for activity page
  168. func (stats *ActivityStats) FillPullRequests(repoID int64, fromTime time.Time) error {
  169. var err error
  170. var count int64
  171. // Merged pull requests
  172. sess := pullRequestsForActivityStatement(repoID, fromTime, true)
  173. sess.OrderBy("pull_request.merged_unix DESC")
  174. stats.MergedPRs = make(PullRequestList, 0)
  175. if err = sess.Find(&stats.MergedPRs); err != nil {
  176. return err
  177. }
  178. if err = stats.MergedPRs.LoadAttributes(); err != nil {
  179. return err
  180. }
  181. // Merged pull request authors
  182. sess = pullRequestsForActivityStatement(repoID, fromTime, true)
  183. if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("pull_request").Get(&count); err != nil {
  184. return err
  185. }
  186. stats.MergedPRAuthorCount = count
  187. // Opened pull requests
  188. sess = pullRequestsForActivityStatement(repoID, fromTime, false)
  189. sess.OrderBy("issue.created_unix ASC")
  190. stats.OpenedPRs = make(PullRequestList, 0)
  191. if err = sess.Find(&stats.OpenedPRs); err != nil {
  192. return err
  193. }
  194. if err = stats.OpenedPRs.LoadAttributes(); err != nil {
  195. return err
  196. }
  197. // Opened pull request authors
  198. sess = pullRequestsForActivityStatement(repoID, fromTime, false)
  199. if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("pull_request").Get(&count); err != nil {
  200. return err
  201. }
  202. stats.OpenedPRAuthorCount = count
  203. return nil
  204. }
  205. func pullRequestsForActivityStatement(repoID int64, fromTime time.Time, merged bool) *xorm.Session {
  206. sess := x.Where("pull_request.base_repo_id=?", repoID).
  207. Join("INNER", "issue", "pull_request.issue_id = issue.id")
  208. if merged {
  209. sess.And("pull_request.has_merged = ?", true)
  210. sess.And("pull_request.merged_unix >= ?", fromTime.Unix())
  211. } else {
  212. sess.And("issue.is_closed = ?", false)
  213. sess.And("issue.created_unix >= ?", fromTime.Unix())
  214. }
  215. return sess
  216. }
  217. // FillIssues returns issue information for activity page
  218. func (stats *ActivityStats) FillIssues(repoID int64, fromTime time.Time) error {
  219. var err error
  220. var count int64
  221. // Closed issues
  222. sess := issuesForActivityStatement(repoID, fromTime, true, false)
  223. sess.OrderBy("issue.closed_unix DESC")
  224. stats.ClosedIssues = make(IssueList, 0)
  225. if err = sess.Find(&stats.ClosedIssues); err != nil {
  226. return err
  227. }
  228. // Closed issue authors
  229. sess = issuesForActivityStatement(repoID, fromTime, true, false)
  230. if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
  231. return err
  232. }
  233. stats.ClosedIssueAuthorCount = count
  234. // New issues
  235. sess = issuesForActivityStatement(repoID, fromTime, false, false)
  236. sess.OrderBy("issue.created_unix ASC")
  237. stats.OpenedIssues = make(IssueList, 0)
  238. if err = sess.Find(&stats.OpenedIssues); err != nil {
  239. return err
  240. }
  241. // Opened issue authors
  242. sess = issuesForActivityStatement(repoID, fromTime, false, false)
  243. if _, err = sess.Select("count(distinct issue.poster_id) as `count`").Table("issue").Get(&count); err != nil {
  244. return err
  245. }
  246. stats.OpenedIssueAuthorCount = count
  247. return nil
  248. }
  249. // FillUnresolvedIssues returns unresolved issue and pull request information for activity page
  250. func (stats *ActivityStats) FillUnresolvedIssues(repoID int64, fromTime time.Time, issues, prs bool) error {
  251. // Check if we need to select anything
  252. if !issues && !prs {
  253. return nil
  254. }
  255. sess := issuesForActivityStatement(repoID, fromTime, false, true)
  256. if !issues || !prs {
  257. sess.And("issue.is_pull = ?", prs)
  258. }
  259. sess.OrderBy("issue.updated_unix DESC")
  260. stats.UnresolvedIssues = make(IssueList, 0)
  261. return sess.Find(&stats.UnresolvedIssues)
  262. }
  263. func issuesForActivityStatement(repoID int64, fromTime time.Time, closed, unresolved bool) *xorm.Session {
  264. sess := x.Where("issue.repo_id = ?", repoID).
  265. And("issue.is_closed = ?", closed)
  266. if !unresolved {
  267. sess.And("issue.is_pull = ?", false)
  268. if closed {
  269. sess.And("issue.closed_unix >= ?", fromTime.Unix())
  270. } else {
  271. sess.And("issue.created_unix >= ?", fromTime.Unix())
  272. }
  273. } else {
  274. sess.And("issue.created_unix < ?", fromTime.Unix())
  275. sess.And("issue.updated_unix >= ?", fromTime.Unix())
  276. }
  277. return sess
  278. }
  279. // FillReleases returns release information for activity page
  280. func (stats *ActivityStats) FillReleases(repoID int64, fromTime time.Time) error {
  281. var err error
  282. var count int64
  283. // Published releases list
  284. sess := releasesForActivityStatement(repoID, fromTime)
  285. sess.OrderBy("release.created_unix DESC")
  286. stats.PublishedReleases = make([]*Release, 0)
  287. if err = sess.Find(&stats.PublishedReleases); err != nil {
  288. return err
  289. }
  290. // Published releases authors
  291. sess = releasesForActivityStatement(repoID, fromTime)
  292. if _, err = sess.Select("count(distinct release.publisher_id) as `count`").Table("release").Get(&count); err != nil {
  293. return err
  294. }
  295. stats.PublishedReleaseAuthorCount = count
  296. return nil
  297. }
  298. func releasesForActivityStatement(repoID int64, fromTime time.Time) *xorm.Session {
  299. return x.Where("release.repo_id = ?", repoID).
  300. And("release.is_draft = ?", false).
  301. And("release.created_unix >= ?", fromTime.Unix())
  302. }