Detailed changes
@@ -859,9 +859,11 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
cx_a.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
+ project::trusted_worktrees::init(HashMap::default(), None, None, cx);
});
server_cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
+ project::trusted_worktrees::init(HashMap::default(), None, None, cx);
});
let mut server = TestServer::start(cx_a.executor().clone()).await;
@@ -29370,6 +29370,7 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_local_worktree_trust(cx: &mut TestAppContext) {
init_test(cx, |_| {});
+ cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
@@ -40,7 +40,6 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
dap.workspace = true
-db.workspace = true
extension.workspace = true
fancy-regex.workspace = true
fs.workspace = true
@@ -37,143 +37,7 @@ impl Domain for ProjectDb {
db::static_connection!(PROJECT_DB, ProjectDb, []);
-impl ProjectDb {
- pub(crate) async fn save_trusted_worktrees(
- &self,
- trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
- trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
- ) -> anyhow::Result<()> {
- use anyhow::Context as _;
- use db::sqlez::statement::Statement;
- use itertools::Itertools as _;
-
- PROJECT_DB
- .clear_trusted_worktrees()
- .await
- .context("clearing previous trust state")?;
-
- let trusted_worktrees = trusted_worktrees
- .into_iter()
- .flat_map(|(host, abs_paths)| {
- abs_paths
- .into_iter()
- .map(move |abs_path| (Some(abs_path), host.clone()))
- })
- .chain(trusted_workspaces.into_iter().map(|host| (None, host)))
- .collect::<Vec<_>>();
- let mut first_worktree;
- let mut last_worktree = 0_usize;
- for (count, placeholders) in std::iter::once("(?, ?, ?)")
- .cycle()
- .take(trusted_worktrees.len())
- .chunks(MAX_QUERY_PLACEHOLDERS / 3)
- .into_iter()
- .map(|chunk| {
- let mut count = 0;
- let placeholders = chunk
- .inspect(|_| {
- count += 1;
- })
- .join(", ");
- (count, placeholders)
- })
- .collect::<Vec<_>>()
- {
- first_worktree = last_worktree;
- last_worktree = last_worktree + count;
- let query = format!(
- r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
-VALUES {placeholders};"#
- );
-
- let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
- self.write(move |conn| {
- let mut statement = Statement::prepare(conn, query)?;
- let mut next_index = 1;
- for (abs_path, host) in trusted_worktrees {
- let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
- next_index = statement.bind(
- &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
- next_index,
- )?;
- next_index = statement.bind(
- &host
- .as_ref()
- .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
- next_index,
- )?;
- next_index = statement.bind(
- &host.as_ref().map(|host| host.host_identifier.as_str()),
- next_index,
- )?;
- }
- statement.exec()
- })
- .await
- .context("inserting new trusted state")?;
- }
- Ok(())
- }
-
- pub(crate) fn fetch_trusted_worktrees(
- &self,
- worktree_store: Option<Entity<WorktreeStore>>,
- host: Option<RemoteHostLocation>,
- cx: &App,
- ) -> anyhow::Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
- let trusted_worktrees = PROJECT_DB.trusted_worktrees()?;
- Ok(trusted_worktrees
- .into_iter()
- .map(|(abs_path, user_name, host_name)| {
- let db_host = match (user_name, host_name) {
- (_, None) => None,
- (None, Some(host_name)) => Some(RemoteHostLocation {
- user_name: None,
- host_identifier: SharedString::new(host_name),
- }),
- (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
- user_name: Some(SharedString::new(user_name)),
- host_identifier: SharedString::new(host_name),
- }),
- };
-
- match abs_path {
- Some(abs_path) => {
- if db_host != host {
- (db_host, PathTrust::AbsPath(abs_path))
- } else if let Some(worktree_store) = &worktree_store {
- find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
- .map(PathTrust::Worktree)
- .map(|trusted_worktree| (host.clone(), trusted_worktree))
- .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
- } else {
- (db_host, PathTrust::AbsPath(abs_path))
- }
- }
- None => (db_host, PathTrust::Workspace),
- }
- })
- .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
- acc.entry(remote_host)
- .or_insert_with(HashSet::default)
- .insert(path_trust);
- acc
- }))
- }
-
- query! {
- fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
- SELECT absolute_path, user_name, host_name
- FROM trusted_worktrees
- }
- }
-
- query! {
- pub async fn clear_trusted_worktrees() -> Result<()> {
- DELETE FROM trusted_worktrees
- }
- }
-}
+impl ProjectDb {}
#[cfg(test)]
mod tests {
@@ -192,220 +56,5 @@ mod tests {
trusted_worktrees::{PathTrust, RemoteHostLocation},
};
- static TEST_LOCK: Mutex<()> = Mutex::new(());
-
- #[gpui::test]
- async fn test_save_and_fetch_trusted_worktrees(cx: &mut TestAppContext) {
- cx.executor().allow_parking();
- let _guard = TEST_LOCK.lock().await;
- PROJECT_DB.clear_trusted_worktrees().await.unwrap();
- cx.update(|cx| {
- if cx.try_global::<SettingsStore>().is_none() {
- let settings = SettingsStore::test(cx);
- cx.set_global(settings);
- }
- });
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(
- path!("/"),
- json!({
- "project_a": { "main.rs": "" },
- "project_b": { "lib.rs": "" }
- }),
- )
- .await;
-
- let project = Project::test(
- fs,
- [path!("/project_a").as_ref(), path!("/project_b").as_ref()],
- cx,
- )
- .await;
- let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
- let mut trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> =
- HashMap::default();
- trusted_paths.insert(
- None,
- HashSet::from_iter([
- PathBuf::from(path!("/project_a")),
- PathBuf::from(path!("/project_b")),
- ]),
- );
-
- PROJECT_DB
- .save_trusted_worktrees(trusted_paths, HashSet::default())
- .await
- .unwrap();
-
- let fetched = cx.update(|cx| {
- PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
- });
- let fetched = fetched.unwrap();
-
- let local_trust = fetched.get(&None).expect("should have local host entry");
- assert_eq!(local_trust.len(), 2);
- assert!(
- local_trust
- .iter()
- .all(|p| matches!(p, PathTrust::Worktree(_)))
- );
-
- let fetched_no_store = cx
- .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
- .unwrap();
- let local_trust_no_store = fetched_no_store
- .get(&None)
- .expect("should have local host entry");
- assert_eq!(local_trust_no_store.len(), 2);
- assert!(
- local_trust_no_store
- .iter()
- .all(|p| matches!(p, PathTrust::AbsPath(_)))
- );
- }
-
- #[gpui::test]
- async fn test_save_and_fetch_workspace_trust(cx: &mut TestAppContext) {
- cx.executor().allow_parking();
- let _guard = TEST_LOCK.lock().await;
- PROJECT_DB.clear_trusted_worktrees().await.unwrap();
- cx.update(|cx| {
- if cx.try_global::<SettingsStore>().is_none() {
- let settings = SettingsStore::test(cx);
- cx.set_global(settings);
- }
- });
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
- .await;
-
- let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
- let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
- let trusted_workspaces = HashSet::from_iter([None]);
- PROJECT_DB
- .save_trusted_worktrees(HashMap::default(), trusted_workspaces)
- .await
- .unwrap();
-
- let fetched = cx.update(|cx| {
- PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
- });
- let fetched = fetched.unwrap();
-
- let local_trust = fetched.get(&None).expect("should have local host entry");
- assert!(local_trust.contains(&PathTrust::Workspace));
-
- let fetched_no_store = cx
- .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
- .unwrap();
- let local_trust_no_store = fetched_no_store
- .get(&None)
- .expect("should have local host entry");
- assert!(local_trust_no_store.contains(&PathTrust::Workspace));
- }
-
- #[gpui::test]
- async fn test_save_and_fetch_remote_host_trust(cx: &mut TestAppContext) {
- cx.executor().allow_parking();
- let _guard = TEST_LOCK.lock().await;
- PROJECT_DB.clear_trusted_worktrees().await.unwrap();
- cx.update(|cx| {
- if cx.try_global::<SettingsStore>().is_none() {
- let settings = SettingsStore::test(cx);
- cx.set_global(settings);
- }
- });
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
- .await;
-
- let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
- let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
- let remote_host = Some(RemoteHostLocation {
- user_name: Some(SharedString::from("testuser")),
- host_identifier: SharedString::from("remote.example.com"),
- });
-
- let mut trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>> =
- HashMap::default();
- trusted_paths.insert(
- remote_host.clone(),
- HashSet::from_iter([PathBuf::from("/home/testuser/project")]),
- );
-
- PROJECT_DB
- .save_trusted_worktrees(trusted_paths, HashSet::default())
- .await
- .unwrap();
-
- let fetched = cx.update(|cx| {
- PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
- });
- let fetched = fetched.unwrap();
-
- let remote_trust = fetched
- .get(&remote_host)
- .expect("should have remote host entry");
- assert_eq!(remote_trust.len(), 1);
- assert!(remote_trust
- .iter()
- .any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project"))));
-
- let fetched_no_store = cx
- .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
- .unwrap();
- let remote_trust_no_store = fetched_no_store
- .get(&remote_host)
- .expect("should have remote host entry");
- assert_eq!(remote_trust_no_store.len(), 1);
- assert!(remote_trust_no_store
- .iter()
- .any(|p| matches!(p, PathTrust::AbsPath(path) if path == &PathBuf::from("/home/testuser/project"))));
- }
-
- #[gpui::test]
- async fn test_clear_trusted_worktrees(cx: &mut TestAppContext) {
- cx.executor().allow_parking();
- let _guard = TEST_LOCK.lock().await;
- PROJECT_DB.clear_trusted_worktrees().await.unwrap();
- cx.update(|cx| {
- if cx.try_global::<SettingsStore>().is_none() {
- let settings = SettingsStore::test(cx);
- cx.set_global(settings);
- }
- });
-
- let fs = FakeFs::new(cx.executor());
- fs.insert_tree(path!("/root"), json!({ "main.rs": "" }))
- .await;
-
- let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
- let worktree_store = project.read_with(cx, |p, _| p.worktree_store());
-
- let trusted_workspaces = HashSet::from_iter([None]);
- PROJECT_DB
- .save_trusted_worktrees(HashMap::default(), trusted_workspaces)
- .await
- .unwrap();
-
- PROJECT_DB.clear_trusted_worktrees().await.unwrap();
-
- let fetched = cx.update(|cx| {
- PROJECT_DB.fetch_trusted_worktrees(Some(worktree_store.clone()), None, cx)
- });
- let fetched = fetched.unwrap();
-
- assert!(fetched.is_empty(), "should be empty after clear");
-
- let fetched_no_store = cx
- .update(|cx| PROJECT_DB.fetch_trusted_worktrees(None, None, cx))
- .unwrap();
- assert!(fetched_no_store.is_empty(), "should be empty after clear");
- }
+ static TEST_WORKTREE_TRUST_LOCK: Mutex<()> = Mutex::new(());
}
@@ -10,7 +10,6 @@ pub mod image_store;
pub mod lsp_command;
pub mod lsp_store;
mod manifest_tree;
-mod persistence;
pub mod prettier_store;
mod project_search;
pub mod project_settings;
@@ -68,19 +68,21 @@ use util::debug_panic;
use crate::{project_settings::ProjectSettings, worktree_store::WorktreeStore};
-#[cfg(not(any(test, feature = "test-support")))]
-use crate::persistence::PROJECT_DB;
-#[cfg(not(any(test, feature = "test-support")))]
-use util::ResultExt as _;
-
pub fn init(
+ db_trusted_paths: TrustedPaths,
downstream_client: Option<(AnyProtoClient, u64)>,
upstream_client: Option<(AnyProtoClient, u64)>,
cx: &mut App,
) {
if TrustedWorktrees::try_get_global(cx).is_none() {
- let trusted_worktrees = cx.new(|cx| {
- TrustedWorktreesStore::new(None, None, downstream_client, upstream_client, cx)
+ let trusted_worktrees = cx.new(|_| {
+ TrustedWorktreesStore::new(
+ db_trusted_paths,
+ None,
+ None,
+ downstream_client,
+ upstream_client,
+ )
});
cx.set_global(TrustedWorktrees(trusted_worktrees))
}
@@ -126,18 +128,7 @@ pub fn track_worktree_trust(
}
});
}
- None => {
- let trusted_worktrees = cx.new(|cx| {
- TrustedWorktreesStore::new(
- Some(worktree_store.clone()),
- remote_host,
- downstream_client,
- upstream_client,
- cx,
- )
- });
- cx.set_global(TrustedWorktrees(trusted_worktrees))
- }
+ None => log::debug!("No TrustedWorktrees initialized, not tracking worktree trust"),
}
}
@@ -212,9 +203,7 @@ pub struct TrustedWorktreesStore {
downstream_client: Option<(AnyProtoClient, u64)>,
upstream_client: Option<(AnyProtoClient, u64)>,
worktree_stores: HashMap<WeakEntity<WorktreeStore>, Option<RemoteHostLocation>>,
- trusted_paths: HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>,
- #[cfg(not(any(test, feature = "test-support")))]
- serialization_task: Task<()>,
+ trusted_paths: TrustedPaths,
restricted: HashSet<WorktreeId>,
remote_host: Option<RemoteHostLocation>,
restricted_workspaces: HashSet<Option<RemoteHostLocation>>,
@@ -307,36 +296,16 @@ pub enum TrustedWorktreesEvent {
impl EventEmitter<TrustedWorktreesEvent> for TrustedWorktreesStore {}
+pub type TrustedPaths = HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>;
+
impl TrustedWorktreesStore {
fn new(
+ trusted_paths: TrustedPaths,
worktree_store: Option<Entity<WorktreeStore>>,
remote_host: Option<RemoteHostLocation>,
downstream_client: Option<(AnyProtoClient, u64)>,
upstream_client: Option<(AnyProtoClient, u64)>,
- cx: &App,
) -> Self {
- #[cfg(any(test, feature = "test-support"))]
- let _ = cx;
-
- #[cfg(not(any(test, feature = "test-support")))]
- let trusted_paths = if downstream_client.is_none() {
- match PROJECT_DB.fetch_trusted_worktrees(
- worktree_store.clone(),
- remote_host.clone(),
- cx,
- ) {
- Ok(trusted_paths) => trusted_paths,
- Err(e) => {
- log::error!("Failed to do initial trusted worktrees fetch: {e:#}");
- HashMap::default()
- }
- }
- } else {
- HashMap::default()
- };
- #[cfg(any(test, feature = "test-support"))]
- let trusted_paths = HashMap::<Option<RemoteHostLocation>, HashSet<PathTrust>>::default();
-
if let Some((upstream_client, upstream_project_id)) = &upstream_client {
let trusted_paths = trusted_paths
.iter()
@@ -366,8 +335,6 @@ impl TrustedWorktreesStore {
remote_host,
restricted_workspaces: HashSet::default(),
restricted: HashSet::default(),
- #[cfg(not(any(test, feature = "test-support")))]
- serialization_task: Task::ready(()),
worktree_stores,
}
}
@@ -496,41 +463,6 @@ impl TrustedWorktreesStore {
trusted_paths.clone(),
));
- #[cfg(not(any(test, feature = "test-support")))]
- if self.downstream_client.is_none() {
- let mut new_trusted_workspaces = HashSet::default();
- let new_trusted_worktrees = self
- .trusted_paths
- .clone()
- .into_iter()
- .map(|(host, paths)| {
- let abs_paths = paths
- .into_iter()
- .flat_map(|path| match path {
- PathTrust::Worktree(worktree_id) => self
- .find_worktree_data(worktree_id, cx)
- .map(|(abs_path, ..)| abs_path.to_path_buf()),
- PathTrust::AbsPath(abs_path) => Some(abs_path),
- PathTrust::Workspace => {
- new_trusted_workspaces.insert(host.clone());
- None
- }
- })
- .collect();
- (host, abs_paths)
- })
- .collect();
- // Do not persist auto trusted worktrees
- if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
- self.serialization_task = cx.background_spawn(async move {
- PROJECT_DB
- .save_trusted_worktrees(new_trusted_worktrees, new_trusted_workspaces)
- .await
- .log_err();
- });
- }
- }
-
if let Some((upstream_client, upstream_project_id)) = &self.upstream_client {
let trusted_paths = trusted_paths
.iter()
@@ -578,31 +510,8 @@ impl TrustedWorktreesStore {
/// Erases all trust information.
/// Requires Zed's restart to take proper effect.
- pub fn clear_trusted_paths(&mut self, cx: &App) -> Task<()> {
- if self.downstream_client.is_none() {
- self.trusted_paths.clear();
-
- #[cfg(not(any(test, feature = "test-support")))]
- {
- let (tx, rx) = smol::channel::bounded(1);
- self.serialization_task = cx.background_spawn(async move {
- PROJECT_DB.clear_trusted_worktrees().await.log_err();
- tx.send(()).await.ok();
- });
-
- return cx.background_spawn(async move {
- rx.recv().await.ok();
- });
- }
-
- #[cfg(any(test, feature = "test-support"))]
- {
- let _ = cx;
- Task::ready(())
- }
- } else {
- Task::ready(())
- }
+ pub fn clear_trusted_paths(&mut self) {
+ self.trusted_paths.clear();
}
/// Checks whether a certain worktree is trusted (or on a larger trust level).
@@ -785,6 +694,39 @@ impl TrustedWorktreesStore {
}
}
+ /// Returns a normalized representation of the trusted paths to store in the DB.
+ pub fn trusted_paths_for_serialization(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> (
+ HashSet<Option<RemoteHostLocation>>,
+ HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
+ ) {
+ let mut new_trusted_workspaces = HashSet::default();
+ let new_trusted_worktrees = self
+ .trusted_paths
+ .clone()
+ .into_iter()
+ .map(|(host, paths)| {
+ let abs_paths = paths
+ .into_iter()
+ .flat_map(|path| match path {
+ PathTrust::Worktree(worktree_id) => self
+ .find_worktree_data(worktree_id, cx)
+ .map(|(abs_path, ..)| abs_path.to_path_buf()),
+ PathTrust::AbsPath(abs_path) => Some(abs_path),
+ PathTrust::Workspace => {
+ new_trusted_workspaces.insert(host.clone());
+ None
+ }
+ })
+ .collect();
+ (host, abs_paths)
+ })
+ .collect();
+ (new_trusted_workspaces, new_trusted_worktrees)
+ }
+
fn find_worktree_data(
&mut self,
worktree_id: WorktreeId,
@@ -841,7 +783,7 @@ impl TrustedWorktreesStore {
}
}
-pub(crate) fn find_worktree_in_store(
+pub fn find_worktree_in_store(
worktree_store: &WorktreeStore,
abs_path: &Path,
cx: &App,
@@ -885,6 +827,7 @@ mod tests {
cx: &mut TestAppContext,
) -> Entity<TrustedWorktreesStore> {
cx.update(|cx| {
+ init(HashMap::default(), None, None, cx);
track_worktree_trust(worktree_store, None, None, None, cx);
TrustedWorktrees::try_get_global(cx).expect("global should be set")
})
@@ -2,6 +2,7 @@ use crate::HeadlessProject;
use crate::headless_project::HeadlessAppState;
use anyhow::{Context as _, Result, anyhow};
use client::ProxySettings;
+use collections::HashMap;
use project::trusted_worktrees;
use util::ResultExt;
@@ -419,7 +420,7 @@ pub fn execute_run(
log::info!("gpui app started, initializing server");
let session = start_server(listeners, log_rx, cx, is_wsl_interop);
- trusted_worktrees::init(Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx);
+ trusted_worktrees::init(HashMap::default(), Some((session.clone(), REMOTE_SERVER_PROJECT_ID)), None, cx);
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
git_hosting_providers::init(cx);
@@ -9,14 +9,18 @@ use std::{
};
use anyhow::{Context as _, Result, bail};
-use collections::{HashMap, IndexSet};
+use collections::{HashMap, HashSet, IndexSet};
use db::{
query,
sqlez::{connection::Connection, domain::Domain},
sqlez_macros::sql,
};
-use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
-use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
+use gpui::{Axis, Bounds, Entity, Task, WindowBounds, WindowId, point, size};
+use project::{
+ debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
+ trusted_worktrees::{PathTrust, RemoteHostLocation, find_worktree_in_store},
+ worktree_store::WorktreeStore,
+};
use language::{LanguageName, Toolchain, ToolchainScope};
use project::WorktreeId;
@@ -46,6 +50,11 @@ use model::{
use self::model::{DockStructure, SerializedWorkspaceLocation};
+// https://www.sqlite.org/limits.html
+// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
+// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
+const MAX_QUERY_PLACEHOLDERS: usize = 32000;
+
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
@@ -708,6 +717,14 @@ impl Domain for WorkspaceDb {
ALTER TABLE remote_connections ADD COLUMN name TEXT;
ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
),
+ sql!(
+ CREATE TABLE IF NOT EXISTS trusted_worktrees (
+ trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
+ absolute_path TEXT,
+ user_name TEXT,
+ host_name TEXT
+ ) STRICT;
+ ),
];
// Allow recovering from bad migration that was initially shipped to nightly
@@ -1796,6 +1813,141 @@ impl WorkspaceDb {
Ok(())
}).await
}
+
+ pub(crate) async fn save_trusted_worktrees(
+ &self,
+ trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
+ trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
+ ) -> anyhow::Result<()> {
+ use anyhow::Context as _;
+ use db::sqlez::statement::Statement;
+ use itertools::Itertools as _;
+
+ DB.clear_trusted_worktrees()
+ .await
+ .context("clearing previous trust state")?;
+
+ let trusted_worktrees = trusted_worktrees
+ .into_iter()
+ .flat_map(|(host, abs_paths)| {
+ abs_paths
+ .into_iter()
+ .map(move |abs_path| (Some(abs_path), host.clone()))
+ })
+ .chain(trusted_workspaces.into_iter().map(|host| (None, host)))
+ .collect::<Vec<_>>();
+ let mut first_worktree;
+ let mut last_worktree = 0_usize;
+ for (count, placeholders) in std::iter::once("(?, ?, ?)")
+ .cycle()
+ .take(trusted_worktrees.len())
+ .chunks(MAX_QUERY_PLACEHOLDERS / 3)
+ .into_iter()
+ .map(|chunk| {
+ let mut count = 0;
+ let placeholders = chunk
+ .inspect(|_| {
+ count += 1;
+ })
+ .join(", ");
+ (count, placeholders)
+ })
+ .collect::<Vec<_>>()
+ {
+ first_worktree = last_worktree;
+ last_worktree = last_worktree + count;
+ let query = format!(
+ r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
+VALUES {placeholders};"#
+ );
+
+ let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
+ self.write(move |conn| {
+ let mut statement = Statement::prepare(conn, query)?;
+ let mut next_index = 1;
+ for (abs_path, host) in trusted_worktrees {
+ let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
+ next_index = statement.bind(
+ &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
+ next_index,
+ )?;
+ next_index = statement.bind(
+ &host
+ .as_ref()
+ .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
+ next_index,
+ )?;
+ next_index = statement.bind(
+ &host.as_ref().map(|host| host.host_identifier.as_str()),
+ next_index,
+ )?;
+ }
+ statement.exec()
+ })
+ .await
+ .context("inserting new trusted state")?;
+ }
+ Ok(())
+ }
+
+ pub fn fetch_trusted_worktrees(
+ &self,
+ worktree_store: Option<Entity<WorktreeStore>>,
+ host: Option<RemoteHostLocation>,
+ cx: &App,
+ ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
+ let trusted_worktrees = DB.trusted_worktrees()?;
+ Ok(trusted_worktrees
+ .into_iter()
+ .map(|(abs_path, user_name, host_name)| {
+ let db_host = match (user_name, host_name) {
+ (_, None) => None,
+ (None, Some(host_name)) => Some(RemoteHostLocation {
+ user_name: None,
+ host_identifier: SharedString::new(host_name),
+ }),
+ (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
+ user_name: Some(SharedString::new(user_name)),
+ host_identifier: SharedString::new(host_name),
+ }),
+ };
+
+ match abs_path {
+ Some(abs_path) => {
+ if db_host != host {
+ (db_host, PathTrust::AbsPath(abs_path))
+ } else if let Some(worktree_store) = &worktree_store {
+ find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
+ .map(PathTrust::Worktree)
+ .map(|trusted_worktree| (host.clone(), trusted_worktree))
+ .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
+ } else {
+ (db_host, PathTrust::AbsPath(abs_path))
+ }
+ }
+ None => (db_host, PathTrust::Workspace),
+ }
+ })
+ .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
+ acc.entry(remote_host)
+ .or_insert_with(HashSet::default)
+ .insert(path_trust);
+ acc
+ }))
+ }
+
+ query! {
+ fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
+ SELECT absolute_path, user_name, host_name
+ FROM trusted_worktrees
+ }
+ }
+
+ query! {
+ pub async fn clear_trusted_worktrees() -> Result<()> {
+ DELETE FROM trusted_worktrees
+ }
+ }
}
pub fn delete_unloaded_items(
@@ -131,14 +131,14 @@ impl Render for SecurityModal {
None => match &restricted_path.host {
Some(remote_host) => match &remote_host.user_name {
Some(user_name) => format!(
- "Empty project ({}@{})",
+ "Workspace trust ({}@{})",
user_name, remote_host.host_identifier
),
None => {
- format!("Empty project ({})", remote_host.host_identifier)
+ format!("Workspace trust ({})", remote_host.host_identifier)
}
},
- None => "Empty project".to_string(),
+ None => "Workspace trust".to_string(),
},
};
h_flex()
@@ -80,7 +80,7 @@ use project::{
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
project_settings::ProjectSettings,
toolchain_store::ToolchainStoreEvent,
- trusted_worktrees::TrustedWorktrees,
+ trusted_worktrees::{TrustedWorktrees, TrustedWorktreesEvent},
};
use remote::{
RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
@@ -1184,6 +1184,7 @@ pub struct Workspace {
_observe_current_user: Task<Result<()>>,
_schedule_serialize_workspace: Option<Task<()>>,
_schedule_serialize_ssh_paths: Option<Task<()>>,
+ _schedule_serialize_worktree_trust: Task<()>,
pane_history_timestamp: Arc<AtomicUsize>,
bounds: Bounds<Pixels>,
pub centered_layout: bool,
@@ -1229,16 +1230,43 @@ impl Workspace {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
- cx.observe_global::<SettingsStore>(|_, cx| {
- if ProjectSettings::get_global(cx).session.trust_all_worktrees {
- if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
- trusted_worktrees.update(cx, |trusted_worktrees, cx| {
- trusted_worktrees.auto_trust_all(cx);
- })
+ if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+ cx.subscribe(&trusted_worktrees, |workspace, worktrees_store, e, cx| {
+ if let TrustedWorktreesEvent::Trusted(..) = e {
+ let (new_trusted_workspaces, new_trusted_worktrees) = worktrees_store
+ .update(cx, |worktrees_store, cx| {
+ worktrees_store.trusted_paths_for_serialization(cx)
+ });
+ // Do not persist auto trusted worktrees
+ if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
+ let timeout = cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
+ workspace._schedule_serialize_worktree_trust =
+ cx.background_spawn(async move {
+ timeout.await;
+ persistence::DB
+ .save_trusted_worktrees(
+ new_trusted_worktrees,
+ new_trusted_workspaces,
+ )
+ .await
+ .log_err();
+ });
+ }
}
- }
- })
- .detach();
+ })
+ .detach();
+
+ cx.observe_global::<SettingsStore>(|_, cx| {
+ if ProjectSettings::get_global(cx).session.trust_all_worktrees {
+ if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+ trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+ trusted_worktrees.auto_trust_all(cx);
+ })
+ }
+ }
+ })
+ .detach();
+ }
cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
match event {
@@ -1250,11 +1278,25 @@ impl Workspace {
this.collaborator_left(*peer_id, window, cx);
}
- project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
- this.update_window_title(window, cx);
- this.serialize_workspace(window, cx);
- // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
- this.update_history(cx);
+ project::Event::WorktreeUpdatedEntries(worktree_id, _) => {
+ if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+ trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+ trusted_worktrees.can_trust(*worktree_id, cx);
+ });
+ }
+ }
+
+ project::Event::WorktreeRemoved(_) => {
+ this.update_worktree_data(window, cx);
+ }
+
+ project::Event::WorktreeAdded(worktree_id) => {
+ if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
+ trusted_worktrees.update(cx, |trusted_worktrees, cx| {
+ trusted_worktrees.can_trust(*worktree_id, cx);
+ });
+ }
+ this.update_worktree_data(window, cx);
}
project::Event::DisconnectedFromHost => {
@@ -1540,6 +1582,7 @@ impl Workspace {
_apply_leader_updates,
_schedule_serialize_workspace: None,
_schedule_serialize_ssh_paths: None,
+ _schedule_serialize_worktree_trust: Task::ready(()),
leader_updates_tx,
_subscriptions: subscriptions,
pane_history_timestamp,
@@ -5970,12 +6013,14 @@ impl Workspace {
.on_action(
cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
- let clear_task = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
- trusted_worktrees.clear_trusted_paths(cx)
+ trusted_worktrees.update(cx, |trusted_worktrees, _| {
+ trusted_worktrees.clear_trusted_paths()
});
+ let clear_task = persistence::DB.clear_trusted_worktrees();
cx.spawn(async move |_, cx| {
- clear_task.await;
- cx.update(|cx| reload(cx)).ok();
+ if clear_task.await.log_err().is_some() {
+ cx.update(|cx| reload(cx)).ok();
+ }
})
.detach();
}
@@ -6496,6 +6541,13 @@ impl Workspace {
}
}
}
+
+ fn update_worktree_data(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) {
+ self.update_window_title(window, cx);
+ self.serialize_workspace(window, cx);
+ // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
+ self.update_history(cx);
+ }
}
fn leader_border_for_pane(
@@ -407,7 +407,14 @@ pub fn main() {
});
app.run(move |cx| {
- trusted_worktrees::init(None, None, cx);
+ let trusted_paths = match workspace::WORKSPACE_DB.fetch_trusted_worktrees(None, None, cx) {
+ Ok(trusted_paths) => trusted_paths,
+ Err(e) => {
+ log::error!("Failed to do initial trusted worktrees fetch: {e:#}");
+ HashMap::default()
+ }
+ };
+ trusted_worktrees::init(trusted_paths, None, None, cx);
menu::init();
zed_actions::init();