github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/repo_stats.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  package git
     7  
     8  import (
     9  	"bufio"
    10  	"context"
    11  	"fmt"
    12  	"os"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    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", fmt.Sprintf("--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  	args := []string{"log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso", fmt.Sprintf("--since='%s'", since)}
    64  	if len(branch) == 0 {
    65  		args = append(args, "--branches=*")
    66  	} else {
    67  		args = append(args, "--first-parent", branch)
    68  	}
    69  
    70  	stderr := new(strings.Builder)
    71  	err = NewCommand(repo.Ctx, args...).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(map[string]bool)
    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  						if _, ok := files[parts[2]]; !ok {
   124  							files[parts[2]] = true
   125  						}
   126  					}
   127  				}
   128  			}
   129  			a := make([]*CodeActivityAuthor, 0, len(authors))
   130  			for _, v := range authors {
   131  				a = append(a, v)
   132  			}
   133  			// Sort authors descending depending on commit count
   134  			sort.Slice(a, func(i, j int) bool {
   135  				return a[i].Commits > a[j].Commits
   136  			})
   137  			stats.AuthorCount = int64(len(authors))
   138  			stats.ChangedFiles = int64(len(files))
   139  			stats.Authors = a
   140  			_ = stdoutReader.Close()
   141  			return nil
   142  		},
   143  	})
   144  	if err != nil {
   145  		return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr)
   146  	}
   147  
   148  	return stats, nil
   149  }