Files
Dev 15bbfdcda2 feat: initial commit of Git Automation CLI
- 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
2025-09-11 17:02:12 +03:00

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
}