first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,29 +1,30 @@
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use std::path::Path;
|
||||
|
||||
use futures::future::ready;
|
||||
use galaxy_util::standardized_path::StandardizedPath;
|
||||
use galaxyui::r#async::{BoxFuture, SpawnedFutureHandle};
|
||||
use galaxyui_core::r#async::{BoxFuture, SpawnedFutureHandle};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use galaxyui::SingletonEntity;
|
||||
use galaxyui::{Entity, ModelContext, ModelHandle};
|
||||
use galaxyui_core::SingletonEntity;
|
||||
use galaxyui_core::{Entity, ModelContext, ModelHandle};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use ignore::gitignore::Gitignore;
|
||||
#[cfg(feature = "local_fs")]
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::watcher::DirectoryWatcher;
|
||||
use crate::watcher::TaskQueue;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::{
|
||||
entry::{matches_gitignores, should_ignore_git_path},
|
||||
gitignores_for_directory,
|
||||
};
|
||||
use crate::{watcher::TaskQueue, RepoMetadataError, RepositoryUpdate};
|
||||
use crate::{RepoMetadataError, RepositoryUpdate};
|
||||
|
||||
/// Trait for entities that want to subscribe to repository file changes.
|
||||
pub trait RepositorySubscriber: Send + Sync {
|
||||
@@ -74,10 +75,46 @@ pub struct Repository {
|
||||
/// Cached gitignore patterns for this repository.
|
||||
#[cfg(feature = "local_fs")]
|
||||
gitignores: Vec<Gitignore>,
|
||||
/// Cached loose remote-tracking ref tracked by the active branch.
|
||||
#[cfg(feature = "local_fs")]
|
||||
tracked_remote_ref: Option<TrackedRemoteRef>,
|
||||
|
||||
task_queue: ModelHandle<TaskQueue>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct TrackedRemoteRef {
|
||||
full_ref_name: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
impl TrackedRemoteRef {
|
||||
pub(crate) fn from_full_ref_name(full_ref_name: impl Into<String>) -> Option<Self> {
|
||||
let full_ref_name = full_ref_name.into();
|
||||
if !full_ref_name.starts_with("refs/remotes/") {
|
||||
return None;
|
||||
}
|
||||
let ref_path = Path::new(&full_ref_name);
|
||||
if ref_path.has_root() {
|
||||
return None;
|
||||
}
|
||||
let mut component_count = 0;
|
||||
for component in ref_path.components() {
|
||||
match component {
|
||||
Component::Normal(_) => component_count += 1,
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
|
||||
}
|
||||
}
|
||||
(component_count >= 4).then_some(Self { full_ref_name })
|
||||
}
|
||||
|
||||
fn full_ref_name(&self) -> &str {
|
||||
&self.full_ref_name
|
||||
}
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
/// Creates a new Repository instance.
|
||||
pub(super) fn new(
|
||||
@@ -91,13 +128,9 @@ impl Repository {
|
||||
gitignores_for_directory(&local_path)
|
||||
};
|
||||
|
||||
let common_git_directory = external_git_directory.as_ref().and_then(|ext| {
|
||||
ext.to_local_path()
|
||||
.and_then(|local| Self::derive_common_git_dir(&local))
|
||||
.and_then(|p| StandardizedPath::try_from_local(&p).ok())
|
||||
// Only store when it differs from external_git_directory.
|
||||
.filter(|common| common != ext)
|
||||
});
|
||||
let common_git_directory = external_git_directory
|
||||
.as_ref()
|
||||
.and_then(Self::derive_common_git_directory);
|
||||
|
||||
Self {
|
||||
root_dir,
|
||||
@@ -107,6 +140,8 @@ impl Repository {
|
||||
next_subscriber_id: 0,
|
||||
#[cfg(feature = "local_fs")]
|
||||
gitignores,
|
||||
#[cfg(feature = "local_fs")]
|
||||
tracked_remote_ref: None,
|
||||
task_queue,
|
||||
}
|
||||
}
|
||||
@@ -123,6 +158,17 @@ impl Repository {
|
||||
None
|
||||
}
|
||||
|
||||
fn derive_common_git_directory(
|
||||
external_git_directory: &StandardizedPath,
|
||||
) -> Option<StandardizedPath> {
|
||||
external_git_directory
|
||||
.to_local_path()
|
||||
.and_then(|local| Self::derive_common_git_dir(&local))
|
||||
.and_then(|path| StandardizedPath::try_from_local(&path).ok())
|
||||
// Only store when it differs from external_git_directory.
|
||||
.filter(|common| common != external_git_directory)
|
||||
}
|
||||
|
||||
/// The root directory of this repository.
|
||||
pub fn root_dir(&self) -> &StandardizedPath {
|
||||
&self.root_dir
|
||||
@@ -134,6 +180,23 @@ impl Repository {
|
||||
self.external_git_directory.as_ref()
|
||||
}
|
||||
|
||||
/// Adds linked-worktree git metadata when it was not known at registration time.
|
||||
///
|
||||
/// Directory registrations can be created from a raw path before git detection completes.
|
||||
/// Preserve any metadata already associated with the repository, since later raw-path
|
||||
/// registrations must not downgrade a known linked worktree.
|
||||
pub(super) fn enrich_external_git_directory(
|
||||
&mut self,
|
||||
external_git_directory: StandardizedPath,
|
||||
) {
|
||||
if self.external_git_directory.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.common_git_directory = Self::derive_common_git_directory(&external_git_directory);
|
||||
self.external_git_directory = Some(external_git_directory);
|
||||
}
|
||||
|
||||
/// Returns the path to the actual `.git` directory for this repository.
|
||||
///
|
||||
/// For normal repositories this is `root_dir/.git`. For worktrees, the
|
||||
@@ -159,6 +222,109 @@ impl Repository {
|
||||
.unwrap_or_else(|| self.git_dir())
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(crate) fn tracked_remote_ref_path(&self) -> Option<PathBuf> {
|
||||
self.tracked_remote_ref
|
||||
.as_ref()
|
||||
.map(|tracked_ref| self.common_git_dir().join(tracked_ref.full_ref_name()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(crate) fn tracks_remote_ref_path(&self, remote_ref_path: &Path) -> bool {
|
||||
self.tracked_remote_ref_path().is_some_and(|tracked_path| {
|
||||
Self::path_for_comparison(&tracked_path) == Self::path_for_comparison(remote_ref_path)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(crate) fn update_tracked_remote_ref(
|
||||
&mut self,
|
||||
tracked_remote_ref: Option<TrackedRemoteRef>,
|
||||
) -> bool {
|
||||
if self.tracked_remote_ref == tracked_remote_ref {
|
||||
return false;
|
||||
}
|
||||
self.tracked_remote_ref = tracked_remote_ref;
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn path_for_comparison(path: &Path) -> PathBuf {
|
||||
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(crate) async fn resolve_tracked_remote_ref(root_dir: PathBuf) -> Option<TrackedRemoteRef> {
|
||||
let output = galaxy_util::git::run_git_command(
|
||||
&root_dir,
|
||||
&["rev-parse", "--symbolic-full-name", "@{u}"],
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
let full_ref_name = output.lines().next()?.trim();
|
||||
TrackedRemoteRef::from_full_ref_name(full_ref_name)
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub(crate) fn refresh_tracked_remote_ref(
|
||||
&mut self,
|
||||
notify: bool,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let root_dir = self.root_dir().to_local_path_lossy();
|
||||
ctx.spawn(
|
||||
Repository::resolve_tracked_remote_ref(root_dir),
|
||||
move |repository, tracked_remote_ref, ctx| {
|
||||
let tracked_remote_ref_changed =
|
||||
repository.update_tracked_remote_ref(tracked_remote_ref);
|
||||
if notify && tracked_remote_ref_changed {
|
||||
repository.enqueue_remote_ref_update(ctx);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn enqueue_remote_ref_update(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
let repository_handle = ctx.handle();
|
||||
let subscriber_ids = self.get_subscriber_ids();
|
||||
let update = RepositoryUpdate {
|
||||
remote_ref_updated: true,
|
||||
..Default::default()
|
||||
};
|
||||
self.task_queue.update(ctx, |queue, ctx| {
|
||||
for subscriber_id in subscriber_ids {
|
||||
queue.enqueue_incremental_update(
|
||||
repository_handle.clone(),
|
||||
subscriber_id,
|
||||
update.clone(),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
fn watch_paths(&self) -> Vec<StandardizedPath> {
|
||||
let mut paths = vec![self.root_dir.clone()];
|
||||
if let Some(external_git_dir) = &self.external_git_directory {
|
||||
paths.push(external_git_dir.clone());
|
||||
}
|
||||
if let Some(common_git_dir) = &self.common_git_directory {
|
||||
if let Some(common_local) = common_git_dir.to_local_path() {
|
||||
let refs_dir = common_local.join("refs");
|
||||
if let Ok(refs_std) = StandardizedPath::from_local_canonicalized(&refs_dir) {
|
||||
paths.push(refs_std);
|
||||
}
|
||||
let config_file = common_local.join("config");
|
||||
if let Ok(config_std) = StandardizedPath::from_local_canonicalized(&config_file) {
|
||||
paths.push(config_std);
|
||||
}
|
||||
}
|
||||
}
|
||||
paths
|
||||
}
|
||||
|
||||
/// Returns the current watcher count.
|
||||
pub fn watcher_count(&self) -> usize {
|
||||
self.subscribers.len()
|
||||
@@ -186,31 +352,16 @@ impl Repository {
|
||||
#[cfg(feature = "local_fs")]
|
||||
let registration_future: BoxFuture<'static, Result<(), RepoMetadataError>> =
|
||||
if should_start_watching {
|
||||
// Prepare list of directories to watch
|
||||
let mut directories_to_watch = vec![self.root_dir.clone()];
|
||||
let directories_to_watch = self.watch_paths();
|
||||
// Reuse the gitignores we already built at construction so the
|
||||
// watch descend filter doesn't re-read `.gitignore` from disk.
|
||||
let gitignores = self.gitignores.clone();
|
||||
|
||||
// Watch the per-worktree gitdir for worktree-specific events
|
||||
// (HEAD, index.lock under .git/worktrees/<name>/).
|
||||
if let Some(external_git_dir) = &self.external_git_directory {
|
||||
directories_to_watch.push(external_git_dir.clone());
|
||||
}
|
||||
|
||||
// For linked worktrees, also watch .git/refs so shared ref
|
||||
// changes (refs/heads/*) are visible even when the main
|
||||
// worktree isn't registered.
|
||||
if let Some(common_git_dir) = &self.common_git_directory {
|
||||
if let Some(common_local) = common_git_dir.to_local_path() {
|
||||
let refs_dir = common_local.join("refs").join("heads");
|
||||
if let Ok(refs_std) = StandardizedPath::from_local_canonicalized(&refs_dir)
|
||||
{
|
||||
directories_to_watch.push(refs_std);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box::pin(DirectoryWatcher::handle(ctx).update(ctx, |watcher, ctx| {
|
||||
watcher.start_watching_directories(directories_to_watch, ctx)
|
||||
}))
|
||||
Box::pin(
|
||||
DirectoryWatcher::handle(ctx).update(ctx, move |watcher, ctx| {
|
||||
watcher.start_watching_directories(directories_to_watch, gitignores, ctx)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
Box::pin(ready(Ok(())))
|
||||
};
|
||||
@@ -223,6 +374,8 @@ impl Repository {
|
||||
self.task_queue.update(ctx, |queue, ctx| {
|
||||
queue.enqueue_scan(self_handle, subscriber_id, ctx);
|
||||
});
|
||||
#[cfg(feature = "local_fs")]
|
||||
self.refresh_tracked_remote_ref(false, ctx);
|
||||
|
||||
StartWatching {
|
||||
subscriber_id,
|
||||
@@ -252,21 +405,8 @@ impl Repository {
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
DirectoryWatcher::handle(ctx).update(ctx, |watcher, ctx| {
|
||||
// Stop watching the working tree directory
|
||||
std::mem::drop(watcher.stop_watching_directory(&self.root_dir, ctx));
|
||||
// Mirror start_watching: stop per-worktree gitdir + shared refs.
|
||||
if let Some(external_git_dir) = &self.external_git_directory {
|
||||
std::mem::drop(watcher.stop_watching_directory(external_git_dir, ctx));
|
||||
}
|
||||
if let Some(common_git_dir) = &self.common_git_directory {
|
||||
if let Some(common_local) = common_git_dir.to_local_path() {
|
||||
let refs_dir = common_local.join("refs").join("heads");
|
||||
if let Ok(refs_std) =
|
||||
StandardizedPath::from_local_canonicalized(&refs_dir)
|
||||
{
|
||||
std::mem::drop(watcher.stop_watching_directory(&refs_std, ctx));
|
||||
}
|
||||
}
|
||||
for path in self.watch_paths() {
|
||||
std::mem::drop(watcher.stop_watching_directory(&path, ctx));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -393,6 +533,7 @@ fn merge_repository_updates(acc: &mut RepositoryUpdate, incoming: &RepositoryUpd
|
||||
|
||||
acc.commit_updated |= incoming.commit_updated;
|
||||
acc.index_lock_detected |= incoming.index_lock_detected;
|
||||
acc.remote_ref_updated |= incoming.remote_ref_updated;
|
||||
}
|
||||
|
||||
/// A generic debouncing layer for any RepositorySubscriber.
|
||||
@@ -459,7 +600,7 @@ where
|
||||
let st = state.lock().unwrap();
|
||||
st.version
|
||||
};
|
||||
galaxyui::r#async::Timer::after(wait).await;
|
||||
galaxyui_core::r#async::Timer::after(wait).await;
|
||||
|
||||
// If version unchanged, we're quiet; flush pending and exit loop.
|
||||
let maybe_merged = {
|
||||
@@ -507,3 +648,7 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "repository_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user