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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user