gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. // Copyright 2023 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repository
  4. import (
  5. "bufio"
  6. "context"
  7. "errors"
  8. "fmt"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. "code.gitea.io/gitea/models/avatars"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/cache"
  18. "code.gitea.io/gitea/modules/git"
  19. "code.gitea.io/gitea/modules/git/gitcmd"
  20. "code.gitea.io/gitea/modules/gitrepo"
  21. "code.gitea.io/gitea/modules/graceful"
  22. "code.gitea.io/gitea/modules/log"
  23. api "code.gitea.io/gitea/modules/structs"
  24. )
  25. const (
  26. contributorStatsCacheKey = "GetContributorStats/%s/%s"
  27. contributorStatsCacheTimeout int64 = 60 * 10
  28. )
  29. var (
  30. ErrAwaitGeneration = errors.New("generation took longer than ")
  31. awaitGenerationTime = time.Second * 5
  32. generateLock = sync.Map{}
  33. )
  34. type WeekData struct {
  35. Week int64 `json:"week"` // Starting day of the week as Unix timestamp
  36. Additions int `json:"additions"` // Number of additions in that week
  37. Deletions int `json:"deletions"` // Number of deletions in that week
  38. Commits int `json:"commits"` // Number of commits in that week
  39. }
  40. // ContributorData represents statistical git commit count data
  41. type ContributorData struct {
  42. Name string `json:"name"` // Display name of the contributor
  43. Login string `json:"login"` // Login name of the contributor in case it exists
  44. AvatarLink string `json:"avatar_link"`
  45. HomeLink string `json:"home_link"`
  46. TotalCommits int64 `json:"total_commits"`
  47. Weeks map[int64]*WeekData `json:"weeks"`
  48. }
  49. // ExtendedCommitStats contains information for commit stats with author data
  50. type ExtendedCommitStats struct {
  51. Author *api.CommitUser `json:"author"`
  52. Stats *api.CommitStats `json:"stats"`
  53. }
  54. const layout = time.DateOnly
  55. func findLastSundayBeforeDate(dateStr string) (string, error) {
  56. date, err := time.Parse(layout, dateStr)
  57. if err != nil {
  58. return "", err
  59. }
  60. weekday := date.Weekday()
  61. daysToSubtract := int(weekday) - int(time.Sunday)
  62. if daysToSubtract < 0 {
  63. daysToSubtract += 7
  64. }
  65. lastSunday := date.AddDate(0, 0, -daysToSubtract)
  66. return lastSunday.Format(layout), nil
  67. }
  68. // GetContributorStats returns contributors stats for git commits for given revision or default branch
  69. func GetContributorStats(ctx context.Context, cache cache.StringCache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
  70. // as GetContributorStats is resource intensive we cache the result
  71. cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision)
  72. if !cache.IsExist(cacheKey) {
  73. genReady := make(chan struct{})
  74. // dont start multiple async generations
  75. _, run := generateLock.Load(cacheKey)
  76. if run {
  77. return nil, ErrAwaitGeneration
  78. }
  79. generateLock.Store(cacheKey, struct{}{})
  80. // run generation async
  81. go generateContributorStats(genReady, cache, cacheKey, repo, revision)
  82. select {
  83. case <-time.After(awaitGenerationTime):
  84. return nil, ErrAwaitGeneration
  85. case <-genReady:
  86. // we got generation ready before timeout
  87. break
  88. }
  89. }
  90. // TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
  91. var res map[string]*ContributorData
  92. if _, cacheErr := cache.GetJSON(cacheKey, &res); cacheErr != nil {
  93. return nil, fmt.Errorf("cached error: %w", cacheErr.ToError())
  94. }
  95. return res, nil
  96. }
  97. // getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision
  98. func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
  99. baseCommit, err := repo.GetCommit(revision)
  100. if err != nil {
  101. return nil, err
  102. }
  103. stdoutReader, stdoutWriter, err := os.Pipe()
  104. if err != nil {
  105. return nil, err
  106. }
  107. defer func() {
  108. _ = stdoutReader.Close()
  109. _ = stdoutWriter.Close()
  110. }()
  111. gitCmd := gitcmd.NewCommand("log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse")
  112. // AddOptionFormat("--max-count=%d", limit)
  113. gitCmd.AddDynamicArguments(baseCommit.ID.String())
  114. var extendedCommitStats []*ExtendedCommitStats
  115. stderr := new(strings.Builder)
  116. err = gitCmd.Run(repo.Ctx, &gitcmd.RunOpts{
  117. Dir: repo.Path,
  118. Stdout: stdoutWriter,
  119. Stderr: stderr,
  120. PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
  121. _ = stdoutWriter.Close()
  122. scanner := bufio.NewScanner(stdoutReader)
  123. for scanner.Scan() {
  124. line := strings.TrimSpace(scanner.Text())
  125. if line != "---" {
  126. continue
  127. }
  128. scanner.Scan()
  129. authorName := strings.TrimSpace(scanner.Text())
  130. scanner.Scan()
  131. authorEmail := strings.TrimSpace(scanner.Text())
  132. scanner.Scan()
  133. date := strings.TrimSpace(scanner.Text())
  134. scanner.Scan()
  135. stats := strings.TrimSpace(scanner.Text())
  136. if authorName == "" || authorEmail == "" || date == "" || stats == "" {
  137. // FIXME: find a better way to parse the output so that we will handle this properly
  138. log.Warn("Something is wrong with git log output, skipping...")
  139. log.Warn("authorName: %s, authorEmail: %s, date: %s, stats: %s", authorName, authorEmail, date, stats)
  140. continue
  141. }
  142. // 1 file changed, 1 insertion(+), 1 deletion(-)
  143. fields := strings.Split(stats, ",")
  144. commitStats := api.CommitStats{}
  145. for _, field := range fields[1:] {
  146. parts := strings.Split(strings.TrimSpace(field), " ")
  147. value, contributionType := parts[0], parts[1]
  148. amount, _ := strconv.Atoi(value)
  149. if strings.HasPrefix(contributionType, "insertion") {
  150. commitStats.Additions = amount
  151. } else {
  152. commitStats.Deletions = amount
  153. }
  154. }
  155. commitStats.Total = commitStats.Additions + commitStats.Deletions
  156. scanner.Text() // empty line at the end
  157. res := &ExtendedCommitStats{
  158. Author: &api.CommitUser{
  159. Identity: api.Identity{
  160. Name: authorName,
  161. Email: authorEmail,
  162. },
  163. Date: date,
  164. },
  165. Stats: &commitStats,
  166. }
  167. extendedCommitStats = append(extendedCommitStats, res)
  168. }
  169. _ = stdoutReader.Close()
  170. return nil
  171. },
  172. })
  173. if err != nil {
  174. return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr)
  175. }
  176. return extendedCommitStats, nil
  177. }
  178. func generateContributorStats(genDone chan struct{}, cache cache.StringCache, cacheKey string, repo *repo_model.Repository, revision string) {
  179. ctx := graceful.GetManager().HammerContext()
  180. gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
  181. if err != nil {
  182. _ = cache.PutJSON(cacheKey, fmt.Errorf("OpenRepository: %w", err), contributorStatsCacheTimeout)
  183. return
  184. }
  185. defer closer.Close()
  186. if len(revision) == 0 {
  187. revision = repo.DefaultBranch
  188. }
  189. extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision)
  190. if err != nil {
  191. _ = cache.PutJSON(cacheKey, fmt.Errorf("ExtendedCommitStats: %w", err), contributorStatsCacheTimeout)
  192. return
  193. }
  194. if len(extendedCommitStats) == 0 {
  195. _ = cache.PutJSON(cacheKey, fmt.Errorf("no commit stats returned for revision '%s'", revision), contributorStatsCacheTimeout)
  196. return
  197. }
  198. layout := time.DateOnly
  199. unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0)
  200. contributorsCommitStats := make(map[string]*ContributorData)
  201. contributorsCommitStats["total"] = &ContributorData{
  202. Name: "Total",
  203. Weeks: make(map[int64]*WeekData),
  204. }
  205. total := contributorsCommitStats["total"]
  206. for _, v := range extendedCommitStats {
  207. userEmail := v.Author.Email
  208. if len(userEmail) == 0 {
  209. continue
  210. }
  211. u, _ := user_model.GetUserByEmail(ctx, userEmail)
  212. if u != nil {
  213. // update userEmail with user's primary email address so
  214. // that different mail addresses will linked to same account
  215. userEmail = u.GetEmail()
  216. }
  217. // duplicated logic
  218. if _, ok := contributorsCommitStats[userEmail]; !ok {
  219. if u == nil {
  220. avatarLink := avatars.GenerateEmailAvatarFastLink(ctx, userEmail, 0)
  221. if avatarLink == "" {
  222. avatarLink = unknownUserAvatarLink
  223. }
  224. contributorsCommitStats[userEmail] = &ContributorData{
  225. Name: v.Author.Name,
  226. AvatarLink: avatarLink,
  227. Weeks: make(map[int64]*WeekData),
  228. }
  229. } else {
  230. contributorsCommitStats[userEmail] = &ContributorData{
  231. Name: u.DisplayName(),
  232. Login: u.LowerName,
  233. AvatarLink: u.AvatarLinkWithSize(ctx, 0),
  234. HomeLink: u.HomeLink(),
  235. Weeks: make(map[int64]*WeekData),
  236. }
  237. }
  238. }
  239. // Update user statistics
  240. user := contributorsCommitStats[userEmail]
  241. startingOfWeek, _ := findLastSundayBeforeDate(v.Author.Date)
  242. val, _ := time.Parse(layout, startingOfWeek)
  243. week := val.UnixMilli()
  244. if user.Weeks[week] == nil {
  245. user.Weeks[week] = &WeekData{
  246. Additions: 0,
  247. Deletions: 0,
  248. Commits: 0,
  249. Week: week,
  250. }
  251. }
  252. if total.Weeks[week] == nil {
  253. total.Weeks[week] = &WeekData{
  254. Additions: 0,
  255. Deletions: 0,
  256. Commits: 0,
  257. Week: week,
  258. }
  259. }
  260. user.Weeks[week].Additions += v.Stats.Additions
  261. user.Weeks[week].Deletions += v.Stats.Deletions
  262. user.Weeks[week].Commits++
  263. user.TotalCommits++
  264. // Update overall statistics
  265. total.Weeks[week].Additions += v.Stats.Additions
  266. total.Weeks[week].Deletions += v.Stats.Deletions
  267. total.Weeks[week].Commits++
  268. total.TotalCommits++
  269. }
  270. _ = cache.PutJSON(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout)
  271. generateLock.Delete(cacheKey)
  272. if genDone != nil {
  273. genDone <- struct{}{}
  274. }
  275. }