Add git worktree remove/rename API (#49135)

Add `remove_worktree()` and `rename_worktree()` to the `GitRepository`
trait with `RealGitRepository` implementations that shell out to `git
worktree remove/move`.

Implement all 4 worktree methods (`worktrees`, `create_worktree`,
`remove_worktree`, `rename_worktree`) on `FakeGitRepository` backed by
`FakeGitRepositoryState`, with `simulated_create_worktree_error` for
test-time fault injection.

Add `set_create_worktree_error()` helper on `FakeFs`.

Add `parse_worktrees_from_str` helper and 7 new tests covering real git
operations and fake worktree lifecycle.

Closes AI-31

Release Notes:

- N/A
This commit is contained in:
Richard Feldman authored and GitHub committed 2026-02-17 12:09:44 -05:00
1 parent 827c56f43a
commit f59baa942a
3 files changed
+679 -23

No files matched your search

+256 -6
View File
@@ -1,4 +1,4 @@
use crate::{FakeFs, FakeFsEntry, Fs};
use crate::{FakeFs, FakeFsEntry, Fs, RemoveOptions, RenameOptions};
use anyhow::{Context as _, Result, bail};
use collections::{HashMap, HashSet};
use futures::future::{self, BoxFuture, join_all};
@@ -49,8 +49,10 @@ pub struct FakeGitRepositoryState {
/// List of remotes, keys are names and values are URLs
pub remotes: HashMap<String, String>,
pub simulated_index_write_error_message: Option<String>,
pub simulated_create_worktree_error: Option<String>,
pub refs: HashMap<String, String>,
pub graph_commits: Vec<Arc<InitialGraphCommitData>>,
pub worktrees: Vec<Worktree>,
}
impl FakeGitRepositoryState {
@@ -64,11 +66,13 @@ impl FakeGitRepositoryState {
current_branch_name: Default::default(),
branches: Default::default(),
simulated_index_write_error_message: Default::default(),
simulated_create_worktree_error: Default::default(),
refs: HashMap::from_iter([("HEAD".into(), "abc".into())]),
merge_base_contents: Default::default(),
oids: Default::default(),
remotes: HashMap::default(),
graph_commits: Vec::new(),
worktrees: Vec::new(),
}
}
}
@@ -402,16 +406,129 @@ impl GitRepository for FakeGitRepository {
}
fn worktrees(&self) -> BoxFuture<'_, Result<Vec<Worktree>>> {
unimplemented!()
self.with_state_async(false, |state| Ok(state.worktrees.clone()))
}
fn create_worktree(
&self,
_: String,
_: PathBuf,
_: Option<String>,
name: String,
directory: PathBuf,
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
let fs = self.fs.clone();
let executor = self.executor.clone();
let dot_git_path = self.dot_git_path.clone();
async move {
let path = directory.join(&name);
executor.simulate_random_delay().await;
// Check for simulated error before any side effects
fs.with_git_state(&dot_git_path, false, |state| {
if let Some(message) = &state.simulated_create_worktree_error {
anyhow::bail!("{message}");
}
Ok(())
})??;
// Create directory before updating state so state is never
// inconsistent with the filesystem
fs.create_dir(&path).await?;
fs.with_git_state(&dot_git_path, true, {
let path = path.clone();
move |state| {
if state.branches.contains(&name) {
bail!("a branch named '{}' already exists", name);
}
let ref_name = format!("refs/heads/{name}");
let sha = from_commit.unwrap_or_else(|| "fake-sha".to_string());
state.refs.insert(ref_name.clone(), sha.clone());
state.worktrees.push(Worktree {
path,
ref_name: ref_name.into(),
sha: sha.into(),
});
state.branches.insert(name);
Ok::<(), anyhow::Error>(())
}
})??;
Ok(())
}
.boxed()
}
fn remove_worktree(&self, path: PathBuf, _force: bool) -> BoxFuture<'_, Result<()>> {
let fs = self.fs.clone();
let executor = self.executor.clone();
let dot_git_path = self.dot_git_path.clone();
async move {
executor.simulate_random_delay().await;
// Validate the worktree exists in state before touching the filesystem
fs.with_git_state(&dot_git_path, false, {
let path = path.clone();
move |state| {
if !state.worktrees.iter().any(|w| w.path == path) {
bail!("no worktree found at path: {}", path.display());
}
Ok(())
}
})??;
// Now remove the directory
fs.remove_dir(
&path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await?;
// Update state
fs.with_git_state(&dot_git_path, true, move |state| {
state.worktrees.retain(|worktree| worktree.path != path);
Ok::<(), anyhow::Error>(())
})??;
Ok(())
}
.boxed()
}
fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
let fs = self.fs.clone();
let executor = self.executor.clone();
let dot_git_path = self.dot_git_path.clone();
async move {
executor.simulate_random_delay().await;
// Validate the worktree exists in state before touching the filesystem
fs.with_git_state(&dot_git_path, false, {
let old_path = old_path.clone();
move |state| {
if !state.worktrees.iter().any(|w| w.path == old_path) {
bail!("no worktree found at path: {}", old_path.display());
}
Ok(())
}
})??;
// Now move the directory
fs.rename(
&old_path,
&new_path,
RenameOptions {
overwrite: false,
ignore_if_exists: false,
create_parents: true,
},
)
.await?;
// Update state
fs.with_git_state(&dot_git_path, true, move |state| {
let worktree = state
.worktrees
.iter_mut()
.find(|worktree| worktree.path == old_path)
.expect("worktree was validated above");
worktree.path = new_path;
Ok::<(), anyhow::Error>(())
})??;
Ok(())
}
.boxed()
}
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
@@ -765,3 +882,136 @@ impl GitRepository for FakeGitRepository {
anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FakeFs, Fs};
use gpui::TestAppContext;
use serde_json::json;
use std::path::Path;
#[gpui::test]
async fn test_fake_worktree_lifecycle(cx: &mut TestAppContext) {
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({".git": {}, "file.txt": "content"}))
.await;
let repo = fs
.open_repo(Path::new("/project/.git"), None)
.expect("should open fake repo");
// Initially no worktrees
let worktrees = repo.worktrees().await.unwrap();
assert!(worktrees.is_empty());
// Create a worktree
repo.create_worktree(
"feature-branch".to_string(),
PathBuf::from("/worktrees"),
Some("abc123".to_string()),
)
.await
.unwrap();
// List worktrees — should have one
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 1);
assert_eq!(worktrees[0].path, Path::new("/worktrees/feature-branch"));
assert_eq!(worktrees[0].ref_name.as_ref(), "refs/heads/feature-branch");
assert_eq!(worktrees[0].sha.as_ref(), "abc123");
// Directory should exist in FakeFs after create
assert!(
fs.is_dir(Path::new("/worktrees/feature-branch")).await,
"worktree directory should be created in FakeFs"
);
// Create a second worktree (without explicit commit)
repo.create_worktree(
"bugfix-branch".to_string(),
PathBuf::from("/worktrees"),
None,
)
.await
.unwrap();
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 2);
assert!(
fs.is_dir(Path::new("/worktrees/bugfix-branch")).await,
"second worktree directory should be created in FakeFs"
);
// Rename the first worktree
repo.rename_worktree(
PathBuf::from("/worktrees/feature-branch"),
PathBuf::from("/worktrees/renamed-branch"),
)
.await
.unwrap();
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 2);
assert!(
worktrees
.iter()
.any(|w| w.path == Path::new("/worktrees/renamed-branch")),
"renamed worktree should exist at new path"
);
assert!(
worktrees
.iter()
.all(|w| w.path != Path::new("/worktrees/feature-branch")),
"old path should no longer exist"
);
// Directory should be moved in FakeFs after rename
assert!(
!fs.is_dir(Path::new("/worktrees/feature-branch")).await,
"old worktree directory should not exist after rename"
);
assert!(
fs.is_dir(Path::new("/worktrees/renamed-branch")).await,
"new worktree directory should exist after rename"
);
// Rename a nonexistent worktree should fail
let result = repo
.rename_worktree(PathBuf::from("/nonexistent"), PathBuf::from("/somewhere"))
.await;
assert!(result.is_err());
// Remove a worktree
repo.remove_worktree(PathBuf::from("/worktrees/renamed-branch"), false)
.await
.unwrap();
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 1);
assert_eq!(worktrees[0].path, Path::new("/worktrees/bugfix-branch"));
// Directory should be removed from FakeFs after remove
assert!(
!fs.is_dir(Path::new("/worktrees/renamed-branch")).await,
"worktree directory should be removed from FakeFs"
);
// Remove a nonexistent worktree should fail
let result = repo
.remove_worktree(PathBuf::from("/nonexistent"), false)
.await;
assert!(result.is_err());
// Remove the last worktree
repo.remove_worktree(PathBuf::from("/worktrees/bugfix-branch"), false)
.await
.unwrap();
let worktrees = repo.worktrees().await.unwrap();
assert!(worktrees.is_empty());
assert!(
!fs.is_dir(Path::new("/worktrees/bugfix-branch")).await,
"last worktree directory should be removed from FakeFs"
);
}
}
+7
View File
@@ -2069,6 +2069,13 @@ impl FakeFs {
.unwrap();
}
pub fn set_create_worktree_error(&self, dot_git: &Path, message: Option<String>) {
self.with_git_state(dot_git, true, |state| {
state.simulated_create_worktree_error = message;
})
.unwrap();
}
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
let mut result = Vec::new();
let mut queue = collections::VecDeque::new();
+416 -17
View File
@@ -203,18 +203,27 @@ impl Worktree {
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");
let normalized = raw_worktrees.as_ref().replace("\r\n", "\n");
let entries = normalized.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()));
let mut path = None;
let mut sha = None;
let mut ref_name = None;
for line in entry.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix("worktree ") {
path = Some(rest.to_string());
} else if let Some(rest) = line.strip_prefix("HEAD ") {
sha = Some(rest.to_string());
} else if let Some(rest) = line.strip_prefix("branch ") {
ref_name = Some(rest.to_string());
}
// Ignore other lines: detached, bare, locked, prunable, etc.
}
if let (Some(path), Some(sha), Some(ref_name)) = (path, sha, ref_name) {
worktrees.push(Worktree {
@@ -629,6 +638,10 @@ pub trait GitRepository: Send + Sync {
from_commit: Option<String>,
) -> BoxFuture<'_, Result<()>>;
fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>>;
fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>>;
fn reset(
&self,
commit: String,
@@ -1573,14 +1586,15 @@ impl GitRepository for RealGitRepository {
OsString::from("--no-optional-locks"),
OsString::from("worktree"),
OsString::from("add"),
OsString::from("-b"),
OsString::from(name.as_str()),
OsString::from("--"),
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),
]);
args.push(OsString::from(from_commit));
} else {
args.push(OsString::from("HEAD"));
}
self.executor
.spawn(async move {
@@ -1593,12 +1607,60 @@ impl GitRepository for RealGitRepository {
Ok(())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git worktree list failed: {stderr}");
anyhow::bail!("git worktree add failed: {stderr}");
}
})
.boxed()
}
fn remove_worktree(&self, path: PathBuf, force: bool) -> BoxFuture<'_, Result<()>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
let executor = self.executor.clone();
self.executor
.spawn(async move {
let mut args: Vec<OsString> = vec![
"--no-optional-locks".into(),
"worktree".into(),
"remove".into(),
];
if force {
args.push("--force".into());
}
args.push("--".into());
args.push(path.as_os_str().into());
GitBinary::new(git_binary_path, working_directory?, executor)
.run(args)
.await?;
anyhow::Ok(())
})
.boxed()
}
fn rename_worktree(&self, old_path: PathBuf, new_path: PathBuf) -> BoxFuture<'_, Result<()>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
let executor = self.executor.clone();
self.executor
.spawn(async move {
let args: Vec<OsString> = vec![
"--no-optional-locks".into(),
"worktree".into(),
"move".into(),
"--".into(),
old_path.as_os_str().into(),
new_path.as_os_str().into(),
];
GitBinary::new(git_binary_path, working_directory?, executor)
.run(args)
.await?;
anyhow::Ok(())
})
.boxed()
}
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
let working_directory = self.working_directory();
@@ -3576,6 +3638,343 @@ mod tests {
assert_eq!(upstream.branch_name(), Some("feature/git-pull-request"));
}
#[test]
fn test_parse_worktrees_from_str() {
// Empty input
let result = parse_worktrees_from_str("");
assert!(result.is_empty());
// Single worktree (main)
let input = "worktree /home/user/project\nHEAD abc123def\nbranch refs/heads/main\n\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123def");
assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
// Multiple worktrees
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
worktree /home/user/project-wt\nHEAD def456\nbranch refs/heads/feature\n\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 2);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
assert_eq!(result[1].path, PathBuf::from("/home/user/project-wt"));
assert_eq!(result[1].ref_name.as_ref(), "refs/heads/feature");
// Detached HEAD entry (should be skipped since ref_name won't parse)
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
worktree /home/user/detached\nHEAD def456\ndetached\n\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
// Bare repo entry (should be skipped)
let input = "worktree /home/user/bare.git\nHEAD abc123\nbare\n\n\
worktree /home/user/project\nHEAD def456\nbranch refs/heads/main\n\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
// Extra porcelain lines (locked, prunable) should be ignored
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
worktree /home/user/locked-wt\nHEAD def456\nbranch refs/heads/locked-branch\nlocked\n\n\
worktree /home/user/prunable-wt\nHEAD 789aaa\nbranch refs/heads/prunable-branch\nprunable\n\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 3);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
assert_eq!(result[1].path, PathBuf::from("/home/user/locked-wt"));
assert_eq!(result[1].ref_name.as_ref(), "refs/heads/locked-branch");
assert_eq!(result[2].path, PathBuf::from("/home/user/prunable-wt"));
assert_eq!(result[2].ref_name.as_ref(), "refs/heads/prunable-branch");
// Leading/trailing whitespace on lines should be tolerated
let input =
" worktree /home/user/project \n HEAD abc123 \n branch refs/heads/main \n\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123");
assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
// Windows-style line endings should be handled
let input = "worktree /home/user/project\r\nHEAD abc123\r\nbranch refs/heads/main\r\n\r\n";
let result = parse_worktrees_from_str(input);
assert_eq!(result.len(), 1);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123");
assert_eq!(result[0].ref_name.as_ref(), "refs/heads/main");
}
#[gpui::test]
async fn test_create_and_list_worktrees(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let repo = RealGitRepository::new(
&repo_dir.path().join(".git"),
None,
Some("git".into()),
cx.executor(),
)
.unwrap();
// Create an initial commit (required for worktrees)
smol::fs::write(repo_dir.path().join("file.txt"), "content")
.await
.unwrap();
repo.stage_paths(vec![repo_path("file.txt")], Arc::new(HashMap::default()))
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
.unwrap();
// List worktrees — should have just the main one
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 1);
assert_eq!(
worktrees[0].path.canonicalize().unwrap(),
repo_dir.path().canonicalize().unwrap()
);
// Create a new worktree
let worktree_dir = tempfile::tempdir().unwrap();
repo.create_worktree(
"test-branch".to_string(),
worktree_dir.path().to_path_buf(),
Some("HEAD".to_string()),
)
.await
.unwrap();
// List worktrees — should have two
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 2);
let new_worktree = worktrees
.iter()
.find(|w| w.branch() == "test-branch")
.expect("should find worktree with test-branch");
assert_eq!(
new_worktree.path.canonicalize().unwrap(),
worktree_dir
.path()
.join("test-branch")
.canonicalize()
.unwrap()
);
}
#[gpui::test]
async fn test_remove_worktree(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let repo = RealGitRepository::new(
&repo_dir.path().join(".git"),
None,
Some("git".into()),
cx.executor(),
)
.unwrap();
// Create an initial commit
smol::fs::write(repo_dir.path().join("file.txt"), "content")
.await
.unwrap();
repo.stage_paths(vec![repo_path("file.txt")], Arc::new(HashMap::default()))
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
.unwrap();
// Create a worktree
let worktree_dir = tempfile::tempdir().unwrap();
repo.create_worktree(
"to-remove".to_string(),
worktree_dir.path().to_path_buf(),
Some("HEAD".to_string()),
)
.await
.unwrap();
let worktree_path = worktree_dir.path().join("to-remove");
assert!(worktree_path.exists());
// Remove the worktree
repo.remove_worktree(worktree_path.clone(), false)
.await
.unwrap();
// Verify it's gone from the list
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 1);
assert!(
worktrees.iter().all(|w| w.branch() != "to-remove"),
"removed worktree should not appear in list"
);
// Verify the directory is removed
assert!(!worktree_path.exists());
}
#[gpui::test]
async fn test_remove_worktree_force(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let repo = RealGitRepository::new(
&repo_dir.path().join(".git"),
None,
Some("git".into()),
cx.executor(),
)
.unwrap();
// Create an initial commit
smol::fs::write(repo_dir.path().join("file.txt"), "content")
.await
.unwrap();
repo.stage_paths(vec![repo_path("file.txt")], Arc::new(HashMap::default()))
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
.unwrap();
// Create a worktree
let worktree_dir = tempfile::tempdir().unwrap();
repo.create_worktree(
"dirty-wt".to_string(),
worktree_dir.path().to_path_buf(),
Some("HEAD".to_string()),
)
.await
.unwrap();
let worktree_path = worktree_dir.path().join("dirty-wt");
// Add uncommitted changes in the worktree
smol::fs::write(worktree_path.join("dirty-file.txt"), "uncommitted")
.await
.unwrap();
// Non-force removal should fail with dirty worktree
let result = repo.remove_worktree(worktree_path.clone(), false).await;
assert!(
result.is_err(),
"non-force removal of dirty worktree should fail"
);
// Force removal should succeed
repo.remove_worktree(worktree_path.clone(), true)
.await
.unwrap();
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 1);
assert!(!worktree_path.exists());
}
#[gpui::test]
async fn test_rename_worktree(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().unwrap();
git2::Repository::init(repo_dir.path()).unwrap();
let repo = RealGitRepository::new(
&repo_dir.path().join(".git"),
None,
Some("git".into()),
cx.executor(),
)
.unwrap();
// Create an initial commit
smol::fs::write(repo_dir.path().join("file.txt"), "content")
.await
.unwrap();
repo.stage_paths(vec![repo_path("file.txt")], Arc::new(HashMap::default()))
.await
.unwrap();
repo.commit(
"Initial commit".into(),
None,
CommitOptions::default(),
AskPassDelegate::new(&mut cx.to_async(), |_, _, _| {}),
Arc::new(checkpoint_author_envs()),
)
.await
.unwrap();
// Create a worktree
let worktree_dir = tempfile::tempdir().unwrap();
repo.create_worktree(
"old-name".to_string(),
worktree_dir.path().to_path_buf(),
Some("HEAD".to_string()),
)
.await
.unwrap();
let old_path = worktree_dir.path().join("old-name");
assert!(old_path.exists());
// Move the worktree to a new path
let new_path = worktree_dir.path().join("new-name");
repo.rename_worktree(old_path.clone(), new_path.clone())
.await
.unwrap();
// Verify the old path is gone and new path exists
assert!(!old_path.exists());
assert!(new_path.exists());
// Verify it shows up in worktree list at the new path
let worktrees = repo.worktrees().await.unwrap();
assert_eq!(worktrees.len(), 2);
let moved_worktree = worktrees
.iter()
.find(|w| w.branch() == "old-name")
.expect("should find worktree by branch name");
assert_eq!(
moved_worktree.path.canonicalize().unwrap(),
new_path.canonicalize().unwrap()
);
}
impl RealGitRepository {
/// Force a Git garbage collection on the repository.
fn gc(&self) -> BoxFuture<'_, Result<()>> {