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