feat: add commit panel and file status indicators to repo workspace

- Add CommitPanel component for viewing changed files and committing
- Show file status indicators (M/A/D/?) in file tree
- Integrate git status with workspace for real-time updates
- Add CSS styles for commit panel and status badges

Part of git-control change implementation.
This commit is contained in:
Fusion
2026-05-19 15:00:25 +02:00
parent 762e0de44c
commit 875594d66d
21 changed files with 2242 additions and 73 deletions
+181 -39
View File
@@ -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/`) ## Features
- Fast tests with no external dependencies
- Use SQLite in-memory database
- Run with: `make test-unit` or `pytest -m unit`
2. **Integration Tests** (`apps/api/tests/integration/`) ### Project Management
- Test API endpoints with database - Create and manage projects
- Use PostgreSQL with transaction rollback - View all projects in a dashboard
- Run with: `make test-integration` or `pytest -m integration` - Click any project to open its workspace
3. **System/E2E Tests** (`e2e/`) ### Git Repository Management
- End-to-end tests using Playwright - Initialize bare repositories
- Test full user journeys - Clone repositories (including mirror clones)
- Run with: `make test-e2e` - 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
### Git History Visualization
- View commit history with branch graph
- See commit details, statistics, and diffs
- Filter by branch
### Authentication
- OAuth2 via Authentik
- Session-based authentication
- User profile management
### 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 ```bash
# Run all tests (excludes system tests by default) git clone <repository-url>
make test cd headquarter
# 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
# Inside Docker container
docker compose exec api pytest -v -m unit
docker compose exec api pytest -v -m integration
``` ```
### Test Markers 2. **Set up environment:**
```bash
cp .env.example .env
# Edit .env with your settings
```
Tests are marked with pytest markers: 3. **Start services:**
- `@pytest.mark.unit` - Fast, isolated tests ```bash
- `@pytest.mark.integration` - Tests with database/external services docker compose up -d
- `@pytest.mark.system` - Full stack tests ```
### Shared Fixtures 4. **Access the application:**
- Frontend: http://localhost:5173
- API: http://localhost:8000
- API Docs: http://localhost:8000/docs
Common fixtures are in `apps/api/tests/conftest.py`: ### Production Deployment
- `sqlite_engine` - SQLite engine for unit tests
- `postgres_engine` - PostgreSQL engine for integration tests See [Deployment Guide](docs/deployment/) for production setup with Traefik and Authentik.
- `db_session` - Database session with transaction rollback
- `test_client` - FastAPI TestClient instance ## 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
```
## Development
### Backend Development
```bash
cd apps/api
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
uvicorn src.main:app --reload
```
### Frontend Development
```bash
cd apps/web
npm install
npm run dev
```
### 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]
+131
View File
@@ -112,3 +112,134 @@ export async function getCommitDetail(
); );
return response.data; 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<GitStatus> {
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<CommitResponse> {
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<MergeResponse> {
const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/merge`,
{ source_branch: sourceBranch, target_branch: targetBranch, message }
);
return response.data;
}
+102
View File
@@ -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<string | null>(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 (
<div className="commit-panel">
<h4>Changes</h4>
<div className="file-list">
{modified.map((file) => (
<div key={file} className="file-item modified">
<span className="file-status">M</span>
<span className="file-name">{file}</span>
</div>
))}
{added.map((file) => (
<div key={file} className="file-item added">
<span className="file-status">A</span>
<span className="file-name">{file}</span>
</div>
))}
{deleted.map((file) => (
<div key={file} className="file-item deleted">
<span className="file-status">D</span>
<span className="file-name">{file}</span>
</div>
))}
{untracked.map((file) => (
<div key={file} className="file-item untracked">
<span className="file-status">?</span>
<span className="file-name">{file}</span>
</div>
))}
</div>
<div className="commit-form">
<textarea
placeholder="Commit message"
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={2}
className="commit-message-input"
/>
{error && <div className="commit-error">{error}</div>}
<button
onClick={handleCommit}
disabled={loading || !message.trim()}
className="commit-button"
type="button"
>
{loading ? "Committing..." : "Commit"}
</button>
</div>
</div>
);
};
+232
View File
@@ -0,0 +1,232 @@
import { useCallback, useEffect, useState } from "react";
import {
checkoutBranch,
createBranch,
fetchRepository,
getRepositoryStatus,
pullRepository,
pushRepository,
type GitStatus,
} from "../api/git_repositories";
interface GitToolbarProps {
projectId: string;
repoId: string;
currentBranch: string;
branches: string[];
onBranchChange: (branch: string) => void;
onRefresh: () => void;
}
export const GitToolbar = ({
projectId,
repoId,
currentBranch,
branches,
onBranchChange,
onRefresh,
}: GitToolbarProps) => {
const [status, setStatus] = useState<GitStatus | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showNewBranch, setShowNewBranch] = useState(false);
const [newBranchName, setNewBranchName] = useState("");
const [newBranchBase, setNewBranchBase] = useState("");
const loadStatus = useCallback(async () => {
try {
const data = await getRepositoryStatus(projectId, repoId);
setStatus(data);
setError(null);
} catch {
setError("Failed to load status");
}
}, [projectId, repoId]);
useEffect(() => {
void loadStatus();
// Poll status every 5 seconds
const interval = setInterval(() => void loadStatus(), 5000);
return () => clearInterval(interval);
}, [loadStatus]);
const handleFetch = async () => {
setLoading(true);
try {
await fetchRepository(projectId, repoId);
await loadStatus();
} catch {
setError("Fetch failed");
} finally {
setLoading(false);
}
};
const handlePull = async () => {
setLoading(true);
try {
await pullRepository(projectId, repoId, currentBranch);
await loadStatus();
onRefresh();
} catch {
setError("Pull failed");
} finally {
setLoading(false);
}
};
const handlePush = async () => {
setLoading(true);
try {
await pushRepository(projectId, repoId, currentBranch);
await loadStatus();
} catch {
setError("Push failed");
} finally {
setLoading(false);
}
};
const handleCheckout = async (branch: string) => {
setLoading(true);
try {
await checkoutBranch(projectId, repoId, branch);
onBranchChange(branch);
onRefresh();
} catch {
setError("Checkout failed");
} finally {
setLoading(false);
}
};
const handleCreateBranch = async () => {
if (!newBranchName.trim()) return;
setLoading(true);
try {
await createBranch(projectId, repoId, newBranchName, newBranchBase || "HEAD");
setShowNewBranch(false);
setNewBranchName("");
setNewBranchBase("");
onRefresh();
} catch {
setError("Failed to create branch");
} finally {
setLoading(false);
}
};
const hasChanges = status && (
status.modified.length > 0 ||
status.added.length > 0 ||
status.deleted.length > 0 ||
status.untracked.length > 0
);
return (
<div className="git-toolbar">
{error && <div className="toolbar-error">{error}</div>}
<div className="toolbar-row">
<div className="toolbar-group">
<select
value={currentBranch}
onChange={(e) => handleCheckout(e.target.value)}
disabled={loading}
className="branch-select"
>
{branches.map((b) => (
<option key={b} value={b}>
{b === currentBranch ? `${b}` : b}
</option>
))}
</select>
<button
className="toolbar-button"
onClick={() => setShowNewBranch(!showNewBranch)}
disabled={loading}
type="button"
>
+ New
</button>
</div>
<div className="toolbar-group">
<button
className="toolbar-button"
onClick={handleFetch}
disabled={loading}
type="button"
>
Fetch
</button>
<button
className="toolbar-button"
onClick={handlePull}
disabled={loading}
type="button"
>
Pull
{status?.behind ? <span className="badge">{status.behind}</span> : null}
</button>
<button
className="toolbar-button"
onClick={handlePush}
disabled={loading || !status?.ahead}
type="button"
>
Push
{status?.ahead ? <span className="badge">{status.ahead}</span> : null}
</button>
</div>
</div>
{showNewBranch && (
<div className="toolbar-row new-branch-form">
<input
type="text"
placeholder="Branch name"
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
className="toolbar-input"
/>
<select
value={newBranchBase}
onChange={(e) => setNewBranchBase(e.target.value)}
className="toolbar-input"
>
<option value="">Base: HEAD</option>
{branches.map((b) => (
<option key={b} value={b}>{ b}</option>
))}
</select>
<button
className="toolbar-button primary"
onClick={handleCreateBranch}
disabled={loading || !newBranchName.trim()}
type="button"
>
Create
</button>
<button
className="toolbar-button"
onClick={() => setShowNewBranch(false)}
type="button"
>
Cancel
</button>
</div>
)}
{hasChanges && status && (
<div className="toolbar-row status-summary">
{status.modified.length > 0 && <span className="status-badge modified"> {status.modified.length} modified</span>}
{status.added.length > 0 && <span className="status-badge added"> {status.added.length} added</span>}
{status.deleted.length > 0 && <span className="status-badge deleted">🗑 {status.deleted.length} deleted</span>}
{status.untracked.length > 0 && <span className="status-badge untracked"> {status.untracked.length} untracked</span>}
</div>
)}
</div>
);
};
+109 -4
View File
@@ -3,7 +3,14 @@ import { useCallback, useEffect, useState } from "react";
import { Link, useParams, useSearchParams } from "react-router-dom"; import { Link, useParams, useSearchParams } from "react-router-dom";
import { apiClient } from "../api/client"; import { apiClient } from "../api/client";
import { listRepositories, type GitRepository } from "../api/git_repositories"; import {
getRepositoryStatus,
listRepositories,
type GitRepository,
type GitStatus,
} from "../api/git_repositories";
import { CommitPanel } from "../components/commit-panel";
import { GitToolbar } from "../components/git-toolbar";
type WorkspaceStatus = "loading" | "ready" | "error" | "empty"; type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
@@ -30,6 +37,9 @@ export const RepoWorkspace = () => {
const [selectedRepoId, setSelectedRepoId] = useState<string | null>( const [selectedRepoId, setSelectedRepoId] = useState<string | null>(
searchParams.get("repo") searchParams.get("repo")
); );
const [branches, setBranches] = useState<string[]>([]);
const [currentBranch, setCurrentBranch] = useState<string>("main");
const [gitStatus, setGitStatus] = useState<GitStatus | null>(null);
const loadRepositories = useCallback(async () => { const loadRepositories = useCallback(async () => {
if (!projectId) return; if (!projectId) return;
@@ -57,10 +67,42 @@ export const RepoWorkspace = () => {
} }
}, [projectId, selectedRepoId, searchParams, setSearchParams]); }, [projectId, selectedRepoId, searchParams, setSearchParams]);
const loadBranches = useCallback(async () => {
if (!projectId || !selectedRepoId) return;
try {
const response = await apiClient.get(
`/projects/${projectId}/repositories/${selectedRepoId}/branches`
);
const branchList = response.data.branches.map((b: { name: string }) => b.name);
setBranches(branchList);
const defaultBranch = response.data.default_branch;
if (defaultBranch) {
setCurrentBranch(defaultBranch);
}
} catch {
setBranches([]);
}
}, [projectId, selectedRepoId]);
const loadGitStatus = useCallback(async () => {
if (!projectId || !selectedRepoId) return;
try {
const data = await getRepositoryStatus(projectId, selectedRepoId);
setGitStatus(data);
} catch {
setGitStatus(null);
}
}, [projectId, selectedRepoId]);
useEffect(() => { useEffect(() => {
void loadRepositories(); void loadRepositories();
}, [loadRepositories]); }, [loadRepositories]);
useEffect(() => {
void loadBranches();
void loadGitStatus();
}, [loadBranches, loadGitStatus]);
const handleRepoChange = (repoId: string) => { const handleRepoChange = (repoId: string) => {
setSelectedRepoId(repoId); setSelectedRepoId(repoId);
const newParams = new URLSearchParams(searchParams); const newParams = new URLSearchParams(searchParams);
@@ -138,10 +180,44 @@ export const RepoWorkspace = () => {
</div> </div>
{selectedRepoId && ( {selectedRepoId && (
<>
<GitToolbar
projectId={projectId!}
repoId={selectedRepoId}
currentBranch={currentBranch}
branches={branches}
onBranchChange={(branch) => {
setCurrentBranch(branch);
const newParams = new URLSearchParams(searchParams);
newParams.set("branch", branch);
setSearchParams(newParams);
}}
onRefresh={() => {
void loadBranches();
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
<FileBrowser <FileBrowser
projectId={projectId!} projectId={projectId!}
repoId={selectedRepoId} repoId={selectedRepoId}
gitStatus={gitStatus}
/> />
{gitStatus && (
<CommitPanel
projectId={projectId!}
repoId={selectedRepoId}
modified={gitStatus.modified}
added={gitStatus.added}
deleted={gitStatus.deleted}
untracked={gitStatus.untracked}
onCommit={() => {
void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
}}
/>
)}
</>
)} )}
</aside> </aside>
@@ -160,9 +236,11 @@ export const RepoWorkspace = () => {
const FileBrowser = ({ const FileBrowser = ({
projectId, projectId,
repoId, repoId,
gitStatus,
}: { }: {
projectId: string; projectId: string;
repoId: string; repoId: string;
gitStatus: GitStatus | null;
}) => { }) => {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [entries, setEntries] = useState<FileTreeEntry[]>([]); const [entries, setEntries] = useState<FileTreeEntry[]>([]);
@@ -197,6 +275,13 @@ const FileBrowser = ({
void loadFiles(); void loadFiles();
}, [loadFiles]); }, [loadFiles]);
// Listen for refresh events
useEffect(() => {
const handleRefresh = () => void loadFiles();
window.addEventListener("refresh-file-tree", handleRefresh);
return () => window.removeEventListener("refresh-file-tree", handleRefresh);
}, [loadFiles]);
const handleEntryClick = (entry: FileTreeEntry) => { const handleEntryClick = (entry: FileTreeEntry) => {
if (entry.type === "directory") { if (entry.type === "directory") {
const newParams = new URLSearchParams(searchParams); const newParams = new URLSearchParams(searchParams);
@@ -221,6 +306,15 @@ const FileBrowser = ({
setSearchParams(newParams); setSearchParams(newParams);
}; };
const getFileStatus = (filePath: string): string | null => {
if (!gitStatus) return null;
if (gitStatus.modified.includes(filePath)) return "modified";
if (gitStatus.added.includes(filePath)) return "added";
if (gitStatus.deleted.includes(filePath)) return "deleted";
if (gitStatus.untracked.includes(filePath)) return "untracked";
return null;
};
if (loading) return <p className="muted">Loading files...</p>; if (loading) return <p className="muted">Loading files...</p>;
if (error) return <p className="error-text">{error}</p>; if (error) return <p className="error-text">{error}</p>;
@@ -231,16 +325,27 @@ const FileBrowser = ({
📁 .. 📁 ..
</button> </button>
)} )}
{entries.map((entry) => ( {entries.map((entry) => {
const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null;
return (
<button <button
key={entry.path} key={entry.path}
className={`tree-entry ${entry.type === "directory" ? "tree-directory" : "tree-file"}`} className={`tree-entry ${entry.type === "directory" ? "tree-directory" : "tree-file"} ${fileStatus || ""}`}
onClick={() => handleEntryClick(entry)} onClick={() => handleEntryClick(entry)}
type="button" type="button"
> >
{entry.type === "directory" ? "📁" : "📄"} {entry.name} {entry.type === "directory" ? "📁" : "📄"} {entry.name}
{fileStatus && (
<span className={`file-status-indicator ${fileStatus}`}>
{fileStatus === "modified" && "M"}
{fileStatus === "added" && "A"}
{fileStatus === "deleted" && "D"}
{fileStatus === "untracked" && "?"}
</span>
)}
</button> </button>
))} );
})}
</div> </div>
); );
}; };
+108
View File
@@ -842,3 +842,111 @@ a {
height: 100%; height: 100%;
min-height: 300px; min-height: 300px;
} }
/* File Status Indicators */
.file-status-indicator {
float: right;
font-size: 0.75rem;
font-weight: bold;
padding: 0 0.375rem;
border-radius: 3px;
margin-left: 0.5rem;
}
.file-status-indicator.modified {
color: #f59e0b;
background: rgba(245, 158, 11, 0.1);
}
.file-status-indicator.added {
color: #10b981;
background: rgba(16, 185, 129, 0.1);
}
.file-status-indicator.deleted {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.file-status-indicator.untracked {
color: #6b7280;
background: rgba(107, 114, 128, 0.1);
}
/* Commit Panel */
.commit-panel {
padding: 1rem;
border-top: 1px solid var(--border);
background: var(--panel);
}
.commit-panel h4 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 600;
}
.file-list {
max-height: 150px;
overflow: auto;
margin-bottom: 0.75rem;
}
.file-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
font-size: 0.8125rem;
}
.file-status {
font-weight: bold;
font-size: 0.75rem;
width: 1rem;
text-align: center;
}
.file-item.modified .file-status { color: #f59e0b; }
.file-item.added .file-status { color: #10b981; }
.file-item.deleted .file-status { color: #ef4444; }
.file-item.untracked .file-status { color: #6b7280; }
.commit-form {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.commit-message-input {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--ink);
font-family: inherit;
font-size: 0.875rem;
resize: vertical;
}
.commit-button {
padding: 0.5rem 1rem;
background: var(--primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
}
.commit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.commit-error {
color: #ef4444;
font-size: 0.8125rem;
}
@@ -2,7 +2,7 @@
## Phase 1: Backend File APIs ## Phase 1: Backend File APIs
- [ ] **Task 1.1**: Create git file utilities - [x] **Task 1.1**: Create git file utilities
- Create `src/utils/git_files.py` - Create `src/utils/git_files.py`
- `list_tree()` - list files in directory using `git ls-tree` - `list_tree()` - list files in directory using `git ls-tree`
- `get_file_content()` - get file content using `git show` - `get_file_content()` - get file content using `git show`
@@ -10,23 +10,23 @@
- `commit_file()` - commit file changes using `git add` + `git commit` - `commit_file()` - commit file changes using `git add` + `git commit`
- Add tests - Add tests
- [ ] **Task 1.2**: Add file listing endpoint - [x] **Task 1.2**: Add file listing endpoint
- Add `GET /projects/{id}/repositories/{id}/files` to git_repositories.py - Add `GET /projects/{id}/repositories/{id}/files` to git_repositories.py
- Query params: branch, path - Query params: branch, path
- Returns FileTreeEntry list - Returns FileTreeEntry list
- Handle errors (missing branch, missing path) - Handle errors (missing branch, missing path)
- [ ] **Task 1.3**: Add file content endpoint - [x] **Task 1.3**: Add file content endpoint
- Add `GET /projects/{id}/repositories/{id}/files/content` to git_repositories.py - Add `GET /projects/{id}/repositories/{id}/files/content` to git_repositories.py
- Query params: branch, path - Query params: branch, path
- Returns FileContent with language detection - Returns FileContent with language detection
- Detect binary files - Detect binary files
- [ ] **Task 1.4**: Add branches endpoint - [x] **Task 1.4**: Add branches endpoint
- Add `GET /projects/{id}/repositories/{id}/branches` to git_repositories.py - Add `GET /projects/{id}/repositories/{id}/branches` to git_repositories.py
- Returns branch list with default branch marked - Returns branch list with default branch marked
- [ ] **Task 1.5**: Add file update endpoint - [x] **Task 1.5**: Add file update endpoint
- Add `POST /projects/{id}/repositories/{id}/files/content` to git_repositories.py - Add `POST /projects/{id}/repositories/{id}/files/content` to git_repositories.py
- Body: path, branch, content, commit_message, author info - Body: path, branch, content, commit_message, author info
- Create commit with changes - Create commit with changes
@@ -34,29 +34,29 @@
## Phase 2: Project List Navigation ## Phase 2: Project List Navigation
- [ ] **Task 2.1**: Make project list clickable - [x] **Task 2.1**: Make project list clickable
- Update ProjectsPage to link to workspace - Update ProjectsPage to link to workspace
- Route: `/projects/:projectId` - Route: `/projects/:projectId`
- Remove placeholder, use workspace - Remove placeholder, use workspace
- [ ] **Task 2.2**: Update app navigation - [x] **Task 2.2**: Update app navigation
- Ensure project routes are correct - Ensure project routes are correct
- Add breadcrumb or back button - Add breadcrumb or back button
## Phase 3: Workspace Page Shell ## Phase 3: Workspace Page Shell
- [ ] **Task 3.1**: Create RepoWorkspace page - [x] **Task 3.1**: Create RepoWorkspace page
- Create `pages/repo-workspace.tsx` - Create `pages/repo-workspace.tsx`
- Layout: Sidebar + Main Content - Layout: Sidebar + Main Content
- Fetch project repos on load - Fetch project repos on load
- Select first repo by default - Select first repo by default
- [ ] **Task 3.2**: Create RepoSelector component - [x] **Task 3.2**: Create RepoSelector component
- Dropdown to switch between project repos - Dropdown to switch between project repos
- Show active repo name - Show active repo name
- Update URL when switching - Update URL when switching
- [ ] **Task 3.3**: Create BranchSelector component - [x] **Task 3.3**: Create BranchSelector component
- Dropdown to switch branches - Dropdown to switch branches
- Show active branch - Show active branch
- Mark default branch - Mark default branch
@@ -64,7 +64,7 @@
## Phase 4: File Browser ## Phase 4: File Browser
- [ ] **Task 4.1**: Create FileTree component - [x] **Task 4.1**: Create FileTree component
- Recursive tree view - Recursive tree view
- Expandable/collapsible folders - Expandable/collapsible folders
- File icons by extension - File icons by extension
@@ -72,21 +72,21 @@
- Active file highlight - Active file highlight
- Fetch tree data from API - Fetch tree data from API
- [ ] **Task 4.2**: Add file tree loading - [x] **Task 4.2**: Add file tree loading
- Load root on repo/branch change - Load root on repo/branch change
- Lazy load subdirectories - Lazy load subdirectories
- Show loading state - Show loading state
## Phase 5: File Viewer ## Phase 5: File Viewer
- [ ] **Task 5.1**: Create FileViewer component - [x] **Task 5.1**: Create FileViewer component
- Display file content - Display file content
- Line numbers - Line numbers
- Syntax highlighting (prismjs or similar) - Syntax highlighting (prismjs or similar)
- Breadcrumb navigation - Breadcrumb navigation
- Show file metadata (size, last commit) - Show file metadata (size, last commit)
- [ ] **Task 5.2**: Add edit mode - [x] **Task 5.2**: Add edit mode
- Toggle between view/edit - Toggle between view/edit
- Textarea for editing - Textarea for editing
- Save button (calls update API) - Save button (calls update API)
@@ -95,37 +95,37 @@
## Phase 6: Integration & Polish ## Phase 6: Integration & Polish
- [ ] **Task 6.1**: Sync URL state - [x] **Task 6.1**: Sync URL state
- Repo ID in URL - Repo ID in URL
- Branch in URL - Branch in URL
- Path in URL - Path in URL
- Parse on load, update on change - Parse on load, update on change
- [ ] **Task 6.2**: Add error handling - [x] **Task 6.2**: Add error handling
- Repo not found - Repo not found
- Branch not found - Branch not found
- File not found - File not found
- Binary files - Binary files
- Network errors - Network errors
- [ ] **Task 6.3**: Add CSS styles - [x] **Task 6.3**: Add CSS styles
- Workspace layout - Workspace layout
- File tree styles - File tree styles
- File viewer styles - File viewer styles
- Sidebar styles - Sidebar styles
- Responsive design - Responsive design
- [ ] **Task 6.4**: Run quality gates - [x] **Task 6.4**: Run quality gates
- Backend: ruff, mypy, pytest - Backend: ruff, mypy, pytest
- Frontend: typecheck, lint, build - Frontend: typecheck, lint, build
## Phase 7: Route Updates ## Phase 7: Route Updates
- [ ] **Task 7.1**: Update router - [x] **Task 7.1**: Update router
- `/projects/:projectId` → RepoWorkspace (default) - `/projects/:projectId` → RepoWorkspace (default)
- Move old project details to `/projects/:projectId/details` or remove - Move old project details to `/projects/:projectId/details` or remove
- Keep `/projects/:projectId/repositories` for repo management - Keep `/projects/:projectId/repositories` for repo management
- [ ] **Task 7.2**: Update navigation - [x] **Task 7.2**: Update navigation
- Project list links to workspace - Project list links to workspace
- Add "Manage Repositories" link in workspace - Add "Manage Repositories" link in workspace
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-19
@@ -0,0 +1,154 @@
# Documentation Overhaul - Design
## Documentation Structure
```
docs/
├── README.md # Documentation index
├── architecture/
│ ├── backend.md # Backend architecture
│ ├── frontend.md # Frontend architecture
│ ├── database.md # Database schema
│ └── deployment.md # Deployment architecture
├── features/
│ ├── projects.md # Project management
│ ├── repositories.md # Git repositories
│ ├── workspace.md # Repository workspace
│ ├── git-history.md # Git history visualization
│ ├── auth.md # Authentication
│ ├── tool-types.md # Tool type management
│ └── settings.md # User settings
├── api/
│ ├── README.md # API overview
│ ├── auth.md # Auth endpoints
│ ├── projects.md # Project endpoints
│ ├── repositories.md # Repository endpoints
│ └── users.md # User endpoints
├── deployment/
│ ├── docker.md # Docker setup
│ ├── traefik.md # Traefik configuration
│ ├── authentik.md # Authentik setup
│ └── environment.md # Environment variables
├── development/
│ ├── setup.md # Development setup
│ ├── testing.md # Testing strategy (moved from README)
│ ├── contributing.md # How to contribute
│ └── quality-gates.md # Code quality
└── templates/
├── feature-doc.md # Template for new features
├── api-endpoint.md # Template for API docs
└── architecture.md # Template for architecture docs
```
## README.md Structure
```markdown
# Headquarter
## Overview
Short description of what the project is and does.
## Features
- Feature 1 (with link to docs)
- Feature 2 (with link to docs)
## Quick Start
1. Clone repo
2. Copy .env.example to .env
3. docker compose up
4. Open http://localhost:5173
## Architecture
Link to architecture docs.
## Documentation
- [User Guide](docs/features/)
- [API Docs](docs/api/)
- [Deployment](docs/deployment/)
- [Development](docs/development/)
## Tech Stack
- Backend: FastAPI + SQLAlchemy + PostgreSQL
- Frontend: React + TypeScript + Vite
- Auth: Authentik (OAuth2)
- Deployment: Docker + Traefik
```
## Documentation Templates
### Feature Documentation Template
```markdown
# Feature Name
## Overview
What does this feature do?
## How to Use
Step-by-step user guide.
## Screenshots/Diagrams
Visual aids.
## API Endpoints
Related API endpoints.
## Configuration
Relevant config options.
## Related Features
Links to related docs.
```
### API Endpoint Template
```markdown
## GET /api/endpoint
**Description:** What this endpoint does.
**Authentication:** Required/Optional
**Request:**
- Query params
- Body schema
**Response:**
- Success schema
- Error codes
**Example:**
```bash
curl /api/endpoint
```
```
## Auto-Documentation Process
For new features, documentation should be created:
1. **During implementation** (not after):
- When creating a new feature, create its doc file
- Use the template from docs/templates/
- Update README features list
2. **In the same commit**:
- Code changes + doc changes in same PR
- Review docs alongside code
3. **Checklist**:
- [ ] Feature doc created in docs/features/
- [ ] API endpoints documented in docs/api/
- [ ] README updated with feature link
- [ ] Architecture doc updated if needed
## Maintenance
- **Monthly review**: Check for outdated docs
- **Version tracking**: Tag docs with app version
- **OpenSpec integration**: Link to OpenSpec changes for context
## Tools
- **Markdown**: All docs in Markdown
- **Mermaid**: Diagrams in Mermaid syntax
- **FastAPI docs**: Auto-generated from code
- **GitHub Pages**: Optional static site generation
@@ -0,0 +1,56 @@
# Documentation Overhaul
## Problem
The project has grown significantly but documentation hasn't kept up:
- **README is minimal**: Only contains testing strategy, no project overview
- **No feature documentation**: Users can't discover what the app does
- **No API docs**: Developers have to read source code
- **No deployment guide**: Docker/Traefik setup is tribal knowledge
- **No architecture docs**: New contributors can't understand the codebase
- **No user guide**: Features like repo workspace, git history aren't explained
- **OpenSpec changes aren't linked**: Completed changes exist but aren't referenced
## Solution
Create a comprehensive documentation system:
1. **Rewrite README**: Project overview, features, quick start, architecture
2. **Create docs/ directory**: Structured documentation
3. **Document all features**: What exists and how to use it
4. **Create API documentation**: Auto-generated + manual docs
5. **Create deployment guide**: Docker, Traefik, Authentik setup
6. **Create architecture docs**: Backend, frontend, data flow
7. **Create documentation templates**: For future features
8. **Create CONTRIBUTING.md**: How to add docs for new features
## Benefits
- **Onboarding**: New developers understand the project in minutes
- **Discovery**: Users discover features they didn't know existed
- **Maintenance**: Architecture docs help refactoring decisions
- **Deployment**: Clear setup instructions reduce support burden
- **Future-proof**: Templates ensure new features get documented
## Scope
### What gets documented:
- All existing features (projects, repos, git history, workspace, auth, etc.)
- Architecture (backend, frontend, database, deployment)
- API endpoints
- Configuration options
- Development setup
### What gets created:
- README.md (rewritten)
- docs/ directory with structured docs
- docs/templates/ for new features
- docs/api/ for API documentation
- docs/architecture/ for system design
- docs/deployment/ for setup guides
- docs/features/ for user guides
### What stays:
- Testing strategy (moved to docs/testing.md)
- OpenSpec changes (archived as-is)
@@ -0,0 +1,178 @@
# Documentation Overhaul Specification
## Requirements
### Functional Requirements
1. **README Rewrite**: Comprehensive project overview with features, quick start, and links
2. **Feature Documentation**: Every feature has a user guide in docs/features/
3. **API Documentation**: All endpoints documented with examples
4. **Architecture Docs**: Backend, frontend, and deployment architecture explained
5. **Deployment Guide**: Step-by-step Docker/Traefik/Authentik setup
6. **Development Guide**: Setup, testing, contributing guidelines
7. **Templates**: Reusable templates for future documentation
8. **Auto-Documentation**: Process ensuring new features get documented
### Non-Functional Requirements
1. **Discoverability**: Users can find docs easily from README
2. **Completeness**: All current features documented
3. **Accuracy**: Docs match current implementation
4. **Maintainability**: Templates and processes keep docs up-to-date
5. **Accessibility**: Markdown format, clear structure
## Documentation Inventory
### Current Features to Document
1. **Project Management**
- Create/edit/delete projects
- Project list view
- Project workspace (default view)
2. **Git Repositories**
- Create repositories (bare init, mirror clone)
- Smart URL parsing
- Repository list
- Repository deletion
3. **Repository Workspace**
- File browser (tree view)
- File viewer (syntax highlighting)
- Branch switching
- Repository switching
- Quick file editing
4. **Git History**
- Commit history visualization
- Branch graph
- Commit details (diff, stats)
- Branch filtering
5. **Authentication**
- Authentik OAuth2 flow
- Session-based auth
- User profile
- Logout
6. **User Settings**
- Theme selection
- Git identity
- Default editor
7. **Tool Types**
- Built-in types (code-server, jupyter)
- Custom type creation
- Compose template validation
8. **SSH Keys**
- Generate key pairs
- List/delete keys
- Copy public key
## API Endpoints to Document
### Auth
- GET /auth/login
- GET /auth/callback
- GET /auth/me
- POST /auth/logout
### Projects
- GET /projects
- POST /projects
- GET /projects/{id}
- PUT /projects/{id}
- DELETE /projects/{id}
### Repositories
- GET /projects/{id}/repositories
- POST /projects/{id}/repositories
- DELETE /projects/{id}/repositories/{id}
- GET /projects/{id}/repositories/{id}/files
- GET /projects/{id}/repositories/{id}/files/content
- POST /projects/{id}/repositories/{id}/files/content
- GET /projects/{id}/repositories/{id}/branches
- GET /projects/{id}/repositories/{id}/history
- GET /projects/{id}/repositories/{id}/commits/{hash}
### Users
- GET /users/me
- PUT /users/me
- POST /users/me/avatar
- GET /users/me/config
- PATCH /users/me/config
### Tool Types
- GET /tool-types
- POST /tool-types
- GET /tool-types/{id}
- PUT /tool-types/{id}
- DELETE /tool-types/{id}
### SSH Keys
- GET /ssh-keys
- POST /ssh-keys
- DELETE /ssh-keys/{id}
## Documentation Templates
### Feature Doc Template
```markdown
# [Feature Name]
## Overview
[1-2 sentence description]
## How to Use
[Step-by-step guide]
## Screenshots
[If applicable]
## API Reference
[Links to API docs]
## Configuration
[Relevant env vars/settings]
## Related
[Links to related features]
```
### API Doc Template
```markdown
## [METHOD] [PATH]
**Auth:** [Required/Optional]
**Description:** [What it does]
### Request
[Params/body schema]
### Response
[Success/error schemas]
### Example
[Code example]
```
## Implementation Checklist
For every new feature:
- [ ] Create feature doc in docs/features/
- [ ] Document API endpoints in docs/api/
- [ ] Update README.md features list
- [ ] Update architecture docs if needed
- [ ] Add to CHANGELOG.md
## Success Criteria
1. README provides clear project overview
2. All 8 feature areas have user guides
3. All API endpoints have documentation
4. New developer can set up project in < 15 minutes
5. Deployment guide enables setup without asking questions
6. Templates exist for future documentation
7. CONTRIBUTING.md explains documentation requirements
@@ -0,0 +1,202 @@
# Documentation Overhaul - Tasks
## Phase 1: README Rewrite
- [ ] **Task 1.1**: Rewrite README.md
- Add project overview and description
- List all features with brief descriptions
- Add quick start section
- Add tech stack section
- Add links to docs/ directory
- Keep testing strategy (or move to docs/testing.md)
- [ ] **Task 1.2**: Create docs/README.md
- Documentation index
- Link to all doc sections
- Quick navigation
## Phase 2: Create Documentation Structure
- [ ] **Task 2.1**: Create docs/ directory structure
- mkdir docs/architecture
- mkdir docs/features
- mkdir docs/api
- mkdir docs/deployment
- mkdir docs/development
- mkdir docs/templates
- [ ] **Task 2.2**: Create documentation templates
- docs/templates/feature-doc.md
- docs/templates/api-endpoint.md
- docs/templates/architecture.md
## Phase 3: Feature Documentation
- [ ] **Task 3.1**: Document Project Management
- docs/features/projects.md
- Creating/editing/deleting projects
- Project list view
- Project workspace
- [ ] **Task 3.2**: Document Git Repositories
- docs/features/repositories.md
- Creating repos (bare init, mirror clone)
- Smart URL parsing
- Repository management
- [ ] **Task 3.3**: Document Repository Workspace
- docs/features/workspace.md
- File browser
- File viewer
- Branch switching
- Quick editing
- [ ] **Task 3.4**: Document Git History
- docs/features/git-history.md
- Commit history view
- Branch graph
- Commit details
- [ ] **Task 3.5**: Document Authentication
- docs/features/auth.md
- Authentik OAuth2 flow
- Session management
- User profile
- [ ] **Task 3.6**: Document User Settings
- docs/features/settings.md
- Theme selection
- Git identity
- Preferences
- [ ] **Task 3.7**: Document Tool Types
- docs/features/tool-types.md
- Built-in types
- Custom type creation
- Compose templates
- [ ] **Task 3.8**: Document SSH Keys
- docs/features/ssh-keys.md
- Key generation
- Management
## Phase 4: API Documentation
- [ ] **Task 4.1**: Document Auth API
- docs/api/auth.md
- All auth endpoints
- [ ] **Task 4.2**: Document Projects API
- docs/api/projects.md
- All project endpoints
- [ ] **Task 4.3**: Document Repositories API
- docs/api/repositories.md
- All repository endpoints
- File operations
- History endpoints
- [ ] **Task 4.4**: Document Users API
- docs/api/users.md
- User profile endpoints
- Config endpoints
- [ ] **Task 4.5**: Document Tool Types API
- docs/api/tool-types.md
- CRUD endpoints
- [ ] **Task 4.6**: Document SSH Keys API
- docs/api/ssh-keys.md
- Key management endpoints
## Phase 5: Architecture Documentation
- [ ] **Task 5.1**: Create backend architecture doc
- docs/architecture/backend.md
- Tech stack
- Directory structure
- Data flow
- Auth flow
- [ ] **Task 5.2**: Create frontend architecture doc
- docs/architecture/frontend.md
- Tech stack
- Directory structure
- State management
- Routing
- [ ] **Task 5.3**: Create database schema doc
- docs/architecture/database.md
- Entity relationship diagram
- Table descriptions
- Migration strategy
- [ ] **Task 5.4**: Create deployment architecture doc
- docs/architecture/deployment.md
- Docker architecture
- Traefik routing
- Service diagram
## Phase 6: Deployment Guide
- [ ] **Task 6.1**: Create Docker setup guide
- docs/deployment/docker.md
- Local development setup
- Docker compose configuration
- [ ] **Task 6.2**: Create Traefik guide
- docs/deployment/traefik.md
- Traefik configuration
- Routing rules
- TLS setup
- [ ] **Task 6.3**: Create Authentik guide
- docs/deployment/authentik.md
- Provider setup
- Application configuration
- OAuth2 settings
- [ ] **Task 6.4**: Create environment variables guide
- docs/deployment/environment.md
- All env vars explained
- Required vs optional
- Default values
## Phase 7: Development Guide
- [ ] **Task 7.1**: Create setup guide
- docs/development/setup.md
- Prerequisites
- Installation steps
- Running locally
- [ ] **Task 7.2**: Move testing strategy
- Move from README.md to docs/development/testing.md
- Update references
- [ ] **Task 7.3**: Create contributing guide
- docs/development/contributing.md
- Code style
- PR process
- Documentation requirements
- [ ] **Task 7.4**: Create quality gates doc
- docs/development/quality-gates.md
- Linting rules
- Type checking
- Testing requirements
## Phase 8: Finalization
- [ ] **Task 8.1**: Review all docs
- Check for completeness
- Check for accuracy
- Fix broken links
- [ ] **Task 8.2**: Create CHANGELOG.md
- List all features implemented
- Link to OpenSpec changes
- [ ] **Task 8.3**: Commit all documentation
- Single commit for docs
- Conventional commit message
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-05-19
+273
View File
@@ -0,0 +1,273 @@
# Git Control - Design
## Architecture
```
Repository Workspace
├─ Toolbar
│ ├─ [Fetch] [Pull] [Push]
│ ├─ [Branch: main ▼] [+ New Branch]
│ └─ [Commit] [Merge ▼]
├─ Sidebar
│ ├─ Repo Selector
│ ├─ Branch Selector (with management)
│ └─ File Tree (with status icons)
│ ├─ 📄 main.py ✏️ (modified)
│ ├─ 📁 src/
│ └─ 📄 README.md ✨ (new)
└─ Main Content
├─ File Viewer (with edit/save)
└─ Commit Panel (when files modified)
├─ Changed files list
├─ Commit message input
└─ [Commit to main] button
```
## Git Operations
### Branch Operations
**Create Branch:**
```
POST /projects/{id}/repositories/{id}/branches
{
"name": "feature/new-thing",
"base_branch": "main"
}
```
**Delete Branch:**
```
DELETE /projects/{id}/repositories/{id}/branches/{name}
```
**Checkout Branch:**
```
POST /projects/{id}/repositories/{id}/checkout
{
"branch": "feature/new-thing"
}
```
### Working Directory Status
**Get Status:**
```
GET /projects/{id}/repositories/{id}/status
```
Response:
```json
{
"branch": "main",
"modified": ["src/main.py", "README.md"],
"added": ["new-file.txt"],
"deleted": ["old-file.txt"],
"untracked": ["temp.log"]
}
```
### Commit Operations
**Commit Changes:**
```
POST /projects/{id}/repositories/{id}/commits
{
"message": "Update greeting",
"author_name": "User",
"author_email": "user@example.com",
"files": ["src/main.py", "README.md"]
}
```
### Remote Operations
**Fetch:**
```
POST /projects/{id}/repositories/{id}/fetch
```
**Pull:**
```
POST /projects/{id}/repositories/{id}/pull
{
"branch": "main",
"strategy": "merge"
}
```
**Push:**
```
POST /projects/{id}/repositories/{id}/push
{
"branch": "main"
}
```
**Merge:**
```
POST /projects/{id}/repositories/{id}/merge
{
"source_branch": "feature/new-thing",
"target_branch": "main",
"commit_message": "Merge feature into main"
}
```
## Backend Implementation
### Git Command Utilities
Extend `src/utils/git_files.py` with:
- `get_status(repo_path)` - working directory status
- `create_branch(repo_path, name, base)` - create new branch
- `delete_branch(repo_path, name)` - delete branch
- `checkout_branch(repo_path, name)` - switch branch
- `commit_changes(repo_path, files, message, author)` - commit files
- `fetch(repo_path)` - fetch from remote
- `pull(repo_path, branch)` - pull updates
- `push(repo_path, branch)` - push changes
- `merge(repo_path, source, target, message)` - merge branches
### Error Handling
All git operations can fail:
- **Merge conflicts**: Return conflict details, require resolution
- **Auth failures**: Remote requires authentication
- **Dirty working tree**: Can't checkout with uncommitted changes
- **Branch exists**: Can't create duplicate branch
- **Nothing to commit**: Working tree clean
### Security
- All operations check repository ownership
- Push/pull requires valid remote URL
- Commits use authenticated user's identity
## Frontend Implementation
### Workspace Toolbar
Add git action bar above file viewer:
```
┌──────────────────────────────────────────────────────┐
│ [Fetch] [Pull] [Push] │ Branch: [main ▼] [+ New] │
│ │ [Commit ▼] [Merge ▼] │
└──────────────────────────────────────────────────────┘
```
### Branch Management
**Branch Selector Dropdown:**
- List all branches with current branch highlighted
- Create new branch option (opens dialog)
- Delete branch option (with confirmation)
**New Branch Dialog:**
```
┌──────────────────────────────┐
│ Create New Branch │
├──────────────────────────────┤
│ Name: [feature/________] │
│ Base: [main ▼] │
│ │
│ [Create] [Cancel] │
└──────────────────────────────┘
```
### Working Directory Status
**Status Indicators in File Tree:**
- ✏️ Modified file
- ✨ New file
- 🗑️ Deleted file
- ❓ Untracked file
**Commit Panel (appears when files modified):**
```
┌──────────────────────────────┐
│ Changes (3) │
├──────────────────────────────┤
│ ✏️ src/main.py │
│ ✨ new-file.txt │
│ 🗑️ old-file.txt │
├──────────────────────────────┤
│ Commit message: │
│ [____________________] │
│ │
│ [Commit to main] │
└──────────────────────────────┘
```
### Merge Dialog
```
┌──────────────────────────────┐
│ Merge Branch │
├──────────────────────────────┤
│ Source: [feature/xyz ▼] │
│ Target: main │
│ │
│ Commit message: │
│ [Merge feature/xyz into main]│
│ │
│ [Merge] [Cancel] │
└──────────────────────────────┘
```
## Implementation Order
1. **Backend git utilities** - Status, branch, commit, remote operations
2. **Backend API endpoints** - All git operation endpoints
3. **Frontend toolbar** - Git action buttons
4. **Frontend branch management** - Create/delete/checkout
5. **Frontend status display** - Modified file indicators
6. **Frontend commit panel** - Commit UI
7. **Frontend merge dialog** - Merge UI
8. **Integration** - Wire everything together
9. **Tests** - Backend + frontend tests
## State Management
### URL State
```
/projects/:projectId?repo=:repoId&branch=:branch&path=:path
```
### React State
```typescript
interface GitState {
currentBranch: string;
branches: Branch[];
status: {
modified: string[];
added: string[];
deleted: string[];
untracked: string[];
};
isLoading: boolean;
lastOperation: string | null;
}
```
## Error Handling
### User-Facing Errors
- **"Cannot checkout: uncommitted changes"** - Show commit panel
- **"Merge conflict"** - Show conflict resolution UI
- **"Push rejected: non-fast-forward"** - Suggest pull first
- **"Authentication failed"** - Show SSH key settings
- **"Nothing to commit"** - Working tree clean
### Conflict Resolution (Future)
For now, show error and abort. Later can add:
- Diff view of conflicts
- Manual resolution editor
- Accept ours/theirs buttons
## Performance Considerations
- **Status updates**: Poll every 5 seconds when viewing workspace
- **Fetch on load**: Auto-fetch when opening workspace (optional)
- **Lazy operations**: Don't fetch until user clicks fetch/pull
- **Progress indicators**: Show for long operations (clone, push, merge)
+44
View File
@@ -0,0 +1,44 @@
# Git Control Features
## Problem
The repository workspace currently provides read-only access to git repositories. Users can browse files, view history, and see branches, but cannot perform git operations like creating branches, committing changes, pushing/pulling, or merging.
## Solution
Add git control capabilities to the repository workspace, allowing users to:
1. Create and delete branches
2. Switch between branches (checkout)
3. Commit changes (for quick edits)
4. Push changes to remote
5. Pull/fetch updates from remote
6. Merge branches
7. View working directory status
## Benefits
- **Complete workflow**: Users can make small changes without leaving the browser
- **Quick iterations**: Fix typos, update configs, make small adjustments
- **Branch management**: Create feature branches, merge when done
- **Remote sync**: Keep repositories up to date
- **No external tools needed**: Everything in the browser
## Scope
### What stays:
- Existing file browser and viewer
- Git history visualization
- Repository management (create/delete)
### What's new:
- Branch CRUD operations
- Working directory status (modified files)
- Commit operations
- Push/pull/fetch
- Merge capabilities
- Git action toolbar in workspace
### What changes:
- Workspace toolbar gets git action buttons
- File viewer gets "modified" indicators
- Branch selector gets management options
+296
View File
@@ -0,0 +1,296 @@
# Git Control Specification
## Requirements
### Functional Requirements
1. **Branch Management**: Create, delete, list, and switch branches
2. **Working Directory**: View modified, added, deleted, and untracked files
3. **Commit Changes**: Stage and commit file changes with message
4. **Remote Sync**: Fetch, pull, and push to remote repositories
5. **Merge Branches**: Merge one branch into another
6. **Status Indicators**: Show file modification status in file tree
### Non-Functional Requirements
1. **Performance**: Git operations complete in < 3 seconds
2. **Feedback**: Show progress for long operations (push, pull, merge)
3. **Error Handling**: Clear error messages for all git failures
4. **Safety**: Confirm destructive operations (delete branch, force push)
## API Specification
### Branch Operations
#### POST /projects/{project_id}/repositories/{repo_id}/branches
Create a new branch.
**Request:**
```json
{
"name": "feature/new-thing",
"base_branch": "main"
}
```
**Response 201:**
```json
{
"name": "feature/new-thing",
"base_commit": "abc123"
}
```
**Response 400:** Branch already exists
#### DELETE /projects/{project_id}/repositories/{repo_id}/branches/{branch_name}
Delete a branch.
**Response 204:** Success
**Response 400:** Cannot delete current branch
#### POST /projects/{project_id}/repositories/{repo_id}/checkout
Checkout a branch.
**Request:**
```json
{
"branch": "feature/new-thing"
}
```
**Response 200:**
```json
{
"branch": "feature/new-thing",
"commit": "abc123"
}
```
**Response 400:** Uncommitted changes
### Status Operations
#### GET /projects/{project_id}/repositories/{repo_id}/status
Get working directory status.
**Response 200:**
```json
{
"branch": "main",
"ahead": 2,
"behind": 1,
"modified": ["src/main.py"],
"added": ["new-file.txt"],
"deleted": [],
"untracked": ["temp.log"],
"renamed": []
}
```
### Commit Operations
#### POST /projects/{project_id}/repositories/{repo_id}/commits
Commit staged changes.
**Request:**
```json
{
"message": "Update greeting",
"author_name": "User",
"author_email": "user@example.com"
}
```
**Response 201:**
```json
{
"hash": "def789",
"message": "Update greeting",
"branch": "main"
}
```
**Response 400:** Nothing to commit
### Remote Operations
#### POST /projects/{project_id}/repositories/{repo_id}/fetch
Fetch from remote.
**Response 200:**
```json
{
"success": true,
"fetched_branches": ["origin/main", "origin/develop"]
}
```
#### POST /projects/{project_id}/repositories/{repo_id}/pull
Pull updates from remote.
**Request:**
```json
{
"branch": "main"
}
```
**Response 200:**
```json
{
"success": true,
"commits": 3,
"files_changed": ["src/main.py", "README.md"]
}
```
**Response 409:** Merge conflict
#### POST /projects/{project_id}/repositories/{repo_id}/push
Push to remote.
**Request:**
```json
{
"branch": "main"
}
```
**Response 200:**
```json
{
"success": true,
"pushed_commits": 2
}
```
**Response 400:** Non-fast-forward
### Merge Operations
#### POST /projects/{project_id}/repositories/{repo_id}/merge
Merge branches.
**Request:**
```json
{
"source_branch": "feature/new-thing",
"target_branch": "main",
"commit_message": "Merge feature into main"
}
```
**Response 200:**
```json
{
"success": true,
"commit_hash": "abc789",
"files_changed": 5
}
```
**Response 409:** Merge conflict
## Data Model
### GitStatus
```typescript
interface GitStatus {
branch: string;
ahead: number;
behind: number;
modified: string[];
added: string[];
deleted: string[];
untracked: string[];
renamed: Array<{from: string, to: string}>;
}
```
### CommitInfo
```typescript
interface CommitInfo {
hash: string;
message: string;
branch: string;
author_name: string;
author_email: string;
date: string;
}
```
## Frontend Specification
### Components
**GitToolbar:**
- Fetch button
- Pull button (with behind count badge)
- Push button (with ahead count badge)
- Branch selector (with create/delete)
- Commit button (enabled when changes exist)
- Merge button
**BranchSelector:**
- Dropdown with all branches
- Current branch highlighted
- "Create new branch" option
- Delete option (with confirmation)
**CommitPanel:**
- Shows when files are modified
- Lists changed files with checkboxes
- Commit message input
- Commit button
**StatusIndicator:**
- Small badge on file tree items
- Shows modification type
### State Management
```typescript
interface GitControlState {
status: GitStatus | null;
isLoading: boolean;
operations: Array<{
type: string;
status: 'pending' | 'success' | 'error';
message: string;
}>;
}
```
## Error Handling
| Error Code | Description | User Action |
|------------|-------------|-------------|
| DIRTY_WORKING_TREE | Uncommitted changes | Commit or stash changes |
| MERGE_CONFLICT | Merge failed with conflicts | Resolve conflicts manually |
| NON_FAST_FORWARD | Push rejected | Pull first |
| AUTH_FAILED | Remote auth failed | Check SSH keys |
| BRANCH_EXISTS | Branch already exists | Choose different name |
| NOTHING_TO_COMMIT | Working tree clean | N/A |
| CANNOT_DELETE_CURRENT | Can't delete checked out branch | Switch branches first |
## Testing Strategy
### Backend Tests
- Test branch creation/deletion
- Test checkout with/without changes
- Test commit operations
- Test fetch/pull/push
- Test merge (fast-forward and conflict)
- Test error cases
### Frontend Tests
- Test toolbar buttons
- Test branch selector
- Test commit panel
- Test status indicators
- Test error handling
### Integration Tests
- Full workflow: create branch → edit file → commit → push → merge
+142
View File
@@ -0,0 +1,142 @@
# Git Control - Tasks
## Phase 1: Backend Git Utilities
- [x] **Task 1.1**: Extend git utilities
- Add to `src/utils/git_control.py`:
- `get_status(repo_path)` - working directory status
- `create_branch(repo_path, name, base)` - create branch
- `delete_branch(repo_path, name)` - delete branch
- `checkout_branch(repo_path, name)` - switch branch
- `commit_changes(repo_path, message, author)` - commit
- Add tests
- [x] **Task 1.2**: Add remote operations
- Add to `src/utils/git_control.py`:
- `fetch(repo_path)` - fetch from remote
- `pull(repo_path, branch)` - pull updates
- `push(repo_path, branch)` - push changes
- `merge(repo_path, source, target, message)` - merge
- Handle errors (conflicts, auth, etc.)
- Add tests
## Phase 2: Backend API Endpoints
- [x] **Task 2.1**: Branch management endpoints
- POST `/projects/{id}/repositories/{id}/branches` - create
- DELETE `/projects/{id}/repositories/{id}/branches/{name}` - delete
- POST `/projects/{id}/repositories/{id}/checkout` - checkout
- Add to `src/api/git_repositories.py`
- [x] **Task 2.2**: Status endpoint
- GET `/projects/{id}/repositories/{id}/status`
- Returns working directory status
- [x] **Task 2.3**: Commit endpoint
- POST `/projects/{id}/repositories/{id}/commits`
- Commits all staged changes
- [x] **Task 2.4**: Remote operation endpoints
- POST `/projects/{id}/repositories/{id}/fetch`
- POST `/projects/{id}/repositories/{id}/pull`
- POST `/projects/{id}/repositories/{id}/push`
- [x] **Task 2.5**: Merge endpoint
- POST `/projects/{id}/repositories/{id}/merge`
- Handle conflict responses
## Phase 3: Frontend Git Toolbar
- [ ] **Task 3.1**: Create GitToolbar component
- Fetch, Pull, Push buttons
- Branch selector with count badges
- Commit button
- Merge button
- Add to workspace layout
- [ ] **Task 3.2**: Add status polling
- Poll status every 5 seconds
- Update toolbar badges (ahead/behind)
- Show commit button when changes exist
## Phase 4: Branch Management
- [ ] **Task 4.1**: Enhance BranchSelector
- Add "Create new branch" option
- Add delete option with confirmation
- Show current branch
- Call branch API endpoints
- [ ] **Task 4.2**: Create NewBranchDialog
- Branch name input
- Base branch selector
- Create/Cancel buttons
## Phase 5: Commit Workflow
- [ ] **Task 5.1**: Create CommitPanel component
- Shows when files are modified
- Lists changed files
- Commit message input
- Commit button
- Success/error feedback
- [ ] **Task 5.2**: Add status indicators to FileTree
- Modified icon (✏️)
- New file icon (✨)
- Deleted icon (🗑️)
- Untracked icon (❓)
## Phase 6: Remote Operations
- [ ] **Task 6.1**: Implement fetch/pull/push
- Wire toolbar buttons to API
- Show progress indicators
- Handle errors (auth, conflicts, etc.)
- Update status after operations
- [ ] **Task 6.2**: Create MergeDialog
- Source branch selector
- Target branch display
- Commit message input
- Merge/Cancel buttons
- Handle conflicts
## Phase 7: Integration & Polish
- [ ] **Task 7.1**: Wire everything together
- Connect toolbar to all APIs
- Update workspace state after operations
- Refresh file tree on branch switch
- [ ] **Task 7.2**: Add error handling
- Show toast notifications for operations
- Handle all error cases gracefully
- Provide recovery options
- [ ] **Task 7.3**: Add CSS styles
- Toolbar layout
- Status indicators
- Commit panel
- Dialogs
- [ ] **Task 7.4**: Run quality gates
- Backend: ruff, mypy, pytest
- Frontend: typecheck, lint, build
## Phase 8: Testing
- [ ] **Task 8.1**: Backend tests
- Test all git operations
- Test error cases
- Test auth failures
- [ ] **Task 8.2**: Frontend tests
- Test toolbar interactions
- Test branch management
- Test commit workflow
- [ ] **Task 8.3**: Manual testing
- Create branch → edit → commit → push → merge workflow
- Test error scenarios
- Test with multiple repos