mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Start new git worktrees in detached HEAD state (#53840)
Create worktrees in detached HEAD state, then attempt to check out the requested branch. If the checkout fails (e.g. the branch is already in use by another worktree), log a warning and stay in detached HEAD state instead of showing an error to the user. This simplifies the worktree creation flow by: - Removing the occupied-branch fallback logic that would generate random branch names - Always using `CreateWorktreeTarget::Detached` for worktree creation - Attempting branch checkout as a best-effort post-creation step - Updating the branch picker UI to reflect the new behavior Release Notes: - Threads now start git worktrees in detached HEAD state if branch is in use or unspecified, instead of generating a random branch name. --------- Co-authored-by: Eric Holk <eric@zed.dev>
This commit is contained in:
parent
549db9fbb3
commit
66ac781e65
7 changed files with 249 additions and 180 deletions
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc,
|
||||
|
|
@ -755,9 +755,9 @@ impl StartThreadIn {
|
|||
fn branch_trigger_label(&self, project: &Project, cx: &App) -> Option<StartThreadInLabel> {
|
||||
match self {
|
||||
Self::NewWorktree { branch_target, .. } => {
|
||||
let (branch_name, is_occupied) = match branch_target {
|
||||
let label: SharedString = match branch_target {
|
||||
NewWorktreeBranchTarget::CurrentBranch => {
|
||||
let name: SharedString = if project.repositories(cx).len() > 1 {
|
||||
if project.repositories(cx).len() > 1 {
|
||||
"current branches".into()
|
||||
} else {
|
||||
project
|
||||
|
|
@ -769,49 +769,25 @@ impl StartThreadIn {
|
|||
.map(|branch| SharedString::from(branch.name().to_string()))
|
||||
})
|
||||
.unwrap_or_else(|| "HEAD".into())
|
||||
};
|
||||
(name, false)
|
||||
}
|
||||
NewWorktreeBranchTarget::ExistingBranch { name } => {
|
||||
let occupied = Self::is_branch_occupied(name, project, cx);
|
||||
(name.clone().into(), occupied)
|
||||
}
|
||||
}
|
||||
NewWorktreeBranchTarget::ExistingBranch { name } => name.clone().into(),
|
||||
NewWorktreeBranchTarget::CreateBranch {
|
||||
from_ref: Some(from_ref),
|
||||
..
|
||||
} => {
|
||||
let occupied = Self::is_branch_occupied(from_ref, project, cx);
|
||||
(from_ref.clone().into(), occupied)
|
||||
}
|
||||
NewWorktreeBranchTarget::CreateBranch { name, .. } => {
|
||||
(name.clone().into(), false)
|
||||
}
|
||||
};
|
||||
|
||||
let prefix = if is_occupied {
|
||||
Some("New From:".into())
|
||||
} else {
|
||||
None
|
||||
} => from_ref.clone().into(),
|
||||
NewWorktreeBranchTarget::CreateBranch { name, .. } => name.clone().into(),
|
||||
};
|
||||
|
||||
Some(StartThreadInLabel {
|
||||
prefix,
|
||||
label: branch_name,
|
||||
prefix: None,
|
||||
label,
|
||||
suffix: None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_branch_occupied(branch_name: &str, project: &Project, cx: &App) -> bool {
|
||||
project.repositories(cx).values().any(|repo| {
|
||||
repo.read(cx)
|
||||
.linked_worktrees
|
||||
.iter()
|
||||
.any(|wt| wt.branch_name() == Some(branch_name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -3147,28 +3123,15 @@ impl AgentPanel {
|
|||
|
||||
fn resolve_worktree_branch_target(
|
||||
branch_target: &NewWorktreeBranchTarget,
|
||||
existing_branches: &HashSet<String>,
|
||||
occupied_branches: &HashSet<String>,
|
||||
rng: &mut impl rand::Rng,
|
||||
) -> Result<(String, bool, Option<String>)> {
|
||||
let mut generate_branch_name = || {
|
||||
let refs: Vec<&str> = existing_branches.iter().map(|s| s.as_str()).collect();
|
||||
crate::branch_names::generate_branch_name(&refs, rng)
|
||||
.ok_or_else(|| anyhow!("Failed to generate a unique branch name"))
|
||||
};
|
||||
|
||||
) -> (Option<String>, Option<String>) {
|
||||
match branch_target {
|
||||
NewWorktreeBranchTarget::CreateBranch { name, from_ref } => {
|
||||
Ok((name.clone(), false, from_ref.clone()))
|
||||
}
|
||||
NewWorktreeBranchTarget::CurrentBranch => (None, None),
|
||||
NewWorktreeBranchTarget::ExistingBranch { name } => {
|
||||
if occupied_branches.contains(name) {
|
||||
Ok((generate_branch_name()?, false, Some(name.clone())))
|
||||
} else {
|
||||
Ok((name.clone(), true, None))
|
||||
}
|
||||
(Some(name.clone()), Some(name.clone()))
|
||||
}
|
||||
NewWorktreeBranchTarget::CreateBranch { name, from_ref } => {
|
||||
(Some(name.clone()), from_ref.clone())
|
||||
}
|
||||
NewWorktreeBranchTarget::CurrentBranch => Ok((generate_branch_name()?, false, None)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3183,9 +3146,7 @@ impl AgentPanel {
|
|||
worktree_name: Option<String>,
|
||||
existing_worktree_names: &[String],
|
||||
existing_worktree_paths: &HashSet<PathBuf>,
|
||||
branch_name: &str,
|
||||
use_existing_branch: bool,
|
||||
start_point: Option<String>,
|
||||
base_ref: Option<String>,
|
||||
worktree_directory_setting: &str,
|
||||
rng: &mut impl rand::Rng,
|
||||
cx: &mut Context<Self>,
|
||||
|
|
@ -3203,8 +3164,8 @@ impl AgentPanel {
|
|||
let worktree_name = worktree_name.unwrap_or_else(|| {
|
||||
let existing_refs: Vec<&str> =
|
||||
existing_worktree_names.iter().map(|s| s.as_str()).collect();
|
||||
crate::branch_names::generate_branch_name(&existing_refs, rng)
|
||||
.unwrap_or_else(|| branch_name.to_string())
|
||||
crate::worktree_names::generate_worktree_name(&existing_refs, rng)
|
||||
.unwrap_or_else(|| "worktree".to_string())
|
||||
});
|
||||
|
||||
for repo in git_repos {
|
||||
|
|
@ -3214,19 +3175,8 @@ impl AgentPanel {
|
|||
if existing_worktree_paths.contains(&new_path) {
|
||||
anyhow::bail!("A worktree already exists at {}", new_path.display());
|
||||
}
|
||||
let target = if use_existing_branch {
|
||||
debug_assert!(
|
||||
git_repos.len() == 1,
|
||||
"use_existing_branch should only be true for a single repo"
|
||||
);
|
||||
git::repository::CreateWorktreeTarget::ExistingBranch {
|
||||
branch_name: branch_name.to_string(),
|
||||
}
|
||||
} else {
|
||||
git::repository::CreateWorktreeTarget::NewBranch {
|
||||
branch_name: branch_name.to_string(),
|
||||
base_sha: start_point.clone(),
|
||||
}
|
||||
let target = git::repository::CreateWorktreeTarget::Detached {
|
||||
base_sha: base_ref.clone(),
|
||||
};
|
||||
let receiver = repo.create_worktree(target, new_path.clone());
|
||||
let work_dir = repo.work_directory_abs_path.clone();
|
||||
|
|
@ -3351,6 +3301,103 @@ impl AgentPanel {
|
|||
Err(anyhow!(error_message))
|
||||
}
|
||||
|
||||
/// Attempts to check out a branch in a newly created worktree.
|
||||
/// First tries checking out an existing branch, then tries creating a new
|
||||
/// branch. If both fail, the worktree stays in detached HEAD state.
|
||||
async fn try_checkout_branch_in_worktree(
|
||||
repo: &Entity<project::git_store::Repository>,
|
||||
branch_name: &str,
|
||||
worktree_path: &Path,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) {
|
||||
// First, try checking out the branch (it may already exist).
|
||||
let Ok(receiver) = cx.update(|_, cx| {
|
||||
repo.update(cx, |repo, _cx| {
|
||||
repo.checkout_branch_in_worktree(
|
||||
branch_name.to_string(),
|
||||
worktree_path.to_path_buf(),
|
||||
false,
|
||||
)
|
||||
})
|
||||
}) else {
|
||||
log::warn!(
|
||||
"Failed to check out branch {branch_name} for worktree at {}. \
|
||||
Staying in detached HEAD state.",
|
||||
worktree_path.display(),
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(result) = receiver.await else {
|
||||
log::warn!(
|
||||
"Branch checkout was canceled for worktree at {}. \
|
||||
Staying in detached HEAD state.",
|
||||
worktree_path.display()
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
log::info!(
|
||||
"Failed to check out branch '{branch_name}' in worktree at {}, \
|
||||
will try creating it: {err}",
|
||||
worktree_path.display()
|
||||
);
|
||||
} else {
|
||||
log::info!(
|
||||
"Checked out branch '{branch_name}' in worktree at {}",
|
||||
worktree_path.display()
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Checkout failed, so try creating the branch.
|
||||
let create_result = cx.update(|_, cx| {
|
||||
repo.update(cx, |repo, _cx| {
|
||||
repo.checkout_branch_in_worktree(
|
||||
branch_name.to_string(),
|
||||
worktree_path.to_path_buf(),
|
||||
true,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
match create_result {
|
||||
Ok(receiver) => match receiver.await {
|
||||
Ok(Ok(())) => {
|
||||
log::info!(
|
||||
"Created and checked out branch '{branch_name}' in worktree at {}",
|
||||
worktree_path.display()
|
||||
);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
log::warn!(
|
||||
"Failed to create branch '{branch_name}' in worktree at {}: {err}. \
|
||||
Staying in detached HEAD state.",
|
||||
worktree_path.display()
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!(
|
||||
"Branch creation was canceled for worktree at {}. \
|
||||
Staying in detached HEAD state.",
|
||||
worktree_path.display()
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Failed to dispatch branch creation for worktree at {}: {err}. \
|
||||
Staying in detached HEAD state.",
|
||||
worktree_path.display(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_worktree_creation_error(
|
||||
&mut self,
|
||||
message: SharedString,
|
||||
|
|
@ -3400,15 +3447,9 @@ impl AgentPanel {
|
|||
return;
|
||||
}
|
||||
|
||||
let (branch_receivers, worktree_receivers, worktree_directory_setting) =
|
||||
let (worktree_receivers, worktree_directory_setting) =
|
||||
if matches!(args, WorktreeCreationArgs::New { .. }) {
|
||||
(
|
||||
Some(
|
||||
git_repos
|
||||
.iter()
|
||||
.map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
Some(
|
||||
git_repos
|
||||
.iter()
|
||||
|
|
@ -3423,7 +3464,7 @@ impl AgentPanel {
|
|||
),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let active_file_path = self.workspace.upgrade().and_then(|workspace| {
|
||||
|
|
@ -3467,38 +3508,17 @@ impl AgentPanel {
|
|||
worktree_name,
|
||||
branch_target,
|
||||
} => {
|
||||
let branch_receivers = branch_receivers
|
||||
.expect("branch receivers must be prepared for new worktree creation");
|
||||
let worktree_receivers = worktree_receivers
|
||||
.expect("worktree receivers must be prepared for new worktree creation");
|
||||
let worktree_directory_setting = worktree_directory_setting
|
||||
.expect("worktree directory must be prepared for new worktree creation");
|
||||
|
||||
let mut existing_branches = HashSet::default();
|
||||
for result in futures::future::join_all(branch_receivers).await {
|
||||
match result {
|
||||
Ok(Ok(branches)) => {
|
||||
for branch in branches {
|
||||
existing_branches.insert(branch.name().to_string());
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
Err::<(), _>(err).log_err();
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut occupied_branches = HashSet::default();
|
||||
let mut existing_worktree_names = Vec::new();
|
||||
let mut existing_worktree_paths = HashSet::default();
|
||||
for result in futures::future::join_all(worktree_receivers).await {
|
||||
match result {
|
||||
Ok(Ok(worktrees)) => {
|
||||
for worktree in worktrees {
|
||||
if let Some(branch_name) = worktree.branch_name() {
|
||||
occupied_branches.insert(branch_name.to_string());
|
||||
}
|
||||
if let Some(name) = worktree
|
||||
.path
|
||||
.parent()
|
||||
|
|
@ -3519,25 +3539,8 @@ impl AgentPanel {
|
|||
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let (branch_name, use_existing_branch, start_point) =
|
||||
match Self::resolve_worktree_branch_target(
|
||||
&branch_target,
|
||||
&existing_branches,
|
||||
&occupied_branches,
|
||||
&mut rng,
|
||||
) {
|
||||
Ok(target) => target,
|
||||
Err(err) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_worktree_creation_error(
|
||||
err.to_string().into(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
};
|
||||
let (branch_to_checkout, base_ref) =
|
||||
Self::resolve_worktree_branch_target(&branch_target);
|
||||
|
||||
let (creation_infos, path_remapping) =
|
||||
match this.update_in(cx, |_this, _window, cx| {
|
||||
|
|
@ -3546,9 +3549,7 @@ impl AgentPanel {
|
|||
worktree_name,
|
||||
&existing_worktree_names,
|
||||
&existing_worktree_paths,
|
||||
&branch_name,
|
||||
use_existing_branch,
|
||||
start_point,
|
||||
base_ref,
|
||||
&worktree_directory_setting,
|
||||
&mut rng,
|
||||
cx,
|
||||
|
|
@ -3569,6 +3570,12 @@ impl AgentPanel {
|
|||
}
|
||||
};
|
||||
|
||||
let repo_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
|
||||
creation_infos
|
||||
.iter()
|
||||
.map(|(repo, path, _)| (repo.clone(), path.clone()))
|
||||
.collect();
|
||||
|
||||
let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
|
||||
|
||||
let created_paths =
|
||||
|
|
@ -3586,6 +3593,18 @@ impl AgentPanel {
|
|||
}
|
||||
};
|
||||
|
||||
if let Some(ref branch_name) = branch_to_checkout {
|
||||
for (repo, worktree_path) in &repo_paths {
|
||||
Self::try_checkout_branch_in_worktree(
|
||||
repo,
|
||||
branch_name,
|
||||
worktree_path,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_paths = created_paths;
|
||||
let has_non_git = !non_git_paths.is_empty();
|
||||
all_paths.extend(non_git_paths.iter().cloned());
|
||||
|
|
@ -5379,7 +5398,7 @@ mod tests {
|
|||
use fs::FakeFs;
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
use project::Project;
|
||||
use rand::rngs::StdRng;
|
||||
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
|
@ -6709,54 +6728,37 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_resolve_worktree_branch_target(mut rng: StdRng) {
|
||||
let existing_branches = HashSet::from_iter([
|
||||
"main".to_string(),
|
||||
"feature".to_string(),
|
||||
"origin/main".to_string(),
|
||||
]);
|
||||
|
||||
let resolved = AgentPanel::resolve_worktree_branch_target(
|
||||
&NewWorktreeBranchTarget::CreateBranch {
|
||||
#[gpui::test]
|
||||
fn test_resolve_worktree_branch_target() {
|
||||
let resolved =
|
||||
AgentPanel::resolve_worktree_branch_target(&NewWorktreeBranchTarget::CreateBranch {
|
||||
name: "new-branch".to_string(),
|
||||
from_ref: Some("main".to_string()),
|
||||
},
|
||||
&existing_branches,
|
||||
&HashSet::from_iter(["main".to_string()]),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
resolved,
|
||||
("new-branch".to_string(), false, Some("main".to_string()))
|
||||
(Some("new-branch".to_string()), Some("main".to_string()))
|
||||
);
|
||||
|
||||
let resolved = AgentPanel::resolve_worktree_branch_target(
|
||||
&NewWorktreeBranchTarget::ExistingBranch {
|
||||
name: "feature".to_string(),
|
||||
},
|
||||
&existing_branches,
|
||||
&HashSet::default(),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolved, ("feature".to_string(), true, None));
|
||||
let resolved =
|
||||
AgentPanel::resolve_worktree_branch_target(&NewWorktreeBranchTarget::CreateBranch {
|
||||
name: "new-branch".to_string(),
|
||||
from_ref: None,
|
||||
});
|
||||
assert_eq!(resolved, (Some("new-branch".to_string()), None));
|
||||
|
||||
let resolved = AgentPanel::resolve_worktree_branch_target(
|
||||
&NewWorktreeBranchTarget::ExistingBranch {
|
||||
name: "main".to_string(),
|
||||
},
|
||||
&existing_branches,
|
||||
&HashSet::from_iter(["main".to_string()]),
|
||||
&mut rng,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolved.1, false);
|
||||
assert_eq!(resolved.2, Some("main".to_string()));
|
||||
assert_ne!(resolved.0, "main");
|
||||
assert!(existing_branches.contains("main"));
|
||||
assert!(!existing_branches.contains(&resolved.0));
|
||||
let resolved =
|
||||
AgentPanel::resolve_worktree_branch_target(&NewWorktreeBranchTarget::ExistingBranch {
|
||||
name: "feature".to_string(),
|
||||
});
|
||||
assert_eq!(
|
||||
resolved,
|
||||
(Some("feature".to_string()), Some("feature".to_string()))
|
||||
);
|
||||
|
||||
let resolved =
|
||||
AgentPanel::resolve_worktree_branch_target(&NewWorktreeBranchTarget::CurrentBranch);
|
||||
assert_eq!(resolved, (None, None));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ mod agent_diff;
|
|||
mod agent_model_selector;
|
||||
mod agent_panel;
|
||||
mod agent_registry_ui;
|
||||
mod branch_names;
|
||||
mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod config_options;
|
||||
|
|
@ -37,6 +36,7 @@ pub mod thread_worktree_archive;
|
|||
mod thread_worktree_picker;
|
||||
pub mod threads_archive_view;
|
||||
mod ui;
|
||||
mod worktree_names;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
|
|
|||
|
|
@ -320,11 +320,9 @@ impl ThreadBranchPickerDelegate {
|
|||
fn branch_aside_text(&self, branch_name: &str, is_remote: bool) -> Option<SharedString> {
|
||||
if self.is_branch_occupied(branch_name) {
|
||||
Some(
|
||||
format!(
|
||||
"This branch is already checked out in another worktree. \
|
||||
A new branch will be created from {branch_name}."
|
||||
)
|
||||
.into(),
|
||||
"This branch is already checked out in another worktree. \
|
||||
The new worktree will start in detached HEAD state."
|
||||
.into(),
|
||||
)
|
||||
} else if is_remote {
|
||||
Some("A new local branch will be created from this remote branch.".into())
|
||||
|
|
@ -726,6 +724,7 @@ impl PickerDelegate for ThreadBranchPickerDelegate {
|
|||
.default_branch_name
|
||||
.as_ref()
|
||||
.filter(|name| *name != &self.current_branch_name)?;
|
||||
|
||||
let is_occupied = self.is_branch_occupied(default_branch_name);
|
||||
|
||||
let item = ListItem::new("default-branch")
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ const NOUNS: &[&str] = &[
|
|||
"vole", "walrus", "warbler", "willow", "wolf", "wren", "yew", "zenith",
|
||||
];
|
||||
|
||||
/// Generates a branch name in `"adjective-noun"` format (e.g. `"swift-falcon"`).
|
||||
/// Generates a worktree name in `"adjective-noun"` format (e.g. `"swift-falcon"`).
|
||||
///
|
||||
/// Tries up to 100 random combinations, skipping any name that already appears
|
||||
/// in `existing_branches`. Returns `None` if no unused name is found.
|
||||
pub fn generate_branch_name(existing_branches: &[&str], rng: &mut impl Rng) -> Option<String> {
|
||||
let existing: HashSet<&str> = existing_branches.iter().copied().collect();
|
||||
/// Tries up to 10 random combinations, skipping any name that already appears
|
||||
/// in `existing_names`. Returns `None` if no unused name is found.
|
||||
pub fn generate_worktree_name(existing_names: &[&str], rng: &mut impl Rng) -> Option<String> {
|
||||
let existing: HashSet<&str> = existing_names.iter().copied().collect();
|
||||
|
||||
for _ in 0..10 {
|
||||
let adjective = ADJECTIVES[rng.random_range(0..ADJECTIVES.len())];
|
||||
|
|
@ -80,8 +80,8 @@ mod tests {
|
|||
use rand::rngs::StdRng;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_generate_branch_name_format(mut rng: StdRng) {
|
||||
let name = generate_branch_name(&[], &mut rng).unwrap();
|
||||
fn test_generate_worktree_name_format(mut rng: StdRng) {
|
||||
let name = generate_worktree_name(&[], &mut rng).unwrap();
|
||||
let (adjective, noun) = name.split_once('-').expect("name should contain a hyphen");
|
||||
assert!(
|
||||
ADJECTIVES.contains(&adjective),
|
||||
|
|
@ -91,9 +91,9 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_generate_branch_name_avoids_existing(mut rng: StdRng) {
|
||||
fn test_generate_worktree_name_avoids_existing(mut rng: StdRng) {
|
||||
let existing = &["swift-falcon", "calm-river", "bold-cedar"];
|
||||
let name = generate_branch_name(existing, &mut rng).unwrap();
|
||||
let name = generate_worktree_name(existing, &mut rng).unwrap();
|
||||
for &branch in existing {
|
||||
assert_ne!(
|
||||
name, branch,
|
||||
|
|
@ -103,13 +103,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_generate_branch_name_returns_none_when_stuck(mut rng: StdRng) {
|
||||
fn test_generate_worktree_name_returns_none_when_stuck(mut rng: StdRng) {
|
||||
let all_names: Vec<String> = ADJECTIVES
|
||||
.iter()
|
||||
.flat_map(|adj| NOUNS.iter().map(move |noun| format!("{adj}-{noun}")))
|
||||
.collect();
|
||||
let refs: Vec<&str> = all_names.iter().map(|s| s.as_str()).collect();
|
||||
let result = generate_branch_name(&refs, &mut rng);
|
||||
let result = generate_worktree_name(&refs, &mut rng);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
|
|
@ -783,6 +783,15 @@ impl GitRepository for FakeGitRepository {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn checkout_branch_in_worktree(
|
||||
&self,
|
||||
_branch_name: String,
|
||||
_worktree_path: PathBuf,
|
||||
_create: bool,
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
async { Ok(()) }.boxed()
|
||||
}
|
||||
|
||||
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
self.with_state_async(true, |state| {
|
||||
state.current_branch_name = Some(name);
|
||||
|
|
|
|||
|
|
@ -757,6 +757,13 @@ pub trait GitRepository: Send + Sync {
|
|||
path: PathBuf,
|
||||
) -> BoxFuture<'_, Result<()>>;
|
||||
|
||||
fn checkout_branch_in_worktree(
|
||||
&self,
|
||||
branch_name: String,
|
||||
worktree_path: PathBuf,
|
||||
create: bool,
|
||||
) -> BoxFuture<'_, Result<()>>;
|
||||
|
||||
fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>>;
|
||||
|
||||
fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>>;
|
||||
|
|
@ -1799,6 +1806,32 @@ impl GitRepository for RealGitRepository {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn checkout_branch_in_worktree(
|
||||
&self,
|
||||
branch_name: String,
|
||||
worktree_path: PathBuf,
|
||||
create: bool,
|
||||
) -> BoxFuture<'_, Result<()>> {
|
||||
let git_binary = GitBinary::new(
|
||||
self.any_git_binary_path.clone(),
|
||||
worktree_path,
|
||||
self.path(),
|
||||
self.executor.clone(),
|
||||
self.is_trusted(),
|
||||
);
|
||||
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
if create {
|
||||
git_binary.run(&["checkout", "-b", &branch_name]).await?;
|
||||
} else {
|
||||
git_binary.run(&["checkout", &branch_name]).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
|
||||
let repo = self.repository.clone();
|
||||
let git_binary = self.git_binary();
|
||||
|
|
|
|||
|
|
@ -6098,6 +6098,32 @@ impl Repository {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn checkout_branch_in_worktree(
|
||||
&mut self,
|
||||
branch_name: String,
|
||||
worktree_path: PathBuf,
|
||||
create: bool,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let description = if create {
|
||||
format!("git checkout -b {branch_name}")
|
||||
} else {
|
||||
format!("git checkout {branch_name}")
|
||||
};
|
||||
self.send_job(Some(description.into()), move |repo, _cx| async move {
|
||||
match repo {
|
||||
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
||||
backend
|
||||
.checkout_branch_in_worktree(branch_name, worktree_path, create)
|
||||
.await
|
||||
}
|
||||
RepositoryState::Remote(_) => {
|
||||
log::warn!("checkout_branch_in_worktree not supported for remote repositories");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn head_sha(&mut self) -> oneshot::Receiver<Result<Option<String>>> {
|
||||
let id = self.id;
|
||||
self.send_job(None, move |repo, _cx| async move {
|
||||
|
|
|
|||
Loading…
Reference in a new issue