Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
810 changes: 520 additions & 290 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src-tauri/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use refs::{
};
pub use types::*;
pub use worktree::{
branch_exists, create_worktree, create_worktree_from_pr, get_commits_since_base, get_head_sha,
get_parent_commit, list_worktrees, remove_worktree, reset_to_commit, update_branch_from_pr,
worktree_path_for, CommitInfo, UpdateFromPrResult,
branch_exists, create_worktree, create_worktree_for_existing_branch, create_worktree_from_pr,
get_commits_since_base, get_head_sha, get_parent_commit, list_worktrees, remove_worktree,
reset_to_commit, update_branch_from_pr, worktree_path_for, CommitInfo, UpdateFromPrResult,
};
36 changes: 36 additions & 0 deletions src-tauri/src/git/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,42 @@ pub fn create_worktree(
Ok(worktree_path)
}

/// Create a new worktree for an existing local branch.
///
/// Uses the standard worktree location and checks out `branch_name` there.
/// Fails if the worktree path already exists.
pub fn create_worktree_for_existing_branch(
repo: &Path,
branch_name: &str,
) -> Result<PathBuf, GitError> {
let worktree_path = worktree_path_for(repo, branch_name)?;

// Ensure parent directory exists
if let Some(parent) = worktree_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
GitError::CommandFailed(format!("Failed to create worktree directory: {e}"))
})?;
}

// Check if worktree already exists
if worktree_path.exists() {
return Err(GitError::CommandFailed(format!(
"Worktree already exists at {}",
worktree_path.display()
)));
}

let worktree_str = worktree_path
.to_str()
.ok_or_else(|| GitError::InvalidPath(worktree_path.display().to_string()))?;

// Create worktree for existing branch:
// git worktree add <path> <branch>
cli::run(repo, &["worktree", "add", worktree_str, branch_name])?;

Ok(worktree_path)
}

/// Remove a worktree and its associated branch.
///
/// Removes the worktree directory, git worktree reference, and the local git branch.
Expand Down
22 changes: 10 additions & 12 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1657,17 +1657,15 @@ async fn create_branch(
_ => git::detect_default_branch(repo).map_err(|e| e.to_string())?,
};

// Create the worktree (this will fail atomically if branch already exists)
let worktree_path =
git::create_worktree(repo, &branch_name, &base_branch).map_err(|e| {
// Provide user-friendly error for common case
let msg = e.to_string();
if msg.contains("already exists") {
format!("Branch '{branch_name}' already exists")
} else {
msg
}
})?;
// If branch already exists locally, set up a worktree for it instead of failing.
// Otherwise create both branch and worktree from the selected base.
let branch_exists = git::branch_exists(repo, &branch_name).map_err(|e| e.to_string())?;
let worktree_path = if branch_exists {
git::create_worktree_for_existing_branch(repo, &branch_name)
.map_err(|e| e.to_string())?
} else {
git::create_worktree(repo, &branch_name, &base_branch).map_err(|e| e.to_string())?
};

// Create the branch record
let branch = Branch::new(
Expand All @@ -1678,7 +1676,7 @@ async fn create_branch(
&base_branch,
);

// If DB insert fails, clean up the worktree
// If DB insert fails, clean up the worktree.
if let Err(e) = store.create_branch(&branch) {
let _ = git::remove_worktree(repo, &worktree_path); // Best-effort cleanup
return Err(e.to_string());
Expand Down
49 changes: 33 additions & 16 deletions src/lib/BranchHome.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
- Escape: Close modals
-->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { onMount, onDestroy, tick } from 'svelte';
import { Plus, Sparkles, Folder, GitBranch, Loader2, X, Trash2, Settings } from 'lucide-svelte';
import type { Branch, GitProject } from './services/branch';
import * as branchService from './services/branch';
Expand Down Expand Up @@ -52,7 +52,8 @@

// Modal state
let showNewBranchModal = $state(false);
let newBranchForProject = $state<GitProject | null>(null);
let newBranchProjectId = $state<string | null>(null);
let newBranchRepoPath = $state<string | null>(null);
let branchToDelete = $state<Branch | null>(null);
let showNewProjectModal = $state(false);
let showProjectSettings = $state(false);
Expand Down Expand Up @@ -193,10 +194,22 @@
}

function handleNewBranch(project?: GitProject) {
newBranchForProject = project || null;
newBranchProjectId = project?.id || null;
newBranchRepoPath = project?.repoPath || null;
showNewBranchModal = true;
}

async function closeNewBranchModal() {
// Hide first, then clear project on next tick.
// This avoids transient prop evaluation on a now-null project during teardown.
showNewBranchModal = false;
await tick();
if (!showNewBranchModal) {
newBranchProjectId = null;
newBranchRepoPath = null;
}
}

async function handleBranchCreating(pending: PendingBranch) {
// Ensure project exists in our local list
let project = projects.find((p) => p.id === pending.projectId);
Expand All @@ -221,16 +234,24 @@
newFailed.delete(key);
failedBranches = newFailed;
}
showNewBranchModal = false;
newBranchForProject = null;
await closeNewBranchModal();
}

function handleBranchCreated(branch: Branch) {
// Remove from pending and add to real branches
// Remove pending by repo+branch name so recovery paths still reconcile even if projectId differs.
pendingBranches = pendingBranches.filter(
(p) => !(p.projectId === branch.projectId && p.branchName === branch.branchName)
(p) => !(p.repoPath === branch.repoPath && p.branchName === branch.branchName)
);
branches = [...branches, branch];

// Upsert branch by id to avoid duplicate keys when recovering an existing DB row.
const existingIndex = branches.findIndex((b) => b.id === branch.id);
if (existingIndex >= 0) {
const next = [...branches];
next[existingIndex] = branch;
branches = next;
} else {
branches = [...branches, branch];
}
}

function handleBranchCreateFailed(pending: PendingBranch, errorMsg: string) {
Expand Down Expand Up @@ -355,8 +376,7 @@
showNewProjectModal = false;
} else if (showNewBranchModal) {
e.preventDefault();
showNewBranchModal = false;
newBranchForProject = null;
void closeNewBranchModal();
}
return;
}
Expand Down Expand Up @@ -563,15 +583,12 @@
<!-- New branch modal -->
{#if showNewBranchModal}
<NewBranchModal
initialRepoPath={newBranchForProject?.repoPath}
projectId={newBranchForProject?.id}
initialRepoPath={newBranchRepoPath ?? undefined}
projectId={newBranchProjectId ?? undefined}
onCreating={handleBranchCreating}
onCreated={handleBranchCreated}
onCreateFailed={handleBranchCreateFailed}
onClose={() => {
showNewBranchModal = false;
newBranchForProject = null;
}}
onClose={closeNewBranchModal}
/>
{/if}

Expand Down
1 change: 1 addition & 0 deletions src/lib/BranchTopBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
});
</script>

<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="top-bar drag-region" onpointerdown={startDrag}>
<div class="traffic-light-spacer drag-region" data-tauri-drag-region></div>

Expand Down
35 changes: 21 additions & 14 deletions src/lib/NewBranchModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@
async function handleSelectPR(pr: PullRequest) {
if (!selectedRepo || creatingFromPR) return;
creatingFromPR = true;
const createProjectId = projectId;
const createRepoPath = selectedRepo;

try {
const effectiveProjectId =
Expand All @@ -276,7 +278,7 @@
const baseBranch = `origin/${pr.base_ref}`;
const pending: PendingBranch = {
projectId: effectiveProjectId,
repoPath: selectedRepo,
repoPath: createRepoPath,
branchName: pr.head_ref,
baseBranch,
};
Expand All @@ -285,7 +287,7 @@

const branch = await branchService.createBranchFromPr(
effectiveProjectId,
selectedRepo,
createRepoPath,
pr.number,
pr.head_ref,
pr.base_ref
Expand All @@ -300,10 +302,10 @@
}, 100);
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
const effectiveProjectId = projectId || '';
const effectiveProjectId = createProjectId || '';
const pending: PendingBranch = {
projectId: effectiveProjectId,
repoPath: selectedRepo,
repoPath: createRepoPath,
branchName: pr.head_ref,
baseBranch: `origin/${pr.base_ref}`,
};
Expand Down Expand Up @@ -373,6 +375,11 @@

async function handleCreate() {
if (!selectedRepo || !branchName.trim()) return;
const createProjectId = projectId;
const createRepoPath = selectedRepo;
const createBranchName = branchName.trim();
const createBaseBranch = effectiveBaseBranch;
const createSelectedBaseBranch = selectedBaseBranch ?? undefined;

try {
// If no project ID was provided, get or create one for this repo
Expand All @@ -381,9 +388,9 @@

const pending: PendingBranch = {
projectId: effectiveProjectId,
repoPath: selectedRepo,
branchName: branchName.trim(),
baseBranch: effectiveBaseBranch,
repoPath: createRepoPath,
branchName: createBranchName,
baseBranch: createBaseBranch,
};

// Notify parent immediately so it can show a placeholder
Expand All @@ -392,9 +399,9 @@
// Pass selected base branch or undefined to use detected default
const branch = await branchService.createBranch(
effectiveProjectId,
selectedRepo,
branchName.trim(),
selectedBaseBranch ?? undefined
createRepoPath,
createBranchName,
createSelectedBaseBranch
);
onCreated(branch);

Expand All @@ -407,12 +414,12 @@
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
// Need to create pending for error case too
const effectiveProjectId = projectId || '';
const effectiveProjectId = createProjectId || '';
const pending: PendingBranch = {
projectId: effectiveProjectId,
repoPath: selectedRepo,
branchName: branchName.trim(),
baseBranch: effectiveBaseBranch,
repoPath: createRepoPath,
branchName: createBranchName,
baseBranch: createBaseBranch,
};
onCreateFailed(pending, errorMsg);
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/TabBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
});
</script>

<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="tab-bar drag-region" onpointerdown={startDrag}>
<div class="traffic-light-spacer drag-region" data-tauri-drag-region></div>
{#if onBack}
Expand Down