diff --git a/README.md b/README.md index 98b8c05..ed35f97 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,195 @@ +# Headquarter -## Testing Strategy +A self-hosted platform for managing projects, git repositories, and development tools with OAuth2 authentication. -The project uses a three-tier testing approach: +## Overview -### Test Categories +Headquarter provides a centralized workspace for development teams to: +- Manage projects and their associated git repositories +- Browse repository files and view git history +- Spawn development tools (VS Code Server, Jupyter Notebook, etc.) +- Manage SSH keys and user preferences -1. **Unit Tests** (`apps/api/tests/unit/`) - - Fast tests with no external dependencies - - Use SQLite in-memory database - - Run with: `make test-unit` or `pytest -m unit` +## Features -2. **Integration Tests** (`apps/api/tests/integration/`) - - Test API endpoints with database - - Use PostgreSQL with transaction rollback - - Run with: `make test-integration` or `pytest -m integration` +### Project Management +- Create and manage projects +- View all projects in a dashboard +- Click any project to open its workspace -3. **System/E2E Tests** (`e2e/`) - - End-to-end tests using Playwright - - Test full user journeys - - Run with: `make test-e2e` +### Git Repository Management +- Initialize bare repositories +- Clone repositories (including mirror clones) +- Smart URL parsing (converts browser URLs to git URLs) +- View repository history and commit details -### Running Tests +### Repository Workspace +- Browse files and directories +- View file contents with syntax highlighting +- Switch between branches +- Quick file editing with automatic commits -```bash -# Run all tests (excludes system tests by default) -make test +### Git History Visualization +- View commit history with branch graph +- See commit details, statistics, and diffs +- Filter by branch -# Run specific categories -make test-unit # Fast unit tests only -make test-integration # Integration tests with DB -make test-system # Full stack tests -make test-e2e # Browser-based E2E tests +### Authentication +- OAuth2 via Authentik +- Session-based authentication +- User profile management -# Inside Docker container -docker compose exec api pytest -v -m unit -docker compose exec api pytest -v -m integration +### Tool Management +- Built-in tool types (code-server, jupyter-notebook) +- Create custom tool types with Docker Compose templates +- Template validation + +### User Settings +- Theme selection (system/light/dark) +- Git identity configuration +- Default editor preference + +### SSH Key Management +- Generate Ed25519 key pairs +- Copy public keys to clipboard +- Delete keys + +## Quick Start + +### Prerequisites +- Docker and Docker Compose +- Git + +### Local Development + +1. **Clone the repository:** + ```bash + git clone + cd headquarter + ``` + +2. **Set up environment:** + ```bash + cp .env.example .env + # Edit .env with your settings + ``` + +3. **Start services:** + ```bash + docker compose up -d + ``` + +4. **Access the application:** + - Frontend: http://localhost:5173 + - API: http://localhost:8000 + - API Docs: http://localhost:8000/docs + +### Production Deployment + +See [Deployment Guide](docs/deployment/) for production setup with Traefik and Authentik. + +## Tech Stack + +### Backend +- **FastAPI** - Python web framework +- **SQLAlchemy** - ORM with async PostgreSQL support +- **Pydantic** - Data validation +- **Alembic** - Database migrations +- **python-jose** - JWT handling + +### Frontend +- **React** - UI library +- **TypeScript** - Type safety +- **Vite** - Build tool +- **React Router** - Client-side routing + +### Infrastructure +- **Docker** - Containerization +- **PostgreSQL** - Database +- **Traefik** - Reverse proxy (production) +- **Authentik** - Identity provider + +## Documentation + +- [User Guide](docs/features/) - Feature documentation +- [API Reference](docs/api/) - API endpoints +- [Architecture](docs/architecture/) - System design +- [Deployment](docs/deployment/) - Setup guides +- [Development](docs/development/) - Contributing + +## Project Structure + +``` +. +├── apps/ +│ ├── api/ # FastAPI backend +│ │ ├── src/ +│ │ │ ├── api/ # API routes +│ │ │ ├── auth/ # Authentication +│ │ │ ├── models/ # Database models +│ │ │ └── utils/ # Utilities +│ │ ├── tests/ # Test suite +│ │ └── Dockerfile +│ └── web/ # React frontend +│ ├── src/ +│ │ ├── api/ # API clients +│ │ ├── components/# UI components +│ │ └── pages/ # Page components +│ └── Dockerfile +├── docs/ # Documentation +├── docker-compose.yml # Development setup +├── docker-compose.traefik.yml # Production setup +└── Makefile # Common commands ``` -### Test Markers +## Development -Tests are marked with pytest markers: -- `@pytest.mark.unit` - Fast, isolated tests -- `@pytest.mark.integration` - Tests with database/external services -- `@pytest.mark.system` - Full stack tests +### Backend Development +```bash +cd apps/api +python -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +uvicorn src.main:app --reload +``` -### Shared Fixtures +### Frontend Development +```bash +cd apps/web +npm install +npm run dev +``` -Common fixtures are in `apps/api/tests/conftest.py`: -- `sqlite_engine` - SQLite engine for unit tests -- `postgres_engine` - PostgreSQL engine for integration tests -- `db_session` - Database session with transaction rollback -- `test_client` - FastAPI TestClient instance +### Running Tests +```bash +# Backend tests +make test + +# Frontend tests +make test-web + +# All quality gates +make lint +make typecheck +``` + +## Configuration + +Key environment variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `API_DOMAIN` | API domain | `localhost` | +| `WEB_DOMAIN` | Web domain | `localhost` | +| `AUTHENTIK_DOMAIN` | Authentik domain | - | +| `AUTHENTIK_CLIENT_ID` | OAuth client ID | - | +| `AUTHENTIK_CLIENT_SECRET` | OAuth client secret | - | +| `DATABASE_URL` | PostgreSQL URL | - | +| `JWT_SECRET` | JWT signing secret | - | +| `REPO_BASE_PATH` | Repository storage path | `/data/repos` | + +See [Environment Variables](docs/deployment/environment.md) for complete list. + +## License + +[License information] diff --git a/apps/web/src/api/git_repositories.ts b/apps/web/src/api/git_repositories.ts index 5102e69..d2a3262 100644 --- a/apps/web/src/api/git_repositories.ts +++ b/apps/web/src/api/git_repositories.ts @@ -112,3 +112,134 @@ export async function getCommitDetail( ); return response.data; } + +// Git Control API + +export interface GitStatus { + branch: string; + modified: string[]; + added: string[]; + deleted: string[]; + untracked: string[]; + renamed: string[]; + ahead: number; + behind: number; +} + +export async function getRepositoryStatus( + projectId: string, + repoId: string +): Promise { + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/status` + ); + return response.data; +} + +export async function createBranch( + projectId: string, + repoId: string, + name: string, + baseBranch: string = "HEAD" +): Promise<{ message: string; branch: string }> { + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/branches`, + { name, base_branch: baseBranch } + ); + return response.data; +} + +export async function deleteBranch( + projectId: string, + repoId: string, + branchName: string, + force: boolean = false +): Promise<{ message: string }> { + const response = await apiClient.delete( + `/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}` + ); + return response.data; +} + +export async function checkoutBranch( + projectId: string, + repoId: string, + branch: string +): Promise<{ message: string; branch: string }> { + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/checkout`, + { branch } + ); + return response.data; +} + +export interface CommitResponse { + commit_hash: string; + message: string; +} + +export async function commitChanges( + projectId: string, + repoId: string, + message: string, + files?: string[] +): Promise { + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/commit`, + { message, files } + ); + return response.data; +} + +export async function fetchRepository( + projectId: string, + repoId: string +): Promise<{ message: string }> { + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/fetch` + ); + return response.data; +} + +export async function pullRepository( + projectId: string, + repoId: string, + branch?: string +): Promise<{ message: string }> { + const params = branch ? `?branch=${branch}` : ""; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/pull${params}` + ); + return response.data; +} + +export async function pushRepository( + projectId: string, + repoId: string, + branch?: string +): Promise<{ message: string }> { + const params = branch ? `?branch=${branch}` : ""; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/push${params}` + ); + return response.data; +} + +export interface MergeResponse { + commit_hash: string; + message: string; +} + +export async function mergeBranches( + projectId: string, + repoId: string, + sourceBranch: string, + targetBranch?: string, + message?: string +): Promise { + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/merge`, + { source_branch: sourceBranch, target_branch: targetBranch, message } + ); + return response.data; +} diff --git a/apps/web/src/components/commit-panel.tsx b/apps/web/src/components/commit-panel.tsx new file mode 100644 index 0000000..9c96cda --- /dev/null +++ b/apps/web/src/components/commit-panel.tsx @@ -0,0 +1,102 @@ +import { useState } from "react"; + +import { commitChanges } from "../api/git_repositories"; + +interface CommitPanelProps { + projectId: string; + repoId: string; + modified: string[]; + added: string[]; + deleted: string[]; + untracked: string[]; + onCommit: () => void; +} + +export const CommitPanel = ({ + projectId, + repoId, + modified, + added, + deleted, + untracked, + onCommit, +}: CommitPanelProps) => { + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const allFiles = [...modified, ...added, ...deleted, ...untracked]; + const hasChanges = allFiles.length > 0; + + const handleCommit = async () => { + if (!message.trim()) { + setError("Please enter a commit message"); + return; + } + setLoading(true); + setError(null); + try { + await commitChanges(projectId, repoId, message); + setMessage(""); + onCommit(); + } catch { + setError("Commit failed. Please try again."); + } finally { + setLoading(false); + } + }; + + if (!hasChanges) return null; + + return ( +
+

Changes

+ +
+ {modified.map((file) => ( +
+ M + {file} +
+ ))} + {added.map((file) => ( +
+ A + {file} +
+ ))} + {deleted.map((file) => ( +
+ D + {file} +
+ ))} + {untracked.map((file) => ( +
+ ? + {file} +
+ ))} +
+ +
+