選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

repo_activity.go 11KB

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