feat: add repository workspace as default project view
- Create git file utilities (list_tree, get_file_content, list_branches, commit_file) - Add file browsing API endpoints (list, content, branches, update) - Create RepoWorkspace page with sidebar + main content layout - Add FileTree component with directory navigation - Add FileViewer component for viewing file contents - Update project list to link to workspace - Add workspace CSS styles - Update router with workspace route Quality gates: ruff ✓, mypy ✓, typecheck ✓, build ✓
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# Repository Workspace Specification
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
1. **Default Project View**: Clicking a project opens the repository workspace
|
||||
2. **File Browser**: Tree view of repository files and directories
|
||||
3. **Branch Navigation**: Switch between branches to view different states
|
||||
4. **File Viewer**: View file contents with syntax highlighting
|
||||
5. **Quick Edit**: Make small changes and commit them
|
||||
6. **Repository Switching**: Switch between repositories in a project
|
||||
7. **Breadcrumb Navigation**: Show current file path with clickable segments
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
1. **Performance**: File tree loads in < 1 second
|
||||
2. **Responsiveness**: UI remains responsive during git operations
|
||||
3. **Usability**: Familiar interface similar to GitHub/GitLab
|
||||
4. **Accessibility**: Keyboard navigation, screen reader support
|
||||
|
||||
## API Specification
|
||||
|
||||
### GET /projects/{project_id}/repositories/{repo_id}/files
|
||||
List files in a directory.
|
||||
|
||||
**Query Parameters:**
|
||||
- `branch` (optional): Branch name, defaults to repository default branch
|
||||
- `path` (optional): Directory path, defaults to root
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"path": "src",
|
||||
"branch": "main",
|
||||
"entries": [
|
||||
{
|
||||
"name": "components",
|
||||
"type": "directory",
|
||||
"path": "src/components",
|
||||
"mode": "040000"
|
||||
},
|
||||
{
|
||||
"name": "main.py",
|
||||
"type": "file",
|
||||
"path": "src/main.py",
|
||||
"size": 1234,
|
||||
"mode": "100644",
|
||||
"last_commit": {
|
||||
"hash": "abc123",
|
||||
"message": "Initial commit",
|
||||
"author": "John Doe",
|
||||
"date": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404:** Branch or path not found
|
||||
|
||||
### GET /projects/{project_id}/repositories/{repo_id}/files/content
|
||||
Get file content.
|
||||
|
||||
**Query Parameters:**
|
||||
- `branch` (required): Branch name
|
||||
- `path` (required): File path
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"path": "src/main.py",
|
||||
"branch": "main",
|
||||
"content": "function hello() {\n return 'world';\n}",
|
||||
"size": 42,
|
||||
"encoding": "utf-8",
|
||||
"language": "python",
|
||||
"is_binary": false,
|
||||
"last_commit": {
|
||||
"hash": "abc123",
|
||||
"message": "Initial commit",
|
||||
"author": "John Doe",
|
||||
"date": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response 404:** File not found
|
||||
|
||||
### GET /projects/{project_id}/repositories/{repo_id}/branches
|
||||
List branches.
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"branches": [
|
||||
{
|
||||
"name": "main",
|
||||
"is_default": true,
|
||||
"last_commit": {
|
||||
"hash": "abc123",
|
||||
"message": "Initial commit",
|
||||
"date": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_branch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /projects/{project_id}/repositories/{repo_id}/files/content
|
||||
Update file content.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"path": "src/main.py",
|
||||
"branch": "main",
|
||||
"content": "new content",
|
||||
"commit_message": "Update file",
|
||||
"author_name": "User",
|
||||
"author_email": "user@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"commit_hash": "def789",
|
||||
"message": "Update file",
|
||||
"branch": "main"
|
||||
}
|
||||
```
|
||||
|
||||
## Data Model
|
||||
|
||||
### FileTreeEntry
|
||||
```typescript
|
||||
interface FileTreeEntry {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
path: string;
|
||||
size?: number;
|
||||
mode?: string;
|
||||
last_commit?: {
|
||||
hash: string;
|
||||
message: string;
|
||||
author: string;
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Branch
|
||||
```typescript
|
||||
interface Branch {
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
last_commit?: {
|
||||
hash: string;
|
||||
message: string;
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### FileContent
|
||||
```typescript
|
||||
interface FileContent {
|
||||
path: string;
|
||||
branch: string;
|
||||
content: string;
|
||||
size: number;
|
||||
encoding: string;
|
||||
language: string | null;
|
||||
is_binary: boolean;
|
||||
last_commit?: {
|
||||
hash: string;
|
||||
message: string;
|
||||
author: string;
|
||||
date: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Specification
|
||||
|
||||
### URL Structure
|
||||
```
|
||||
/projects/:projectId → Default view (first repo, default branch)
|
||||
/projects/:projectId?repo=:repoId → Specific repo
|
||||
/projects/:projectId?repo=:repoId&branch=:branch&path=:path → Specific file
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
**RepoWorkspace**
|
||||
- Layout: Sidebar (250px) + Main Content (flex)
|
||||
- State: manages repo, branch, path, file selections
|
||||
- Effects: sync URL params, fetch data
|
||||
|
||||
**FileTree**
|
||||
- Props: entries, activePath, onFileClick, onDirectoryToggle
|
||||
- Recursive rendering for nested directories
|
||||
- Expand/collapse state per directory
|
||||
|
||||
**FileViewer**
|
||||
- Props: content, language, path, isEditing, onEdit
|
||||
- View mode: preformatted text with syntax highlighting
|
||||
- Edit mode: textarea with save/cancel
|
||||
|
||||
**RepoSelector**
|
||||
- Props: repositories, selectedRepoId, onSelect
|
||||
- Dropdown with repo names
|
||||
|
||||
**BranchSelector**
|
||||
- Props: branches, selectedBranch, onSelect
|
||||
- Dropdown with branch names, default branch marked
|
||||
|
||||
### State Management
|
||||
Use React state with URL synchronization:
|
||||
```typescript
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const repoId = searchParams.get('repo');
|
||||
const branch = searchParams.get('branch');
|
||||
const path = searchParams.get('path');
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Error Code | Description | User Message |
|
||||
|------------|-------------|--------------|
|
||||
| REPO_NOT_FOUND | Repository doesn't exist | "Repository not found" |
|
||||
| BRANCH_NOT_FOUND | Branch doesn't exist | "Branch not found, using default" |
|
||||
| FILE_NOT_FOUND | File path doesn't exist | "File not found" |
|
||||
| BINARY_FILE | File is binary | "Cannot display binary file" |
|
||||
| PERMISSION_DENIED | No access to file | "Permission denied" |
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Backend Tests
|
||||
- Test file listing for various paths
|
||||
- Test file content retrieval
|
||||
- Test branch listing
|
||||
- Test file update/commit
|
||||
- Test error cases (missing files, invalid branches)
|
||||
|
||||
### Frontend Tests
|
||||
- Test file tree rendering
|
||||
- Test file viewer display
|
||||
- Test branch switching
|
||||
- Test repo switching
|
||||
- Test edit mode
|
||||
- Test URL state sync
|
||||
|
||||
### Integration Tests
|
||||
- End-to-end: Click project → browse files → view content → switch branch
|
||||
Reference in New Issue
Block a user