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.

153 lines
3.8 KiB

  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package git
  5. import (
  6. "bufio"
  7. "context"
  8. "fmt"
  9. "os"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. "time"
  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 acitivity page
  32. func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) (*CodeActivityStats, error) {
  33. stats := &CodeActivityStats{}
  34. since := fromTime.Format(time.RFC3339)
  35. stdout, err := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", fmt.Sprintf("--since='%s'", since)).RunInDirBytes(repo.Path)
  36. if err != nil {
  37. return nil, err
  38. }
  39. c, err := strconv.ParseInt(strings.TrimSpace(string(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. args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%an%n%ae%n", "--date=iso", fmt.Sprintf("--since='%s'", since)}
  53. if len(branch) == 0 {
  54. args = append(args, "--branches=*")
  55. } else {
  56. args = append(args, "--first-parent", branch)
  57. }
  58. stderr := new(strings.Builder)
  59. err = NewCommand(args...).RunInDirTimeoutEnvFullPipelineFunc(
  60. nil, -1, repo.Path,
  61. stdoutWriter, stderr, nil,
  62. func(ctx context.Context, cancel context.CancelFunc) error {
  63. _ = stdoutWriter.Close()
  64. scanner := bufio.NewScanner(stdoutReader)
  65. scanner.Split(bufio.ScanLines)
  66. stats.CommitCount = 0
  67. stats.Additions = 0
  68. stats.Deletions = 0
  69. authors := make(map[string]*CodeActivityAuthor)
  70. files := make(map[string]bool)
  71. var author string
  72. p := 0
  73. for scanner.Scan() {
  74. l := strings.TrimSpace(scanner.Text())
  75. if l == "---" {
  76. p = 1
  77. } else if p == 0 {
  78. continue
  79. } else {
  80. p++
  81. }
  82. if p > 4 && len(l) == 0 {
  83. continue
  84. }
  85. switch p {
  86. case 1: // Separator
  87. case 2: // Commit sha-1
  88. stats.CommitCount++
  89. case 3: // Author
  90. author = l
  91. case 4: // E-mail
  92. email := strings.ToLower(l)
  93. if _, ok := authors[email]; !ok {
  94. authors[email] = &CodeActivityAuthor{
  95. Name: author,
  96. Email: email,
  97. Commits: 0,
  98. }
  99. }
  100. authors[email].Commits++
  101. default: // Changed file
  102. if parts := strings.Fields(l); len(parts) >= 3 {
  103. if parts[0] != "-" {
  104. if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil {
  105. stats.Additions += c
  106. }
  107. }
  108. if parts[1] != "-" {
  109. if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil {
  110. stats.Deletions += c
  111. }
  112. }
  113. if _, ok := files[parts[2]]; !ok {
  114. files[parts[2]] = true
  115. }
  116. }
  117. }
  118. }
  119. a := make([]*CodeActivityAuthor, 0, len(authors))
  120. for _, v := range authors {
  121. a = append(a, v)
  122. }
  123. // Sort authors descending depending on commit count
  124. sort.Slice(a, func(i, j int) bool {
  125. return a[i].Commits > a[j].Commits
  126. })
  127. stats.AuthorCount = int64(len(authors))
  128. stats.ChangedFiles = int64(len(files))
  129. stats.Authors = a
  130. _ = stdoutReader.Close()
  131. return nil
  132. })
  133. if err != nil {
  134. return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr)
  135. }
  136. return stats, nil
  137. }