From 006724744a943aad877a92406a5e2b4d5d12acd3 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 23 Jun 2025 23:26:52 +0300 Subject: Add GitHub repository creation and improve error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --create-github-repos flag to automatically create missing GitHub repositories - Implement GitHub API client with token support from config/env/file - Add Codeberg API integration to sync all public repositories - Make sync operations stop on first error for better debugging - Support GitHub repo creation for all sync commands (--sync, --sync-all, --sync-codeberg-public) - Add comprehensive error messages and debug logging 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- internal/github/github.go | 175 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 internal/github/github.go (limited to 'internal/github') diff --git a/internal/github/github.go b/internal/github/github.go new file mode 100644 index 0000000..c0d3ffc --- /dev/null +++ b/internal/github/github.go @@ -0,0 +1,175 @@ +package github + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" +) + +// Client handles GitHub API operations +type Client struct { + token string + org string +} + +// NewClient creates a new GitHub API client +func NewClient(token, org string) *Client { + // If no token provided, try other sources + if token == "" { + // Try environment variable + token = os.Getenv("GITHUB_TOKEN") + + // If still no token, try reading from file + if token == "" { + home, err := os.UserHomeDir() + if err == nil { + tokenFile := filepath.Join(home, ".gitsyncer_github_token") + data, err := os.ReadFile(tokenFile) + if err == nil { + token = strings.TrimSpace(string(data)) + } + } + } + } + return &Client{ + token: token, + org: org, + } +} + +// CreateRepoRequest represents the request to create a repository +type CreateRepoRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Private bool `json:"private"` + AutoInit bool `json:"auto_init"` +} + +// CreateRepoResponse represents the response from creating a repository +type CreateRepoResponse struct { + ID int64 `json:"id"` + Name string `json:"name"` + FullName string `json:"full_name"` + Private bool `json:"private"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` +} + +// ErrorResponse represents an error response from GitHub API +type ErrorResponse struct { + Message string `json:"message"` + Errors []struct { + Resource string `json:"resource"` + Field string `json:"field"` + Code string `json:"code"` + } `json:"errors,omitempty"` +} + +// RepoExists checks if a repository exists +func (c *Client) RepoExists(repoName string) (bool, error) { + if c.token == "" { + return false, fmt.Errorf("GitHub token required") + } + + url := fmt.Sprintf("https://api.github.com/repos/%s/%s", c.org, repoName) + + 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 { + return false, nil + } + + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) +} + +// CreateRepo creates a new repository +func (c *Client) CreateRepo(repoName, description string, private bool) error { + if c.token == "" { + return fmt.Errorf("GitHub token required to create repository") + } + + fmt.Printf(" Checking if GitHub repo %s/%s exists...\n", c.org, repoName) + // First check if it already exists + exists, err := c.RepoExists(repoName) + if err != nil { + return fmt.Errorf("failed to check if repo exists: %w", err) + } + if exists { + fmt.Printf(" GitHub repo already exists, skipping creation\n") + // 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 { + return fmt.Errorf("failed to parse response: %w", err) + } + 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) +} + +// HasToken returns whether a token is configured +func (c *Client) HasToken() bool { + return c.token != "" +} \ No newline at end of file -- cgit v1.2.3