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