first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,17 +1,19 @@
|
||||
use std::path::Path;
|
||||
use std::{collections::HashSet, future::Future, path::PathBuf};
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
#[cfg(test)]
|
||||
use galaxyui::r#async::FutureId;
|
||||
use galaxyui::{AppContext, Entity, ModelContext, ModelHandle};
|
||||
use futures::future::{ready, Either};
|
||||
#[cfg(test)]
|
||||
use virtual_fs::{Stub, VirtualFS};
|
||||
use galaxy_util::host_id::HostId;
|
||||
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
|
||||
use galaxy_util::remote_path::{RemoteNavigationResult, RemotePath};
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
#[cfg(test)]
|
||||
use galaxyui_core::r#async::FutureId;
|
||||
use galaxyui_core::{AppContext, Entity, ModelContext, ModelHandle, SingletonEntity};
|
||||
|
||||
use crate::DirectoryWatcher;
|
||||
use crate::Repository;
|
||||
use futures::future::{ready, Either};
|
||||
use galaxyui::SingletonEntity;
|
||||
use crate::{DirectoryWatcher, Repository};
|
||||
|
||||
/// Indicates why a repository detection event was emitted.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -36,17 +38,57 @@ pub enum DetectedRepositoriesEvent {
|
||||
/// Tracks the detected _git_ repositories during the lifetime of the application. This should be the canonical source of truth for repository information.
|
||||
#[derive(Default)]
|
||||
pub struct DetectedRepositories {
|
||||
repository_roots: HashSet<StandardizedPath>,
|
||||
repository_roots: HashSet<LocalOrRemotePath>,
|
||||
#[cfg(test)]
|
||||
/// List of spawned background tasks, for testing.
|
||||
spawned_futures: Vec<FutureId>,
|
||||
}
|
||||
|
||||
impl DetectedRepositories {
|
||||
/// Detects the git repository root for the given working directory.
|
||||
///
|
||||
/// For **local sessions**, pass `None` for `remote_detect` — this delegates
|
||||
/// to the local filesystem detection path.
|
||||
///
|
||||
/// For **remote sessions**, pass `Some(future)` where the future resolves
|
||||
/// with `(RemotePath, is_git)` from the remote server. The future is
|
||||
/// typically obtained from `RemoteServerManager::navigate_to_directory`.
|
||||
/// When `is_git` is true, the result is `Some(LocalOrRemotePath::Remote(...))`.
|
||||
///
|
||||
/// This design avoids a circular dependency between `repo_metadata` and
|
||||
/// `remote_server` — the caller in `app/` constructs the remote future
|
||||
/// and injects it here.
|
||||
pub fn detect_possible_git_repo<
|
||||
F: Future<Output = Option<RemoteNavigationResult>> + 'static,
|
||||
>(
|
||||
&mut self,
|
||||
active_directory: &str,
|
||||
source: RepoDetectionSource,
|
||||
remote_detect: Option<F>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> impl Future<Output = Option<LocalOrRemotePath>> {
|
||||
match remote_detect {
|
||||
None => {
|
||||
// Local detection path.
|
||||
let fut = self.detect_possible_local_git_repo(active_directory, source, ctx);
|
||||
Either::Left(async move { fut.await.map(LocalOrRemotePath::Local) })
|
||||
}
|
||||
Some(remote_fut) => Either::Right(async move {
|
||||
match remote_fut.await {
|
||||
Some(RemoteNavigationResult {
|
||||
remote_path,
|
||||
is_git: true,
|
||||
}) => Some(LocalOrRemotePath::Remote(remote_path)),
|
||||
_ => None,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the active directory pwd, kick off a background job to detect the git project root and emit an event
|
||||
/// to interested listeners.
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
|
||||
pub fn detect_possible_git_repo(
|
||||
pub fn detect_possible_local_git_repo(
|
||||
&mut self,
|
||||
active_directory: &str,
|
||||
source: RepoDetectionSource,
|
||||
@@ -61,19 +103,28 @@ impl DetectedRepositories {
|
||||
return Either::Right(ready(None));
|
||||
};
|
||||
|
||||
if let Some(repository) = self.repository_roots.get(&path) {
|
||||
if let Some(local_path) = repository.to_local_path() {
|
||||
if let Some(repository) =
|
||||
DirectoryWatcher::as_ref(ctx).get_watched_directory_for_path(&local_path)
|
||||
{
|
||||
ctx.emit(DetectedRepositoriesEvent::DetectedGitRepo {
|
||||
repository: repository.clone(),
|
||||
source,
|
||||
});
|
||||
let local_key = path.to_local_path().map(LocalOrRemotePath::Local);
|
||||
if let Some(ref key) = local_key {
|
||||
if self.repository_roots.contains(key) {
|
||||
if let Some(local_path) = path.to_local_path() {
|
||||
if let Some(repository) = DirectoryWatcher::as_ref(ctx)
|
||||
.get_watched_directory_for_path(&local_path)
|
||||
{
|
||||
ctx.emit(DetectedRepositoriesEvent::DetectedGitRepo {
|
||||
repository: repository.clone(),
|
||||
source,
|
||||
});
|
||||
// Watcher is alive — use the cached result.
|
||||
return Either::Right(ready(path.to_local_path()));
|
||||
}
|
||||
// Watcher was cleaned up (e.g. diff state model dropped
|
||||
// and recreated). Fall through to the full scan which
|
||||
// will re-register the watcher.
|
||||
} else {
|
||||
return Either::Right(ready(path.to_local_path()));
|
||||
}
|
||||
}
|
||||
return Either::Right(ready(repository.to_local_path()));
|
||||
};
|
||||
}
|
||||
|
||||
let local_path_for_search = path.to_local_path();
|
||||
let (tx, rx) = oneshot::channel::<Option<PathBuf>>();
|
||||
@@ -92,7 +143,10 @@ impl DetectedRepositories {
|
||||
.as_ref()
|
||||
.and_then(|path| StandardizedPath::from_local_canonicalized(path).ok())
|
||||
{
|
||||
me.repository_roots.insert(repo_root_path.clone());
|
||||
if let Some(local_path) = repo_root_path.to_local_path() {
|
||||
me.repository_roots
|
||||
.insert(LocalOrRemotePath::Local(local_path));
|
||||
}
|
||||
|
||||
let external_git_dir = StandardizedPath::from_local_canonicalized(
|
||||
info.git_dir_path.as_path(),
|
||||
@@ -152,35 +206,95 @@ impl DetectedRepositories {
|
||||
&self.spawned_futures
|
||||
}
|
||||
|
||||
/// Given a path, return its corresdponding watched repository, if any.
|
||||
pub fn get_watched_repo_for_path(
|
||||
/// Given a local path, return its corresponding watched repository, if any.
|
||||
pub fn get_local_watched_repo_for_path(
|
||||
&self,
|
||||
path: &Path,
|
||||
ctx: &AppContext,
|
||||
) -> Option<ModelHandle<Repository>> {
|
||||
let root = self.get_root_for_path(path)?;
|
||||
DirectoryWatcher::as_ref(ctx).get_watched_directory_for_path(&root)
|
||||
let root = self.get_root_for_path(&LocalOrRemotePath::Local(path.to_path_buf()))?;
|
||||
let local_path = root.to_local_path()?;
|
||||
DirectoryWatcher::as_ref(ctx).get_watched_directory_for_path(local_path)
|
||||
}
|
||||
|
||||
/// Given a path, return its corresponding repo root. Note that this does not run the check
|
||||
/// against the actual file system. Instead it checks against our cached path to root mapping.
|
||||
pub fn get_root_for_path(&self, path: &Path) -> Option<PathBuf> {
|
||||
let std_path = StandardizedPath::from_local_canonicalized(path).ok()?;
|
||||
let repo = self.find_repository_root(&std_path)?;
|
||||
repo.to_local_path()
|
||||
/// Given a local or remote path, return its corresponding repo root.
|
||||
///
|
||||
/// No git detection is performed; roots are looked up in our cached
|
||||
/// path-to-root mapping. Note that for local paths this still hits the
|
||||
/// file system: the path is canonicalized first (resolving symlinks and
|
||||
/// requiring it to exist) so it can match the canonicalized cached roots.
|
||||
/// If the input is already canonicalized, prefer
|
||||
/// [`Self::get_root_for_canonical_path`], which performs no I/O.
|
||||
pub fn get_root_for_path(&self, path: &LocalOrRemotePath) -> Option<LocalOrRemotePath> {
|
||||
match path {
|
||||
LocalOrRemotePath::Local(local_path) => {
|
||||
let std_path = StandardizedPath::from_local_canonicalized(local_path).ok()?;
|
||||
self.find_local_repository_root(&std_path)
|
||||
}
|
||||
LocalOrRemotePath::Remote(remote_path) => self.find_remote_repository_root(remote_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the repository that contains the given path, if any.
|
||||
fn find_repository_root(&self, path: &StandardizedPath) -> Option<StandardizedPath> {
|
||||
let mut current = Some(path.clone());
|
||||
while let Some(ancestor) = current {
|
||||
if let Some(repo) = self.repository_roots.get(&ancestor) {
|
||||
return Some(repo.clone());
|
||||
/// Given a local or remote path, return its corresponding repo root.
|
||||
/// This does not run the check against the actual file system.
|
||||
/// Instead it checks against our cached path to root mapping.
|
||||
///
|
||||
/// Local paths must already be canonicalized (symlinks resolved); they
|
||||
/// are only normalized here, without any filesystem I/O. A
|
||||
/// non-canonical path may fail to match the canonicalized cached roots
|
||||
/// — use [`Self::get_root_for_path`] for such paths instead.
|
||||
pub fn get_root_for_canonical_path(
|
||||
&self,
|
||||
path: &LocalOrRemotePath,
|
||||
) -> Option<LocalOrRemotePath> {
|
||||
match path {
|
||||
LocalOrRemotePath::Local(local_path) => {
|
||||
let std_path = StandardizedPath::try_from_local(local_path).ok()?;
|
||||
self.find_local_repository_root(&std_path)
|
||||
}
|
||||
LocalOrRemotePath::Remote(remote_path) => self.find_remote_repository_root(remote_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the local repository that contains the given path, if any.
|
||||
fn find_local_repository_root(&self, path: &StandardizedPath) -> Option<LocalOrRemotePath> {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(local_path) = ancestor.to_local_path() {
|
||||
let key = LocalOrRemotePath::Local(local_path);
|
||||
if self.repository_roots.contains(&key) {
|
||||
return Some(key);
|
||||
}
|
||||
}
|
||||
current = ancestor.parent();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the remote repository that contains the given path, if any.
|
||||
fn find_remote_repository_root(&self, remote_path: &RemotePath) -> Option<LocalOrRemotePath> {
|
||||
for ancestor in remote_path.path.ancestors() {
|
||||
let candidate =
|
||||
LocalOrRemotePath::Remote(RemotePath::new(remote_path.host_id.clone(), ancestor));
|
||||
if self.repository_roots.contains(&candidate) {
|
||||
return Some(candidate);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Register a remote repository root discovered via the remote server.
|
||||
pub fn register_remote_repo_root(&mut self, remote_path: RemotePath) {
|
||||
self.repository_roots
|
||||
.insert(LocalOrRemotePath::Remote(remote_path));
|
||||
}
|
||||
|
||||
/// Remove all cached repository roots for a given remote host.
|
||||
/// Call on `HostDisconnected` to prevent stale entries.
|
||||
pub fn remove_roots_for_host(&mut self, host_id: &HostId) {
|
||||
self.repository_roots.retain(|entry| match entry {
|
||||
LocalOrRemotePath::Local(_) => true,
|
||||
LocalOrRemotePath::Remote(remote) => remote.host_id != *host_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for DetectedRepositories {
|
||||
@@ -192,9 +306,12 @@ impl SingletonEntity for DetectedRepositories {}
|
||||
/// Test helpers: direct mutation of internal state.
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
impl DetectedRepositories {
|
||||
/// Insert a repository root path directly, bypassing git detection.
|
||||
/// Insert a local repository root path directly, bypassing git detection.
|
||||
pub fn insert_test_repo_root(&mut self, path: StandardizedPath) {
|
||||
self.repository_roots.insert(path);
|
||||
if let Some(local_path) = path.to_local_path() {
|
||||
self.repository_roots
|
||||
.insert(LocalOrRemotePath::Local(local_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user