diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-11 18:39:16 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-11 18:39:16 +0200 |
| commit | 0011f18e8494a4e57dc277b826d56c0a1df041ce (patch) | |
| tree | 7ffc0ad73b95c32b201adabbaf8baccbdbf6b04f | |
| parent | 8e3b69cdface52a755ee64003832557e8b15e23b (diff) | |
refactor(internal): extract sync and summary helpers
| -rw-r--r-- | internal/cli/sync_handlers.go | 298 | ||||
| -rw-r--r-- | internal/showcase/showcase.go | 226 | ||||
| -rw-r--r-- | internal/showcase/showcase_test.go | 11 |
3 files changed, 241 insertions, 294 deletions
diff --git a/internal/cli/sync_handlers.go b/internal/cli/sync_handlers.go index 538e18c..8fb3a93 100644 --- a/internal/cli/sync_handlers.go +++ b/internal/cli/sync_handlers.go @@ -187,32 +187,7 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int { fmt.Print(summary) } - // Generate script for abandoned branches - if scriptPath, err := syncer.GenerateDeleteScript(); err != nil { - fmt.Printf("\n⚠️ Failed to generate script: %v\n", err) - } else if scriptPath != "" { - fmt.Printf("\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n") - fmt.Printf("Generated script: %s\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("Usage:\n") - fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath) - fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath) - fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath) - fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("💡 Recommended workflow:\n") - fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath) - fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath) - fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n") - } + printDeleteScript(syncer) return 0 } @@ -457,6 +432,105 @@ func printFullSyncSeparator() { fmt.Println(strings.Repeat("=", 70) + "\n") } +type syncExecution struct { + syncer *sync.Syncer + descCache map[string]string + throttleManager *state.Manager + throttleState *state.State +} + +func newSyncExecution(cfg *config.Config, flags *Flags) *syncExecution { + execution := &syncExecution{ + descCache: loadDescriptionCache(flags.WorkDir), + syncer: sync.New(cfg, flags.WorkDir), + } + execution.syncer.SetBackupEnabled(flags.Backup) + + if flags.Throttle { + manager, st, err := loadThrottleState(flags.WorkDir) + if err != nil { + fmt.Printf("Warning: Failed to load throttle state: %v\n", err) + } + execution.throttleManager = manager + execution.throttleState = st + } + + return execution +} + +func (e *syncExecution) maybeThrottle(repoName string, flags *Flags) bool { + if !flags.Throttle { + return false + } + + decision := evaluateThrottle(repoName, e.throttleState, flags.DryRun) + if decision.Message != "" { + fmt.Println(decision.Message) + } + if decision.SetNextAllowed && e.throttleManager != nil && !flags.DryRun { + e.throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed) + if err := e.throttleManager.Save(e.throttleState); err != nil { + fmt.Printf("Warning: Failed to save throttle state: %v\n", err) + } + } + + return decision.Skip +} + +func (e *syncExecution) markSynced(repoName string, flags *Flags) { + if !flags.Throttle || e.throttleManager == nil { + return + } + + updateRepoSyncState(repoName, e.throttleState) + if err := e.throttleManager.Save(e.throttleState); err != nil { + fmt.Printf("Warning: Failed to save throttle state: %v\n", err) + } +} + +func (e *syncExecution) finishDiscoveredSync(successCount int, flags *Flags) { + if err := saveDescriptionCache(flags.WorkDir, e.descCache); err != nil { + fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err) + } + + fmt.Printf("\n=== Summary ===\n") + fmt.Printf("Successfully synced: %d repositories\n", successCount) + + if summary := e.syncer.GenerateAbandonedBranchSummary(); summary != "" { + fmt.Print(summary) + } + + printDeleteScript(e.syncer) +} + +func printDeleteScript(syncer *sync.Syncer) { + if scriptPath, err := syncer.GenerateDeleteScript(); err != nil { + fmt.Printf("\n⚠️ Failed to generate script: %v\n", err) + } else if scriptPath != "" { + fmt.Printf("\n") + fmt.Print(strings.Repeat("=", 70)) + fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n") + fmt.Print(strings.Repeat("=", 70)) + fmt.Printf("\n") + fmt.Printf("Generated script: %s\n", scriptPath) + fmt.Printf("\n") + fmt.Printf("Usage:\n") + fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath) + fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath) + fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath) + fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath) + fmt.Printf("\n") + fmt.Printf("💡 Recommended workflow:\n") + fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath) + fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath) + fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath) + fmt.Printf("\n") + fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n") + fmt.Print(strings.Repeat("=", 70)) + fmt.Printf("\n") + } +} + func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Repository, repoNames []string) int { // Initialize GitHub client if needed var githubClient github.Client @@ -470,24 +544,9 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames)) - // Load descriptions cache - descCache := loadDescriptionCache(flags.WorkDir) - - syncer := sync.New(cfg, flags.WorkDir) - syncer.SetBackupEnabled(flags.Backup) + execution := newSyncExecution(cfg, flags) successCount := 0 - var throttleManager *state.Manager - var throttleState *state.State - if flags.Throttle { - manager, st, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - throttleManager = manager - throttleState = st - } - // Create map for descriptions repoMap := make(map[string]codeberg.Repository) for _, repo := range repos { @@ -497,20 +556,8 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi for i, repoName := range repoNames { fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName) - if flags.Throttle { - decision := evaluateThrottle(repoName, throttleState, flags.DryRun) - if decision.Message != "" { - fmt.Println(decision.Message) - } - if decision.SetNextAllowed && throttleManager != nil && !flags.DryRun { - throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) - } - } - if decision.Skip { - continue - } + if execution.maybeThrottle(repoName, flags) { + continue } // Create GitHub repo if needed @@ -528,66 +575,23 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi } } - if err := syncer.SyncRepository(repoName); err != nil { + if err := execution.syncer.SyncRepository(repoName); err != nil { fmt.Printf("ERROR: Failed to sync %s: %v\n", repoName, err) fmt.Printf("Stopping sync due to error.\n") return 1 } - if flags.Throttle && throttleManager != nil { - updateRepoSyncState(repoName, throttleState) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) - } - } + execution.markSynced(repoName, flags) successCount++ // After syncing, sync descriptions according to precedence if cbRepo, ok := repoMap[repoName]; ok { - syncRepoDescriptions(cfg, flags.DryRun, repoName, cbRepo.Description, "", descCache) + syncRepoDescriptions(cfg, flags.DryRun, repoName, cbRepo.Description, "", execution.descCache) } else { - syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", descCache) + syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", execution.descCache) } } - // Save descriptions cache - if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil { - fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err) - } - - fmt.Printf("\n=== Summary ===\n") - fmt.Printf("Successfully synced: %d repositories\n", successCount) - - // Print abandoned branches summary - if summary := syncer.GenerateAbandonedBranchSummary(); summary != "" { - fmt.Print(summary) - } - - // Generate script for abandoned branches - if scriptPath, err := syncer.GenerateDeleteScript(); err != nil { - fmt.Printf("\n⚠️ Failed to generate script: %v\n", err) - } else if scriptPath != "" { - fmt.Printf("\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n") - fmt.Printf("Generated script: %s\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("Usage:\n") - fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath) - fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath) - fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath) - fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("💡 Recommended workflow:\n") - fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath) - fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath) - fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n") - } + execution.finishDiscoveredSync(successCount, flags) if !flags.SyncGitHubPublic { return 0 @@ -611,24 +615,9 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames)) - // Load descriptions cache - descCache := loadDescriptionCache(flags.WorkDir) - - syncer := sync.New(cfg, flags.WorkDir) - syncer.SetBackupEnabled(flags.Backup) + execution := newSyncExecution(cfg, flags) successCount := 0 - var throttleManager *state.Manager - var throttleState *state.State - if flags.Throttle { - manager, st, err := loadThrottleState(flags.WorkDir) - if err != nil { - fmt.Printf("Warning: Failed to load throttle state: %v\n", err) - } - throttleManager = manager - throttleState = st - } - // Create map for descriptions repoMap := make(map[string]github.Repository) for _, repo := range repos { @@ -638,20 +627,8 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository for i, repoName := range repoNames { fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName) - if flags.Throttle { - decision := evaluateThrottle(repoName, throttleState, flags.DryRun) - if decision.Message != "" { - fmt.Println(decision.Message) - } - if decision.SetNextAllowed && throttleManager != nil && !flags.DryRun { - throttleState.SetNextRepoSyncAllowed(repoName, decision.NextAllowed) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) - } - } - if decision.Skip { - continue - } + if execution.maybeThrottle(repoName, flags) { + continue } // Create Codeberg repo if needed @@ -669,66 +646,23 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository } } - if err := syncer.SyncRepository(repoName); err != nil { + if err := execution.syncer.SyncRepository(repoName); err != nil { fmt.Printf("ERROR: Failed to sync %s: %v\n", repoName, err) fmt.Printf("Stopping sync due to error.\n") return 1 } - if flags.Throttle && throttleManager != nil { - updateRepoSyncState(repoName, throttleState) - if err := throttleManager.Save(throttleState); err != nil { - fmt.Printf("Warning: Failed to save throttle state: %v\n", err) - } - } + execution.markSynced(repoName, flags) successCount++ // After syncing, sync descriptions according to precedence if ghRepo, ok := repoMap[repoName]; ok { - syncRepoDescriptions(cfg, flags.DryRun, repoName, "", ghRepo.Description, descCache) + syncRepoDescriptions(cfg, flags.DryRun, repoName, "", ghRepo.Description, execution.descCache) } else { - syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", descCache) + syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", execution.descCache) } } - // Save descriptions cache - if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil { - fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err) - } - - fmt.Printf("\n=== Summary ===\n") - fmt.Printf("Successfully synced: %d repositories\n", successCount) - - // Print abandoned branches summary - if summary := syncer.GenerateAbandonedBranchSummary(); summary != "" { - fmt.Print(summary) - } - - // Generate script for abandoned branches - if scriptPath, err := syncer.GenerateDeleteScript(); err != nil { - fmt.Printf("\n⚠️ Failed to generate script: %v\n", err) - } else if scriptPath != "" { - fmt.Printf("\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n📋 ABANDONED BRANCH MANAGEMENT SCRIPT\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n") - fmt.Printf("Generated script: %s\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("Usage:\n") - fmt.Printf(" bash %s --review # Review diffs before deletion\n", scriptPath) - fmt.Printf(" bash %s --review-full # Review full diffs\n", scriptPath) - fmt.Printf(" bash %s --dry-run # Preview what will be deleted\n", scriptPath) - fmt.Printf(" bash %s # Delete branches (with confirmation)\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("💡 Recommended workflow:\n") - fmt.Printf(" 1. Review branches: bash %s --review\n", scriptPath) - fmt.Printf(" 2. Dry-run delete: bash %s --dry-run\n", scriptPath) - fmt.Printf(" 3. Delete branches: bash %s\n", scriptPath) - fmt.Printf("\n") - fmt.Printf("⚠️ WARNING: Review carefully before deleting branches!\n") - fmt.Print(strings.Repeat("=", 70)) - fmt.Printf("\n") - } + execution.finishDiscoveredSync(successCount, flags) return 0 } diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go index 7fef655..dfc3a0d 100644 --- a/internal/showcase/showcase.go +++ b/internal/showcase/showcase.go @@ -212,6 +212,101 @@ func findReadmeContent(repoPath string) ([]byte, string, bool) { return nil, "", false } +func selectSummaryTool(aiTool string) string { + switch aiTool { + case "amp", "": + if _, err := exec.LookPath("amp"); err == nil { + return "amp" + } + if _, err := exec.LookPath("hexai"); err == nil { + return "hexai" + } + if _, err := exec.LookPath("claude"); err == nil { + return "claude" + } + if _, err := exec.LookPath("aichat"); err == nil { + return "aichat" + } + case "claude", "claude-code": + if _, err := exec.LookPath("claude"); err == nil { + return "claude" + } + if _, err := exec.LookPath("hexai"); err == nil { + return "hexai" + } + if _, err := exec.LookPath("aichat"); err == nil { + return "aichat" + } + case "hexai", "aichat": + if _, err := exec.LookPath(aiTool); err == nil { + return aiTool + } + } + + return "" +} + +func runSummaryTool(selectedTool, prompt, repoPath, readmeFile string, readmeContent []byte, readmeFound bool) string { + var cmd *exec.Cmd + + switch selectedTool { + case "amp": + fmt.Printf("Running amp command (stdin payload)\n") + if readmeFound { + fmt.Printf(" echo <README content> | amp --execute \"%s\"\n", prompt) + fmt.Printf(" Using %s as input\n", readmeFile) + cmd = exec.Command("amp", "--execute", prompt) + cmd.Stdin = strings.NewReader(string(readmeContent)) + } + case "claude": + fmt.Printf("Running Claude command:\n") + fmt.Printf(" claude --model sonnet \"%s\"\n", prompt) + cmd = exec.Command("claude", "--model", "sonnet", prompt) + case "hexai": + fmt.Printf("Running hexai command (stdin payload)\n") + if readmeFound { + fmt.Printf(" echo <README content> | hexai \"%s\"\n", prompt) + fmt.Printf(" Using %s as input\n", readmeFile) + cmd = exec.Command("hexai", prompt) + cmd.Stdin = strings.NewReader(string(readmeContent)) + } + case "aichat": + fmt.Printf("Running aichat command:\n") + if readmeFound { + fmt.Printf(" echo <README content> | aichat \"%s\"\n", prompt) + fmt.Printf(" Using %s as input\n", readmeFile) + cmd = exec.Command("aichat", prompt) + cmd.Stdin = strings.NewReader(string(readmeContent)) + } + } + + if cmd == nil { + return "" + } + + cmd.Dir = repoPath + output, err := cmd.Output() + if err != nil { + return "" + } + + return strings.TrimSpace(string(output)) +} + +func fallbackSummary(repoName string, readmeContent []byte, readmeFound bool) string { + if readmeFound { + parts := strings.Split(strings.TrimSpace(string(readmeContent)), "\n\n") + if len(parts) > 0 { + summary := strings.TrimSpace(parts[0]) + if summary != "" { + return summary + } + } + } + + return fmt.Sprintf("%s: source code repository.", repoName) +} + // getRepositories returns a list of repository directories in the work directory func (g *Generator) getRepositories() ([]string, error) { entries, err := os.ReadDir(g.workDir) @@ -237,6 +332,21 @@ func (g *Generator) getRepositories() ([]string, error) { return repos, nil } +func (g *Generator) buildProjectLinks(repoName string) (string, string) { + codebergURL := "" + githubURL := "" + + if codebergOrg := g.config.FindCodebergOrg(); codebergOrg != nil { + codebergURL = fmt.Sprintf("https://codeberg.org/%s/%s", codebergOrg.Name, repoName) + } + + if githubOrg := g.config.FindGitHubOrg(); githubOrg != nil { + githubURL = fmt.Sprintf("https://github.com/%s/%s", githubOrg.Name, repoName) + } + + return codebergURL, githubURL +} + // generateProjectSummary generates a summary for a single project func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool) (*ProjectSummary, error) { repoPath := filepath.Join(g.workDir, repoName) @@ -260,43 +370,7 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool // Prefer amp if available when default tool is "" (aligns with release flow) selectedTool := g.aiTool if !haveCachedSummary { - switch g.aiTool { - case "amp", "": - // Try amp -> hexai -> claude -> aichat - if _, err := exec.LookPath("amp"); err == nil { - selectedTool = "amp" - } else if _, err := exec.LookPath("hexai"); err == nil { - selectedTool = "hexai" - } else if _, err := exec.LookPath("claude"); err == nil { - selectedTool = "claude" - } else if _, err := exec.LookPath("aichat"); err == nil { - selectedTool = "aichat" - } else { - // No AI tool available; fall back to README-based summary later - selectedTool = "" - } - case "claude", "claude-code": - // Try claude -> hexai -> aichat - if _, err := exec.LookPath("claude"); err == nil { - selectedTool = "claude" - } else if _, err := exec.LookPath("hexai"); err == nil { - selectedTool = "hexai" - } else if _, err := exec.LookPath("aichat"); err == nil { - selectedTool = "aichat" - } else { - selectedTool = "" - } - case "hexai", "aichat": - if _, err := exec.LookPath(g.aiTool); err != nil { - // Requested tool missing; fall back to README-based summary later - selectedTool = "" - } else { - selectedTool = g.aiTool - } - default: - // Unsupported tool configured; fall back to README-based summary later - selectedTool = "" - } + selectedTool = selectSummaryTool(g.aiTool) } readmeContent, readmeFile, readmeFound := findReadmeContent(repoPath) @@ -316,88 +390,16 @@ func (g *Generator) generateProjectSummary(repoName string, forceRegenerate bool fmt.Printf("Using cached AI summary\n") } else { prompt := "Please provide a 1-2 paragraph summary of this project, explaining what it does, why it's useful, and how it's implemented. Focus on the key features and architecture. Be concise but informative." - - var cmd *exec.Cmd - - switch selectedTool { - case "amp": - // Use README content as stdin and pass the prompt as --execute argument - fmt.Printf("Running amp command (stdin payload)\n") - if readmeFound { - fmt.Printf(" echo <README content> | amp --execute \"%s\"\n", prompt) - fmt.Printf(" Using %s as input\n", readmeFile) - cmd = exec.Command("amp", "--execute", prompt) - cmd.Stdin = strings.NewReader(string(readmeContent)) - } else { - // Will fall back below - cmd = nil - } - case "claude": - fmt.Printf("Running Claude command:\n") - fmt.Printf(" claude --model sonnet \"%s\"\n", prompt) - cmd = exec.Command("claude", "--model", "sonnet", prompt) - case "hexai": - // Use README content as stdin and pass the prompt as argument - fmt.Printf("Running hexai command (stdin payload)\n") - if readmeFound { - fmt.Printf(" echo <README content> | hexai \"%s\"\n", prompt) - fmt.Printf(" Using %s as input\n", readmeFile) - cmd = exec.Command("hexai", prompt) - cmd.Stdin = strings.NewReader(string(readmeContent)) - } else { - // Will fall back below - cmd = nil - } - case "aichat": - // For aichat, we need to read README.md and pipe it to aichat - fmt.Printf("Running aichat command:\n") - - if readmeFound { - fmt.Printf(" echo <README content> | aichat \"%s\"\n", prompt) - fmt.Printf(" Using %s as input\n", readmeFile) - cmd = exec.Command("aichat", prompt) - cmd.Stdin = strings.NewReader(string(readmeContent)) - } else { - // Will fall back below - cmd = nil - } - default: - // No/unsupported tool; will fall back below - cmd = nil - } - - if cmd != nil { - cmd.Dir = repoPath - if output, err := cmd.Output(); err == nil { - summary = strings.TrimSpace(string(output)) - } - } + summary = runSummaryTool(selectedTool, prompt, repoPath, readmeFile, readmeContent, readmeFound) // Fallback: create a minimal summary from README if AI unavailable/failed if summary == "" { - if readmeFound { - parts := strings.Split(strings.TrimSpace(string(readmeContent)), "\n\n") - if len(parts) > 0 { - summary = strings.TrimSpace(parts[0]) - } - } - if summary == "" { - summary = fmt.Sprintf("%s: source code repository.", repoName) - } + summary = fallbackSummary(repoName, readmeContent, readmeFound) } } // Build URLs - codebergURL := "" - githubURL := "" - - if codebergOrg := g.config.FindCodebergOrg(); codebergOrg != nil { - codebergURL = fmt.Sprintf("https://codeberg.org/%s/%s", codebergOrg.Name, repoName) - } - - if githubOrg := g.config.FindGitHubOrg(); githubOrg != nil { - githubURL = fmt.Sprintf("https://github.com/%s/%s", githubOrg.Name, repoName) - } + codebergURL, githubURL := g.buildProjectLinks(repoName) // Always extract images from README (not cached) fmt.Printf("Extracting images from README...\n") diff --git a/internal/showcase/showcase_test.go b/internal/showcase/showcase_test.go index f480bdb..fb20c96 100644 --- a/internal/showcase/showcase_test.go +++ b/internal/showcase/showcase_test.go @@ -149,3 +149,14 @@ func TestFindReadmeContent_UsesRepoPathWithoutChangingCWD(t *testing.T) { t.Fatalf("unexpected README content: %q", string(content)) } } + +func TestFallbackSummary_UsesFirstReadmeParagraph(t *testing.T) { + t.Parallel() + + readme := []byte("first paragraph\n\nsecond paragraph") + summary := fallbackSummary("repo", readme, true) + + if summary != "first paragraph" { + t.Fatalf("expected first paragraph summary, got %q", summary) + } +} |
