Files
devops-tui/internal/api/client.go
T
Samuel Enocsson 2555afce19 Initial commit: Azure DevOps TUI client
A terminal-based user interface for browsing and managing Azure DevOps
work items. Features include:

- Browse work items with filtering by area, iteration, state, and type
- View work item details with markdown rendering
- Open work items in browser
- Create git branches from work items
- Update work item state
- Keyboard-driven navigation

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 09:56:11 +01:00

180 lines
4.5 KiB
Go

package api
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/samuelenocsson/devops-tui/internal/config"
)
const apiVersion = "7.1"
// Client is the Azure DevOps API client
type Client struct {
httpClient *http.Client
baseURL string
teamURL string
webURL string
authHeader string
organization string
project string
team string
}
// NewClient creates a new Azure DevOps API client
func NewClient(cfg *config.Config) *Client {
// Azure DevOps uses Basic auth with empty username and PAT as password
auth := base64.StdEncoding.EncodeToString([]byte(":" + cfg.PAT))
return &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: cfg.BaseURL(),
teamURL: cfg.TeamURL(),
webURL: cfg.WebURL(),
authHeader: "Basic " + auth,
organization: cfg.Organization,
project: cfg.Project,
team: cfg.Team,
}
}
// doRequest performs an HTTP request with authentication
func (c *Client) doRequest(method, url string, body io.Reader) (*http.Response, error) {
return c.doRequestWithContentType(method, url, body, "application/json")
}
// doRequestWithContentType performs an HTTP request with authentication and custom content type
func (c *Client) doRequestWithContentType(method, url string, body io.Reader, contentType string) (*http.Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Authorization", c.authHeader)
req.Header.Set("Content-Type", contentType)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(body))
}
return resp, nil
}
// get performs a GET request to base URL
func (c *Client) get(endpoint string) (*http.Response, error) {
return c.getWithBase(c.baseURL, endpoint)
}
// getTeam performs a GET request to team-specific URL
func (c *Client) getTeam(endpoint string) (*http.Response, error) {
return c.getWithBase(c.teamURL, endpoint)
}
// getWithBase performs a GET request with a specific base URL
func (c *Client) getWithBase(baseURL, endpoint string) (*http.Response, error) {
url := fmt.Sprintf("%s%s", baseURL, endpoint)
if endpoint[0] != '/' {
url = fmt.Sprintf("%s/%s", baseURL, endpoint)
}
// Add API version
if len(url) > 0 {
separator := "?"
if len(url) > 0 && url[len(url)-1] != '?' {
for _, c := range url {
if c == '?' {
separator = "&"
break
}
}
}
url = fmt.Sprintf("%s%sapi-version=%s", url, separator, apiVersion)
}
return c.doRequest("GET", url, nil)
}
// post performs a POST request
func (c *Client) post(endpoint string, body io.Reader) (*http.Response, error) {
url := fmt.Sprintf("%s%s", c.baseURL, endpoint)
if endpoint[0] != '/' {
url = fmt.Sprintf("%s/%s", c.baseURL, endpoint)
}
// Add API version
separator := "?"
for _, ch := range url {
if ch == '?' {
separator = "&"
break
}
}
url = fmt.Sprintf("%s%sapi-version=%s", url, separator, apiVersion)
return c.doRequest("POST", url, body)
}
// patch performs a PATCH request (for work item updates)
func (c *Client) patch(endpoint string, body io.Reader) (*http.Response, error) {
url := fmt.Sprintf("%s%s", c.baseURL, endpoint)
if endpoint[0] != '/' {
url = fmt.Sprintf("%s/%s", c.baseURL, endpoint)
}
// Add API version
separator := "?"
for _, ch := range url {
if ch == '?' {
separator = "&"
break
}
}
url = fmt.Sprintf("%s%sapi-version=%s", url, separator, apiVersion)
return c.doRequestWithContentType("PATCH", url, body, "application/json-patch+json")
}
// decode decodes a JSON response into the given target
func decode(resp *http.Response, target interface{}) error {
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
}
// WorkItemWebURL returns the web URL for a work item
func (c *Client) WorkItemWebURL(id int) string {
return fmt.Sprintf("%s/_workitems/edit/%d", c.webURL, id)
}
// Organization returns the organization name
func (c *Client) Organization() string {
return c.organization
}
// Project returns the project name
func (c *Client) Project() string {
return c.project
}
// Team returns the team name
func (c *Client) Team() string {
return c.team
}