mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
git: Add trusted worktree support to git integrations (#50649)
This PR cleans up the git command spawning by wrapping everything in GitBinary instead to follow a builder/factory pattern. It also extends trusted workspace support to git commands. I also added a `clippy.toml` configuration to our git crate that warns against using `Command` struct to spawn git commands instead of going through `GitBinary`. This should help us maintain the factory pattern in the future Before you mark this PR as ready for review, make sure that you have: - [x] Added solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects Release Notes: - git: Add trusted workspace support for Zed's git integration
This commit is contained in:
parent
0b58d34936
commit
d5137d76c1
8 changed files with 644 additions and 365 deletions
|
|
@ -20,7 +20,7 @@ use ignore::gitignore::GitignoreBuilder;
|
|||
use parking_lot::Mutex;
|
||||
use rope::Rope;
|
||||
use smol::{channel::Sender, future::FutureExt as _};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{path::PathBuf, sync::Arc, sync::atomic::AtomicBool};
|
||||
use text::LineEnding;
|
||||
use util::{paths::PathStyle, rel_path::RelPath};
|
||||
|
||||
|
|
@ -32,6 +32,7 @@ pub struct FakeGitRepository {
|
|||
pub(crate) dot_git_path: PathBuf,
|
||||
pub(crate) repository_dir_path: PathBuf,
|
||||
pub(crate) common_dir_path: PathBuf,
|
||||
pub(crate) is_trusted: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -1035,4 +1036,13 @@ impl GitRepository for FakeGitRepository {
|
|||
fn commit_data_reader(&self) -> Result<CommitDataReader> {
|
||||
anyhow::bail!("commit_data_reader not supported for FakeGitRepository")
|
||||
}
|
||||
|
||||
fn set_trusted(&self, trusted: bool) {
|
||||
self.is_trusted
|
||||
.store(trusted, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
|
||||
fn is_trusted(&self) -> bool {
|
||||
self.is_trusted.load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2776,6 +2776,7 @@ impl Fs for FakeFs {
|
|||
repository_dir_path: repository_dir_path.to_owned(),
|
||||
common_dir_path: common_dir_path.to_owned(),
|
||||
checkpoints: Arc::default(),
|
||||
is_trusted: Arc::default(),
|
||||
}) as _
|
||||
},
|
||||
)
|
||||
|
|
|
|||
28
crates/git/clippy.toml
Normal file
28
crates/git/clippy.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
allow-private-module-inception = true
|
||||
avoid-breaking-exported-api = false
|
||||
ignore-interior-mutability = [
|
||||
# Suppresses clippy::mutable_key_type, which is a false positive as the Eq
|
||||
# and Hash impls do not use fields with interior mutability.
|
||||
"agent_ui::context::AgentContextKey"
|
||||
]
|
||||
disallowed-methods = [
|
||||
{ path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" },
|
||||
{ path = "std::process::Command::output", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::output" },
|
||||
{ path = "std::process::Command::status", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::status" },
|
||||
{ path = "std::process::Command::stdin", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stdin" },
|
||||
{ path = "std::process::Command::stdout", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stdout" },
|
||||
{ path = "std::process::Command::stderr", reason = "`smol::process::Command::from()` does not preserve stdio configuration", replacement = "smol::process::Command::stderr" },
|
||||
{ path = "smol::Timer::after", reason = "smol::Timer introduces non-determinism in tests", replacement = "gpui::BackgroundExecutor::timer" },
|
||||
{ path = "serde_json::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892. Use `serde_json::from_slice` instead." },
|
||||
{ path = "serde_json_lenient::from_reader", reason = "Parsing from a buffer is much slower than first reading the buffer into a Vec/String, see https://github.com/serde-rs/json/issues/160#issuecomment-253446892, Use `serde_json_lenient::from_slice` instead." },
|
||||
{ path = "cocoa::foundation::NSString::alloc", reason = "NSString must be autoreleased to avoid memory leaks. Use `ns_string()` helper instead." },
|
||||
{ path = "smol::process::Command::new", reason = "Git commands must go through `GitBinary::build_command` to ensure security flags like `-c core.fsmonitor=false` are always applied.", replacement = "GitBinary::build_command" },
|
||||
{ path = "util::command::new_command", reason = "Git commands must go through `GitBinary::build_command` to ensure security flags like `-c core.fsmonitor=false` are always applied.", replacement = "GitBinary::build_command" },
|
||||
{ path = "util::command::Command::new", reason = "Git commands must go through `GitBinary::build_command` to ensure security flags like `-c core.fsmonitor=false` are always applied.", replacement = "GitBinary::build_command" },
|
||||
]
|
||||
disallowed-types = [
|
||||
# { path = "std::collections::HashMap", replacement = "collections::HashMap" },
|
||||
# { path = "std::collections::HashSet", replacement = "collections::HashSet" },
|
||||
# { path = "indexmap::IndexSet", replacement = "collections::IndexSet" },
|
||||
# { path = "indexmap::IndexMap", replacement = "collections::IndexMap" },
|
||||
]
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
use crate::Oid;
|
||||
use crate::commit::get_messages;
|
||||
use crate::repository::RepoPath;
|
||||
use crate::repository::{GitBinary, RepoPath};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::AsyncWriteExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{ops::Range, path::Path};
|
||||
use std::ops::Range;
|
||||
use text::{LineEnding, Rope};
|
||||
use time::OffsetDateTime;
|
||||
use time::UtcOffset;
|
||||
|
|
@ -21,15 +21,13 @@ pub struct Blame {
|
|||
}
|
||||
|
||||
impl Blame {
|
||||
pub async fn for_path(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
pub(crate) async fn for_path(
|
||||
git: &GitBinary,
|
||||
path: &RepoPath,
|
||||
content: &Rope,
|
||||
line_ending: LineEnding,
|
||||
) -> Result<Self> {
|
||||
let output =
|
||||
run_git_blame(git_binary, working_directory, path, content, line_ending).await?;
|
||||
let output = run_git_blame(git, path, content, line_ending).await?;
|
||||
let mut entries = parse_git_blame(&output)?;
|
||||
entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start));
|
||||
|
||||
|
|
@ -40,7 +38,7 @@ impl Blame {
|
|||
}
|
||||
|
||||
let shas = unique_shas.into_iter().collect::<Vec<_>>();
|
||||
let messages = get_messages(working_directory, &shas)
|
||||
let messages = get_messages(git, &shas)
|
||||
.await
|
||||
.context("failed to get commit messages")?;
|
||||
|
||||
|
|
@ -52,8 +50,7 @@ const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD";
|
|||
const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
|
||||
|
||||
async fn run_git_blame(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
git: &GitBinary,
|
||||
path: &RepoPath,
|
||||
contents: &Rope,
|
||||
line_ending: LineEnding,
|
||||
|
|
@ -61,12 +58,7 @@ async fn run_git_blame(
|
|||
let mut child = {
|
||||
let span = ztracing::debug_span!("spawning git-blame command", path = path.as_unix_str());
|
||||
let _enter = span.enter();
|
||||
util::command::new_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
.arg("blame")
|
||||
.arg("--incremental")
|
||||
.arg("--contents")
|
||||
.arg("-")
|
||||
git.build_command(["blame", "--incremental", "--contents", "-"])
|
||||
.arg(path.as_unix_str())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{
|
||||
BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
|
||||
status::StatusCode,
|
||||
repository::GitBinary, status::StatusCode,
|
||||
};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::SharedString;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
|
|
@ -48,7 +48,7 @@ impl ParsedCommitMessage {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
pub(crate) async fn get_messages(git: &GitBinary, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
return Ok(HashMap::default());
|
||||
}
|
||||
|
|
@ -63,12 +63,12 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<Hash
|
|||
|
||||
let mut result = vec![];
|
||||
for shas in shas.chunks(MAX_ENTRIES_PER_INVOCATION) {
|
||||
let partial = get_messages_impl(working_directory, shas).await?;
|
||||
let partial = get_messages_impl(git, shas).await?;
|
||||
result.extend(partial);
|
||||
}
|
||||
result
|
||||
} else {
|
||||
get_messages_impl(working_directory, shas).await?
|
||||
get_messages_impl(git, shas).await?
|
||||
};
|
||||
|
||||
Ok(shas
|
||||
|
|
@ -78,11 +78,10 @@ pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<Hash
|
|||
.collect::<HashMap<Oid, String>>())
|
||||
}
|
||||
|
||||
async fn get_messages_impl(working_directory: &Path, shas: &[Oid]) -> Result<Vec<String>> {
|
||||
async fn get_messages_impl(git: &GitBinary, shas: &[Oid]) -> Result<Vec<String>> {
|
||||
const MARKER: &str = "<MARKER>";
|
||||
let output = util::command::new_command("git")
|
||||
.current_dir(working_directory)
|
||||
.arg("show")
|
||||
let output = git
|
||||
.build_command(["show"])
|
||||
.arg("-s")
|
||||
.arg(format!("--format=%B{}", MARKER))
|
||||
.args(shas.iter().map(ToString::to_string))
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,9 @@ pub mod pending_op;
|
|||
use crate::{
|
||||
ProjectEnvironment, ProjectItem, ProjectPath,
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
trusted_worktrees::{
|
||||
PathTrust, TrustedWorktrees, TrustedWorktreesEvent, TrustedWorktreesStore,
|
||||
},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
|
|
@ -354,6 +357,7 @@ impl LocalRepositoryState {
|
|||
dot_git_abs_path: Arc<Path>,
|
||||
project_environment: WeakEntity<ProjectEnvironment>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_trusted: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<Self> {
|
||||
let environment = project_environment
|
||||
|
|
@ -381,6 +385,7 @@ impl LocalRepositoryState {
|
|||
}
|
||||
})
|
||||
.await?;
|
||||
backend.set_trusted(is_trusted);
|
||||
Ok(LocalRepositoryState {
|
||||
backend,
|
||||
environment: Arc::new(environment),
|
||||
|
|
@ -495,11 +500,15 @@ impl GitStore {
|
|||
state: GitStoreState,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let _subscriptions = vec![
|
||||
let mut _subscriptions = vec![
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event),
|
||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event),
|
||||
];
|
||||
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
_subscriptions.push(cx.subscribe(&trusted_worktrees, Self::on_trusted_worktrees_event));
|
||||
}
|
||||
|
||||
GitStore {
|
||||
state,
|
||||
buffer_store,
|
||||
|
|
@ -1517,6 +1526,13 @@ impl GitStore {
|
|||
let original_repo_abs_path: Arc<Path> =
|
||||
git::repository::original_repo_path_from_common_dir(common_dir_abs_path).into();
|
||||
let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release));
|
||||
let is_trusted = TrustedWorktrees::try_get_global(cx)
|
||||
.map(|trusted_worktrees| {
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
trusted_worktrees.can_trust(&self.worktree_store, worktree_id, cx)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let git_store = cx.weak_entity();
|
||||
let repo = cx.new(|cx| {
|
||||
let mut repo = Repository::local(
|
||||
|
|
@ -1526,6 +1542,7 @@ impl GitStore {
|
|||
dot_git_abs_path.clone(),
|
||||
project_environment.downgrade(),
|
||||
fs.clone(),
|
||||
is_trusted,
|
||||
git_store,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -1566,6 +1583,39 @@ impl GitStore {
|
|||
}
|
||||
}
|
||||
|
||||
fn on_trusted_worktrees_event(
|
||||
&mut self,
|
||||
_: Entity<TrustedWorktreesStore>,
|
||||
event: &TrustedWorktreesEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if !matches!(self.state, GitStoreState::Local { .. }) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (is_trusted, event_paths) = match event {
|
||||
TrustedWorktreesEvent::Trusted(_, trusted_paths) => (true, trusted_paths),
|
||||
TrustedWorktreesEvent::Restricted(_, restricted_paths) => (false, restricted_paths),
|
||||
};
|
||||
|
||||
for (repo_id, worktree_ids) in &self.worktree_ids {
|
||||
if worktree_ids
|
||||
.iter()
|
||||
.any(|worktree_id| event_paths.contains(&PathTrust::Worktree(*worktree_id)))
|
||||
{
|
||||
if let Some(repo) = self.repositories.get(repo_id) {
|
||||
let repository_state = repo.read(cx).repository_state.clone();
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(RepositoryState::Local(state)) = repository_state.await {
|
||||
state.backend.set_trusted(is_trusted);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_store_event(
|
||||
&mut self,
|
||||
_: Entity<BufferStore>,
|
||||
|
|
@ -3763,6 +3813,13 @@ impl MergeDetails {
|
|||
}
|
||||
|
||||
impl Repository {
|
||||
pub fn is_trusted(&self) -> bool {
|
||||
match self.repository_state.peek() {
|
||||
Some(Ok(RepositoryState::Local(state))) => state.backend.is_trusted(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> RepositorySnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
|
|
@ -3788,6 +3845,7 @@ impl Repository {
|
|||
dot_git_abs_path: Arc<Path>,
|
||||
project_environment: WeakEntity<ProjectEnvironment>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_trusted: bool,
|
||||
git_store: WeakEntity<GitStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
|
|
@ -3804,6 +3862,7 @@ impl Repository {
|
|||
dot_git_abs_path,
|
||||
project_environment,
|
||||
fs,
|
||||
is_trusted,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1293,3 +1293,208 @@ mod git_worktrees {
|
|||
|
||||
use crate::Project;
|
||||
}
|
||||
|
||||
mod trust_tests {
|
||||
use collections::HashSet;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use project::trusted_worktrees::*;
|
||||
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
use crate::Project;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
zlog::init_test();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_defaults_to_untrusted_without_trust_system(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"a.txt": "hello",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create project without trust system — repos should default to untrusted.
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let repository = project.read_with(cx, |project, cx| {
|
||||
project.repositories(cx).values().next().unwrap().clone()
|
||||
});
|
||||
|
||||
repository.read_with(cx, |repo, _| {
|
||||
assert!(
|
||||
!repo.is_trusted(),
|
||||
"repository should default to untrusted when no trust system is initialized"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_repos_trust_with_single_worktree(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"a.txt": "hello",
|
||||
"sub": {
|
||||
".git": {},
|
||||
"b.txt": "world",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
init(DbTrustedPaths::default(), cx);
|
||||
});
|
||||
|
||||
let project =
|
||||
Project::test_with_worktree_trust(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_id = worktree_store.read_with(cx, |store, cx| {
|
||||
store.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let repos = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.repositories(cx)
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(repos.len(), 2, "should have two repositories");
|
||||
for repo in &repos {
|
||||
repo.read_with(cx, |repo, _| {
|
||||
assert!(
|
||||
!repo.is_trusted(),
|
||||
"all repos should be untrusted initially"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let trusted_worktrees = cx
|
||||
.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should be set"));
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
for repo in &repos {
|
||||
repo.read_with(cx, |repo, _| {
|
||||
assert!(
|
||||
repo.is_trusted(),
|
||||
"all repos should be trusted after worktree is trusted"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_trust_restrict_trust_cycle(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".git": {},
|
||||
"a.txt": "hello",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
project::trusted_worktrees::init(DbTrustedPaths::default(), cx);
|
||||
});
|
||||
|
||||
let project =
|
||||
Project::test_with_worktree_trust(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let worktree_id = worktree_store.read_with(cx, |store, cx| {
|
||||
store.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let repository = project.read_with(cx, |project, cx| {
|
||||
project.repositories(cx).values().next().unwrap().clone()
|
||||
});
|
||||
|
||||
repository.read_with(cx, |repo, _| {
|
||||
assert!(!repo.is_trusted(), "repository should start untrusted");
|
||||
});
|
||||
|
||||
let trusted_worktrees = cx
|
||||
.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should be set"));
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
repository.read_with(cx, |repo, _| {
|
||||
assert!(
|
||||
repo.is_trusted(),
|
||||
"repository should be trusted after worktree is trusted"
|
||||
);
|
||||
});
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.restrict(
|
||||
worktree_store.downgrade(),
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
repository.read_with(cx, |repo, _| {
|
||||
assert!(
|
||||
!repo.is_trusted(),
|
||||
"repository should be untrusted after worktree is restricted"
|
||||
);
|
||||
});
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
repository.read_with(cx, |repo, _| {
|
||||
assert!(
|
||||
repo.is_trusted(),
|
||||
"repository should be trusted again after second trust"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue