refactor 3
This commit is contained in:
+208
-73
@@ -31,8 +31,14 @@ const MAX_PATCH_EVENT_BYTES: usize = 60 * 1024;
|
||||
/// Holds the announcement, state, issues, patches, PRs, comments and resolved statuses.
|
||||
/// Always derived from the local database.
|
||||
pub struct RepoStore {
|
||||
addr: RepoAddr,
|
||||
/// NIP-34 address. `None` while the repository is local-only.
|
||||
addr: Option<RepoAddr>,
|
||||
/// Latest announcement. Seeded from the open-time hint, replaced by the
|
||||
/// database's latest on the first pass. `None` while local-only.
|
||||
pub announcement: Option<Announcement>,
|
||||
/// Local working copy. The scan path for a local repository, kept when it is
|
||||
/// later announced so the panel keeps its worktree.
|
||||
pub path: Option<PathBuf>,
|
||||
/// The first local pass has been applied.
|
||||
///
|
||||
/// Views distinguish "no data yet" from a genuinely empty repository with it.
|
||||
@@ -82,52 +88,21 @@ pub struct RepoStore {
|
||||
root_fetches: HashSet<EventId>,
|
||||
/// Refresh coalescing, see [`RefreshGate`].
|
||||
refresh: RefreshGate,
|
||||
_subscription: Subscription,
|
||||
/// Backend subscription of an announced repository. `None` while local-only.
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl RepoStore {
|
||||
pub fn new(addr: RepoAddr, announced_relays: Vec<RelayUrl>, cx: &mut Context<Self>) -> Self {
|
||||
let backend = Backend::global(cx);
|
||||
|
||||
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
|
||||
let relevant = match event {
|
||||
BackendEvent::NostrUpdate(updates) => updates.iter().any(|update| {
|
||||
// Deletions may target any event of this repository.
|
||||
let deletion =
|
||||
update.kind == Kind::EventDeletion || update.kind == Kind::RequestToVanish;
|
||||
let coordinate = update.coordinate.as_ref() == Some(&this.addr);
|
||||
let author = update.author == this.addr.public_key;
|
||||
let kind = update.kind == Kind::GitRepoAnnouncement;
|
||||
// NIP-22 comments carry no `a` tag.
|
||||
// Coordinate matching fails for them.
|
||||
// Any comment may reference this repository's roots.
|
||||
let comment = update.kind == Kind::Comment;
|
||||
// Status events may omit their `a` tag, NIP-34.
|
||||
// Any status event may reference a root of this repository.
|
||||
let status = RepoStatus::from_kind(update.kind).is_some();
|
||||
|
||||
deletion || coordinate || (author && kind) || comment || status
|
||||
}),
|
||||
BackendEvent::Published(event) => {
|
||||
let kind = event.kind == Kind::GitRepoAnnouncement;
|
||||
let author = event.pubkey == this.addr.public_key;
|
||||
let coordinate = event.tags.coordinates().into_iter().any(|c| c == this.addr);
|
||||
// Locally published deletions may target any event of this repository.
|
||||
// Refresh so they take effect immediately, like relay deletions.
|
||||
let deletion =
|
||||
event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish;
|
||||
|
||||
coordinate || (kind && author) || deletion
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if relevant {
|
||||
this.refresh(cx);
|
||||
}
|
||||
});
|
||||
|
||||
/// Announced repository.
|
||||
pub fn new(addr: RepoAddr, hint: Option<Announcement>, cx: &mut Context<Self>) -> Self {
|
||||
let weak = cx.entity().downgrade();
|
||||
let subscription = Self::subscribe_backend(cx);
|
||||
|
||||
let announced_relays = hint
|
||||
.as_ref()
|
||||
.map(|announcement| announcement.relays.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
cx.defer(move |cx| {
|
||||
let result = weak.update(cx, |this, cx| {
|
||||
this.subscribe_remote(cx);
|
||||
@@ -141,8 +116,9 @@ impl RepoStore {
|
||||
});
|
||||
|
||||
Self {
|
||||
addr,
|
||||
announcement: None,
|
||||
addr: Some(addr),
|
||||
announcement: hint,
|
||||
path: None,
|
||||
loaded: false,
|
||||
head: None,
|
||||
issues: Vec::new(),
|
||||
@@ -161,13 +137,103 @@ impl RepoStore {
|
||||
repo_relays: HashSet::new(),
|
||||
root_fetches: HashSet::new(),
|
||||
refresh: RefreshGate::default(),
|
||||
_subscription: subscription,
|
||||
_subscription: Some(subscription),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the repository's address.
|
||||
pub fn addr(&self) -> &RepoAddr {
|
||||
&self.addr
|
||||
/// Local repository discovered by the scan, not announced to NIP-34 yet.
|
||||
pub fn new_local(path: PathBuf) -> Self {
|
||||
Self {
|
||||
addr: None,
|
||||
announcement: None,
|
||||
path: Some(path),
|
||||
loaded: true,
|
||||
head: None,
|
||||
issues: Vec::new(),
|
||||
patches: Vec::new(),
|
||||
pull_requests: Vec::new(),
|
||||
comments: Vec::new(),
|
||||
status_by_root: HashMap::new(),
|
||||
open_issue_count: 0,
|
||||
open_pr_count: 0,
|
||||
version: 0,
|
||||
last_error: None,
|
||||
last_warning: None,
|
||||
last_push_warning: None,
|
||||
pushing: false,
|
||||
cloning: false,
|
||||
repo_relays: HashSet::new(),
|
||||
root_fetches: HashSet::new(),
|
||||
refresh: RefreshGate::default(),
|
||||
_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch a local repository to its NIP-34 mode, keeping its path.
|
||||
pub fn announce(&mut self, announcement: Announcement, cx: &mut Context<Self>) {
|
||||
self.addr = Some(announcement.addr());
|
||||
self.announcement = Some(announcement.clone());
|
||||
self.loaded = false;
|
||||
|
||||
if self._subscription.is_none() {
|
||||
self._subscription = Some(Self::subscribe_backend(cx));
|
||||
}
|
||||
|
||||
self.subscribe_remote(cx);
|
||||
self.connect_announced_relays(&announcement.relays, cx);
|
||||
self.refresh(cx);
|
||||
}
|
||||
|
||||
/// Subscriptions to the backend events concerning this repository.
|
||||
fn subscribe_backend(cx: &mut Context<Self>) -> Subscription {
|
||||
let backend = Backend::global(cx);
|
||||
|
||||
cx.subscribe(&backend, |this, _backend, event, cx| {
|
||||
let Some(addr) = this.addr.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let relevant = match event {
|
||||
BackendEvent::NostrUpdate(updates) => updates.iter().any(|update| {
|
||||
// Deletions may target any event of this repository.
|
||||
let deletion =
|
||||
update.kind == Kind::EventDeletion || update.kind == Kind::RequestToVanish;
|
||||
let coordinate = update.coordinate.as_ref() == Some(addr);
|
||||
let author = update.author == addr.public_key;
|
||||
let kind = update.kind == Kind::GitRepoAnnouncement;
|
||||
// NIP-22 comments carry no `a` tag.
|
||||
// Coordinate matching fails for them.
|
||||
// Any comment may reference this repository's roots.
|
||||
let comment = update.kind == Kind::Comment;
|
||||
// Status events may omit their `a` tag, NIP-34.
|
||||
// Any status event may reference a root of this repository.
|
||||
let status = RepoStatus::from_kind(update.kind).is_some();
|
||||
|
||||
deletion || coordinate || (author && kind) || comment || status
|
||||
}),
|
||||
BackendEvent::Published(event) => {
|
||||
let kind = event.kind == Kind::GitRepoAnnouncement;
|
||||
let author = event.pubkey == addr.public_key;
|
||||
let coordinate = event.tags.coordinates().into_iter().any(|c| c == *addr);
|
||||
// Locally published deletions may target any event of this repository.
|
||||
// Refresh so they take effect immediately, like relay deletions.
|
||||
let deletion =
|
||||
event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish;
|
||||
|
||||
coordinate || (kind && author) || deletion
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if relevant {
|
||||
this.refresh(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the repository's NIP-34 address. `None` while it is local-only.
|
||||
pub fn addr(&self) -> Option<&RepoAddr> {
|
||||
self.addr.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the repository's name, or `Unknown` when not known.
|
||||
@@ -199,6 +265,10 @@ impl RepoStore {
|
||||
|
||||
/// Fetch this repository's events from the relays in its NIP-34 `relays` tag.
|
||||
fn connect_announced_relays(&mut self, relays: &[RelayUrl], cx: &mut Context<Self>) {
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new: Vec<RelayUrl> = relays
|
||||
.iter()
|
||||
.filter(|url| !self.repo_relays.contains(*url))
|
||||
@@ -211,7 +281,6 @@ impl RepoStore {
|
||||
self.repo_relays.extend(new.iter().cloned());
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
let addr = self.addr.clone();
|
||||
|
||||
backend.update(cx, |backend, cx| {
|
||||
backend.connect_repo_relays(new, Self::repo_filters(&addr), cx);
|
||||
@@ -220,8 +289,11 @@ impl RepoStore {
|
||||
|
||||
/// Fetch this repository's events from the bootstrap relays.
|
||||
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
let addr = self.addr.clone();
|
||||
|
||||
backend.update(cx, |backend, cx| {
|
||||
backend.subscribe_bootstrap(Self::repo_filters(&addr), cx);
|
||||
@@ -233,6 +305,10 @@ impl RepoStore {
|
||||
/// Runs immediately. The backend pump already batches the relay events that
|
||||
/// trigger a refresh, so no per-store debounce is needed.
|
||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
if self.addr.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.refresh.request() != RefreshRequest::Schedule {
|
||||
return;
|
||||
}
|
||||
@@ -241,11 +317,14 @@ impl RepoStore {
|
||||
}
|
||||
|
||||
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.refresh.begin();
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
let client = backend.read(cx).client();
|
||||
let addr = self.addr.clone();
|
||||
|
||||
let work = cx.background_spawn(async move {
|
||||
let (announcements, states, activity, deletion_events) = async {
|
||||
@@ -405,12 +484,18 @@ impl RepoStore {
|
||||
// The first pass is the exception: it must notify even when it
|
||||
// found nothing, so views can leave their loading state and show
|
||||
// the empty result.
|
||||
//
|
||||
// Keep the open-time hint until that first pass has confirmed what
|
||||
// the database holds; afterwards the database is the truth,
|
||||
// including a deletion.
|
||||
let keep_hint = announcement.is_none() && !this.loaded;
|
||||
|
||||
let first_pass = !this.loaded;
|
||||
let head_changed = state
|
||||
.as_ref()
|
||||
.is_some_and(|(_, head)| this.head.as_deref() != head.as_deref());
|
||||
let changed = first_pass
|
||||
|| this.announcement != announcement
|
||||
|| (!keep_hint && this.announcement != announcement)
|
||||
|| head_changed
|
||||
|| this.issues != issues
|
||||
|| this.patches != patches
|
||||
@@ -418,7 +503,9 @@ impl RepoStore {
|
||||
|| this.comments != comments
|
||||
|| this.status_by_root != status_by_root;
|
||||
|
||||
this.announcement = announcement;
|
||||
if !keep_hint {
|
||||
this.announcement = announcement;
|
||||
}
|
||||
|
||||
// The announcement may list relays for this repository's activity.
|
||||
// Connect to any we have not fetched from yet.
|
||||
@@ -526,13 +613,20 @@ impl RepoStore {
|
||||
/// The author is the public key of the repository address.
|
||||
/// Only the author may manage pull requests, close, reopen or merge.
|
||||
pub fn is_author(&self, user: &PublicKey) -> bool {
|
||||
&self.addr.public_key == user
|
||||
self.addr
|
||||
.as_ref()
|
||||
.is_some_and(|addr| &addr.public_key == user)
|
||||
}
|
||||
|
||||
/// Open an issue on this repository.
|
||||
pub fn open_issue(&mut self, subject: Option<String>, content: String, cx: &mut Context<Self>) {
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
self.not_announced(cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let builder = GitIssue {
|
||||
repository: self.addr.clone(),
|
||||
repository: addr,
|
||||
content,
|
||||
subject,
|
||||
labels: Vec::new(),
|
||||
@@ -564,6 +658,11 @@ impl RepoStore {
|
||||
content: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
self.not_announced(cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let relay_hint = self
|
||||
.announcement
|
||||
.as_ref()
|
||||
@@ -571,7 +670,7 @@ impl RepoStore {
|
||||
.cloned();
|
||||
|
||||
self.publish(
|
||||
comment_builder(root, parent, relay_hint.as_ref(), &self.addr, content),
|
||||
comment_builder(root, parent, relay_hint.as_ref(), &addr, content),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -592,6 +691,11 @@ impl RepoStore {
|
||||
self.last_error = None;
|
||||
self.last_warning = None;
|
||||
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
self.not_announced(cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let series: Vec<String> = signed_git::split_patch_series(&patch)
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
@@ -635,8 +739,7 @@ impl RepoStore {
|
||||
// The author's npub names their GRASP-06 namespace, `/prs/...`.
|
||||
let author_npub = user.to_bech32().unwrap();
|
||||
|
||||
let addr = self.addr.clone();
|
||||
let owner = self.addr.public_key;
|
||||
let owner = addr.public_key;
|
||||
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
||||
let repo_id = addr.identifier.clone();
|
||||
let base_npub = owner.to_bech32().unwrap();
|
||||
@@ -752,7 +855,7 @@ impl RepoStore {
|
||||
let clone = pr_clone_urls(prs_urls, base_clone);
|
||||
|
||||
let builder = GitPullRequest {
|
||||
repository: this.addr.clone(),
|
||||
repository: addr.clone(),
|
||||
content: description,
|
||||
subject,
|
||||
labels: Vec::new(),
|
||||
@@ -971,8 +1074,11 @@ impl RepoStore {
|
||||
.map(|p| p.id)
|
||||
});
|
||||
|
||||
let addr = self.addr.clone();
|
||||
let owner = self.addr.public_key;
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
self.not_announced(cx);
|
||||
return;
|
||||
};
|
||||
let owner = addr.public_key;
|
||||
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
||||
let root = root.clone();
|
||||
let clone: Vec<Url> = self
|
||||
@@ -1000,9 +1106,9 @@ impl RepoStore {
|
||||
});
|
||||
}
|
||||
|
||||
let builder = this.update(cx, |this, _cx| {
|
||||
let builder = this.update(cx, |_this, _cx| {
|
||||
let builder = GitPullRequestUpdate {
|
||||
repository: this.addr.clone(),
|
||||
repository: addr.clone(),
|
||||
pull_request_event: root.id,
|
||||
pull_request_author: root.pubkey,
|
||||
current_commit,
|
||||
@@ -1059,6 +1165,11 @@ impl RepoStore {
|
||||
pub fn set_status(&mut self, root: &Event, status: RepoStatus, cx: &mut Context<Self>) {
|
||||
self.last_error = None;
|
||||
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
self.not_announced(cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let maintainers = self
|
||||
.announcement
|
||||
.as_ref()
|
||||
@@ -1084,9 +1195,9 @@ impl RepoStore {
|
||||
|
||||
let builder = EventBuilder::new(status.kind(), "").tags([
|
||||
root_ref,
|
||||
Tag::public_key(self.addr.public_key),
|
||||
Tag::public_key(addr.public_key),
|
||||
Tag::public_key(root.pubkey),
|
||||
Tag::coordinate(self.addr.clone(), None),
|
||||
Tag::coordinate(addr, None),
|
||||
]);
|
||||
|
||||
self.publish(builder, cx);
|
||||
@@ -1097,6 +1208,11 @@ impl RepoStore {
|
||||
self.last_error = None;
|
||||
self.last_warning = None;
|
||||
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
self.not_announced(cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let is_author = Backend::global(cx)
|
||||
.read(cx)
|
||||
.current_user()
|
||||
@@ -1107,7 +1223,6 @@ impl RepoStore {
|
||||
}
|
||||
|
||||
let cache = GitStore::global(cx).cache().clone();
|
||||
let addr = self.addr.clone();
|
||||
|
||||
let clone_urls: Vec<Url> = self
|
||||
.announcement
|
||||
@@ -1176,12 +1291,13 @@ impl RepoStore {
|
||||
/// The latest announcement of this repository,
|
||||
/// for operations that need its clone URLs and relays.
|
||||
fn action_announcement(&self, cx: &App) -> Option<Announcement> {
|
||||
let addr = self.addr.as_ref()?;
|
||||
self.announcement.clone().or_else(|| {
|
||||
RepoListStore::global(cx)
|
||||
.read(cx)
|
||||
.announcements
|
||||
.iter()
|
||||
.find(|announcement| announcement.addr() == self.addr)
|
||||
.find(|announcement| announcement.addr() == *addr)
|
||||
.cloned()
|
||||
})
|
||||
}
|
||||
@@ -1243,6 +1359,10 @@ impl RepoStore {
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return self.action_error("This repository is not published to Nostr yet", cx);
|
||||
};
|
||||
|
||||
let Some(announcement) = self.action_announcement(cx) else {
|
||||
return self.action_error("Repository announcement is not loaded yet", cx);
|
||||
};
|
||||
@@ -1250,7 +1370,6 @@ impl RepoStore {
|
||||
// The state event's `HEAD` stays the announced default branch.
|
||||
// The checkout may be on a side branch.
|
||||
let head = self.head.clone();
|
||||
let addr = self.addr.clone();
|
||||
|
||||
self.pushing = true;
|
||||
self.last_error = None;
|
||||
@@ -1298,7 +1417,9 @@ impl RepoStore {
|
||||
/// Only the repository owner may delete it. The lists update when the
|
||||
/// deletion events arrive.
|
||||
pub fn delete_repository(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
||||
let addr = self.addr.clone();
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return self.action_error("This repository is not published to Nostr yet", cx);
|
||||
};
|
||||
self.last_error = None;
|
||||
|
||||
let backend = Backend::global(cx);
|
||||
@@ -1328,12 +1449,16 @@ impl RepoStore {
|
||||
"A clone of this repository is already in progress"
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return self.action_error("This repository is not published to Nostr yet", cx);
|
||||
};
|
||||
|
||||
let Some(announcement) = self.action_announcement(cx) else {
|
||||
return self.action_error("Repository announcement is not loaded yet", cx);
|
||||
};
|
||||
|
||||
let clone_urls = announcement.clone.clone();
|
||||
let addr = self.addr.clone();
|
||||
|
||||
self.cloning = true;
|
||||
self.last_error = None;
|
||||
@@ -1370,6 +1495,12 @@ impl RepoStore {
|
||||
})
|
||||
}
|
||||
|
||||
/// Record that an action needs a NIP-34 address this repository does not have.
|
||||
fn not_announced(&mut self, cx: &mut Context<Self>) {
|
||||
self.last_error = Some("This repository is not published to Nostr yet".into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Fail an operation whose announcement is not loaded yet.
|
||||
fn action_error(
|
||||
&mut self,
|
||||
@@ -1393,11 +1524,15 @@ impl RepoStore {
|
||||
euc: Option<&str>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(addr) = self.addr.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut tags = vec![
|
||||
Tag::parse(["e", &root.id.to_hex(), "", "root"]).expect("valid root tag"),
|
||||
Tag::public_key(self.addr.public_key),
|
||||
Tag::public_key(addr.public_key),
|
||||
Tag::public_key(root.pubkey),
|
||||
Tag::coordinate(self.addr.clone(), None),
|
||||
Tag::coordinate(addr, None),
|
||||
];
|
||||
|
||||
if let Some(euc) = euc
|
||||
|
||||
Reference in New Issue
Block a user