gitea源码

repo_stats.go 4.0KB

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