scan local repo

This commit is contained in:
2026-08-31 07:54:41 +07:00
parent 767638eda2
commit 6fcc945dae
8 changed files with 366 additions and 34 deletions
+38 -11
View File
@@ -1,5 +1,6 @@
mod backend;
mod git_store;
mod local_repos;
mod profile;
mod repo;
mod repo_list;
@@ -9,6 +10,7 @@ use std::path::{Path, PathBuf};
pub use backend::{Backend, BackendEvent};
pub use git_store::GitStore;
use gpui::{App, AppContext, Entity};
pub use local_repos::LocalReposStore;
pub use nostr_sdk::prelude::Timestamp;
pub use profile::{Profile, ProfileStore};
pub use repo::RepoStore;
@@ -16,8 +18,15 @@ pub use repo_list::{RepoActivityCounts, RepoListStore};
use signed_nostr::new_backend;
pub use utils::shorten_pubkey;
/// Initialize the backend and stores, and install them as globals. Call once
/// at startup, before opening any window that uses the stores.
/// The default directories scanned for local git repositories
/// on every platform: the user's Desktop and Documents folders.
#[cfg(not(target_arch = "wasm32"))]
fn default_scan_paths() -> Vec<PathBuf> {
vec![paths::desktop_dir(), paths::documents_dir()]
}
/// Initialize the backend and stores, and install them as globals.
/// Call once at startup, before opening any window that uses the stores.
#[cfg(not(target_arch = "wasm32"))]
pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
// rustls uses the `aws_lc_rs` provider by default; ignore if already installed.
@@ -25,46 +34,64 @@ pub fn init(db_path: impl AsRef<Path>, cx: &mut App) -> Entity<Backend> {
.install_default()
.ok();
let path = db_path.as_ref().to_path_buf();
// Initialize the nostr client and universal signer.
let (client, signer) = cx.foreground_executor().block_on(async move {
let path = db_path.as_ref().to_path_buf();
new_backend(path)
.await
.expect("failed to initialize nostr backend")
});
// Initialize the backend and stores.
let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx);
// Initialize the profile store.
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
// Start the explore list from the local database before the first
// window opens; relay syncs continue in the background, so the list
// never waits for them.
// Start the explore list from the local database before
// the first window opens, relay syncs continue in the background,
// so the list never waits for them.
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
// The clone cache is only meaningful on native platforms; the wasm
// build registers an empty store so `GitStore::global` still works.
// The clone cache is only meaningful on native platforms,
// the wasm build registers an empty store so `GitStore::global` still works.
GitStore::set_global(PathBuf::new(), cx);
// Scan the default directories (Desktop, Documents) for local git
// repositories; the sidebar lists them next to the user's NIP-34 repos.
LocalReposStore::set_global(
cx.new(|cx| LocalReposStore::new(default_scan_paths(), cx)),
cx,
);
entity
}
/// Initialize the backend with an in-memory database on wasm.
#[cfg(target_arch = "wasm32")]
pub fn init(cx: &mut App) -> Entity<Backend> {
// Initialize the nostr client and universal signer.
let (client, signer) = new_backend().expect("failed to initialize nostr backend");
// Initialize the backend and stores.
let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx);
// Initialize the profile store.
ProfileStore::set_global(cx.new(ProfileStore::new), cx);
// Start the explore list from the local database before the first
// window opens; relay syncs continue in the background, so the list
// never waits for them.
// Start the explore list from the local database before
// the first window opens, relay syncs continue in the background,
// so the list never waits for them.
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx);
// The clone cache is only meaningful on native platforms,
// the wasm build registers an empty store so `GitStore::global` still works.
GitStore::set_global(PathBuf::new(), cx);
// No filesystem scan on wasm: there are no local git repositories.
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx);
entity
}
+101
View File
@@ -0,0 +1,101 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use signed_git::find_git_repos;
struct GlobalLocalReposStore(Entity<LocalReposStore>);
impl Global for GlobalLocalReposStore {}
/// Store of the git repositories discovered under a set of scan paths.
///
/// Created at startup by [`crate::init`] with the default scan paths
/// (the Desktop and Documents folders; empty on wasm, where no scan runs),
/// then installed as a global so the sidebar can list local repositories.
/// The scan runs on a background thread; only the results cross back into the entity.
pub struct LocalReposStore {
/// The directories being scanned.
pub roots: Arc<Vec<PathBuf>>,
/// Git repositories discovered under [`Self::roots`], sorted by path.
pub repos: Arc<Vec<PathBuf>>,
/// A scan is currently running.
pub scanning: bool,
/// A scan was requested while one was already running.
scan_dirty: bool,
tasks: Vec<Task<Result<(), Error>>>,
}
impl LocalReposStore {
/// Retrieve the global local-repositories store
/// (created at startup by [`crate::init`]).
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalLocalReposStore>().0.clone()
}
pub(crate) fn set_global(entity: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalLocalReposStore(entity));
}
/// Create a store scanning `roots` right away
/// (a no-op when the list is empty, e.g. on wasm).
pub fn new(roots: Vec<PathBuf>, cx: &mut Context<Self>) -> Self {
let mut store = Self {
roots: Arc::new(roots),
repos: Arc::new(Vec::new()),
scanning: false,
scan_dirty: false,
tasks: Vec::new(),
};
store.rescan(cx);
store
}
/// Re-run the scan. Requests that arrive while a scan is running are
/// folded into one follow-up scan; the results replace the list atomically.
pub fn rescan(&mut self, cx: &mut Context<Self>) {
if self.scanning {
self.scan_dirty = true;
return;
}
if self.roots.is_empty() {
return;
}
self.scanning = true;
cx.notify();
let roots = self.roots.clone();
let work = cx.background_spawn(async move {
let mut repos = Vec::new();
for root in roots.iter() {
repos.extend(find_git_repos(root));
}
repos.sort();
repos.dedup();
repos
});
self.tasks.push(cx.spawn(async move |this, cx| {
let repos = work.await;
let again = this.update(cx, |this, cx| {
this.repos = Arc::new(repos);
this.scanning = false;
cx.notify();
let dirty = this.scan_dirty;
this.scan_dirty = false;
dirty
})?;
// Scans requested while this one was running are coalesced into
// a single follow-up scan.
if again {
this.update(cx, |this, cx| this.rescan(cx))?;
}
Ok(())
}));
}
}