Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 47 additions & 23 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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)

Expand All @@ -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,
Expand All @@ -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 {
Expand All @@ -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])
Expand All @@ -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]

Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
37 changes: 25 additions & 12 deletions internal/ui/progress.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,6 +19,7 @@ type ProgressTracker struct {
stages []ProgressStage
current int
startTime time.Time
mu sync.RWMutex
}

// ProgressUpdateMsg is sent to update progress
Expand Down Expand Up @@ -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(),
Expand All @@ -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
Expand All @@ -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]
}
Expand All @@ -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 {
Expand Down