feat: simplify git mounts to use direct URLs instead of repo references

- Change git mount schema from repo_id to remote_url
- Remove database lookups for git mount resolution
- Clone directly from URL at instance startup
- Simplify frontend UI to text input for Git URL
- Fix route ordering in git_repositories.py to prevent 422 errors
- Update all tests to use remote_url field

Breaking change: Git mounts now use remote_url instead of repo_id
This commit is contained in:
Alex Blank
2026-05-27 11:53:25 +02:00
parent baabd1fa62
commit 89ca9f10c7
10 changed files with 176 additions and 321 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ export interface ConfigProfileMount {
}
export interface GitMount {
repo_id: string;
remote_url: string;
source_path: string;
target_path: string;
branch?: string;
+31 -112
View File
@@ -1,28 +1,25 @@
import { useState } from "react";
import { Icon } from "./icon";
import type { GitMount } from "../api/config_profiles";
import type { GitRepository } from "../api/git_repositories";
interface GitMountEditorProps {
mounts: GitMount[];
repositories: GitRepository[];
onChange: (mounts: GitMount[]) => void;
onCreateRepository?: (name: string, remoteUrl: string) => Promise<GitRepository>;
}
export const GitMountEditor = ({ mounts, repositories, onChange, onCreateRepository }: GitMountEditorProps) => {
export const GitMountEditor = ({ mounts, onChange }: GitMountEditorProps) => {
const [editingIndex, setEditingIndex] = useState<number | null>(null);
const [newMount, setNewMount] = useState<GitMount>({
repo_id: "",
remote_url: "",
source_path: ".",
target_path: "",
branch: "",
});
const handleAdd = () => {
if (!newMount.repo_id || !newMount.target_path) return;
if (!newMount.remote_url || !newMount.target_path) return;
onChange([...mounts, { ...newMount }]);
setNewMount({ repo_id: "", source_path: ".", target_path: "", branch: "" });
setNewMount({ remote_url: "", source_path: ".", target_path: "", branch: "" });
};
const handleUpdate = (index: number, updated: GitMount) => {
@@ -44,6 +41,14 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit
return null;
};
const validateUrl = (url: string): string | null => {
if (!url) return "Git URL is required";
if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("git@") && !url.startsWith("ssh://")) {
return "Must be a valid git URL (https://, git@, or ssh://)";
}
return null;
};
return (
<div className="git-mount-editor">
<h4 className="section-subtitle">Git Mounts</h4>
@@ -55,18 +60,15 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit
{editingIndex === index ? (
<GitMountForm
mount={mount}
repositories={repositories}
onSave={(updated) => handleUpdate(index, updated)}
onCancel={() => setEditingIndex(null)}
validatePath={validatePath}
onCreateRepository={onCreateRepository}
validateUrl={validateUrl}
/>
) : (
<div className="git-mount-display">
<div className="git-mount-info">
<span className="git-mount-repo">
{repositories.find((r) => r.id === mount.repo_id)?.name || mount.repo_id}
</span>
<span className="git-mount-repo">{mount.remote_url}</span>
<span className="git-mount-paths">
{mount.source_path || "."} {mount.target_path}
</span>
@@ -103,11 +105,10 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit
<h5>Add Git Mount</h5>
<GitMountForm
mount={newMount}
repositories={repositories}
onSave={handleAdd}
onCancel={() => setNewMount({ repo_id: "", source_path: ".", target_path: "", branch: "" })}
onCancel={() => setNewMount({ remote_url: "", source_path: ".", target_path: "", branch: "" })}
validatePath={validatePath}
onCreateRepository={onCreateRepository}
validateUrl={validateUrl}
isNew
/>
</div>
@@ -117,21 +118,16 @@ export const GitMountEditor = ({ mounts, repositories, onChange, onCreateReposit
interface GitMountFormProps {
mount: GitMount;
repositories: GitRepository[];
onSave: (mount: GitMount) => void;
onCancel: () => void;
validatePath: (path: string, isTarget: boolean) => string | null;
onCreateRepository?: (name: string, remoteUrl: string) => Promise<GitRepository>;
validateUrl: (url: string) => string | null;
isNew?: boolean;
}
const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onCreateRepository, isNew }: GitMountFormProps) => {
const GitMountForm = ({ mount, onSave, onCancel, validatePath, validateUrl, isNew }: GitMountFormProps) => {
const [form, setForm] = useState<GitMount>({ ...mount });
const [errors, setErrors] = useState<Record<string, string>>({});
const [isCreatingRepo, setIsCreatingRepo] = useState(false);
const [newRepoName, setNewRepoName] = useState("");
const [newRepoUrl, setNewRepoUrl] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (field: keyof GitMount, value: string) => {
setForm((prev) => ({ ...prev, [field]: value }));
@@ -144,32 +140,11 @@ const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onC
}
};
const handleCreateRepo = async () => {
if (!onCreateRepository || !newRepoName.trim() || !newRepoUrl.trim()) return;
setIsSubmitting(true);
try {
const repo = await onCreateRepository(newRepoName.trim(), newRepoUrl.trim());
handleChange("repo_id", repo.id);
setIsCreatingRepo(false);
setNewRepoName("");
setNewRepoUrl("");
} catch (err) {
setErrors((prev) => ({
...prev,
repo_id: err instanceof Error ? err.message : "Failed to create repository",
}));
} finally {
setIsSubmitting(false);
}
};
const handleSubmit = () => {
const newErrors: Record<string, string> = {};
if (!form.repo_id) {
newErrors.repo_id = "Repository is required";
}
const urlError = validateUrl(form.remote_url);
if (urlError) newErrors.remote_url = urlError;
const sourceError = validatePath(form.source_path || ".", false);
if (sourceError) newErrors.source_path = sourceError;
@@ -184,79 +159,23 @@ const GitMountForm = ({ mount, repositories, onSave, onCancel, validatePath, onC
onSave(form);
if (isNew) {
setForm({ repo_id: "", source_path: ".", target_path: "", branch: "" });
setForm({ remote_url: "", source_path: ".", target_path: "", branch: "" });
}
};
return (
<div className="git-mount-form">
<div className="form-row">
<label>Repository</label>
{!isCreatingRepo ? (
<>
<select
value={form.repo_id}
onChange={(e) => {
if (e.target.value === "__new__") {
setIsCreatingRepo(true);
} else {
handleChange("repo_id", e.target.value);
}
}}
className={errors.repo_id ? "error" : ""}
>
<option value="">Select a repository...</option>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
</option>
))}
{onCreateRepository && (
<option value="__new__">+ Add new repository...</option>
)}
</select>
{errors.repo_id && <span className="error-text">{errors.repo_id}</span>}
</>
) : (
<div className="new-repo-form">
<input
type="text"
value={newRepoName}
onChange={(e) => setNewRepoName(e.target.value)}
placeholder="Repository name"
disabled={isSubmitting}
/>
<input
type="text"
value={newRepoUrl}
onChange={(e) => setNewRepoUrl(e.target.value)}
placeholder="https://github.com/user/repo.git"
disabled={isSubmitting}
/>
<div className="new-repo-actions">
<button
type="button"
className="primary-button small"
onClick={handleCreateRepo}
disabled={isSubmitting || !newRepoName.trim() || !newRepoUrl.trim()}
>
{isSubmitting ? "Creating..." : "Create Repository"}
</button>
<button
type="button"
className="secondary-button small"
onClick={() => {
setIsCreatingRepo(false);
setNewRepoName("");
setNewRepoUrl("");
}}
disabled={isSubmitting}
>
Cancel
</button>
</div>
</div>
)}
<label>Git URL</label>
<input
type="text"
value={form.remote_url}
onChange={(e) => handleChange("remote_url", e.target.value)}
placeholder="https://github.com/user/repo.git"
className={errors.remote_url ? "error" : ""}
/>
<span className="hint">Repository URL (HTTPS or SSH)</span>
{errors.remote_url && <span className="error-text">{errors.remote_url}</span>}
</div>
<div className="form-row">
-19
View File
@@ -19,7 +19,6 @@ import {
type ResolvedProfile,
} from "../api/config_profiles";
import { listProjects } from "../api/projects";
import { listAllUserRepositories, createExternalRepository, type GitRepository } from "../api/git_repositories";
import type { Project } from "../types";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { GitMountEditor } from "../components/git-mount-editor";
@@ -34,7 +33,6 @@ export const ConfigProfilesPage = () => {
const [profiles, setProfiles] = useState<ConfigProfile[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
@@ -72,14 +70,6 @@ export const ConfigProfilesPage = () => {
setProjects(projs || []);
setToolTypes(types || []);
// Load all user repositories (including external ones)
try {
const allRepos = await listAllUserRepositories();
setRepositories(allRepos);
} catch {
setRepositories([]);
}
setStatus("ready");
} catch {
setStatus("error");
@@ -1257,16 +1247,7 @@ export const ConfigProfilesPage = () => {
<div className="form-section">
<GitMountEditor
mounts={formData.git_mounts || []}
repositories={repositories}
onChange={(git_mounts) => updateFormField("git_mounts", git_mounts)}
onCreateRepository={async (name, remoteUrl) => {
const repo = await createExternalRepository({
name,
remote_url: remoteUrl,
});
setRepositories((prev) => [...prev, repo]);
return repo;
}}
/>
</div>