git: Add git worktree picker (#38719)

Related discussions #26084 

Worktree creations are implemented similar to how branch creations are
handled on the branch picker (the user types a new name that's not on
the list and a new entry option appears to create a new branch with that
name).


https://github.com/user-attachments/assets/39e58983-740c-4a91-be88-57ef95aed85b

With this picker you have a few workflows: 

- Open the picker and type the name of a branch that's checked out on an
existing worktree:
    - Press enter to open the worktree on a new window
- Press ctrl-enter to open the worktree and replace the current window
- Open the picker and type the name of a new branch or an existing one
that's not checked out in another worktree:
- Press enter to create the worktree and open in a new window. If the
branch doesn't exists, we will create a new one based on the branch you
have currently checked out. If the branch does exists then we create a
worktree with that branch checked out.
- Press ctrl-enter to do everything on the previous point but instead,
replace the current window with the new worktre.
- Open the picker and type the name of a new branch or an existing one
that's not checked out in another worktree:
- If a default branch is detected on the repo, you can create a new
worktree based on that branch by pressing ctrl-enter or
ctrl-shift-enter. The first one will open a new window and the last one
will replace the current one.


Note: If you preffer to not use the system prompt for choosing a
directory, you can set `"use_system_path_prompts": false` in zed
settings.

Release Notes:

- Added git worktree picker to open a git worktree on a new window or
replace the current one
- Added git worktree creation action

---------

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Alvaro Parker
2025-11-03 21:38:00 -05:00
committed by GitHub
co-authored by Cole Miller
parent 57adf42492
commit e5660d25f1
16 changed files with 1059 additions and 7 deletions
+113
View File
@@ -72,6 +72,50 @@ impl Branch {
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Worktree {
pub path: PathBuf,
pub ref_name: SharedString,
pub sha: SharedString,
}
impl Worktree {
pub fn branch(&self) -> &str {
self.ref_name
.as_ref()
.strip_prefix("refs/heads/")
.or_else(|| self.ref_name.as_ref().strip_prefix("refs/remotes/"))
.unwrap_or(self.ref_name.as_ref())
}
}
pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> {
let mut worktrees = Vec::new();
let entries = raw_worktrees.as_ref().split("\n\n");
for entry in entries {
let mut parts = entry.splitn(3, '\n');
let path = parts
.next()
.and_then(|p| p.split_once(' ').map(|(_, path)| path.to_string()));
let sha = parts
.next()
.and_then(|p| p.split_once(' ').map(|(_, sha)| sha.to_string()));
let ref_name = parts
.next()
.and_then(|p| p.split_once(' ').map(|(_, ref_name)| ref_name.to_string()));
if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) {
worktrees.push(Worktree {
path: PathBuf::from(path),
ref_name: ref_name.into(),
sha: sha.into(),
})
}
}
worktrees
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Upstream {
pub ref_name: SharedString,
@@ -390,6 +434,15 @@ pub trait GitRepository: Send + Sync {
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn rename_branch(&self, branch: String, new_name: String) -> BoxFuture<'_, Result<()>>;
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>>;
fn create_worktree(
&self,
name: String,
directory: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>>;
fn reset(
&self,
commit: String,
@@ -1206,6 +1259,66 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
self.executor
.spawn(async move {
let output = new_smol_command(&git_binary_path)
.current_dir(working_directory?)
.args(&["--no-optional-locks", "worktree", "list", "--porcelain"])
.output()
.await?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(parse_worktrees_from_str(&stdout))
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree list failed: {stderr}");
}
})
.boxed()
}
fn create_worktree(
&self,
name: String,
directory: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
let final_path = directory.join(&name);
let mut args = vec![
OsString::from("--no-optional-locks"),
OsString::from("worktree"),
OsString::from("add"),
OsString::from(final_path.as_os_str()),
];
if let Some(from_commit) = from_commit {
args.extend([
OsString::from("-b"),
OsString::from(name.as_str()),
OsString::from(from_commit),
]);
}
self.executor
.spawn(async move {
let output = new_smol_command(&git_binary_path)
.current_dir(working_directory?)
.args(args)
.output()
.await?;
if output.status.success() {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree list failed: {stderr}");
}
})
.boxed()
}
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
let working_directory = self.working_directory();