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>
This commit is contained in:
Samuel Enocsson
2025-12-04 09:56:11 +01:00
commit 2555afce19
30 changed files with 4906 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
package api
import (
"sort"
"strings"
"github.com/samuelenocsson/devops-tui/internal/models"
)
// classificationNode represents an area node from the API
type classificationNode struct {
ID int `json:"id"`
Identifier string `json:"identifier"`
Name string `json:"name"`
Path string `json:"path"`
HasChildren bool `json:"hasChildren"`
Children []classificationNode `json:"children"`
}
// GetAreas fetches all areas for the project
func (c *Client) GetAreas() ([]models.Area, error) {
// Use the classification nodes API with depth to get area hierarchy
resp, err := c.get("/wit/classificationnodes/areas?$depth=10")
if err != nil {
return nil, err
}
var rootNode classificationNode
if err := decode(resp, &rootNode); err != nil {
return nil, err
}
// Flatten the tree into a list
areas := flattenAreas(rootNode, "")
// Sort areas by path for consistent ordering
sort.Slice(areas, func(i, j int) bool {
return areas[i].Path < areas[j].Path
})
return areas, nil
}
// flattenAreas recursively flattens the area tree
func flattenAreas(node classificationNode, parentPath string) []models.Area {
var areas []models.Area
// Build the path
path := node.Path
if path == "" {
path = node.Name
}
// Clean up the path:
// 1. Remove leading backslash
// 2. Remove "\Area" from path (API returns \Project\Area\Team but work items use Project\Team)
path = strings.TrimPrefix(path, "\\")
path = strings.TrimSuffix(path, "\\")
// Remove the "Area" segment from the path (e.g., "Project\Area\Team" -> "Project\Team")
parts := strings.Split(path, "\\")
if len(parts) >= 2 && parts[1] == "Area" {
// Remove the "Area" part
newParts := []string{parts[0]}
if len(parts) > 2 {
newParts = append(newParts, parts[2:]...)
}
path = strings.Join(newParts, "\\")
}
// Add this node
areas = append(areas, models.Area{
ID: node.ID,
Name: node.Name,
Path: path,
})
// Recursively add children
for _, child := range node.Children {
childAreas := flattenAreas(child, path)
areas = append(areas, childAreas...)
}
return areas
}
// GetAreaDisplayName returns a shortened display name for an area
func GetAreaDisplayName(path string) string {
parts := strings.Split(path, "\\")
if len(parts) > 1 {
// Return the last part for brevity
return parts[len(parts)-1]
}
return path
}