summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-28 00:45:19 +0300
committerPaul Buetow <paul@buetow.org>2025-06-28 00:45:19 +0300
commit0c072d964d4d07e69d1c0af1f3b09f9adc543571 (patch)
tree64df6d5011ac946c0439fc98291e66a08d2e88f1 /internal
parent921782160c1f07577035db56b5461a2edc3b30a8 (diff)
feat: add --delete-repo command to delete repositories from all organizations
- Added --delete-repo flag that accepts a repository name - Implemented DeleteRepo() methods in both GitHub and Codeberg API clients - Created HandleDeleteRepo handler with interactive confirmation - Shows clear status of repository across all configured organizations - Requires user to type "yes" to confirm deletion - Provides detailed feedback on success/failure for each deletion - Updated documentation and usage help 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/cli/flags.go2
-rw-r--r--internal/cli/handlers.go112
-rw-r--r--internal/codeberg/codeberg.go49
-rw-r--r--internal/github/github.go104
4 files changed, 240 insertions, 27 deletions
diff --git a/internal/cli/flags.go b/internal/cli/flags.go
index aac339e..71708a2 100644
--- a/internal/cli/flags.go
+++ b/internal/cli/flags.go
@@ -23,6 +23,7 @@ type Flags struct {
WorkDir string
TestGitHubToken bool
Clean bool
+ DeleteRepo string
}
// ParseFlags parses command-line flags and returns the flags struct
@@ -46,6 +47,7 @@ func ParseFlags() *Flags {
flag.StringVar(&f.WorkDir, "work-dir", "", "working directory for cloning repositories (default: ~/git/gitsyncer-workdir)")
flag.BoolVar(&f.TestGitHubToken, "test-github-token", false, "test GitHub token authentication")
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.Parse()
diff --git a/internal/cli/handlers.go b/internal/cli/handlers.go
index a2a9c94..02df7ae 100644
--- a/internal/cli/handlers.go
+++ b/internal/cli/handlers.go
@@ -1,11 +1,13 @@
package cli
import (
+ "bufio"
"fmt"
"os"
"path/filepath"
"strings"
+ "codeberg.org/snonux/gitsyncer/internal/codeberg"
"codeberg.org/snonux/gitsyncer/internal/config"
"codeberg.org/snonux/gitsyncer/internal/github"
"codeberg.org/snonux/gitsyncer/internal/version"
@@ -144,6 +146,7 @@ func ShowUsage(cfg *config.Config) {
fmt.Println(" gitsyncer --list-orgs List configured organizations")
fmt.Println(" gitsyncer --list-repos List configured repositories")
fmt.Println(" gitsyncer --test-github-token Test GitHub token authentication")
+ fmt.Println(" gitsyncer --delete-repo <name> Delete repository from all organizations")
fmt.Println(" gitsyncer --version Show version information")
fmt.Println("\nOptions:")
fmt.Println(" --config <path> Path to configuration file")
@@ -153,4 +156,113 @@ func ShowUsage(cfg *config.Config) {
fmt.Println(" --dry-run Show what would be done without doing it")
fmt.Println("\nGitHub Token:")
fmt.Println(" Set via: config file, GITHUB_TOKEN env var, or ~/.gitsyncer_github_token file")
+}
+
+// HandleDeleteRepo handles the --delete-repo flag
+func HandleDeleteRepo(cfg *config.Config, repoName string) int {
+ if repoName == "" {
+ fmt.Println("Error: Repository name is required for --delete-repo")
+ return 1
+ }
+
+ 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)
+ exists, err = client.RepoExists(repoName)
+ case "git@codeberg.org":
+ client := codeberg.NewClient(org.Name, org.CodebergToken)
+ exists, err = client.RepoExists(repoName)
+ default:
+ 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
+ for _, info := range orgsWithRepo {
+ if info.err != nil {
+ fmt.Printf(" ❌ %s: Error checking - %v\n", info.org.GetGitURL(), info.err)
+ } else if info.exists {
+ fmt.Printf(" ✅ %s: EXISTS - will be DELETED\n", info.org.GetGitURL())
+ foundAny = true
+ } else {
+ 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":
+ client := github.NewClient(info.org.GitHubToken, info.org.Name)
+ deleteErr = client.DeleteRepo(repoName)
+ case "git@codeberg.org":
+ client := codeberg.NewClient(info.org.Name, info.org.CodebergToken)
+ deleteErr = client.DeleteRepo(repoName)
+ }
+
+ if deleteErr != nil {
+ fmt.Printf("FAILED: %v\n", deleteErr)
+ hasError = true
+ } else {
+ 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/codeberg/codeberg.go b/internal/codeberg/codeberg.go
index 4eaed66..e608016 100644
--- a/internal/codeberg/codeberg.go
+++ b/internal/codeberg/codeberg.go
@@ -248,3 +248,52 @@ func (c *Client) CreateRepo(repoName, description string, private bool) error {
return nil
}
+
+// DeleteRepo deletes a repository from Codeberg
+func (c *Client) DeleteRepo(repoName string) error {
+ if !c.HasToken() {
+ return fmt.Errorf("Codeberg token required to delete repository")
+ }
+
+ // First check if the repo exists
+ exists, err := c.RepoExists(repoName)
+ if err != nil {
+ return fmt.Errorf("failed to check if repo exists: %w", err)
+ }
+ if !exists {
+ // Repo doesn't exist, nothing to delete
+ return fmt.Errorf("repository %s/%s does not exist", c.org, repoName)
+ }
+
+ url := fmt.Sprintf("%s/repos/%s/%s", c.baseURL, c.org, repoName)
+
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Authorization", "token "+c.token)
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 204 {
+ // Successfully deleted
+ return nil
+ } else if resp.StatusCode == 404 {
+ // Already gone, consider it a success
+ return nil
+ } else if resp.StatusCode == 403 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("permission denied (403): %s", string(body))
+ } else if resp.StatusCode == 401 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("authentication failed (401): %s", string(body))
+ }
+
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to delete repository: status %d: %s", resp.StatusCode, string(body))
+}
diff --git a/internal/github/github.go b/internal/github/github.go
index 479bd54..cee25f8 100644
--- a/internal/github/github.go
+++ b/internal/github/github.go
@@ -24,7 +24,7 @@ func NewClient(token, org string) Client {
fmt.Println(" No token in config, trying environment variable...")
// Try environment variable
token = os.Getenv("GITHUB_TOKEN")
-
+
// If still no token, try reading from file
if token == "" {
fmt.Println(" No GITHUB_TOKEN env var, trying ~/.gitsyncer_github_token file...")
@@ -95,21 +95,21 @@ func (c *Client) RepoExists(repoName string) (bool, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s", c.org, repoName)
fmt.Printf(" Checking URL: %s\n", url)
fmt.Printf(" Token present: %v (length: %d)\n", c.token != "", len(c.token))
-
+
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return false, err
}
-
+
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/vnd.github.v3+json")
-
+
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
-
+
if resp.StatusCode == 200 {
return true, nil
} else if resp.StatusCode == 404 {
@@ -121,7 +121,7 @@ func (c *Client) RepoExists(repoName string) (bool, error) {
fmt.Printf(" Authorization header: %s\n", req.Header.Get("Authorization"))
return false, fmt.Errorf("authentication failed (401): %s", string(body))
}
-
+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
@@ -142,36 +142,36 @@ func (c *Client) CreateRepo(repoName, description string, private bool) error {
// Repo already exists, nothing to do
return nil
}
-
+
url := fmt.Sprintf("https://api.github.com/user/repos")
-
+
reqBody := CreateRepoRequest{
Name: repoName,
Description: description,
Private: private,
AutoInit: false, // Don't auto-init, we'll push content
}
-
+
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return err
}
-
+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return err
}
-
+
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/vnd.github.v3+json")
req.Header.Set("Content-Type", "application/json")
-
+
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
-
+
if resp.StatusCode == 201 {
var createResp CreateRepoResponse
if err := json.NewDecoder(resp.Body).Decode(&createResp); err != nil {
@@ -180,17 +180,17 @@ func (c *Client) CreateRepo(repoName, description string, private bool) error {
fmt.Printf("Created GitHub repository: %s\n", createResp.FullName)
return nil
}
-
+
// Handle error response
var errResp ErrorResponse
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
-
+
if errResp.Message != "" {
return fmt.Errorf("GitHub API error: %s", errResp.Message)
}
-
+
return fmt.Errorf("failed to create repository: status %d", resp.StatusCode)
}
@@ -223,45 +223,45 @@ func (c *Client) ListPublicRepos() ([]Repository, error) {
for {
url := fmt.Sprintf("https://api.github.com/users/%s/repos?page=%d&per_page=%d&type=owner", c.org, page, perPage)
fmt.Printf(" Fetching page %d...\n", page)
-
+
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
-
+
req.Header.Set("Authorization", "Bearer "+c.token)
req.Header.Set("Accept", "application/vnd.github.v3+json")
-
+
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
-
+
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to list repos: status %d: %s", resp.StatusCode, string(body))
}
-
+
var repos []Repository
if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
-
- // Filter for public, non-fork, non-archived, non-empty repos
+
+ // Filter for public, non-fork, non-archived
for _, repo := range repos {
- if !repo.Private && !repo.Fork && !repo.Archived && !repo.Disabled && repo.Size > 0 {
+ if !repo.Private && !repo.Fork && !repo.Archived && !repo.Disabled {
allRepos = append(allRepos, repo)
}
}
-
+
// Check if there are more pages
if len(repos) < perPage {
break
}
page++
}
-
+
return allRepos, nil
}
@@ -272,4 +272,54 @@ func GetRepoNames(repos []Repository) []string {
names[i] = repo.Name
}
return names
-} \ No newline at end of file
+}
+
+// DeleteRepo deletes a repository from GitHub
+func (c *Client) DeleteRepo(repoName string) error {
+ if c.token == "" {
+ return fmt.Errorf("GitHub token required to delete repository")
+ }
+
+ // First check if the repo exists
+ exists, err := c.RepoExists(repoName)
+ if err != nil {
+ return fmt.Errorf("failed to check if repo exists: %w", err)
+ }
+ if !exists {
+ // Repo doesn't exist, nothing to delete
+ return fmt.Errorf("repository %s/%s does not exist", c.org, repoName)
+ }
+
+ url := fmt.Sprintf("https://api.github.com/repos/%s/%s", c.org, repoName)
+
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Authorization", "Bearer "+c.token)
+ req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == 204 {
+ // Successfully deleted
+ return nil
+ } else if resp.StatusCode == 404 {
+ // Already gone, consider it a success
+ return nil
+ } else if resp.StatusCode == 403 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("permission denied (403): %s", string(body))
+ } else if resp.StatusCode == 401 {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("authentication failed (401): %s", string(body))
+ }
+
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("failed to delete repository: status %d: %s", resp.StatusCode, string(body))
+}