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.
|
/// Holds the announcement, state, issues, patches, PRs, comments and resolved statuses.
|
||||||
/// Always derived from the local database.
|
/// Always derived from the local database.
|
||||||
pub struct RepoStore {
|
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>,
|
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.
|
/// The first local pass has been applied.
|
||||||
///
|
///
|
||||||
/// Views distinguish "no data yet" from a genuinely empty repository with it.
|
/// Views distinguish "no data yet" from a genuinely empty repository with it.
|
||||||
@@ -82,52 +88,21 @@ pub struct RepoStore {
|
|||||||
root_fetches: HashSet<EventId>,
|
root_fetches: HashSet<EventId>,
|
||||||
/// Refresh coalescing, see [`RefreshGate`].
|
/// Refresh coalescing, see [`RefreshGate`].
|
||||||
refresh: RefreshGate,
|
refresh: RefreshGate,
|
||||||
_subscription: Subscription,
|
/// Backend subscription of an announced repository. `None` while local-only.
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepoStore {
|
impl RepoStore {
|
||||||
pub fn new(addr: RepoAddr, announced_relays: Vec<RelayUrl>, cx: &mut Context<Self>) -> Self {
|
/// Announced repository.
|
||||||
let backend = Backend::global(cx);
|
pub fn new(addr: RepoAddr, hint: Option<Announcement>, cx: &mut Context<Self>) -> Self {
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let weak = cx.entity().downgrade();
|
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| {
|
cx.defer(move |cx| {
|
||||||
let result = weak.update(cx, |this, cx| {
|
let result = weak.update(cx, |this, cx| {
|
||||||
this.subscribe_remote(cx);
|
this.subscribe_remote(cx);
|
||||||
@@ -141,8 +116,9 @@ impl RepoStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
addr,
|
addr: Some(addr),
|
||||||
announcement: None,
|
announcement: hint,
|
||||||
|
path: None,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
head: None,
|
head: None,
|
||||||
issues: Vec::new(),
|
issues: Vec::new(),
|
||||||
@@ -161,13 +137,103 @@ impl RepoStore {
|
|||||||
repo_relays: HashSet::new(),
|
repo_relays: HashSet::new(),
|
||||||
root_fetches: HashSet::new(),
|
root_fetches: HashSet::new(),
|
||||||
refresh: RefreshGate::default(),
|
refresh: RefreshGate::default(),
|
||||||
_subscription: subscription,
|
_subscription: Some(subscription),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the repository's address.
|
/// Local repository discovered by the scan, not announced to NIP-34 yet.
|
||||||
pub fn addr(&self) -> &RepoAddr {
|
pub fn new_local(path: PathBuf) -> Self {
|
||||||
&self.addr
|
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.
|
/// 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.
|
/// 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>) {
|
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
|
let new: Vec<RelayUrl> = relays
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|url| !self.repo_relays.contains(*url))
|
.filter(|url| !self.repo_relays.contains(*url))
|
||||||
@@ -211,7 +281,6 @@ impl RepoStore {
|
|||||||
self.repo_relays.extend(new.iter().cloned());
|
self.repo_relays.extend(new.iter().cloned());
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let addr = self.addr.clone();
|
|
||||||
|
|
||||||
backend.update(cx, |backend, cx| {
|
backend.update(cx, |backend, cx| {
|
||||||
backend.connect_repo_relays(new, Self::repo_filters(&addr), 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.
|
/// Fetch this repository's events from the bootstrap relays.
|
||||||
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.addr.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let addr = self.addr.clone();
|
|
||||||
|
|
||||||
backend.update(cx, |backend, cx| {
|
backend.update(cx, |backend, cx| {
|
||||||
backend.subscribe_bootstrap(Self::repo_filters(&addr), 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
|
/// Runs immediately. The backend pump already batches the relay events that
|
||||||
/// trigger a refresh, so no per-store debounce is needed.
|
/// trigger a refresh, so no per-store debounce is needed.
|
||||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.addr.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if self.refresh.request() != RefreshRequest::Schedule {
|
if self.refresh.request() != RefreshRequest::Schedule {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -241,11 +317,14 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(addr) = self.addr.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
self.refresh.begin();
|
self.refresh.begin();
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let client = backend.read(cx).client();
|
let client = backend.read(cx).client();
|
||||||
let addr = self.addr.clone();
|
|
||||||
|
|
||||||
let work = cx.background_spawn(async move {
|
let work = cx.background_spawn(async move {
|
||||||
let (announcements, states, activity, deletion_events) = async {
|
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
|
// The first pass is the exception: it must notify even when it
|
||||||
// found nothing, so views can leave their loading state and show
|
// found nothing, so views can leave their loading state and show
|
||||||
// the empty result.
|
// 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 first_pass = !this.loaded;
|
||||||
let head_changed = state
|
let head_changed = state
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|(_, head)| this.head.as_deref() != head.as_deref());
|
.is_some_and(|(_, head)| this.head.as_deref() != head.as_deref());
|
||||||
let changed = first_pass
|
let changed = first_pass
|
||||||
|| this.announcement != announcement
|
|| (!keep_hint && this.announcement != announcement)
|
||||||
|| head_changed
|
|| head_changed
|
||||||
|| this.issues != issues
|
|| this.issues != issues
|
||||||
|| this.patches != patches
|
|| this.patches != patches
|
||||||
@@ -418,7 +503,9 @@ impl RepoStore {
|
|||||||
|| this.comments != comments
|
|| this.comments != comments
|
||||||
|| this.status_by_root != status_by_root;
|
|| 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.
|
// The announcement may list relays for this repository's activity.
|
||||||
// Connect to any we have not fetched from yet.
|
// 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.
|
/// The author is the public key of the repository address.
|
||||||
/// Only the author may manage pull requests, close, reopen or merge.
|
/// Only the author may manage pull requests, close, reopen or merge.
|
||||||
pub fn is_author(&self, user: &PublicKey) -> bool {
|
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.
|
/// Open an issue on this repository.
|
||||||
pub fn open_issue(&mut self, subject: Option<String>, content: String, cx: &mut Context<Self>) {
|
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 {
|
let builder = GitIssue {
|
||||||
repository: self.addr.clone(),
|
repository: addr,
|
||||||
content,
|
content,
|
||||||
subject,
|
subject,
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
@@ -564,6 +658,11 @@ impl RepoStore {
|
|||||||
content: String,
|
content: String,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let Some(addr) = self.addr.clone() else {
|
||||||
|
self.not_announced(cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let relay_hint = self
|
let relay_hint = self
|
||||||
.announcement
|
.announcement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -571,7 +670,7 @@ impl RepoStore {
|
|||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
self.publish(
|
self.publish(
|
||||||
comment_builder(root, parent, relay_hint.as_ref(), &self.addr, content),
|
comment_builder(root, parent, relay_hint.as_ref(), &addr, content),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -592,6 +691,11 @@ impl RepoStore {
|
|||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
self.last_warning = 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)
|
let series: Vec<String> = signed_git::split_patch_series(&patch)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
@@ -635,8 +739,7 @@ impl RepoStore {
|
|||||||
// The author's npub names their GRASP-06 namespace, `/prs/...`.
|
// The author's npub names their GRASP-06 namespace, `/prs/...`.
|
||||||
let author_npub = user.to_bech32().unwrap();
|
let author_npub = user.to_bech32().unwrap();
|
||||||
|
|
||||||
let addr = self.addr.clone();
|
let owner = addr.public_key;
|
||||||
let owner = self.addr.public_key;
|
|
||||||
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
||||||
let repo_id = addr.identifier.clone();
|
let repo_id = addr.identifier.clone();
|
||||||
let base_npub = owner.to_bech32().unwrap();
|
let base_npub = owner.to_bech32().unwrap();
|
||||||
@@ -752,7 +855,7 @@ impl RepoStore {
|
|||||||
let clone = pr_clone_urls(prs_urls, base_clone);
|
let clone = pr_clone_urls(prs_urls, base_clone);
|
||||||
|
|
||||||
let builder = GitPullRequest {
|
let builder = GitPullRequest {
|
||||||
repository: this.addr.clone(),
|
repository: addr.clone(),
|
||||||
content: description,
|
content: description,
|
||||||
subject,
|
subject,
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
@@ -971,8 +1074,11 @@ impl RepoStore {
|
|||||||
.map(|p| p.id)
|
.map(|p| p.id)
|
||||||
});
|
});
|
||||||
|
|
||||||
let addr = self.addr.clone();
|
let Some(addr) = self.addr.clone() else {
|
||||||
let owner = self.addr.public_key;
|
self.not_announced(cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let owner = addr.public_key;
|
||||||
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
||||||
let root = root.clone();
|
let root = root.clone();
|
||||||
let clone: Vec<Url> = self
|
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 {
|
let builder = GitPullRequestUpdate {
|
||||||
repository: this.addr.clone(),
|
repository: addr.clone(),
|
||||||
pull_request_event: root.id,
|
pull_request_event: root.id,
|
||||||
pull_request_author: root.pubkey,
|
pull_request_author: root.pubkey,
|
||||||
current_commit,
|
current_commit,
|
||||||
@@ -1059,6 +1165,11 @@ impl RepoStore {
|
|||||||
pub fn set_status(&mut self, root: &Event, status: RepoStatus, cx: &mut Context<Self>) {
|
pub fn set_status(&mut self, root: &Event, status: RepoStatus, cx: &mut Context<Self>) {
|
||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
|
|
||||||
|
let Some(addr) = self.addr.clone() else {
|
||||||
|
self.not_announced(cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let maintainers = self
|
let maintainers = self
|
||||||
.announcement
|
.announcement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1084,9 +1195,9 @@ impl RepoStore {
|
|||||||
|
|
||||||
let builder = EventBuilder::new(status.kind(), "").tags([
|
let builder = EventBuilder::new(status.kind(), "").tags([
|
||||||
root_ref,
|
root_ref,
|
||||||
Tag::public_key(self.addr.public_key),
|
Tag::public_key(addr.public_key),
|
||||||
Tag::public_key(root.pubkey),
|
Tag::public_key(root.pubkey),
|
||||||
Tag::coordinate(self.addr.clone(), None),
|
Tag::coordinate(addr, None),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
self.publish(builder, cx);
|
self.publish(builder, cx);
|
||||||
@@ -1097,6 +1208,11 @@ impl RepoStore {
|
|||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
self.last_warning = None;
|
self.last_warning = None;
|
||||||
|
|
||||||
|
let Some(addr) = self.addr.clone() else {
|
||||||
|
self.not_announced(cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let is_author = Backend::global(cx)
|
let is_author = Backend::global(cx)
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.current_user()
|
.current_user()
|
||||||
@@ -1107,7 +1223,6 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let cache = GitStore::global(cx).cache().clone();
|
let cache = GitStore::global(cx).cache().clone();
|
||||||
let addr = self.addr.clone();
|
|
||||||
|
|
||||||
let clone_urls: Vec<Url> = self
|
let clone_urls: Vec<Url> = self
|
||||||
.announcement
|
.announcement
|
||||||
@@ -1176,12 +1291,13 @@ impl RepoStore {
|
|||||||
/// The latest announcement of this repository,
|
/// The latest announcement of this repository,
|
||||||
/// for operations that need its clone URLs and relays.
|
/// for operations that need its clone URLs and relays.
|
||||||
fn action_announcement(&self, cx: &App) -> Option<Announcement> {
|
fn action_announcement(&self, cx: &App) -> Option<Announcement> {
|
||||||
|
let addr = self.addr.as_ref()?;
|
||||||
self.announcement.clone().or_else(|| {
|
self.announcement.clone().or_else(|| {
|
||||||
RepoListStore::global(cx)
|
RepoListStore::global(cx)
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.announcements
|
.announcements
|
||||||
.iter()
|
.iter()
|
||||||
.find(|announcement| announcement.addr() == self.addr)
|
.find(|announcement| announcement.addr() == *addr)
|
||||||
.cloned()
|
.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 {
|
let Some(announcement) = self.action_announcement(cx) else {
|
||||||
return self.action_error("Repository announcement is not loaded yet", cx);
|
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 state event's `HEAD` stays the announced default branch.
|
||||||
// The checkout may be on a side branch.
|
// The checkout may be on a side branch.
|
||||||
let head = self.head.clone();
|
let head = self.head.clone();
|
||||||
let addr = self.addr.clone();
|
|
||||||
|
|
||||||
self.pushing = true;
|
self.pushing = true;
|
||||||
self.last_error = None;
|
self.last_error = None;
|
||||||
@@ -1298,7 +1417,9 @@ impl RepoStore {
|
|||||||
/// Only the repository owner may delete it. The lists update when the
|
/// Only the repository owner may delete it. The lists update when the
|
||||||
/// deletion events arrive.
|
/// deletion events arrive.
|
||||||
pub fn delete_repository(&mut self, cx: &mut Context<Self>) -> Task<Result<(), Error>> {
|
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;
|
self.last_error = None;
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
@@ -1328,12 +1449,16 @@ impl RepoStore {
|
|||||||
"A clone of this repository is already in progress"
|
"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 {
|
let Some(announcement) = self.action_announcement(cx) else {
|
||||||
return self.action_error("Repository announcement is not loaded yet", cx);
|
return self.action_error("Repository announcement is not loaded yet", cx);
|
||||||
};
|
};
|
||||||
|
|
||||||
let clone_urls = announcement.clone.clone();
|
let clone_urls = announcement.clone.clone();
|
||||||
let addr = self.addr.clone();
|
|
||||||
|
|
||||||
self.cloning = true;
|
self.cloning = true;
|
||||||
self.last_error = None;
|
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.
|
/// Fail an operation whose announcement is not loaded yet.
|
||||||
fn action_error(
|
fn action_error(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -1393,11 +1524,15 @@ impl RepoStore {
|
|||||||
euc: Option<&str>,
|
euc: Option<&str>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
let Some(addr) = self.addr.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let mut tags = vec![
|
let mut tags = vec![
|
||||||
Tag::parse(["e", &root.id.to_hex(), "", "root"]).expect("valid root tag"),
|
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::public_key(root.pubkey),
|
||||||
Tag::coordinate(self.addr.clone(), None),
|
Tag::coordinate(addr, None),
|
||||||
];
|
];
|
||||||
|
|
||||||
if let Some(euc) = euc
|
if let Some(euc) = euc
|
||||||
|
|||||||
@@ -151,35 +151,37 @@ impl PullRequestDetailView {
|
|||||||
let binding = {
|
let binding = {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
|
|
||||||
store
|
store.addr().and_then(|addr| {
|
||||||
.pull_requests
|
store
|
||||||
.iter()
|
.pull_requests
|
||||||
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
.iter()
|
||||||
.map(|root| {
|
.find(|pr| pr.id == self.pr_id && pr.kind == Kind::GitPullRequest)
|
||||||
let update = latest_update(store.pull_requests.iter(), root);
|
.map(|root| {
|
||||||
|
let update = latest_update(store.pull_requests.iter(), root);
|
||||||
|
|
||||||
let tip = update
|
let tip = update
|
||||||
.and_then(current_commit_of)
|
.and_then(current_commit_of)
|
||||||
.or_else(|| current_commit_of(root));
|
.or_else(|| current_commit_of(root));
|
||||||
|
|
||||||
let base = update
|
let base = update
|
||||||
.and_then(merge_base_of)
|
.and_then(merge_base_of)
|
||||||
.or_else(|| merge_base_of(root));
|
.or_else(|| merge_base_of(root));
|
||||||
|
|
||||||
let clone_urls = clone_urls_of(root)
|
let clone_urls = clone_urls_of(root)
|
||||||
.or_else(|| store.announcement.as_ref().map(|a| a.clone.clone()))
|
.or_else(|| store.announcement.as_ref().map(|a| a.clone.clone()))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
PrBinding {
|
PrBinding {
|
||||||
description: root.content.clone(),
|
description: root.content.clone(),
|
||||||
patch: pull_request_patch(root, store.patches.iter()),
|
patch: pull_request_patch(root, store.patches.iter()),
|
||||||
tip,
|
tip,
|
||||||
base,
|
base,
|
||||||
clone_urls,
|
clone_urls,
|
||||||
addr: store.addr().clone(),
|
addr: addr.clone(),
|
||||||
has_patch_link: root.tags.event_ids().next().is_some(),
|
has_patch_link: root.tags.event_ids().next().is_some(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(binding) = binding else {
|
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(),
|
focus_handle: cx.focus_handle(),
|
||||||
dock_area,
|
dock_area,
|
||||||
store,
|
store,
|
||||||
@@ -330,20 +347,7 @@ impl NewPullRequestView {
|
|||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
item_sizes: Rc::new(Vec::new()),
|
item_sizes: Rc::new(Vec::new()),
|
||||||
_subscriptions: subscriptions,
|
_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.
|
/// 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.
|
// Remember this folder as a checkout of the target repository.
|
||||||
// The next panel pre-fills it.
|
// The next panel pre-fills it.
|
||||||
let addr = self.store.read(cx).addr().clone();
|
if let Some(addr) = self.store.read(cx).addr().cloned() {
|
||||||
let checkout_store = CheckoutsStore::global(cx);
|
let checkout_store = CheckoutsStore::global(cx);
|
||||||
checkout_store.update(cx, |store, cx| {
|
checkout_store.update(cx, |store, cx| {
|
||||||
store.record(PathBuf::from(&path), addr, cx);
|
store.record(PathBuf::from(&path), addr, cx);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let branches = self.branches.clone();
|
let branches = self.branches.clone();
|
||||||
let base = SharedString::from(base.clone());
|
let base = SharedString::from(base.clone());
|
||||||
@@ -524,18 +529,21 @@ impl NewPullRequestView {
|
|||||||
|
|
||||||
/// The base repository of the panel, its address and announced EUC.
|
/// The base repository of the panel, its address and announced EUC.
|
||||||
///
|
///
|
||||||
/// Used to find fork candidates.
|
/// Used to find fork candidates. `None` while the repository is not announced.
|
||||||
fn base_repo(&self, cx: &App) -> (RepoAddr, Option<String>) {
|
fn base_repo(&self, cx: &App) -> Option<(RepoAddr, Option<String>)> {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
|
let addr = store.addr()?.clone();
|
||||||
let euc = store.announcement.as_ref().and_then(|a| a.euc.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.
|
/// Announced forks of the target repository a compare can use, own first.
|
||||||
///
|
///
|
||||||
/// Re-read whenever the picker opens.
|
/// Re-read whenever the picker opens.
|
||||||
fn fork_candidates(&self, cx: &App) -> Vec<Announcement> {
|
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 user = Backend::global(cx).read(cx).current_user();
|
||||||
let announcements = RepoListStore::global(cx).read(cx).announcements.clone();
|
let announcements = RepoListStore::global(cx).read(cx).announcements.clone();
|
||||||
fork_candidates(&announcements, &base, euc.as_deref(), user)
|
fork_candidates(&announcements, &base, euc.as_deref(), user)
|
||||||
@@ -556,7 +564,9 @@ impl NewPullRequestView {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|fork| fork.announcement.addr() == announcement.addr());
|
.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 cache = GitStore::global(cx).cache().clone();
|
||||||
let mirror_path = cache.repo_path(&base);
|
let mirror_path = cache.repo_path(&base);
|
||||||
let namespace = fork_namespace(&announcement);
|
let namespace = fork_namespace(&announcement);
|
||||||
@@ -1124,8 +1134,12 @@ impl NewPullRequestView {
|
|||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static {
|
) -> impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static {
|
||||||
let view = cx.entity().downgrade();
|
let view = cx.entity().downgrade();
|
||||||
let addr = self.store.read(cx).addr().clone();
|
let associated = self
|
||||||
let associated = CheckoutsStore::global(cx).read(cx).associations_of(&addr);
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.addr()
|
||||||
|
.map(|addr| CheckoutsStore::global(cx).read(cx).associations_of(addr))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let active_path = (self.fork.is_none())
|
let active_path = (self.fork.is_none())
|
||||||
.then(|| self.repo_path.clone())
|
.then(|| self.repo_path.clone())
|
||||||
|
|||||||
@@ -20,14 +20,10 @@ use crate::views::repo::init_dialog;
|
|||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
/// Re-push the repository's refs to its announced grasp servers.
|
/// 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>) {
|
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;
|
self.error = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
store
|
self.store
|
||||||
.update(cx, |store, cx| store.push_repository(cx))
|
.update(cx, |store, cx| store.push_repository(cx))
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@@ -39,9 +35,7 @@ impl RepoDetailView {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
let Some(store) = self.store.clone() else {
|
let store = self.store.clone();
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if store.read(cx).pushing {
|
if store.read(cx).pushing {
|
||||||
return;
|
return;
|
||||||
@@ -72,23 +66,22 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// Delete the repository from nostr, announcement, state and activity.
|
/// Delete the repository from nostr, announcement, state and activity.
|
||||||
pub(super) fn delete_repository(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
pub(super) fn delete_repository(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(store) = self.store.clone() else {
|
self.store
|
||||||
return;
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.update(cx, |store, cx| store.delete_repository(cx))
|
.update(cx, |store, cx| store.delete_repository(cx))
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the issues list panel in the dock area.
|
/// Open the issues list panel in the dock area.
|
||||||
pub(super) fn open_issue_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
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;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
let Some(dock_area) = self.dock_area.upgrade() else {
|
let Some(dock_area) = self.dock_area.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let store = self.store.clone();
|
||||||
let panel = cx.new(|cx| IssuesView::new(self.dock_area.clone(), store, window, cx));
|
let panel = cx.new(|cx| IssuesView::new(self.dock_area.clone(), store, window, cx));
|
||||||
|
|
||||||
dock_area.update(cx, |dock_area, cx| {
|
dock_area.update(cx, |dock_area, cx| {
|
||||||
@@ -98,13 +91,15 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// Open the pull requests list panel in the dock area.
|
/// 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>) {
|
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;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
let Some(dock_area) = self.dock_area.upgrade() else {
|
let Some(dock_area) = self.dock_area.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let store = self.store.clone();
|
||||||
let panel = cx.new(|cx| PullRequestsView::new(self.dock_area.clone(), store, window, cx));
|
let panel = cx.new(|cx| PullRequestsView::new(self.dock_area.clone(), store, window, cx));
|
||||||
|
|
||||||
dock_area.update(cx, |dock_area, 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.
|
/// 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>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
let view = cx.entity().downgrade();
|
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
|
/// relays to connect to right away; the store loads the announcement from the
|
||||||
/// local database on its first pass, so the hint is optional.
|
/// local database on its first pass, so the hint is optional.
|
||||||
fn repo_store(addr: &RepoAddr, hint: Option<&Announcement>, cx: &mut App) -> Entity<RepoStore> {
|
fn repo_store(addr: &RepoAddr, hint: Option<&Announcement>, cx: &mut App) -> Entity<RepoStore> {
|
||||||
let relays = hint
|
cx.new(|cx| RepoStore::new(addr.clone(), hint.cloned(), cx))
|
||||||
.map(|announcement| announcement.relays.clone())
|
|
||||||
.unwrap_or_default();
|
|
||||||
cx.new(|cx| RepoStore::new(addr.clone(), relays, cx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An item of a repository to open from outside its detail panel.
|
/// 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.
|
/// The repository's own checkouts are not suggested here.
|
||||||
/// Their work is pushed, see [`Self::push_suggestion`].
|
/// Their work is pushed, see [`Self::push_suggestion`].
|
||||||
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
fn ready_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
||||||
let store = self.store.as_ref()?;
|
let store = self.store.read(cx);
|
||||||
let addr = store.read(cx).addr().clone();
|
let addr = store.addr()?;
|
||||||
let user = Backend::global(cx).read(cx).current_user()?;
|
let user = Backend::global(cx).read(cx).current_user()?;
|
||||||
if store.read(cx).is_author(&user) {
|
|
||||||
|
if store.is_author(&user) {
|
||||||
return None;
|
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 {
|
'status: for status in statuses {
|
||||||
if self
|
if self
|
||||||
@@ -33,7 +34,6 @@ impl RepoDetailView {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let store = store.read(cx);
|
|
||||||
for pr in &store.pull_requests {
|
for pr in &store.pull_requests {
|
||||||
if pr_proposes_checkout(pr, store.status_of(pr) == RepoStatus::Open, user, &status)
|
if pr_proposes_checkout(pr, store.status_of(pr) == RepoStatus::Open, user, &status)
|
||||||
{
|
{
|
||||||
@@ -50,15 +50,15 @@ impl RepoDetailView {
|
|||||||
///
|
///
|
||||||
/// Not dismissed in this panel.
|
/// Not dismissed in this panel.
|
||||||
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
|
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()?;
|
let user = Backend::global(cx).read(cx).current_user()?;
|
||||||
|
|
||||||
if !entity.read(cx).is_author(&user) {
|
if !store.is_author(&user) {
|
||||||
return None;
|
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| {
|
statuses.into_iter().find(|status| {
|
||||||
!self
|
!self
|
||||||
@@ -75,10 +75,7 @@ impl RepoDetailView {
|
|||||||
let key = (status.path.clone(), status.branch.clone());
|
let key = (status.path.clone(), status.branch.clone());
|
||||||
let path = status.path.clone();
|
let path = status.path.clone();
|
||||||
// The push busy flag lives on the store; it disables the banner's triggers.
|
// The push busy flag lives on the store; it disables the banner's triggers.
|
||||||
let pushing = self
|
let pushing = self.store.read(cx).pushing;
|
||||||
.store
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|store| store.read(cx).pushing);
|
|
||||||
|
|
||||||
let commits = if status.ahead == 1 {
|
let commits = if status.ahead == 1 {
|
||||||
SharedString::from("1 commit")
|
SharedString::from("1 commit")
|
||||||
@@ -160,8 +157,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// Warning after a push that only some grasp servers accepted.
|
/// Warning after a push that only some grasp servers accepted.
|
||||||
pub(super) fn render_push_warning_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
pub(super) fn render_push_warning_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||||
let store = self.store.as_ref()?;
|
let store = self.store.read(cx);
|
||||||
let store = store.read(cx);
|
|
||||||
let warning = store.last_push_warning.clone()?;
|
let warning = store.last_push_warning.clone()?;
|
||||||
let pushing = store.pushing;
|
let pushing = store.pushing;
|
||||||
|
|
||||||
@@ -213,11 +209,9 @@ impl RepoDetailView {
|
|||||||
.ghost()
|
.ghost()
|
||||||
.disabled(pushing)
|
.disabled(pushing)
|
||||||
.on_click(cx.listener(|this, _ev, _window, cx| {
|
.on_click(cx.listener(|this, _ev, _window, cx| {
|
||||||
if let Some(store) = this.store.clone() {
|
this.store.update(cx, |store, _| {
|
||||||
store.update(cx, |store, _| {
|
store.last_push_warning = None;
|
||||||
store.last_push_warning = None;
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
@@ -299,14 +293,12 @@ impl RepoDetailView {
|
|||||||
.small()
|
.small()
|
||||||
.info()
|
.info()
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
if let Some(store) = this.store.clone() {
|
open_new_pull_panel(
|
||||||
open_new_pull_panel(
|
this.dock_area.clone(),
|
||||||
this.dock_area.clone(),
|
this.store.clone(),
|
||||||
store,
|
window,
|
||||||
window,
|
cx,
|
||||||
cx,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -27,15 +27,11 @@ impl RepoDetailView {
|
|||||||
/// The NIP-34 header, actions and issues/PR counts.
|
/// The NIP-34 header, actions and issues/PR counts.
|
||||||
/// Or the local header with an Init button for an unpublished repository.
|
/// Or the local header with an Init button for an unpublished repository.
|
||||||
pub(super) fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
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);
|
return self.render_local_header(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(store_entity) = self.store.as_ref() else {
|
let store = self.store.read(cx);
|
||||||
return div().into_any_element();
|
|
||||||
};
|
|
||||||
|
|
||||||
let store = store_entity.read(cx);
|
|
||||||
let issue_count = SharedString::from(store.issue_count().to_string());
|
let issue_count = SharedString::from(store.issue_count().to_string());
|
||||||
let pr_count = SharedString::from(store.pull_request_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 pushing = store.pushing;
|
||||||
let cloning = store.cloning;
|
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();
|
return div().into_any_element();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,19 +69,18 @@ impl RepoDetailView {
|
|||||||
.on_action(
|
.on_action(
|
||||||
cx.listener(|this, action: &RepoAction, window, cx| match action {
|
cx.listener(|this, action: &RepoAction, window, cx| match action {
|
||||||
RepoAction::NewIssue => {
|
RepoAction::NewIssue => {
|
||||||
if let Some(store) = this.store.clone() {
|
open_new_issue_dialog(this.store.clone(), window, cx);
|
||||||
open_new_issue_dialog(store, window, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
RepoAction::NewPR => {
|
RepoAction::NewPR => {
|
||||||
if let Some(store) = this.store.clone() {
|
open_new_pull_panel(this.dock_area.clone(), this.store.clone(), window, cx);
|
||||||
open_new_pull_panel(this.dock_area.clone(), store, window, cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
RepoAction::SendPatch => {
|
RepoAction::SendPatch => {
|
||||||
if let Some(store) = this.store.clone() {
|
open_send_patch_panel(
|
||||||
open_send_patch_panel(this.dock_area.clone(), store, window, cx);
|
this.dock_area.clone(),
|
||||||
}
|
this.store.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
RepoAction::About => {
|
RepoAction::About => {
|
||||||
if let Some(announcement) = this.announcement(cx) {
|
if let Some(announcement) = this.announcement(cx) {
|
||||||
@@ -421,7 +416,9 @@ impl RepoDetailView {
|
|||||||
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_local_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let name = self.display_name(cx);
|
let name = self.display_name(cx);
|
||||||
let path = self
|
let path = self
|
||||||
.local_path
|
.store
|
||||||
|
.read(cx)
|
||||||
|
.path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|path| path.display().to_string())
|
.map(|path| path.display().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ fn init_repository(
|
|||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
if let Some(view) = view.upgrade() {
|
if let Some(view) = view.upgrade() {
|
||||||
view.update(cx, |this, cx| {
|
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;
|
self.error = None;
|
||||||
cx.notify();
|
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.
|
// Local repositories live on disk at their scan path.
|
||||||
// No clone step or network refresh applies here.
|
// 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 task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||||
let data = cx
|
let data = cx
|
||||||
.background_spawn(async move {
|
.background_spawn(async move {
|
||||||
@@ -67,13 +82,14 @@ impl RepoDetailView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(initial) = self.initial.as_ref() else {
|
let Some(announcement) = announcement else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
self.repo_started = true;
|
||||||
|
|
||||||
let cache = GitStore::global(cx).cache().clone();
|
let cache = GitStore::global(cx).cache().clone();
|
||||||
let addr = initial.addr();
|
let addr = announcement.addr();
|
||||||
let clone_urls: Vec<Url> = initial.clone.clone();
|
let clone_urls: Vec<Url> = announcement.clone.clone();
|
||||||
|
|
||||||
// Captured before the loads start.
|
// Captured before the loads start.
|
||||||
// A branch/tag switch bumps the generation, discarding the refresh below.
|
// 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.
|
/// 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>) {
|
pub(super) fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(store) = self.store.clone() else {
|
let store = self.store.clone();
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = {
|
let name = {
|
||||||
let Some(announcement) = self.announcement(cx) else {
|
let Some(announcement) = self.announcement(cx) else {
|
||||||
|
|||||||
@@ -71,19 +71,14 @@ pub struct RepoDetailView {
|
|||||||
///
|
///
|
||||||
/// New panels, commit diffs, are added there.
|
/// New panels, commit diffs, are added there.
|
||||||
dock_area: WeakEntity<DockArea>,
|
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
|
/// A repository opened by address alone starts without an announcement; the
|
||||||
/// repository opened by address until its store loads the announcement.
|
/// store observer starts the load once the first one lands.
|
||||||
initial: Option<Announcement>,
|
repo_started: bool,
|
||||||
/// 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>,
|
|
||||||
/// File explorer state, the worktree of the local clone.
|
/// File explorer state, the worktree of the local clone.
|
||||||
tree_state: Entity<TreeState>,
|
tree_state: Entity<TreeState>,
|
||||||
/// Root of the local clone, for reading files on demand.
|
/// Root of the local clone, for reading files on demand.
|
||||||
@@ -178,18 +173,8 @@ impl RepoDetailView {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// The announcement we opened from already carries the NIP-34 `relays` tag.
|
let store = cx.new(|cx| RepoStore::new(addr, hint, cx));
|
||||||
//
|
Self::new_common(dock_area, store, window, cx)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a local repository discovered by the scan.
|
/// Open a local repository discovered by the scan.
|
||||||
@@ -199,7 +184,8 @@ impl RepoDetailView {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> 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.
|
/// Shared construction.
|
||||||
@@ -207,9 +193,7 @@ impl RepoDetailView {
|
|||||||
/// File explorer state, ref selectors and the deferred repository load.
|
/// File explorer state, ref selectors and the deferred repository load.
|
||||||
fn new_common(
|
fn new_common(
|
||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
initial: Option<Announcement>,
|
store: Entity<RepoStore>,
|
||||||
store: Option<Entity<RepoStore>>,
|
|
||||||
local_path: Option<PathBuf>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -271,11 +255,10 @@ impl RepoDetailView {
|
|||||||
this.load_repo(window, cx);
|
this.load_repo(window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
let mut view = Self {
|
||||||
initial,
|
|
||||||
dock_area,
|
dock_area,
|
||||||
store,
|
store: store.clone(),
|
||||||
local_path,
|
repo_started: false,
|
||||||
tree_state,
|
tree_state,
|
||||||
worktree: None,
|
worktree: None,
|
||||||
worktree_paths: Vec::new(),
|
worktree_paths: Vec::new(),
|
||||||
@@ -311,31 +294,40 @@ impl RepoDetailView {
|
|||||||
push_statuses: Vec::new(),
|
push_statuses: Vec::new(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
view.attach_store(&store, window, cx);
|
||||||
|
view
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The latest announcement from the store or the open-time snapshot.
|
/// The latest announcement of the repository, `None` while local-only or
|
||||||
/// `None` for local repositories that haven't been published yet.
|
/// until the store's first pass loads it.
|
||||||
fn announcement<'a>(&'a self, cx: &'a App) -> Option<&'a Announcement> {
|
fn announcement<'a>(&'a self, cx: &'a App) -> Option<&'a Announcement> {
|
||||||
let store = self.store.as_ref()?;
|
self.store.read(cx).announcement.as_ref()
|
||||||
store
|
|
||||||
.read(cx)
|
|
||||||
.announcement
|
|
||||||
.as_ref()
|
|
||||||
.or(self.initial.as_ref())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Display name, the announcement's name or ID for announced repositories.
|
/// Display name, the announcement's name or ID for announced repositories.
|
||||||
/// The directory name for local ones.
|
/// The directory name for local ones.
|
||||||
fn display_name(&self, cx: &App) -> SharedString {
|
fn display_name(&self, cx: &App) -> SharedString {
|
||||||
if let Some(path) = &self.local_path {
|
let store = self.store.read(cx);
|
||||||
return SharedString::from(
|
|
||||||
path.file_name()
|
if store.addr().is_none() {
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
return store
|
||||||
.unwrap_or_else(|| path.display().to_string()),
|
.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| {
|
.map(|announcement| {
|
||||||
announcement
|
announcement
|
||||||
.name
|
.name
|
||||||
@@ -386,8 +378,10 @@ impl Render for RepoDetailView {
|
|||||||
// operations, republish, checkout push, delete and clone-to-folder.
|
// operations, republish, checkout push, delete and clone-to-folder.
|
||||||
let error = self.error.clone().or_else(|| {
|
let error = self.error.clone().or_else(|| {
|
||||||
self.store
|
self.store
|
||||||
.as_ref()
|
.read(cx)
|
||||||
.and_then(|store| store.read(cx).last_error.clone().map(SharedString::from))
|
.last_error
|
||||||
|
.clone()
|
||||||
|
.map(SharedString::from)
|
||||||
});
|
});
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -405,9 +399,7 @@ impl Render for RepoDetailView {
|
|||||||
.banner()
|
.banner()
|
||||||
.on_close(cx.listener(|this, _event, _window, cx| {
|
.on_close(cx.listener(|this, _event, _window, cx| {
|
||||||
this.error = None;
|
this.error = None;
|
||||||
if let Some(store) = this.store.clone() {
|
this.store.update(cx, |store, _| store.last_error = None);
|
||||||
store.update(cx, |store, _| store.last_error = None);
|
|
||||||
}
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use gpui::prelude::*;
|
|
||||||
use gpui::{Context, Entity, Window};
|
use gpui::{Context, Entity, Window};
|
||||||
use signed_core::Announcement;
|
use signed_core::Announcement;
|
||||||
use signed_state::{Backend, CheckoutsStore, LocalReposStore, RepoStore};
|
use signed_state::{Backend, CheckoutsStore, LocalReposStore, RepoStore};
|
||||||
@@ -6,33 +5,29 @@ use signed_state::{Backend, CheckoutsStore, LocalReposStore, RepoStore};
|
|||||||
use super::RepoDetailView;
|
use super::RepoDetailView;
|
||||||
|
|
||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
/// Switch the repository into its NIP-34 mode after a successful init.
|
/// Switch a local repository into its NIP-34 mode after a successful init.
|
||||||
/// Creates the nostr store for the announced repository.
|
/// The store is kept, so the panel keeps its path and loaded worktree.
|
||||||
/// Drops the local scan identity.
|
/// Drops the local scan identity so it leaves the sidebar's local section.
|
||||||
/// The worktree is unchanged, so the explorer keeps its loaded content.
|
|
||||||
pub(crate) fn apply_announcement(
|
pub(crate) fn apply_announcement(
|
||||||
&mut self,
|
&mut self,
|
||||||
announcement: Announcement,
|
announcement: Announcement,
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
// The repository is no longer a bare local repo.
|
let path = self.store.read(cx).path.clone();
|
||||||
// Drop it from the scan results so it leaves the sidebar's local section.
|
if let Some(path) = path {
|
||||||
if let Some(path) = self.local_path.take() {
|
|
||||||
LocalReposStore::global(cx).update(cx, |store, cx| store.remove(&path, cx));
|
LocalReposStore::global(cx).update(cx, |store, cx| store.remove(&path, cx));
|
||||||
}
|
}
|
||||||
let store =
|
|
||||||
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx));
|
self.store
|
||||||
// Re-render on store refreshes, issues, PRs and statuses.
|
.update(cx, |store, cx| store.announce(announcement, cx));
|
||||||
// Keep the ready-to-contribute statuses of this repository requested.
|
|
||||||
self.attach_store(&store, window, cx);
|
// The new address needs its ready-to-contribute statuses requested.
|
||||||
self.store = Some(store);
|
self.refresh_ready_statuses(cx);
|
||||||
self.initial = Some(announcement);
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observe the repository's store, re-render on refreshes.
|
/// Observe the repository's store and re-render on its refreshes.
|
||||||
/// Request the ready-to-contribute statuses for it.
|
/// Start the explorer once the store has an announcement.
|
||||||
pub(super) fn attach_store(
|
pub(super) fn attach_store(
|
||||||
&mut self,
|
&mut self,
|
||||||
store: &Entity<RepoStore>,
|
store: &Entity<RepoStore>,
|
||||||
@@ -42,18 +37,9 @@ impl RepoDetailView {
|
|||||||
self._subscriptions
|
self._subscriptions
|
||||||
.push(cx.observe_in(store, window, |this, store, window, cx| {
|
.push(cx.observe_in(store, window, |this, store, window, cx| {
|
||||||
this.refresh_ready_statuses(cx);
|
this.refresh_ready_statuses(cx);
|
||||||
|
if !this.repo_started && store.read(cx).announcement.is_some() {
|
||||||
// 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);
|
|
||||||
this.load_repo(window, cx);
|
this.load_repo(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}));
|
}));
|
||||||
self.refresh_ready_statuses(cx);
|
self.refresh_ready_statuses(cx);
|
||||||
@@ -64,11 +50,11 @@ impl RepoDetailView {
|
|||||||
/// Owned repositories are watched for unpushed commits.
|
/// Owned repositories are watched for unpushed commits.
|
||||||
/// Other repositories for ready-to-contribute checkouts.
|
/// Other repositories for ready-to-contribute checkouts.
|
||||||
fn refresh_ready_statuses(&mut self, cx: &mut Context<Self>) {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let head = entity.read(cx).head.clone();
|
let head = self.store.read(cx).head.clone();
|
||||||
|
|
||||||
if self.ready_requested && self.ready_head == head {
|
if self.ready_requested && self.ready_head == head {
|
||||||
return;
|
return;
|
||||||
@@ -77,14 +63,13 @@ impl RepoDetailView {
|
|||||||
self.ready_requested = true;
|
self.ready_requested = true;
|
||||||
self.ready_head = head.clone();
|
self.ready_head = head.clone();
|
||||||
|
|
||||||
let addr = entity.read(cx).addr().clone();
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let checkout = CheckoutsStore::global(cx);
|
let checkout = CheckoutsStore::global(cx);
|
||||||
|
|
||||||
let owned = backend
|
let owned = backend
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.current_user()
|
.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| {
|
checkout.update(cx, |store, cx| {
|
||||||
// The ready statuses keep the fast poll running while the panel is open.
|
// 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
|
/// The ready-to-contribute and ready-to-push statuses of this repository.
|
||||||
/// store changed since they last drove a render.
|
|
||||||
///
|
|
||||||
/// Updates the cached slices. `None` store (a local, not yet published,
|
|
||||||
/// repository) has no statuses.
|
|
||||||
pub(super) fn refresh_statuses(&mut self, cx: &mut Context<Self>) -> bool {
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
let addr = entity.read(cx).addr().clone();
|
|
||||||
let checkouts = CheckoutsStore::global(cx).read(cx);
|
let checkouts = CheckoutsStore::global(cx).read(cx);
|
||||||
let ready_statuses = checkouts.ready_statuses_of(&addr);
|
let ready_statuses = checkouts.ready_statuses_of(&addr);
|
||||||
let push_statuses = checkouts.push_statuses_of(&addr);
|
let push_statuses = checkouts.push_statuses_of(&addr);
|
||||||
|
|||||||
+42
-13
@@ -1,6 +1,6 @@
|
|||||||
# Repository state and panel flow plan
|
# 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),
|
Builds on `docs/backend-rearchitecture.md`, especially §7 (notify audit),
|
||||||
§11 (split independently-observed state), §12 (one debounce at the source)
|
§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
|
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
|
hint, so a repository panel opens from a `RepoAddr` alone. This pulls the
|
||||||
address-based constructor forward from Phase 3 step 2.
|
address-based constructor forward from Phase 3 step 2.
|
||||||
4. `RepoDetailView::attach_store` adopts the store's first announcement when
|
4. `RepoDetailView::attach_store` starts the explorer from the store's first
|
||||||
`initial` is still empty and calls `load_repo`, so a panel opened by address
|
announcement when the panel was opened by address alone, so a panel opened
|
||||||
fills in instead of waiting for the caller to have the announcement.
|
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
|
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
|
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.
|
the announcement from the bootstrap relays and step 4 loads the explorer.
|
||||||
|
|
||||||
### Phase 3 - one entity for local and NIP-34
|
### Phase 3 - one entity for local and NIP-34
|
||||||
|
|
||||||
1. `signed_state/src/repo.rs`: `addr`/`path` options, `new_local`,
|
Status: implemented.
|
||||||
`announce`, `Option<Subscription>`, action guards.
|
|
||||||
2. `views/repo/mod.rs`: single `store` field; `new_local`; header,
|
1. `signed_state/src/repo.rs`: `addr: Option<RepoAddr>`,
|
||||||
display name, `load_repo`, `open_init_dialog` derive from the store. The
|
`path: Option<PathBuf>`, `announcement: Option<Announcement>`,
|
||||||
address-based `new` is already in place from Phase 2.
|
`_subscription: Option<Subscription>`. `new(addr, hint, cx)` seeds the
|
||||||
3. `views/repo/store.rs`: always observe; `refresh_statuses` returns false
|
announcement and relays from the hint; `new_local(path)`; `announce`
|
||||||
when not announced.
|
switches a local store to NIP-34 in place, keeping `path`. `addr()` returns
|
||||||
4. `views/repo/actions.rs`, `header.rs`, `banners.rs`: drop
|
`Option<&RepoAddr>`; `refresh`/`connect_announced_relays`/`subscribe_remote`
|
||||||
`Option<Entity<RepoStore>>` guards, guard on `addr()` instead.
|
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
|
5. `LocalReposStore` stays as the scan index; `CheckoutsStore` stays the
|
||||||
association authority.
|
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
|
### Phase 4 - deferred, only if duplicate stores become a problem
|
||||||
|
|
||||||
One store per address via `HashMap<RepoAddr, WeakEntity<RepoStore>>` inside
|
One store per address via `HashMap<RepoAddr, WeakEntity<RepoStore>>` inside
|
||||||
|
|||||||
Reference in New Issue
Block a user