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:
Generated
+1
@@ -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",
|
||||||
|
|||||||
@@ -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 = [] }
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
log::info!(target: "gitty::remote", "remove_remote invoked: path={path:?}, name={name:?}");
|
||||||
|
let result = (|| {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
|
log::info!(target: "gitty::remote", "remove_remote resolved repository: {}", repo.display());
|
||||||
let name = validate_remote_name(&repo, &name, true)?;
|
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()])?;
|
run_git(&repo, ["remote", "remove", name.as_str()])?;
|
||||||
|
log::info!(target: "gitty::remote", "remove_remote git command succeeded: {name}");
|
||||||
list_remotes(path)
|
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,6 +590,8 @@ pub fn delete_remote_branch(
|
|||||||
remote: String,
|
remote: String,
|
||||||
branch: String,
|
branch: String,
|
||||||
) -> Result<GitStatus, String> {
|
) -> Result<GitStatus, String> {
|
||||||
|
log::info!(target: "gitty::remote", "delete_remote_branch invoked: path={path:?}, remote={remote:?}, branch={branch:?}");
|
||||||
|
let result = (|| {
|
||||||
let repo = resolve_repo(&path)?;
|
let repo = resolve_repo(&path)?;
|
||||||
let remote = validate_remote_name(&repo, &remote, true)?;
|
let remote = validate_remote_name(&repo, &remote, true)?;
|
||||||
let branch = branch.trim();
|
let branch = branch.trim();
|
||||||
@@ -583,8 +599,15 @@ pub fn delete_remote_branch(
|
|||||||
return Err("Invalid remote branch name.".to_string());
|
return Err("Invalid remote branch name.".to_string());
|
||||||
}
|
}
|
||||||
run_git(&repo, ["check-ref-format", "--branch", branch])?;
|
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])?;
|
run_git(&repo, ["push", remote.as_str(), "--delete", branch])?;
|
||||||
|
log::info!(target: "gitty::remote", "remote branch deleted successfully: {remote}/{branch}");
|
||||||
status_for_repo(&repo)
|
status_for_repo(&repo)
|
||||||
|
})();
|
||||||
|
if let Err(error) = &result {
|
||||||
|
log::error!(target: "gitty::remote", "delete_remote_branch failed: {error}");
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
|||||||
+40
-9
@@ -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;
|
||||||
|
operation = `Removing remote ${name}`;
|
||||||
|
try {
|
||||||
syncSettingsRemotes = await removeRemote(activeRepoPath, name);
|
syncSettingsRemotes = await removeRemote(activeRepoPath, name);
|
||||||
if (selectedRemote === name) selectedRemote = "";
|
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);
|
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
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 }); }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user