diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-22 00:51:00 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-22 00:51:00 +0300 |
| commit | 89ca3ba933eaf493234912972be8236c9c2493e5 (patch) | |
| tree | 508a0489cf3aa15f6d454a1c86697b0a7ba4ed72 /src | |
| parent | a5182282a490cfb60b22776257edc5ac0fce6adc (diff) | |
fix: check out existing branches for worktree create
refs #729
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/api/worktrees.rs | 75 | ||||
| -rw-r--r-- | src/app/api/worktrees/deferred.rs | 23 | ||||
| -rw-r--r-- | src/app/worktrees.rs | 50 | ||||
| -rw-r--r-- | src/worktree.rs | 80 |
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"); |
