From 7373467ffd148bcb6be06b48556cac023814fc87 Mon Sep 17 00:00:00 2001 From: Robin Bohrer Date: Mon, 30 Dec 2024 17:49:57 +0100 Subject: [PATCH] feat(stats): add repository filtering to stats command Add ability to filter stats for a specific repository using the stats command. This allows users to focus on activity metrics for a single repository. Changes: - Update stats command to accept optional repository argument - Add repository filtering logic in DisplayStats and related functions - Update CLI help text and documentation with new feature - Add comprehensive tests for repository filtering functionality Example usage: streakode stats myproject Breaking changes: none --- FEATURE_PLAN.md | 3 - README.md | 5 +- cmd/stats.go | 211 +++++++++++++++++++++++++++------------------- cmd/stats_test.go | 206 +++++++++++++++++++++++++++++++++++--------- main.go | 27 ++++-- 5 files changed, 315 insertions(+), 137 deletions(-) diff --git a/FEATURE_PLAN.md b/FEATURE_PLAN.md index e4340ee..3ee127c 100644 --- a/FEATURE_PLAN.md +++ b/FEATURE_PLAN.md @@ -5,9 +5,6 @@ This document outlines new and additional features that are planned or already i --- ## IDEAS -- add optional "repository" argument to stats command - -> i.e.: streakode stats streakode -> to get more detailed stats about just the streakode repository - - add optional "author" argument to the author command -> i.e.: streakode author AccursedGalaxy -> to get detailed stats about this author (currently just shows author set from the config to verify it's setup correctly, but can easily expand this to show detailed stats abou the author as well) diff --git a/README.md b/README.md index 4ddafd2..212d449 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,12 @@ streakode version # Check your Git author configuration streakode author -# Show statistics +# Show statistics for all repositories streakode stats +# Show statistics for a specific repository +streakode stats myproject + # Show statistics with debug output streakode stats --debug # or diff --git a/cmd/stats.go b/cmd/stats.go index 652c25b..eee60a0 100644 --- a/cmd/stats.go +++ b/cmd/stats.go @@ -16,44 +16,44 @@ import ( ) type repoInfo struct { - name string - metadata scan.RepoMetadata - lastCommit time.Time + name string + metadata scan.RepoMetadata + lastCommit time.Time } type CommitTrend struct { - indicator string - text string + indicator string + text string } type LanguageStats map[string]int type HourStats map[int]int const ( - defaultTerminalWidth = 80 - maxTableWidth = 120 - hoursInDay = 24 - daysInWeek = 7 + defaultTerminalWidth = 80 + maxTableWidth = 120 + hoursInDay = 24 + daysInWeek = 7 ) var calculator = &DefaultStatsCalculator{} func (c *DefaultStatsCalculator) CalculateCommitTrend(current int, previous int) CommitTrend { - diff := current - previous - switch { - case diff > 0: - return CommitTrend{"↗️", fmt.Sprintf("up %d", diff)} - case diff < 0: - return CommitTrend{"↘️", fmt.Sprintf("down %d", -diff)} - default: - return CommitTrend{"-", ""} - } + diff := current - previous + switch { + case diff > 0: + return CommitTrend{"↗️", fmt.Sprintf("up %d", diff)} + case diff < 0: + return CommitTrend{"↘️", fmt.Sprintf("down %d", -diff)} + default: + return CommitTrend{"-", ""} + } } -// DisplayStats - Displays stats for all active repositories in a more compact format -func DisplayStats() { +// DisplayStats - Displays stats for all active repositories or a specific repository +func DisplayStats(targetRepo string) { // Get table width from the rendered table first - projectsSection := buildProjectsSection() + projectsSection := buildProjectsSection(targetRepo) tableLines := strings.Split(projectsSection, "\n") if len(tableLines) == 0 { return @@ -76,12 +76,14 @@ func DisplayStats() { // Header section if config.AppConfig.DisplayStats.ShowWelcomeMessage { headerText := fmt.Sprintf("🚀 %s's Coding Activity", config.AppConfig.Author) + if targetRepo != "" { + headerText = fmt.Sprintf("🚀 %s's Activity in %s", config.AppConfig.Author, targetRepo) + } padding := (tableWidth - len([]rune(headerText))) / 2 centeredHeader := fmt.Sprintf("%*s%s%*s", padding, "", headerText, padding, "") sections = append(sections, headerStyle.Render(centeredHeader)) } - // Active projects section (table) if config.AppConfig.DisplayStats.ShowActiveProjects && projectsSection != "" { sections = append(sections, projectsSection) @@ -89,7 +91,7 @@ func DisplayStats() { // Insights section if config.AppConfig.DisplayStats.ShowInsights { - insights := buildInsightsSection() + insights := buildInsightsSection(targetRepo) if insights != "" { sections = append(sections, insights) } @@ -124,11 +126,11 @@ func DisplayStats() { } func (c *DefaultStatsCalculator) CalculateTableWidth() int { - width, _, err := term.GetSize(0) - if err != nil { - width = defaultTerminalWidth - } - return min(width-2, maxTableWidth) + width, _, err := term.GetSize(0) + if err != nil { + width = defaultTerminalWidth + } + return min(width-2, maxTableWidth) } // prepareRepoData converts the cache map into a sorted slice of repository information @@ -154,7 +156,7 @@ func prepareRepoData() []repoInfo { // initializeTable creates and configures a new table writer with proper settings func initializeTable(tableWidth int) table.Writer { t := table.NewWriter() - + // Configure table column widths t.SetColumnConfigs([]table.ColumnConfig{ {Number: 1, WidthMax: int(float64(tableWidth) * 0.35)}, // Repository name @@ -198,13 +200,13 @@ func formatActivityIndicator(weeklyCommits int) string { func formatStreakString(currentStreak, longestStreak int) string { indicators := config.AppConfig.DisplayStats.ActivityIndicators streakStr := fmt.Sprintf("%dd", currentStreak) - + if currentStreak == longestStreak && currentStreak > 0 { streakStr += indicators.StreakRecord } else if currentStreak > 0 { streakStr += indicators.ActiveStreak } - + return streakStr } @@ -212,14 +214,14 @@ func formatStreakString(currentStreak, longestStreak int) string { func calculateWeeklyChanges(commitHistory []scan.CommitHistory) (int, int) { var weeklyAdditions, weeklyDeletions int weekStart := time.Now().AddDate(0, 0, -daysInWeek) - + for _, commit := range commitHistory { if commit.Date.After(weekStart) { weeklyAdditions += commit.Additions weeklyDeletions += commit.Deletions } } - + return weeklyAdditions, weeklyDeletions } @@ -231,18 +233,34 @@ func formatLastActivity(lastCommit time.Time) string { return "today" } -// buildProjectsSection - Displays stats for all active repositories in a more compact format -func buildProjectsSection() string { +// buildProjectsSection - Displays stats for all active repositories or a specific repository +func buildProjectsSection(targetRepo string) string { if !config.AppConfig.DisplayStats.ShowActiveProjects { return "" } // Create buffer for table buf := new(bytes.Buffer) - + // Get sorted repo data repos := prepareRepoData() - + + // Filter for target repository if specified + if targetRepo != "" { + filteredRepos := make([]repoInfo, 0) + for _, repo := range repos { + if repo.name == targetRepo { + filteredRepos = append(filteredRepos, repo) + break + } + } + if len(filteredRepos) == 0 { + fmt.Printf("Repository '%s' not found in cache. Run 'streakode cache reload' to update cache.\n", targetRepo) + return "" + } + repos = filteredRepos + } + // Initialize table with proper width tableWidth := calculator.CalculateTableWidth() t := initializeTable(tableWidth) @@ -290,21 +308,21 @@ func buildProjectsSection() string { func formatLanguages(stats map[string]int, topCount int) string { // Language icons mapping with more descriptive emojis languageIcons := map[string]string{ - "go": config.AppConfig.LanguageSettings.LanguageDisplay.GoDisplay, - "py": config.AppConfig.LanguageSettings.LanguageDisplay.PythonDisplay, - "lua": config.AppConfig.LanguageSettings.LanguageDisplay.LuaDisplay, - "js": config.AppConfig.LanguageSettings.LanguageDisplay.JavaDisplay, - "ts": config.AppConfig.LanguageSettings.LanguageDisplay.TypeScriptDisplay, - "rust": config.AppConfig.LanguageSettings.LanguageDisplay.RustDisplay, - "cpp": config.AppConfig.LanguageSettings.LanguageDisplay.CppDisplay, - "c": config.AppConfig.LanguageSettings.LanguageDisplay.CDisplay, - "java": config.AppConfig.LanguageSettings.LanguageDisplay.JavaDisplay, - "ruby": config.AppConfig.LanguageSettings.LanguageDisplay.RubyDisplay, - "php": config.AppConfig.LanguageSettings.LanguageDisplay.PHPDisplay, - "html": config.AppConfig.LanguageSettings.LanguageDisplay.HTMLDisplay, - "css": config.AppConfig.LanguageSettings.LanguageDisplay.CSSDisplay, - "shell": config.AppConfig.LanguageSettings.LanguageDisplay.ShellDisplay, - "default": config.AppConfig.LanguageSettings.LanguageDisplay.DefaultDisplay, + "go": config.AppConfig.LanguageSettings.LanguageDisplay.GoDisplay, + "py": config.AppConfig.LanguageSettings.LanguageDisplay.PythonDisplay, + "lua": config.AppConfig.LanguageSettings.LanguageDisplay.LuaDisplay, + "js": config.AppConfig.LanguageSettings.LanguageDisplay.JavaDisplay, + "ts": config.AppConfig.LanguageSettings.LanguageDisplay.TypeScriptDisplay, + "rust": config.AppConfig.LanguageSettings.LanguageDisplay.RustDisplay, + "cpp": config.AppConfig.LanguageSettings.LanguageDisplay.CppDisplay, + "c": config.AppConfig.LanguageSettings.LanguageDisplay.CDisplay, + "java": config.AppConfig.LanguageSettings.LanguageDisplay.JavaDisplay, + "ruby": config.AppConfig.LanguageSettings.LanguageDisplay.RubyDisplay, + "php": config.AppConfig.LanguageSettings.LanguageDisplay.PHPDisplay, + "html": config.AppConfig.LanguageSettings.LanguageDisplay.HTMLDisplay, + "css": config.AppConfig.LanguageSettings.LanguageDisplay.CSSDisplay, + "shell": config.AppConfig.LanguageSettings.LanguageDisplay.ShellDisplay, + "default": config.AppConfig.LanguageSettings.LanguageDisplay.DefaultDisplay, } // Convert map to slice for sorting @@ -324,16 +342,16 @@ func formatLanguages(stats map[string]int, topCount int) string { return langs[i].lines > langs[j].lines }) - // Calculate size needed for formatted slice - size := 0 - for i := 0; i < min(len(langs), topCount); i++ { - if langs[i].lines > 0 { - size++ - } - } + // Calculate size needed for formatted slice + size := 0 + for i := 0; i < min(len(langs), topCount); i++ { + if langs[i].lines > 0 { + size++ + } + } // Format languages with icons and better number formatting - formatted := make([]string, 0, size) + formatted := make([]string, 0, size) for i := 0; i < min(len(langs), topCount); i++ { if langs[i].lines > 0 { // Retrieve icon or default if not found @@ -363,29 +381,29 @@ func formatLanguages(stats map[string]int, topCount int) string { } func getTableStyle() table.Style { - return table.Style{ - Options: table.Options{ - DrawBorder: config.AppConfig.DisplayStats.TableStyle.Options.DrawBorder, - SeparateColumns: config.AppConfig.DisplayStats.TableStyle.Options.SeparateColumns, - SeparateHeader: config.AppConfig.DisplayStats.TableStyle.Options.SeparateHeader, - SeparateRows: config.AppConfig.DisplayStats.TableStyle.Options.SeparateRows, - }, - Box: table.BoxStyle{ - PaddingLeft: "", - PaddingRight: " ", - MiddleVertical: "", - }, - } + return table.Style{ + Options: table.Options{ + DrawBorder: config.AppConfig.DisplayStats.TableStyle.Options.DrawBorder, + SeparateColumns: config.AppConfig.DisplayStats.TableStyle.Options.SeparateColumns, + SeparateHeader: config.AppConfig.DisplayStats.TableStyle.Options.SeparateHeader, + SeparateRows: config.AppConfig.DisplayStats.TableStyle.Options.SeparateRows, + }, + Box: table.BoxStyle{ + PaddingLeft: "", + PaddingRight: " ", + MiddleVertical: "", + }, + } } func (c *DefaultStatsCalculator) ProcessLanguageStats(cache map[string]scan.RepoMetadata) map[string]int { - languageStats := make(map[string]int) - for _, repo := range cache { - for lang, lines := range repo.Languages { - languageStats[lang] += lines - } - } - return languageStats + languageStats := make(map[string]int) + for _, repo := range cache { + for lang, lines := range repo.Languages { + languageStats[lang] += lines + } + } + return languageStats } // calculateGlobalStats calculates overall statistics across all repositories @@ -515,7 +533,7 @@ func buildSimpleInsights(repos map[string]scan.RepoMetadata) string { } // buildInsightsSection - Displays insights about coding activity -func buildInsightsSection() string { +func buildInsightsSection(targetRepo string) string { if !config.AppConfig.DisplayStats.ShowInsights { return "" } @@ -524,24 +542,41 @@ func buildInsightsSection() string { tableWidth := calculator.CalculateTableWidth() insights := config.AppConfig.DisplayStats.InsightSettings + // Filter cache for target repository if specified + var repoCache map[string]scan.RepoMetadata + repoCache = cache.Cache + if targetRepo != "" { + filteredCache := make(map[string]scan.RepoMetadata) + for path, repo := range cache.Cache { + if strings.HasSuffix(path, "/"+targetRepo) { + filteredCache[path] = repo + break + } + } + if len(filteredCache) == 0 { + return "" + } + repoCache = filteredCache + } + if config.AppConfig.DetailedStats { t := table.NewWriter() t.SetStyle(getTableStyle()) - t.SetAllowedRowLength(tableWidth-2) + t.SetAllowedRowLength(tableWidth - 2) // Calculate all stats - weeklyCommits, lastWeeksCommits, _, additions, deletions, hourStats := calculateGlobalStats(cache.Cache) + weeklyCommits, lastWeeksCommits, _, additions, deletions, hourStats := calculateGlobalStats(repoCache) peakHour, peakCommits := findPeakCodingHour(hourStats) commitTrend := calculator.CalculateCommitTrend(weeklyCommits, lastWeeksCommits) - languageStats := calculator.ProcessLanguageStats(cache.Cache) + languageStats := calculator.ProcessLanguageStats(repoCache) // Append rows based on configuration appendInsightRows(t, insights, insightStats{ weeklyCommits: weeklyCommits, additions: additions, deletions: deletions, - peakHour: peakHour, - peakCommits: peakCommits, + peakHour: peakHour, + peakCommits: peakCommits, commitTrend: commitTrend, languageStats: languageStats, }) @@ -549,9 +584,9 @@ func buildInsightsSection() string { return t.Render() } - return buildSimpleInsights(cache.Cache) + return buildSimpleInsights(repoCache) } func (rc *DefaultRepoCache) GetRepos() map[string]scan.RepoMetadata { - return rc.cache + return rc.cache } diff --git a/cmd/stats_test.go b/cmd/stats_test.go index 9654663..1f81c81 100644 --- a/cmd/stats_test.go +++ b/cmd/stats_test.go @@ -12,7 +12,7 @@ import ( func TestCalculateCommitTrend(t *testing.T) { calculator := &DefaultStatsCalculator{} - + tests := []struct { name string current int @@ -49,7 +49,7 @@ func TestCalculateCommitTrend(t *testing.T) { func TestProcessLanguageStats(t *testing.T) { calculator := &DefaultStatsCalculator{} - + testCache := map[string]scan.RepoMetadata{ "repo1": { Languages: map[string]int{ @@ -88,22 +88,22 @@ func TestBuildProjectsSection(t *testing.T) { DisplayStats: struct { ShowWelcomeMessage bool `mapstructure:"show_welcome_message"` ShowActiveProjects bool `mapstructure:"show_active_projects"` - ShowInsights bool `mapstructure:"show_insights"` - MaxProjects int `mapstructure:"max_projects"` - TableStyle struct { - UseTableHeader bool `mapstructure:"use_table_header"` - Style string `mapstructure:"style"` - Options struct { - DrawBorder bool `mapstructure:"draw_border"` - SeparateColumns bool `mapstructure:"separate_columns"` - SeparateHeader bool `mapstructure:"separate_header"` - SeparateRows bool `mapstructure:"separate_rows"` + ShowInsights bool `mapstructure:"show_insights"` + MaxProjects int `mapstructure:"max_projects"` + TableStyle struct { + UseTableHeader bool `mapstructure:"use_table_header"` + Style string `mapstructure:"style"` + Options struct { + DrawBorder bool `mapstructure:"draw_border"` + SeparateColumns bool `mapstructure:"separate_columns"` + SeparateHeader bool `mapstructure:"separate_header"` + SeparateRows bool `mapstructure:"separate_rows"` } `mapstructure:"options"` } `mapstructure:"table_style"` ActivityIndicators struct { - HighActivity string `mapstructure:"high_activity"` - NormalActivity string `mapstructure:"normal_activity"` - NoActivity string `mapstructure:"no_activity"` + HighActivity string `mapstructure:"high_activity"` + NormalActivity string `mapstructure:"normal_activity"` + NoActivity string `mapstructure:"no_activity"` StreakRecord string `mapstructure:"streak_record"` ActiveStreak string `mapstructure:"active_streak"` } `mapstructure:"activity_indicators"` @@ -121,32 +121,32 @@ func TestBuildProjectsSection(t *testing.T) { } `mapstructure:"insight_settings"` }{ ShowActiveProjects: true, - MaxProjects: 10, + MaxProjects: 10, TableStyle: struct { - UseTableHeader bool `mapstructure:"use_table_header"` - Style string `mapstructure:"style"` - Options struct { - DrawBorder bool `mapstructure:"draw_border"` - SeparateColumns bool `mapstructure:"separate_columns"` - SeparateHeader bool `mapstructure:"separate_header"` - SeparateRows bool `mapstructure:"separate_rows"` + UseTableHeader bool `mapstructure:"use_table_header"` + Style string `mapstructure:"style"` + Options struct { + DrawBorder bool `mapstructure:"draw_border"` + SeparateColumns bool `mapstructure:"separate_columns"` + SeparateHeader bool `mapstructure:"separate_header"` + SeparateRows bool `mapstructure:"separate_rows"` } `mapstructure:"options"` }{ UseTableHeader: true, Options: struct { - DrawBorder bool `mapstructure:"draw_border"` - SeparateColumns bool `mapstructure:"separate_columns"` - SeparateHeader bool `mapstructure:"separate_header"` - SeparateRows bool `mapstructure:"separate_rows"` + DrawBorder bool `mapstructure:"draw_border"` + SeparateColumns bool `mapstructure:"separate_columns"` + SeparateHeader bool `mapstructure:"separate_header"` + SeparateRows bool `mapstructure:"separate_rows"` }{ - DrawBorder: true, + DrawBorder: true, SeparateColumns: true, }, }, ActivityIndicators: struct { - HighActivity string `mapstructure:"high_activity"` - NormalActivity string `mapstructure:"normal_activity"` - NoActivity string `mapstructure:"no_activity"` + HighActivity string `mapstructure:"high_activity"` + NormalActivity string `mapstructure:"normal_activity"` + NoActivity string `mapstructure:"no_activity"` StreakRecord string `mapstructure:"streak_record"` ActiveStreak string `mapstructure:"active_streak"` }{ @@ -158,17 +158,147 @@ func TestBuildProjectsSection(t *testing.T) { mockCache := &MockRepoCache{ repos: map[string]scan.RepoMetadata{ "test-repo": { - WeeklyCommits: 10, - CurrentStreak: 5, + WeeklyCommits: 10, + CurrentStreak: 5, LongestStreak: 7, LastCommit: time.Now(), // Need this for sorting }, + "another-repo": { + WeeklyCommits: 15, + CurrentStreak: 3, + LongestStreak: 10, + LastCommit: time.Now().Add(-24 * time.Hour), // 1 day ago + }, }, } cache.Cache = mockCache.GetRepos() - output := buildProjectsSection() - assert.Contains(t, output, "test-repo") - assert.Contains(t, output, "10") - assert.Contains(t, output, "5d") -} \ No newline at end of file + + // Test all repositories (no filter) + t.Run("all repositories", func(t *testing.T) { + output := buildProjectsSection("") + assert.Contains(t, output, "test-repo") + assert.Contains(t, output, "another-repo") + assert.Contains(t, output, "10") + assert.Contains(t, output, "15") + }) + + // Test single repository filter + t.Run("single repository", func(t *testing.T) { + output := buildProjectsSection("test-repo") + assert.Contains(t, output, "test-repo") + assert.NotContains(t, output, "another-repo") + assert.Contains(t, output, "10") + assert.NotContains(t, output, "15") + }) + + // Test non-existent repository + t.Run("non-existent repository", func(t *testing.T) { + output := buildProjectsSection("non-existent-repo") + assert.Empty(t, output) + }) +} + +func TestBuildInsightsSection(t *testing.T) { + config.AppConfig = config.Config{ + DisplayStats: struct { + ShowWelcomeMessage bool `mapstructure:"show_welcome_message"` + ShowActiveProjects bool `mapstructure:"show_active_projects"` + ShowInsights bool `mapstructure:"show_insights"` + MaxProjects int `mapstructure:"max_projects"` + TableStyle struct { + UseTableHeader bool `mapstructure:"use_table_header"` + Style string `mapstructure:"style"` + Options struct { + DrawBorder bool `mapstructure:"draw_border"` + SeparateColumns bool `mapstructure:"separate_columns"` + SeparateHeader bool `mapstructure:"separate_header"` + SeparateRows bool `mapstructure:"separate_rows"` + } `mapstructure:"options"` + } `mapstructure:"table_style"` + ActivityIndicators struct { + HighActivity string `mapstructure:"high_activity"` + NormalActivity string `mapstructure:"normal_activity"` + NoActivity string `mapstructure:"no_activity"` + StreakRecord string `mapstructure:"streak_record"` + ActiveStreak string `mapstructure:"active_streak"` + } `mapstructure:"activity_indicators"` + Thresholds struct { + HighActivity int `mapstructure:"high_activity"` + } `mapstructure:"thresholds"` + InsightSettings struct { + TopLanguagesCount int `mapstructure:"top_languages_count"` + ShowDailyAverage bool `mapstructure:"show_daily_average"` + ShowTopLanguages bool `mapstructure:"show_top_languages"` + ShowPeakCoding bool `mapstructure:"show_peak_coding"` + ShowWeeklySummary bool `mapstructure:"show_weekly_summary"` + ShowWeeklyGoal bool `mapstructure:"show_weekly_goal"` + ShowMostActive bool `mapstructure:"show_most_active"` + } `mapstructure:"insight_settings"` + }{ + ShowInsights: true, + InsightSettings: struct { + TopLanguagesCount int `mapstructure:"top_languages_count"` + ShowDailyAverage bool `mapstructure:"show_daily_average"` + ShowTopLanguages bool `mapstructure:"show_top_languages"` + ShowPeakCoding bool `mapstructure:"show_peak_coding"` + ShowWeeklySummary bool `mapstructure:"show_weekly_summary"` + ShowWeeklyGoal bool `mapstructure:"show_weekly_goal"` + ShowMostActive bool `mapstructure:"show_most_active"` + }{ + ShowWeeklySummary: true, + ShowDailyAverage: true, + }, + }, + DetailedStats: true, + } + + mockCache := &MockRepoCache{ + repos: map[string]scan.RepoMetadata{ + "/path/to/test-repo": { + WeeklyCommits: 10, + LastWeeksCommits: 8, + CommitHistory: []scan.CommitHistory{ + { + Date: time.Now().Add(-24 * time.Hour), + Additions: 100, + Deletions: 50, + }, + }, + }, + "/path/to/another-repo": { + WeeklyCommits: 15, + LastWeeksCommits: 12, + CommitHistory: []scan.CommitHistory{ + { + Date: time.Now().Add(-48 * time.Hour), + Additions: 200, + Deletions: 100, + }, + }, + }, + }, + } + + cache.Cache = mockCache.GetRepos() + + // Test all repositories (no filter) + t.Run("all repositories", func(t *testing.T) { + output := buildInsightsSection("") + assert.Contains(t, output, "25 commits") // Total weekly commits (10 + 15) + assert.Contains(t, output, "3.6 commits") // Daily average (25/7) + }) + + // Test single repository filter + t.Run("single repository", func(t *testing.T) { + output := buildInsightsSection("test-repo") + assert.Contains(t, output, "10 commits") // Only test-repo weekly commits + assert.Contains(t, output, "1.4 commits") // Daily average (10/7) + }) + + // Test non-existent repository + t.Run("non-existent repository", func(t *testing.T) { + output := buildInsightsSection("non-existent-repo") + assert.Empty(t, output) + }) +} diff --git a/main.go b/main.go index 177662d..9ce20d0 100644 --- a/main.go +++ b/main.go @@ -97,8 +97,8 @@ func main() { ) rootCmd := &cobra.Command{ - Use: "streakode", - Short: "A Git activity tracker for monitoring coding streaks", + Use: "streakode", + Short: "A Git activity tracker for monitoring coding streaks", Version: Version, PersistentPreRun: func(cmd *cobra.Command, args []string) { // Load the state first to get the active profile @@ -131,10 +131,23 @@ func main() { rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "Enable debug mode") statsCmd := &cobra.Command{ - Use: "stats", - Short: "Display stats for all active repositories", + Use: "stats [repository]", + Short: "Display stats for all active repositories or a specific repository", + Long: `Display Git activity statistics for your repositories. + +Without arguments, shows stats for all active repositories. +With a repository name argument, shows detailed stats for just that repository. + +Example: + streakode stats # Show stats for all repositories + streakode stats myproject # Show stats for only the myproject repository`, + Args: cobra.MaximumNArgs(1), Run: func(cobraCmd *cobra.Command, args []string) { - cmd.DisplayStats() + var targetRepo string + if len(args) > 0 { + targetRepo = args[0] + } + cmd.DisplayStats(targetRepo) }, } @@ -230,7 +243,7 @@ func main() { os.Exit(1) } - // Validate the config + // Validate the config if err := newConfig.ValidateConfig(); err != nil { fmt.Printf("Error: Invalid configuration for profile '%s': %v\n", newProfile, err) os.Exit(1) @@ -270,7 +283,7 @@ func main() { }, } - authorCmd := &cobra.Command{ + authorCmd := &cobra.Command{ Use: "author", Short: "Show configured Git author information", Run: func(cmd *cobra.Command, args []string) {