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  }