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.

repo_activity.go 11KB

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