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