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
119 changes: 119 additions & 0 deletions cmd/prune_archived.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package cmd

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/google/go-github/v84/github"
"github.com/spf13/cobra"
)

var (
pruneOrg string
prunePath string
dryRun bool
)

var pruneArchivedCmd = &cobra.Command{
Use: "prune-archived",
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.

By default runs in dry-run mode (--dry-run) so you can preview what would be deleted.
Pass --dry-run=false to actually remove directories.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runPruneArchived(cmd.Context())
},
SilenceUsage: true,
}

func init() {
pruneArchivedCmd.Flags().StringVarP(&pruneOrg, "org", "o", "", "GitHub organization to check repositories against (required)")
pruneArchivedCmd.Flags().StringVarP(&prunePath, "path", "p", "", "Local path containing cloned repositories (required)")
pruneArchivedCmd.Flags().BoolVar(&dryRun, "dry-run", true, "Preview what would be removed without deleting anything")

_ = pruneArchivedCmd.MarkFlagRequired("org")
_ = pruneArchivedCmd.MarkFlagRequired("path")

rootCmd.AddCommand(pruneArchivedCmd)
}

func runPruneArchived(ctx context.Context) error {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
return fmt.Errorf("GITHUB_TOKEN not set: set it with: export GITHUB_TOKEN=\"your-personal-access-token\"")
}

absPath, err := filepath.Abs(prunePath)
if err != nil {
return fmt.Errorf("resolving target path: %w", err)
}

entries, err := os.ReadDir(absPath)
if err != nil {
return fmt.Errorf("reading target directory: %w", err)
}

client := github.NewClient(nil).WithAuthToken(token)

allRepos, err := fetchOrgRepos(ctx, client, pruneOrg)
if err != nil {
return 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
}

dirPath := filepath.Join(absPath, name)

if dryRun {
fmt.Printf("[dry-run] would remove: %s\n", dirPath)
removedCount++
continue
}

fmt.Printf("Removing archived repository: %s\n", dirPath)
if err := os.RemoveAll(dirPath); err != nil {
fmt.Printf("Error removing %s: %v\n", dirPath, err)
skippedCount++
continue
}
removedCount++
}

if dryRun {
fmt.Printf("\nDry-run complete: %d archived repositories would be removed\n", removedCount)
if removedCount > 0 {
fmt.Println("Run with --dry-run=false to actually remove them")
}
} else {
fmt.Printf("\nSummary: Removed %d archived repositories", removedCount)
if skippedCount > 0 {
fmt.Printf(" (%d failed)", skippedCount)
}
fmt.Println()
}

return nil
}
40 changes: 26 additions & 14 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ var version = func() string {
}()

var (
organization string
targetPath string
skipUpdate bool
verbose bool
parallelLimit int
organization string
targetPath string
skipUpdate bool
verbose bool
parallelLimit int
includeArchived bool
noColor bool
)

Expand Down Expand Up @@ -61,7 +62,7 @@ var rootCmd = &cobra.Command{
Short: "Clone non-archived repositories for a GitHub organization",
Long: `This tool fetches all non-archived repositories for a specified GitHub organization
and either clones them (if they don't exist locally) or fetches remote updates (if they do exist)
to a specified local path.
to a specified local path. Use --include-archived to also process archived repositories.

The tool will never modify local branches - it only fetches remote tracking information
for existing repositories.
Expand Down Expand Up @@ -94,6 +95,7 @@ func init() {
rootCmd.Flags().BoolVarP(&skipUpdate, "skip-update", "s", false, "Skip updating existing repositories")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
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(&noColor, "no-color", false, "Disable colored output")

Expand Down Expand Up @@ -292,21 +294,31 @@ func runRootCommand(ctx context.Context) error {
return err
}

nonArchivedRepos := filterNonArchived(allRepos)
nonArchivedCount := len(nonArchivedRepos)
var repos []*github.Repository
if includeArchived {
repos = allRepos
} else {
repos = filterNonArchived(allRepos)
}
repoCount := len(repos)

nonArchivedCount := len(filterNonArchived(allRepos))
fmt.Printf("Found %d repositories in organization %s (%d non-archived)\n",
len(allRepos), organization, nonArchivedCount)

if nonArchivedCount == 0 {
fmt.Println("No non-archived repositories found. Nothing to process.")
if includeArchived {
fmt.Printf("Including archived repositories (--include-archived)\n")
}

if repoCount == 0 {
fmt.Println("No repositories found. Nothing to process.")
return nil
}

fmt.Printf("Processing repositories with %d parallel workers\n\n", parallelLimit)

jobs := make(chan *github.Repository, nonArchivedCount)
results := make(chan repoResult, nonArchivedCount)
jobs := make(chan *github.Repository, repoCount)
results := make(chan repoResult, repoCount)

var wg sync.WaitGroup
for w := 0; w < parallelLimit; w++ {
Expand All @@ -317,7 +329,7 @@ func runRootCommand(ctx context.Context) error {
}()
}

for _, repo := range nonArchivedRepos {
for _, repo := range repos {
jobs <- repo
}
close(jobs)
Expand All @@ -327,7 +339,7 @@ func runRootCommand(ctx context.Context) error {
close(results)
}()

errorCount, _ := collectAndDisplay(results, nonArchivedCount, verbose, startTime)
errorCount, _ := collectAndDisplay(results, repoCount, verbose, startTime)

if errorCount > 0 {
return fmt.Errorf("failed to process %d repositories", errorCount)
Expand Down