You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

repo_stats.go 3.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package git
  4. import (
  5. "bufio"
  6. "context"
  7. "fmt"
  8. "os"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "code.gitea.io/gitea/modules/container"
  14. )
  15. // CodeActivityStats represents git statistics data
  16. type CodeActivityStats struct {
  17. AuthorCount int64
  18. CommitCount int64
  19. ChangedFiles int64
  20. Additions int64
  21. Deletions int64
  22. CommitCountInAllBranches int64
  23. Authors []*CodeActivityAuthor
  24. }
  25. // CodeActivityAuthor represents git statistics data for commit authors
  26. type CodeActivityAuthor struct {
  27. Name string
  28. Email string
  29. Commits int64
  30. }
  31. // GetCodeActivityStats returns code statistics for activity page
  32. func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) (*CodeActivityStats, error) {
  33. stats := &CodeActivityStats{}
  34. since := fromTime.Format(time.RFC3339)
  35. stdout, _, runErr := NewCommand(repo.Ctx, "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").AddOptionFormat("--since='%s'", since).RunStdString(&RunOpts{Dir: repo.Path})
  36. if runErr != nil {
  37. return nil, runErr
  38. }
  39. c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
  40. if err != nil {
  41. return nil, err
  42. }
  43. stats.CommitCountInAllBranches = c
  44. stdoutReader, stdoutWriter, err := os.Pipe()
  45. if err != nil {
  46. return nil, err
  47. }
  48. defer func() {
  49. _ = stdoutReader.Close()
  50. _ = stdoutWriter.Close()
  51. }()
  52. gitCmd := NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").AddOptionFormat("--since='%s'", since)
  53. if len(branch) == 0 {
  54. gitCmd.AddArguments("--branches=*")
  55. } else {
  56. gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch)
  57. }
  58. stderr := new(strings.Builder)
  59. err = gitCmd.Run(&RunOpts{
  60. Env: []string{},
  61. Dir: repo.Path,
  62. Stdout: stdoutWriter,
  63. Stderr: stderr,
  64. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  65. _ = stdoutWriter.Close()
  66. scanner := bufio.NewScanner(stdoutReader)
  67. scanner.Split(bufio.ScanLines)
  68. stats.CommitCount = 0
  69. stats.Additions = 0
  70. stats.Deletions = 0
  71. authors := make(map[string]*CodeActivityAuthor)
  72. files := make(container.Set[string])
  73. var author string
  74. p := 0
  75. for scanner.Scan() {
  76. l := strings.TrimSpace(scanner.Text())
  77. if l == "---" {
  78. p = 1
  79. } else if p == 0 {
  80. continue
  81. } else {
  82. p++
  83. }
  84. if p > 4 && len(l) == 0 {
  85. continue
  86. }
  87. switch p {
  88. case 1: // Separator
  89. case 2: // Commit sha-1
  90. stats.CommitCount++
  91. case 3: // Author
  92. author = l
  93. case 4: // E-mail
  94. email := strings.ToLower(l)
  95. if _, ok := authors[email]; !ok {
  96. authors[email] = &CodeActivityAuthor{Name: author, Email: email, Commits: 0}
  97. }
  98. authors[email].Commits++
  99. default: // Changed file
  100. if parts := strings.Fields(l); len(parts) >= 3 {
  101. if parts[0] != "-" {
  102. if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil {
  103. stats.Additions += c
  104. }
  105. }
  106. if parts[1] != "-" {
  107. if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil {
  108. stats.Deletions += c
  109. }
  110. }
  111. files.Add(parts[2])
  112. }
  113. }
  114. }
  115. a := make([]*CodeActivityAuthor, 0, len(authors))
  116. for _, v := range authors {
  117. a = append(a, v)
  118. }
  119. // Sort authors descending depending on commit count
  120. sort.Slice(a, func(i, j int) bool {
  121. return a[i].Commits > a[j].Commits
  122. })
  123. stats.AuthorCount = int64(len(authors))
  124. stats.ChangedFiles = int64(len(files))
  125. stats.Authors = a
  126. _ = stdoutReader.Close()
  127. return nil
  128. },
  129. })
  130. if err != nil {
  131. return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr)
  132. }
  133. return stats, nil
  134. }