diff --git a/cmd/gt/main.go b/cmd/gt/main.go index 036e192..ca636a7 100644 --- a/cmd/gt/main.go +++ b/cmd/gt/main.go @@ -109,6 +109,12 @@ func main() { Usage: "run linter", Action: withManager(cmdLint), }, + { + Name: "authors", + Aliases: []string{"a"}, + Usage: "list all commit authors", + Action: withManager(cmdAuthors), + }, { Name: "hooks", Aliases: []string{"h"}, @@ -254,6 +260,43 @@ func cmdLint(ctx context.Context, c *cli.Command, m *repomanager.Manager) error return nil } +func cmdAuthors(ctx context.Context, c *cli.Command, m *repomanager.Manager) error { + authors, err := m.GetAuthors() + if err != nil { + return fmt.Errorf("cannot get authors: %w", err) + } + + if len(authors) == 0 { + fmt.Println("No authors found") + return nil + } + + // Calculate max widths for better formatting + maxNameLen := 0 + maxEmailLen := 0 + for _, author := range authors { + if len(author.Name) > maxNameLen { + maxNameLen = len(author.Name) + } + if len(author.Email) > maxEmailLen { + maxEmailLen = len(author.Email) + } + } + + // Print header + fmt.Printf("%-*s | %-*s | %s\n", maxNameLen, "Name", maxEmailLen, "Email", "Commits") + fmt.Printf("%s-+-%s-+-%s\n", strings.Repeat("-", maxNameLen), strings.Repeat("-", maxEmailLen), strings.Repeat("-", 7)) + + // Print authors + for _, author := range authors { + fmt.Printf("%-*s | %-*s | %d\n", maxNameLen, author.Name, maxEmailLen, author.Email, author.Count) + } + + fmt.Printf("\nTotal authors: %d\n", len(authors)) + + return nil +} + func cmdHooksList(ctx context.Context, c *cli.Command, m *repomanager.Manager) error { fmt.Println("commit-msg") diff --git a/internal/repo-manager/manager.go b/internal/repo-manager/manager.go index 41c161c..04158c0 100644 --- a/internal/repo-manager/manager.go +++ b/internal/repo-manager/manager.go @@ -3,6 +3,7 @@ package repomanager import ( "errors" "fmt" + "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/storer" "github.com/kazhuravlev/optional" "sort" @@ -248,3 +249,53 @@ func (m *Manager) GetCurrentBranch() (string, error) { return "", fmt.Errorf("HEAD is not pointing to a branch") } + +type Author struct { + Name string + Email string + Count int +} + +func (m *Manager) GetAuthors() ([]Author, error) { + commitIter, err := m.repo.CommitObjects() + if err != nil { + return nil, fmt.Errorf("get commit objects: %w", err) + } + + authorMap := make(map[string]*Author) + + err = commitIter.ForEach(func(c *object.Commit) error { + email := c.Author.Email + if _, exists := authorMap[email]; !exists { + authorMap[email] = &Author{ + Name: c.Author.Name, + Email: email, + Count: 0, + } + } + authorMap[email].Count++ + + // Update name if it's different (use the most recent name) + if authorMap[email].Name != c.Author.Name && c.Author.When.After(c.Committer.When) { + authorMap[email].Name = c.Author.Name + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("iterate commits: %w", err) + } + + // Convert map to slice + authors := make([]Author, 0, len(authorMap)) + for _, author := range authorMap { + authors = append(authors, *author) + } + + // Sort by commit count (descending) + sort.Slice(authors, func(i, j int) bool { + return authors[i].Count > authors[j].Count + }) + + return authors, nil +}