strip redundant doc comments from signed_core and signed_git

This commit is contained in:
2026-10-03 16:31:37 +07:00
parent 6706e328ec
commit a56b0b8881
31 changed files with 265 additions and 1031 deletions
+35 -147
View File
@@ -21,74 +21,41 @@ use crate::checkouts::CheckoutsStore;
use crate::push::{GraspPush, PushOutcome, grasp_base_url, grasp06_prs_url, pr_clone_urls};
use crate::repos::RepoListStore;
/// Maximum size of one patch event.
///
/// NIP-34 suggests patches when each event is under 60kb.
// NIP-34 suggests patches when each event is under 60kb.
const MAX_PATCH_EVENT_BYTES: usize = 60 * 1024;
/// Per-repository store.
///
/// Holds the announcement, state, issues, patches, PRs, comments and resolved statuses.
pub struct RepoStore {
/// 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.
// 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.
// The scan path for a local repository, kept when it is later announced so
// the panel keeps its worktree.
pub path: Option<PathBuf>,
/// NIP-34 state detected on disk for a local repository, if any.
pub nip34: Option<Nip34Binding>,
/// 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.
pub loaded: bool,
/// Branch pointed to by `HEAD` in the latest state announcement.
pub head: Option<String>,
pub issues: Vec<Event>,
pub patches: Vec<Event>,
pub pull_requests: Vec<Event>,
/// Comments on issues / PRs, oldest first.
pub comments: Vec<Event>,
/// Resolved status per root event, issue, patch or PR.
status_by_root: HashMap<EventId, RepoStatus>,
/// Open issue and root PR counts.
/// Computed with [`Self::status_by_root`] on every refresh.
open_issue_count: usize,
open_pr_count: usize,
pub last_error: Option<String>,
/// Non-fatal warning of the last action, if any.
///
/// Example, a PR published without its commit reaching a grasp server.
pub last_warning: Option<String>,
/// Warning of the last push that only some grasp servers accepted.
///
/// The repository is out of sync on the rejected servers until it is republished.
// The repository is out of sync on the rejected servers until republished.
pub last_push_warning: Option<String>,
/// A republish or a checkout push is in flight.
///
/// Views show a spinner and disable their push triggers while it is set.
pub pushing: bool,
/// A clone-into-a-folder operation is in flight.
///
/// Views show a spinner and disable the clone trigger while it is set.
pub cloning: bool,
/// Relays already asked to connect to, from this repository's NIP-34 `relays` tag.
///
/// Avoids re-subscribing and re-fetching on every refresh.
// Avoids re-subscribing and re-fetching on every refresh.
repo_relays: HashSet<RelayUrl>,
/// Root events, issues, patches and PRs, already fetched per root.
///
/// The per-root fetches cover NIP-22 comments and statuses without an `a` tag.
// Covers NIP-22 comments and statuses without an `a` tag.
root_fetches: HashSet<EventId>,
/// Maintainers already synced through gossip in Uncensored mode.
///
/// Avoids re-running the maintainer Auto sync on every refresh.
synced_maintainers: HashSet<PublicKey>,
/// In-flight tasks, cancelled when the store drops.
// In-flight tasks, cancelled when the store drops.
tasks: Vec<Task<Result<(), Error>>>,
/// Backend subscription of an announced repository. `None` while local-only.
_subscription: Option<Subscription>,
}
@@ -141,7 +108,6 @@ impl RepoStore {
}
}
/// Local repository discovered by the scan, not announced to NIP-34 yet.
pub fn new_local(path: PathBuf, nip34: Option<Nip34Binding>) -> Self {
Self {
addr: None,
@@ -170,7 +136,6 @@ impl RepoStore {
}
}
/// An announced repository whose working copy is already on disk.
pub fn from_worktree(
addr: RepoAddr,
announcement: Announcement,
@@ -182,7 +147,6 @@ impl RepoStore {
store
}
/// 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());
@@ -235,7 +199,6 @@ impl RepoStore {
self.addr.as_ref()
}
/// Returns the repository's name, or `Unknown` when not known.
pub fn name(&self) -> SharedString {
self.announcement
.as_ref()
@@ -244,9 +207,6 @@ impl RepoStore {
})
}
/// Filters that make up a repository.
///
/// Announcement, state, activity and deletions targeting it.
fn repo_filters(addr: &RepoAddr) -> Vec<Filter> {
let mut filters = vec![
Filter::new()
@@ -255,12 +215,11 @@ impl RepoStore {
.identifier(addr.identifier()),
addr.activity_filter(),
];
// Deletion requests, NIP-09/62, must be known before any event is shown.
// Deletion requests must be known before any event is shown.
filters.extend(addr.deletion_filters());
filters
}
/// 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;
@@ -285,39 +244,33 @@ impl RepoStore {
});
}
/// Filters the SDK resolves through NIP-65 gossip in Uncensored mode.
// NIP-34 events tag the announcement author, which may not be a
// maintainer for subordinate forks.
fn maintainer_filters(addr: &RepoAddr, maintainers: &[PublicKey]) -> Vec<Filter> {
let mut pubkeys = maintainers.to_vec();
// NIP-34 events tag the announcement author,
// which may not be a maintainer for subordinate forks.
if !pubkeys.contains(&addr.public_key()) {
pubkeys.push(addr.public_key());
}
vec![
// Announcement and state events, including co-maintainer states.
Filter::new()
.kinds([Kind::GitRepoAnnouncement, Kind::RepoState])
.authors(pubkeys.clone())
.identifier(addr.identifier()),
// Activity tagging a maintainer, resolved to their read relays.
Filter::new()
.kinds(filters::ACTIVITY_KINDS)
.coordinate(addr.coordinate())
.pubkeys(pubkeys.clone()),
// Activity authored by a maintainer, resolved to their write relays.
Filter::new()
.kinds(filters::ACTIVITY_KINDS)
.coordinate(addr.coordinate())
.authors(pubkeys.clone()),
// Deletions authored by a maintainer.
Filter::new()
.kinds([Kind::EventDeletion, Kind::RequestToVanish])
.authors(pubkeys),
]
}
/// In Uncensored mode, sync the maintainer-shaped filters through the SDK's NIP-65 gossip targeting
fn sync_maintainer_relays(&mut self, maintainers: &[PublicKey], cx: &mut Context<Self>) {
let strategy = SettingsStore::try_global(cx)
.map(|store| store.read(cx).settings().event_fetching)
@@ -358,7 +311,6 @@ impl RepoStore {
});
}
/// Re-query the local database and update all fields.
pub fn refresh(&mut self, cx: &mut Context<Self>) {
if self.addr.is_none() {
return;
@@ -531,7 +483,6 @@ impl RepoStore {
this.announcement = announcement;
}
// The announcement may list relays for this repository's activity.
let relays = this
.announcement
.as_ref()
@@ -540,7 +491,6 @@ impl RepoStore {
this.connect_announced_relays(&relays, cx);
// Uncensored mode also covers the maintainers' NIP-65 relays.
let maintainers = this
.announcement
.as_ref()
@@ -602,22 +552,18 @@ impl RepoStore {
self.tasks.push(task);
}
/// Resolve the status of a root event, an issue, patch or PR, per NIP-34.
pub fn status_of(&self, root: &Event) -> RepoStatus {
status_of(&self.status_by_root, root)
}
/// Number of open issues.
pub fn issue_count(&self) -> usize {
self.open_issue_count
}
/// Number of open pull requests.
pub fn pull_request_count(&self) -> usize {
self.open_pr_count
}
/// Whether `user` is the author or owner of this repository.
pub fn is_author(&self, user: &PublicKey) -> bool {
self.addr
.as_ref()
@@ -647,14 +593,10 @@ impl RepoStore {
.filter(move |e| e.references_root(root))
}
/// Comment on a root event, an issue or PR, per NIP-34, kind 1111.
pub fn comment(&mut self, root: &Event, content: String, cx: &mut Context<Self>) {
self.reply(root, None, content, cx);
}
/// Reply to `parent`, a comment on `root`, with a NIP-22 threaded comment.
///
/// `None` publishes a top-level comment on the root itself.
fn reply(
&mut self,
root: &Event,
@@ -710,8 +652,8 @@ impl RepoStore {
return;
}
// The tip of the series is its last commit.
// `git format-patch` orders patches oldest first.
// The tip of the series is its last commit; `git format-patch` orders
// patches oldest first.
let Some(current_commit) = series.tip_commit() else {
self.last_error = Some(
"Patch must be `git format-patch` output with a `From <commit-id>` header".into(),
@@ -731,7 +673,6 @@ impl RepoStore {
cx.notify();
return;
};
// The author's npub names their GRASP-06 namespace, `/prs/...`.
let author_npub = user.to_bech32().unwrap();
let owner = addr.public_key();
@@ -762,8 +703,6 @@ impl RepoStore {
};
let task: Task<Result<(), Error>> = cx.spawn(async move |this, cx| {
// The PR references the root patch,
// viewers can then find the patch without carrying it inline.
let root_patch = match publish_patch_series(
&client,
&signer,
@@ -785,11 +724,10 @@ impl RepoStore {
}
};
// GRASP-06 pushes the tip to the author's own grasp servers.
// The path is `/prs/<author-npub>/<repo-id>.git`.
// Contributing to another project never depends on that project's servers.
// Resolve the servers from the author's latest kind-10317 grasp list.
// The settings defaults stand in when no list is published or the query fails.
// GRASP-06 pushes the tip to the author's own grasp servers at
// `/prs/<author-npub>/<repo-id>.git`; contributing to another
// project never depends on that project's servers. Resolve the
// servers from the author's latest kind-10317 grasp list.
let author_servers = {
let query_client = client.clone();
let published = cx
@@ -866,7 +804,6 @@ impl RepoStore {
}
})?;
// Sign before publishing.
let event = cx
.background_spawn({
let signer = signer.clone();
@@ -883,7 +820,6 @@ impl RepoStore {
let path = path.clone();
let tip = tip.clone();
let reference = reference.clone();
// Author servers first, then the announced base grasp servers.
let targets: Vec<(String, String)> = author_targets
.into_iter()
.chain(base_targets)
@@ -933,8 +869,6 @@ impl RepoStore {
}
};
// A draft PR carries a kind-1633 status event, NIP-34.
// Publish it right after the PR event so viewers never show it open.
if draft {
this.update(cx, |this, cx| {
this.set_status(&pr_event, RepoStatus::Draft, cx);
@@ -946,12 +880,6 @@ impl RepoStore {
self.tasks.push(task);
}
/// Generate the patch between `merge_base` and `compare_ref` in `repo_path`,
/// then open a pull request from it.
///
/// Fails descriptively when there are no commits to propose or the patch
/// could not be generated; otherwise publishes exactly like
/// [`Self::open_pull_request`].
#[allow(clippy::too_many_arguments)]
pub fn open_pull_request_from_refs(
&mut self,
@@ -965,8 +893,8 @@ impl RepoStore {
cx: &mut Context<Self>,
) -> Task<Result<(), Error>> {
cx.spawn(async move |this, cx| {
// Regenerate the series at submit time.
// The published patch covers the current tip of the compare branch.
// Regenerate at submit time so the published patch covers the
// current tip of the compare branch.
let patch = cx
.background_spawn({
let repo_path = repo_path.clone();
@@ -999,9 +927,6 @@ impl RepoStore {
})
}
/// Update a pull request.
///
/// Other authors must open a new PR.
pub fn update_pull_request(&mut self, root: &Event, patch: String, cx: &mut Context<Self>) {
self.last_error = None;
self.last_warning = None;
@@ -1034,7 +959,7 @@ impl RepoStore {
return;
}
// The new tip of the PR is the last commit of the series.
// The tip of the updated PR is the last commit of the series.
let Some(current_commit) = series.tip_commit() else {
self.last_error = Some(
"Patch must be `git format-patch` output with a `From <commit-id>` header".into(),
@@ -1043,8 +968,8 @@ impl RepoStore {
return;
};
// The first revision patch replies to the original root patch, NIP-34.
// Use the PR's `e` tag, or the oldest patch of the linked set if the PR has none.
// The first revision patch replies to the original root patch, NIP-34:
// use the PR's `e` tag, or the oldest patch of the linked set.
let root_patch_id = root.tags.event_ids().next().or_else(|| {
PullRequest::new(root)
.patches(self.patches.iter())
@@ -1097,8 +1022,8 @@ impl RepoStore {
}
.into_event_builder();
// The `r` EUC tag lets clients subscribe to all PR updates.
// The SDK builder omits it.
// The `r` EUC tag lets clients subscribe to all PR updates; the
// SDK builder omits it.
match euc.as_deref() {
Some(euc) => builder.tag(Tag::parse(["r", euc]).expect("valid r tag")),
None => builder,
@@ -1122,9 +1047,6 @@ impl RepoStore {
self.tasks.push(task);
}
/// Set the status of a root event.
///
/// Only the root author or a maintainer may set it, per NIP-34.
fn set_status(&mut self, root: &Event, status: RepoStatus, cx: &mut Context<Self>) {
self.last_error = None;
@@ -1166,8 +1088,6 @@ impl RepoStore {
self.publish(builder, cx);
}
/// 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(|| {
@@ -1227,11 +1147,6 @@ impl RepoStore {
self.run_push(push, Some((addr, path)), cx)
}
/// Run a backend push task, tracking progress in [`Self::pushing`] and
/// the outcome in [`Self::last_error`] and [`Self::last_push_warning`].
///
/// `pushed_checkout` names the checkout whose ready-to-push statuses
/// should be recomputed after the remote moved.
fn run_push(
&mut self,
push: Task<Result<PushOutcome, Error>>,
@@ -1252,11 +1167,8 @@ impl RepoStore {
match &result {
Ok(outcome) => {
this.last_error = None;
// A push only some grasp servers accepted is a warning:
// the repo is out of sync on the rest until it is republished.
this.last_push_warning = outcome.partial_warning();
if let Some((addr, path)) = &pushed_checkout {
// The remote moved, so recompute the ready-to-push statuses.
CheckoutsStore::global(cx).update(cx, |store, cx| {
store.checkout_pushed(addr, path, cx);
});
@@ -1275,10 +1187,6 @@ impl RepoStore {
})
}
/// Delete the repository from nostr, announcement, state and activity.
///
/// 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 Some(addr) = self.addr.clone() else {
return self.action_error("This repository is not published to Nostr yet", cx);
@@ -1300,8 +1208,7 @@ impl RepoStore {
})
}
/// Clone the repository into `destination`, a user-chosen folder outside
/// the cache, and remember the clone as a checkout of this repository.
// A user-chosen folder outside the cache; remembered as a checkout.
pub fn clone_to_folder(
&mut self,
destination: PathBuf,
@@ -1329,8 +1236,7 @@ impl RepoStore {
let clone = {
let destination = destination.clone();
// The clone's repository handle is dropped in the task: gix handles
// are not `Send`, they must not cross the spawn boundary.
// gix handles are not `Send` and must not cross the spawn boundary.
cx.background_spawn(async move { Repo::clone(&clone_urls, &destination).map(|_| ()) })
};
@@ -1376,12 +1282,8 @@ impl RepoStore {
Task::ready(Err(anyhow::anyhow!("{message}")))
}
/// Sign `builder`, broadcast it and track the outcome in [`Self::last_error`].
///
/// Every one-shot repository event (issue, comment, status) goes through
/// this. Multi-step flows (opening or updating a pull request, a patch
/// series) call the SDK directly instead, since their error handling and
/// post-conditions differ per step.
// Every one-shot repository event (issue, comment, status) goes through
// this; multi-step flows call the SDK directly instead.
fn publish(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
self.last_error = None;
@@ -1446,15 +1348,13 @@ fn resolve_statuses(
.collect()
}
/// The proposed commit of a `git format-patch` output.
/// It is the `From <commit>` header on the first line.
// The `From <commit>` header on the first line.
fn patch_current_commit(patch: &str) -> Option<&str> {
let line = patch.lines().next()?;
let hex = line.strip_prefix("From ")?;
hex.split_whitespace().next().filter(|hex| hex.len() == 40)
}
/// A `git format-patch` series with the facts derived from its parts.
struct PatchSeries {
parts: Vec<String>,
}
@@ -1469,7 +1369,7 @@ impl PatchSeries {
}
}
/// The byte length of the first part over the NIP-34 size suggestion.
// The byte length of the first part over the NIP-34 size suggestion.
fn oversized_length(&self) -> Option<usize> {
self.parts
.iter()
@@ -1477,8 +1377,8 @@ impl PatchSeries {
.find(|length| *length > MAX_PATCH_EVENT_BYTES)
}
/// The tip of the series is its last commit;
/// `git format-patch` orders patches oldest first.
// The tip of the series is its last commit; `git format-patch` orders
// patches oldest first.
fn tip_commit(&self) -> Option<Sha1Hash> {
self.parts
.last()
@@ -1494,9 +1394,7 @@ impl PatchSeries {
}
}
/// Publish a `git format-patch` series as chained kind-1617 events.
///
/// Returns the root event, the one a PR references.
// Returns the root event, the one a PR references.
#[allow(clippy::too_many_arguments)]
async fn publish_patch_series(
client: &Client,
@@ -1566,7 +1464,6 @@ async fn publish_patch_series(
root.ok_or_else(|| anyhow::anyhow!("patch series is empty"))
}
/// Build a NIP-22 kind-1111 comment.
fn comment_builder(
root: &Event,
parent: Option<&Event>,
@@ -1630,19 +1527,15 @@ mod tests {
assert!(kinds.contains(&expected), "missing {expected} tag");
}
// The uppercase `E` tag scopes the root, with its id, relay hint and author.
let e = event.tags.iter().find(|t| t.kind() == "E").expect("E tag");
let slice = e.as_slice();
assert_eq!(slice[1], root.id.to_hex());
assert_eq!(slice[2], relay.as_str());
assert_eq!(slice[3], root.pubkey.to_hex());
// The lowercase `e` tag references the parent.
// For a top-level comment the parent is the root itself.
let e = event.tags.iter().find(|t| t.kind() == "e").expect("e tag");
assert_eq!(e.as_slice()[1], root.id.to_hex());
// Signed's own `references_root` must keep matching the comment.
assert!(event.references_root(&root.id));
}
@@ -1652,13 +1545,11 @@ mod tests {
let maintainer = Keys::generate().public_key();
let addr = signed_core::RepoAddr::new(owner, "my-repo");
// The owner is not among the maintainers, as on a subordinate fork.
let filters = RepoStore::maintainer_filters(&addr, &[maintainer]);
assert_eq!(filters.len(), 4);
let expected = HashSet::from([owner, maintainer]);
// Gossip only resolves pubkeys from `authors` and the lowercase `#p` tag.
let named = |filter: &Filter| -> HashSet<PublicKey> {
let authors = filter.authors.iter().flatten().copied();
let p_tag = filter
@@ -1670,7 +1561,6 @@ mod tests {
authors.chain(p_tag).collect()
};
// Announcement and state events, scoped to the repository identifier.
let announcement = &filters[0];
assert_eq!(named(announcement), expected);
assert!(
@@ -1679,7 +1569,6 @@ mod tests {
.contains_key(&SingleLetterTag::LOWERCASE_D)
);
// Activity filters, scoped to the repository coordinate.
for filter in &filters[1..3] {
assert_eq!(named(filter), expected);
assert!(
@@ -1689,7 +1578,6 @@ mod tests {
);
}
// Deletions, named by author only.
let deletions = &filters[3];
assert_eq!(
deletions