Skip to content
Merged
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
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 47 additions & 20 deletions cmd/prune_archived.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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 {
Expand Down Expand Up @@ -188,4 +215,4 @@ func runPruneArchived(ctx context.Context, org string, path string) error {
}

return nil
}
}
17 changes: 17 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var (
includeArchived bool
noColor bool
configPath string
prune bool
)

const maxParallelLimit = 50
Expand Down Expand Up @@ -109,6 +110,8 @@ func init() {
rootCmd.Flags().IntVarP(&parallelLimit, "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)")
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading