feat(git): rename remote branches atomically via push

Adds a new command to rename remote branches and update tracking refs.
Renaming remote refs is performed atomically using a single push.
The push creates the new remote ref and deletes the old if it succeeds.
The frontend now invokes remote-rename when needed and shows labels.

- Atomic remote rename via push with create/ref and delete
- Frontend supports remote branch renames from the branch panel
- Compare UI now shows labels for remote refs in results
This commit is contained in:
Christoph Brandau
2026-08-13 18:32:16 +02:00
parent ca14fac90c
commit f43fe00873
11 changed files with 447 additions and 71 deletions
+39 -14
View File
@@ -18,14 +18,29 @@
let name = $state("");
function remoteName(branch: GitBranchInfo): string {
if (!branch.remote) return "";
const slash = branch.name.indexOf("/");
return slash > 0 ? branch.name.slice(0, slash) : branch.name;
}
function editableName(branch: GitBranchInfo): string {
if (!branch.remote) return branch.name;
const slash = branch.name.indexOf("/");
return slash >= 0 ? branch.name.slice(slash + 1) : branch.name;
}
let originalName = $derived(editableName(branch));
let remote = $derived(remoteName(branch));
$effect(() => {
name = branch.name;
name = originalName;
});
function submit(event: SubmitEvent) {
event.preventDefault();
const value = name.trim();
if (!value || value === branch.name) return;
if (!value || value === originalName) return;
onRename(value);
}
</script>
@@ -34,10 +49,10 @@
class="dialog-backdrop"
role="presentation"
>
<div class="dialog rename-branch-dialog" role="dialog" aria-modal="true" aria-label="Rename branch" tabindex="-1">
<div class="dialog rename-branch-dialog" role="dialog" aria-modal="true" aria-label={branch.remote ? "Rename remote branch" : "Rename branch"} tabindex="-1">
<header class="dialog-header">
<div>
<span class="eyebrow">Rename branch</span>
<span class="eyebrow">{branch.remote ? "Rename remote branch" : "Rename branch"}</span>
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">{branch.name}</h2>
</div>
<button class="dialog-close" type="button" onclick={onClose} title="Close">
@@ -48,27 +63,37 @@
<form class="rename-branch-form" onsubmit={submit}>
<label class="new-branch-field">
<span>Branch name</span>
<!-- svelte-ignore a11y_autofocus -->
<input
bind:value={name}
autocomplete="off"
spellcheck="false"
disabled={isBusy}
autofocus
/>
<div class:remote-branch-name-field={branch.remote}>
{#if branch.remote}<strong>{remote}/</strong>{/if}
<!-- svelte-ignore a11y_autofocus -->
<input
bind:value={name}
autocomplete="off"
spellcheck="false"
disabled={isBusy}
autofocus
/>
</div>
</label>
{#if branch.remote}
<p class="rename-remote-note">
Gitty creates <strong>{remote}/{name.trim() || "new-name"}</strong> and removes
<strong>{branch.name}</strong> in one atomic push. The operation stops if the remote changed in the meantime.
</p>
{/if}
<div class="new-branch-actions">
<button class="btn-secondary" type="button" onclick={onClose} disabled={isBusy}>
Cancel
</button>
<button class="btn-primary" type="submit" disabled={isBusy || name.trim().length === 0 || name.trim() === branch.name}>
<button class="btn-primary" type="submit" disabled={isBusy || name.trim().length === 0 || name.trim() === originalName}>
{#if isBusy}
<LoaderCircle class="spin" size={16} aria-hidden="true" />
{:else}
<GitBranch size={16} aria-hidden="true" />
{/if}
Rename
{branch.remote ? "Rename on remote" : "Rename"}
</button>
</div>
</form>