From 3a0fdff048bcd03b3e51fc015164b892c8086566 Mon Sep 17 00:00:00 2001 From: Bear Date: Thu, 8 Oct 2026 11:49:04 -0600 Subject: [PATCH] feat: add --prune flag and notify about locally cloned archived repos After a normal run, list local repositories that are archived on GitHub and print the command to remove them. Add --prune/--confirm to the root command as a replacement for the prune-archived subcommand, which is now deprecated. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 18 ++++++------ cmd/prune_archived.go | 67 ++++++++++++++++++++++++++++++------------- cmd/root.go | 17 +++++++++++ 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5f0f21b..0143c1b 100644 --- a/README.md +++ b/README.md @@ -116,24 +116,26 @@ All orgs will be processed sequentially. You can also point to a custom config f github-pokemon --config /path/to/my-config.yaml ``` -### Subcommands +### Pruning archived repositories -#### `prune-archived` - -Remove local directories for repositories that have been archived on GitHub: +After each run, github-pokemon tells you if any locally cloned repositories have been +archived on GitHub, along with the command to remove them. Use `--prune` to remove those +local directories: ```bash # Dry-run (default) — shows what would be removed -github-pokemon prune-archived --org "my-org" --path "./repos" +github-pokemon --prune --org "my-org" --path "./repos" # Actually remove archived repo directories -github-pokemon prune-archived --org "my-org" --path "./repos" --confirm +github-pokemon --prune --org "my-org" --path "./repos" --confirm # Or use config file to prune across all orgs -github-pokemon prune-archived -github-pokemon prune-archived --confirm +github-pokemon --prune +github-pokemon --prune --confirm ``` +> The `prune-archived` subcommand is deprecated; use `--prune` instead. + ### Examples ```bash diff --git a/cmd/prune_archived.go b/cmd/prune_archived.go index 8072c79..5e3b525 100644 --- a/cmd/prune_archived.go +++ b/cmd/prune_archived.go @@ -17,8 +17,9 @@ var ( ) var pruneArchivedCmd = &cobra.Command{ - Use: "prune-archived", - Short: "Remove local directories for repositories that are archived on GitHub", + Use: "prune-archived", + Deprecated: "use the --prune flag instead (e.g. github-pokemon --prune --org my-org --path ./repos)", + Short: "Remove local directories for repositories that are archived on GitHub", Long: `Scans directories in the target path and checks each one against the GitHub API. Any directory that corresponds to an archived repository in the specified organization will be removed. @@ -110,6 +111,48 @@ func runPruneRoot(cmd *cobra.Command) error { return firstErr } +// localArchivedDirs returns the names of directories in entries that match +// archived repositories in repos. +func localArchivedDirs(entries []os.DirEntry, repos []*github.Repository) []string { + archivedSet := make(map[string]bool) + for _, repo := range repos { + if repo.GetArchived() { + archivedSet[repo.GetName()] = true + } + } + + var names []string + for _, entry := range entries { + if entry.IsDir() && archivedSet[entry.Name()] { + names = append(names, entry.Name()) + } + } + return names +} + +// printPruneNotice tells the user which locally cloned repos are archived on +// GitHub and how to remove them. It prints nothing if there are none. +func printPruneNotice(org string, absPath string, repos []*github.Repository) { + entries, err := os.ReadDir(absPath) + if err != nil { + return + } + + names := localArchivedDirs(entries, repos) + if len(names) == 0 { + return + } + + fmt.Println() + fmt.Printf("Notice: %d local repositories are archived on GitHub and can be removed:\n", len(names)) + for _, name := range names { + fmt.Printf(" - %s\n", name) + } + fmt.Println("Preview removal (dry-run):") + fmt.Printf(" github-pokemon --prune --org %q --path %q\n", org, absPath) + fmt.Println("Add --confirm to actually remove them.") +} + func runPruneArchived(ctx context.Context, org string, path string) error { token := os.Getenv("GITHUB_TOKEN") if token == "" { @@ -133,26 +176,10 @@ func runPruneArchived(ctx context.Context, org string, path string) error { return fmt.Errorf("fetching repos for org %s: %w", org, err) } - archivedSet := make(map[string]bool) - for _, repo := range allRepos { - if repo.GetArchived() { - archivedSet[repo.GetName()] = true - } - } - removedCount := 0 skippedCount := 0 - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - name := entry.Name() - if !archivedSet[name] { - continue - } - + for _, name := range localArchivedDirs(entries, allRepos) { dirPath := filepath.Join(absPath, name) if !pruneConfirm { @@ -188,4 +215,4 @@ func runPruneArchived(ctx context.Context, org string, path string) error { } return nil -} \ No newline at end of file +} diff --git a/cmd/root.go b/cmd/root.go index 69c9a18..01cfcd5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,6 +36,7 @@ var ( includeArchived bool noColor bool configPath string + prune bool ) const maxParallelLimit = 50 @@ -109,6 +110,8 @@ func init() { rootCmd.Flags().IntVarP(¶llelLimit, "parallel", "j", 5, "Number of repositories to process in parallel") rootCmd.Flags().BoolVar(&includeArchived, "include-archived", false, "Include archived repositories") + rootCmd.Flags().BoolVar(&prune, "prune", false, "Remove local directories for repositories archived on GitHub instead of syncing (dry-run unless --confirm is set)") + rootCmd.Flags().BoolVar(&pruneConfirm, "confirm", false, "With --prune, actually remove directories (without this flag, runs in dry-run mode)") rootCmd.Flags().BoolVar(&noColor, "no-color", false, "Disable colored output") rootCmd.Flags().StringVar(&configPath, "config", "", "Path to config file (default: ~/.config/github-pokemon/config.yaml)") } @@ -266,6 +269,16 @@ func runRoot(cmd *cobra.Command) error { color.NoColor = true } + if prune { + pruneOrg = organization + prunePath = targetPath + return runPruneRoot(cmd) + } + + if pruneConfirm { + return fmt.Errorf("--confirm can only be used with --prune") + } + // If --org and --path are provided, run in single-org mode (backward compatible). if organization != "" && targetPath != "" { if err := runRootCommand(ctx, organization, targetPath); err != nil { @@ -433,6 +446,10 @@ func runRootCommand(ctx context.Context, org string, path string) error { fmt.Printf("Including archived repositories (--include-archived)\n") } + if !includeArchived { + defer printPruneNotice(org, absTargetPath, allRepos) + } + if repoCount == 0 { fmt.Println("No repositories found. Nothing to process.") return nil