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