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