diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f756607..366f6ae 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -116,7 +116,9 @@ "Bash(cp /mnt/data/Development/GitLite/PKGBUILD .)", "Bash(sed -i 's/^pkgrel=1/pkgrel=3/' PKGBUILD)", "Bash(node -e \"const fs=require\\('fs'\\); const version='2026.8.1'; const pkgbuild=fs.readFileSync\\('PKGBUILD','utf8'\\).replace\\(/^pkgver=.*\\\\$/m, 'pkgver='+version\\).replace\\(/^pkgrel=.*\\\\$/m, 'pkgrel=1'\\); fs.writeFileSync\\('PKGBUILD', pkgbuild\\);\")", - "Bash(rm PKGBUILD)" + "Bash(rm PKGBUILD)", + "Bash(git -C /mnt/data/Development/GitLite stash)", + "Bash(git -C /mnt/data/Development/GitLite stash pop)" ] } } diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index c0d13fd..259b768 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -300,6 +300,10 @@ static CANCELLABLE_GIT_OUTPUT_COUNTER: AtomicU64 = AtomicU64::new(0); fn git_command() -> Command { let mut command = Command::new("git"); + // Force English output regardless of the system locale, so is_auth_error() + // and other message heuristics keep working (e.g. German git prints + // "Authentifizierung fehlgeschlagen" instead of "Authentication failed"). + command.env("LC_ALL", "C"); #[cfg(windows)] command.creation_flags(CREATE_NO_WINDOW); command @@ -4484,6 +4488,9 @@ fn is_auth_error(details: &str) -> bool { || d.contains(" 401") || d.contains("authorization failed") || d.contains("authentication required") + // Server-side message (e.g. Gitea/Forgejo: "remote: Failed to + // authenticate user"), independent of the local git locale. + || d.contains("failed to authenticate") } // Windows' CreateProcess rejects command lines longer than ~32K chars with diff --git a/src/App.svelte b/src/App.svelte index bcd419a..71fc2d8 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -144,6 +144,7 @@ isCredentialExpired, isAuthError, stripAuthPrefix, + summarizeGitError, } from "./lib/credentials"; import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics"; @@ -1997,7 +1998,10 @@ setCloneDialogError(""); if (fromStore) { if (credentialKey) void credDelete(credentialKey).catch(() => {}); - credDialogError = "Credentials were rejected or have expired. Please sign in again."; + const detail = summarizeGitError(message); + credDialogError = detail + ? `${detail} — please sign in again.` + : "Credentials were rejected or have expired. Please sign in again."; } else { credDialogError = message || "Sign-in is required to clone this repository."; } @@ -2596,8 +2600,10 @@ if (fromStore) { if (auth) { if (key) void credDelete(key).catch(() => {}); - credDialogError = - "Credentials were rejected or have expired. Please sign in again."; + const detail = summarizeGitError(message); + credDialogError = detail + ? `${detail} — please sign in again.` + : "Credentials were rejected or have expired. Please sign in again."; credDialogAction = action; credDialogKey = key; credDialogOpen = true; diff --git a/src/lib/credentials.ts b/src/lib/credentials.ts index 558b867..8eab5e2 100644 --- a/src/lib/credentials.ts +++ b/src/lib/credentials.ts @@ -55,3 +55,17 @@ export function stripAuthPrefix(message: string): string { ? message.slice(AUTH_PREFIX.length).trim() : message; } + +/** + * Condenses raw git stderr to the single most relevant line for the + * credential dialog. Prefers the server's own explanation ("remote: ...") + * over git's generic wrapper ("fatal: Authentication failed for ..."). + */ +export function summarizeGitError(message: string): string { + const lines = message + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + const remote = lines.find((line) => line.toLowerCase().startsWith("remote:")); + return remote ?? lines[0] ?? ""; +}