Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
@@ -61,6 +62,7 @@ class CreateInstanceRequest(BaseModel):
|
||||
display_name: str | None = Field(default=None, description="Optional display name for the instance")
|
||||
clone_mode: str = Field(default="mount", description="Repository access mode: 'mount' or 'clone'")
|
||||
branch: str | None = Field(default="main", description="Branch to clone (when clone_mode='clone')")
|
||||
new_branch: str | None = Field(default=None, description="Create a new local branch after cloning")
|
||||
|
||||
|
||||
def _modify_compose_file(
|
||||
@@ -256,6 +258,25 @@ async def create_instance(
|
||||
else:
|
||||
repo_path = repo.path
|
||||
|
||||
# Create new local branch if requested
|
||||
if data.clone_mode == "clone" and data.new_branch:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", repo_path, "checkout", "-b", data.new_branch],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error("Failed to create branch %s: %s", data.new_branch, result.stderr)
|
||||
raise RuntimeError(f"Failed to create branch: {result.stderr}")
|
||||
logger.info("Created local branch %s in cloned repository", data.new_branch)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to create local branch: %s", exc)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to create local branch: {exc}"
|
||||
)
|
||||
|
||||
# Handle based on definition type
|
||||
if tool_type.definition_type == "dockerfile":
|
||||
# Build image from Dockerfile
|
||||
@@ -318,7 +339,7 @@ services:
|
||||
compose_path=compose_path,
|
||||
port=tool_port,
|
||||
clone_mode=data.clone_mode,
|
||||
branch=data.branch if data.clone_mode == "clone" else None,
|
||||
branch=data.new_branch if data.new_branch else (data.branch if data.clone_mode == "clone" else None),
|
||||
)
|
||||
session.add(instance)
|
||||
await session.commit()
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Tests for session creation with branch selection and new branch creation."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.tool_instances import CreateInstanceRequest
|
||||
|
||||
|
||||
class TestCreateInstanceRequest:
|
||||
"""Tests for CreateInstanceRequest model."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test default values for CreateInstanceRequest."""
|
||||
request = CreateInstanceRequest(tool_type_id="123")
|
||||
assert request.clone_mode == "mount"
|
||||
assert request.branch == "main"
|
||||
assert request.new_branch is None
|
||||
assert request.display_name is None
|
||||
|
||||
def test_clone_mode_with_branch(self):
|
||||
"""Test CreateInstanceRequest with clone mode and branch."""
|
||||
request = CreateInstanceRequest(
|
||||
tool_type_id="123",
|
||||
clone_mode="clone",
|
||||
branch="dev",
|
||||
)
|
||||
assert request.clone_mode == "clone"
|
||||
assert request.branch == "dev"
|
||||
|
||||
def test_new_branch_field(self):
|
||||
"""Test CreateInstanceRequest with new_branch field."""
|
||||
request = CreateInstanceRequest(
|
||||
tool_type_id="123",
|
||||
clone_mode="clone",
|
||||
branch="main",
|
||||
new_branch="feature/test",
|
||||
)
|
||||
assert request.new_branch == "feature/test"
|
||||
|
||||
|
||||
class TestBranchCreationInClone:
|
||||
"""Tests for branch creation logic in clone process."""
|
||||
|
||||
def test_create_local_branch_success(self):
|
||||
"""Test successful local branch creation."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Initialize repo
|
||||
subprocess.run(
|
||||
["git", "init", tmpdir],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "config", "user.email", "test@test.com"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "config", "user.name", "Test User"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create initial commit
|
||||
readme = os.path.join(tmpdir, "README.md")
|
||||
with open(readme, "w") as f:
|
||||
f.write("# Test\n")
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "add", "README.md"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "commit", "-m", "Initial commit"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create new branch
|
||||
result = subprocess.run(
|
||||
["git", "-C", tmpdir, "checkout", "-b", "feature/new-branch"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode == 0
|
||||
|
||||
# Verify branch exists
|
||||
branches_result = subprocess.run(
|
||||
["git", "-C", tmpdir, "branch", "--show-current"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert branches_result.stdout.strip() == "feature/new-branch"
|
||||
|
||||
def test_create_local_branch_invalid_name(self):
|
||||
"""Test local branch creation with invalid name fails."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
# Initialize repo
|
||||
subprocess.run(
|
||||
["git", "init", tmpdir],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "config", "user.email", "test@test.com"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "config", "user.name", "Test User"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create initial commit
|
||||
readme = os.path.join(tmpdir, "README.md")
|
||||
with open(readme, "w") as f:
|
||||
f.write("# Test\n")
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "add", "README.md"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", tmpdir, "commit", "-m", "Initial commit"],
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Try to create branch with invalid name (contains spaces)
|
||||
result = subprocess.run(
|
||||
["git", "-C", tmpdir, "checkout", "-b", "invalid branch name"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# Git accepts branch names with spaces but it's not recommended
|
||||
# This test verifies the command structure
|
||||
assert result.returncode == 0 or "fatal" in result.stderr
|
||||
|
||||
|
||||
class TestCreateInstanceAPI:
|
||||
"""Tests for create instance API endpoint with branch options."""
|
||||
|
||||
def test_create_instance_request_validation(self):
|
||||
"""Test that CreateInstanceRequest validates correctly."""
|
||||
# Valid request with new_branch
|
||||
request = CreateInstanceRequest(
|
||||
tool_type_id="550e8400-e29b-41d4-a716-446655440000",
|
||||
clone_mode="clone",
|
||||
branch="main",
|
||||
new_branch="feature/test",
|
||||
)
|
||||
assert request.new_branch == "feature/test"
|
||||
|
||||
# Valid request without new_branch
|
||||
request2 = CreateInstanceRequest(
|
||||
tool_type_id="550e8400-e29b-41d4-a716-446655440000",
|
||||
clone_mode="clone",
|
||||
branch="dev",
|
||||
)
|
||||
assert request2.new_branch is None
|
||||
|
||||
def test_create_instance_with_new_branch_sets_instance_branch(self):
|
||||
"""Test that instance branch is set to new_branch when provided."""
|
||||
# This tests the logic: data.new_branch if data.new_branch else data.branch
|
||||
new_branch = "feature/test"
|
||||
base_branch = "main"
|
||||
|
||||
# Simulate the logic from create_instance
|
||||
stored_branch = new_branch if new_branch else base_branch
|
||||
assert stored_branch == "feature/test"
|
||||
|
||||
# Without new_branch
|
||||
stored_branch2 = None if None else base_branch
|
||||
assert stored_branch2 == "main"
|
||||
@@ -64,6 +64,27 @@ export async function updateRepositorySshKey(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface Branch {
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
last_commit: string | null;
|
||||
}
|
||||
|
||||
export interface BranchesResponse {
|
||||
branches: Branch[];
|
||||
default_branch: string;
|
||||
}
|
||||
|
||||
export async function listRepositoryBranches(
|
||||
projectId: string,
|
||||
repoId: string
|
||||
): Promise<BranchesResponse> {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/branches`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export interface CommitHistoryEntry {
|
||||
hash: string;
|
||||
short_hash: string;
|
||||
|
||||
@@ -47,7 +47,8 @@ export async function createInstance(
|
||||
toolTypeId: string,
|
||||
displayName?: string,
|
||||
cloneMode?: string,
|
||||
branch?: string
|
||||
branch?: string,
|
||||
newBranch?: string
|
||||
): Promise<ToolInstance> {
|
||||
const response = await apiClient.post(
|
||||
`/projects/${projectId}/repositories/${repoId}/instances`,
|
||||
@@ -56,6 +57,7 @@ export async function createInstance(
|
||||
display_name: displayName,
|
||||
clone_mode: cloneMode || "mount",
|
||||
branch: branch || undefined,
|
||||
new_branch: newBranch || undefined,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { Icon } from "./icon";
|
||||
import { createInstance, startInstance, type ToolInstance } from "../api/sessions";
|
||||
import type { Project } from "../types";
|
||||
import type { GitRepository } from "../api/git_repositories";
|
||||
import { listRepositoryBranches, type GitRepository, type Branch } from "../api/git_repositories";
|
||||
import type { ToolType } from "../api/tool_types";
|
||||
import { listSSHKeys, type SSHKey } from "../api/ssh_keys";
|
||||
|
||||
@@ -47,6 +47,12 @@ export const CreateSessionForm = ({
|
||||
const [branch, setBranch] = useState("main");
|
||||
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
|
||||
|
||||
const [branches, setBranches] = useState<Branch[]>([]);
|
||||
const [isLoadingBranches, setIsLoadingBranches] = useState(false);
|
||||
const [isCreatingNewBranch, setIsCreatingNewBranch] = useState(false);
|
||||
const [newBranchName, setNewBranchName] = useState("");
|
||||
const [baseBranch, setBaseBranch] = useState("");
|
||||
|
||||
const [status, setStatus] = useState<"idle" | "creating" | "error">("idle");
|
||||
const [progress, setProgress] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -65,6 +71,31 @@ export const CreateSessionForm = ({
|
||||
void loadKeys();
|
||||
}, [showCloneMode]);
|
||||
|
||||
// Load branches when selected repo changes
|
||||
useEffect(() => {
|
||||
if (!selectedRepo || !showCloneMode) {
|
||||
setBranches([]);
|
||||
return;
|
||||
}
|
||||
const loadBranches = async () => {
|
||||
setIsLoadingBranches(true);
|
||||
try {
|
||||
const branchList = await listRepositoryBranches(selectedRepo);
|
||||
setBranches(branchList);
|
||||
const defaultBranch = branchList.find((b) => b.is_default);
|
||||
if (defaultBranch) {
|
||||
setBranch(defaultBranch.name);
|
||||
setBaseBranch(defaultBranch.name);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setIsLoadingBranches(false);
|
||||
}
|
||||
};
|
||||
void loadBranches();
|
||||
}, [selectedRepo, showCloneMode]);
|
||||
|
||||
// Filter repositories by selected project
|
||||
const availableRepos = selectedProject
|
||||
? repositories.filter((r) => r.project_id === selectedProject)
|
||||
@@ -100,7 +131,14 @@ export const CreateSessionForm = ({
|
||||
selectedToolType,
|
||||
displayName || undefined,
|
||||
showCloneMode ? cloneMode : undefined,
|
||||
showCloneMode && cloneMode === "clone" ? branch : undefined
|
||||
showCloneMode && cloneMode === "clone"
|
||||
? isCreatingNewBranch
|
||||
? baseBranch
|
||||
: branch
|
||||
: undefined,
|
||||
showCloneMode && cloneMode === "clone" && isCreatingNewBranch
|
||||
? newBranchName
|
||||
: undefined
|
||||
);
|
||||
|
||||
setProgress("Starting container...");
|
||||
@@ -113,6 +151,10 @@ export const CreateSessionForm = ({
|
||||
setDisplayName("");
|
||||
setCloneMode("mount");
|
||||
setBranch("main");
|
||||
setIsCreatingNewBranch(false);
|
||||
setNewBranchName("");
|
||||
setBaseBranch("");
|
||||
setBranches([]);
|
||||
setStatus("idle");
|
||||
|
||||
onSuccess?.(instance);
|
||||
@@ -247,16 +289,89 @@ export const CreateSessionForm = ({
|
||||
</label>
|
||||
|
||||
{cloneMode === "clone" && (
|
||||
<label className="form-field">
|
||||
Branch
|
||||
<input
|
||||
type="text"
|
||||
value={branch}
|
||||
onChange={(e) => setBranch(e.target.value)}
|
||||
placeholder="main"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
<>
|
||||
<label className="form-field">
|
||||
Branch
|
||||
{isLoadingBranches ? (
|
||||
<span className="muted">Loading branches...</span>
|
||||
) : (
|
||||
<select
|
||||
value={isCreatingNewBranch ? "__new__" : branch}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "__new__") {
|
||||
setIsCreatingNewBranch(true);
|
||||
setNewBranchName("");
|
||||
} else {
|
||||
setIsCreatingNewBranch(false);
|
||||
setBranch(value);
|
||||
setBaseBranch(value);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{branches.map((b) => (
|
||||
<option key={b.name} value={b.name}>
|
||||
{b.name} {b.is_default ? "(default)" : ""}
|
||||
</option>
|
||||
))}
|
||||
<option value="__new__">Create new branch...</option>
|
||||
</select>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{isCreatingNewBranch && (
|
||||
<>
|
||||
<label className="form-field">
|
||||
New Branch Name
|
||||
<input
|
||||
type="text"
|
||||
value={newBranchName}
|
||||
onChange={(e) => setNewBranchName(e.target.value)}
|
||||
placeholder="feature/my-new-branch"
|
||||
required
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
Base Branch
|
||||
<select
|
||||
value={baseBranch}
|
||||
onChange={(e) => setBaseBranch(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{branches.map((b) => (
|
||||
<option key={b.name} value={b.name}>
|
||||
{b.name} {b.is_default ? "(default)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedRepo && (
|
||||
<div className="form-field ssh-key-info">
|
||||
{(() => {
|
||||
const repo = repositories.find((r) => r.id === selectedRepo);
|
||||
if (!repo) return null;
|
||||
if (repo.ssh_key_id) {
|
||||
const key = sshKeys.find((k) => k.id === repo.ssh_key_id);
|
||||
return (
|
||||
<span className="success-text">
|
||||
SSH key: {key?.name || "Assigned"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="warning-text">
|
||||
No SSH key assigned to this repository. Clone mode requires an SSH key.
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
type Session,
|
||||
deleteInstance,
|
||||
stopInstance,
|
||||
startInstance,
|
||||
checkInstanceHealth,
|
||||
recreateInstanceTunnel,
|
||||
} from "../api/sessions";
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Session Branch Selection with New Branch Creation
|
||||
|
||||
## Summary
|
||||
|
||||
Replace the free-text branch input in the session creation form with a dropdown of available branches from the repository. Add the ability to create a new local branch at clone time by selecting "Create new branch..." from the dropdown.
|
||||
|
||||
## Context
|
||||
|
||||
The current session creation UI (`apps/web/src/pages/sessions.tsx`) has a free-text input for the branch name when "Clone fresh copy" mode is selected. Users must manually type the branch name, which is error-prone and doesn't show what branches are available.
|
||||
|
||||
The backend already has:
|
||||
- A `GET /projects/{project_id}/repositories/{repo_id}/branches` endpoint that returns all branches and the default branch
|
||||
- A `clone_repository` service that clones a specific branch
|
||||
- Branch creation APIs for the original repository
|
||||
|
||||
## Design
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
#### 1. Branch API Integration
|
||||
|
||||
Add a new API function in `apps/web/src/api/git_repositories.ts`:
|
||||
|
||||
```typescript
|
||||
export interface Branch {
|
||||
name: string;
|
||||
is_default: boolean;
|
||||
last_commit: string | null;
|
||||
}
|
||||
|
||||
export interface BranchesResponse {
|
||||
branches: Branch[];
|
||||
default_branch: string;
|
||||
}
|
||||
|
||||
export async function listRepositoryBranches(
|
||||
projectId: string,
|
||||
repoId: string
|
||||
): Promise<BranchesResponse> {
|
||||
const response = await apiClient.get(
|
||||
`/projects/${projectId}/repositories/${repoId}/branches`
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. UI State Management
|
||||
|
||||
In `apps/web/src/pages/sessions.tsx`, add state for:
|
||||
- `branches`: `Branch[]` — loaded when a repository is selected and clone mode is active
|
||||
- `isLoadingBranches`: `boolean`
|
||||
- `isCreatingNewBranch`: `boolean` — toggled when "Create new branch..." is selected
|
||||
- `newBranchName`: `string` — the name for the new branch
|
||||
- `baseBranch`: `string` — the base branch for the new branch
|
||||
|
||||
#### 3. Branch Loading
|
||||
|
||||
When a repository is selected and clone mode is "clone", fetch branches:
|
||||
- Call `listRepositoryBranches(selectedProject, selectedRepo)`
|
||||
- Set `baseBranch` to `default_branch` from the response
|
||||
- If the current `branch` state is not in the list, reset it to `default_branch`
|
||||
|
||||
#### 4. Branch Dropdown
|
||||
|
||||
Replace the free-text input with a `<select>`:
|
||||
- Options populated from `branches` state
|
||||
- Default branch marked visually: `"main (default)"`
|
||||
- Last option: `"Create new branch..."` (disabled separator style or as a real option)
|
||||
- When selected, set `isCreatingNewBranch = true`
|
||||
|
||||
#### 5. New Branch Form
|
||||
|
||||
When `isCreatingNewBranch` is true, show:
|
||||
- **New branch name** input (required, validated for valid git branch name)
|
||||
- **Base branch** dropdown (populated from `branches`, defaulting to `default_branch`)
|
||||
|
||||
#### 6. Form Submission
|
||||
|
||||
Update `handleCreate` to handle new branch creation:
|
||||
- If `isCreatingNewBranch` is true, pass `newBranchName` and `baseBranch` to the API
|
||||
- The `branch` parameter sent to the API should be:
|
||||
- `newBranchName` if creating a new branch
|
||||
- The selected existing branch otherwise
|
||||
|
||||
### Backend Changes
|
||||
|
||||
#### 1. Update `CreateInstanceRequest`
|
||||
|
||||
In `apps/api/src/api/tool_instances.py`, extend the request model:
|
||||
|
||||
```python
|
||||
class CreateInstanceRequest(BaseModel):
|
||||
model_config = {"extra": "ignore"}
|
||||
|
||||
tool_type_id: str = Field(description="UUID of the tool type to instantiate")
|
||||
display_name: str | None = Field(default=None, description="Optional display name")
|
||||
clone_mode: str = Field(default="mount", description="'mount' or 'clone'")
|
||||
branch: str | None = Field(default="main", description="Branch to clone")
|
||||
new_branch: str | None = Field(default=None, description="Create a new branch from 'branch' after clone")
|
||||
```
|
||||
|
||||
#### 2. Update Clone Logic
|
||||
|
||||
In `create_instance`, after cloning:
|
||||
- If `data.new_branch` is provided:
|
||||
1. Clone the `data.branch` (base branch) as usual
|
||||
2. Run `git -C <clone_path> checkout -b <new_branch>` to create the local branch
|
||||
3. Store `new_branch` in the `branch` field of the ToolInstance record
|
||||
|
||||
#### 3. Update `clone_repository` Service
|
||||
|
||||
No changes needed — it already clones a specific branch. The new branch creation happens after clone.
|
||||
|
||||
#### 4. Database Schema
|
||||
|
||||
No changes needed — the existing `branch` field on `ToolInstance` can store the new branch name.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User selects repo + "Clone fresh copy"
|
||||
→ Frontend fetches branches from GET /branches
|
||||
→ User selects "Create new branch..."
|
||||
→ User fills: newBranchName="feature-x", baseBranch="dev"
|
||||
→ Frontend sends: { branch: "dev", new_branch: "feature-x", ... }
|
||||
→ Backend clones "dev" branch
|
||||
→ Backend runs: git checkout -b feature-x
|
||||
→ Instance record stores branch="feature-x"
|
||||
→ Container starts with the new branch checked out
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- **Branch fetch fails**: Show error, fallback to free-text input
|
||||
- **Invalid branch name**: Frontend validation (regex for valid git branch names)
|
||||
- **New branch creation fails**: Backend returns 400 with git error message
|
||||
- **Branch already exists locally**: Backend handles gracefully (git checkout -b will fail if branch exists)
|
||||
|
||||
### Testing
|
||||
|
||||
1. **Frontend**: Test branch dropdown loads correctly, "Create new branch" toggle works, form submission sends correct payload
|
||||
2. **Backend**: Test instance creation with `new_branch` parameter, verify git command runs correctly
|
||||
3. **Integration**: End-to-end test creating a session with a new branch
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `apps/web/src/api/git_repositories.ts` — Add `listRepositoryBranches` function
|
||||
- `apps/web/src/pages/sessions.tsx` — Replace branch input with dropdown + new branch form
|
||||
- `apps/api/src/api/tool_instances.py` — Extend `CreateInstanceRequest` and clone logic
|
||||
- `apps/api/src/services/clone.py` — Add `create_local_branch` helper (optional)
|
||||
|
||||
## Trade-offs
|
||||
|
||||
- **Local branch only**: The new branch is created in the cloned workspace only, not pushed to the remote. This is intentional — it's a disposable work branch.
|
||||
- **No branch deletion**: When the instance is deleted, the branch is lost with the clone. This matches the "disposable" mental model.
|
||||
Reference in New Issue
Block a user