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
|
||||
|
||||
@@ -151,35 +151,37 @@ impl PullRequestDetailView {
|
||||
let binding = {
|
||||
let store = self.store.read(cx);
|
||||
|
||||
store
|
||||
.pull_requests
|
||||
.iter()
|
||||
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||
.map(|root| {
|
||||
let update = latest_update(store.pull_requests.iter(), root);
|
||||
store.addr().and_then(|addr| {
|
||||
store
|
||||
.pull_requests
|
||||
.iter()
|
||||
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||
.map(|root| {
|
||||
let update = latest_update(store.pull_requests.iter(), root);
|
||||
|
||||
let tip = update
|
||||
.and_then(current_commit_of)
|
||||
.or_else(|| current_commit_of(root));
|
||||
let tip = update
|
||||
.and_then(current_commit_of)
|
||||
.or_else(|| current_commit_of(root));
|
||||
|
||||
let base = update
|
||||
.and_then(merge_base_of)
|
||||
.or_else(|| merge_base_of(root));
|
||||
let base = update
|
||||
.and_then(merge_base_of)
|
||||
.or_else(|| merge_base_of(root));
|
||||
|
||||
let clone_urls = clone_urls_of(root)
|
||||
.or_else(|| store.announcement.as_ref().map(|a| a.clone.clone()))
|
||||
.unwrap_or_default();
|
||||
let clone_urls = clone_urls_of(root)
|
||||
.or_else(|| store.announcement.as_ref().map(|a| a.clone.clone()))
|
||||
.unwrap_or_default();
|
||||
|
||||
PrBinding {
|
||||
description: root.content.clone(),
|
||||
patch: pull_request_patch(root, store.patches.iter()),
|
||||
tip,
|
||||
base,
|
||||
clone_urls,
|
||||
addr: store.addr().clone(),
|
||||
has_patch_link: root.tags.event_ids().next().is_some(),
|
||||
}
|
||||
})
|
||||
PrBinding {
|
||||
description: root.content.clone(),
|
||||
patch: pull_request_patch(root, store.patches.iter()),
|
||||
tip,
|
||||
base,
|
||||
clone_urls,
|
||||
addr: addr.clone(),
|
||||
has_patch_link: root.tags.event_ids().next().is_some(),
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let Some(binding) = binding else {
|
||||
|
||||
@@ -305,7 +305,24 @@ impl NewPullRequestView {
|
||||
),
|
||||
];
|
||||
|
||||
let mut view = Self {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
let Some(addr) = this.store.read(cx).addr().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(path) = CheckoutsStore::global(cx)
|
||||
.read(cx)
|
||||
.associations_of(&addr)
|
||||
.into_iter()
|
||||
.next()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.apply_folder_path(path, window, cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
dock_area,
|
||||
store,
|
||||
@@ -330,20 +347,7 @@ impl NewPullRequestView {
|
||||
scroll_handle: VirtualListScrollHandle::new(),
|
||||
item_sizes: Rc::new(Vec::new()),
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
// Prefill with the store's freshest associated checkout, no folder dialog.
|
||||
let addr = view.store.read(cx).addr().clone();
|
||||
if let Some(path) = CheckoutsStore::global(cx)
|
||||
.read(cx)
|
||||
.associations_of(&addr)
|
||||
.into_iter()
|
||||
.next()
|
||||
{
|
||||
view.apply_folder_path(path, window, cx);
|
||||
}
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
/// Whether a compare source, a checkout or a fork, is applied.
|
||||
@@ -496,11 +500,12 @@ impl NewPullRequestView {
|
||||
|
||||
// Remember this folder as a checkout of the target repository.
|
||||
// The next panel pre-fills it.
|
||||
let addr = self.store.read(cx).addr().clone();
|
||||
let checkout_store = CheckoutsStore::global(cx);
|
||||
checkout_store.update(cx, |store, cx| {
|
||||
store.record(PathBuf::from(&path), addr, cx);
|
||||
});
|
||||
if let Some(addr) = self.store.read(cx).addr().cloned() {
|
||||
let checkout_store = CheckoutsStore::global(cx);
|
||||
checkout_store.update(cx, |store, cx| {
|
||||
store.record(PathBuf::from(&path), addr, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let branches = self.branches.clone();
|
||||
let base = SharedString::from(base.clone());
|
||||
@@ -524,18 +529,21 @@ impl NewPullRequestView {
|
||||
|
||||
/// The base repository of the panel, its address and announced EUC.
|
||||
///
|
||||
/// Used to find fork candidates.
|
||||
fn base_repo(&self, cx: &App) -> (RepoAddr, Option<String>) {
|
||||
/// Used to find fork candidates. `None` while the repository is not announced.
|
||||
fn base_repo(&self, cx: &App) -> Option<(RepoAddr, Option<String>)> {
|
||||
let store = self.store.read(cx);
|
||||
let addr = store.addr()?.clone();
|
||||
let euc = store.announcement.as_ref().and_then(|a| a.euc.clone());
|
||||
(store.addr().clone(), euc)
|
||||
Some((addr, euc))
|
||||
}
|
||||
|
||||
/// Announced forks of the target repository a compare can use, own first.
|
||||
///
|
||||
/// Re-read whenever the picker opens.
|
||||
fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
|
||||
let (base, euc) = self.base_repo(cx);
|
||||
let Some((base, euc)) = self.base_repo(cx) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let user = Backend::global(cx).read(cx).current_user();
|
||||
let announcements = RepoListStore::global(cx).read(cx).announcements.clone();
|
||||
fork_candidates(&announcements, &base, euc.as_deref(), user)
|
||||
@@ -556,7 +564,9 @@ impl NewPullRequestView {
|
||||
.as_ref()
|
||||
.is_some_and(|fork| fork.announcement.addr() == announcement.addr());
|
||||
|
||||
let (base, _euc) = self.base_repo(cx);
|
||||
let Some((base, _euc)) = self.base_repo(cx) else {
|
||||
return;
|
||||
};
|
||||
let cache = GitStore::global(cx).cache().clone();
|
||||
let mirror_path = cache.repo_path(&base);
|
||||
let namespace = fork_namespace(&announcement);
|
||||
@@ -1124,8 +1134,12 @@ impl NewPullRequestView {
|
||||
cx: &Context<Self>,
|
||||
) -> impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static {
|
||||
let view = cx.entity().downgrade();
|
||||
let addr = self.store.read(cx).addr().clone();
|
||||
let associated = CheckoutsStore::global(cx).read(cx).associations_of(&addr);
|
||||
let associated = self
|
||||
.store
|
||||
.read(cx)
|
||||
.addr()
|
||||
.map(|addr| CheckoutsStore::global(cx).read(cx).associations_of(addr))
|
||||
.unwrap_or_default();
|
||||
|
||||
let active_path = (self.fork.is_none())
|
||||
.then(|| self.repo_path.clone())
|
||||
|
||||
@@ -20,14 +20,10 @@ use crate::views::repo::init_dialog;
|
||||
impl RepoDetailView {
|
||||
/// Re-push the repository's refs to its announced grasp servers.
|
||||
pub(super) fn push_repository(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(store) = self.store.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
store
|
||||
self.store
|
||||
.update(cx, |store, cx| store.push_repository(cx))
|
||||
.detach();
|
||||
}
|
||||
@@ -39,9 +35,7 @@ impl RepoDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(store) = self.store.clone() else {
|
||||
return;
|
||||
};
|
||||
let store = self.store.clone();
|
||||
|
||||
if store.read(cx).pushing {
|
||||
return;
|
||||
@@ -72,23 +66,22 @@ impl RepoDetailView {
|
||||
|
||||
/// Delete the repository from nostr, announcement, state and activity.
|
||||
pub(super) fn delete_repository(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(store) = self.store.clone() else {
|
||||
return;
|
||||
};
|
||||
store
|
||||
self.store
|
||||
.update(cx, |store, cx| store.delete_repository(cx))
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Open the issues list panel in the dock area.
|
||||
pub(super) fn open_issue_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(store) = self.store.clone() else {
|
||||
if self.store.read(cx).addr().is_none() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let Some(dock_area) = self.dock_area.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let store = self.store.clone();
|
||||
let panel = cx.new(|cx| IssuesView::new(self.dock_area.clone(), store, window, cx));
|
||||
|
||||
dock_area.update(cx, |dock_area, cx| {
|
||||
@@ -98,13 +91,15 @@ impl RepoDetailView {
|
||||
|
||||
/// Open the pull requests list panel in the dock area.
|
||||
pub(super) fn open_pull_request_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(store) = self.store.clone() else {
|
||||
if self.store.read(cx).addr().is_none() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let Some(dock_area) = self.dock_area.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let store = self.store.clone();
|
||||
let panel = cx.new(|cx| PullRequestsView::new(self.dock_area.clone(), store, window, cx));
|
||||
|
||||
dock_area.update(cx, |dock_area, cx| {
|
||||
@@ -131,7 +126,7 @@ impl RepoDetailView {
|
||||
|
||||
/// Open the dialog guiding the user through publishing the local repository to NIP-34.
|
||||
pub(super) fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(local_path) = self.local_path.clone() else {
|
||||
let Some(local_path) = self.store.read(cx).path.clone() else {
|
||||
return;
|
||||
};
|
||||
let view = cx.entity().downgrade();
|
||||
@@ -170,10 +165,7 @@ pub(crate) fn open_repo_panel(
|
||||
/// relays to connect to right away; the store loads the announcement from the
|
||||
/// local database on its first pass, so the hint is optional.
|
||||
fn repo_store(addr: &RepoAddr, hint: Option<&Announcement>, cx: &mut App) -> Entity<RepoStore> {
|
||||
let relays = hint
|
||||
.map(|announcement| announcement.relays.clone())
|
||||
.unwrap_or_default();
|
||||
cx.new(|cx| RepoStore::new(addr.clone(), relays, cx))
|
||||
cx.new(|cx| RepoStore::new(addr.clone(), hint.cloned(), cx))
|
||||
}
|
||||
|
||||
/// An item of a repository to open from outside its detail panel.
|
||||
|
||||
@@ -17,14 +17,15 @@ impl RepoDetailView {
|
||||
/// The repository's own checkouts are not suggested here.
|
||||
/// Their work is pushed, see [`Self::push_suggestion`].
|
||||
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||
let store = self.store.as_ref()?;
|
||||
let addr = store.read(cx).addr().clone();
|
||||
let store = self.store.read(cx);
|
||||
let addr = store.addr()?;
|
||||
let user = Backend::global(cx).read(cx).current_user()?;
|
||||
if store.read(cx).is_author(&user) {
|
||||
|
||||
if store.is_author(&user) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(&addr);
|
||||
let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(addr);
|
||||
|
||||
'status: for status in statuses {
|
||||
if self
|
||||
@@ -33,7 +34,6 @@ impl RepoDetailView {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let store = store.read(cx);
|
||||
for pr in &store.pull_requests {
|
||||
if pr_proposes_checkout(pr, store.status_of(pr) == RepoStatus::Open, user, &status)
|
||||
{
|
||||
@@ -50,15 +50,15 @@ impl RepoDetailView {
|
||||
///
|
||||
/// Not dismissed in this panel.
|
||||
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||
let entity = self.store.as_ref()?;
|
||||
let store = self.store.read(cx);
|
||||
let addr = store.addr()?;
|
||||
let user = Backend::global(cx).read(cx).current_user()?;
|
||||
|
||||
if !entity.read(cx).is_author(&user) {
|
||||
if !store.is_author(&user) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let addr = entity.read(cx).addr().clone();
|
||||
let statuses = CheckoutsStore::global(cx).read(cx).push_statuses_of(&addr);
|
||||
let statuses = CheckoutsStore::global(cx).read(cx).push_statuses_of(addr);
|
||||
|
||||
statuses.into_iter().find(|status| {
|
||||
!self
|
||||
@@ -75,10 +75,7 @@ impl RepoDetailView {
|
||||
let key = (status.path.clone(), status.branch.clone());
|
||||
let path = status.path.clone();
|
||||
// The push busy flag lives on the store; it disables the banner's triggers.
|
||||
let pushing = self
|
||||
.store
|
||||
.as_ref()
|
||||
.is_some_and(|store| store.read(cx).pushing);
|
||||
let pushing = self.store.read(cx).pushing;
|
||||
|
||||
let commits = if status.ahead == 1 {
|
||||
SharedString::from("1 commit")
|
||||
@@ -160,8 +157,7 @@ impl RepoDetailView {
|
||||
|
||||
/// Warning after a push that only some grasp servers accepted.
|
||||
pub(super) fn render_push_warning_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||
let store = self.store.as_ref()?;
|
||||
let store = store.read(cx);
|
||||
let store = self.store.read(cx);
|
||||
let warning = store.last_push_warning.clone()?;
|
||||
let pushing = store.pushing;
|
||||
|
||||
@@ -213,11 +209,9 @@ impl RepoDetailView {
|
||||
.ghost()
|
||||
.disabled(pushing)
|
||||
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||
if let Some(store) = this.store.clone() {
|
||||
store.update(cx, |store, _| {
|
||||
store.last_push_warning = None;
|
||||
});
|
||||
}
|
||||
this.store.update(cx, |store, _| {
|
||||
store.last_push_warning = None;
|
||||
});
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
@@ -299,14 +293,12 @@ impl RepoDetailView {
|
||||
.small()
|
||||
.info()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
if let Some(store) = this.store.clone() {
|
||||
open_new_pull_panel(
|
||||
this.dock_area.clone(),
|
||||
store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
open_new_pull_panel(
|
||||
this.dock_area.clone(),
|
||||
this.store.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -27,15 +27,11 @@ impl RepoDetailView {
|
||||
/// The NIP-34 header, actions and issues/PR counts.
|
||||
/// Or the local header with an Init button for an unpublished repository.
|
||||
pub(super) fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
if self.local_path.is_some() {
|
||||
if self.store.read(cx).addr().is_none() {
|
||||
return self.render_local_header(cx);
|
||||
}
|
||||
|
||||
let Some(store_entity) = self.store.as_ref() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let store = store_entity.read(cx);
|
||||
let store = self.store.read(cx);
|
||||
let issue_count = SharedString::from(store.issue_count().to_string());
|
||||
let pr_count = SharedString::from(store.pull_request_count().to_string());
|
||||
|
||||
@@ -43,7 +39,7 @@ impl RepoDetailView {
|
||||
let pushing = store.pushing;
|
||||
let cloning = store.cloning;
|
||||
|
||||
let Some(source) = store.announcement.as_ref().or(self.initial.as_ref()) else {
|
||||
let Some(source) = store.announcement.as_ref() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
@@ -73,19 +69,18 @@ impl RepoDetailView {
|
||||
.on_action(
|
||||
cx.listener(|this, action: &RepoAction, window, cx| match action {
|
||||
RepoAction::NewIssue => {
|
||||
if let Some(store) = this.store.clone() {
|
||||
open_new_issue_dialog(store, window, cx);
|
||||
}
|
||||
open_new_issue_dialog(this.store.clone(), window, cx);
|
||||
}
|
||||
RepoAction::NewPR => {
|
||||
if let Some(store) = this.store.clone() {
|
||||
open_new_pull_panel(this.dock_area.clone(), store, window, cx);
|
||||
}
|
||||
open_new_pull_panel(this.dock_area.clone(), this.store.clone(), window, cx);
|
||||
}
|
||||
RepoAction::SendPatch => {
|
||||
if let Some(store) = this.store.clone() {
|
||||
open_send_patch_panel(this.dock_area.clone(), store, window, cx);
|
||||
}
|
||||
open_send_patch_panel(
|
||||
this.dock_area.clone(),
|
||||
this.store.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
RepoAction::About => {
|
||||
if let Some(announcement) = this.announcement(cx) {
|
||||
@@ -421,7 +416,9 @@ impl RepoDetailView {
|
||||
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let name = self.display_name(cx);
|
||||
let path = self
|
||||
.local_path
|
||||
.store
|
||||
.read(cx)
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -190,7 +190,7 @@ fn init_repository(
|
||||
window.close_dialog(cx);
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |this, cx| {
|
||||
this.apply_announcement(announcement, window, cx);
|
||||
this.apply_announcement(announcement, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,9 +39,24 @@ impl RepoDetailView {
|
||||
self.error = None;
|
||||
cx.notify();
|
||||
|
||||
let (addr, announcement, local_path) = {
|
||||
let store = self.store.read(cx);
|
||||
(
|
||||
store.addr().cloned(),
|
||||
store.announcement.clone(),
|
||||
store.path.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// Local repositories live on disk at their scan path.
|
||||
// No clone step or network refresh applies here.
|
||||
if let Some(local_path) = self.local_path.clone() {
|
||||
if addr.is_none() {
|
||||
self.repo_started = true;
|
||||
|
||||
let Some(local_path) = local_path else {
|
||||
return;
|
||||
};
|
||||
|
||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||
let data = cx
|
||||
.background_spawn(async move {
|
||||
@@ -67,13 +82,14 @@ impl RepoDetailView {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(initial) = self.initial.as_ref() else {
|
||||
let Some(announcement) = announcement else {
|
||||
return;
|
||||
};
|
||||
self.repo_started = true;
|
||||
|
||||
let cache = GitStore::global(cx).cache().clone();
|
||||
let addr = initial.addr();
|
||||
let clone_urls: Vec<Url> = initial.clone.clone();
|
||||
let addr = announcement.addr();
|
||||
let clone_urls: Vec<Url> = announcement.clone.clone();
|
||||
|
||||
// Captured before the loads start.
|
||||
// A branch/tag switch bumps the generation, discarding the refresh below.
|
||||
@@ -324,9 +340,7 @@ impl RepoDetailView {
|
||||
|
||||
/// Clone the repository into a user-chosen folder outside the cache.
|
||||
pub(super) fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(store) = self.store.clone() else {
|
||||
return;
|
||||
};
|
||||
let store = self.store.clone();
|
||||
|
||||
let name = {
|
||||
let Some(announcement) = self.announcement(cx) else {
|
||||
|
||||
@@ -71,19 +71,14 @@ pub struct RepoDetailView {
|
||||
///
|
||||
/// New panels, commit diffs, are added there.
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
/// Snapshot taken at open time.
|
||||
/// Per-repository store, holding the local path and the announcement,
|
||||
/// issues, PRs and statuses. The single identity of both modes.
|
||||
store: Entity<RepoStore>,
|
||||
/// The initial explorer load has been started.
|
||||
///
|
||||
/// `None` for local repositories that haven't been published yet, and for a
|
||||
/// repository opened by address until its store loads the announcement.
|
||||
initial: Option<Announcement>,
|
||||
/// Per-repository nostr store, holding announcement, issues, PRs and statuses.
|
||||
///
|
||||
/// `None` until a local repository is initialized to NIP-34.
|
||||
store: Option<Entity<RepoStore>>,
|
||||
/// Path of the local repository when opened from the scan.
|
||||
///
|
||||
/// `None` once it is initialized to NIP-34, or for announced repositories.
|
||||
local_path: Option<PathBuf>,
|
||||
/// A repository opened by address alone starts without an announcement; the
|
||||
/// store observer starts the load once the first one lands.
|
||||
repo_started: bool,
|
||||
/// File explorer state, the worktree of the local clone.
|
||||
tree_state: Entity<TreeState>,
|
||||
/// Root of the local clone, for reading files on demand.
|
||||
@@ -178,18 +173,8 @@ impl RepoDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// The announcement we opened from already carries the NIP-34 `relays` tag.
|
||||
//
|
||||
// The store connects to those relays immediately, no bootstrap fetch wait.
|
||||
let relays = hint
|
||||
.as_ref()
|
||||
.map(|announcement| announcement.relays.clone())
|
||||
.unwrap_or_default();
|
||||
let store = cx.new(|cx| RepoStore::new(addr, relays, cx));
|
||||
|
||||
let mut view = Self::new_common(dock_area, hint, Some(store.clone()), None, window, cx);
|
||||
view.attach_store(&store, window, cx);
|
||||
view
|
||||
let store = cx.new(|cx| RepoStore::new(addr, hint, cx));
|
||||
Self::new_common(dock_area, store, window, cx)
|
||||
}
|
||||
|
||||
/// Open a local repository discovered by the scan.
|
||||
@@ -199,7 +184,8 @@ impl RepoDetailView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_common(dock_area, None, None, Some(local_path), window, cx)
|
||||
let store = cx.new(move |_cx| RepoStore::new_local(local_path));
|
||||
Self::new_common(dock_area, store, window, cx)
|
||||
}
|
||||
|
||||
/// Shared construction.
|
||||
@@ -207,9 +193,7 @@ impl RepoDetailView {
|
||||
/// File explorer state, ref selectors and the deferred repository load.
|
||||
fn new_common(
|
||||
dock_area: WeakEntity<DockArea>,
|
||||
initial: Option<Announcement>,
|
||||
store: Option<Entity<RepoStore>>,
|
||||
local_path: Option<PathBuf>,
|
||||
store: Entity<RepoStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -271,11 +255,10 @@ impl RepoDetailView {
|
||||
this.load_repo(window, cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
initial,
|
||||
let mut view = Self {
|
||||
dock_area,
|
||||
store,
|
||||
local_path,
|
||||
store: store.clone(),
|
||||
repo_started: false,
|
||||
tree_state,
|
||||
worktree: None,
|
||||
worktree_paths: Vec::new(),
|
||||
@@ -311,31 +294,40 @@ impl RepoDetailView {
|
||||
push_statuses: Vec::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
};
|
||||
|
||||
view.attach_store(&store, window, cx);
|
||||
view
|
||||
}
|
||||
|
||||
/// The latest announcement from the store or the open-time snapshot.
|
||||
/// `None` for local repositories that haven't been published yet.
|
||||
/// The latest announcement of the repository, `None` while local-only or
|
||||
/// until the store's first pass loads it.
|
||||
fn announcement<'a>(&'a self, cx: &'a App) -> Option<&'a Announcement> {
|
||||
let store = self.store.as_ref()?;
|
||||
store
|
||||
.read(cx)
|
||||
.announcement
|
||||
.as_ref()
|
||||
.or(self.initial.as_ref())
|
||||
self.store.read(cx).announcement.as_ref()
|
||||
}
|
||||
|
||||
/// Display name, the announcement's name or ID for announced repositories.
|
||||
/// The directory name for local ones.
|
||||
fn display_name(&self, cx: &App) -> SharedString {
|
||||
if let Some(path) = &self.local_path {
|
||||
return SharedString::from(
|
||||
path.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.display().to_string()),
|
||||
);
|
||||
let store = self.store.read(cx);
|
||||
|
||||
if store.addr().is_none() {
|
||||
return store
|
||||
.path
|
||||
.as_ref()
|
||||
.map(|path| {
|
||||
SharedString::from(
|
||||
path.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| path.display().to_string()),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
self.announcement(cx)
|
||||
|
||||
store
|
||||
.announcement
|
||||
.as_ref()
|
||||
.map(|announcement| {
|
||||
announcement
|
||||
.name
|
||||
@@ -386,8 +378,10 @@ impl Render for RepoDetailView {
|
||||
// operations, republish, checkout push, delete and clone-to-folder.
|
||||
let error = self.error.clone().or_else(|| {
|
||||
self.store
|
||||
.as_ref()
|
||||
.and_then(|store| store.read(cx).last_error.clone().map(SharedString::from))
|
||||
.read(cx)
|
||||
.last_error
|
||||
.clone()
|
||||
.map(SharedString::from)
|
||||
});
|
||||
|
||||
v_flex()
|
||||
@@ -405,9 +399,7 @@ impl Render for RepoDetailView {
|
||||
.banner()
|
||||
.on_close(cx.listener(|this, _event, _window, cx| {
|
||||
this.error = None;
|
||||
if let Some(store) = this.store.clone() {
|
||||
store.update(cx, |store, _| store.last_error = None);
|
||||
}
|
||||
this.store.update(cx, |store, _| store.last_error = None);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use gpui::prelude::*;
|
||||
use gpui::{Context, Entity, Window};
|
||||
use signed_core::Announcement;
|
||||
use signed_state::{Backend, CheckoutsStore, LocalReposStore, RepoStore};
|
||||
@@ -6,33 +5,29 @@ use signed_state::{Backend, CheckoutsStore, LocalReposStore, RepoStore};
|
||||
use super::RepoDetailView;
|
||||
|
||||
impl RepoDetailView {
|
||||
/// Switch the repository into its NIP-34 mode after a successful init.
|
||||
/// Creates the nostr store for the announced repository.
|
||||
/// Drops the local scan identity.
|
||||
/// The worktree is unchanged, so the explorer keeps its loaded content.
|
||||
/// Switch a local repository into its NIP-34 mode after a successful init.
|
||||
/// The store is kept, so the panel keeps its path and loaded worktree.
|
||||
/// Drops the local scan identity so it leaves the sidebar's local section.
|
||||
pub(crate) fn apply_announcement(
|
||||
&mut self,
|
||||
announcement: Announcement,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
// The repository is no longer a bare local repo.
|
||||
// Drop it from the scan results so it leaves the sidebar's local section.
|
||||
if let Some(path) = self.local_path.take() {
|
||||
let path = self.store.read(cx).path.clone();
|
||||
if let Some(path) = path {
|
||||
LocalReposStore::global(cx).update(cx, |store, cx| store.remove(&path, cx));
|
||||
}
|
||||
let store =
|
||||
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx));
|
||||
// Re-render on store refreshes, issues, PRs and statuses.
|
||||
// Keep the ready-to-contribute statuses of this repository requested.
|
||||
self.attach_store(&store, window, cx);
|
||||
self.store = Some(store);
|
||||
self.initial = Some(announcement);
|
||||
|
||||
self.store
|
||||
.update(cx, |store, cx| store.announce(announcement, cx));
|
||||
|
||||
// The new address needs its ready-to-contribute statuses requested.
|
||||
self.refresh_ready_statuses(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Observe the repository's store, re-render on refreshes.
|
||||
/// Request the ready-to-contribute statuses for it.
|
||||
/// Observe the repository's store and re-render on its refreshes.
|
||||
/// Start the explorer once the store has an announcement.
|
||||
pub(super) fn attach_store(
|
||||
&mut self,
|
||||
store: &Entity<RepoStore>,
|
||||
@@ -42,18 +37,9 @@ impl RepoDetailView {
|
||||
self._subscriptions
|
||||
.push(cx.observe_in(store, window, |this, store, window, cx| {
|
||||
this.refresh_ready_statuses(cx);
|
||||
|
||||
// A repository opened from its address alone starts without an
|
||||
// announcement. Adopt the store's first one so the explorer can
|
||||
// load; later passes leave the snapshot and the selection alone.
|
||||
let announcement = store.read(cx).announcement.clone();
|
||||
if this.initial.is_none()
|
||||
&& let Some(announcement) = announcement
|
||||
{
|
||||
this.initial = Some(announcement);
|
||||
if !this.repo_started && store.read(cx).announcement.is_some() {
|
||||
this.load_repo(window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}));
|
||||
self.refresh_ready_statuses(cx);
|
||||
@@ -64,11 +50,11 @@ impl RepoDetailView {
|
||||
/// Owned repositories are watched for unpushed commits.
|
||||
/// Other repositories for ready-to-contribute checkouts.
|
||||
fn refresh_ready_statuses(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(entity) = self.store.clone() else {
|
||||
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let head = entity.read(cx).head.clone();
|
||||
let head = self.store.read(cx).head.clone();
|
||||
|
||||
if self.ready_requested && self.ready_head == head {
|
||||
return;
|
||||
@@ -77,14 +63,13 @@ impl RepoDetailView {
|
||||
self.ready_requested = true;
|
||||
self.ready_head = head.clone();
|
||||
|
||||
let addr = entity.read(cx).addr().clone();
|
||||
let backend = Backend::global(cx);
|
||||
let checkout = CheckoutsStore::global(cx);
|
||||
|
||||
let owned = backend
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.is_some_and(|user| entity.read(cx).is_author(&user));
|
||||
.is_some_and(|user| self.store.read(cx).is_author(&user));
|
||||
|
||||
checkout.update(cx, |store, cx| {
|
||||
// The ready statuses keep the fast poll running while the panel is open.
|
||||
@@ -97,16 +82,12 @@ impl RepoDetailView {
|
||||
});
|
||||
}
|
||||
|
||||
/// The ready-to-push statuses of this repository in the global checkouts
|
||||
/// store changed since they last drove a render.
|
||||
///
|
||||
/// Updates the cached slices. `None` store (a local, not yet published,
|
||||
/// repository) has no statuses.
|
||||
/// The ready-to-contribute and ready-to-push statuses of this repository.
|
||||
pub(super) fn refresh_statuses(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
let Some(entity) = self.store.clone() else {
|
||||
let Some(addr) = self.store.read(cx).addr().cloned() else {
|
||||
return false;
|
||||
};
|
||||
let addr = entity.read(cx).addr().clone();
|
||||
|
||||
let checkouts = CheckoutsStore::global(cx).read(cx);
|
||||
let ready_statuses = checkouts.ready_statuses_of(&addr);
|
||||
let push_statuses = checkouts.push_statuses_of(&addr);
|
||||
|
||||
+42
-13
@@ -1,6 +1,6 @@
|
||||
# Repository state and panel flow plan
|
||||
|
||||
Status: phases 1-2 implemented, phase 3 next (2026-09-13)
|
||||
Status: phases 1-3 implemented (2026-09-13)
|
||||
|
||||
Builds on `docs/backend-rearchitecture.md`, especially §7 (notify audit),
|
||||
§11 (split independently-observed state), §12 (one debounce at the source)
|
||||
@@ -281,27 +281,56 @@ Status: implemented.
|
||||
3. `open_repo_panel` and `RepoDetailView::new` take an address plus an optional
|
||||
hint, so a repository panel opens from a `RepoAddr` alone. This pulls the
|
||||
address-based constructor forward from Phase 3 step 2.
|
||||
4. `RepoDetailView::attach_store` adopts the store's first announcement when
|
||||
`initial` is still empty and calls `load_repo`, so a panel opened by address
|
||||
fills in instead of waiting for the caller to have the announcement.
|
||||
4. `RepoDetailView::attach_store` starts the explorer from the store's first
|
||||
announcement when the panel was opened by address alone, so a panel opened
|
||||
by address fills in instead of waiting for the caller to have the
|
||||
announcement. Phase 3 moves this onto the single store observer, gated by
|
||||
`repo_started`.
|
||||
5. `open_upstream`: the 60 x 250 ms poll and the `pending_upstream` field are
|
||||
gone. It opens the panel by address; the store's `subscribe_remote` fetches
|
||||
the announcement from the bootstrap relays and step 4 loads the explorer.
|
||||
|
||||
### Phase 3 - one entity for local and NIP-34
|
||||
|
||||
1. `signed_state/src/repo.rs`: `addr`/`path` options, `new_local`,
|
||||
`announce`, `Option<Subscription>`, action guards.
|
||||
2. `views/repo/mod.rs`: single `store` field; `new_local`; header,
|
||||
display name, `load_repo`, `open_init_dialog` derive from the store. The
|
||||
address-based `new` is already in place from Phase 2.
|
||||
3. `views/repo/store.rs`: always observe; `refresh_statuses` returns false
|
||||
when not announced.
|
||||
4. `views/repo/actions.rs`, `header.rs`, `banners.rs`: drop
|
||||
`Option<Entity<RepoStore>>` guards, guard on `addr()` instead.
|
||||
Status: implemented.
|
||||
|
||||
1. `signed_state/src/repo.rs`: `addr: Option<RepoAddr>`,
|
||||
`path: Option<PathBuf>`, `announcement: Option<Announcement>`,
|
||||
`_subscription: Option<Subscription>`. `new(addr, hint, cx)` seeds the
|
||||
announcement and relays from the hint; `new_local(path)`; `announce`
|
||||
switches a local store to NIP-34 in place, keeping `path`. `addr()` returns
|
||||
`Option<&RepoAddr>`; `refresh`/`connect_announced_relays`/`subscribe_remote`
|
||||
no-op without an address. Nostr-side actions guard with `not_announced`
|
||||
(unit actions) or `action_error` (task actions).
|
||||
2. `views/repo/mod.rs`: one `store: Entity<RepoStore>` field. `initial` and
|
||||
`local_path` are deleted; `new_local` builds a local store. The store
|
||||
observer starts the explorer once an announcement lands, tracked by
|
||||
`repo_started`. `display_name`, `load_repo` and `open_init_dialog` derive
|
||||
their mode from `addr()`/`path` instead of the removed fields.
|
||||
3. `views/repo/store.rs`: the store is observed from construction for both
|
||||
modes; `apply_announcement` calls `store.announce` on the existing entity.
|
||||
`refresh_ready_statuses` and `refresh_statuses` return early when `addr()`
|
||||
is `None`.
|
||||
4. `views/repo/{actions,header,banners,loading}.rs`: the
|
||||
`Option<Entity<RepoStore>>` guards are gone. Announced-only entry points
|
||||
(issue/PR lists, new PR, send patch) guard on `addr()`; `NewPullRequestView`
|
||||
and `PullRequestDetailView` thread the address option through their
|
||||
prefill/binding paths.
|
||||
5. `LocalReposStore` stays as the scan index; `CheckoutsStore` stays the
|
||||
association authority.
|
||||
|
||||
Deviations from the sketch above:
|
||||
|
||||
- `path` is set only by `new_local` and kept by `announce`. `new` does not
|
||||
resolve an associated checkout: the explorer still mirrors the cache for
|
||||
announced repositories, so a stored checkout path would be dead weight.
|
||||
The field is the seam for the open question below.
|
||||
- `new_local` takes no `Context`: a local store has nothing to subscribe to and
|
||||
no first pass to defer.
|
||||
- `new` keeps the open-time hint until the first pass has confirmed what the
|
||||
database holds, so a panel opened from a hint renders before the query lands
|
||||
and still adopts a later deletion.
|
||||
|
||||
### Phase 4 - deferred, only if duplicate stores become a problem
|
||||
|
||||
One store per address via `HashMap<RepoAddr, WeakEntity<RepoStore>>` inside
|
||||
|
||||
Reference in New Issue
Block a user