aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-22 00:51:00 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-22 00:51:00 +0300
commit89ca3ba933eaf493234912972be8236c9c2493e5 (patch)
tree508a0489cf3aa15f6d454a1c86697b0a7ba4ed72 /src
parenta5182282a490cfb60b22776257edc5ac0fce6adc (diff)
fix: check out existing branches for worktree create
refs #729
Diffstat (limited to 'src')
-rw-r--r--src/app/api/worktrees.rs75
-rw-r--r--src/app/api/worktrees/deferred.rs23
-rw-r--r--src/app/worktrees.rs50
-rw-r--r--src/worktree.rs80
4 files changed, 191 insertions, 37 deletions
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs
index 62a46fdb..111d50cf 100644
--- a/src/app/api/worktrees.rs
+++ b/src/app/api/worktrees.rs
@@ -1038,12 +1038,77 @@ mod tests {
let _ = std::fs::remove_dir_all(plugin_root);
}
+ #[tokio::test]
+ async fn deferred_api_worktree_create_checks_out_existing_branch() {
+ let repo = create_committed_repo("api-worktree-create-existing-branch-repo");
+ let worktree_root = unique_temp_path("api-worktree-create-existing-branch-root");
+ let branch = "foo";
+ run_git(&repo, &["branch", branch]);
+ let mut app = test_app();
+ let mut parent = Workspace::test_new("main");
+ parent.identity_cwd = repo.clone();
+ let parent_id = parent.id.clone();
+ app.state.workspaces = vec![parent];
+ app.state.ensure_test_terminals();
+ app.state.worktree_directory = worktree_root.clone();
+ let request = || Request {
+ id: "req".into(),
+ method: crate::api::schema::Method::WorktreeCreate(WorktreeCreateParams {
+ workspace_id: Some(parent_id.clone()),
+ branch: Some(branch.into()),
+ ..WorktreeCreateParams::default()
+ }),
+ };
+
+ let (response_tx, response_rx) = response_channel();
+ assert!(app.handle_deferred_worktree_api_request(request(), response_tx));
+ let event = wait_for_app_event(&mut app);
+ app.handle_internal_event(event);
+ let response = response_rx
+ .recv_timeout(std::time::Duration::from_secs(2))
+ .expect("create should respond");
+ let success: SuccessResponse = serde_json::from_str(&response).unwrap();
+ let ResponseResult::WorktreeCreated { worktree, .. } = success.result else {
+ panic!("expected worktree_created response");
+ };
+ assert_eq!(worktree.branch.as_deref(), Some(branch));
+ let checkout = Path::new(&worktree.path);
+ assert!(checkout.join("README.md").exists());
+ let branch_name = std::process::Command::new("git")
+ .arg("-C")
+ .arg(checkout)
+ .args(["branch", "--show-current"])
+ .output()
+ .unwrap();
+ assert!(branch_name.status.success());
+ assert_eq!(
+ String::from_utf8(branch_name.stdout).unwrap().trim(),
+ branch
+ );
+ assert!(app.pending_api_worktree_creates.is_empty());
+
+ for (_, runtime) in app.terminal_runtimes.drain() {
+ runtime.shutdown();
+ }
+ let _ = std::fs::remove_dir_all(worktree_root);
+ let _ = std::fs::remove_dir_all(repo);
+ }
+
#[test]
fn deferred_api_worktree_create_failure_clears_pending_checkout() {
let repo = create_committed_repo("api-worktree-create-failure-repo");
let worktree_root = unique_temp_path("api-worktree-create-failure-root");
- let branch = "foo";
- run_git(&repo, &["branch", branch]);
+ let branch_name = std::process::Command::new("git")
+ .arg("-C")
+ .arg(&repo)
+ .args(["branch", "--show-current"])
+ .output()
+ .unwrap();
+ assert!(branch_name.status.success());
+ let branch = String::from_utf8(branch_name.stdout)
+ .unwrap()
+ .trim()
+ .to_string();
let mut app = test_app();
let mut parent = Workspace::test_new("main");
parent.identity_cwd = repo.clone();
@@ -1055,7 +1120,7 @@ mod tests {
id: "req".into(),
method: crate::api::schema::Method::WorktreeCreate(WorktreeCreateParams {
workspace_id: Some(parent_id.clone()),
- branch: Some(branch.into()),
+ branch: Some(branch.clone()),
..WorktreeCreateParams::default()
}),
};
@@ -1069,10 +1134,6 @@ mod tests {
.expect("failed create should respond");
let error: ErrorResponse = serde_json::from_str(&response).unwrap();
assert_eq!(error.error.code, "worktree_create_failed");
- assert!(error
- .error
- .message
- .contains("fatal: a branch named 'foo' already exists"));
assert!(app.pending_api_worktree_creates.is_empty());
let (second_tx, second_rx) = response_channel();
diff --git a/src/app/api/worktrees/deferred.rs b/src/app/api/worktrees/deferred.rs
index 19794a6e..f63a47fd 100644
--- a/src/app/api/worktrees/deferred.rs
+++ b/src/app/api/worktrees/deferred.rs
@@ -159,12 +159,6 @@ impl App {
self.pending_api_worktree_creates
.insert(checkout_key.clone(), operation_id);
- let command = crate::worktree::build_worktree_add_new_branch_command(
- &source.source_checkout_path,
- &checkout_path,
- &branch,
- &base,
- );
let parent_dir = checkout_path.parent().map(Path::to_path_buf);
let source_workspace_id = source
.workspace_idx
@@ -191,15 +185,22 @@ impl App {
respond_to,
};
let path = checkout_path;
+ let source_checkout_path = api_request.source_checkout_path.clone();
let event_tx = self.event_tx.clone();
std::thread::spawn(move || {
let result = if let Some(parent_dir) = parent_dir {
- std::fs::create_dir_all(&parent_dir)
- .map_err(|err| err.to_string())
- .and_then(|()| crate::worktree::run_worktree_command(&command))
+ std::fs::create_dir_all(&parent_dir).map_err(|err| err.to_string())
} else {
- crate::worktree::run_worktree_command(&command)
- };
+ Ok(())
+ }
+ .and_then(|()| {
+ crate::worktree::run_worktree_add_command(
+ &source_checkout_path,
+ &path,
+ &branch,
+ &base,
+ )
+ });
let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new(
crate::events::WorktreeAddResult {
path,
diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs
index e1c30a77..12e7ae84 100644
--- a/src/app/worktrees.rs
+++ b/src/app/worktrees.rs
@@ -521,12 +521,6 @@ impl App {
create.creating = true;
create.error = None;
- let command = crate::worktree::build_worktree_add_new_branch_command(
- &create.source_checkout_path,
- &create.checkout_path,
- &create.branch,
- "HEAD",
- );
let parent_dir = create
.checkout_path
.parent()
@@ -538,15 +532,23 @@ impl App {
"starting git worktree add"
);
let path = create.checkout_path.clone();
+ let source_checkout_path = create.source_checkout_path.clone();
+ let branch = create.branch.clone();
let event_tx = self.event_tx.clone();
std::thread::spawn(move || {
let result = if let Some(parent_dir) = parent_dir {
- std::fs::create_dir_all(&parent_dir)
- .map_err(|err| err.to_string())
- .and_then(|()| crate::worktree::run_worktree_command(&command))
+ std::fs::create_dir_all(&parent_dir).map_err(|err| err.to_string())
} else {
- crate::worktree::run_worktree_command(&command)
- };
+ Ok(())
+ }
+ .and_then(|()| {
+ crate::worktree::run_worktree_add_command(
+ &source_checkout_path,
+ &path,
+ &branch,
+ "HEAD",
+ )
+ });
let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new(
WorktreeAddResult {
path,
@@ -1632,7 +1634,7 @@ mod tests {
}
#[test]
- fn start_worktree_add_existing_branch_clears_creating_and_shows_fatal_error() {
+ fn start_worktree_add_existing_branch_checks_out_branch() {
let repo = create_committed_repo("app-worktree-add-existing-branch-repo");
let worktree_root = unique_temp_path("app-worktree-add-existing-branch-root");
let branch = "foo";
@@ -1664,18 +1666,28 @@ mod tests {
let event = wait_for_worktree_event(&mut app);
match event {
AppEvent::WorktreeAddFinished(result) => {
- app.handle_worktree_add_finished(*result);
+ let result = *result;
+ assert_eq!(result.path, checkout);
+ assert_eq!(result.result, Ok(()));
}
other => panic!("unexpected event: {other:?}"),
}
- let create = app.state.worktree_create.as_ref().unwrap();
- assert!(!create.creating);
- let error = create.error.as_deref().unwrap();
- assert!(error.contains("Preparing worktree"));
- assert!(error.contains("fatal: a branch named 'foo' already exists"));
- assert!(!checkout.exists());
+ assert!(checkout.join("README.md").exists());
+ let branch_name = std::process::Command::new("git")
+ .arg("-C")
+ .arg(&checkout)
+ .args(["branch", "--show-current"])
+ .output()
+ .unwrap();
+ assert!(branch_name.status.success());
+ assert_eq!(
+ String::from_utf8(branch_name.stdout).unwrap().trim(),
+ branch
+ );
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
}
diff --git a/src/worktree.rs b/src/worktree.rs
index f2f1dffe..33b03d56 100644
--- a/src/worktree.rs
+++ b/src/worktree.rs
@@ -246,6 +246,65 @@ pub(crate) fn build_worktree_add_new_branch_command(
}
}
+pub(crate) fn build_worktree_add_existing_branch_command(
+ repo_root: &Path,
+ path: &Path,
+ branch: &str,
+) -> WorktreeCommand {
+ WorktreeCommand {
+ program: "git".to_string(),
+ args: vec![
+ "-C".to_string(),
+ repo_root.display().to_string(),
+ "worktree".to_string(),
+ "add".to_string(),
+ path.display().to_string(),
+ branch.to_string(),
+ ],
+ }
+}
+
+pub(crate) fn local_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, String> {
+ let output = std::process::Command::new("git")
+ .arg("-C")
+ .arg(repo_root)
+ .args(["show-ref", "--verify", "--quiet"])
+ .arg(format!("refs/heads/{branch}"))
+ .output()
+ .map_err(|err| err.to_string())?;
+
+ if output.status.success() {
+ return Ok(true);
+ }
+ if output.status.code() == Some(1) {
+ return Ok(false);
+ }
+
+ let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
+ let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !stderr.is_empty() {
+ Err(stderr)
+ } else if !stdout.is_empty() {
+ Err(stdout)
+ } else {
+ Err(format!("git show-ref failed with status {}", output.status))
+ }
+}
+
+pub(crate) fn run_worktree_add_command(
+ repo_root: &Path,
+ path: &Path,
+ branch: &str,
+ base: &str,
+) -> Result<(), String> {
+ let command = if local_branch_exists(repo_root, branch)? {
+ build_worktree_add_existing_branch_command(repo_root, path, branch)
+ } else {
+ build_worktree_add_new_branch_command(repo_root, path, branch, base)
+ };
+ run_worktree_command(&command)
+}
+
pub(crate) fn run_worktree_command(command: &WorktreeCommand) -> Result<(), String> {
let output = std::process::Command::new(&command.program)
.args(&command.args)
@@ -756,6 +815,27 @@ prunable stale
}
#[test]
+ fn worktree_add_command_checks_out_existing_branch() {
+ let command = build_worktree_add_existing_branch_command(
+ Path::new("/repo/herdr"),
+ Path::new("/w/herdr/worktree-brave-river"),
+ "worktree/brave-river",
+ );
+ assert_eq!(command.program, "git");
+ assert_eq!(
+ command.args,
+ vec![
+ "-C",
+ "/repo/herdr",
+ "worktree",
+ "add",
+ "/w/herdr/worktree-brave-river",
+ "worktree/brave-river"
+ ]
+ );
+ }
+
+ #[test]
fn run_worktree_add_and_remove_create_and_delete_checkout() {
let repo = create_committed_repo("worktree-run-repo");
let checkout = unique_temp_path("worktree-run-checkout");