summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-27 23:24:36 +0300
committerPaul Buetow <paul@buetow.org>2025-06-27 23:24:36 +0300
commit79e882713b195e4674dc693169ecf7dbf956a91e (patch)
treeb00325ea3f519676bc389cce735447249bef69a6
parent59d7b258cff47b0640f44f1068460403459a4bde (diff)
feat: implement --create-codeberg-repos
-rw-r--r--README.md2
-rw-r--r--doc/configuration.md50
-rw-r--r--internal/cli/sync_handlers.go63
-rw-r--r--internal/codeberg/codeberg.go105
-rw-r--r--internal/config/config.go7
5 files changed, 211 insertions, 16 deletions
diff --git a/README.md b/README.md
index b44ef36..a0b5334 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ GitSyncer is a tool for synchronizing git repositories between multiple organiza
- Batch sync multiple repositories with a single command
- Sync all public repositories from Codeberg to GitHub
- Sync all public repositories from GitHub to Codeberg
-- Automatic repository creation on GitHub (Codeberg support planned)
+- Automatic repository creation on GitHub and Codeberg
- Merge conflict detection with clear error messages
- Never deletes branches (only adds/updates)
- GitHub token validation tool
diff --git a/doc/configuration.md b/doc/configuration.md
index c4dcc50..e3b94cb 100644
--- a/doc/configuration.md
+++ b/doc/configuration.md
@@ -55,6 +55,9 @@ Array of organization objects. At least one organization must be configured.
- **github_token** (string, optional): GitHub personal access token
- Only needed for GitHub organizations
- Can also be set via environment variable or file
+- **codeberg_token** (string, optional): Codeberg personal access token
+ - Only needed for Codeberg organizations
+ - Can also be set via environment variable or file
#### repositories (optional)
Array of repository names to sync. If empty, use `--sync-codeberg-public` or `--sync-github-public` to discover repositories.
@@ -192,6 +195,53 @@ chmod 600 ~/.gitsyncer_github_token
gitsyncer --test-github-token
```
+## Codeberg Token Configuration
+
+Codeberg tokens are required for:
+- Creating repositories (`--create-codeberg-repos`)
+- Listing private repositories
+
+### Token Sources (in order of precedence)
+
+1. **Configuration file**: `codeberg_token` field in organization object
+2. **Environment variable**: `CODEBERG_TOKEN`
+3. **Token file**: `~/.gitsyncer_codeberg_token`
+
+### Creating a Codeberg Token
+
+1. Go to Codeberg Settings → Applications → Manage Access Tokens
+2. Click "Generate New Token"
+3. Select scopes:
+ - `repository` (full control of repositories)
+4. Save the token securely
+
+### Setting the Token
+
+#### Method 1: Configuration File
+```json
+{
+ "organizations": [
+ {
+ "host": "git@codeberg.org",
+ "name": "myorg",
+ "codeberg_token": "xxxxxxxxxxxx"
+ }
+ ]
+}
+```
+
+#### Method 2: Environment Variable
+```bash
+export CODEBERG_TOKEN="xxxxxxxxxxxx"
+gitsyncer --sync-all
+```
+
+#### Method 3: Token File
+```bash
+echo "xxxxxxxxxxxx" > ~/.gitsyncer_codeberg_token
+chmod 600 ~/.gitsyncer_codeberg_token
+```
+
## Branch Exclusion Patterns
The `exclude_branches` field accepts regular expressions to filter out branches from synchronization.
diff --git a/internal/cli/sync_handlers.go b/internal/cli/sync_handlers.go
index 4fbb6cf..c1fefb6 100644
--- a/internal/cli/sync_handlers.go
+++ b/internal/cli/sync_handlers.go
@@ -89,7 +89,7 @@ func HandleSyncCodebergPublic(cfg *config.Config, flags *Flags) int {
fmt.Printf("Fetching public repositories from Codeberg user/org: %s...\n", codebergOrg.Name)
- client := codeberg.NewClient(codebergOrg.Name)
+ client := codeberg.NewClient(codebergOrg.Name, codebergOrg.CodebergToken)
// Try fetching as organization first, then as user
repos, err := client.ListPublicRepos()
@@ -219,6 +219,24 @@ func createRepoWithClient(client *github.Client, repoName, description string) e
return client.CreateRepo(repoName, description, false)
}
+func initCodebergClient(cfg *config.Config) *codeberg.Client {
+ codebergOrg := cfg.FindCodebergOrg()
+ if codebergOrg == nil {
+ fmt.Println("Warning: --create-codeberg-repos specified but no Codeberg organization found in config")
+ return nil
+ }
+
+ fmt.Printf("Initializing Codeberg client for organization: %s\n", codebergOrg.Name)
+ codebergClient := codeberg.NewClient(codebergOrg.Name, codebergOrg.CodebergToken)
+ if !codebergClient.HasToken() {
+ fmt.Println("Warning: No Codeberg token found. Cannot create repositories.")
+ return nil
+ }
+
+ fmt.Println("Codeberg client initialized successfully with token")
+ return &codebergClient
+}
+
func showReposToSync(repoNames []string) {
fmt.Println("\nRepositories to sync:")
for _, name := range repoNames {
@@ -298,20 +316,45 @@ func syncCodebergRepos(cfg *config.Config, flags *Flags, repos []codeberg.Reposi
}
func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository, repoNames []string) int {
- // TODO: Add Codeberg API client for repo creation
+ // Initialize Codeberg client if needed
+ var codebergClient codeberg.Client
+ var hasCodebergClient bool
if flags.CreateCodebergRepos {
- fmt.Println("WARNING: --create-codeberg-repos is not yet implemented")
- fmt.Println(" Repositories must exist on Codeberg before syncing")
+ if client := initCodebergClient(cfg); client != nil {
+ codebergClient = *client
+ hasCodebergClient = true
+ }
}
-
+
fmt.Printf("\nStarting sync of %d repositories...\n", len(repoNames))
-
+
syncer := sync.New(cfg, flags.WorkDir)
successCount := 0
-
+
+ // Create map for descriptions
+ repoMap := make(map[string]github.Repository)
+ for _, repo := range repos {
+ repoMap[repo.Name] = repo
+ }
+
for i, repoName := range repoNames {
fmt.Printf("\n[%d/%d] Syncing %s...\n", i+1, len(repoNames), repoName)
-
+
+ // Create Codeberg repo if needed
+ if hasCodebergClient && flags.CreateCodebergRepos {
+ githubRepo := repoMap[repoName]
+ description := githubRepo.Description
+ if description == "" {
+ description = fmt.Sprintf("Mirror of %s from GitHub", repoName)
+ }
+
+ fmt.Printf("Checking/creating Codeberg repository %s...\n", repoName)
+ err := codebergClient.CreateRepo(repoName, description, false)
+ if err != nil {
+ fmt.Printf("Warning: Failed to create Codeberg repo %s: %v\n", repoName, err)
+ }
+ }
+
if err := syncer.SyncRepository(repoName); err != nil {
fmt.Printf("ERROR: Failed to sync %s: %v\n", repoName, err)
fmt.Printf("Stopping sync due to error.\n")
@@ -322,12 +365,12 @@ func syncGitHubRepos(cfg *config.Config, flags *Flags, repos []github.Repository
fmt.Printf("\n=== Summary ===\n")
fmt.Printf("Successfully synced: %d repositories\n", successCount)
-
+
// Print abandoned branches summary
if summary := syncer.GenerateAbandonedBranchSummary(); summary != "" {
fmt.Print(summary)
}
-
+
return 0
}
diff --git a/internal/codeberg/codeberg.go b/internal/codeberg/codeberg.go
index 4f0a76d..1cf5659 100644
--- a/internal/codeberg/codeberg.go
+++ b/internal/codeberg/codeberg.go
@@ -1,9 +1,12 @@
package codeberg
import (
+ "bytes"
"encoding/json"
"fmt"
"net/http"
+ "os"
+ "path/filepath"
"time"
)
@@ -28,14 +31,45 @@ type Repository struct {
type Client struct {
baseURL string
org string
+ token string
}
// NewClient creates a new Codeberg API client
-func NewClient(org string) Client {
- return Client{
+func NewClient(org, token string) Client {
+ c := Client{
baseURL: "https://codeberg.org/api/v1",
org: org,
}
+ c.loadToken(token)
+ return c
+}
+
+// loadToken loads the Codeberg API token from config, env, or file
+func (c *Client) loadToken(tokenFromConfig string) {
+ if tokenFromConfig != "" {
+ c.token = tokenFromConfig
+ return
+ }
+
+ // Check environment variable
+ if token := os.Getenv("CODEBERG_TOKEN"); token != "" {
+ c.token = token
+ return
+ }
+
+ // Check token file
+ home, err := os.UserHomeDir()
+ if err == nil {
+ tokenFile := filepath.Join(home, ".gitsyncer_codeberg_token")
+ if data, err := os.ReadFile(tokenFile); err == nil {
+ c.token = string(data)
+ }
+ }
+}
+
+// HasToken returns true if a token is loaded
+func (c *Client) HasToken() bool {
+ return c.token != ""
}
// ListPublicRepos lists all public repositories for an organization
@@ -130,3 +164,70 @@ func GetRepoNames(repos []Repository) []string {
}
return names
}
+
+// RepoExists checks if a repository exists on Codeberg
+func (c *Client) RepoExists(repoName string) (bool, error) {
+ url := fmt.Sprintf("%s/repos/%s/%s", c.baseURL, c.org, repoName)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return false, err
+ }
+
+ if c.HasToken() {
+ req.Header.Set("Authorization", "token "+c.token)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+
+ return resp.StatusCode == 200, nil
+}
+
+// CreateRepo creates a new repository on Codeberg
+func (c *Client) CreateRepo(repoName, description string, private bool) error {
+ exists, err := c.RepoExists(repoName)
+ if err != nil {
+ return fmt.Errorf("failed to check if repo exists: %w", err)
+ }
+ if exists {
+ return nil // Repository already exists
+ }
+
+ url := fmt.Sprintf("%s/user/repos", c.baseURL)
+
+ payload := map[string]interface{}{
+ "name": repoName,
+ "description": description,
+ "private": private,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ if c.HasToken() {
+ 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 != http.StatusCreated {
+ return fmt.Errorf("failed to create repository: status code %d", resp.StatusCode)
+ }
+
+ return nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 92d2c86..ef8e3b6 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -10,9 +10,10 @@ import (
// Organization represents a git organization with its host and name
type Organization struct {
- Host string `json:"host"`
- Name string `json:"name"`
- GitHubToken string `json:"github_token,omitempty"`
+ Host string `json:"host"`
+ Name string `json:"name"`
+ GitHubToken string `json:"github_token,omitempty"`
+ CodebergToken string `json:"codeberg_token,omitempty"`
}
// Config holds the application configuration