diff --git a/internal/ui/app.go b/internal/ui/app.go index 3d08a5b..6ccf38a 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -322,6 +322,8 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case AnalyzeRepoMsg: m.state = stateLoading m.loading.SetRepoName(msg.repoName) + m.progress = NewProgressTracker() + m.loading.SetProgress(m.progress) ctx, cancel := context.WithCancel(context.Background()) m.analysisCancel = cancel cmds = append(cmds, m.analyzeRepo(ctx, msg.repoName), TickProgressCmd()) @@ -489,6 +491,8 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input.input = repoName m.state = stateLoading m.loading.SetRepoName(repoName) + m.progress = NewProgressTracker() + m.loading.SetProgress(m.progress) ctx, cancel := context.WithCancel(context.Background()) m.analysisCancel = cancel cmds = append(cmds, m.analyzeRepo(ctx, repoName), TickProgressCmd()) @@ -534,6 +538,8 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input.input = repoName m.state = stateLoading m.loading.SetRepoName(repoName) + m.progress = NewProgressTracker() + m.loading.SetProgress(m.progress) ctx, cancel := context.WithCancel(context.Background()) m.analysisCancel = cancel cmds = append(cmds, m.analyzeRepo(ctx, repoName), TickProgressCmd()) @@ -765,6 +771,8 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.input.input = m.dashboard.data.Repo.FullName m.state = stateLoading m.loading.SetRepoName(m.input.input) + m.progress = NewProgressTracker() + m.loading.SetProgress(m.progress) ctx, cancel := context.WithCancel(context.Background()) m.analysisCancel = cancel cmds = append(cmds, m.analyzeRepo(ctx, m.input.input), TickProgressCmd()) @@ -945,6 +953,11 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { return fmt.Errorf("invalid repository URL: must be in owner/repo format or a valid GitHub URL") } + tracker := m.progress + if tracker == nil { + tracker = NewProgressTracker() + } + analysisType := normalizeAnalysisType(m.analysisType) cacheKey := analysisCacheKey(repoName, analysisType) @@ -954,6 +967,10 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { // Unmarshal cached analysis var result AnalysisResult if err := json.Unmarshal(entry.Analysis, &result); err == nil { + // Fast-forward tracker for cached result + for i := 0; i < 10; i++ { + tracker.NextStage() + } // Return cached result with status return CachedAnalysisResult{ Result: result, @@ -964,8 +981,6 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { } } - tracker := NewProgressTracker() - // Stage 1: Fetch repository token := "" if m.appConfig != nil { @@ -977,21 +992,21 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { if err != nil { return err } - tracker.NextStage() + tracker.NextStage() // Done Stage 1: Fetching metadata // Stage 2: Analyze commits commits, err := client.GetCommits(parts[0], parts[1], 365) if err != nil { return fmt.Errorf("failed to get commits: %w", err) } - tracker.NextStage() + tracker.NextStage() // Done Stage 2: Analyzing commits // Stage 3: Analyze contributors contributors, err := client.GetContributorsWithAvatars(parts[0], parts[1], 15) if err != nil { return fmt.Errorf("failed to get contributors: %w", err) } - tracker.NextStage() + tracker.NextStage() // Done Stage 3: Analyzing contributors // Stage 4: Analyze languages languages, err := client.GetLanguages(parts[0], parts[1]) @@ -1018,12 +1033,10 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { } // Compare with cached incremental metadata - changedFiles := []string{} - if m.cache != nil { if entry, found := m.cache.GetWithoutTTLExpiration(cacheKey); found { if entry.IncrementalMetadata != nil { - + changedFiles := []string{} for path, currentMeta := range currentHashes { cachedMeta, exists := entry.IncrementalMetadata[path] @@ -1033,14 +1046,14 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { } } - fmt.Printf("🔄 Incremental analysis enabled\n") - fmt.Printf("📂 Changed files detected: %d\n", len(changedFiles)) - // No changes detected if len(changedFiles) == 0 { - fmt.Println("✅ No repository changes detected. Using cached analysis.") var result AnalysisResult if err := json.Unmarshal(entry.Analysis, &result); err == nil { + // Fast-forward tracker + for i := 0; i < 10; i++ { + tracker.NextStage() + } // Return cached result with status return CachedAnalysisResult{ Result: result, @@ -1053,7 +1066,7 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { } } - tracker.NextStage() + tracker.NextStage() // Done Stage 4: Analyzing languages & files // Stage 5: Compute metrics score := analyzer.CalculateHealth(repo, commits) @@ -1077,6 +1090,8 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { false, ) + tracker.NextStage() // Done Stage 5: Computing health metrics + if analysisType == "quick" { qualityDashboard := analyzer.GenerateQualityDashboard( repo, @@ -1113,6 +1128,11 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { m.cache.SetWithMetadata(cacheKey, result, currentHashes) } + // Fast-forward remaining stages + for i := 0; i < 5; i++ { + tracker.NextStage() + } + AddAnalysisNotification(repoName, true) return result @@ -1126,6 +1146,7 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { if err := ctx.Err(); err != nil { return err } + tracker.NextStage() // Done Stage 6: Analyzing dependencies // Stage 7: Security vulnerability scan security, securityErr := analyzer.ScanDependencies(deps) @@ -1135,16 +1156,7 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { if err := ctx.Err(); err != nil { return err } - tracker.NextStage() - - // Mark complete - tracker.NextStage() - riskAlerts = analyzer.AnalyzeRiskAlerts( - busFactor, - score, - commitsLast90Days, - security != nil && security.CriticalCount > 0, - ) + tracker.NextStage() // Done Stage 7: Scanning for vulnerabilities // Analyze Hotspots hotspots, hotspotsErr := analyzer.AnalyzeHotspots(repo, commits, fileTree, client) @@ -1154,8 +1166,16 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { if err := ctx.Err(); err != nil { return err } + tracker.NextStage() // Done Stage 8: Analyzing hotspots // Generate quality dashboard + riskAlerts = analyzer.AnalyzeRiskAlerts( + busFactor, + score, + commitsLast90Days, + security != nil && security.CriticalCount > 0, + ) + qualityDashboard := analyzer.GenerateQualityDashboard( repo, commits, @@ -1207,6 +1227,8 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { } contribScore := contribution.Calculate(hasContributing, readmeContent, issues, commits, contributors) + tracker.NextStage() // Done Stage 9: Generating report + result := AnalysisResult{ Repo: repo, Commits: commits, @@ -1235,6 +1257,8 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd { m.cache.SetWithMetadata(cacheKey, result, currentHashes) } + tracker.NextStage() // Done Stage 10: Complete + // Add success notification AddAnalysisNotification(repoName, true) diff --git a/internal/ui/progress.go b/internal/ui/progress.go index c554f22..caa4117 100644 --- a/internal/ui/progress.go +++ b/internal/ui/progress.go @@ -1,8 +1,10 @@ package ui import ( - tea "github.com/charmbracelet/bubbletea" + "sync" "time" + + tea "github.com/charmbracelet/bubbletea" ) // ProgressStage represents a step in the analysis process @@ -17,6 +19,7 @@ type ProgressTracker struct { stages []ProgressStage current int startTime time.Time + mu sync.RWMutex } // ProgressUpdateMsg is sent to update progress @@ -80,17 +83,16 @@ var SatelliteFrames = []string{ func NewProgressTracker() *ProgressTracker { return &ProgressTracker{ stages: []ProgressStage{ - {Name: "🔗 Fetching repository data", IsComplete: false, IsActive: true}, - {Name: "🔗 Connecting to GitHub API", IsComplete: false, IsActive: false}, - {Name: "📝 Analyzing commits", IsComplete: false, IsActive: false}, - {Name: "📝 Processing commit history", IsComplete: false, IsActive: false}, + {Name: "🔗 Fetching repository metadata", IsComplete: false, IsActive: true}, + {Name: "📝 Analyzing commit history", IsComplete: false, IsActive: false}, {Name: "👥 Analyzing contributors", IsComplete: false, IsActive: false}, - {Name: "👥 Calculating contributor metrics", IsComplete: false, IsActive: false}, - {Name: "🗣️ Analyzing languages", IsComplete: false, IsActive: false}, - {Name: "🗣️ Processing language statistics", IsComplete: false, IsActive: false}, - {Name: "📊 Computing metrics", IsComplete: false, IsActive: false}, - {Name: "📊 Generating final report", IsComplete: false, IsActive: false}, - {Name: "✅ Analysis complete", IsComplete: false, IsActive: false}, + {Name: "🗣️ Analyzing languages & files", IsComplete: false, IsActive: false}, + {Name: "📊 Computing health metrics", IsComplete: false, IsActive: false}, + {Name: "📦 Analyzing dependencies", IsComplete: false, IsActive: false}, + {Name: "🛡️ Scanning for vulnerabilities", IsComplete: false, IsActive: false}, + {Name: "🔥 Analyzing hotspots", IsComplete: false, IsActive: false}, + {Name: "📑 Generating report", IsComplete: false, IsActive: false}, + {Name: "✅ Complete", IsComplete: false, IsActive: false}, }, current: 0, startTime: time.Now(), @@ -99,6 +101,8 @@ func NewProgressTracker() *ProgressTracker { // NextStage moves to the next analysis stage func (pt *ProgressTracker) NextStage() { + pt.mu.Lock() + defer pt.mu.Unlock() if pt.current < len(pt.stages) { pt.stages[pt.current].IsComplete = true pt.stages[pt.current].IsActive = false @@ -111,6 +115,8 @@ func (pt *ProgressTracker) NextStage() { // GetCurrentStage returns the current stage information func (pt *ProgressTracker) GetCurrentStage() ProgressStage { + pt.mu.RLock() + defer pt.mu.RUnlock() if pt.current < len(pt.stages) { return pt.stages[pt.current] } @@ -119,11 +125,18 @@ func (pt *ProgressTracker) GetCurrentStage() ProgressStage { // GetAllStages returns all stages with their status func (pt *ProgressTracker) GetAllStages() []ProgressStage { - return pt.stages + pt.mu.RLock() + defer pt.mu.RUnlock() + // Return a copy to avoid race conditions when iterating over the slice + stages := make([]ProgressStage, len(pt.stages)) + copy(stages, pt.stages) + return stages } // GetProgress returns completed stages / total stages func (pt *ProgressTracker) GetProgress() (completed int, total int) { + pt.mu.RLock() + defer pt.mu.RUnlock() total = len(pt.stages) for _, stage := range pt.stages { if stage.IsComplete {