feat(remote): Enhance remote branch management and stability

Improved handling for deleting remote branches across the application, enhancing both user experience and backend reliability. This includes adding structured logging to all Git remote operations in Rust, refining UI components to handle remote-specific deletion flows, and providing clear status/error feedback in sync settings.

- Standardized styling for action toggles (Stash, Branch, Explorer) using consistent dimensions.
- Implemented detailed console logging for all Git remote operations on the backend.
- Refined dialogs and sync settings to provide explicit status and error messages during remote management.
This commit is contained in:
Christoph Brandau
2026-07-14 00:29:37 +02:00
parent 7800f0fb24
commit 95c8b01ed1
10 changed files with 219 additions and 57 deletions
+1
View File
@@ -2537,6 +2537,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"commit_ai", "commit_ai",
"keyring", "keyring",
"log",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
+1
View File
@@ -21,6 +21,7 @@ tauri-plugin-aptabase = "1.0"
keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] } keyring = { version = "3", features = ["apple-native", "windows-native", "async-secret-service", "crypto-rust", "async-io"] }
commit_ai = { path = "crates/commit_ai" } commit_ai = { path = "crates/commit_ai" }
tokio = "1.52.3" tokio = "1.52.3"
log = "0.4"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
+35 -12
View File
@@ -493,6 +493,7 @@ pub fn list_branches(path: String) -> Result<Vec<GitBranch>, String> {
#[tauri::command] #[tauri::command]
pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> { pub fn list_remotes(path: String) -> Result<Vec<GitRemote>, String> {
log::info!(target: "gitty::remote", "list_remotes invoked: path={path:?}");
let repo = resolve_repo(&path)?; let repo = resolve_repo(&path)?;
let names = run_git(&repo, ["remote"])?; let names = run_git(&repo, ["remote"])?;
Ok(String::from_utf8_lossy(&names) Ok(String::from_utf8_lossy(&names)
@@ -531,10 +532,23 @@ pub fn update_remote(path: String, name: String, url: String) -> Result<Vec<GitR
#[tauri::command] #[tauri::command]
pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, String> { pub fn remove_remote(path: String, name: String) -> Result<Vec<GitRemote>, String> {
let repo = resolve_repo(&path)?; log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}");
let name = validate_remote_name(&repo, &name, true)?; let result = (|| {
run_git(&repo, ["remote", "remove", name.as_str()])?; let repo = resolve_repo(&path)?;
list_remotes(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::<Vec<_>>())
}
Err(error) => log::error!(target: "gitty::remote", "remove_remote failed: {error}"),
}
result
} }
#[tauri::command] #[tauri::command]
@@ -576,15 +590,24 @@ pub fn delete_remote_branch(
remote: String, remote: String,
branch: String, branch: String,
) -> Result<GitStatus, String> { ) -> Result<GitStatus, String> {
let repo = resolve_repo(&path)?; log::info!(target: "gitty::remote", "delete_remote_branch invoked: path={path:?}, remote={remote:?}, branch={branch:?}");
let remote = validate_remote_name(&repo, &remote, true)?; let result = (|| {
let branch = branch.trim(); let repo = resolve_repo(&path)?;
if branch.is_empty() || branch.starts_with('-') { let remote = validate_remote_name(&repo, &remote, true)?;
return Err("Invalid remote branch name.".to_string()); 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])?; result
run_git(&repo, ["push", remote.as_str(), "--delete", branch])?;
status_for_repo(&repo)
} }
#[tauri::command] #[tauri::command]
+31
View File
@@ -25,6 +25,36 @@ use git::{
}; };
use tauri::Manager; 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] #[tauri::command]
fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> { fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
if let Some(window) = app.get_webview_window("splashscreen") { if let Some(window) = app.get_webview_window("splashscreen") {
@@ -47,6 +77,7 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
init_console_logging();
if let Some(result) = run_sequence_editor_if_requested() { if let Some(result) = run_sequence_editor_if_requested() {
if let Err(error) = result { if let Err(error) = result {
eprintln!("{error}"); eprintln!("{error}");
+42 -11
View File
@@ -2276,7 +2276,27 @@
async function confirmDeleteBranch() { async function confirmDeleteBranch() {
const branch = deleteBranchTarget; 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}`; operation = `${deleteBranchForce ? "Force deleting" : "Deleting"} ${branch.name}`;
errorMessage = ""; errorMessage = "";
@@ -2809,11 +2829,10 @@
async function deleteTrackedRemoteBranch(branch: GitBranchInfo) { async function deleteTrackedRemoteBranch(branch: GitBranchInfo) {
if (!activeRepoPath || !branch.remote) return; if (!activeRepoPath || !branch.remote) return;
const slash = branch.name.indexOf("/"); if (import.meta.env.DEV) console.info("[Gitty remote] remote branch delete requested", branch);
if (slash < 1) { errorMessage = "Could not determine remote name."; return; } deleteBranchTarget = branch;
const remote = branch.name.slice(0, slash); const remoteBranch = branch.name.slice(slash + 1); deleteBranchForce = false;
if (!window.confirm(`Delete '${remoteBranch}' from remote '${remote}'?`)) return; trackEvent("remote_branch_delete_dialog_opened");
await runOperation("Deleting remote branch", async () => { applyStatus(await deleteRemoteBranch(activeRepoPath, remote, remoteBranch)); await refreshRefsAndCommitGraph(activeRepoPath); });
} }
async function initializeRepository() { 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 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 updateSyncRemote(name: string, url: string) { if (!activeRepoPath) return; syncSettingsRemotes = await updateRemote(activeRepoPath, name, url); }
async function removeSyncRemote(name: string) { async function removeSyncRemote(name: string) {
if (!activeRepoPath || !window.confirm(`Remove remote '${name}'? Local commits and branches are kept.`)) return; if (!activeRepoPath) return;
syncSettingsRemotes = await removeRemote(activeRepoPath, name); operation = `Removing remote ${name}`;
if (selectedRemote === name) selectedRemote = ""; try {
await refreshBranchList(activeRepoPath); 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) { async function saveStash(message: string, includeUntracked: boolean) {
@@ -3667,6 +3697,7 @@
} }
function handleWindowContextMenu(event: MouseEvent) { function handleWindowContextMenu(event: MouseEvent) {
if (import.meta.env.DEV) return;
event.preventDefault(); event.preventDefault();
if (repoTabContextMenu) closeRepoTabContextMenu(); if (repoTabContextMenu) closeRepoTabContextMenu();
} }
@@ -4404,7 +4435,7 @@
/> />
{/if} {/if}
<!-- Delete a local branch from the branch context menu --> <!-- Confirm deletion of a local or remote branch from the shared branch context menu -->
{#if deleteBranchTarget} {#if deleteBranchTarget}
<BranchDeleteConfirmDialog <BranchDeleteConfirmDialog
branch={deleteBranchTarget} branch={deleteBranchTarget}
+66 -25
View File
@@ -1703,20 +1703,23 @@
.stash-toggle { .stash-toggle {
display: inline-grid; display: inline-grid;
place-items: center; place-items: center;
width: 26px; width: 28px;
min-width: 26px; min-width: 28px;
min-height: 26px; height: 28px;
min-height: 28px;
aspect-ratio: 1;
flex: 0 0 28px;
padding: 0; padding: 0;
border-color: rgba(94,110,156,0.18); border-color: rgba(94,110,156,0.18);
border-radius: 7px; border-radius: 6px;
color: var(--color-ink-dim); color: var(--color-ink-dim);
background: rgba(255,255,255,0.035); background: rgba(255,255,255,0.018);
} }
.stash-toggle:hover:not(:disabled) { .stash-toggle:hover:not(:disabled) {
border-color: rgba(65,209,255,0.28); border-color: rgba(65,209,255,0.28);
color: var(--color-ink); color: var(--color-ink);
background: rgba(65,209,255,0.08); background: rgba(65,209,255,0.055);
} }
.stash-create { .stash-create {
@@ -1847,19 +1850,24 @@
} }
.branch-create-toggle { .branch-create-toggle {
width: 26px; display: inline-grid;
min-width: 26px; place-items: center;
min-height: 26px; width: 28px;
min-width: 28px;
height: 28px;
min-height: 28px;
aspect-ratio: 1;
flex: 0 0 28px;
padding: 0; padding: 0;
border-color: rgba(65,209,255,0.2); border-color: rgba(94,110,156,0.18);
border-radius: 7px; border-radius: 6px;
color: var(--color-ink-dim); color: var(--color-ink-dim);
background: rgba(65,209,255,0.06); background: rgba(255,255,255,0.018);
} }
.branch-create-toggle:hover:not(:disabled) { .branch-create-toggle:hover:not(:disabled) {
border-color: rgba(65,209,255,0.45); border-color: rgba(65,209,255,0.3);
color: #ffffff; color: var(--color-ink);
background: rgba(65,209,255,0.13); background: rgba(65,209,255,0.06);
} }
.branch-list { gap: 8px; } .branch-list { gap: 8px; }
@@ -2124,19 +2132,24 @@
.explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; } .explorer-head-actions { display: flex; align-items: center; gap: 5px; flex: 0 0 auto; }
.explorer-bulk-button { .explorer-bulk-button {
width: 26px; display: inline-grid;
min-width: 26px; place-items: center;
min-height: 26px; width: 28px;
min-width: 28px;
height: 28px;
min-height: 28px;
aspect-ratio: 1;
flex: 0 0 28px;
padding: 0; padding: 0;
border-color: rgba(65,209,255,0.2); border-color: rgba(94,110,156,0.18);
border-radius: 7px; border-radius: 6px;
color: var(--color-ink-dim); color: var(--color-ink-dim);
background: rgba(65,209,255,0.06); background: rgba(255,255,255,0.018);
} }
.explorer-bulk-button:hover:not(:disabled) { .explorer-bulk-button:hover:not(:disabled) {
border-color: rgba(65,209,255,0.45); border-color: rgba(65,209,255,0.3);
color: #ffffff; color: var(--color-ink);
background: rgba(65,209,255,0.13); background: rgba(65,209,255,0.06);
} }
.explorer-row { .explorer-row {
@@ -6036,21 +6049,49 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.sync-fields select, .remote-add input, .remote-edit input { width: 100%; height: 35px; border: 1px solid var(--color-border-input); border-radius: 7px; color: var(--color-ink); background: var(--app-input-bg); font-size: 11px; } .sync-fields select, .remote-add input, .remote-edit input { width: 100%; height: 35px; border: 1px solid var(--color-border-input); border-radius: 7px; color: var(--color-ink); background: var(--app-input-bg); font-size: 11px; }
.sync-fields select { padding: 0 9px; } .sync-fields select { padding: 0 9px; }
.remote-list { display: grid; gap: 5px; } .remote-list { display: grid; gap: 5px; }
.sync-action-error { display: grid; gap: 4px; margin-bottom: 5px; padding: 10px 11px; border: 1px solid rgba(232,96,96,.28); border-radius: 8px; color: #ef8888; background: rgba(232,96,96,.08); }
.sync-action-error strong { font-size: 11px; }
.sync-action-error span { font: 9.5px/1.45 var(--font-mono); overflow-wrap: anywhere; }
.sync-action-status { margin-bottom: 5px; padding: 9px 11px; border: 1px solid rgba(90,140,248,.28); border-radius: 8px; color: var(--color-accent); background: rgba(90,140,248,.08); font-size: 10.5px; font-weight: 700; }
.remote-row { display: grid; grid-template-columns: 28px minmax(0,1fr) auto auto; align-items: center; gap: 7px; min-height: 48px; padding: 6px 7px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--app-input-bg); } .remote-row { display: grid; grid-template-columns: 28px minmax(0,1fr) auto auto; align-items: center; gap: 7px; min-height: 48px; padding: 6px 7px; border: 1px solid var(--color-border-subtle); border-radius: 8px; background: var(--app-input-bg); }
.remote-mark { display: grid; place-items: center; color: var(--color-accent); } .remote-mark { display: grid; place-items: center; color: var(--color-accent); }
.remote-main { display: grid; gap: 3px; min-width: 0; padding: 0; border: 0; color: var(--color-ink); background: transparent; text-align: left; } .remote-main { display: grid; gap: 3px; min-width: 0; padding: 0; border: 0; color: var(--color-ink); background: transparent; text-align: left; }
.remote-main strong, .remote-edit strong { font-size: 11.5px; } .remote-main strong, .remote-edit strong { font-size: 11.5px; }
.remote-main span { overflow: hidden; color: var(--color-ink-faint); font: 10px var(--font-mono); text-overflow: ellipsis; white-space: nowrap; } .remote-main span { overflow: hidden; color: var(--color-ink-faint); font: 10px var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
.remote-edit { display: grid; grid-template-columns: 80px minmax(0,1fr); align-items: center; gap: 8px; } .remote-edit { display: grid; grid-template-columns: 80px minmax(0,1fr); align-items: center; gap: 8px; }
.remote-confirm { display: grid; gap: 3px; min-width: 0; }
.remote-confirm strong { color: var(--color-ink); font-size: 11px; }
.remote-confirm span { color: var(--color-ink-faint); font-size: 9.5px; }
.remote-edit input, .remote-add input { padding: 0 9px; } .remote-edit input, .remote-add input { padding: 0 9px; }
.remote-delete { display: grid; place-items: center; width: 30px; height: 30px; border: 0; border-radius: 6px; color: #e86060; background: transparent; } .remote-delete { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 8px; border: 0; border-radius: 6px; color: #e86060; background: transparent; font-size: 10px; font-weight: 750; }
.remote-delete:hover { background: rgba(235,87,87,.1); } .remote-delete:hover { background: rgba(235,87,87,.1); }
.btn-danger { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-height: 34px; padding: 0 11px; border: 1px solid rgba(232,96,96,.38); border-radius: 7px; color: #fff; background: #c84f4f; font-size: 10.5px; font-weight: 750; }
.btn-danger:hover:not(:disabled) { background: #dd5b5b; }
.remote-empty { margin: 4px 0 10px; color: var(--color-ink-faint); font-size: 11px; } .remote-empty { margin: 4px 0 10px; color: var(--color-ink-faint); font-size: 11px; }
.remote-add { display: grid; grid-template-columns: 120px minmax(180px,1fr) auto; gap: 7px; margin-top: 9px; padding-top: 10px; border-top: 1px solid var(--color-border-subtle); } .remote-add { display: grid; grid-template-columns: 120px minmax(180px,1fr) auto; gap: 7px; margin-top: 9px; padding-top: 10px; border-top: 1px solid var(--color-border-subtle); }
.sync-settings-footer { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 13px 20px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); } .sync-settings-footer { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 13px 20px; border-top: 1px solid var(--color-border-subtle); background: var(--app-dialog-chrome); }
.sync-settings-footer p { max-width: 440px; margin: 0; color: var(--color-ink-faint); font-size: 9.5px; line-height: 1.4; } .sync-settings-footer p { max-width: 440px; margin: 0; color: var(--color-ink-faint); font-size: 9.5px; line-height: 1.4; }
.sync-settings-footer > div { display: flex; gap: 8px; } .sync-settings-footer > 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; } } @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 { 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 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; } .ai-review-suggestion span { color: var(--color-ink-muted); font-size: 11.5px; line-height: 1.45; }
@@ -18,7 +18,8 @@
onClose = () => {}, onClose = () => {},
}: Props = $props(); }: 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(/\/(.+)/) : []);
</script> </script>
@@ -26,7 +27,7 @@
<div class="dialog branch-delete-dialog" role="dialog" aria-modal="true" aria-label={title}> <div class="dialog branch-delete-dialog" role="dialog" aria-modal="true" aria-label={title}>
<header class="dialog-header"> <header class="dialog-header">
<div> <div>
<span class="eyebrow">{force ? "Force delete" : "Delete branch"}</span> <span class="eyebrow">{branch.remote ? "Remote branch" : force ? "Force delete" : "Delete branch"}</span>
<p class="dialog-title">{title}</p> <p class="dialog-title">{title}</p>
</div> </div>
<button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close"> <button class="btn-sm dialog-close" type="button" onclick={onClose} disabled={isBusy} aria-label="Close">
@@ -41,7 +42,9 @@
<div class="discard-confirm-copy"> <div class="discard-confirm-copy">
<p> <p>
{#if force} {#if branch.remote}
Delete this branch from the remote server?
{:else if force}
This branch is not fully merged. Force deleting removes the branch pointer even if some commits are only reachable from this branch. This branch is not fully merged. Force deleting removes the branch pointer even if some commits are only reachable from this branch.
{:else} {:else}
Delete this local branch from the repository? Delete this local branch from the repository?
@@ -52,7 +55,9 @@
{branch.name} {branch.name}
</code> </code>
<p class="discard-warning-text"> <p class="discard-warning-text">
{#if force} {#if branch.remote}
This affects everyone using <strong>{remoteParts[0] || "the remote"}</strong>. Your local commits and local branches are kept.
{:else if force}
Make sure you no longer need the unique commits on this branch. Make sure you no longer need the unique commits on this branch.
{:else} {:else}
Git will refuse if the branch is not fully merged. Git will refuse if the branch is not fully merged.
@@ -69,7 +74,7 @@
{:else} {:else}
<Trash2 size={15} aria-hidden="true" /> <Trash2 size={15} aria-hidden="true" />
{/if} {/if}
{force ? "Force delete" : "Delete"} {branch.remote ? "Delete from remote" : force ? "Force delete" : "Delete"}
</button> </button>
</footer> </footer>
</div> </div>
+1 -1
View File
@@ -266,7 +266,7 @@
async function deleteContextBranch() { async function deleteContextBranch() {
const branch = contextBranch; const branch = contextBranch;
if (!branch || branch.current || branch.remote || isBusy) return; if (!branch || branch.current || isBusy) return;
closeBranchContextMenu(); closeBranchContextMenu();
if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch); if (branch.remote) await onDeleteRemoteBranch(branch); else await onDeleteBranch(branch);
} }
+29 -2
View File
@@ -23,12 +23,37 @@
let newUrl = ""; let newUrl = "";
let editingName = ""; let editingName = "";
let editingUrl = ""; let editingUrl = "";
let actionError = "";
let actionStatus = "";
$: de = language === "de"; $: 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 = ""; } function cancelEdit() { editingName = ""; editingUrl = ""; }
async function add() { if (!newName.trim() || !newUrl.trim()) return; await onAddRemote(newName.trim(), newUrl.trim()); newName = "origin"; newUrl = ""; } 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 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 = "";
}
}
</script> </script>
<div class="dialog-backdrop" role="presentation"> <div class="dialog-backdrop" role="presentation">
@@ -59,6 +84,8 @@
<section class="sync-settings-card"> <section class="sync-settings-card">
<div class="sync-card-heading"><div><h3>Remotes</h3><p>{de ? "Server-Verbindungen dieses Repositorys verwalten." : "Manage this repository's server connections."}</p></div><span class="count-pill">{remotes.length}</span></div> <div class="sync-card-heading"><div><h3>Remotes</h3><p>{de ? "Server-Verbindungen dieses Repositorys verwalten." : "Manage this repository's server connections."}</p></div><span class="count-pill">{remotes.length}</span></div>
<div class="remote-list"> <div class="remote-list">
{#if actionError}<div class="sync-action-error" role="alert"><strong>{de ? "Remote konnte nicht entfernt werden" : "Remote could not be removed"}</strong><span>{actionError}</span></div>{/if}
{#if actionStatus}<div class="sync-action-status" role="status">{actionStatus}</div>{/if}
{#each remotes as remote (remote.name)} {#each remotes as remote (remote.name)}
<div class="remote-row"> <div class="remote-row">
<span class="remote-mark"><GitBranch size={15} /></span> <span class="remote-mark"><GitBranch size={15} /></span>
@@ -68,7 +95,7 @@
<button class="btn-sm" type="button" onclick={cancelEdit} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button> <button class="btn-sm" type="button" onclick={cancelEdit} disabled={isBusy}>{de ? "Abbrechen" : "Cancel"}</button>
{:else} {:else}
<button class="remote-main" type="button" onclick={() => beginEdit(remote)} disabled={isBusy}><strong>{remote.name}</strong><span>{remote.fetch_url}</span></button> <button class="remote-main" type="button" onclick={() => beginEdit(remote)} disabled={isBusy}><strong>{remote.name}</strong><span>{remote.fetch_url}</span></button>
<button class="remote-delete" type="button" onclick={() => onRemoveRemote(remote.name)} disabled={isBusy} aria-label={`${de ? "Remote löschen" : "Remove remote"} ${remote.name}`}><Trash2 size={14} /></button> <button class="remote-delete" type="button" onclick={(event) => requestDelete(event, remote.name)} data-remote-name={remote.name} title={de ? "Remote-Verbindung sofort entfernen; lokale Daten bleiben erhalten" : "Remove remote connection now; local data is kept"}><Trash2 size={13} /><span>{de ? "Entfernen" : "Remove"}</span></button>
{/if} {/if}
</div> </div>
{:else}<p class="remote-empty">{de ? "Noch kein Remote eingerichtet." : "No remote configured yet."}</p>{/each} {:else}<p class="remote-empty">{de ? "Noch kein Remote eingerichtet." : "No remote configured yet."}</p>{/each}
+3 -1
View File
@@ -82,7 +82,9 @@ export function listBranches(path: string): Promise<GitBranch[]> {
export function listRemotes(path: string): Promise<GitRemote[]> { return invoke("list_remotes", { path }); } export function listRemotes(path: string): Promise<GitRemote[]> { return invoke("list_remotes", { path }); }
export function addRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("add_remote", { path, name, url }); } export function addRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("add_remote", { path, name, url }); }
export function updateRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("update_remote", { path, name, url }); } export function updateRemote(path: string, name: string, url: string): Promise<GitRemote[]> { return invoke("update_remote", { path, name, url }); }
export function removeRemote(path: string, name: string): Promise<GitRemote[]> { return invoke("remove_remote", { path, name }); } export function removeRemote(path: string, name: string): Promise<GitRemote[]> {
console.log("remove_remote")
return invoke("remove_remote", { path, name }); }
export function setBranchUpstream(path: string, branch: string, upstream?: string): Promise<GitStatus> { return invoke("set_branch_upstream", { path, branch, upstream: upstream || null }); } export function setBranchUpstream(path: string, branch: string, upstream?: string): Promise<GitStatus> { return invoke("set_branch_upstream", { path, branch, upstream: upstream || null }); }
export function deleteRemoteBranch(path: string, remote: string, branch: string): Promise<GitStatus> { return invoke("delete_remote_branch", { path, remote, branch }); } export function deleteRemoteBranch(path: string, remote: string, branch: string): Promise<GitStatus> { return invoke("delete_remote_branch", { path, remote, branch }); }