
- Add comprehensive Git workflow automation tools - Include branch management utilities - Add commit helpers with conventional commit support - Implement GitHub integration for PR management - Add configuration management system - Include comprehensive test coverage - Add professional documentation and examples
337 lines
9.5 KiB
Go
337 lines
9.5 KiB
Go
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 <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 <remote>
|
|
// 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
|
|
} |