diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0e6cbdf..4209062 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -18,7 +18,8 @@ "Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b4ujhgmdk.output)", "Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/bt54rikhh.output)", "Bash(grep -qE \"\\(Running \\\\`target|error\\\\[|panicked|terminated\\)\" /tmp/claude-1000/-mnt-data-Development-GitLite/71a8b49f-ac61-4ad9-aaba-dc0219044038/tasks/b7nyub68s.output)", - "Bash(npm install *)" + "Bash(npm install *)", + "Bash(cargo check *)" ] } } diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 84b13cb..e8dca9c 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -252,16 +252,38 @@ pub fn commit(path: String, message: String) -> Result { } #[tauri::command] -pub fn pull(path: String) -> Result { +pub fn pull( + path: String, + username: Option, + password: Option, +) -> Result { let repo = resolve_repo(&path)?; - run_git(&repo, ["pull", "--ff-only"])?; + match (username.as_deref(), password.as_deref()) { + (Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => { + run_git_authenticated(&repo, ["pull", "--ff-only"], u, p)?; + } + _ => { + run_git(&repo, ["pull", "--ff-only"])?; + } + } status_for_repo(&repo) } #[tauri::command] -pub fn push(path: String) -> Result { +pub fn push( + path: String, + username: Option, + password: Option, +) -> Result { let repo = resolve_repo(&path)?; - run_git(&repo, ["push"])?; + match (username.as_deref(), password.as_deref()) { + (Some(u), Some(p)) if !u.is_empty() || !p.is_empty() => { + run_git_authenticated(&repo, ["push"], u, p)?; + } + _ => { + run_git(&repo, ["push"])?; + } + } status_for_repo(&repo) } @@ -1163,6 +1185,70 @@ fn validate_files(files: &[String]) -> Result<(), String> { Ok(()) } +#[cfg(unix)] +fn write_askpass_script() -> Result { + use std::os::unix::fs::PermissionsExt; + let path = std::env::temp_dir().join("gitlite_askpass.sh"); + let script = "#!/bin/sh\ncase \"$1\" in\n *[Uu]sername*) printf '%s\\n' \"$GIT_CRED_USER\" ;;\n *) printf '%s\\n' \"$GIT_CRED_PASS\" ;;\nesac\n"; + std::fs::write(&path, script) + .map_err(|e| format!("Konnte Authentifizierungsskript nicht schreiben: {e}"))?; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)) + .map_err(|e| format!("Konnte Skriptrechte nicht setzen: {e}"))?; + Ok(path) +} + +#[cfg(not(unix))] +fn write_askpass_script() -> Result { + let path = std::env::temp_dir().join("gitlite_askpass.bat"); + let script = "@echo off\necho %1 | findstr /I \"sername\" >nul 2>&1\nif %errorlevel% == 0 (echo %GIT_CRED_USER%) else (echo %GIT_CRED_PASS%)\n"; + std::fs::write(&path, script) + .map_err(|e| format!("Konnte Authentifizierungsskript nicht schreiben: {e}"))?; + Ok(path) +} + +fn run_git_authenticated( + repo: &Path, + args: I, + username: &str, + password: &str, +) -> Result, String> +where + I: IntoIterator, + S: AsRef, +{ + let askpass = write_askpass_script()?; + + let result = Command::new("git") + .arg("-C") + .arg(repo) + .args(args) + .env("GIT_ASKPASS", &askpass) + .env("GIT_TERMINAL_PROMPT", "0") + .env("GIT_CRED_USER", username) + .env("GIT_CRED_PASS", password) + .output() + .map_err(|err| format!("Git konnte nicht gestartet werden: {err}")); + + let _ = std::fs::remove_file(&askpass); + + let output = result?; + + if output.status.success() { + return Ok(output.stdout); + } + + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + let details = if !stderr.trim().is_empty() { + stderr.trim().to_string() + } else if !stdout.trim().is_empty() { + stdout.trim().to_string() + } else { + "Unbekannter Fehler".to_string() + }; + Err(format!("Git-Befehl fehlgeschlagen: {details}")) +} + fn run_git_with_paths( repo: &Path, base_args: &[&str], diff --git a/src/App.svelte b/src/App.svelte index 1b7ab7a..95dc9a1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -7,6 +7,7 @@ import CommitPanel from "./lib/components/CommitPanel.svelte"; import CompareDialog from "./lib/components/CompareDialog.svelte"; import ComparePanel from "./lib/components/ComparePanel.svelte"; + import CredentialDialog from "./lib/components/CredentialDialog.svelte"; import ExplorerPanel from "./lib/components/ExplorerPanel.svelte"; import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte"; import HistoryPanel from "./lib/components/HistoryPanel.svelte"; @@ -79,6 +80,10 @@ let preparedResolutions: Record = {}; let autoRefreshEnabled = true; let autoRefreshInFlight = false; + let credDialogOpen = false; + let credDialogAction: "push" | "pull" | null = null; + let credDialogError = ""; + let sessionCredentials: { username: string; password: string } | null = null; let lastStatusFingerprint = ""; const AUTO_REFRESH_INTERVAL = 4000; let autoRefreshTimer: ReturnType | undefined; @@ -265,26 +270,60 @@ }); } - async function pullRepo() { + function openCredentialDialog(action: "push" | "pull") { if (!activeRepoPath) return; + credDialogError = ""; + credDialogAction = action; + credDialogOpen = true; + } + + async function doActualPull(username: string, password: string) { + errorMessage = ""; await runOperation("Pulling", async () => { - applyStatus(await pull(activeRepoPath)); + applyStatus(await pull(activeRepoPath, username, password)); await refreshBranchList(activeRepoPath); await refreshCommitHistory(activeRepoPath); await refreshExplorerFiles(activeRepoPath); await refreshFileHistory(activeRepoPath); }); + if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; } + else { credDialogOpen = false; credDialogAction = null; } + } + + async function doActualPush(username: string, password: string) { + errorMessage = ""; + await runOperation("Pushing", async () => { + applyStatus(await push(activeRepoPath, username, password)); + await refreshBranchList(activeRepoPath); + await refreshCommitHistory(activeRepoPath); + await refreshFileHistory(activeRepoPath); + }); + if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; } + else { credDialogOpen = false; credDialogAction = null; } + } + + async function handleCredentialSubmit(username: string, password: string, save: boolean) { + if (credDialogAction === "pull") await doActualPull(username, password); + else if (credDialogAction === "push") await doActualPush(username, password); + if (!credDialogOpen && save) sessionCredentials = { username, password }; + } + + async function pullRepo() { + if (!activeRepoPath) return; + if (sessionCredentials) { + await doActualPull(sessionCredentials.username, sessionCredentials.password); + } else { + openCredentialDialog("pull"); + } } async function pushRepo() { if (!activeRepoPath) return; - await runOperation("Pushing", async () => { - applyStatus(await push(activeRepoPath)); - await refreshBranchList(activeRepoPath); - await refreshCommitHistory(activeRepoPath); - await refreshExplorerFiles(activeRepoPath); - await refreshFileHistory(activeRepoPath); - }); + if (sessionCredentials) { + await doActualPush(sessionCredentials.username, sessionCredentials.password); + } else { + openCredentialDialog("push"); + } } // ── File staging / restore ───────────────────────────────────────────────── @@ -697,6 +736,17 @@ /> {/if} + +{#if credDialogOpen && credDialogAction} + { credDialogOpen = false; credDialogAction = null; credDialogError = ""; }} + /> +{/if} + {#if resolveDialogOpen} + import { + AlertCircle, + Download, + Eye, + EyeOff, + Key, + LoaderCircle, + Lock, + Upload, + User, + X, + } from "@lucide/svelte"; + + interface Props { + action: "push" | "pull"; + error: string; + isBusy: boolean; + onSubmit: (username: string, password: string, save: boolean) => void; + onCancel: () => void; + } + + let { + action, + error = "", + isBusy = false, + onSubmit, + onCancel, + }: Props = $props(); + + type Mode = "credentials" | "token"; + + let mode = $state("credentials"); + let username = $state(""); + let password = $state(""); + let showPassword = $state(false); + let saveSession = $state(false); + + let canSubmit = $derived( + !isBusy && + password.trim().length > 0 && + (mode === "token" || username.trim().length > 0), + ); + + function handleSubmit(e: SubmitEvent) { + e.preventDefault(); + if (!canSubmit) return; + onSubmit(mode === "token" ? "oauth2" : username, password, saveSession); + } + + + diff --git a/src/lib/git.ts b/src/lib/git.ts index ca469a4..d628f3c 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -45,12 +45,12 @@ export function commit(path: string, message: string): Promise { return invoke("commit", { path, message }); } -export function pull(path: string): Promise { - return invoke("pull", { path }); +export function pull(path: string, username?: string, password?: string): Promise { + return invoke("pull", { path, username: username ?? null, password: password ?? null }); } -export function push(path: string): Promise { - return invoke("push", { path }); +export function push(path: string, username?: string, password?: string): Promise { + return invoke("push", { path, username: username ?? null, password: password ?? null }); } export function listCommits(path: string, limit = 100): Promise {