Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 20 additions & 139 deletions CONTRIBUTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ Thank you for your interest in contributing to Hippo! This guide will help you s
### Prerequisites

- **Go 1.21 or higher** - [Install Go](https://go.dev/doc/install)
- **Azure CLI** - For authentication
- macOS: `brew install azure-cli`
- Windows: Download from [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)
- Linux: Follow instructions at [Microsoft Docs](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)
- **Azure DevOps account** - With access to a project for testing
- **Git** - For version control

### Initial Setup

Expand All @@ -47,46 +41,40 @@ az login
4. **Run the setup wizard**:
```bash
cd app
go run . --init
go run .
```

The wizard will prompt you for:
- Azure DevOps organization URL (e.g., `https://dev.azure.com/your-org`)
- Project name
- Team name (optional)

Your configuration will be saved to `~/.config/hippo/config.yaml`.

## Building from Source

### Standard Build
### Dummy Mode (Development Backend)

Build without version information (shows "dev"):
For development and testing without Azure DevOps access, use dummy mode. This provides an in-memory mock backend with sample work items.

**Enable via environment variable:**
```bash
cd app
go build -o hippo
./hippo --version # Output: Hippo dev
HIPPO_DUMMY_MODE=true go run .
```

### Build with Custom Version

Inject a custom version at build time:

**Enable via flag:**
```bash
cd app
go build -ldflags="-X main.Version=v0.3.0-custom" -o hippo
./hippo --version # Output: Hippo v0.3.0-custom
go run . --dummy
```

### Run Without Building
Dummy mode provides:
- Sample work items with parent-child relationships (User Stories, Tasks, Bugs)
- Three sprints (previous, current, next) based on current date
- Full CRUD operations (create, update, delete) - all in-memory
- Various work item states (New, Active, Closed)
- Backlog items and abandoned work items
- No Azure CLI login or configuration required

For quick testing during development:
This is useful for:
- UI development without Azure DevOps access
- Testing new features locally
- Demo and screenshot purposes
- Running the app in CI environments

```bash
cd app
go run .
```
Note: All data is stored in memory and resets when the application restarts.

## Testing

Expand Down Expand Up @@ -126,17 +114,6 @@ go test -bench=. -benchmem
go test -bench=BenchmarkTreeCacheVsNoCacheScrolling -benchmem
```

### Test Coverage

The test suite includes:
- Unit tests for tree building and flattening
- Cache hit/miss verification tests
- Cache invalidation tests across operations
- Integration tests for multi-list caching
- Performance benchmarks comparing cached vs uncached tree operations

See `app/main_test.go` and `app/fixtures_test.go` for test implementations.

## Code Style

### General Guidelines
Expand Down Expand Up @@ -341,99 +318,3 @@ The version is automatically injected at build time:
- **Released builds:** Show actual version (e.g., "v0.3.0")
- **Build command:** GoReleaser uses `-ldflags="-X main.Version={{.Version}}"`

### Testing a Release Locally

Before creating an official release, you can test the release process:

```bash
# Install GoReleaser
brew install goreleaser

# Test release build (doesn't publish)
goreleaser release --snapshot --clean

# Check generated artifacts in dist/
ls -la dist/
```

### Release Checklist

Before creating a release:

- [ ] All tests pass: `go test -v ./...`
- [ ] Code is formatted: `go fmt ./...`
- [ ] Code is vetted: `go vet ./...`
- [ ] Version number follows semantic versioning
- [ ] CHANGELOG or commit messages are clear
- [ ] Documentation is up to date
- [ ] All changes committed and pushed to main

### Key Release Files

- `.goreleaser.yml` - Build configuration for releases
- `.github/workflows/release.yml` - CI/CD workflow (not yet created in Phase 2)
- `app/constants.go` - Version variable

## Submitting Changes

### Workflow

1. **Fork the repository** (or create a branch if you have access)

2. **Create a feature branch**:
```bash
git checkout -b feature/your-feature-name
```

3. **Make your changes** following the code style guidelines

4. **Test your changes**:
```bash
cd app
go fmt ./...
go vet ./...
go test -v ./...
```

5. **Commit your changes**:
```bash
git add .
git commit -m "feat: add your feature description"
```

Use conventional commit messages:
- `feat:` - New feature
- `fix:` - Bug fix
- `docs:` - Documentation changes
- `refactor:` - Code refactoring
- `test:` - Test additions or changes
- `chore:` - Maintenance tasks

6. **Push to your fork/branch**:
```bash
git push origin feature/your-feature-name
```

7. **Create a Pull Request** on GitHub with:
- Clear description of changes
- Reference to related issues
- Screenshots/demos if UI changes

### Pull Request Guidelines

- Keep PRs focused on a single feature or fix
- Update documentation if needed
- Add tests for new functionality
- Ensure all tests pass
- Follow the existing code style
- Be responsive to review feedback

## Getting Help

- **Issues:** [GitHub Issues](https://github.com/oribarilan/hippo/issues)
- **Discussions:** [GitHub Discussions](https://github.com/oribarilan/hippo/discussions)
- **Documentation:** See [AGENTS.md](./AGENTS.md) for detailed architecture

## License

By contributing to Hippo, you agree that your contributions will be licensed under the MIT License.
33 changes: 33 additions & 0 deletions app/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

// Backend defines the interface for work item data sources.
// Implementations include AzureDevOpsClient (production) and DummyBackend (development).
type Backend interface {
// Work Item CRUD Operations
GetWorkItemByID(id int) (*WorkItem, error)
GetWorkItems() ([]WorkItem, error)
GetWorkItemsForSprint(sprintPath string, excludeIDs []int, limit int) ([]WorkItem, error)
GetWorkItemsExcluding(excludeIDs []int, sprintPath string, limit int) ([]WorkItem, error)
GetWorkItemsCount() (int, error)
GetWorkItemsCountForSprint(sprintPath string) (int, error)
UpdateWorkItemState(workItemID int, newState string) error
UpdateWorkItem(workItemID int, updates map[string]interface{}) error
CreateWorkItem(title string, workItemType string, iterationPath string, parentID *int, areaPath string) (*WorkItem, error)
DeleteWorkItem(workItemID int) error
MoveWorkItemToSprint(workItemID int, iterationPath string) error
GetWorkItemTypeStates(workItemType string) ([]string, map[string]string, error)

// Sprint Operations
GetCurrentAndAdjacentSprints() (prev *Sprint, curr *Sprint, next *Sprint, err error)

// Backlog Operations
GetRecentBacklogItems(limit int) ([]WorkItem, error)
GetRecentBacklogItemsExcluding(excludeIDs []int, limit int) ([]WorkItem, error)
GetRecentBacklogItemsCount() (int, error)
GetAbandonedWorkItems(currentSprintPath string, limit int) ([]WorkItem, error)
GetAbandonedWorkItemsExcluding(excludeIDs []int, currentSprintPath string, limit int) ([]WorkItem, error)
GetAbandonedWorkItemsCount(currentSprintPath string) (int, error)
}

// Compile-time check that AzureDevOpsClient implements Backend
var _ Backend = (*AzureDevOpsClient)(nil)
47 changes: 37 additions & 10 deletions app/client_sprints.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (c *AzureDevOpsClient) GetTeamIterations() ([]work.TeamSettingsIteration, e
}

// GetCurrentAndAdjacentSprints returns previous, current, and next sprint
func (c *AzureDevOpsClient) GetCurrentAndAdjacentSprints() (prev, curr, next *work.TeamSettingsIteration, err error) {
func (c *AzureDevOpsClient) GetCurrentAndAdjacentSprints() (prev *Sprint, curr *Sprint, next *Sprint, err error) {
iterations, err := c.GetTeamIterations()
if err != nil {
return nil, nil, nil, err
Expand All @@ -42,6 +42,7 @@ func (c *AzureDevOpsClient) GetCurrentAndAdjacentSprints() (prev, curr, next *wo

now := time.Now()
var currentIdx = -1
var currentIter *work.TeamSettingsIteration

// Find current sprint
for i, iter := range iterations {
Expand All @@ -57,14 +58,14 @@ func (c *AzureDevOpsClient) GetCurrentAndAdjacentSprints() (prev, curr, next *wo
finish := finishDate.Time

// Truncate to start of day (removes time component) for date-only comparison
startDate := start.Truncate(24 * time.Hour)
finishDate := finish.Truncate(24 * time.Hour)
startDateTrunc := start.Truncate(24 * time.Hour)
finishDateTrunc := finish.Truncate(24 * time.Hour)
nowDate := now.Truncate(24 * time.Hour)

// Check if today falls within the sprint (inclusive of start and end dates)
if !nowDate.Before(startDate) && !nowDate.After(finishDate) {
if !nowDate.Before(startDateTrunc) && !nowDate.After(finishDateTrunc) {
currentIdx = i
curr = &iterations[i]
currentIter = &iterations[i]
break
}
}
Expand All @@ -73,18 +74,44 @@ func (c *AzureDevOpsClient) GetCurrentAndAdjacentSprints() (prev, curr, next *wo
// If no current sprint found, use the most recent one
if currentIdx == -1 && len(iterations) > 0 {
currentIdx = len(iterations) - 1
curr = &iterations[currentIdx]
currentIter = &iterations[currentIdx]
}

// Get previous sprint
// Convert to Sprint structs
if currentIdx > 0 {
prev = &iterations[currentIdx-1]
prev = convertIterationToSprint(&iterations[currentIdx-1])
}

if currentIter != nil {
curr = convertIterationToSprint(currentIter)
}

// Get next sprint
if currentIdx >= 0 && currentIdx < len(iterations)-1 {
next = &iterations[currentIdx+1]
next = convertIterationToSprint(&iterations[currentIdx+1])
}

return prev, curr, next, nil
}

// convertIterationToSprint converts an Azure DevOps TeamSettingsIteration to our Sprint struct
func convertIterationToSprint(iter *work.TeamSettingsIteration) *Sprint {
if iter == nil || iter.Name == nil || iter.Path == nil {
return nil
}

sprint := &Sprint{
Name: *iter.Name,
Path: *iter.Path,
}

if iter.Attributes != nil {
if iter.Attributes.StartDate != nil {
sprint.StartDate = iter.Attributes.StartDate.Time.Format("2006-01-02")
}
if iter.Attributes.FinishDate != nil {
sprint.EndDate = iter.Attributes.FinishDate.Time.Format("2006-01-02")
}
}

return sprint
}
Loading