diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4284170..9e300b4 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2537,6 +2537,7 @@ version = "0.1.0" dependencies = [ "commit_ai", "keyring", + "log", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 51920cc..f81922f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-plugin-aptabase = "1.0" keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] } commit_ai = { path = "crates/commit_ai" } tokio = "1.52.3" +log = "0.4" [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index c182510..90d9b8f 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -493,6 +493,7 @@ pub fn list_branches(path: String) -> Result, String> { #[tauri::command] pub fn list_remotes(path: String) -> Result, String> { + log::info!(target: "gitty::remote", "list_remotes invoked: path={path:?}"); let repo = resolve_repo(&path)?; let names = run_git(&repo, ["remote"])?; Ok(String::from_utf8_lossy(&names) @@ -531,10 +532,23 @@ pub fn update_remote(path: String, name: String, url: String) -> Result Result, String> { - let repo = resolve_repo(&path)?; - let name = validate_remote_name(&repo, &name, true)?; - run_git(&repo, ["remote", "remove", name.as_str()])?; - list_remotes(path) + log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}"); + let result = (|| { + let repo = resolve_repo(&path)?; + log::info!(target: "gitty::remote", "remove_remote resolved repository: {}", repo.display()); + let name = validate_remote_name(&repo, &name, true)?; + log::info!(target: "gitty::remote", "remove_remote validated remote: {name}"); + run_git(&repo, ["remote", "remove", name.as_str()])?; + log::info!(target: "gitty::remote", "remove_remote git command succeeded: {name}"); + list_remotes(path) + })(); + match &result { + Ok(remotes) => { + log::info!(target: "gitty::remote", "remove_remote completed; remaining={:?}", remotes.iter().map(|remote| remote.name.as_str()).collect::>()) + } + Err(error) => log::error!(target: "gitty::remote", "remove_remote failed: {error}"), + } + result } #[tauri::command] @@ -576,15 +590,24 @@ pub fn delete_remote_branch( remote: String, branch: String, ) -> Result { - let repo = resolve_repo(&path)?; - let remote = validate_remote_name(&repo, &remote, true)?; - let branch = branch.trim(); - if branch.is_empty() || branch.starts_with('-') { - return Err("Invalid remote branch name.".to_string()); + log::info!(target: "gitty::remote", "delete_remote_branch invoked: path={path:?}, remote={remote:?}, branch={branch:?}"); + let result = (|| { + let repo = resolve_repo(&path)?; + let remote = validate_remote_name(&repo, &remote, true)?; + let branch = branch.trim(); + if branch.is_empty() || branch.starts_with('-') { + return Err("Invalid remote branch name.".to_string()); + } + run_git(&repo, ["check-ref-format", "--branch", branch])?; + log::info!(target: "gitty::remote", "deleting remote branch with git push: {remote}/{branch}"); + run_git(&repo, ["push", remote.as_str(), "--delete", branch])?; + log::info!(target: "gitty::remote", "remote branch deleted successfully: {remote}/{branch}"); + status_for_repo(&repo) + })(); + if let Err(error) = &result { + log::error!(target: "gitty::remote", "delete_remote_branch failed: {error}"); } - run_git(&repo, ["check-ref-format", "--branch", branch])?; - run_git(&repo, ["push", remote.as_str(), "--delete", branch])?; - status_for_repo(&repo) + result } #[tauri::command] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index aff4d15..8e0d866 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -25,6 +25,36 @@ use git::{ }; use tauri::Manager; +struct ConsoleLogger; + +impl log::Log for ConsoleLogger { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.level() <= log::Level::Info + } + + fn log(&self, record: &log::Record<'_>) { + if self.enabled(record.metadata()) { + eprintln!( + "[{}] [{}] {}", + record.level(), + record.target(), + record.args() + ); + } + } + + fn flush(&self) {} +} + +static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger; + +fn init_console_logging() { + if log::set_logger(&CONSOLE_LOGGER).is_ok() { + log::set_max_level(log::LevelFilter::Info); + log::info!(target: "gitty", "Rust console logging initialized"); + } +} + #[tauri::command] fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> { if let Some(window) = app.get_webview_window("splashscreen") { @@ -47,6 +77,7 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> { #[tokio::main] async fn main() { + init_console_logging(); if let Some(result) = run_sequence_editor_if_requested() { if let Err(error) = result { eprintln!("{error}"); diff --git a/src/App.svelte b/src/App.svelte index a366617..29fe331 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -2276,7 +2276,27 @@ async function confirmDeleteBranch() { const branch = deleteBranchTarget; - if (!activeRepoPath || !branch || branch.remote || branch.current || isBusy) return; + if (!activeRepoPath || !branch || branch.current || isBusy) return; + + if (branch.remote) { + const slash = branch.name.indexOf("/"); + if (slash < 1) { errorMessage = "Could not determine remote name."; return; } + const remote = branch.name.slice(0, slash); + const remoteBranch = branch.name.slice(slash + 1); + operation = `Deleting ${branch.name} from remote`; + errorMessage = ""; + try { + applyStatus(await deleteRemoteBranch(activeRepoPath, remote, remoteBranch)); + deleteBranchTarget = null; + await refreshRefsAndCommitGraph(activeRepoPath); + trackEvent("remote_branch_deleted"); + } catch (error) { + errorMessage = errorToMessage(error); + } finally { + operation = ""; + } + return; + } operation = `${deleteBranchForce ? "Force deleting" : "Deleting"} ${branch.name}`; errorMessage = ""; @@ -2809,11 +2829,10 @@ async function deleteTrackedRemoteBranch(branch: GitBranchInfo) { if (!activeRepoPath || !branch.remote) return; - const slash = branch.name.indexOf("/"); - if (slash < 1) { errorMessage = "Could not determine remote name."; return; } - const remote = branch.name.slice(0, slash); const remoteBranch = branch.name.slice(slash + 1); - if (!window.confirm(`Delete '${remoteBranch}' from remote '${remote}'?`)) return; - await runOperation("Deleting remote branch", async () => { applyStatus(await deleteRemoteBranch(activeRepoPath, remote, remoteBranch)); await refreshRefsAndCommitGraph(activeRepoPath); }); + if (import.meta.env.DEV) console.info("[Gitty remote] remote branch delete requested", branch); + deleteBranchTarget = branch; + deleteBranchForce = false; + trackEvent("remote_branch_delete_dialog_opened"); } async function initializeRepository() { @@ -2873,10 +2892,21 @@ async function addSyncRemote(name: string, url: string) { if (!activeRepoPath) return; syncSettingsRemotes = await addRemote(activeRepoPath, name, url); await refreshBranchList(activeRepoPath); } async function updateSyncRemote(name: string, url: string) { if (!activeRepoPath) return; syncSettingsRemotes = await updateRemote(activeRepoPath, name, url); } async function removeSyncRemote(name: string) { - if (!activeRepoPath || !window.confirm(`Remove remote '${name}'? Local commits and branches are kept.`)) return; - syncSettingsRemotes = await removeRemote(activeRepoPath, name); - if (selectedRemote === name) selectedRemote = ""; - await refreshBranchList(activeRepoPath); + if (!activeRepoPath) return; + operation = `Removing remote ${name}`; + try { + syncSettingsRemotes = await removeRemote(activeRepoPath, name); + if (syncSettingsRemotes.some((remote) => remote.name === name)) throw new Error(`Remote '${name}' still exists after removal.`); + if (selectedRemote === name) { selectedRemote = ""; localStorage.setItem("gitlite.selectedRemote", ""); } + applyStatus(await getStatus(activeRepoPath)); + await refreshBranchList(activeRepoPath); + } catch (error) { + const message = errorToMessage(error); + if (import.meta.env.DEV) console.error("[Gitty remote] remove_remote failed", { name, path: activeRepoPath, error }); + throw new Error(message); + } finally { + operation = ""; + } } async function saveStash(message: string, includeUntracked: boolean) { @@ -3667,6 +3697,7 @@ } function handleWindowContextMenu(event: MouseEvent) { + if (import.meta.env.DEV) return; event.preventDefault(); if (repoTabContextMenu) closeRepoTabContextMenu(); } @@ -4404,7 +4435,7 @@ /> {/if} - + {#if deleteBranchTarget} div { display: flex; gap: 8px; } @media (max-width: 720px) { .strategy-options, .sync-fields { grid-template-columns: 1fr; } .remote-add { grid-template-columns: 1fr; } .sync-settings-footer { align-items: stretch; flex-direction: column; } .sync-settings-footer > div { justify-content: flex-end; } } + +/* Compact square controls shared by the Branches, Stash and Explorer headers. */ +.branch-head-actions .branch-create-toggle, +.stash-head-actions .stash-toggle, +.explorer-head-actions .explorer-bulk-button { + box-sizing: border-box; + display: inline-grid; + place-items: center; + inline-size: 24px; + min-inline-size: 24px; + max-inline-size: 24px; + block-size: 24px; + min-block-size: 24px; + max-block-size: 24px; + flex: 0 0 24px; + aspect-ratio: 1 / 1; + padding: 0; + border-radius: 5px; +} .ai-review-suggestion { display: grid; gap: 3px; margin-top: 9px; padding: 8px 9px; border-radius: 5px; background: var(--color-surface-dim); } .ai-review-suggestion strong { color: var(--color-ink-dim); font-size: 9.5px; text-transform: uppercase; letter-spacing: .05em; } .ai-review-suggestion span { color: var(--color-ink-muted); font-size: 11.5px; line-height: 1.45; } diff --git a/src/lib/components/BranchDeleteConfirmDialog.svelte b/src/lib/components/BranchDeleteConfirmDialog.svelte index 0d6b563..3aea620 100644 --- a/src/lib/components/BranchDeleteConfirmDialog.svelte +++ b/src/lib/components/BranchDeleteConfirmDialog.svelte @@ -18,7 +18,8 @@ onClose = () => {}, }: Props = $props(); - let title = $derived(force ? "Force delete branch?" : "Delete branch?"); + let title = $derived(branch.remote ? "Delete remote branch?" : force ? "Force delete branch?" : "Delete branch?"); + let remoteParts = $derived(branch.remote ? branch.name.split(/\/(.+)/) : []); @@ -26,7 +27,7 @@ diff --git a/src/lib/components/BranchPanel.svelte b/src/lib/components/BranchPanel.svelte index 5a6697b..5731ab4 100644 --- a/src/lib/components/BranchPanel.svelte +++ b/src/lib/components/BranchPanel.svelte @@ -266,7 +266,7 @@ async function deleteContextBranch() { const branch = contextBranch; - if (!branch || branch.current || branch.remote || isBusy) return; + if (!branch || branch.current || isBusy) return; closeBranchContextMenu(); if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch); } diff --git a/src/lib/components/SyncSettingsDialog.svelte b/src/lib/components/SyncSettingsDialog.svelte index af006f8..34823cb 100644 --- a/src/lib/components/SyncSettingsDialog.svelte +++ b/src/lib/components/SyncSettingsDialog.svelte @@ -23,12 +23,37 @@ let newUrl = ""; let editingName = ""; let editingUrl = ""; + let actionError = ""; + let actionStatus = ""; $: de = language === "de"; - function beginEdit(remote: GitRemote) { editingName = remote.name; editingUrl = remote.fetch_url; } + function beginEdit(remote: GitRemote) { actionError = ""; editingName = remote.name; editingUrl = remote.fetch_url; } + async function requestDelete(event: MouseEvent, name: string) { + console.log(name) + event.preventDefault(); + event.stopPropagation(); + editingName = ""; + actionError = ""; + actionStatus = de ? `Remote „${name}“ wird entfernt …` : `Removing remote “${name}” …`; + console.info("[Gitty remote] remove button activated", { name }); + await remove(name); + } function cancelEdit() { editingName = ""; editingUrl = ""; } async function add() { if (!newName.trim() || !newUrl.trim()) return; await onAddRemote(newName.trim(), newUrl.trim()); newName = "origin"; newUrl = ""; } async function update() { if (!editingName || !editingUrl.trim()) return; await onUpdateRemote(editingName, editingUrl.trim()); cancelEdit(); } + async function remove(name: string) { + actionError = ""; + console.log("remove") + try { + await onRemoveRemote(name); + if (draftRemote === name) draftRemote = ""; + if (draftUpstream.startsWith(`${name}/`)) draftUpstream = ""; + } catch (error) { + actionError = error instanceof Error ? error.message : String(error); + } finally { + actionStatus = ""; + } + }