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  }