feat(ui): localize commit AI and git error messages to English
Translate commit AI prompts, model labels, and git/keychain errors to English so the app and generated messages are consistent. Also add a discard confirmation dialog and update the UI text/styles to match the new flow. - src-tauri/crates/commit_ai/src/cloud.rs - Translate HTTP and API error messages to English. - Keep request timeout and token sizing behavior unchanged. - src-tauri/crates/commit_ai/src/lib.rs - Translate model labels, prompt text, and validation errors. - Keep diff truncation and message sanitization logic intact. - src-tauri/src/git.rs - Translate git, credential, merge, and history errors. - Update AI provider validation messages to English. - src/lib/components/* - Update AI settings, commit panel, credential, and loading UI text. - Add discard confirmation dialog for destructive actions. - src/App.svelte, src/app.css - Adjust app layout and styling for the new dialog and text changes.
This commit is contained in:
@@ -58,7 +58,20 @@
|
|||||||
"Bash(: *)",
|
"Bash(: *)",
|
||||||
"Bash(exit 0 *)",
|
"Bash(exit 0 *)",
|
||||||
"Bash(rustc -O sanitize_test.rs -o sanitize_test)",
|
"Bash(rustc -O sanitize_test.rs -o sanitize_test)",
|
||||||
"Bash(./sanitize_test)"
|
"Bash(./sanitize_test)",
|
||||||
|
"Bash(echo \"exit:$?\")",
|
||||||
|
"Bash(grep -rlP \"[äöüßÄÖÜ]\" src src-tauri/src src-tauri/crates --include=\"*.rs\" --include=\"*.svelte\" --include=\"*.ts\")",
|
||||||
|
"Bash(echo \"---exit $?---\")",
|
||||||
|
"Bash(apt-cache policy *)",
|
||||||
|
"Bash(timeout 5 curl -sI http://archive.ubuntu.com)",
|
||||||
|
"Bash(sudo -n apt-get install -y libdbus-1-dev pkg-config)",
|
||||||
|
"Bash(grep -n '\"Unerwarteter Git-Log-Eintrag: {}\",' src-tauri/src/git.rs)",
|
||||||
|
"Bash(grep -E \"git_lite$|tauri_git_lite$\")",
|
||||||
|
"Bash(rustfmt --edition 2024 --check src-tauri/src/git.rs)",
|
||||||
|
"Bash(echo \"EXIT:$?\")",
|
||||||
|
"Bash(ls target/)",
|
||||||
|
"Bash(rustup target *)",
|
||||||
|
"Bash(echo \"exit code: $?\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{build_messages, sanitize_message};
|
use crate::{build_messages, sanitize_message};
|
||||||
|
|
||||||
// Großzügig bemessen, damit ein ausführlicher Body mit Stichpunkten nicht abgeschnitten wird.
|
// Generous sizing so a detailed body with bullet points isn't cut off.
|
||||||
const DEFAULT_MAX_TOKENS: u32 = 1500;
|
const DEFAULT_MAX_TOKENS: u32 = 1500;
|
||||||
// Ohne Timeout würde ein hängender Endpoint den "AI"-Button dauerhaft blockieren.
|
// Without a timeout, a hanging endpoint would permanently block the "AI" button.
|
||||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
fn http_client() -> Result<reqwest::Client, String> {
|
fn http_client() -> Result<reqwest::Client, String> {
|
||||||
reqwest::Client::builder()
|
reqwest::Client::builder()
|
||||||
.timeout(REQUEST_TIMEOUT)
|
.timeout(REQUEST_TIMEOUT)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|err| format!("HTTP-Client konnte nicht erstellt werden: {err}"))
|
.map_err(|err| format!("Could not create HTTP client: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@@ -71,19 +71,19 @@ async fn openai_compatible_request(
|
|||||||
let response = request
|
let response = request
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Anfrage an das KI-Modell fehlgeschlagen: {err}"))?;
|
.map_err(|err| format!("Request to the AI model failed: {err}"))?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let text = response
|
let text = response
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Antwort konnte nicht gelesen werden: {err}"))?;
|
.map_err(|err| format!("Could not read response: {err}"))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(format!("API-Fehler ({status}): {text}"));
|
return Err(format!("API error ({status}): {text}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: OpenAiResponse = serde_json::from_str(&text)
|
let parsed: OpenAiResponse = serde_json::from_str(&text)
|
||||||
.map_err(|err| format!("Antwort konnte nicht verarbeitet werden: {err}"))?;
|
.map_err(|err| format!("Could not process response: {err}"))?;
|
||||||
|
|
||||||
parsed
|
parsed
|
||||||
.choices
|
.choices
|
||||||
@@ -92,7 +92,7 @@ async fn openai_compatible_request(
|
|||||||
.and_then(|choice| choice.message.content)
|
.and_then(|choice| choice.message.content)
|
||||||
.map(|content| sanitize_message(&content))
|
.map(|content| sanitize_message(&content))
|
||||||
.filter(|content| !content.is_empty())
|
.filter(|content| !content.is_empty())
|
||||||
.ok_or_else(|| "Das Modell hat keine Antwort geliefert.".to_string())
|
.ok_or_else(|| "The model did not return a response.".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn generate_openai(
|
pub async fn generate_openai(
|
||||||
@@ -102,7 +102,7 @@ pub async fn generate_openai(
|
|||||||
notes: Option<&str>,
|
notes: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
if api_key.trim().is_empty() {
|
if api_key.trim().is_empty() {
|
||||||
return Err("OpenAI-API-Key fehlt.".to_string());
|
return Err("OpenAI API key is missing.".to_string());
|
||||||
}
|
}
|
||||||
openai_compatible_request(
|
openai_compatible_request(
|
||||||
"https://api.openai.com/v1/chat/completions".to_string(),
|
"https://api.openai.com/v1/chat/completions".to_string(),
|
||||||
@@ -122,7 +122,7 @@ pub async fn generate_custom(
|
|||||||
notes: Option<&str>,
|
notes: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
if base_url.trim().is_empty() {
|
if base_url.trim().is_empty() {
|
||||||
return Err("Endpoint-URL fehlt.".to_string());
|
return Err("Endpoint URL is missing.".to_string());
|
||||||
}
|
}
|
||||||
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
|
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
|
||||||
openai_compatible_request(url, api_key, model, diff, notes).await
|
openai_compatible_request(url, api_key, model, diff, notes).await
|
||||||
@@ -161,7 +161,7 @@ pub async fn generate_anthropic(
|
|||||||
notes: Option<&str>,
|
notes: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
if api_key.trim().is_empty() {
|
if api_key.trim().is_empty() {
|
||||||
return Err("Anthropic-API-Key fehlt.".to_string());
|
return Err("Anthropic API key is missing.".to_string());
|
||||||
}
|
}
|
||||||
let (system, user) = build_messages(diff, notes)?;
|
let (system, user) = build_messages(diff, notes)?;
|
||||||
let body = AnthropicRequest {
|
let body = AnthropicRequest {
|
||||||
@@ -179,19 +179,19 @@ pub async fn generate_anthropic(
|
|||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Anfrage an Anthropic fehlgeschlagen: {err}"))?;
|
.map_err(|err| format!("Request to Anthropic failed: {err}"))?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let text = response
|
let text = response
|
||||||
.text()
|
.text()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Antwort konnte nicht gelesen werden: {err}"))?;
|
.map_err(|err| format!("Could not read response: {err}"))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(format!("API-Fehler ({status}): {text}"));
|
return Err(format!("API error ({status}): {text}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: AnthropicResponse = serde_json::from_str(&text)
|
let parsed: AnthropicResponse = serde_json::from_str(&text)
|
||||||
.map_err(|err| format!("Antwort konnte nicht verarbeitet werden: {err}"))?;
|
.map_err(|err| format!("Could not process response: {err}"))?;
|
||||||
|
|
||||||
parsed
|
parsed
|
||||||
.content
|
.content
|
||||||
@@ -199,5 +199,5 @@ pub async fn generate_anthropic(
|
|||||||
.find_map(|block| block.text)
|
.find_map(|block| block.text)
|
||||||
.map(|text| sanitize_message(&text))
|
.map(|text| sanitize_message(&text))
|
||||||
.filter(|text| !text.is_empty())
|
.filter(|text| !text.is_empty())
|
||||||
.ok_or_else(|| "Das Modell hat keine Antwort geliefert.".to_string())
|
.ok_or_else(|| "The model did not return a response.".to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub const DEFAULT_LOCAL_MODEL_ID: &str = "qwen2.5-1.5b";
|
|||||||
pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
||||||
LocalModelOption {
|
LocalModelOption {
|
||||||
id: "qwen2.5-0.5b",
|
id: "qwen2.5-0.5b",
|
||||||
label: "Qwen2.5 0.5B Instruct — schnell, geringere Qualität",
|
label: "Qwen2.5 0.5B Instruct — fast, lower quality",
|
||||||
approx_size_mb: 490,
|
approx_size_mb: 490,
|
||||||
repo: "Qwen/Qwen2.5-0.5B-Instruct-GGUF",
|
repo: "Qwen/Qwen2.5-0.5B-Instruct-GGUF",
|
||||||
file: "qwen2.5-0.5b-instruct-q4_k_m.gguf",
|
file: "qwen2.5-0.5b-instruct-q4_k_m.gguf",
|
||||||
@@ -32,7 +32,7 @@ pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
|||||||
},
|
},
|
||||||
LocalModelOption {
|
LocalModelOption {
|
||||||
id: "qwen2.5-1.5b",
|
id: "qwen2.5-1.5b",
|
||||||
label: "Qwen2.5 1.5B Instruct — empfohlen",
|
label: "Qwen2.5 1.5B Instruct — recommended",
|
||||||
approx_size_mb: 1050,
|
approx_size_mb: 1050,
|
||||||
repo: "Qwen/Qwen2.5-1.5B-Instruct-GGUF",
|
repo: "Qwen/Qwen2.5-1.5B-Instruct-GGUF",
|
||||||
file: "qwen2.5-1.5b-instruct-q4_k_m.gguf",
|
file: "qwen2.5-1.5b-instruct-q4_k_m.gguf",
|
||||||
@@ -40,7 +40,7 @@ pub const LOCAL_MODELS: &[LocalModelOption] = &[
|
|||||||
},
|
},
|
||||||
LocalModelOption {
|
LocalModelOption {
|
||||||
id: "qwen2.5-3b",
|
id: "qwen2.5-3b",
|
||||||
label: "Qwen2.5 3B Instruct — beste Qualität, langsamer",
|
label: "Qwen2.5 3B Instruct — best quality, slower",
|
||||||
approx_size_mb: 2100,
|
approx_size_mb: 2100,
|
||||||
repo: "Qwen/Qwen2.5-3B-Instruct-GGUF",
|
repo: "Qwen/Qwen2.5-3B-Instruct-GGUF",
|
||||||
file: "qwen2.5-3b-instruct-q4_k_m.gguf",
|
file: "qwen2.5-3b-instruct-q4_k_m.gguf",
|
||||||
@@ -128,7 +128,7 @@ impl CommitAiEngine {
|
|||||||
let mut guard = self.inner.write().await;
|
let mut guard = self.inner.write().await;
|
||||||
guard.phase = CommitAiPhase::Error;
|
guard.phase = CommitAiPhase::Error;
|
||||||
guard.model_id = Some(model_id.to_string());
|
guard.model_id = Some(model_id.to_string());
|
||||||
guard.error = Some(format!("Unbekanntes lokales Modell: {model_id}"));
|
guard.error = Some(format!("Unknown local model: {model_id}"));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ impl CommitAiEngine {
|
|||||||
let guard = self.inner.read().await;
|
let guard = self.inner.read().await;
|
||||||
match (guard.phase, &guard.model) {
|
match (guard.phase, &guard.model) {
|
||||||
(CommitAiPhase::Ready, Some(model)) => model.clone(),
|
(CommitAiPhase::Ready, Some(model)) => model.clone(),
|
||||||
_ => return Err("Das lokale KI-Modell ist noch nicht bereit.".to_string()),
|
_ => return Err("The local AI model is not ready yet.".to_string()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,11 +192,11 @@ impl CommitAiEngine {
|
|||||||
.choices
|
.choices
|
||||||
.first()
|
.first()
|
||||||
.and_then(|choice| choice.message.content.clone())
|
.and_then(|choice| choice.message.content.clone())
|
||||||
.ok_or_else(|| "Das Modell hat keine Antwort geliefert.".to_string())?;
|
.ok_or_else(|| "The model did not return a response.".to_string())?;
|
||||||
|
|
||||||
let message = sanitize_message(&content);
|
let message = sanitize_message(&content);
|
||||||
if message.is_empty() {
|
if message.is_empty() {
|
||||||
return Err("Das Modell hat keine Antwort geliefert.".to_string());
|
return Err("The model did not return a response.".to_string());
|
||||||
}
|
}
|
||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
@@ -223,36 +223,36 @@ pub(crate) fn sanitize_message(raw: &str) -> String {
|
|||||||
|
|
||||||
pub(crate) fn build_messages(diff: &str, notes: Option<&str>) -> Result<(String, String), String> {
|
pub(crate) fn build_messages(diff: &str, notes: Option<&str>) -> Result<(String, String), String> {
|
||||||
if diff.trim().is_empty() {
|
if diff.trim().is_empty() {
|
||||||
return Err("Keine gestagten Änderungen für eine Commit-Message vorhanden.".to_string());
|
return Err("No staged changes available for a commit message.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// grobe Token-Schätzung, kleine Modelle haben oft 8–32k Kontext
|
// Rough token estimate — small models often have an 8-32k context window.
|
||||||
const MAX_CHARS: usize = 24_000;
|
const MAX_CHARS: usize = 24_000;
|
||||||
let diff = if diff.len() > MAX_CHARS {
|
let diff = if diff.len() > MAX_CHARS {
|
||||||
// Byte-Index auf eine gültige UTF-8-Zeichengrenze zurückziehen, sonst
|
// Pull the byte index back to a valid UTF-8 char boundary, otherwise
|
||||||
// paniken Slices mitten in einem Umlaut o. Ä.
|
// slicing mid-multi-byte-character would panic.
|
||||||
let mut cut = MAX_CHARS;
|
let mut cut = MAX_CHARS;
|
||||||
while !diff.is_char_boundary(cut) {
|
while !diff.is_char_boundary(cut) {
|
||||||
cut -= 1;
|
cut -= 1;
|
||||||
}
|
}
|
||||||
format!("{}\n\n[... Diff gekürzt ...]", &diff[..cut])
|
format!("{}\n\n[... diff truncated ...]", &diff[..cut])
|
||||||
} else {
|
} else {
|
||||||
diff.to_string()
|
diff.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
let system = "Du bist ein Werkzeug, das Git-Commit-Messages erzeugt. \
|
let system = "You are a tool that generates Git commit messages. \
|
||||||
Antworte ausschließlich mit der Commit-Message im Conventional-Commits-Format \
|
Respond only with the commit message in Conventional Commits format \
|
||||||
(<type>(<scope>): <subject>), gefolgt von einem Body nach einer Leerzeile. \
|
(<type>(<scope>): <subject>), followed by a body after a blank line. \
|
||||||
Subject imperativ, max. 72 Zeichen. \
|
Subject in imperative mood, max. 72 characters. \
|
||||||
Der Body ist Pflicht: Fasse in einem kurzen Absatz zusammen, was und warum geändert wurde, \
|
The body is required: summarize in a short paragraph what changed and why, \
|
||||||
und liste danach die wesentlichen Änderungen als Stichpunkte (- ...) auf, \
|
then list the key changes as bullet points (- ...), \
|
||||||
gruppiert nach betroffenem Bereich/Datei. Zeilen im Body max. 72 Zeichen. \
|
grouped by affected area/file. Lines in the body max. 72 characters. \
|
||||||
Kein Vorspann, keine Erklärung, keine Code-Fences, in Englisch antworten"
|
No preamble, no explanation, no code fences, answer in English"
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let mut user = String::new();
|
let mut user = String::new();
|
||||||
if let Some(n) = notes.filter(|n| !n.trim().is_empty()) {
|
if let Some(n) = notes.filter(|n| !n.trim().is_empty()) {
|
||||||
user.push_str(&format!("Anmerkungen des Entwicklers:\n{n}\n\n"));
|
user.push_str(&format!("Developer notes:\n{n}\n\n"));
|
||||||
}
|
}
|
||||||
user.push_str(&format!("Staged diff:\n{diff}"));
|
user.push_str(&format!("Staged diff:\n{diff}"));
|
||||||
Ok((system, user))
|
Ok((system, user))
|
||||||
|
|||||||
+102
-102
@@ -146,7 +146,7 @@ enum CheckoutPlan {
|
|||||||
|
|
||||||
const FULL_FILE_DIFF_CONTEXT: &str = "--unified=1000000";
|
const FULL_FILE_DIFF_CONTEXT: &str = "--unified=1000000";
|
||||||
const EMPTY_TREE_HASH: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
const EMPTY_TREE_HASH: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
||||||
const SEARCH_CANCELLED_MESSAGE: &str = "Suche wurde abgebrochen.";
|
const SEARCH_CANCELLED_MESSAGE: &str = "Search was cancelled.";
|
||||||
static CANCELLABLE_GIT_OUTPUT_COUNTER: AtomicU64 = AtomicU64::new(0);
|
static CANCELLABLE_GIT_OUTPUT_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
fn git_command() -> Command {
|
fn git_command() -> Command {
|
||||||
@@ -165,7 +165,7 @@ impl SearchCancellationState {
|
|||||||
fn cancel(&self, search_id: &str) -> Result<(), String> {
|
fn cancel(&self, search_id: &str) -> Result<(), String> {
|
||||||
self.cancelled
|
self.cancelled
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "Such-Abbruchstatus ist nicht erreichbar.".to_string())?
|
.map_err(|_| "Search cancellation status is unavailable.".to_string())?
|
||||||
.insert(search_id.to_string());
|
.insert(search_id.to_string());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ impl SearchCancellationState {
|
|||||||
fn clear(&self, search_id: &str) -> Result<(), String> {
|
fn clear(&self, search_id: &str) -> Result<(), String> {
|
||||||
self.cancelled
|
self.cancelled
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "Such-Abbruchstatus ist nicht erreichbar.".to_string())?
|
.map_err(|_| "Search cancellation status is unavailable.".to_string())?
|
||||||
.remove(search_id);
|
.remove(search_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ impl SearchCancellationState {
|
|||||||
Ok(self
|
Ok(self
|
||||||
.cancelled
|
.cancelled
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "Such-Abbruchstatus ist nicht erreichbar.".to_string())?
|
.map_err(|_| "Search cancellation status is unavailable.".to_string())?
|
||||||
.contains(search_id))
|
.contains(search_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,10 +221,10 @@ pub fn open_repository_file(path: String, file: String) -> Result<(), String> {
|
|||||||
let file_path = resolve_repo_child_path(&repo, &file)?;
|
let file_path = resolve_repo_child_path(&repo, &file)?;
|
||||||
|
|
||||||
if !file_path.exists() {
|
if !file_path.exists() {
|
||||||
return Err(format!("Datei '{file}' existiert im Working Tree nicht."));
|
return Err(format!("File '{file}' does not exist in the working tree."));
|
||||||
}
|
}
|
||||||
if !file_path.is_file() {
|
if !file_path.is_file() {
|
||||||
return Err(format!("'{file}' ist keine Datei."));
|
return Err(format!("'{file}' is not a file."));
|
||||||
}
|
}
|
||||||
|
|
||||||
reveal_path_in_file_manager(&file_path)
|
reveal_path_in_file_manager(&file_path)
|
||||||
@@ -262,7 +262,7 @@ pub async fn open_repository_bundle(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Repository konnte nicht geladen werden: {err}"))?
|
.map_err(|err| format!("Could not load repository: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -321,7 +321,7 @@ pub fn checkout_branch(path: String, branch: String) -> Result<GitStatus, String
|
|||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let branch = branch.trim().to_string();
|
let branch = branch.trim().to_string();
|
||||||
if branch.is_empty() {
|
if branch.is_empty() {
|
||||||
return Err("Branch-Name darf nicht leer sein.".to_string());
|
return Err("Branch name must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
match checkout_plan(&repo, &branch)? {
|
match checkout_plan(&repo, &branch)? {
|
||||||
@@ -392,7 +392,7 @@ pub fn delete_branch(path: String, branch: String) -> Result<GitStatus, String>
|
|||||||
let branch = validate_existing_local_branch_name(&repo, &branch)?;
|
let branch = validate_existing_local_branch_name(&repo, &branch)?;
|
||||||
let status = status_for_repo(&repo)?;
|
let status = status_for_repo(&repo)?;
|
||||||
if status.current_branch.as_deref() == Some(branch.as_str()) {
|
if status.current_branch.as_deref() == Some(branch.as_str()) {
|
||||||
return Err("Der aktuelle Branch kann nicht geloescht werden.".to_string());
|
return Err("The current branch cannot be deleted.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
run_git(&repo, ["branch", "-d", "--", branch.as_str()])?;
|
run_git(&repo, ["branch", "-d", "--", branch.as_str()])?;
|
||||||
@@ -560,21 +560,21 @@ pub async fn commit_ai_generate(
|
|||||||
match provider.as_str() {
|
match provider.as_str() {
|
||||||
"local" => engine.generate_commit_message(&diff, notes).await,
|
"local" => engine.generate_commit_message(&diff, notes).await,
|
||||||
"openai" => {
|
"openai" => {
|
||||||
let api_key = api_key.ok_or_else(|| "OpenAI-API-Key fehlt.".to_string())?;
|
let api_key = api_key.ok_or_else(|| "OpenAI API key is missing.".to_string())?;
|
||||||
let model = model.unwrap_or_else(|| "gpt-4o-mini".to_string());
|
let model = model.unwrap_or_else(|| "gpt-4o-mini".to_string());
|
||||||
commit_ai::generate_openai(&api_key, &model, &diff, notes).await
|
commit_ai::generate_openai(&api_key, &model, &diff, notes).await
|
||||||
}
|
}
|
||||||
"anthropic" => {
|
"anthropic" => {
|
||||||
let api_key = api_key.ok_or_else(|| "Anthropic-API-Key fehlt.".to_string())?;
|
let api_key = api_key.ok_or_else(|| "Anthropic API key is missing.".to_string())?;
|
||||||
let model = model.unwrap_or_else(|| "claude-3-5-haiku-latest".to_string());
|
let model = model.unwrap_or_else(|| "claude-3-5-haiku-latest".to_string());
|
||||||
commit_ai::generate_anthropic(&api_key, &model, &diff, notes).await
|
commit_ai::generate_anthropic(&api_key, &model, &diff, notes).await
|
||||||
}
|
}
|
||||||
"custom" => {
|
"custom" => {
|
||||||
let base_url = base_url.ok_or_else(|| "Endpoint-URL fehlt.".to_string())?;
|
let base_url = base_url.ok_or_else(|| "Endpoint URL is missing.".to_string())?;
|
||||||
let model = model.ok_or_else(|| "Modellname fehlt.".to_string())?;
|
let model = model.ok_or_else(|| "Model name is missing.".to_string())?;
|
||||||
commit_ai::generate_custom(&base_url, api_key.as_deref(), &model, &diff, notes).await
|
commit_ai::generate_custom(&base_url, api_key.as_deref(), &model, &diff, notes).await
|
||||||
}
|
}
|
||||||
other => Err(format!("Unbekannter KI-Provider: {other}")),
|
other => Err(format!("Unknown AI provider: {other}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,7 +588,7 @@ pub fn apply_file_patch(
|
|||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
if patch.trim().is_empty() {
|
if patch.trim().is_empty() {
|
||||||
return Err("Kein Patch ausgewaehlt.".to_string());
|
return Err("No patch selected.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let patch_path = write_temp_patch(&patch)?;
|
let patch_path = write_temp_patch(&patch)?;
|
||||||
@@ -603,7 +603,7 @@ pub fn apply_file_patch(
|
|||||||
.and_then(|_| check_apply_patch(&repo, &patch_path, &["--reverse"]))
|
.and_then(|_| check_apply_patch(&repo, &patch_path, &["--reverse"]))
|
||||||
.and_then(|_| run_apply_patch(&repo, &patch_path, &["--cached", "--reverse"]))
|
.and_then(|_| run_apply_patch(&repo, &patch_path, &["--cached", "--reverse"]))
|
||||||
.and_then(|_| run_apply_patch(&repo, &patch_path, &["--reverse"])),
|
.and_then(|_| run_apply_patch(&repo, &patch_path, &["--reverse"])),
|
||||||
_ => Err("Ungueltige Patch-Aktion.".to_string()),
|
_ => Err("Invalid patch action.".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&patch_path);
|
let _ = std::fs::remove_file(&patch_path);
|
||||||
@@ -615,13 +615,13 @@ pub fn apply_file_patch(
|
|||||||
pub fn commit(path: String, message: String) -> Result<GitStatus, String> {
|
pub fn commit(path: String, message: String) -> Result<GitStatus, String> {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
if message.trim().is_empty() {
|
if message.trim().is_empty() {
|
||||||
return Err("Commit-Message darf nicht leer sein.".to_string());
|
return Err("Commit message must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_status = status_for_repo(&repo)?;
|
let current_status = status_for_repo(&repo)?;
|
||||||
if has_unresolved_conflicts(¤t_status) {
|
if has_unresolved_conflicts(¤t_status) {
|
||||||
return Err(
|
return Err(
|
||||||
"Merge-Konflikte muessen geloest werden, bevor du committen kannst.".to_string(),
|
"Merge conflicts must be resolved before you can commit.".to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +647,7 @@ pub fn pull(
|
|||||||
.args(pull_args)
|
.args(pull_args)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}")
|
format!("Could not start Git. Is Git installed? {err}")
|
||||||
})?,
|
})?,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -664,7 +664,7 @@ pub fn pull(
|
|||||||
if is_auth_error(&details) {
|
if is_auth_error(&details) {
|
||||||
return Err(format!("AUTH_FAILED:{details}"));
|
return Err(format!("AUTH_FAILED:{details}"));
|
||||||
}
|
}
|
||||||
Err(format!("Git-Befehl fehlgeschlagen: {details}"))
|
Err(format!("Git command failed: {details}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -701,10 +701,10 @@ pub struct StoredCredential {
|
|||||||
fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
fn cred_entry(key: &str) -> Result<keyring::Entry, String> {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
return Err("Kein Schlüssel für die Zugangsdaten angegeben.".to_string());
|
return Err("No key provided for the credentials.".to_string());
|
||||||
}
|
}
|
||||||
keyring::Entry::new(CRED_SERVICE, key)
|
keyring::Entry::new(CRED_SERVICE, key)
|
||||||
.map_err(|err| format!("Schlüsselbund nicht verfügbar: {err}"))
|
.map_err(|err| format!("Keychain unavailable: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
/// Returns the remote URL used for auth key derivation (upstream remote of the
|
||||||
@@ -770,7 +770,7 @@ fn current_branch_name(repo: &Path) -> Result<String, String> {
|
|||||||
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"])?;
|
let branch = run_git(repo, ["rev-parse", "--abbrev-ref", "HEAD"])?;
|
||||||
let branch = String::from_utf8_lossy(&branch).trim().to_string();
|
let branch = String::from_utf8_lossy(&branch).trim().to_string();
|
||||||
if branch.is_empty() || branch == "HEAD" {
|
if branch.is_empty() || branch == "HEAD" {
|
||||||
return Err("Aktueller Branch konnte nicht ermittelt werden.".to_string());
|
return Err("Could not determine current branch.".to_string());
|
||||||
}
|
}
|
||||||
Ok(branch)
|
Ok(branch)
|
||||||
}
|
}
|
||||||
@@ -791,7 +791,7 @@ fn initial_push_remote_name(repo: &Path) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
first_remote_name(repo).ok_or_else(|| {
|
first_remote_name(repo).ok_or_else(|| {
|
||||||
"Dieser Branch hat keinen Upstream und es ist kein Remote konfiguriert.".to_string()
|
"This branch has no upstream and no remote is configured.".to_string()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -816,11 +816,11 @@ pub fn cred_load(key: String) -> Result<Option<StoredCredential>, String> {
|
|||||||
match entry.get_password() {
|
match entry.get_password() {
|
||||||
Ok(json) => {
|
Ok(json) => {
|
||||||
let cred = serde_json::from_str::<StoredCredential>(&json)
|
let cred = serde_json::from_str::<StoredCredential>(&json)
|
||||||
.map_err(|err| format!("Gespeicherte Zugangsdaten unlesbar: {err}"))?;
|
.map_err(|err| format!("Stored credentials unreadable: {err}"))?;
|
||||||
Ok(Some(cred))
|
Ok(Some(cred))
|
||||||
}
|
}
|
||||||
Err(keyring::Error::NoEntry) => Ok(None),
|
Err(keyring::Error::NoEntry) => Ok(None),
|
||||||
Err(err) => Err(format!("Schlüsselbund-Zugriff fehlgeschlagen: {err}")),
|
Err(err) => Err(format!("Keychain access failed: {err}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,10 +839,10 @@ pub fn cred_save(
|
|||||||
expires_at,
|
expires_at,
|
||||||
};
|
};
|
||||||
let json = serde_json::to_string(&cred)
|
let json = serde_json::to_string(&cred)
|
||||||
.map_err(|err| format!("Zugangsdaten konnten nicht serialisiert werden: {err}"))?;
|
.map_err(|err| format!("Could not serialize credentials: {err}"))?;
|
||||||
entry
|
entry
|
||||||
.set_password(&json)
|
.set_password(&json)
|
||||||
.map_err(|err| format!("Speichern im Schlüsselbund fehlgeschlagen: {err}"))
|
.map_err(|err| format!("Saving to keychain failed: {err}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -850,7 +850,7 @@ pub fn cred_delete(key: String) -> Result<(), String> {
|
|||||||
let entry = cred_entry(&key)?;
|
let entry = cred_entry(&key)?;
|
||||||
match entry.delete_credential() {
|
match entry.delete_credential() {
|
||||||
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
||||||
Err(err) => Err(format!("Löschen im Schlüsselbund fehlgeschlagen: {err}")),
|
Err(err) => Err(format!("Deleting from keychain failed: {err}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,7 +859,7 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
|||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let branch = branch.trim();
|
let branch = branch.trim();
|
||||||
if branch.is_empty() {
|
if branch.is_empty() {
|
||||||
return Err("Branch-Name darf nicht leer sein.".to_string());
|
return Err("Branch name must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = git_command()
|
let output = git_command()
|
||||||
@@ -867,7 +867,7 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
|||||||
.arg(&repo)
|
.arg(&repo)
|
||||||
.args(["merge", "--no-edit", branch])
|
.args(["merge", "--no-edit", branch])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
return status_for_repo(&repo);
|
return status_for_repo(&repo);
|
||||||
@@ -888,10 +888,10 @@ pub fn merge_branch(path: String, branch: String) -> Result<GitStatus, String> {
|
|||||||
} else if !stdout.trim().is_empty() {
|
} else if !stdout.trim().is_empty() {
|
||||||
stdout.trim()
|
stdout.trim()
|
||||||
} else {
|
} else {
|
||||||
"unbekannter Fehler"
|
"unknown error"
|
||||||
};
|
};
|
||||||
|
|
||||||
Err(format!("Merge fehlgeschlagen: {details}"))
|
Err(format!("Merge failed: {details}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -962,7 +962,7 @@ pub async fn list_file_history(
|
|||||||
result
|
result
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Dateihistorie konnte nicht geladen werden: {err}"))?
|
.map_err(|err| format!("Could not load file history: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1003,7 +1003,7 @@ fn list_file_history_core(
|
|||||||
args.push(OsString::from("--follow"));
|
args.push(OsString::from("--follow"));
|
||||||
}
|
}
|
||||||
args.extend([OsString::from("--"), OsString::from(file)]);
|
args.extend([OsString::from("--"), OsString::from(file)]);
|
||||||
let output = run_git_cancellable(repo, args, cancellation, "Git-Dateihistorie fehlgeschlagen")?;
|
let output = run_git_cancellable(repo, args, cancellation, "Git file history failed")?;
|
||||||
check_search_cancelled(cancellation)?;
|
check_search_cancelled(cancellation)?;
|
||||||
|
|
||||||
parse_commit_log(repo, &output)
|
parse_commit_log(repo, &output)
|
||||||
@@ -1025,7 +1025,7 @@ pub async fn search_code_introductions(
|
|||||||
let query = normalize_newlines(&query);
|
let query = normalize_newlines(&query);
|
||||||
let query = query.trim_matches('\n').to_string();
|
let query = query.trim_matches('\n').to_string();
|
||||||
if query.trim().is_empty() {
|
if query.trim().is_empty() {
|
||||||
return Err("Suchtext darf nicht leer sein.".to_string());
|
return Err("Search text must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if verify_commit(&repo, "HEAD").is_err() {
|
if verify_commit(&repo, "HEAD").is_err() {
|
||||||
@@ -1058,7 +1058,7 @@ pub async fn search_code_introductions(
|
|||||||
result
|
result
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| format!("Such-Task konnte nicht abgeschlossen werden: {err}"))?
|
.map_err(|err| format!("Could not complete search task: {err}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1451,7 +1451,7 @@ pub fn read_conflict(path: String, file: String) -> Result<ConflictFile, String>
|
|||||||
validate_files(std::slice::from_ref(&file))?;
|
validate_files(std::slice::from_ref(&file))?;
|
||||||
|
|
||||||
let bytes = std::fs::read(repo.join(&file))
|
let bytes = std::fs::read(repo.join(&file))
|
||||||
.map_err(|err| format!("Konfliktdatei konnte nicht gelesen werden: {err}"))?;
|
.map_err(|err| format!("Could not read conflict file: {err}"))?;
|
||||||
let binary = is_binary_bytes(&bytes);
|
let binary = is_binary_bytes(&bytes);
|
||||||
|
|
||||||
// For binary files we cannot offer a text merge, so we only report the side
|
// For binary files we cannot offer a text merge, so we only report the side
|
||||||
@@ -1493,7 +1493,7 @@ pub fn resolve_conflict_side(
|
|||||||
let flag = match side.as_str() {
|
let flag = match side.as_str() {
|
||||||
"ours" => "--ours",
|
"ours" => "--ours",
|
||||||
"theirs" => "--theirs",
|
"theirs" => "--theirs",
|
||||||
_ => return Err("Ungueltige Seite. Erlaubt sind 'ours' oder 'theirs'.".to_string()),
|
_ => return Err("Invalid side. Allowed values are 'ours' or 'theirs'.".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
run_git_with_paths(&repo, &["checkout", flag], std::slice::from_ref(&file))?;
|
run_git_with_paths(&repo, &["checkout", flag], std::slice::from_ref(&file))?;
|
||||||
@@ -1531,10 +1531,10 @@ pub fn resolve_conflict(path: String, file: String, content: String) -> Result<G
|
|||||||
let target = repo.join(&file);
|
let target = repo.join(&file);
|
||||||
if let Some(parent) = target.parent() {
|
if let Some(parent) = target.parent() {
|
||||||
std::fs::create_dir_all(parent)
|
std::fs::create_dir_all(parent)
|
||||||
.map_err(|err| format!("Verzeichnis konnte nicht erstellt werden: {err}"))?;
|
.map_err(|err| format!("Could not create directory: {err}"))?;
|
||||||
}
|
}
|
||||||
std::fs::write(&target, content)
|
std::fs::write(&target, content)
|
||||||
.map_err(|err| format!("Konfliktdatei konnte nicht geschrieben werden: {err}"))?;
|
.map_err(|err| format!("Could not write conflict file: {err}"))?;
|
||||||
|
|
||||||
run_git_with_paths(&repo, &["add"], std::slice::from_ref(&file))?;
|
run_git_with_paths(&repo, &["add"], std::slice::from_ref(&file))?;
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
@@ -1562,19 +1562,19 @@ fn short_hash(hash: &str) -> String {
|
|||||||
|
|
||||||
fn resolve_repo(path: &str) -> Result<PathBuf, String> {
|
fn resolve_repo(path: &str) -> Result<PathBuf, String> {
|
||||||
if path.trim().is_empty() {
|
if path.trim().is_empty() {
|
||||||
return Err("Repository-Pfad darf nicht leer sein.".to_string());
|
return Err("Repository path must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let input = PathBuf::from(path);
|
let input = PathBuf::from(path);
|
||||||
let output = run_git_at(
|
let output = run_git_at(
|
||||||
&input,
|
&input,
|
||||||
["rev-parse", "--show-toplevel"],
|
["rev-parse", "--show-toplevel"],
|
||||||
"Kein Git-Repository oder nicht erreichbar",
|
"Not a Git repository or unreachable",
|
||||||
)?;
|
)?;
|
||||||
let top_level = String::from_utf8_lossy(&output).trim().to_string();
|
let top_level = String::from_utf8_lossy(&output).trim().to_string();
|
||||||
|
|
||||||
if top_level.is_empty() {
|
if top_level.is_empty() {
|
||||||
return Err("Git konnte keinen Repository-Wurzelpfad ermitteln.".to_string());
|
return Err("Git could not determine the repository root path.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PathBuf::from(top_level))
|
Ok(PathBuf::from(top_level))
|
||||||
@@ -1588,7 +1588,7 @@ fn open_path_in_file_manager(path: &Path) -> Result<(), String> {
|
|||||||
command.creation_flags(CREATE_NO_WINDOW);
|
command.creation_flags(CREATE_NO_WINDOW);
|
||||||
command
|
command
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Explorer konnte nicht gestartet werden: {err}"))?;
|
.map_err(|err| format!("Could not launch Explorer: {err}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1597,7 +1597,7 @@ fn open_path_in_file_manager(path: &Path) -> Result<(), String> {
|
|||||||
Command::new("open")
|
Command::new("open")
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Finder konnte nicht gestartet werden: {err}"))?;
|
.map_err(|err| format!("Could not launch Finder: {err}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1606,7 +1606,7 @@ fn open_path_in_file_manager(path: &Path) -> Result<(), String> {
|
|||||||
Command::new("xdg-open")
|
Command::new("xdg-open")
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Dateimanager konnte nicht gestartet werden: {err}"))?;
|
.map_err(|err| format!("Could not launch file manager: {err}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1622,19 +1622,19 @@ fn resolve_repo_child_path(repo: &Path, child: &str) -> Result<PathBuf, String>
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
return Err("Dateipfad muss innerhalb des Repositorys liegen.".to_string());
|
return Err("File path must stay within the repository.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidate = repo.join(child_path);
|
let candidate = repo.join(child_path);
|
||||||
let repo = repo
|
let repo = repo
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|err| format!("Repository-Pfad konnte nicht aufgeloest werden: {err}"))?;
|
.map_err(|err| format!("Could not resolve repository path: {err}"))?;
|
||||||
let candidate = candidate
|
let candidate = candidate
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|err| format!("Dateipfad konnte nicht aufgeloest werden: {err}"))?;
|
.map_err(|err| format!("Could not resolve file path: {err}"))?;
|
||||||
|
|
||||||
if !candidate.starts_with(&repo) {
|
if !candidate.starts_with(&repo) {
|
||||||
return Err("Dateipfad liegt ausserhalb des Repositorys.".to_string());
|
return Err("File path lies outside the repository.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(candidate)
|
Ok(candidate)
|
||||||
@@ -1648,7 +1648,7 @@ fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> {
|
|||||||
command.creation_flags(CREATE_NO_WINDOW);
|
command.creation_flags(CREATE_NO_WINDOW);
|
||||||
command
|
command
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Explorer konnte nicht gestartet werden: {err}"))?;
|
.map_err(|err| format!("Could not launch Explorer: {err}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1658,7 +1658,7 @@ fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> {
|
|||||||
.arg("-R")
|
.arg("-R")
|
||||||
.arg(path)
|
.arg(path)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Finder konnte nicht gestartet werden: {err}"))?;
|
.map_err(|err| format!("Could not launch Finder: {err}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1669,23 +1669,23 @@ fn reveal_path_in_file_manager(path: &Path) -> Result<(), String> {
|
|||||||
Command::new("xdg-open")
|
Command::new("xdg-open")
|
||||||
.arg(target)
|
.arg(target)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Dateimanager konnte nicht gestartet werden: {err}"))?;
|
.map_err(|err| format!("Could not launch file manager: {err}"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_commit(repo: &Path, commit: &str) -> Result<String, String> {
|
fn verify_commit(repo: &Path, commit: &str) -> Result<String, String> {
|
||||||
let commit = commit.trim();
|
let commit = commit.trim();
|
||||||
if commit.is_empty() {
|
if commit.is_empty() {
|
||||||
return Err("Commit darf nicht leer sein.".to_string());
|
return Err("Commit must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let rev = format!("{commit}^{{commit}}");
|
let rev = format!("{commit}^{{commit}}");
|
||||||
let output = run_git(repo, ["rev-parse", "--verify", "--quiet", rev.as_str()])
|
let output = run_git(repo, ["rev-parse", "--verify", "--quiet", rev.as_str()])
|
||||||
.map_err(|err| format!("Commit konnte nicht gefunden werden: {err}"))?;
|
.map_err(|err| format!("Could not find commit: {err}"))?;
|
||||||
let hash = String::from_utf8_lossy(&output).trim().to_string();
|
let hash = String::from_utf8_lossy(&output).trim().to_string();
|
||||||
|
|
||||||
if hash.is_empty() {
|
if hash.is_empty() {
|
||||||
return Err("Commit konnte nicht gefunden werden.".to_string());
|
return Err("Commit could not be found.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
@@ -1940,7 +1940,7 @@ fn search_candidate_commits(
|
|||||||
repo,
|
repo,
|
||||||
["rev-list", "--all", "--reverse"],
|
["rev-list", "--all", "--reverse"],
|
||||||
cancellation,
|
cancellation,
|
||||||
"Git-Suche fehlgeschlagen",
|
"Git search failed",
|
||||||
)?
|
)?
|
||||||
} else {
|
} else {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
@@ -1957,7 +1957,7 @@ fn search_candidate_commits(
|
|||||||
args.push(OsString::from("-i"));
|
args.push(OsString::from("-i"));
|
||||||
}
|
}
|
||||||
args.push(OsString::from(format!("-S{query}")));
|
args.push(OsString::from(format!("-S{query}")));
|
||||||
run_git_cancellable(repo, args, cancellation, "Git-Suche fehlgeschlagen")?
|
run_git_cancellable(repo, args, cancellation, "Git search failed")?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(String::from_utf8_lossy(&output)
|
Ok(String::from_utf8_lossy(&output)
|
||||||
@@ -2001,7 +2001,7 @@ fn read_text_blob(repo: &Path, commit: &str, file: &str) -> Result<Option<String
|
|||||||
.arg(repo)
|
.arg(repo)
|
||||||
.args(["show", spec.as_str()])
|
.args(["show", spec.as_str()])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -2064,7 +2064,7 @@ fn first_added_match_line(
|
|||||||
&["diff", "--no-textconv", "--unified=0", parent, commit],
|
&["diff", "--no-textconv", "--unified=0", parent, commit],
|
||||||
&[file.to_string()],
|
&[file.to_string()],
|
||||||
cancellation,
|
cancellation,
|
||||||
"Git-Diff fuer Suchtreffer fehlgeschlagen",
|
"Git diff for search result failed",
|
||||||
)?;
|
)?;
|
||||||
let patch = String::from_utf8_lossy(&output);
|
let patch = String::from_utf8_lossy(&output);
|
||||||
let line_query = first_query_line(query);
|
let line_query = first_query_line(query);
|
||||||
@@ -2169,7 +2169,7 @@ fn commit_search_metadata(repo: &Path, commit: &str) -> Result<GitSearchCommitMe
|
|||||||
let text = String::from_utf8_lossy(&output);
|
let text = String::from_utf8_lossy(&output);
|
||||||
let fields: Vec<&str> = text.trim_end().splitn(6, '\x1f').collect();
|
let fields: Vec<&str> = text.trim_end().splitn(6, '\x1f').collect();
|
||||||
if fields.len() != 6 {
|
if fields.len() != 6 {
|
||||||
return Err(format!("Unerwarteter Git-Commit-Eintrag: {text}"));
|
return Err(format!("Unexpected Git commit entry: {text}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(GitSearchCommitMetadata {
|
Ok(GitSearchCommitMetadata {
|
||||||
@@ -2204,7 +2204,7 @@ fn parse_commit_log_inline(output: &[u8]) -> Result<Vec<GitCommit>, String> {
|
|||||||
let parts: Vec<&[u8]> = record.splitn(9, |byte| *byte == FIELD_SEPARATOR).collect();
|
let parts: Vec<&[u8]> = record.splitn(9, |byte| *byte == FIELD_SEPARATOR).collect();
|
||||||
if parts.len() < 8 {
|
if parts.len() < 8 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Unerwarteter Git-Log-Eintrag: {}",
|
"Unexpected Git log entry: {}",
|
||||||
String::from_utf8_lossy(record)
|
String::from_utf8_lossy(record)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -2261,7 +2261,7 @@ fn parse_commit_log(repo: &Path, output: &[u8]) -> Result<Vec<GitCommit>, String
|
|||||||
|
|
||||||
let fields: Vec<&str> = record.splitn(8, FIELD_SEPARATOR).collect();
|
let fields: Vec<&str> = record.splitn(8, FIELD_SEPARATOR).collect();
|
||||||
if fields.len() != 8 {
|
if fields.len() != 8 {
|
||||||
return Err(format!("Unerwarteter Git-Log-Eintrag: {record}"));
|
return Err(format!("Unexpected Git log entry: {record}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let refs = fields[5]
|
let refs = fields[5]
|
||||||
@@ -2310,7 +2310,7 @@ fn parse_commit_log_metadata(output: &[u8]) -> Result<Vec<GitCommit>, String> {
|
|||||||
|
|
||||||
let fields: Vec<&str> = record.splitn(8, FIELD_SEPARATOR).collect();
|
let fields: Vec<&str> = record.splitn(8, FIELD_SEPARATOR).collect();
|
||||||
if fields.len() != 8 {
|
if fields.len() != 8 {
|
||||||
return Err(format!("Unerwarteter Git-Log-Eintrag: {record}"));
|
return Err(format!("Unexpected Git log entry: {record}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let refs = fields[5]
|
let refs = fields[5]
|
||||||
@@ -2374,7 +2374,7 @@ fn parse_commit_files(output: &[u8]) -> Result<Vec<GitCommitFile>, String> {
|
|||||||
let status = map_name_status(&status_text);
|
let status = map_name_status(&status_text);
|
||||||
|
|
||||||
if index >= entries.len() {
|
if index >= entries.len() {
|
||||||
return Err(format!("Git-Diff-Eintrag ohne Pfad: {status_text}"));
|
return Err(format!("Git diff entry without path: {status_text}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(status, FileStatusKind::Renamed) {
|
if matches!(status, FileStatusKind::Renamed) {
|
||||||
@@ -2517,7 +2517,7 @@ fn local_branch_name_for_remote(remote_branch: &str) -> Option<&str> {
|
|||||||
fn validate_new_branch_name(repo: &Path, branch: &str) -> Result<String, String> {
|
fn validate_new_branch_name(repo: &Path, branch: &str) -> Result<String, String> {
|
||||||
let branch = branch.trim();
|
let branch = branch.trim();
|
||||||
if branch.is_empty() {
|
if branch.is_empty() {
|
||||||
return Err("Branch-Name darf nicht leer sein.".to_string());
|
return Err("Branch name must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = git_command()
|
let output = git_command()
|
||||||
@@ -2525,11 +2525,11 @@ fn validate_new_branch_name(repo: &Path, branch: &str) -> Result<String, String>
|
|||||||
.arg(repo)
|
.arg(repo)
|
||||||
.args(["check-ref-format", "--branch", branch])
|
.args(["check-ref-format", "--branch", branch])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let details = command_output_details(&output);
|
let details = command_output_details(&output);
|
||||||
return Err(format!("Ungueltiger Branch-Name: {details}"));
|
return Err(format!("Invalid branch name: {details}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalized = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let normalized = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
@@ -2540,7 +2540,7 @@ fn validate_new_branch_name(repo: &Path, branch: &str) -> Result<String, String>
|
|||||||
};
|
};
|
||||||
|
|
||||||
if ref_exists(repo, &format!("refs/heads/{normalized}"))? {
|
if ref_exists(repo, &format!("refs/heads/{normalized}"))? {
|
||||||
return Err(format!("Branch '{normalized}' existiert bereits."));
|
return Err(format!("Branch '{normalized}' already exists."));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(normalized)
|
Ok(normalized)
|
||||||
@@ -2549,13 +2549,13 @@ fn validate_new_branch_name(repo: &Path, branch: &str) -> Result<String, String>
|
|||||||
fn validate_existing_local_branch_name(repo: &Path, branch: &str) -> Result<String, String> {
|
fn validate_existing_local_branch_name(repo: &Path, branch: &str) -> Result<String, String> {
|
||||||
let branch = branch.trim();
|
let branch = branch.trim();
|
||||||
if branch.is_empty() {
|
if branch.is_empty() {
|
||||||
return Err("Branch-Name darf nicht leer sein.".to_string());
|
return Err("Branch name must not be empty.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalized = validate_branch_ref_name(branch)?;
|
let normalized = validate_branch_ref_name(branch)?;
|
||||||
if !ref_exists(repo, &format!("refs/heads/{normalized}"))? {
|
if !ref_exists(repo, &format!("refs/heads/{normalized}"))? {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Lokaler Branch '{normalized}' wurde nicht gefunden."
|
"Local branch '{normalized}' was not found."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2566,11 +2566,11 @@ fn validate_branch_ref_name(branch: &str) -> Result<String, String> {
|
|||||||
let output = git_command()
|
let output = git_command()
|
||||||
.args(["check-ref-format", "--branch", branch])
|
.args(["check-ref-format", "--branch", branch])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let details = command_output_details(&output);
|
let details = command_output_details(&output);
|
||||||
return Err(format!("Ungueltiger Branch-Name: {details}"));
|
return Err(format!("Invalid branch name: {details}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalized = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let normalized = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
@@ -2587,7 +2587,7 @@ fn ref_exists(repo: &Path, ref_name: &str) -> Result<bool, String> {
|
|||||||
.arg(repo)
|
.arg(repo)
|
||||||
.args(["show-ref", "--verify", "--quiet", ref_name])
|
.args(["show-ref", "--verify", "--quiet", ref_name])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
match output.status.code() {
|
match output.status.code() {
|
||||||
Some(0) => Ok(true),
|
Some(0) => Ok(true),
|
||||||
@@ -2595,11 +2595,11 @@ fn ref_exists(repo: &Path, ref_name: &str) -> Result<bool, String> {
|
|||||||
_ => {
|
_ => {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
let details = if stderr.trim().is_empty() {
|
let details = if stderr.trim().is_empty() {
|
||||||
"unbekannter Fehler".to_string()
|
"unknown error".to_string()
|
||||||
} else {
|
} else {
|
||||||
stderr.trim().to_string()
|
stderr.trim().to_string()
|
||||||
};
|
};
|
||||||
Err(format!("Git-Ref konnte nicht geprueft werden: {details}"))
|
Err(format!("Could not verify Git ref: {details}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2755,7 +2755,7 @@ fn find_status<'a>(statuses: &'a [GitFileStatus], file: &str) -> Option<&'a GitF
|
|||||||
|
|
||||||
fn validate_files(files: &[String]) -> Result<(), String> {
|
fn validate_files(files: &[String]) -> Result<(), String> {
|
||||||
if files.iter().any(|file| file.is_empty()) {
|
if files.iter().any(|file| file.is_empty()) {
|
||||||
return Err("Dateiliste enthaelt einen leeren Pfad.".to_string());
|
return Err("File list contains an empty path.".to_string());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2768,7 +2768,7 @@ fn write_temp_patch(patch: &str) -> Result<PathBuf, String> {
|
|||||||
counter
|
counter
|
||||||
));
|
));
|
||||||
std::fs::write(&path, patch.as_bytes())
|
std::fs::write(&path, patch.as_bytes())
|
||||||
.map_err(|err| format!("Patch-Datei konnte nicht geschrieben werden: {err}"))?;
|
.map_err(|err| format!("Could not write patch file: {err}"))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2805,9 +2805,9 @@ fn write_askpass_script() -> Result<std::path::PathBuf, String> {
|
|||||||
let path = std::env::temp_dir().join("gitlite_askpass.sh");
|
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";
|
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)
|
std::fs::write(&path, script)
|
||||||
.map_err(|e| format!("Konnte Authentifizierungsskript nicht schreiben: {e}"))?;
|
.map_err(|e| format!("Could not write authentication script: {e}"))?;
|
||||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))
|
||||||
.map_err(|e| format!("Konnte Skriptrechte nicht setzen: {e}"))?;
|
.map_err(|e| format!("Could not set script permissions: {e}"))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2816,7 +2816,7 @@ fn write_askpass_script() -> Result<std::path::PathBuf, String> {
|
|||||||
let path = std::env::temp_dir().join("gitlite_askpass.bat");
|
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";
|
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)
|
std::fs::write(&path, script)
|
||||||
.map_err(|e| format!("Konnte Authentifizierungsskript nicht schreiben: {e}"))?;
|
.map_err(|e| format!("Could not write authentication script: {e}"))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2840,7 +2840,7 @@ where
|
|||||||
if is_auth_error(&details) {
|
if is_auth_error(&details) {
|
||||||
return Err(format!("AUTH_FAILED:{details}"));
|
return Err(format!("AUTH_FAILED:{details}"));
|
||||||
}
|
}
|
||||||
Err(format!("Git-Befehl fehlgeschlagen: {details}"))
|
Err(format!("Git command failed: {details}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_git_authenticated_output<I, S>(
|
fn run_git_authenticated_output<I, S>(
|
||||||
@@ -2864,7 +2864,7 @@ where
|
|||||||
.env("GIT_CRED_USER", username)
|
.env("GIT_CRED_USER", username)
|
||||||
.env("GIT_CRED_PASS", password)
|
.env("GIT_CRED_PASS", password)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden: {err}"));
|
.map_err(|err| format!("Could not start Git: {err}"));
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&askpass);
|
let _ = std::fs::remove_file(&askpass);
|
||||||
result
|
result
|
||||||
@@ -2878,7 +2878,7 @@ fn command_output_details(output: &Output) -> String {
|
|||||||
} else if !stdout.trim().is_empty() {
|
} else if !stdout.trim().is_empty() {
|
||||||
stdout.trim().to_string()
|
stdout.trim().to_string()
|
||||||
} else {
|
} else {
|
||||||
"Unbekannter Fehler".to_string()
|
"Unknown error".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2932,7 +2932,7 @@ where
|
|||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
S: AsRef<OsStr>,
|
S: AsRef<OsStr>,
|
||||||
{
|
{
|
||||||
run_git_at(repo, args, "Git-Befehl fehlgeschlagen")
|
run_git_at(repo, args, "Git command failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_git_cancellable<I, S>(
|
fn run_git_cancellable<I, S>(
|
||||||
@@ -2960,9 +2960,9 @@ where
|
|||||||
counter
|
counter
|
||||||
));
|
));
|
||||||
let stdout_file = std::fs::File::create(&stdout_path)
|
let stdout_file = std::fs::File::create(&stdout_path)
|
||||||
.map_err(|err| format!("Git-Ausgabedatei konnte nicht erstellt werden: {err}"))?;
|
.map_err(|err| format!("Could not create Git output file: {err}"))?;
|
||||||
let stderr_file = std::fs::File::create(&stderr_path)
|
let stderr_file = std::fs::File::create(&stderr_path)
|
||||||
.map_err(|err| format!("Git-Fehlerdatei konnte nicht erstellt werden: {err}"))?;
|
.map_err(|err| format!("Could not create Git error file: {err}"))?;
|
||||||
|
|
||||||
let mut child = git_command()
|
let mut child = git_command()
|
||||||
.arg("-C")
|
.arg("-C")
|
||||||
@@ -2974,7 +2974,7 @@ where
|
|||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
let _ = std::fs::remove_file(&stdout_path);
|
let _ = std::fs::remove_file(&stdout_path);
|
||||||
let _ = std::fs::remove_file(&stderr_path);
|
let _ = std::fs::remove_file(&stderr_path);
|
||||||
format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}")
|
format!("Could not start Git. Is Git installed? {err}")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let status = loop {
|
let status = loop {
|
||||||
@@ -2988,7 +2988,7 @@ where
|
|||||||
|
|
||||||
if let Some(status) = child
|
if let Some(status) = child
|
||||||
.try_wait()
|
.try_wait()
|
||||||
.map_err(|err| format!("Git-Prozess konnte nicht geprueft werden: {err}"))?
|
.map_err(|err| format!("Could not check Git process: {err}"))?
|
||||||
{
|
{
|
||||||
break status;
|
break status;
|
||||||
}
|
}
|
||||||
@@ -2997,9 +2997,9 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let stdout = std::fs::read(&stdout_path)
|
let stdout = std::fs::read(&stdout_path)
|
||||||
.map_err(|err| format!("Git-Ausgabe konnte nicht gelesen werden: {err}"))?;
|
.map_err(|err| format!("Could not read Git output: {err}"))?;
|
||||||
let stderr = std::fs::read(&stderr_path)
|
let stderr = std::fs::read(&stderr_path)
|
||||||
.map_err(|err| format!("Git-Fehlerausgabe konnte nicht gelesen werden: {err}"))?;
|
.map_err(|err| format!("Could not read Git error output: {err}"))?;
|
||||||
let _ = std::fs::remove_file(&stdout_path);
|
let _ = std::fs::remove_file(&stdout_path);
|
||||||
let _ = std::fs::remove_file(&stderr_path);
|
let _ = std::fs::remove_file(&stderr_path);
|
||||||
|
|
||||||
@@ -3014,7 +3014,7 @@ where
|
|||||||
} else if !stdout_text.trim().is_empty() {
|
} else if !stdout_text.trim().is_empty() {
|
||||||
stdout_text.trim()
|
stdout_text.trim()
|
||||||
} else {
|
} else {
|
||||||
"unbekannter Fehler"
|
"unknown error"
|
||||||
};
|
};
|
||||||
|
|
||||||
Err(format!("{context}: {details}"))
|
Err(format!("{context}: {details}"))
|
||||||
@@ -3030,7 +3030,7 @@ where
|
|||||||
.arg(path)
|
.arg(path)
|
||||||
.args(args)
|
.args(args)
|
||||||
.output()
|
.output()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
return Ok(output.stdout);
|
return Ok(output.stdout);
|
||||||
@@ -3043,7 +3043,7 @@ where
|
|||||||
} else if !stdout.trim().is_empty() {
|
} else if !stdout.trim().is_empty() {
|
||||||
stdout.trim()
|
stdout.trim()
|
||||||
} else {
|
} else {
|
||||||
"unbekannter Fehler"
|
"unknown error"
|
||||||
};
|
};
|
||||||
|
|
||||||
Err(format!("{context}: {details}"))
|
Err(format!("{context}: {details}"))
|
||||||
@@ -3064,17 +3064,17 @@ where
|
|||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped())
|
.stderr(Stdio::piped())
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|err| format!("Git konnte nicht gestartet werden. Ist Git installiert? {err}"))?;
|
.map_err(|err| format!("Could not start Git. Is Git installed? {err}"))?;
|
||||||
|
|
||||||
if let Some(mut stdin) = child.stdin.take() {
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
stdin
|
stdin
|
||||||
.write_all(stdin_data)
|
.write_all(stdin_data)
|
||||||
.map_err(|err| format!("Eingabe konnte nicht an Git gesendet werden: {err}"))?;
|
.map_err(|err| format!("Could not send input to Git: {err}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = child
|
let output = child
|
||||||
.wait_with_output()
|
.wait_with_output()
|
||||||
.map_err(|err| format!("Git-Ausgabe konnte nicht gelesen werden: {err}"))?;
|
.map_err(|err| format!("Could not read Git output: {err}"))?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
return Ok(output.stdout);
|
return Ok(output.stdout);
|
||||||
@@ -3087,10 +3087,10 @@ where
|
|||||||
} else if !stdout.trim().is_empty() {
|
} else if !stdout.trim().is_empty() {
|
||||||
stdout.trim()
|
stdout.trim()
|
||||||
} else {
|
} else {
|
||||||
"unbekannter Fehler"
|
"unknown error"
|
||||||
};
|
};
|
||||||
|
|
||||||
Err(format!("Git-Befehl fehlgeschlagen: {details}"))
|
Err(format!("Git command failed: {details}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_status_output(output: &[u8]) -> Result<(BranchInfo, Vec<GitFileStatus>), String> {
|
fn parse_status_output(output: &[u8]) -> Result<(BranchInfo, Vec<GitFileStatus>), String> {
|
||||||
@@ -3978,7 +3978,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
|
|
||||||
assert!(err.contains("Merge-Konflikte"));
|
assert!(err.contains("Merge conflicts"));
|
||||||
let status = status_for_repo(&repo.path).unwrap();
|
let status = status_for_repo(&repo.path).unwrap();
|
||||||
assert!(has_unresolved_conflicts(&status));
|
assert!(has_unresolved_conflicts(&status));
|
||||||
}
|
}
|
||||||
@@ -4134,7 +4134,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(err.contains("existiert bereits"));
|
assert!(err.contains("already exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+153
-16
@@ -11,6 +11,7 @@
|
|||||||
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
import CompareDialog from "./lib/components/CompareDialog.svelte";
|
||||||
import CompareSelectDialog from "./lib/components/CompareSelectDialog.svelte";
|
import CompareSelectDialog from "./lib/components/CompareSelectDialog.svelte";
|
||||||
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
import CredentialDialog from "./lib/components/CredentialDialog.svelte";
|
||||||
|
import DiscardConfirmDialog from "./lib/components/DiscardConfirmDialog.svelte";
|
||||||
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
|
||||||
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
|
||||||
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
|
||||||
@@ -96,6 +97,9 @@
|
|||||||
|
|
||||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||||
type AppView = "management" | "repository";
|
type AppView = "management" | "repository";
|
||||||
|
type PendingDiscard =
|
||||||
|
| { kind: "file"; file: GitFileStatus; staged: boolean }
|
||||||
|
| { kind: "hunk"; file: GitFileStatus; staged: boolean; action: PatchApplyAction; patch: string };
|
||||||
|
|
||||||
interface RepoTab {
|
interface RepoTab {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -110,6 +114,10 @@
|
|||||||
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
|
||||||
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||||
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
const AI_SETTINGS_KEY = "gitlite.aiSettings.v1";
|
||||||
|
const COMMIT_PANEL_HEIGHT_KEY = "gitlite.commitPanelHeight.v1";
|
||||||
|
const COMMIT_PANEL_DEFAULT_HEIGHT = 220;
|
||||||
|
const COMMIT_PANEL_MIN_HEIGHT = COMMIT_PANEL_DEFAULT_HEIGHT;
|
||||||
|
const COMMIT_PANEL_MAX_HEIGHT = 640;
|
||||||
|
|
||||||
// ── State ──────────────────────────────────────────────────────────────────
|
// ── State ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -157,6 +165,7 @@
|
|||||||
let linePatchText = "";
|
let linePatchText = "";
|
||||||
let linePatchLoading = false;
|
let linePatchLoading = false;
|
||||||
let linePatchError = "";
|
let linePatchError = "";
|
||||||
|
let pendingDiscard: PendingDiscard | null = null;
|
||||||
let globalSearchOpen = false;
|
let globalSearchOpen = false;
|
||||||
let lastSearchQuery = "";
|
let lastSearchQuery = "";
|
||||||
let globalSearchResults: GitSearchHit[] = [];
|
let globalSearchResults: GitSearchHit[] = [];
|
||||||
@@ -186,6 +195,10 @@
|
|||||||
let updateCheckInFlight = false;
|
let updateCheckInFlight = false;
|
||||||
let updateDownloadTotal = 0;
|
let updateDownloadTotal = 0;
|
||||||
let updateDownloadedBytes = 0;
|
let updateDownloadedBytes = 0;
|
||||||
|
let commitPanelHeight = loadCommitPanelHeight();
|
||||||
|
let resizingCommitPanel = false;
|
||||||
|
let resizeStartY = 0;
|
||||||
|
let resizeStartHeight = 0;
|
||||||
|
|
||||||
// ── Derived ────────────────────────────────────────────────────────────────
|
// ── Derived ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -542,6 +555,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampCommitPanelHeight(value: number): number {
|
||||||
|
return Math.min(COMMIT_PANEL_MAX_HEIGHT, Math.max(COMMIT_PANEL_MIN_HEIGHT, Math.round(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCommitPanelHeight(): number {
|
||||||
|
try {
|
||||||
|
const stored = Number(localStorage.getItem(COMMIT_PANEL_HEIGHT_KEY));
|
||||||
|
if (Number.isFinite(stored) && stored > 0) return clampCommitPanelHeight(stored);
|
||||||
|
} catch {
|
||||||
|
// Fall through to the default below.
|
||||||
|
}
|
||||||
|
return COMMIT_PANEL_DEFAULT_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistCommitPanelHeight(value: number) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(COMMIT_PANEL_HEIGHT_KEY, String(value));
|
||||||
|
} catch {
|
||||||
|
// Local storage is best-effort only; resizing must keep working without it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCommitPanelResize(event: PointerEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
resizingCommitPanel = true;
|
||||||
|
resizeStartY = event.clientY;
|
||||||
|
resizeStartHeight = commitPanelHeight;
|
||||||
|
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCommitPanelResizeMove(event: PointerEvent) {
|
||||||
|
if (!resizingCommitPanel) return;
|
||||||
|
commitPanelHeight = clampCommitPanelHeight(resizeStartHeight + (resizeStartY - event.clientY));
|
||||||
|
}
|
||||||
|
|
||||||
|
function endCommitPanelResize(event: PointerEvent) {
|
||||||
|
if (!resizingCommitPanel) return;
|
||||||
|
resizingCommitPanel = false;
|
||||||
|
persistCommitPanelHeight(commitPanelHeight);
|
||||||
|
const target = event.currentTarget as HTMLElement;
|
||||||
|
if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCommitPanelResizeKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||||
|
event.preventDefault();
|
||||||
|
commitPanelHeight = clampCommitPanelHeight(commitPanelHeight + (event.key === "ArrowUp" ? 20 : -20));
|
||||||
|
persistCommitPanelHeight(commitPanelHeight);
|
||||||
|
}
|
||||||
|
|
||||||
function rememberRecentRepo(path: string) {
|
function rememberRecentRepo(path: string) {
|
||||||
recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40);
|
recentRepoPaths = uniqueRepoPaths([path, ...recentRepoPaths]).slice(0, 40);
|
||||||
persistRepoLists();
|
persistRepoLists();
|
||||||
@@ -692,7 +755,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isCancellationMessage(message: string): boolean {
|
function isCancellationMessage(message: string): boolean {
|
||||||
return message.toLowerCase().includes("abgebrochen");
|
return message.toLowerCase().includes("cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelActiveFileHistoryLoad() {
|
function cancelActiveFileHistoryLoad() {
|
||||||
@@ -766,7 +829,7 @@
|
|||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
try {
|
try {
|
||||||
const selected = await openDialog({
|
const selected = await openDialog({
|
||||||
title: "Repository folder auswaehlen",
|
title: "Select repository folder",
|
||||||
directory: true,
|
directory: true,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
defaultPath: repoPath.trim() || activeRepoPath || undefined,
|
defaultPath: repoPath.trim() || activeRepoPath || undefined,
|
||||||
@@ -975,7 +1038,7 @@
|
|||||||
if (auth) {
|
if (auth) {
|
||||||
if (key) void credDelete(key).catch(() => {});
|
if (key) void credDelete(key).catch(() => {});
|
||||||
credDialogError =
|
credDialogError =
|
||||||
"Zugangsdaten wurden abgelehnt oder sind abgelaufen. Bitte erneut anmelden.";
|
"Credentials were rejected or have expired. Please sign in again.";
|
||||||
credDialogAction = action;
|
credDialogAction = action;
|
||||||
credDialogKey = key;
|
credDialogKey = key;
|
||||||
credDialogOpen = true;
|
credDialogOpen = true;
|
||||||
@@ -984,7 +1047,7 @@
|
|||||||
errorMessage = message;
|
errorMessage = message;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
credDialogError = message || "Anmeldung fehlgeschlagen.";
|
credDialogError = message || "Sign-in failed.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,11 +1085,11 @@
|
|||||||
if (errorMessage && !isAuthError(errorMessage) && isNonFastForwardPushError(errorMessage)) {
|
if (errorMessage && !isAuthError(errorMessage) && isNonFastForwardPushError(errorMessage)) {
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
const shouldSync = window.confirm(
|
const shouldSync = window.confirm(
|
||||||
"Der Remote hat neuere Commits, deshalb wurde der Push abgelehnt.\n\nJetzt Pull/Merge ausfuehren und danach den Push erneut versuchen?",
|
"The remote has newer commits, so the push was rejected.\n\nRun Pull/Merge now and try pushing again afterwards?",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!shouldSync) {
|
if (!shouldSync) {
|
||||||
const message = "Push abgelehnt: Der Remote hat neuere Commits. Pull zuerst ausfuehren, dann erneut pushen.";
|
const message = "Push rejected: the remote has newer commits. Pull first, then push again.";
|
||||||
if (fromStore) errorMessage = message;
|
if (fromStore) errorMessage = message;
|
||||||
else credDialogError = message;
|
else credDialogError = message;
|
||||||
return;
|
return;
|
||||||
@@ -1050,7 +1113,7 @@
|
|||||||
if (statusHasConflicts(status)) {
|
if (statusHasConflicts(status)) {
|
||||||
credDialogOpen = false;
|
credDialogOpen = false;
|
||||||
credDialogAction = null;
|
credDialogAction = null;
|
||||||
errorMessage = "Pull hat Merge-Konflikte erzeugt. Loese die Konflikte, committe den Merge und pushe danach erneut.";
|
errorMessage = "Pull produced merge conflicts. Resolve the conflicts, commit the merge, and then push again.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1125,7 +1188,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discardFile(file: GitFileStatus, staged: boolean) {
|
function discardFile(file: GitFileStatus, staged: boolean) {
|
||||||
|
if (!activeRepoPath || isBusy) return;
|
||||||
|
pendingDiscard = { kind: "file", file, staged };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDiscardFile(file: GitFileStatus, staged: boolean) {
|
||||||
await runOperation(`Discarding ${file.path}`, async () => {
|
await runOperation(`Discarding ${file.path}`, async () => {
|
||||||
applyStatus(await restoreFiles(activeRepoPath, [file.path], staged));
|
applyStatus(await restoreFiles(activeRepoPath, [file.path], staged));
|
||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
@@ -1176,9 +1244,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyLinePatch(action: PatchApplyAction, patch: string) {
|
function isDiscardPatchAction(action: PatchApplyAction): boolean {
|
||||||
if (!activeRepoPath || !linePatchFile || isBusy) return;
|
return action === "discard-staged" || action === "discard-unstaged";
|
||||||
const file = linePatchFile;
|
}
|
||||||
|
|
||||||
|
async function runLinePatchAction(
|
||||||
|
action: PatchApplyAction,
|
||||||
|
patch: string,
|
||||||
|
file: GitFileStatus,
|
||||||
|
staged: boolean,
|
||||||
|
) {
|
||||||
|
if (!activeRepoPath || isBusy) return;
|
||||||
operation = patchOperationLabel(action, file);
|
operation = patchOperationLabel(action, file);
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
linePatchError = "";
|
linePatchError = "";
|
||||||
@@ -1188,7 +1264,7 @@
|
|||||||
await refreshExplorerFiles(activeRepoPath);
|
await refreshExplorerFiles(activeRepoPath);
|
||||||
await refreshFileHistory(activeRepoPath);
|
await refreshFileHistory(activeRepoPath);
|
||||||
|
|
||||||
const updatedPatch = await getFilePatch(activeRepoPath, file.path, linePatchStaged);
|
const updatedPatch = await getFilePatch(activeRepoPath, file.path, staged);
|
||||||
if (updatedPatch.trim()) {
|
if (updatedPatch.trim()) {
|
||||||
linePatchText = updatedPatch;
|
linePatchText = updatedPatch;
|
||||||
} else {
|
} else {
|
||||||
@@ -1204,6 +1280,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyLinePatch(action: PatchApplyAction, patch: string) {
|
||||||
|
if (!activeRepoPath || !linePatchFile || isBusy) return;
|
||||||
|
const file = linePatchFile;
|
||||||
|
const staged = linePatchStaged;
|
||||||
|
|
||||||
|
if (isDiscardPatchAction(action)) {
|
||||||
|
pendingDiscard = { kind: "hunk", file, staged, action, patch };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await runLinePatchAction(action, patch, file, staged);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmDiscard() {
|
||||||
|
const discard = pendingDiscard;
|
||||||
|
if (!discard || !activeRepoPath || isBusy) return;
|
||||||
|
|
||||||
|
if (discard.kind === "file") {
|
||||||
|
await runDiscardFile(discard.file, discard.staged);
|
||||||
|
} else {
|
||||||
|
await runLinePatchAction(discard.action, discard.patch, discard.file, discard.staged);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingDiscard = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDiscardConfirm() {
|
||||||
|
if (isBusy) return;
|
||||||
|
pendingDiscard = null;
|
||||||
|
}
|
||||||
|
|
||||||
async function stageAllFiles() {
|
async function stageAllFiles() {
|
||||||
const paths = changedFiles.filter((f) => f.unstaged !== null).map((f) => f.path);
|
const paths = changedFiles.filter((f) => f.unstaged !== null).map((f) => f.path);
|
||||||
if (paths.length === 0) return;
|
if (paths.length === 0) return;
|
||||||
@@ -1441,7 +1548,7 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (globalSearchId === searchId) {
|
if (globalSearchId === searchId) {
|
||||||
const message = errorToMessage(error);
|
const message = errorToMessage(error);
|
||||||
globalSearchError = message.includes("abgebrochen") ? "Suche wurde abgebrochen." : message;
|
globalSearchError = message.includes("cancelled") ? "Search was cancelled." : message;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (globalSearchId === searchId) {
|
if (globalSearchId === searchId) {
|
||||||
@@ -1454,7 +1561,7 @@
|
|||||||
async function cancelGlobalSearch() {
|
async function cancelGlobalSearch() {
|
||||||
if (!globalSearchId) return;
|
if (!globalSearchId) return;
|
||||||
const searchId = globalSearchId;
|
const searchId = globalSearchId;
|
||||||
globalSearchError = "Abbruch wird angefordert...";
|
globalSearchError = "Requesting cancellation...";
|
||||||
try {
|
try {
|
||||||
await cancelCodeSearch(searchId);
|
await cancelCodeSearch(searchId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1533,7 +1640,8 @@
|
|||||||
// ── Event handlers ─────────────────────────────────────────────────────────
|
// ── Event handlers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function handleWindowKeydown(event: KeyboardEvent) {
|
function handleWindowKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
|
||||||
|
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
|
||||||
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
|
||||||
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
|
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
|
||||||
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
else if (event.key === "Escape" && compareSelectOpen) compareSelectOpen = false;
|
||||||
@@ -1813,7 +1921,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="top-section">
|
<div class="top-section" style="--commit-panel-height: {commitPanelHeight}px;">
|
||||||
<StatusPanel
|
<StatusPanel
|
||||||
{changedFiles}
|
{changedFiles}
|
||||||
{stagedCount}
|
{stagedCount}
|
||||||
@@ -1830,6 +1938,24 @@
|
|||||||
onStageAll={stageAllFiles}
|
onStageAll={stageAllFiles}
|
||||||
onUnstageAll={unstageAllFiles}
|
onUnstageAll={unstageAllFiles}
|
||||||
/>
|
/>
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="panel-resize-handle"
|
||||||
|
class:resizing={resizingCommitPanel}
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
aria-label="Resize commit panel height"
|
||||||
|
aria-valuenow={commitPanelHeight}
|
||||||
|
aria-valuemin={COMMIT_PANEL_MIN_HEIGHT}
|
||||||
|
aria-valuemax={COMMIT_PANEL_MAX_HEIGHT}
|
||||||
|
tabindex="0"
|
||||||
|
onpointerdown={startCommitPanelResize}
|
||||||
|
onpointermove={onCommitPanelResizeMove}
|
||||||
|
onpointerup={endCommitPanelResize}
|
||||||
|
onpointercancel={endCommitPanelResize}
|
||||||
|
onkeydown={onCommitPanelResizeKeydown}
|
||||||
|
></div>
|
||||||
<CommitPanel
|
<CommitPanel
|
||||||
{commitMessage}
|
{commitMessage}
|
||||||
{canCommit}
|
{canCommit}
|
||||||
@@ -1908,6 +2034,17 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if pendingDiscard}
|
||||||
|
<DiscardConfirmDialog
|
||||||
|
file={pendingDiscard.file}
|
||||||
|
staged={pendingDiscard.staged}
|
||||||
|
scope={pendingDiscard.kind === "hunk" ? "hunk" : "file"}
|
||||||
|
{isBusy}
|
||||||
|
onConfirm={confirmDiscard}
|
||||||
|
onClose={closeDiscardConfirm}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if globalSearchOpen}
|
{#if globalSearchOpen}
|
||||||
<GlobalSearchDialog
|
<GlobalSearchDialog
|
||||||
{hasRepository}
|
{hasRepository}
|
||||||
|
|||||||
+133
-7
@@ -150,6 +150,19 @@
|
|||||||
background: linear-gradient(180deg, rgba(65, 209, 255, 0.18), rgba(100, 108, 255, 0.16));
|
background: linear-gradient(180deg, rgba(65, 209, 255, 0.18), rgba(100, 108, 255, 0.16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
border-color: rgba(255, 90, 103, 0.72);
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 90, 103, 0.92), rgba(195, 44, 64, 0.9));
|
||||||
|
box-shadow: 0 0 22px rgba(255, 90, 103, 0.16);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
border-color: rgba(255, 161, 169, 0.86);
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, rgba(255, 111, 124, 0.96), rgba(214, 55, 77, 0.95));
|
||||||
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -940,13 +953,39 @@
|
|||||||
.top-section {
|
.top-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
grid-template-rows: minmax(0, 1fr) minmax(210px, auto);
|
grid-template-rows: minmax(120px, 1fr) 14px var(--commit-panel-height, 220px);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
gap: 8px;
|
gap: 0;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-resize-handle {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: row-resize;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.panel-resize-handle::before {
|
||||||
|
content: "";
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-border);
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
.panel-resize-handle:hover::before,
|
||||||
|
.panel-resize-handle.resizing::before {
|
||||||
|
background: var(--color-accent);
|
||||||
|
}
|
||||||
|
.panel-resize-handle:focus-visible {
|
||||||
|
outline: 2px solid var(--color-accent);
|
||||||
|
outline-offset: -2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- File list / change lanes --- */
|
/* --- File list / change lanes --- */
|
||||||
|
|
||||||
.file-list { padding: 6px; overflow: auto; }
|
.file-list { padding: 6px; overflow: auto; }
|
||||||
@@ -1330,9 +1369,35 @@
|
|||||||
|
|
||||||
/* --- Commit form --- */
|
/* --- Commit form --- */
|
||||||
|
|
||||||
.commit-form { display: flex; flex-direction: column; flex: 1 1 0; min-height: 0; gap: 8px; padding: 10px; }
|
.commit-panel {
|
||||||
.commit-form textarea { flex: 1 1 0; min-height: 80px; resize: none; }
|
display: flex;
|
||||||
.commit-actions-row { display: flex; gap: 8px; }
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.commit-panel .section-head { flex: 0 0 auto; }
|
||||||
|
.commit-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: minmax(0, 1fr) auto auto;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.commit-form textarea {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
resize: none;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.commit-actions-row {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.commit-actions-row .btn-primary { min-width: 0; }
|
||||||
.commit-ai-button { flex: 0 0 auto; min-width: 64px; justify-content: center; }
|
.commit-ai-button { flex: 0 0 auto; min-width: 64px; justify-content: center; }
|
||||||
.commit-ai-settings-button { flex: 0 0 auto; width: 38px; min-width: 38px; padding: 0; justify-content: center; }
|
.commit-ai-settings-button { flex: 0 0 auto; width: 38px; min-width: 38px; padding: 0; justify-content: center; }
|
||||||
.commit-block-reason {
|
.commit-block-reason {
|
||||||
@@ -1606,6 +1671,14 @@
|
|||||||
max-height: calc(100vh - 32px);
|
max-height: calc(100vh - 32px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
.discard-confirm-dialog {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
width: min(560px, calc(100vw - 32px));
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
.ai-settings-dialog {
|
.ai-settings-dialog {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(560px, calc(100vw - 32px));
|
width: min(560px, calc(100vw - 32px));
|
||||||
@@ -1857,6 +1930,59 @@
|
|||||||
|
|
||||||
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #4eca76; font-size: 12px; font-weight: 700; }
|
.prepared-tag { display: inline-flex; align-items: center; gap: 4px; margin-right: auto; color: #4eca76; font-size: 12px; font-weight: 700; }
|
||||||
|
|
||||||
|
.discard-confirm-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px 16px 16px;
|
||||||
|
}
|
||||||
|
.discard-warning-icon {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid rgba(255, 90, 103, 0.32);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #ff9aa4;
|
||||||
|
background: rgba(255, 90, 103, 0.1);
|
||||||
|
}
|
||||||
|
.discard-confirm-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--color-ink-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.discard-confirm-copy p { margin: 0; }
|
||||||
|
.discard-target {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
max-height: 84px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px 9px;
|
||||||
|
border: 1px solid var(--color-border-subtle);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--color-ink);
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.discard-warning-text {
|
||||||
|
color: #ffb8bf;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.discard-confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px 14px;
|
||||||
|
border-top: 1px solid var(--color-border-subtle);
|
||||||
|
background: var(--color-surface-dim);
|
||||||
|
}
|
||||||
|
|
||||||
.line-patch-body {
|
.line-patch-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: minmax(0, 1fr);
|
grid-template-rows: minmax(0, 1fr);
|
||||||
@@ -2893,7 +3019,7 @@
|
|||||||
/* Stack CommitPanel below StatusPanel; history panels stay side by side */
|
/* Stack CommitPanel below StatusPanel; history panels stay side by side */
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.workspace { grid-template-columns: clamp(185px, 16vw, 220px) minmax(0, 1fr) clamp(380px, 38vw, 500px); }
|
.workspace { grid-template-columns: clamp(185px, 16vw, 220px) minmax(0, 1fr) clamp(380px, 38vw, 500px); }
|
||||||
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 190px; }
|
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact: stack history panels vertically, narrow sidebars */
|
/* Compact: stack history panels vertically, narrow sidebars */
|
||||||
@@ -2919,7 +3045,7 @@
|
|||||||
.workspace { grid-template-columns: 1fr; gap: 6px; }
|
.workspace { grid-template-columns: 1fr; gap: 6px; }
|
||||||
.history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(200px, 1fr) minmax(150px, 0.5fr); min-height: 380px; }
|
.history-aside { grid-template-columns: 1fr; grid-template-rows: minmax(200px, 1fr) minmax(150px, 0.5fr); min-height: 380px; }
|
||||||
.left-sidebar { grid-template-rows: minmax(200px, 1fr) minmax(240px, 1.2fr); min-height: 440px; }
|
.left-sidebar { grid-template-rows: minmax(200px, 1fr) minmax(240px, 1.2fr); min-height: 440px; }
|
||||||
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 190px; }
|
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); }
|
||||||
.repo-form { grid-template-columns: 1fr; }
|
.repo-form { grid-template-columns: 1fr; }
|
||||||
.repo-tabbar { grid-template-columns: minmax(0, 1fr) auto; }
|
.repo-tabbar { grid-template-columns: minmax(0, 1fr) auto; }
|
||||||
.repo-tab.management { min-width: 0; }
|
.repo-tab.management { min-width: 0; }
|
||||||
|
|||||||
@@ -153,9 +153,9 @@
|
|||||||
<div class="cred-token-hint">
|
<div class="cred-token-hint">
|
||||||
<AlertCircle size={13} aria-hidden="true" />
|
<AlertCircle size={13} aria-hidden="true" />
|
||||||
<span>
|
<span>
|
||||||
Beim Wechsel wird das Modell{selectedLocalModel ? ` (${formatSize(selectedLocalModel.approx_size_mb)})` : ""}
|
Switching downloads the model{selectedLocalModel ? ` (${formatSize(selectedLocalModel.approx_size_mb)})` : ""}
|
||||||
im Hintergrund heruntergeladen — je nach Internetverbindung kann das mehrere Minuten dauern.
|
in the background — depending on your internet connection this can take several minutes.
|
||||||
Danach bleibt es lokal zwischengespeichert und lädt beim nächsten Start sofort.
|
After that it stays cached locally and loads instantly on the next start.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{:else if provider === "openai"}
|
{:else if provider === "openai"}
|
||||||
@@ -230,7 +230,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="cred-token-hint">
|
<div class="cred-token-hint">
|
||||||
<Globe size={13} aria-hidden="true" />
|
<Globe size={13} aria-hidden="true" />
|
||||||
<span>Für lokale OpenAI-kompatible Server wie Ollama oder LM Studio. Die Basis-URL sollte auf /v1 enden.</span>
|
<span>For local OpenAI-compatible servers like Ollama or LM Studio. The base URL should end in /v1.</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="panel flex flex-col" aria-label="Commit">
|
<section class="panel commit-panel" aria-label="Commit">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Commit</span>
|
<span class="eyebrow">Commit</span>
|
||||||
|
|||||||
@@ -44,10 +44,10 @@
|
|||||||
(mode === "token" || username.trim().length > 0),
|
(mode === "token" || username.trim().length > 0),
|
||||||
);
|
);
|
||||||
let actionLabel = $derived(action === "push" ? "Push" : "Pull");
|
let actionLabel = $derived(action === "push" ? "Push" : "Pull");
|
||||||
let actionTitle = $derived(action === "push" ? "Push authentifizieren" : "Pull authentifizieren");
|
let actionTitle = $derived(action === "push" ? "Authenticate push" : "Authenticate pull");
|
||||||
let actionHint = $derived(action === "push"
|
let actionHint = $derived(action === "push"
|
||||||
? "Der Remote braucht Schreibrechte. Nutze ein Passwort oder einen Token mit passenden Repository-Rechten."
|
? "The remote needs write access. Use a password or a token with the appropriate repository permissions."
|
||||||
: "Der Remote braucht Zugriff auf das Repository. Nutze deine Git-Zugangsdaten oder einen Personal Access Token.");
|
: "The remote needs access to the repository. Use your Git credentials or a personal access token.");
|
||||||
|
|
||||||
function handleSubmit(e: SubmitEvent) {
|
function handleSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
onclick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
|
onclick={(e) => { if (e.target === e.currentTarget) onCancel(); }}
|
||||||
>
|
>
|
||||||
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git-Zugangsdaten" tabindex="-1">
|
<div class="cred-card" role="dialog" aria-modal="true" aria-label="Git credentials" tabindex="-1">
|
||||||
<div class="cred-hero">
|
<div class="cred-hero">
|
||||||
<div class="cred-hero-top">
|
<div class="cred-hero-top">
|
||||||
<div class="cred-hero-icon">
|
<div class="cred-hero-icon">
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<p class="cred-hero-label">{actionLabel} Remote</p>
|
<p class="cred-hero-label">{actionLabel} Remote</p>
|
||||||
<h2 class="cred-hero-title">{actionTitle}</h2>
|
<h2 class="cred-hero-title">{actionTitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="cred-close" type="button" onclick={onCancel} title="Abbrechen" aria-label="Abbrechen">
|
<button class="cred-close" type="button" onclick={onCancel} title="Cancel" aria-label="Cancel">
|
||||||
<X size={16} aria-hidden="true" />
|
<X size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,12 +89,12 @@
|
|||||||
|
|
||||||
<div class="cred-security-note">
|
<div class="cred-security-note">
|
||||||
<ShieldCheck size={14} aria-hidden="true" />
|
<ShieldCheck size={14} aria-hidden="true" />
|
||||||
<span>Beim Speichern landet der Token verschluesselt im Schluesselbund des Betriebssystems – nie im Klartext.</span>
|
<span>When saved, the token is stored encrypted in the operating system's keychain — never in plain text.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="cred-body" onsubmit={handleSubmit}>
|
<form class="cred-body" onsubmit={handleSubmit}>
|
||||||
<div class="cred-segment" role="group" aria-label="Authentifizierungsart">
|
<div class="cred-segment" role="group" aria-label="Authentication method">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="cred-seg-btn"
|
class="cred-seg-btn"
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
aria-pressed={mode === "credentials"}
|
aria-pressed={mode === "credentials"}
|
||||||
>
|
>
|
||||||
<User size={13} aria-hidden="true" />
|
<User size={13} aria-hidden="true" />
|
||||||
Username + Passwort
|
Username + password
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
id="cred-username"
|
id="cred-username"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={username}
|
bind:value={username}
|
||||||
placeholder="z. B. mein-github-username"
|
placeholder="e.g. my-github-username"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
/>
|
/>
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
|
|
||||||
<div class="cred-field">
|
<div class="cred-field">
|
||||||
<label class="cred-field-label" for="cred-password">
|
<label class="cred-field-label" for="cred-password">
|
||||||
{mode === "token" ? "Token" : "Passwort"}
|
{mode === "token" ? "Token" : "Password"}
|
||||||
</label>
|
</label>
|
||||||
<div class="cred-input">
|
<div class="cred-input">
|
||||||
<Lock size={15} class="cred-field-icon" aria-hidden="true" />
|
<Lock size={15} class="cred-field-icon" aria-hidden="true" />
|
||||||
@@ -146,8 +146,8 @@
|
|||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
placeholder={mode === "token"
|
placeholder={mode === "token"
|
||||||
? "ghp_... oder anderer Zugangstoken"
|
? "ghp_... or another access token"
|
||||||
: "Passwort oder Personal Access Token"}
|
: "Password or personal access token"}
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
/>
|
/>
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
class="cred-reveal"
|
class="cred-reveal"
|
||||||
onclick={() => { showPassword = !showPassword; }}
|
onclick={() => { showPassword = !showPassword; }}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-label={showPassword ? "Verbergen" : "Anzeigen"}
|
aria-label={showPassword ? "Hide" : "Show"}
|
||||||
>
|
>
|
||||||
{#if showPassword}
|
{#if showPassword}
|
||||||
<EyeOff size={14} aria-hidden="true" />
|
<EyeOff size={14} aria-hidden="true" />
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
{#if mode === "token"}
|
{#if mode === "token"}
|
||||||
<div class="cred-token-hint">
|
<div class="cred-token-hint">
|
||||||
<Key size={13} aria-hidden="true" />
|
<Key size={13} aria-hidden="true" />
|
||||||
<span>Username wird automatisch auf <code>oauth2</code> gesetzt. Das funktioniert mit GitHub, GitLab und Bitbucket.</span>
|
<span>Username is automatically set to <code>oauth2</code>. This works with GitHub, GitLab, and Bitbucket.</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -184,26 +184,26 @@
|
|||||||
|
|
||||||
{#if saveSession}
|
{#if saveSession}
|
||||||
<div class="cred-expiry">
|
<div class="cred-expiry">
|
||||||
<label class="cred-field-label" for="cred-expiry">Ablaufdatum (optional)</label>
|
<label class="cred-field-label" for="cred-expiry">Expiration date (optional)</label>
|
||||||
<input
|
<input
|
||||||
id="cred-expiry"
|
id="cred-expiry"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={expiresAt}
|
bind:value={expiresAt}
|
||||||
disabled={isBusy}
|
disabled={isBusy}
|
||||||
/>
|
/>
|
||||||
<span class="cred-expiry-hint">Nach diesem Datum wird automatisch erneut nach dem Login gefragt.</span>
|
<span class="cred-expiry-hint">After this date you'll automatically be asked to log in again.</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="cred-footer">
|
<div class="cred-footer">
|
||||||
<label class="cred-save">
|
<label class="cred-save">
|
||||||
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
|
<input type="checkbox" bind:checked={saveSession} disabled={isBusy} />
|
||||||
<span>Im Schluesselbund speichern</span>
|
<span>Save in keychain</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="cred-btns">
|
<div class="cred-btns">
|
||||||
<button type="button" class="cred-cancel" onclick={onCancel} disabled={isBusy}>
|
<button type="button" class="cred-cancel" onclick={onCancel} disabled={isBusy}>
|
||||||
Abbrechen
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button class="cred-submit" type="submit" disabled={!canSubmit}>
|
<button class="cred-submit" type="submit" disabled={!canSubmit}>
|
||||||
{#if isBusy}
|
{#if isBusy}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertTriangle, LoaderCircle, RotateCcw, X } from "@lucide/svelte";
|
||||||
|
import type { GitFileStatus } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
file: GitFileStatus;
|
||||||
|
staged: boolean;
|
||||||
|
scope: "file" | "hunk";
|
||||||
|
isBusy: boolean;
|
||||||
|
onConfirm: () => void | Promise<void>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
file,
|
||||||
|
staged = false,
|
||||||
|
scope = "file",
|
||||||
|
isBusy = false,
|
||||||
|
onConfirm = () => {},
|
||||||
|
onClose = () => {},
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let targetPath = $derived(file.old_path ? `${file.old_path} -> ${file.path}` : file.path);
|
||||||
|
let title = $derived(scope === "hunk" ? "Discard hunk?" : "Discard file changes?");
|
||||||
|
let scopeLabel = $derived(scope === "hunk" ? "Selected hunk" : "File changes");
|
||||||
|
let sourceLabel = $derived(staged ? "staged changes" : "unstaged changes");
|
||||||
|
|
||||||
|
function closeFromBackdrop(event: MouseEvent) {
|
||||||
|
if (isBusy || event.target !== event.currentTarget) return;
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dialog-backdrop" role="presentation" onclick={closeFromBackdrop}>
|
||||||
|
<div class="dialog discard-confirm-dialog" role="dialog" aria-modal="true" aria-label={title}>
|
||||||
|
<header class="dialog-header">
|
||||||
|
<div>
|
||||||
|
<span class="eyebrow">Confirm discard</span>
|
||||||
|
<p class="dialog-title">{title}</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
|
||||||
|
<X size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="discard-confirm-body">
|
||||||
|
<div class="discard-warning-icon" aria-hidden="true">
|
||||||
|
<AlertTriangle size={22} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="discard-confirm-copy">
|
||||||
|
<p>
|
||||||
|
This will reset the {sourceLabel} for the {scopeLabel.toLowerCase()} below.
|
||||||
|
</p>
|
||||||
|
<code class="discard-target" title={targetPath}>{targetPath}</code>
|
||||||
|
<p class="discard-warning-text">
|
||||||
|
This cannot be undone. If the file only exists in your working tree, it can be deleted entirely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="discard-confirm-actions">
|
||||||
|
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>Cancel</button>
|
||||||
|
<button class="btn-danger" type="button" onclick={onConfirm} disabled={isBusy}>
|
||||||
|
{#if isBusy}
|
||||||
|
<LoaderCircle class="spin" size={15} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<RotateCcw size={15} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let label = "Repository wird geöffnet";
|
export let label = "Opening repository";
|
||||||
export let repoName = "";
|
export let repoName = "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user