diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go new file mode 100644 index 0000000..1cffcdb --- /dev/null +++ b/modules/git/repo_stats.go @@ -0,0 +1,184 @@ +package git + +import ( + "bufio" + "context" + "fmt" + "os" + "sort" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/container" + gitea_git "code.gitea.io/gitea/modules/git" +) + +type CodeActivityStats struct { + AuthorCount int64 `json:"author_count"` + CommitCount int64 `json:"commit_count"` + ChangedFiles int64 `json:"changed_files"` + Additions int64 `json:"additions"` + Deletions int64 `json:"deletions"` + CommitCountInAllBranches int64 `json:"commit_count_in_all_branches"` + Authors []*CodeActivityAuthor `json:"authors"` +} + +type CodeActivityAuthor struct { + Name string `json:"name"` + Email string `json:"email"` + Commits int64 `json:"commits"` + Additions int64 `json:"additions"` + Deletions int64 `json:"deletions"` +} + +// GetCodeActivityStats returns code statistics for activity page +func GetCodeActivityStatsWithoutSince(repo *gitea_git.Repository, branch string, page, pageSize int) (int64, *CodeActivityStats, error) { + var total int64 + + stats := &CodeActivityStats{} + + authorStdout, _, runErr := gitea_git.NewCommand(repo.Ctx, "log", "--all", "--format='%aN <%cE>'").RunStdString(&gitea_git.RunOpts{Dir: repo.Path}) + if runErr != nil { + return total, nil, runErr + } + + skip := (page - 1) * pageSize + + var filterAuthor string + var ca int + authorArr := strings.Split(authorStdout, "\n") + authorSet := make(map[string]bool, len(authorArr)) + for _, author := range authorArr { + _, ok := authorSet[author] + if ok || author == "" { + continue + } + authorSet[author] = true + authorArr[total] = author + total++ + author = strings.ReplaceAll(author, "'", "") + if skip > 0 { + skip-- + } else { + if ca < pageSize { + if ca != 0 { + filterAuthor += "\\|" + } + filterAuthor += author + ca++ + } + } + } + + if filterAuthor == "" { + return total, &CodeActivityStats{Authors: make([]*CodeActivityAuthor, 0)}, nil + } + + stdout, _, runErr := gitea_git.NewCommand(repo.Ctx, "rev-list", "--count", "--no-merges", "--branches=*", "--date=iso", gitea_git.CmdArg(fmt.Sprintf("--author=%s", filterAuthor))).RunStdString(&gitea_git.RunOpts{Dir: repo.Path}) + if runErr != nil { + return total, nil, runErr + } + + c, err := strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) + if err != nil { + return total, nil, err + } + stats.CommitCountInAllBranches = c + + stdoutReader, stdoutWriter, err := os.Pipe() + if err != nil { + return total, nil, err + } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + gitCmd := gitea_git.NewCommand(repo.Ctx, "log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso", gitea_git.CmdArg(fmt.Sprintf("--author=%s", filterAuthor))) + if len(branch) == 0 { + gitCmd.AddArguments("--branches=*") + } else { + gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch) + } + + stderr := new(strings.Builder) + err = gitCmd.Run(&gitea_git.RunOpts{ + Env: []string{}, + Dir: repo.Path, + Stdout: stdoutWriter, + Stderr: stderr, + PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { + _ = stdoutWriter.Close() + scanner := bufio.NewScanner(stdoutReader) + scanner.Split(bufio.ScanLines) + stats.CommitCount = 0 + stats.Additions = 0 + stats.Deletions = 0 + authors := make(map[string]*CodeActivityAuthor) + files := make(container.Set[string]) + var author string + var currentAuthor *CodeActivityAuthor + p := 0 + for scanner.Scan() { + l := strings.TrimSpace(scanner.Text()) + if l == "---" { + p = 1 + } else if p == 0 { + continue + } else { + p++ + } + if p > 4 && len(l) == 0 { + continue + } + switch p { + case 1: // Separator + case 2: // Commit sha-1 + stats.CommitCount++ + case 3: // Author + author = l + case 4: // E-mail + email := strings.ToLower(l) + if _, ok := authors[email+author]; !ok { + authors[email+author] = &CodeActivityAuthor{Name: author, Email: email, Commits: 0} + } + authors[email+author].Commits++ + currentAuthor = authors[email+author] + default: // Changed file + if parts := strings.Fields(l); len(parts) >= 3 { + if parts[0] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64); err == nil { + stats.Additions += c + currentAuthor.Additions += c + } + } + if parts[1] != "-" { + if c, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil { + stats.Deletions += c + currentAuthor.Deletions += c + } + } + files.Add(parts[2]) + } + } + } + a := make([]*CodeActivityAuthor, 0, len(authors)) + for _, v := range authors { + a = append(a, v) + } + // Sort authors descending depending on commit count + sort.Slice(a, func(i, j int) bool { + return a[i].Commits > a[j].Commits + }) + stats.AuthorCount = int64(len(authors)) + stats.ChangedFiles = int64(len(files)) + stats.Authors = a + _ = stdoutReader.Close() + return nil + }, + }) + if err != nil { + return total, nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr) + } + + return total, stats, nil +} diff --git a/routers/hat/hat.go b/routers/hat/hat.go index 1ff0d48..fbd3937 100644 --- a/routers/hat/hat.go +++ b/routers/hat/hat.go @@ -144,6 +144,7 @@ func Routers(ctx gocontext.Context) *web.Route { }) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Get("/blame", context.RepoRef(), repo.GetRepoRefBlame) + m.Get("/code_stats", context.RepoRef(), repo.ListCodeStats) }, repoAssignment()) }) m.Group("/users", func() { diff --git a/routers/hat/repo/repo.go b/routers/hat/repo/repo.go index 9e82d44..8bb6d31 100644 --- a/routers/hat/repo/repo.go +++ b/routers/hat/repo/repo.go @@ -2,7 +2,9 @@ package repo import ( "fmt" + "math" "net/http" + "strconv" "strings" access_model "code.gitea.io/gitea/models/perm/access" @@ -15,8 +17,10 @@ import ( gitea_git "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/gitdiff" hat_activities_models "code.gitlink.org.cn/Gitlink/gitea_hat.git/models/activities" + hat_git "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/git" ) func PrepareComapreDiff( @@ -549,3 +553,34 @@ func GetCommitCount(ctx *context.APIContext) { }) } + +func ListCodeStats(ctx *context.APIContext) { + if ctx.Repo.Repository.IsEmpty { + ctx.NotFound() + return + } + + listOpts := utils.GetListOptions(ctx) + + ref := ctx.FormString("ref") + // if ref == "" { + // ref = ctx.Repo.Repository.DefaultBranch + // } + + total, stats, err := hat_git.GetCodeActivityStatsWithoutSince(ctx.Repo.GitRepo, ref, listOpts.Page, listOpts.PageSize) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetCodeActivityStatsWithoutSince", err) + return + } + + pageCount := int(math.Ceil(float64(total) / float64(listOpts.PageSize))) + ctx.Resp.Header().Set("X-Page", strconv.Itoa(listOpts.Page)) + ctx.Resp.Header().Set("X-PerPage", strconv.Itoa(listOpts.PageSize)) + ctx.Resp.Header().Set("X-Total", strconv.FormatInt(total, 10)) + ctx.Resp.Header().Set("X-PageCount", strconv.Itoa(pageCount)) + ctx.Resp.Header().Set("X-HasMore", strconv.FormatBool(listOpts.Page < pageCount)) + + ctx.SetLinkHeader(int(total), listOpts.PageSize) + ctx.Resp.Header().Set("X-Total-Count", fmt.Sprintf("%d", total)) + ctx.JSON(http.StatusOK, stats) +}