feat(git): enhance error reporting and auth robustness

The commit enhances Git operation reliability by improving how authentication errors are detected and displayed to the user. It standardizes Git output language across different locales and introduces a mechanism to summarize complex raw Git stderr messages, ensuring users receive clear feedback when cloning or interacting with repositories that fail due to credentials.

- Standardize git command output using LC_ALL=C for consistent English messaging.
- Implement error summarization logic to extract the most relevant message from raw Git stderr.
- Update credential dialogs to display summarized and improved authentication failure details.
This commit is contained in:
2026-07-19 16:41:21 +02:00
parent 35310bec6d
commit 2bd60f0e5b
4 changed files with 33 additions and 4 deletions
+3 -1
View File
@@ -116,7 +116,9 @@
"Bash(cp /mnt/data/Development/GitLite/PKGBUILD .)", "Bash(cp /mnt/data/Development/GitLite/PKGBUILD .)",
"Bash(sed -i 's/^pkgrel=1/pkgrel=3/' 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(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)"
] ]
} }
} }
+7
View File
@@ -300,6 +300,10 @@ static CANCELLABLE_GIT_OUTPUT_COUNTER: AtomicU64 = AtomicU64::new(0);
fn git_command() -> Command { fn git_command() -> Command {
let mut command = Command::new("git"); 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)] #[cfg(windows)]
command.creation_flags(CREATE_NO_WINDOW); command.creation_flags(CREATE_NO_WINDOW);
command command
@@ -4484,6 +4488,9 @@ fn is_auth_error(details: &str) -> bool {
|| d.contains(" 401") || d.contains(" 401")
|| d.contains("authorization failed") || d.contains("authorization failed")
|| d.contains("authentication required") || 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 // Windows' CreateProcess rejects command lines longer than ~32K chars with
+9 -3
View File
@@ -144,6 +144,7 @@
isCredentialExpired, isCredentialExpired,
isAuthError, isAuthError,
stripAuthPrefix, stripAuthPrefix,
summarizeGitError,
} from "./lib/credentials"; } from "./lib/credentials";
import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics"; import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics";
@@ -1997,7 +1998,10 @@
setCloneDialogError(""); setCloneDialogError("");
if (fromStore) { if (fromStore) {
if (credentialKey) void credDelete(credentialKey).catch(() => {}); 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 { } else {
credDialogError = message || "Sign-in is required to clone this repository."; credDialogError = message || "Sign-in is required to clone this repository.";
} }
@@ -2596,8 +2600,10 @@
if (fromStore) { if (fromStore) {
if (auth) { if (auth) {
if (key) void credDelete(key).catch(() => {}); if (key) void credDelete(key).catch(() => {});
credDialogError = const detail = summarizeGitError(message);
"Credentials were rejected or have expired. Please sign in again."; credDialogError = detail
? `${detail} — please sign in again.`
: "Credentials were rejected or have expired. Please sign in again.";
credDialogAction = action; credDialogAction = action;
credDialogKey = key; credDialogKey = key;
credDialogOpen = true; credDialogOpen = true;
+14
View File
@@ -55,3 +55,17 @@ export function stripAuthPrefix(message: string): string {
? message.slice(AUTH_PREFIX.length).trim() ? message.slice(AUTH_PREFIX.length).trim()
: message; : 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] ?? "";
}