Detailed changes
@@ -401,16 +401,19 @@ async fn test_linked_worktrees_sync(
path: PathBuf::from(path!("/project")),
ref_name: Some("refs/heads/main".into()),
sha: "aaa111".into(),
+ is_main: false,
});
state.worktrees.push(GitWorktree {
path: PathBuf::from(path!("/project/feature-branch")),
ref_name: Some("refs/heads/feature-branch".into()),
sha: "bbb222".into(),
+ is_main: false,
});
state.worktrees.push(GitWorktree {
path: PathBuf::from(path!("/project/bugfix-branch")),
ref_name: Some("refs/heads/bugfix-branch".into()),
sha: "ccc333".into(),
+ is_main: false,
});
})
.unwrap();
@@ -480,6 +483,7 @@ async fn test_linked_worktrees_sync(
path: PathBuf::from(path!("/project/hotfix-branch")),
ref_name: Some("refs/heads/hotfix-branch".into()),
sha: "ddd444".into(),
+ is_main: false,
});
})
.unwrap();
@@ -429,6 +429,7 @@ impl GitRepository for FakeGitRepository {
path: work_dir,
ref_name: Some(branch_ref.into()),
sha: head_sha.into(),
+ is_main: true,
};
let mut all = vec![main_worktree];
all.extend(state.worktrees.iter().cloned());
@@ -470,6 +471,7 @@ impl GitRepository for FakeGitRepository {
path,
ref_name: Some(ref_name.into()),
sha: sha.into(),
+ is_main: false,
});
state.branches.insert(branch_name);
Ok::<(), anyhow::Error>(())
@@ -238,6 +238,7 @@ pub struct Worktree {
pub ref_name: Option<SharedString>,
// todo(git_worktree) This type should be a Oid
pub sha: SharedString,
+ pub is_main: bool,
}
impl Worktree {
@@ -259,6 +260,7 @@ impl Worktree {
pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree> {
let mut worktrees = Vec::new();
+ let mut is_first = true;
let normalized = raw_worktrees.as_ref().replace("\r\n", "\n");
let entries = normalized.split("\n\n");
for entry in entries {
@@ -286,7 +288,9 @@ pub fn parse_worktrees_from_str<T: AsRef<str>>(raw_worktrees: T) -> Vec<Worktree
path: PathBuf::from(path),
ref_name: ref_name.map(Into::into),
sha: sha.into(),
- })
+ is_main: is_first,
+ });
+ is_first = false;
}
}
@@ -3876,6 +3880,7 @@ mod tests {
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123def");
assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert!(result[0].is_main);
// Multiple worktrees
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3884,8 +3889,10 @@ mod tests {
assert_eq!(result.len(), 2);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert!(result[0].is_main);
assert_eq!(result[1].path, PathBuf::from("/home/user/project-wt"));
assert_eq!(result[1].ref_name, Some("refs/heads/feature".into()));
+ assert!(!result[1].is_main);
// Detached HEAD entry (included with ref_name: None)
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3894,9 +3901,11 @@ mod tests {
assert_eq!(result.len(), 2);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert!(result[0].is_main);
assert_eq!(result[1].path, PathBuf::from("/home/user/detached"));
assert_eq!(result[1].ref_name, None);
assert_eq!(result[1].sha.as_ref(), "def456");
+ assert!(!result[1].is_main);
// Bare repo entry (included with ref_name: None)
let input = "worktree /home/user/bare.git\nHEAD abc123\nbare\n\n\
@@ -3905,8 +3914,10 @@ mod tests {
assert_eq!(result.len(), 2);
assert_eq!(result[0].path, PathBuf::from("/home/user/bare.git"));
assert_eq!(result[0].ref_name, None);
+ assert!(result[0].is_main);
assert_eq!(result[1].path, PathBuf::from("/home/user/project"));
assert_eq!(result[1].ref_name, Some("refs/heads/main".into()));
+ assert!(!result[1].is_main);
// Extra porcelain lines (locked, prunable) should be ignored
let input = "worktree /home/user/project\nHEAD abc123\nbranch refs/heads/main\n\n\
@@ -3916,13 +3927,16 @@ mod tests {
assert_eq!(result.len(), 3);
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert!(result[0].is_main);
assert_eq!(result[1].path, PathBuf::from("/home/user/locked-wt"));
assert_eq!(result[1].ref_name, Some("refs/heads/locked-branch".into()));
+ assert!(!result[1].is_main);
assert_eq!(result[2].path, PathBuf::from("/home/user/prunable-wt"));
assert_eq!(
result[2].ref_name,
Some("refs/heads/prunable-branch".into())
);
+ assert!(!result[2].is_main);
// Leading/trailing whitespace on lines should be tolerated
let input =
@@ -3932,6 +3946,7 @@ mod tests {
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123");
assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert!(result[0].is_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";
@@ -3940,6 +3955,7 @@ mod tests {
assert_eq!(result[0].path, PathBuf::from("/home/user/project"));
assert_eq!(result[0].sha.as_ref(), "abc123");
assert_eq!(result[0].ref_name, Some("refs/heads/main".into()));
+ assert!(result[0].is_main);
}
#[gpui::test]
@@ -254,6 +254,14 @@ struct WorktreeEntry {
is_new: bool,
}
+impl WorktreeEntry {
+ fn can_delete(&self, forbidden_deletion_path: Option<&PathBuf>) -> bool {
+ !self.is_new
+ && !self.worktree.is_main
+ && forbidden_deletion_path != Some(&self.worktree.path)
+ }
+}
+
pub struct WorktreeListDelegate {
matches: Vec<WorktreeEntry>,
all_worktrees: Option<Vec<GitWorktree>>,
@@ -462,7 +470,7 @@ impl WorktreeListDelegate {
let Some(entry) = self.matches.get(idx).cloned() else {
return;
};
- if entry.is_new || self.forbidden_deletion_path.as_ref() == Some(&entry.worktree.path) {
+ if !entry.can_delete(self.forbidden_deletion_path.as_ref()) {
return;
}
let Some(repo) = self.repo.clone() else {
@@ -719,6 +727,7 @@ impl PickerDelegate for WorktreeListDelegate {
path: Default::default(),
ref_name: Some(format!("refs/heads/{query}").into()),
sha: Default::default(),
+ is_main: false,
},
positions: Vec::new(),
is_new: true,
@@ -805,8 +814,7 @@ impl PickerDelegate for WorktreeListDelegate {
let focus_handle = self.focus_handle.clone();
- let can_delete =
- !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path);
+ let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
let delete_button = |entry_ix: usize| {
IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
@@ -894,9 +902,8 @@ impl PickerDelegate for WorktreeListDelegate {
let focus_handle = self.focus_handle.clone();
let selected_entry = self.matches.get(self.selected_index);
let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
- let can_delete = selected_entry.is_some_and(|entry| {
- !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path)
- });
+ let can_delete = selected_entry
+ .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
let footer_container = h_flex()
.w_full()
@@ -7060,6 +7060,7 @@ fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree {
.map(|s| s.to_string())
.unwrap_or_default(),
sha: worktree.sha.to_string(),
+ is_main: worktree.is_main,
}
}
@@ -7068,6 +7069,7 @@ fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree {
path: PathBuf::from(proto.path.clone()),
ref_name: Some(SharedString::from(&proto.ref_name)),
sha: proto.sha.clone().into(),
+ is_main: proto.is_main,
}
}
@@ -575,6 +575,7 @@ message Worktree {
string path = 1;
string ref_name = 2;
string sha = 3;
+ bool is_main = 4;
}
message GitCreateWorktree {
@@ -255,6 +255,7 @@ mod tests {
path: std::path::PathBuf::from("/wt/feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "abc".into(),
+ is_main: false,
});
})
.expect("git state should be set");
@@ -2463,6 +2463,7 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -2577,6 +2578,7 @@ async fn test_search_matches_worktree_name(cx: &mut TestAppContext) {
path: std::path::PathBuf::from("/wt/rosewood"),
ref_name: Some("refs/heads/rosewood".into()),
sha: "abc".into(),
+ is_main: false,
});
})
.unwrap();
@@ -2638,6 +2640,7 @@ async fn test_git_worktree_added_live_updates_sidebar(cx: &mut TestAppContext) {
path: std::path::PathBuf::from("/wt/rosewood"),
ref_name: Some("refs/heads/rosewood".into()),
sha: "abc".into(),
+ is_main: false,
});
})
.unwrap();
@@ -2739,11 +2742,13 @@ async fn test_two_worktree_workspaces_absorbed_when_main_added(cx: &mut TestAppC
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-b"),
ref_name: Some("refs/heads/feature-b".into()),
sha: "bbb".into(),
+ is_main: false,
});
})
.unwrap();
@@ -2821,11 +2826,13 @@ async fn test_threadless_workspace_shows_new_thread_with_worktree_chip(cx: &mut
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
state.worktrees.push(git::repository::Worktree {
path: std::path::PathBuf::from("/wt-feature-b"),
ref_name: Some("refs/heads/feature-b".into()),
sha: "bbb".into(),
+ is_main: false,
});
})
.unwrap();
@@ -2941,6 +2948,7 @@ async fn test_multi_worktree_thread_shows_multiple_chips(cx: &mut TestAppContext
path: std::path::PathBuf::from(format!("/worktrees/{repo}/{branch}/{repo}")),
ref_name: Some(format!("refs/heads/{branch}").into()),
sha: "aaa".into(),
+ is_main: false,
});
}
})
@@ -3043,6 +3051,7 @@ async fn test_same_named_worktree_chips_are_deduplicated(cx: &mut TestAppContext
path: std::path::PathBuf::from(format!("/worktrees/{repo}/olivetti/{repo}")),
ref_name: Some("refs/heads/olivetti".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -3133,6 +3142,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -3248,6 +3258,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -3354,6 +3365,7 @@ async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(cx: &mut
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -3459,6 +3471,7 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -3609,6 +3622,7 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace(
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -4203,6 +4217,7 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -4374,6 +4389,7 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test
path: std::path::PathBuf::from("/wt-feature-a"),
ref_name: Some("refs/heads/feature-a".into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();
@@ -5180,6 +5196,7 @@ mod property_test {
path: worktree_pathbuf,
ref_name: Some(format!("refs/heads/{}", worktree_name).into()),
sha: "aaa".into(),
+ is_main: false,
});
})
.unwrap();