package github import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "github.com/iwasforcedtobehere/git-automation-cli/internal/config" ) // Client represents a GitHub API client type Client struct { httpClient *http.Client baseURL string token string organization string repository string } // NewClient creates a new GitHub API client func NewClient(ctx context.Context) (*Client, error) { // Get configuration token := config.GlobalConfig.GitHubToken if token == "" { return nil, fmt.Errorf("GitHub token not configured. Use 'gitauto config set github.token ' to set it") } baseURL := config.GlobalConfig.GitHubURL if baseURL == "" { baseURL = "https://api.github.com" } // Create HTTP client with timeout httpClient := &http.Client{ Timeout: 30 * time.Second, } return &Client{ httpClient: httpClient, baseURL: baseURL, token: token, }, nil } // SetRepository sets the organization and repository for the client func (c *Client) SetRepository(org, repo string) { c.organization = org c.repository = repo } // makeRequest makes an HTTP request to the GitHub API func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) { var reqBody io.Reader if body != nil { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } reqBody = bytes.NewReader(jsonBody) } url := fmt.Sprintf("%s%s", c.baseURL, path) req, err := http.NewRequestWithContext(ctx, method, url, reqBody) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } // Set headers req.Header.Set("Authorization", fmt.Sprintf("token %s", c.token)) req.Header.Set("Accept", "application/vnd.github.v3+json") req.Header.Set("Content-Type", "application/json") // Make the request resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to make request: %w", err) } // Check for errors if resp.StatusCode >= 400 { defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("GitHub API error (%d): %s", resp.StatusCode, string(body)) } return resp, nil } // GetUser gets the authenticated user func (c *Client) GetUser(ctx context.Context) (*User, error) { resp, err := c.makeRequest(ctx, "GET", "/user", nil) if err != nil { return nil, err } defer resp.Body.Close() var user User if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &user, nil } // GetRepository gets a repository func (c *Client) GetRepository(ctx context.Context, owner, repo string) (*Repository, error) { path := fmt.Sprintf("/repos/%s/%s", owner, repo) resp, err := c.makeRequest(ctx, "GET", path, nil) if err != nil { return nil, err } defer resp.Body.Close() var repository Repository if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &repository, nil } // CreatePullRequest creates a new pull request func (c *Client) CreatePullRequest(ctx context.Context, pr *PullRequestRequest) (*PullRequest, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/pulls", c.organization, c.repository) resp, err := c.makeRequest(ctx, "POST", path, pr) if err != nil { return nil, err } defer resp.Body.Close() var pullRequest PullRequest if err := json.NewDecoder(resp.Body).Decode(&pullRequest); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &pullRequest, nil } // GetPullRequests gets pull requests for a repository func (c *Client) GetPullRequests(ctx context.Context, state string) ([]PullRequest, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/pulls?state=%s", c.organization, c.repository, state) resp, err := c.makeRequest(ctx, "GET", path, nil) if err != nil { return nil, err } defer resp.Body.Close() var pullRequests []PullRequest if err := json.NewDecoder(resp.Body).Decode(&pullRequests); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return pullRequests, nil } // GetPullRequest gets a specific pull request func (c *Client) GetPullRequest(ctx context.Context, number int) (*PullRequest, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/pulls/%d", c.organization, c.repository, number) resp, err := c.makeRequest(ctx, "GET", path, nil) if err != nil { return nil, err } defer resp.Body.Close() var pullRequest PullRequest if err := json.NewDecoder(resp.Body).Decode(&pullRequest); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &pullRequest, nil } // UpdatePullRequest updates a pull request func (c *Client) UpdatePullRequest(ctx context.Context, number int, pr *PullRequestRequest) (*PullRequest, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/pulls/%d", c.organization, c.repository, number) resp, err := c.makeRequest(ctx, "PATCH", path, pr) if err != nil { return nil, err } defer resp.Body.Close() var pullRequest PullRequest if err := json.NewDecoder(resp.Body).Decode(&pullRequest); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &pullRequest, nil } // MergePullRequest merges a pull request func (c *Client) MergePullRequest(ctx context.Context, number int, mergeRequest *MergePullRequestRequest) (*MergePullRequestResponse, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", c.organization, c.repository, number) resp, err := c.makeRequest(ctx, "PUT", path, mergeRequest) if err != nil { return nil, err } defer resp.Body.Close() var mergeResponse MergePullRequestResponse if err := json.NewDecoder(resp.Body).Decode(&mergeResponse); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &mergeResponse, nil } // CreateIssue creates a new issue func (c *Client) CreateIssue(ctx context.Context, issue *IssueRequest) (*Issue, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/issues", c.organization, c.repository) resp, err := c.makeRequest(ctx, "POST", path, issue) if err != nil { return nil, err } defer resp.Body.Close() var createdIssue Issue if err := json.NewDecoder(resp.Body).Decode(&createdIssue); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return &createdIssue, nil } // GetIssues gets issues for a repository func (c *Client) GetIssues(ctx context.Context, state string) ([]Issue, error) { if c.organization == "" || c.repository == "" { return nil, fmt.Errorf("organization and repository must be set") } path := fmt.Sprintf("/repos/%s/%s/issues?state=%s", c.organization, c.repository, state) resp, err := c.makeRequest(ctx, "GET", path, nil) if err != nil { return nil, err } defer resp.Body.Close() var issues []Issue if err := json.NewDecoder(resp.Body).Decode(&issues); err != nil { return nil, fmt.Errorf("failed to decode response: %w", err) } return issues, nil } // GetRepositoryFromRemote tries to extract the repository information from the git remote func (c *Client) GetRepositoryFromRemote(ctx context.Context, remote string) (string, string, error) { // This would typically call git to get the remote URL // For now, we'll implement a placeholder // In a real implementation, this would use the git package to get the remote URL // Placeholder implementation // In a real implementation, this would: // 1. Get the remote URL using git remote get-url // 2. Parse the URL to extract owner and repo // 3. Return the owner and repo return "", "", fmt.Errorf("not implemented yet") } // ValidateToken checks if the GitHub token is valid func (c *Client) ValidateToken(ctx context.Context) error { // Try to get the authenticated user _, err := c.GetUser(ctx) return err } // IsGitHubURL checks if a URL is a GitHub URL func IsGitHubURL(url string) bool { return strings.Contains(url, "github.com") } // ParseGitHubURL parses a GitHub URL to extract owner and repo func ParseGitHubURL(url string) (string, string, error) { // Remove protocol and .git suffix if present url = strings.TrimPrefix(url, "https://") url = strings.TrimPrefix(url, "http://") url = strings.TrimPrefix(url, "git@") url = strings.TrimSuffix(url, ".git") // Split by : or / var parts []string if strings.Contains(url, ":") { parts = strings.Split(url, ":") if len(parts) != 2 { return "", "", fmt.Errorf("invalid GitHub URL format") } parts = strings.Split(parts[1], "/") } else { parts = strings.Split(url, "/") } // Filter out empty strings var filteredParts []string for _, part := range parts { if part != "" { filteredParts = append(filteredParts, part) } } if len(filteredParts) < 2 { return "", "", fmt.Errorf("invalid GitHub URL format") } owner := filteredParts[len(filteredParts)-2] repo := filteredParts[len(filteredParts)-1] return owner, repo, nil }