summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-10-31 20:13:32 +0200
committerPaul Buetow <paul@buetow.org>2025-10-31 20:13:32 +0200
commit11eea6a82cbfdde40ec1457c6ea080da4da6b7dc (patch)
tree8026068f6a3beb3ee02c45f06f4487f4b89caaf1 /internal/cli
parent5c3e0b5cf99d028c4f06be7a825388b296e37a22 (diff)
feat: implement amp AI tool support and replace Taskfile with Magev0.10.0
- Add amp as default AI tool for release notes and showcase generation - Fallback chain: amp → hexai → claude → aichat - Replace Taskfile.yaml with magefile.go for build automation - Update all documentation (README.md, AGENTS.md, doc/development.md) - Update version to 0.10.0 Amp-Thread-ID: https://ampcode.com/threads/T-735ba1e2-0255-4b43-8ed1-6c0d2f78301b Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/description_cache.go51
-rw-r--r--internal/cli/description_sync.go185
-rw-r--r--internal/cli/flags.go72
-rw-r--r--internal/cli/handlers.go46
-rw-r--r--internal/cli/release.go270
-rw-r--r--internal/cli/showcase_handler.go10
-rw-r--r--internal/cli/showcase_only_handler.go152
-rw-r--r--internal/cli/sync_handlers.go256
8 files changed, 520 insertions, 522 deletions
diff --git a/internal/cli/description_cache.go b/internal/cli/description_cache.go
index 1cfc951..a2ce9ab 100644
--- a/internal/cli/description_cache.go
+++ b/internal/cli/description_cache.go
@@ -1,38 +1,37 @@
package cli
import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
)
// loadDescriptionCache loads the per-repo canonical description cache
func loadDescriptionCache(workDir string) map[string]string {
- cache := make(map[string]string)
- cacheFile := filepath.Join(workDir, ".gitsyncer-descriptions-cache.json")
- data, err := os.ReadFile(cacheFile)
- if err != nil {
- return cache
- }
- if err := json.Unmarshal(data, &cache); err != nil {
- fmt.Printf("Warning: Failed to parse descriptions cache: %v\n", err)
- return make(map[string]string)
- }
- fmt.Printf("Loaded descriptions cache with %d entries\n", len(cache))
- return cache
+ cache := make(map[string]string)
+ cacheFile := filepath.Join(workDir, ".gitsyncer-descriptions-cache.json")
+ data, err := os.ReadFile(cacheFile)
+ if err != nil {
+ return cache
+ }
+ if err := json.Unmarshal(data, &cache); err != nil {
+ fmt.Printf("Warning: Failed to parse descriptions cache: %v\n", err)
+ return make(map[string]string)
+ }
+ fmt.Printf("Loaded descriptions cache with %d entries\n", len(cache))
+ return cache
}
// saveDescriptionCache saves the per-repo canonical description cache
func saveDescriptionCache(workDir string, cache map[string]string) error {
- cacheFile := filepath.Join(workDir, ".gitsyncer-descriptions-cache.json")
- data, err := json.MarshalIndent(cache, "", " ")
- if err != nil {
- return fmt.Errorf("failed to marshal descriptions cache: %w", err)
- }
- if err := os.WriteFile(cacheFile, data, 0644); err != nil {
- return fmt.Errorf("failed to write descriptions cache: %w", err)
- }
- return nil
+ cacheFile := filepath.Join(workDir, ".gitsyncer-descriptions-cache.json")
+ data, err := json.MarshalIndent(cache, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal descriptions cache: %w", err)
+ }
+ if err := os.WriteFile(cacheFile, data, 0644); err != nil {
+ return fmt.Errorf("failed to write descriptions cache: %w", err)
+ }
+ return nil
}
-
diff --git a/internal/cli/description_sync.go b/internal/cli/description_sync.go
index ae275ce..4904b56 100644
--- a/internal/cli/description_sync.go
+++ b/internal/cli/description_sync.go
@@ -1,113 +1,112 @@
package cli
import (
- "fmt"
- "strings"
+ "fmt"
+ "strings"
- "codeberg.org/snonux/gitsyncer/internal/codeberg"
- "codeberg.org/snonux/gitsyncer/internal/config"
- "codeberg.org/snonux/gitsyncer/internal/github"
+ "codeberg.org/snonux/gitsyncer/internal/codeberg"
+ "codeberg.org/snonux/gitsyncer/internal/config"
+ "codeberg.org/snonux/gitsyncer/internal/github"
)
// syncRepoDescriptions ensures both platforms have the canonical description
// Precedence: Codeberg > GitHub; if Codeberg empty and GitHub has one, use GitHub.
// knownCBDesc and knownGHDesc can be empty; the function fetches as needed.
func syncRepoDescriptions(cfg *config.Config, dryRun bool, repoName, knownCBDesc, knownGHDesc string, cache map[string]string) {
- // Load orgs
- ghOrg := cfg.FindGitHubOrg()
- cbOrg := cfg.FindCodebergOrg()
+ // Load orgs
+ ghOrg := cfg.FindGitHubOrg()
+ cbOrg := cfg.FindCodebergOrg()
- var ghClient *github.Client
- var cbClient *codeberg.Client
- if ghOrg != nil {
- c := github.NewClient(ghOrg.GitHubToken, ghOrg.Name)
- ghClient = &c
- }
- if cbOrg != nil {
- c := codeberg.NewClient(cbOrg.Name, cbOrg.CodebergToken)
- cbClient = &c
- }
+ var ghClient *github.Client
+ var cbClient *codeberg.Client
+ if ghOrg != nil {
+ c := github.NewClient(ghOrg.GitHubToken, ghOrg.Name)
+ ghClient = &c
+ }
+ if cbOrg != nil {
+ c := codeberg.NewClient(cbOrg.Name, cbOrg.CodebergToken)
+ cbClient = &c
+ }
- // Get current descriptions (use known if provided)
- cbDesc := strings.TrimSpace(knownCBDesc)
- ghDesc := strings.TrimSpace(knownGHDesc)
- var cbExists, ghExists bool
+ // Get current descriptions (use known if provided)
+ cbDesc := strings.TrimSpace(knownCBDesc)
+ ghDesc := strings.TrimSpace(knownGHDesc)
+ var cbExists, ghExists bool
- if cbDesc == "" && cbClient != nil {
- if repo, exists, err := cbClient.GetRepo(repoName); err == nil {
- cbExists = exists
- if exists {
- cbDesc = strings.TrimSpace(repo.Description)
- }
- } else {
- fmt.Printf(" Warning: Codeberg repo lookup failed: %v\n", err)
- }
- } else if cbClient != nil {
- cbExists = true
- }
+ if cbDesc == "" && cbClient != nil {
+ if repo, exists, err := cbClient.GetRepo(repoName); err == nil {
+ cbExists = exists
+ if exists {
+ cbDesc = strings.TrimSpace(repo.Description)
+ }
+ } else {
+ fmt.Printf(" Warning: Codeberg repo lookup failed: %v\n", err)
+ }
+ } else if cbClient != nil {
+ cbExists = true
+ }
- if ghClient != nil {
- if ghDesc == "" || !ghExists {
- if repo, exists, err := ghClient.GetRepo(repoName); err == nil {
- ghExists = exists
- if exists {
- ghDesc = strings.TrimSpace(repo.Description)
- }
- } else {
- fmt.Printf(" Warning: GitHub repo lookup failed: %v\n", err)
- }
- }
- }
+ if ghClient != nil {
+ if ghDesc == "" || !ghExists {
+ if repo, exists, err := ghClient.GetRepo(repoName); err == nil {
+ ghExists = exists
+ if exists {
+ ghDesc = strings.TrimSpace(repo.Description)
+ }
+ } else {
+ fmt.Printf(" Warning: GitHub repo lookup failed: %v\n", err)
+ }
+ }
+ }
- // Determine canonical description
- canonical := cbDesc
- if canonical == "" {
- canonical = ghDesc
- }
- canonical = strings.TrimSpace(canonical)
+ // Determine canonical description
+ canonical := cbDesc
+ if canonical == "" {
+ canonical = ghDesc
+ }
+ canonical = strings.TrimSpace(canonical)
- // If nothing to sync, bail
- if canonical == "" {
- return
- }
+ // If nothing to sync, bail
+ if canonical == "" {
+ return
+ }
- // Update Codeberg if needed
- if cbClient != nil && cbExists {
- if cbDesc != canonical {
- if dryRun {
- fmt.Printf(" [DRY RUN] Would update Codeberg description for %s -> %q\n", repoName, canonical)
- } else if cbClient.HasToken() {
- if err := cbClient.UpdateRepoDescription(repoName, canonical); err != nil {
- fmt.Printf(" Warning: Failed to update Codeberg description: %v\n", err)
- } else {
- fmt.Printf(" Updated Codeberg description for %s\n", repoName)
- }
- } else {
- fmt.Println(" Warning: No Codeberg token; cannot update description")
- }
- }
- }
+ // Update Codeberg if needed
+ if cbClient != nil && cbExists {
+ if cbDesc != canonical {
+ if dryRun {
+ fmt.Printf(" [DRY RUN] Would update Codeberg description for %s -> %q\n", repoName, canonical)
+ } else if cbClient.HasToken() {
+ if err := cbClient.UpdateRepoDescription(repoName, canonical); err != nil {
+ fmt.Printf(" Warning: Failed to update Codeberg description: %v\n", err)
+ } else {
+ fmt.Printf(" Updated Codeberg description for %s\n", repoName)
+ }
+ } else {
+ fmt.Println(" Warning: No Codeberg token; cannot update description")
+ }
+ }
+ }
- // Update GitHub if needed
- if ghClient != nil && ghExists {
- if ghDesc != canonical {
- if dryRun {
- fmt.Printf(" [DRY RUN] Would update GitHub description for %s -> %q\n", repoName, canonical)
- } else if ghClient.HasToken() {
- if err := ghClient.UpdateRepoDescription(repoName, canonical); err != nil {
- fmt.Printf(" Warning: Failed to update GitHub description: %v\n", err)
- } else {
- fmt.Printf(" Updated GitHub description for %s\n", repoName)
- }
- } else {
- fmt.Println(" Warning: No GitHub token; cannot update description")
- }
- }
- }
+ // Update GitHub if needed
+ if ghClient != nil && ghExists {
+ if ghDesc != canonical {
+ if dryRun {
+ fmt.Printf(" [DRY RUN] Would update GitHub description for %s -> %q\n", repoName, canonical)
+ } else if ghClient.HasToken() {
+ if err := ghClient.UpdateRepoDescription(repoName, canonical); err != nil {
+ fmt.Printf(" Warning: Failed to update GitHub description: %v\n", err)
+ } else {
+ fmt.Printf(" Updated GitHub description for %s\n", repoName)
+ }
+ } else {
+ fmt.Println(" Warning: No GitHub token; cannot update description")
+ }
+ }
+ }
- // Update cache
- if cache != nil {
- cache[repoName] = canonical
- }
+ // Update cache
+ if cache != nil {
+ cache[repoName] = canonical
+ }
}
-
diff --git a/internal/cli/flags.go b/internal/cli/flags.go
index 43d9fa3..5c6914c 100644
--- a/internal/cli/flags.go
+++ b/internal/cli/flags.go
@@ -4,39 +4,39 @@ import (
"flag"
"os"
"path/filepath"
-
+
"codeberg.org/snonux/gitsyncer/internal/state"
)
// Flags holds all command-line flag values
type Flags struct {
- VersionFlag bool
- ConfigPath string
- ListOrgs bool
- ListRepos bool
- SyncRepo string
- SyncAll bool
- SyncCodebergPublic bool
- SyncGitHubPublic bool
- FullSync bool
- CreateGitHubRepos bool
+ VersionFlag bool
+ ConfigPath string
+ ListOrgs bool
+ ListRepos bool
+ SyncRepo string
+ SyncAll bool
+ SyncCodebergPublic bool
+ SyncGitHubPublic bool
+ FullSync bool
+ CreateGitHubRepos bool
CreateCodebergRepos bool
- DryRun bool
- WorkDir string
- TestGitHubToken bool
- Clean bool
- DeleteRepo string
- Backup bool
- Showcase bool
- Force bool
- BatchRun bool
- CheckReleases bool
- NoCheckReleases bool
- AutoCreateReleases bool
- AIReleaseNotes bool
- UpdateReleases bool
- AITool string
-
+ DryRun bool
+ WorkDir string
+ TestGitHubToken bool
+ Clean bool
+ DeleteRepo string
+ Backup bool
+ Showcase bool
+ Force bool
+ BatchRun bool
+ CheckReleases bool
+ NoCheckReleases bool
+ AutoCreateReleases bool
+ AIReleaseNotes bool
+ UpdateReleases bool
+ AITool string
+
// Internal fields for batch run state management (not set by flags)
BatchRunStateManager *state.Manager
BatchRunState *state.State
@@ -45,7 +45,7 @@ type Flags struct {
// ParseFlags parses command-line flags and returns the flags struct
func ParseFlags() *Flags {
f := &Flags{}
-
+
flag.BoolVar(&f.VersionFlag, "version", false, "print version information")
flag.BoolVar(&f.VersionFlag, "v", false, "print version information (short)")
flag.StringVar(&f.ConfigPath, "config", "", "path to configuration file")
@@ -65,17 +65,17 @@ func ParseFlags() *Flags {
flag.BoolVar(&f.Clean, "clean", false, "delete all repositories in work directory (with confirmation)")
flag.StringVar(&f.DeleteRepo, "delete-repo", "", "delete specified repository from all configured organizations (with confirmation)")
flag.BoolVar(&f.Backup, "backup", false, "enable syncing to backup locations")
- flag.BoolVar(&f.Showcase, "showcase", false, "generate project showcase using Claude after syncing")
+ flag.BoolVar(&f.Showcase, "showcase", false, "generate project showcase using AI (amp by default) after syncing")
flag.BoolVar(&f.Force, "force", false, "force regeneration of cached data")
flag.BoolVar(&f.BatchRun, "batch-run", false, "enable --full and --showcase (runs only once per week)")
flag.BoolVar(&f.CheckReleases, "check-releases", false, "manually check for version tags without releases and create them (with confirmation)")
flag.BoolVar(&f.NoCheckReleases, "no-check-releases", false, "disable automatic release checking after sync operations")
flag.BoolVar(&f.AutoCreateReleases, "auto-create-releases", false, "automatically create releases without confirmation prompts")
- flag.BoolVar(&f.AIReleaseNotes, "ai-release-notes", false, "generate release notes using Claude AI based on git diff")
+ flag.BoolVar(&f.AIReleaseNotes, "ai-release-notes", false, "generate release notes using AI (amp by default) based on git diff")
flag.BoolVar(&f.UpdateReleases, "update-releases", false, "update existing releases with new AI-generated notes")
-
+
flag.Parse()
-
+
// Set default WorkDir if not provided
if f.WorkDir == "" {
home, err := os.UserHomeDir()
@@ -86,7 +86,7 @@ func ParseFlags() *Flags {
f.WorkDir = ".gitsyncer-work"
}
}
-
+
// Handle --full flag by enabling all sync operations
if f.FullSync {
f.SyncCodebergPublic = true
@@ -94,7 +94,7 @@ func ParseFlags() *Flags {
f.CreateGitHubRepos = true
f.CreateCodebergRepos = true
}
-
+
// Handle --batch-run flag by enabling --full and --showcase
if f.BatchRun {
f.FullSync = true
@@ -105,6 +105,6 @@ func ParseFlags() *Flags {
f.CreateGitHubRepos = true
f.CreateCodebergRepos = true
}
-
+
return f
-} \ No newline at end of file
+}
diff --git a/internal/cli/handlers.go b/internal/cli/handlers.go
index 02df7ae..aa43a86 100644
--- a/internal/cli/handlers.go
+++ b/internal/cli/handlers.go
@@ -28,7 +28,7 @@ func HandleTestGitHubToken() int {
fmt.Println("Please set GITHUB_TOKEN environment variable or create ~/.gitsyncer_github_token file")
return 1
}
-
+
// Test the token by checking a known repo
exists, err := client.RepoExists("gitsyncer")
if err != nil {
@@ -41,7 +41,7 @@ func HandleTestGitHubToken() int {
}
return 1
}
-
+
fmt.Printf("SUCCESS: Token is valid! Repository check returned: %v\n", exists)
return 0
}
@@ -54,7 +54,7 @@ func LoadConfig(configPath string) (*config.Config, error) {
return nil, fmt.Errorf("no configuration file found")
}
}
-
+
fmt.Printf("Loaded configuration from: %s\n", configPath)
return config.Load(configPath)
}
@@ -78,14 +78,14 @@ func findDefaultConfigPath() string {
return loc
}
}
-
+
return ""
}
// ShowConfigHelp displays help for creating a configuration file
func ShowConfigHelp() {
home, _ := os.UserHomeDir()
-
+
fmt.Println("No configuration file found. Please create one of:")
fmt.Printf(" - ./gitsyncer.json\n")
fmt.Printf(" - %s/.config/gitsyncer/config.json\n", home)
@@ -135,7 +135,7 @@ func HandleListRepos(cfg *config.Config) int {
// ShowUsage displays the usage information
func ShowUsage(cfg *config.Config) {
fmt.Println("\ngitsyncer - Git repository synchronization tool")
- fmt.Printf("Configured with %d organization(s) and %d repository(ies)\n",
+ fmt.Printf("Configured with %d organization(s) and %d repository(ies)\n",
len(cfg.Organizations), len(cfg.Repositories))
fmt.Println("\nUsage:")
fmt.Println(" gitsyncer --sync <repo-name> Sync a specific repository")
@@ -166,18 +166,18 @@ func HandleDeleteRepo(cfg *config.Config, repoName string) int {
}
fmt.Printf("\n⚠️ WARNING: This will permanently delete the repository '%s' from all configured organizations!\n\n", repoName)
-
+
// Find organizations where the repo exists
var orgsWithRepo []struct {
org config.Organization
exists bool
err error
}
-
+
for _, org := range cfg.Organizations {
var exists bool
var err error
-
+
switch org.Host {
case "git@github.com":
client := github.NewClient(org.GitHubToken, org.Name)
@@ -189,14 +189,14 @@ func HandleDeleteRepo(cfg *config.Config, repoName string) int {
fmt.Printf("Skipping unsupported host: %s\n", org.Host)
continue
}
-
+
orgsWithRepo = append(orgsWithRepo, struct {
org config.Organization
exists bool
err error
}{org, exists, err})
}
-
+
// Show summary of where the repo exists
fmt.Println("Repository status:")
foundAny := false
@@ -210,36 +210,36 @@ func HandleDeleteRepo(cfg *config.Config, repoName string) int {
fmt.Printf(" ⬜ %s: Not found\n", info.org.GetGitURL())
}
}
-
+
if !foundAny {
fmt.Printf("\nRepository '%s' not found in any configured organization.\n", repoName)
return 0
}
-
+
// Confirm deletion
fmt.Printf("\nAre you sure you want to delete '%s' from the above organizations? This action cannot be undone!\n", repoName)
fmt.Print("Type 'yes' to confirm: ")
-
+
reader := bufio.NewReader(os.Stdin)
confirmation, _ := reader.ReadString('\n')
confirmation = strings.TrimSpace(confirmation)
-
+
if confirmation != "yes" {
fmt.Println("Deletion cancelled.")
return 0
}
-
+
// Perform deletions
fmt.Println("\nDeleting repositories...")
hasError := false
-
+
for _, info := range orgsWithRepo {
if !info.exists || info.err != nil {
continue
}
-
+
fmt.Printf(" Deleting from %s... ", info.org.GetGitURL())
-
+
var deleteErr error
switch info.org.Host {
case "git@github.com":
@@ -249,7 +249,7 @@ func HandleDeleteRepo(cfg *config.Config, repoName string) int {
client := codeberg.NewClient(info.org.Name, info.org.CodebergToken)
deleteErr = client.DeleteRepo(repoName)
}
-
+
if deleteErr != nil {
fmt.Printf("FAILED: %v\n", deleteErr)
hasError = true
@@ -257,12 +257,12 @@ func HandleDeleteRepo(cfg *config.Config, repoName string) int {
fmt.Println("SUCCESS")
}
}
-
+
if hasError {
fmt.Println("\n⚠️ Some deletions failed. Check the errors above.")
return 1
}
-
+
fmt.Printf("\n✅ Repository '%s' has been successfully deleted from all organizations.\n", repoName)
return 0
-} \ No newline at end of file
+}
diff --git a/internal/cli/release.go b/internal/cli/release.go
index 86cc5f9..e6dd057 100644
--- a/internal/cli/release.go
+++ b/internal/cli/release.go
@@ -29,7 +29,7 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int {
fmt.Printf("Error reading work directory %s: %v\n", flags.WorkDir, err)
return 1
}
-
+
var repositories []string
for _, entry := range entries {
if entry.IsDir() {
@@ -40,12 +40,12 @@ func HandleCheckReleases(cfg *config.Config, flags *Flags) int {
}
}
}
-
+
if len(repositories) == 0 {
fmt.Println("No repositories found in work directory")
return 1
}
-
+
fmt.Printf("Found %d repositories in work directory\n", len(repositories))
return HandleCheckReleasesForRepos(cfg, flags, repositories)
}
@@ -60,23 +60,23 @@ func HandleCheckReleasesForRepo(cfg *config.Config, flags *Flags, repoName strin
func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories []string) int {
releaseManager := release.NewManager(flags.WorkDir)
releaseManager.SetAITool(flags.AITool)
-
+
// Load persistent AI release notes cache
cacheFile := filepath.Join(flags.WorkDir, ".gitsyncer-ai-release-notes-cache.json")
aiReleaseNotesCache := loadAIReleaseNotesCache(cacheFile)
initialCacheSize := len(aiReleaseNotesCache)
-
+
// Track failed AI generations
failedAIGenerations := []string{}
-
+
// Print summary at the end
defer func() {
if len(aiReleaseNotesCache) > initialCacheSize {
- fmt.Printf("\nAI release notes cache updated: %d new entries added (total: %d entries)\n",
+ fmt.Printf("\nAI release notes cache updated: %d new entries added (total: %d entries)\n",
len(aiReleaseNotesCache)-initialCacheSize, len(aiReleaseNotesCache))
fmt.Printf("Cache file: %s\n", cacheFile)
}
-
+
if len(failedAIGenerations) > 0 {
fmt.Printf("\n⚠️ AI release notes generation failed for %d releases:\n", len(failedAIGenerations))
for _, failed := range failedAIGenerations {
@@ -86,12 +86,12 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
fmt.Println("Run again to retry generation for these releases.")
}
}()
-
+
// Set tokens from config with fallback to environment variables and files
githubOrg := cfg.FindGitHubOrg()
if githubOrg != nil {
fmt.Printf("Found GitHub org: %s\n", githubOrg.Name)
-
+
// Try config token first, then fallback to env var and file
token := githubOrg.GitHubToken
if token == "" {
@@ -109,7 +109,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
}
}
-
+
if token != "" {
releaseManager.SetGitHubToken(token)
} else {
@@ -118,11 +118,11 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
fmt.Println("No GitHub organization found in config")
}
-
+
codebergOrg := cfg.FindCodebergOrg()
if codebergOrg != nil {
fmt.Printf("Found Codeberg org: %s\n", codebergOrg.Name)
-
+
// Try config token first, then fallback to env var and file
token := codebergOrg.CodebergToken
if token == "" {
@@ -140,7 +140,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
}
}
-
+
if token != "" {
releaseManager.SetCodebergToken(token)
fmt.Printf(" Codeberg token loaded (length: %d)\n", len(token))
@@ -150,114 +150,114 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
fmt.Println("No Codeberg organization found in config")
}
-
- // Process the specified repositories
- for _, repoName := range repositories {
- fmt.Printf("\nChecking releases for repository: %s\n", repoName)
-
+
+ // Process the specified repositories
+ for _, repoName := range repositories {
+ fmt.Printf("\nChecking releases for repository: %s\n", repoName)
+
// Check if the repository is cloned locally
repoPath := filepath.Join(flags.WorkDir, repoName)
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
fmt.Printf(" Repository not found locally at %s, skipping...\n", repoPath)
continue
}
-
+
// Get local tags
localTags, err := releaseManager.GetLocalTags(repoPath)
if err != nil {
fmt.Printf(" Error getting local tags: %v\n", err)
continue
}
-
+
if len(localTags) == 0 {
fmt.Println(" No version tags found")
continue
}
-
- fmt.Printf(" Found %d version tags: %s\n", len(localTags), strings.Join(localTags, ", "))
- // Log configured skip rules for this repo, if any
- if cfg.SkipReleases != nil {
- if skipTags, ok := cfg.SkipReleases[repoName]; ok && len(skipTags) > 0 {
- fmt.Printf(" Config skip_releases for %s: %s\n", repoName, strings.Join(skipTags, ", "))
- }
- }
-
+
+ fmt.Printf(" Found %d version tags: %s\n", len(localTags), strings.Join(localTags, ", "))
+ // Log configured skip rules for this repo, if any
+ if cfg.SkipReleases != nil {
+ if skipTags, ok := cfg.SkipReleases[repoName]; ok && len(skipTags) > 0 {
+ fmt.Printf(" Config skip_releases for %s: %s\n", repoName, strings.Join(skipTags, ", "))
+ }
+ }
+
// Check GitHub releases if GitHub is configured
var missingGitHub []string
githubOrg := cfg.FindGitHubOrg()
- if githubOrg != nil && githubOrg.Name != "" {
- githubReleases, err := releaseManager.GetGitHubReleases(githubOrg.Name, repoName)
- if err != nil {
- fmt.Printf(" Error checking GitHub releases: %v\n", err)
- } else {
- missingGitHub = releaseManager.FindMissingReleases(localTags, githubReleases)
- // Filter out tags that should be skipped per config
- if len(missingGitHub) > 0 {
- var filtered []string
- var skipped []string
- for _, t := range missingGitHub {
- if cfg.ShouldSkipRelease(repoName, t) {
- skipped = append(skipped, t)
- } else {
- filtered = append(filtered, t)
- }
- }
- if len(skipped) > 0 {
- fmt.Printf(" Skipping GitHub releases per config for tags: %s\n", strings.Join(skipped, ", "))
- }
- missingGitHub = filtered
- if len(missingGitHub) > 0 {
- fmt.Printf(" Missing GitHub releases: %s\n", strings.Join(missingGitHub, ", "))
- }
- }
- }
- }
-
+ if githubOrg != nil && githubOrg.Name != "" {
+ githubReleases, err := releaseManager.GetGitHubReleases(githubOrg.Name, repoName)
+ if err != nil {
+ fmt.Printf(" Error checking GitHub releases: %v\n", err)
+ } else {
+ missingGitHub = releaseManager.FindMissingReleases(localTags, githubReleases)
+ // Filter out tags that should be skipped per config
+ if len(missingGitHub) > 0 {
+ var filtered []string
+ var skipped []string
+ for _, t := range missingGitHub {
+ if cfg.ShouldSkipRelease(repoName, t) {
+ skipped = append(skipped, t)
+ } else {
+ filtered = append(filtered, t)
+ }
+ }
+ if len(skipped) > 0 {
+ fmt.Printf(" Skipping GitHub releases per config for tags: %s\n", strings.Join(skipped, ", "))
+ }
+ missingGitHub = filtered
+ if len(missingGitHub) > 0 {
+ fmt.Printf(" Missing GitHub releases: %s\n", strings.Join(missingGitHub, ", "))
+ }
+ }
+ }
+ }
+
// Check Codeberg releases if Codeberg is configured
var missingCodeberg []string
codebergOrg := cfg.FindCodebergOrg()
- if codebergOrg != nil && codebergOrg.Name != "" {
- codebergReleases, err := releaseManager.GetCodebergReleases(codebergOrg.Name, repoName)
- if err != nil {
- fmt.Printf(" Error checking Codeberg releases: %v\n", err)
- } else {
- missingCodeberg = releaseManager.FindMissingReleases(localTags, codebergReleases)
- // Filter out tags that should be skipped per config
- if len(missingCodeberg) > 0 {
- var filtered []string
- var skipped []string
- for _, t := range missingCodeberg {
- if cfg.ShouldSkipRelease(repoName, t) {
- skipped = append(skipped, t)
- } else {
- filtered = append(filtered, t)
- }
- }
- if len(skipped) > 0 {
- fmt.Printf(" Skipping Codeberg releases per config for tags: %s\n", strings.Join(skipped, ", "))
- }
- missingCodeberg = filtered
- if len(missingCodeberg) > 0 {
- fmt.Printf(" Missing Codeberg releases: %s\n", strings.Join(missingCodeberg, ", "))
- }
- }
- }
- }
-
+ if codebergOrg != nil && codebergOrg.Name != "" {
+ codebergReleases, err := releaseManager.GetCodebergReleases(codebergOrg.Name, repoName)
+ if err != nil {
+ fmt.Printf(" Error checking Codeberg releases: %v\n", err)
+ } else {
+ missingCodeberg = releaseManager.FindMissingReleases(localTags, codebergReleases)
+ // Filter out tags that should be skipped per config
+ if len(missingCodeberg) > 0 {
+ var filtered []string
+ var skipped []string
+ for _, t := range missingCodeberg {
+ if cfg.ShouldSkipRelease(repoName, t) {
+ skipped = append(skipped, t)
+ } else {
+ filtered = append(filtered, t)
+ }
+ }
+ if len(skipped) > 0 {
+ fmt.Printf(" Skipping Codeberg releases per config for tags: %s\n", strings.Join(skipped, ", "))
+ }
+ missingCodeberg = filtered
+ if len(missingCodeberg) > 0 {
+ fmt.Printf(" Missing Codeberg releases: %s\n", strings.Join(missingCodeberg, ", "))
+ }
+ }
+ }
+ }
+
// Create missing releases with confirmation
- if len(missingGitHub) > 0 && githubOrg != nil {
- for _, tag := range missingGitHub {
- // Skip if configured to skip this repo/tag
- if cfg.ShouldSkipRelease(repoName, tag) {
- fmt.Printf(" Skipping GitHub release for %s:%s per config skip_releases\n", repoName, tag)
- continue
- }
+ if len(missingGitHub) > 0 && githubOrg != nil {
+ for _, tag := range missingGitHub {
+ // Skip if configured to skip this repo/tag
+ if cfg.ShouldSkipRelease(repoName, tag) {
+ fmt.Printf(" Skipping GitHub release for %s:%s per config skip_releases\n", repoName, tag)
+ continue
+ }
// Get commits for this tag
commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag)
if err != nil {
commits = []string{}
}
-
+
// Generate release notes
var releaseNotes string
if flags.AIReleaseNotes {
@@ -295,16 +295,16 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
releaseNotes = releaseManager.GenerateReleaseNotes(repoPath, tag, localTags)
}
-
+
// Print release notes to stdout
fmt.Printf("\n%s\n", strings.Repeat("=", 70))
fmt.Printf("Release Notes for %s/%s tag %s:\n", githubOrg.Name, repoName, tag)
fmt.Printf("%s\n", strings.Repeat("-", 70))
fmt.Println(releaseNotes)
fmt.Printf("%s\n\n", strings.Repeat("=", 70))
-
+
msg := fmt.Sprintf("Create GitHub release for %s/%s tag %s?", githubOrg.Name, repoName, tag)
-
+
// Check if auto-create is enabled
createRelease := false
if flags.AutoCreateReleases {
@@ -313,7 +313,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
createRelease = release.PromptConfirmation(msg)
}
-
+
if createRelease {
if err := releaseManager.CreateGitHubRelease(githubOrg.Name, repoName, tag, releaseNotes); err != nil {
fmt.Printf(" Error creating GitHub release: %v\n", err)
@@ -323,24 +323,24 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
}
}
-
- if len(missingCodeberg) > 0 && codebergOrg != nil {
- // Ensure Releases feature is enabled on Codeberg before creating releases
- if err := releaseManager.EnsureCodebergReleasesEnabled(codebergOrg.Name, repoName); err != nil {
- fmt.Printf(" Warning: Could not ensure Codeberg releases are enabled: %v\n", err)
- }
- for _, tag := range missingCodeberg {
- // Skip if configured to skip this repo/tag
- if cfg.ShouldSkipRelease(repoName, tag) {
- fmt.Printf(" Skipping Codeberg release for %s:%s per config skip_releases\n", repoName, tag)
- continue
- }
+
+ if len(missingCodeberg) > 0 && codebergOrg != nil {
+ // Ensure Releases feature is enabled on Codeberg before creating releases
+ if err := releaseManager.EnsureCodebergReleasesEnabled(codebergOrg.Name, repoName); err != nil {
+ fmt.Printf(" Warning: Could not ensure Codeberg releases are enabled: %v\n", err)
+ }
+ for _, tag := range missingCodeberg {
+ // Skip if configured to skip this repo/tag
+ if cfg.ShouldSkipRelease(repoName, tag) {
+ fmt.Printf(" Skipping Codeberg release for %s:%s per config skip_releases\n", repoName, tag)
+ continue
+ }
// Get commits for this tag
commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag)
if err != nil {
commits = []string{}
}
-
+
// Generate release notes
var releaseNotes string
if flags.AIReleaseNotes {
@@ -378,16 +378,16 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
releaseNotes = releaseManager.GenerateReleaseNotes(repoPath, tag, localTags)
}
-
+
// Print release notes to stdout
fmt.Printf("\n%s\n", strings.Repeat("=", 70))
fmt.Printf("Release Notes for %s/%s tag %s:\n", codebergOrg.Name, repoName, tag)
fmt.Printf("%s\n", strings.Repeat("-", 70))
fmt.Println(releaseNotes)
fmt.Printf("%s\n\n", strings.Repeat("=", 70))
-
+
msg := fmt.Sprintf("Create Codeberg release for %s/%s tag %s?", codebergOrg.Name, repoName, tag)
-
+
// Check if auto-create is enabled
createRelease := false
if flags.AutoCreateReleases {
@@ -396,7 +396,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
createRelease = release.PromptConfirmation(msg)
}
-
+
if createRelease {
if err := releaseManager.CreateCodebergRelease(codebergOrg.Name, repoName, tag, releaseNotes); err != nil {
fmt.Printf(" Error creating Codeberg release: %v\n", err)
@@ -406,7 +406,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
}
}
-
+
// Update existing releases if requested
if flags.UpdateReleases {
// Update GitHub releases
@@ -419,13 +419,13 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
if !isVersionTag(tag) {
continue
}
-
+
// Get commits for this tag
commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag)
if err != nil {
commits = []string{}
}
-
+
// Generate AI release notes
if flags.AIReleaseNotes {
// Check cache first (unless --force is used)
@@ -464,16 +464,16 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
fmt.Printf(" Warning: Failed to save cache: %v\n", err)
}
}
-
+
// Print release notes to stdout
fmt.Printf("\n%s\n", strings.Repeat("=", 70))
fmt.Printf("Updated Release Notes for %s/%s tag %s:\n", githubOrg.Name, repoName, tag)
fmt.Printf("%s\n", strings.Repeat("-", 70))
fmt.Println(aiNotes)
fmt.Printf("%s\n\n", strings.Repeat("=", 70))
-
+
msg := fmt.Sprintf("Update GitHub release for %s/%s tag %s?", githubOrg.Name, repoName, tag)
-
+
updateRelease := false
if flags.AutoCreateReleases {
fmt.Printf(" Auto-updating GitHub release for %s/%s tag %s\n", githubOrg.Name, repoName, tag)
@@ -481,7 +481,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
updateRelease = release.PromptConfirmation(msg)
}
-
+
if updateRelease {
if err := releaseManager.UpdateGitHubRelease(githubOrg.Name, repoName, tag, aiNotes); err != nil {
fmt.Printf(" Error updating GitHub release: %v\n", err)
@@ -493,7 +493,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
}
}
-
+
// Update Codeberg releases
if codebergOrg != nil && codebergOrg.Name != "" {
codebergReleases, err := releaseManager.GetCodebergReleases(codebergOrg.Name, repoName)
@@ -504,13 +504,13 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
if !isVersionTag(tag) {
continue
}
-
+
// Get commits for this tag
commits, err := releaseManager.GetCommitsSinceTag(repoPath, "", tag)
if err != nil {
commits = []string{}
}
-
+
// Generate AI release notes
if flags.AIReleaseNotes {
// Check cache first (unless --force is used)
@@ -549,16 +549,16 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
fmt.Printf(" Warning: Failed to save cache: %v\n", err)
}
}
-
+
// Print release notes to stdout
fmt.Printf("\n%s\n", strings.Repeat("=", 70))
fmt.Printf("Updated Release Notes for %s/%s tag %s:\n", codebergOrg.Name, repoName, tag)
fmt.Printf("%s\n", strings.Repeat("-", 70))
fmt.Println(aiNotes)
fmt.Printf("%s\n\n", strings.Repeat("=", 70))
-
+
msg := fmt.Sprintf("Update Codeberg release for %s/%s tag %s?", codebergOrg.Name, repoName, tag)
-
+
updateRelease := false
if flags.AutoCreateReleases {
fmt.Printf(" Auto-updating Codeberg release for %s/%s tag %s\n", codebergOrg.Name, repoName, tag)
@@ -566,7 +566,7 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
} else {
updateRelease = release.PromptConfirmation(msg)
}
-
+
if updateRelease {
if err := releaseManager.UpdateCodebergRelease(codebergOrg.Name, repoName, tag, aiNotes); err != nil {
fmt.Printf(" Error updating Codeberg release: %v\n", err)
@@ -580,25 +580,25 @@ func HandleCheckReleasesForRepos(cfg *config.Config, flags *Flags, repositories
}
}
}
-
+
return 0
}
// loadAIReleaseNotesCache loads the AI release notes cache from disk
func loadAIReleaseNotesCache(cacheFile string) map[string]string {
cache := make(map[string]string)
-
+
data, err := os.ReadFile(cacheFile)
if err != nil {
// Cache file doesn't exist yet, return empty cache
return cache
}
-
+
if err := json.Unmarshal(data, &cache); err != nil {
fmt.Printf("Warning: Failed to parse AI release notes cache: %v\n", err)
return make(map[string]string)
}
-
+
fmt.Printf("Loaded AI release notes cache with %d entries\n", len(cache))
return cache
}
@@ -609,11 +609,11 @@ func saveAIReleaseNotesCache(cacheFile string, cache map[string]string) error {
if err != nil {
return fmt.Errorf("failed to marshal cache: %w", err)
}
-
+
if err := os.WriteFile(cacheFile, data, 0644); err != nil {
return fmt.Errorf("failed to write cache file: %w", err)
}
-
+
// Don't print on every save since we save after each generation
return nil
}
diff --git a/internal/cli/showcase_handler.go b/internal/cli/showcase_handler.go
index 929ce95..642313f 100644
--- a/internal/cli/showcase_handler.go
+++ b/internal/cli/showcase_handler.go
@@ -20,21 +20,21 @@ func HandleShowcase(cfg *config.Config, flags *Flags) int {
// Process all repositories for --sync-all or public sync operations
fmt.Println("\nGenerating project showcase for all repositories...")
}
-
+
// Create showcase generator
generator := showcase.New(cfg, flags.WorkDir)
-
+
// Set AI tool if specified
if flags.AITool != "" {
generator.SetAITool(flags.AITool)
}
-
+
// Generate showcase with optional filter
if err := generator.GenerateShowcase(repoFilter, flags.Force); err != nil {
log.Printf("ERROR: Failed to generate showcase: %v\n", err)
return 1
}
-
+
fmt.Println("Showcase generated successfully!")
return 0
-} \ No newline at end of file
+}
diff --git a/internal/cli/showcase_only_handler.go b/internal/cli/showcase_only_handler.go
index 2055612..1a2a835 100644
--- a/internal/cli/showcase_only_handler.go
+++ b/internal/cli/showcase_only_handler.go
@@ -14,90 +14,90 @@ import (
// HandleShowcaseOnly handles showcase generation without syncing
// It will clone repositories if they don't exist locally, but won't sync changes
func HandleShowcaseOnly(cfg *config.Config, flags *Flags) int {
- // If a specific repo is requested, only generate for that repo
- if flags.SyncRepo != "" {
- repo := flags.SyncRepo
-
- // Ensure the repository is cloned
- syncer := sync.New(cfg, flags.WorkDir)
- syncer.SetBackupEnabled(false)
- if err := syncer.EnsureRepositoryCloned(repo); err != nil {
- fmt.Printf("ERROR: Failed to clone %s: %v\n", repo, err)
- return 1
- }
-
- // Generate showcase for just this repository
- fmt.Printf("\nGenerating showcase for repository: %s...\n", repo)
- generator := showcase.New(cfg, flags.WorkDir)
- if flags.AITool != "" {
- generator.SetAITool(flags.AITool)
- }
- if err := generator.GenerateShowcase([]string{repo}, flags.Force); err != nil {
- log.Printf("ERROR: Failed to generate showcase for %s: %v\n", repo, err)
- return 1
- }
- fmt.Println("Showcase generation completed!")
- return 0
- }
-
- // Otherwise, process all repositories
- allRepos, err := getAllRepositories(cfg)
- if err != nil {
- log.Printf("ERROR: Failed to get repositories: %v\n", err)
- return 1
- }
- if len(allRepos) == 0 {
- fmt.Println("No repositories found")
- return 1
- }
- fmt.Printf("Found %d repositories total\n", len(allRepos))
-
- // Create a minimal syncer just for cloning
- syncer := sync.New(cfg, flags.WorkDir)
- syncer.SetBackupEnabled(false) // Never use backup in showcase-only mode
-
- // Ensure repositories are cloned (but not synced)
- fmt.Println("\nEnsuring repositories are cloned locally...")
- for _, repo := range allRepos {
- if err := syncer.EnsureRepositoryCloned(repo); err != nil {
- fmt.Printf("WARNING: Failed to clone %s: %v\n", repo, err)
- // Continue with other repos
- }
- }
-
- // Generate showcase for all repositories
- fmt.Println("\nGenerating showcase for all repositories...")
- generator := showcase.New(cfg, flags.WorkDir)
-
- // Set AI tool if specified
- if flags.AITool != "" {
- generator.SetAITool(flags.AITool)
- }
-
- // Pass empty filter to process all repos
- if err := generator.GenerateShowcase(nil, flags.Force); err != nil {
- log.Printf("ERROR: Failed to generate showcase: %v\n", err)
- return 1
- }
-
- fmt.Println("Showcase generation completed!")
- return 0
+ // If a specific repo is requested, only generate for that repo
+ if flags.SyncRepo != "" {
+ repo := flags.SyncRepo
+
+ // Ensure the repository is cloned
+ syncer := sync.New(cfg, flags.WorkDir)
+ syncer.SetBackupEnabled(false)
+ if err := syncer.EnsureRepositoryCloned(repo); err != nil {
+ fmt.Printf("ERROR: Failed to clone %s: %v\n", repo, err)
+ return 1
+ }
+
+ // Generate showcase for just this repository
+ fmt.Printf("\nGenerating showcase for repository: %s...\n", repo)
+ generator := showcase.New(cfg, flags.WorkDir)
+ if flags.AITool != "" {
+ generator.SetAITool(flags.AITool)
+ }
+ if err := generator.GenerateShowcase([]string{repo}, flags.Force); err != nil {
+ log.Printf("ERROR: Failed to generate showcase for %s: %v\n", repo, err)
+ return 1
+ }
+ fmt.Println("Showcase generation completed!")
+ return 0
+ }
+
+ // Otherwise, process all repositories
+ allRepos, err := getAllRepositories(cfg)
+ if err != nil {
+ log.Printf("ERROR: Failed to get repositories: %v\n", err)
+ return 1
+ }
+ if len(allRepos) == 0 {
+ fmt.Println("No repositories found")
+ return 1
+ }
+ fmt.Printf("Found %d repositories total\n", len(allRepos))
+
+ // Create a minimal syncer just for cloning
+ syncer := sync.New(cfg, flags.WorkDir)
+ syncer.SetBackupEnabled(false) // Never use backup in showcase-only mode
+
+ // Ensure repositories are cloned (but not synced)
+ fmt.Println("\nEnsuring repositories are cloned locally...")
+ for _, repo := range allRepos {
+ if err := syncer.EnsureRepositoryCloned(repo); err != nil {
+ fmt.Printf("WARNING: Failed to clone %s: %v\n", repo, err)
+ // Continue with other repos
+ }
+ }
+
+ // Generate showcase for all repositories
+ fmt.Println("\nGenerating showcase for all repositories...")
+ generator := showcase.New(cfg, flags.WorkDir)
+
+ // Set AI tool if specified
+ if flags.AITool != "" {
+ generator.SetAITool(flags.AITool)
+ }
+
+ // Pass empty filter to process all repos
+ if err := generator.GenerateShowcase(nil, flags.Force); err != nil {
+ log.Printf("ERROR: Failed to generate showcase: %v\n", err)
+ return 1
+ }
+
+ fmt.Println("Showcase generation completed!")
+ return 0
}
// getAllRepositories collects all unique repository names from all sources
func getAllRepositories(cfg *config.Config) ([]string, error) {
repoMap := make(map[string]bool)
-
+
// Add configured repositories
for _, repo := range cfg.Repositories {
repoMap[repo] = true
}
-
+
// Add Codeberg public repos if configured
if codebergOrg := cfg.FindCodebergOrg(); codebergOrg != nil {
fmt.Printf("Fetching public repositories from Codeberg user/org: %s...\n", codebergOrg.Name)
client := codeberg.NewClient(codebergOrg.Name, codebergOrg.CodebergToken)
-
+
repos, err := client.ListPublicRepos()
if err != nil {
// Try as user
@@ -106,17 +106,17 @@ func getAllRepositories(cfg *config.Config) ([]string, error) {
fmt.Printf("Warning: Failed to fetch Codeberg repos: %v\n", err)
}
}
-
+
for _, repo := range repos {
repoMap[repo.Name] = true
}
}
-
+
// Add GitHub public repos if configured
if githubOrg := cfg.FindGitHubOrg(); githubOrg != nil {
fmt.Printf("Fetching public repositories from GitHub user/org: %s...\n", githubOrg.Name)
client := github.NewClient(githubOrg.GitHubToken, githubOrg.Name)
-
+
if client.HasToken() {
repos, err := client.ListPublicRepos()
if err != nil {
@@ -130,12 +130,12 @@ func getAllRepositories(cfg *config.Config) ([]string, error) {
fmt.Println("Warning: No GitHub token found, skipping GitHub repos")
}
}
-
+
// Convert map to slice
var allRepos []string
for repo := range repoMap {
allRepos = append(allRepos, repo)
}
-
+
return allRepos, nil
}
diff --git a/internal/cli/sync_handlers.go b/internal/cli/sync_handlers.go
index 09b993a..5c0c9bf 100644
--- a/internal/cli/sync_handlers.go
+++ b/internal/cli/sync_handlers.go
@@ -20,7 +20,7 @@ func HandleSync(cfg *config.Config, flags *Flags) int {
return 1
}
}
-
+
// If create-codeberg-repos is enabled, create the repo if needed
if flags.CreateCodebergRepos {
if err := createCodebergRepoIfNeeded(cfg, flags.SyncRepo); err != nil {
@@ -28,20 +28,20 @@ func HandleSync(cfg *config.Config, flags *Flags) int {
return 1
}
}
-
+
syncer := sync.New(cfg, flags.WorkDir)
syncer.SetBackupEnabled(flags.Backup)
- if err := syncer.SyncRepository(flags.SyncRepo); err != nil {
- log.Fatal("Sync failed:", err)
- return 1
- }
- // Also sync descriptions for this single repository
- descCache := loadDescriptionCache(flags.WorkDir)
- syncRepoDescriptions(cfg, flags.DryRun, flags.SyncRepo, "", "", descCache)
- if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil {
- fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
- }
- return 0
+ if err := syncer.SyncRepository(flags.SyncRepo); err != nil {
+ log.Fatal("Sync failed:", err)
+ return 1
+ }
+ // Also sync descriptions for this single repository
+ descCache := loadDescriptionCache(flags.WorkDir)
+ syncRepoDescriptions(cfg, flags.DryRun, flags.SyncRepo, "", "", descCache)
+ if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil {
+ fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
+ }
+ return 0
}
// HandleSyncAll handles syncing all configured repositories
@@ -71,15 +71,15 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int {
}
}
- syncer := sync.New(cfg, flags.WorkDir)
- syncer.SetBackupEnabled(flags.Backup)
- successCount := 0
- // Load descriptions cache
- descCache := loadDescriptionCache(flags.WorkDir)
-
+ syncer := sync.New(cfg, flags.WorkDir)
+ syncer.SetBackupEnabled(flags.Backup)
+ successCount := 0
+ // Load descriptions cache
+ descCache := loadDescriptionCache(flags.WorkDir)
+
for i, repo := range cfg.Repositories {
fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(cfg.Repositories), repo)
-
+
// Create GitHub repo if needed
if hasGithubClient {
if err := createRepoWithClient(&githubClient, repo, fmt.Sprintf("Mirror of %s", repo)); err != nil {
@@ -88,7 +88,7 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int {
return 1
}
}
-
+
// Create Codeberg repo if needed
if hasCodebergClient {
fmt.Printf("Checking/creating Codeberg repository %s...\n", repo)
@@ -96,28 +96,28 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int {
fmt.Printf("Warning: Failed to create Codeberg repo %s: %v\n", repo, err)
}
}
-
- if err := syncer.SyncRepository(repo); err != nil {
- fmt.Printf("ERROR: Failed to sync %s: %v\n", repo, err)
- fmt.Printf("Stopping sync due to error.\n")
- return 1
- }
- successCount++
- // Sync descriptions after repo sync
- syncRepoDescriptions(cfg, flags.DryRun, repo, "", "", descCache)
- }
- // Save descriptions cache
- if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil {
- fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
- }
+
+ if err := syncer.SyncRepository(repo); err != nil {
+ fmt.Printf("ERROR: Failed to sync %s: %v\n", repo, err)
+ fmt.Printf("Stopping sync due to error.\n")
+ return 1
+ }
+ successCount++
+ // Sync descriptions after repo sync
+ syncRepoDescriptions(cfg, flags.DryRun, repo, "", "", 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("\nSuccessfully synced all %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)
@@ -144,7 +144,7 @@ func HandleSyncAll(cfg *config.Config, flags *Flags) int {
fmt.Printf(strings.Repeat("=", 70))
fmt.Printf("\n")
}
-
+
return 0
}
@@ -157,9 +157,9 @@ func HandleSyncCodebergPublic(cfg *config.Config, flags *Flags) int {
}
fmt.Printf("Fetching public repositories from Codeberg user/org: %s...\n", codebergOrg.Name)
-
+
client := codeberg.NewClient(codebergOrg.Name, codebergOrg.CodebergToken)
-
+
// Try fetching as organization first, then as user
repos, err := client.ListPublicRepos()
if err != nil {
@@ -172,15 +172,15 @@ func HandleSyncCodebergPublic(cfg *config.Config, flags *Flags) int {
repoNames := codeberg.GetRepoNames(repos)
fmt.Printf("Found %d public repositories on Codeberg\n", len(repoNames))
-
+
if len(repoNames) == 0 {
fmt.Println("No public repositories found")
return 0
}
- // Show the repositories that will be synced
- showReposToSync(repoNames)
-
+ // Show the repositories that will be synced
+ showReposToSync(repoNames)
+
if flags.DryRun {
fmt.Printf("\n[DRY RUN] Would sync %d repositories from Codeberg to GitHub\n", len(repoNames))
if flags.CreateGitHubRepos {
@@ -190,11 +190,11 @@ func HandleSyncCodebergPublic(cfg *config.Config, flags *Flags) int {
return 0
}
}
-
- if !flags.DryRun {
- return syncCodebergRepos(cfg, flags, repos, repoNames)
- }
-
+
+ if !flags.DryRun {
+ return syncCodebergRepos(cfg, flags, repos, repoNames)
+ }
+
return 0
}
@@ -207,14 +207,14 @@ func HandleSyncGitHubPublic(cfg *config.Config, flags *Flags) int {
}
fmt.Printf("Fetching public repositories from GitHub user/org: %s...\n", githubOrg.Name)
-
+
client := github.NewClient(githubOrg.GitHubToken, githubOrg.Name)
if !client.HasToken() {
fmt.Println("ERROR: GitHub token required to list repositories")
fmt.Println("Set GITHUB_TOKEN env var or create ~/.gitsyncer_github_token file")
return 1
}
-
+
repos, err := client.ListPublicRepos()
if err != nil {
log.Fatal("Failed to fetch repositories:", err)
@@ -222,15 +222,15 @@ func HandleSyncGitHubPublic(cfg *config.Config, flags *Flags) int {
repoNames := github.GetRepoNames(repos)
fmt.Printf("Found %d public repositories on GitHub\n", len(repoNames))
-
+
if len(repoNames) == 0 {
fmt.Println("No public repositories found")
return 0
}
- // Show the repositories that will be synced
- showReposToSync(repoNames)
-
+ // Show the repositories that will be synced
+ showReposToSync(repoNames)
+
if flags.DryRun {
fmt.Printf("\n[DRY RUN] Would sync %d repositories from GitHub to Codeberg\n", len(repoNames))
if flags.CreateCodebergRepos {
@@ -238,11 +238,11 @@ func HandleSyncGitHubPublic(cfg *config.Config, flags *Flags) int {
}
return 0
}
-
- if !flags.DryRun {
- return syncGitHubRepos(cfg, flags, repos, repoNames)
- }
-
+
+ if !flags.DryRun {
+ return syncGitHubRepos(cfg, flags, repos, repoNames)
+ }
+
return 0
}
@@ -253,14 +253,14 @@ func createGitHubRepoIfNeeded(cfg *config.Config, repoName string) error {
if githubOrg == nil {
return nil
}
-
+
fmt.Printf("Initializing GitHub client for organization: %s\n", githubOrg.Name)
githubClient := github.NewClient(githubOrg.GitHubToken, githubOrg.Name)
if !githubClient.HasToken() {
fmt.Println("Warning: No GitHub token found. Cannot create repository.")
return nil
}
-
+
fmt.Println("Checking/creating GitHub repository...")
return githubClient.CreateRepo(repoName, fmt.Sprintf("Mirror of %s", repoName), false)
}
@@ -270,14 +270,14 @@ func createCodebergRepoIfNeeded(cfg *config.Config, repoName string) error {
if codebergOrg == nil {
return nil
}
-
+
fmt.Printf("Initializing Codeberg client for organization: %s\n", codebergOrg.Name)
codebergClient := codeberg.NewClient(codebergOrg.Name, codebergOrg.CodebergToken)
if !codebergClient.HasToken() {
fmt.Println("Warning: No Codeberg token found. Cannot create repository.")
return nil
}
-
+
fmt.Println("Checking/creating Codeberg repository...")
return codebergClient.CreateRepo(repoName, fmt.Sprintf("Mirror of %s", repoName), false)
}
@@ -288,14 +288,14 @@ func initGitHubClient(cfg *config.Config) *github.Client {
fmt.Println("Warning: --create-github-repos specified but no GitHub organization found in config")
return nil
}
-
+
fmt.Printf("Initializing GitHub client for organization: %s\n", githubOrg.Name)
githubClient := github.NewClient(githubOrg.GitHubToken, githubOrg.Name)
if !githubClient.HasToken() {
fmt.Println("Warning: No GitHub token found. Cannot create repositories.")
return nil
}
-
+
fmt.Println("GitHub client initialized successfully with token")
return &githubClient
}
@@ -346,25 +346,25 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
hasGithubClient = true
}
}
-
- fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames))
- // Load descriptions cache
- descCache := loadDescriptionCache(flags.WorkDir)
-
+ 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)
successCount := 0
-
+
// Create map for descriptions
repoMap := make(map[string]codeberg.Repository)
for _, repo := range repos {
repoMap[repo.Name] = repo
}
-
- for i, repoName := range repoNames {
- fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
-
+
+ for i, repoName := range repoNames {
+ fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
+
// Create GitHub repo if needed
if hasGithubClient && flags.CreateGitHubRepos {
codebergRepo := repoMap[repoName]
@@ -372,42 +372,42 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
if description == "" {
description = fmt.Sprintf("Mirror of %s from Codeberg", repoName)
}
-
+
fmt.Printf("Checking/creating GitHub repository %s...\n", repoName)
err := githubClient.CreateRepo(repoName, description, false)
if err != nil {
fmt.Printf("Warning: Failed to create GitHub repo %s: %v\n", repoName, err)
}
}
-
- if err := 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
- }
- successCount++
-
- // After syncing, sync descriptions according to precedence
- if cbRepo, ok := repoMap[repoName]; ok {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, cbRepo.Description, "", descCache)
- } else {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", 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")
+
+ if err := 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
+ }
+ successCount++
+
+ // After syncing, sync descriptions according to precedence
+ if cbRepo, ok := repoMap[repoName]; ok {
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, cbRepo.Description, "", descCache)
+ } else {
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", 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)
@@ -434,11 +434,11 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
fmt.Printf(strings.Repeat("=", 70))
fmt.Printf("\n")
}
-
+
if !flags.SyncGitHubPublic {
return 0
}
-
+
// Print separator for full sync
printFullSyncSeparator()
return 0
@@ -455,10 +455,10 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
}
}
- fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames))
+ fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames))
- // Load descriptions cache
- descCache := loadDescriptionCache(flags.WorkDir)
+ // Load descriptions cache
+ descCache := loadDescriptionCache(flags.WorkDir)
syncer := sync.New(cfg, flags.WorkDir)
syncer.SetBackupEnabled(flags.Backup)
@@ -470,8 +470,8 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
repoMap[repo.Name] = repo
}
- for i, repoName := range repoNames {
- fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
+ for i, repoName := range repoNames {
+ fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
// Create Codeberg repo if needed
if hasCodebergClient && flags.CreateCodebergRepos {
@@ -488,25 +488,25 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
}
}
- if err := 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
- }
- successCount++
-
- // After syncing, sync descriptions according to precedence
- if ghRepo, ok := repoMap[repoName]; ok {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, "", ghRepo.Description, descCache)
- } else {
- syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", descCache)
- }
- }
-
- // Save descriptions cache
- if err := saveDescriptionCache(flags.WorkDir, descCache); err != nil {
- fmt.Printf("Warning: Failed to save descriptions cache: %v\n", err)
- }
+ if err := 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
+ }
+ successCount++
+
+ // After syncing, sync descriptions according to precedence
+ if ghRepo, ok := repoMap[repoName]; ok {
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, "", ghRepo.Description, descCache)
+ } else {
+ syncRepoDescriptions(cfg, flags.DryRun, repoName, "", "", 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)
@@ -515,7 +515,7 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
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)