From f1a649641fba32646ffcb09077dcf603e021e1b1 Mon Sep 17 00:00:00 2001 From: Hein Date: Mon, 22 Sep 2025 15:21:35 +0200 Subject: [PATCH] initial --- .vscode/launch.json | 15 +++ cmd/git-package-cleanup/main.go | 211 ++++++++++++++++++++++++++++++++ go.mod | 3 + 3 files changed, 229 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 cmd/git-package-cleanup/main.go create mode 100644 go.mod diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..608d3c6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}" + } + ] +} \ No newline at end of file diff --git a/cmd/git-package-cleanup/main.go b/cmd/git-package-cleanup/main.go new file mode 100644 index 0000000..1ccddfa --- /dev/null +++ b/cmd/git-package-cleanup/main.go @@ -0,0 +1,211 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" +) + +type PackageVersion struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type GitHubClient struct { + token string + owner string + client *http.Client + baseURL string +} + +func NewGitHubClient(token, owner string) *GitHubClient { + return &GitHubClient{ + token: token, + owner: owner, + client: &http.Client{}, + baseURL: "https://api.github.com", + } +} + +func (g *GitHubClient) makeRequest(method, url string) (*http.Response, error) { + req, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+g.token) + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + return g.client.Do(req) +} + +func (g *GitHubClient) getPackageVersions(packageName, packageType string) ([]PackageVersion, error) { + url := fmt.Sprintf("%s/users/%s/packages/%s/%s/versions", g.baseURL, g.owner, packageType, packageName) + + var allVersions []PackageVersion + page := 1 + + for { + pageURL := fmt.Sprintf("%s?page=%d&per_page=100", url, page) + resp, err := g.makeRequest("GET", pageURL) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body)) + } + + var versions []PackageVersion + if err := json.NewDecoder(resp.Body).Decode(&versions); err != nil { + return nil, fmt.Errorf("decode failed: %w", err) + } + + if len(versions) == 0 { + break + } + + allVersions = append(allVersions, versions...) + page++ + } + + return allVersions, nil +} + +func (g *GitHubClient) deleteVersion(packageName, packageType string, versionID int) error { + url := fmt.Sprintf("%s/users/%s/packages/%s/%s/versions/%d", + g.baseURL, g.owner, packageType, packageName, versionID) + + resp, err := g.makeRequest("DELETE", url) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("delete failed %d: %s", resp.StatusCode, string(body)) + } + + return nil +} + +func findVersionsToDelete(versions []PackageVersion, keepVersion string) []PackageVersion { + var toDelete []PackageVersion + found := false + + // Find the version to keep and mark everything after it for deletion + for i := len(versions) - 1; i >= 0; i-- { + if versions[i].Name == keepVersion { + found = true + break + } + toDelete = append([]PackageVersion{versions[i]}, toDelete...) + } + + if !found { + fmt.Printf("Warning: Version '%s' not found in package versions\n", keepVersion) + return nil + } + + return toDelete +} + +func getUserInput(prompt string) string { + fmt.Print(prompt) + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + return strings.TrimSpace(scanner.Text()) +} + +func confirmDeletion(count int) bool { + response := getUserInput(fmt.Sprintf("Delete %d versions? (y/N): ", count)) + return strings.ToLower(response) == "y" +} + +func main() { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + fmt.Println("Error: GITHUB_TOKEN environment variable is required") + fmt.Println("Create a token with 'packages:read' and 'packages:delete' scopes") + os.Exit(1) + } + + owner := getUserInput("Owner (username/org): ") + if owner == "" { + fmt.Println("Error: Owner is required") + os.Exit(1) + } + + packageName := getUserInput("Package name: ") + if packageName == "" { + fmt.Println("Error: Package name is required") + os.Exit(1) + } + + packageType := getUserInput("Package type [npm]: ") + if packageType == "" { + packageType = "npm" + } + + client := NewGitHubClient(token, owner) + + fmt.Printf("Fetching versions for %s/%s...\n", owner, packageName) + versions, err := client.getPackageVersions(packageName, packageType) + if err != nil { + fmt.Printf("Error fetching versions: %v\n", err) + os.Exit(1) + } + + if len(versions) == 0 { + fmt.Println("No versions found") + return + } + + fmt.Printf("\nFound %d versions:\n", len(versions)) + for i, v := range versions { + fmt.Printf("%d. %s (ID: %d)\n", i+1, v.Name, v.ID) + } + + keepVersion := getUserInput("\nVersion to keep (and newer): ") + if keepVersion == "" { + fmt.Println("Error: Version is required") + os.Exit(1) + } + + toDelete := findVersionsToDelete(versions, keepVersion) + if len(toDelete) == 0 { + fmt.Println("No versions to delete") + return + } + + fmt.Printf("\nVersions to delete:\n") + for _, v := range toDelete { + fmt.Printf("- %s (ID: %d)\n", v.Name, v.ID) + } + + if !confirmDeletion(len(toDelete)) { + fmt.Println("Cancelled") + return + } + + fmt.Println("\nDeleting versions...") + deleted := 0 + for _, v := range toDelete { + if err := client.deleteVersion(packageName, packageType, v.ID); err != nil { + fmt.Printf("Failed to delete %s: %v\n", v.Name, err) + } else { + fmt.Printf("Deleted %s\n", v.Name) + deleted++ + } + } + + fmt.Printf("\nDeleted %d/%d versions\n", deleted, len(toDelete)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c35099b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.warky.dev/wdevs/go-git-tools + +go 1.25.1