diff --git a/AGENTS.md b/AGENTS.md index d4f25d08c82..7d1b5aa5efb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,7 @@ - In `options/locale`, only edit `locale_en-US.json`, other locales are synced automatically - In TS, use `!` instead of `?.`/`??` when a value always exists - In Go, prefer to use modern language features wherever possible +- In Go, function-name prefixes in errors like `fmt.Errorf("Foo: %w", err)` must always name the function they are in - Write sizes as multiplications like `64 * 1024`, not bit shifts like `64 << 10` - Prefer `tw-*` utilities over inline `style` and `flex-*` helpers over per-child `tw-ml-*`/`tw-mr-*` margins, falling back to `tw-*` where specificity requires `!important` - Run `make fmt` after `.go` edits, `make tidy` after `go.mod` edits, `make generate-swagger` after API changes, and lint what changed with `make lint-go`, `lint-js`, `lint-css` or `lint-templates` diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 2e3af13db52..06a9dbb7e44 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -83,23 +83,17 @@ func GetActivityStats(ctx context.Context, repo *repo_model.Repository, timeFrom // GetActivityStatsTopAuthors returns top author stats for git commits for all branches func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository, timeFrom time.Time, count int) ([]*ActivityAuthorData, error) { - gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return nil, fmt.Errorf("OpenRepository: %w", err) - } - defer closer.Close() - - code, err := gitRepo.GetCodeActivityStats(ctx, timeFrom, "") + authors, err := git.GetCodeActivityAuthors(ctx, repo, timeFrom) if err != nil { return nil, fmt.Errorf("FillFromGit: %w", err) } - if code.Authors == nil { + if authors == nil { return nil, nil } users := make(map[int64]*ActivityAuthorData) var unknownUserID int64 unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx) - for _, v := range code.Authors { + for _, v := range authors { if len(v.Email) == 0 { continue } diff --git a/modules/git/log_name_status_nogogit.go b/modules/git/log_name_status_nogogit.go index acb0860b4ab..a6469fadaa6 100644 --- a/modules/git/log_name_status_nogogit.go +++ b/modules/git/log_name_status_nogogit.go @@ -23,26 +23,17 @@ import ( // logNameStatusRepo opens git log --raw in the provided repo and returns a parser func logNameStatusRepo(ctx context.Context, repo RepositoryFacade, head, treepath string, paths ...string) *logNameStatusRepoParser { cmd := gitcmd.NewCommand() - cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head) + cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "--no-follow", "-t", "-z").AddDynamicArguments(head) var files []string - if len(paths) < 70 { - if treepath != "" { - files = append(files, treepath) - for _, pth := range paths { - if pth != "" { - files = append(files, path.Join(treepath, pth)) - } - } - } else { - for _, pth := range paths { - if pth != "" { - files = append(files, pth) - } + if treepath != "" { + files = append(files, treepath) // matches all entries, adding them would not narrow the log + } else if len(paths) < 70 { + for _, pth := range paths { + if pth != "" { + files = append(files, pth) } } - } else if treepath != "" { - files = append(files, treepath) } // Use the :(literal) pathspec magic to handle edge cases with files named like ":file.txt" or "*.jpg" for i, file := range files { diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index c512529cfe7..5acfd33c92b 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -61,10 +61,28 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time. } else { gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch) } + if err := parseCodeActivityLog(ctx, repo, gitCmd, stats); err != nil { + return nil, err + } + return stats, nil +} + +// GetCodeActivityAuthors returns the authors of non-merge commits in all branches since fromTime, sorted by commit count +func GetCodeActivityAuthors(ctx context.Context, repo RepositoryFacade, fromTime time.Time) ([]*CodeActivityAuthor, error) { + stats := &CodeActivityStats{} + gitCmd := gitcmd.NewCommand("log", "--no-merges", "--pretty=format:---%n%H%n%aN%n%aE%n", "--branches=*"). + AddOptionFormat("--since=%s", fromTime.Format(time.RFC3339)) + if err := parseCodeActivityLog(ctx, repo, gitCmd, stats); err != nil { + return nil, err + } + return stats.Authors, nil +} + +func parseCodeActivityLog(ctx context.Context, repo RepositoryFacade, gitCmd *gitcmd.Command, stats *CodeActivityStats) error { stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe() defer stdoutReaderClose() - err = gitCmd. + err := gitCmd. WithRepo(repo). WithPipelineFunc(func(ctx gitcmd.Context) error { scanner := bufio.NewScanner(stdoutReader) @@ -116,8 +134,8 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time. } } } - if err = scanner.Err(); err != nil { - return fmt.Errorf("GetCodeActivityStats scan: %w", err) + if err := scanner.Err(); err != nil { + return fmt.Errorf("parseCodeActivityLog scan: %w", err) } a := make([]*CodeActivityAuthor, 0, len(authors)) for _, v := range authors { @@ -134,8 +152,7 @@ func (repo *Repository) GetCodeActivityStats(ctx context.Context, fromTime time. }). RunWithStderr(ctx) if err != nil { - return nil, fmt.Errorf("GetCodeActivityStats: %w", err) + return fmt.Errorf("parseCodeActivityLog: %w", err) } - - return stats, nil + return nil } diff --git a/modules/git/repo_stats_test.go b/modules/git/repo_stats_test.go index c8c6c7da1bc..c998fd46a5c 100644 --- a/modules/git/repo_stats_test.go +++ b/modules/git/repo_stats_test.go @@ -33,4 +33,8 @@ func TestRepository_GetCodeActivityStats(t *testing.T) { assert.Equal(t, "tris.git@shoddynet.org", code.Authors[1].Email) assert.EqualValues(t, 3, code.Authors[1].Commits) assert.EqualValues(t, 5, code.Authors[0].Commits) + + authors, err := GetCodeActivityAuthors(t.Context(), bareRepo1, timeFrom) + assert.NoError(t, err) + assert.Equal(t, code.Authors, authors) }