diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs index d6d44b4..68d3d6f 100644 --- a/crates/assets/src/lib.rs +++ b/crates/assets/src/lib.rs @@ -151,14 +151,14 @@ mod tests { assert_eq!(theme.border, parse("#27272A")); // neutral-800 assert_eq!(theme.green, parse("#22C55E")); // green-500 } else { - // Light theme chrome is neutral; lime is a brand accent only. + // Light theme chrome is neutral, lime is a brand accent only. assert_eq!(theme.background, parse("#FFFFFF")); assert_eq!(theme.foreground, parse("#18181B")); assert_eq!(theme.border, parse("#E4E4E7")); assert_eq!(theme.green, parse("#16A34A")); } - // Active tab: a paler lime on light, a dim moss on dark — each - // paired with readable, contrasting text. + // Active tab, a paler lime on light and a dim moss on dark. + // Each is paired with readable contrasting text. if config.mode.is_dark() { assert_eq!(theme.tab_active, parse("#19200A")); // dim lime assert_eq!(theme.tab_active_foreground, parse("#C6FF4D")); // nostr-lime diff --git a/crates/dock/src/dock_area.rs b/crates/dock/src/dock_area.rs index 2c604cb..6928d32 100644 --- a/crates/dock/src/dock_area.rs +++ b/crates/dock/src/dock_area.rs @@ -1,7 +1,3 @@ -//! The dock-area appearance: the outer frame, the split frames, and one -//! dock's chrome. Ported from the vendored dock's `DockArea`/`Dock` render -//! onto `gpui_base::dock::DockAreaRenderer`. - use std::cell::Cell; use std::ops::Deref as _; use std::rc::Rc; @@ -26,8 +22,7 @@ use crate::tab_panel::SignedTabGroupSkin; use crate::tiles::SignedTilesSkin; use crate::{TAB_BAR_HEIGHT, panel_handle}; -/// What every part of the skin reads, and the dock area it belongs to. -/// Shared by reference with the per-container renderers. +/// State the skin shares with its per-container renderers. pub(crate) struct SkinShared { area: WeakEntity, toggle_button_visible: Cell, @@ -53,16 +48,14 @@ impl SkinShared { &self.resizing_dock } - /// Redraw the area after a setting changed. The skin is not an entity, so - /// nothing else would notice. + /// Redraw the area after a setting changed. The skin is not an entity, so nothing else would. pub(crate) fn notify(&self, cx: &mut App) { _ = self.area.update(cx, |_, cx| cx.notify()); } } /// The Signed appearance for a [`DockArea`]. -/// -/// Install it at construction, where the area's own weak handle is available: +/// Install it in the constructor, the only place the area's weak handle is available. /// /// ```ignore /// let dock = cx.new(|cx| { @@ -90,8 +83,7 @@ impl SignedDockSkin { &self.shared } - /// Whether tab bars offer the affordance that collapses a neighbouring - /// dock. + /// Whether tab bars offer the affordance that collapses a neighbouring dock. pub fn is_toggle_button_visible(&self) -> bool { self.shared.is_toggle_button_visible() } @@ -112,8 +104,8 @@ impl SignedDockSkin { } } -/// The payload a dock's resize handle drags. It draws nothing: the handle -/// itself is the affordance. +/// Payload a dock's resize handle drags. +/// It draws nothing, the handle element is the visible affordance. #[derive(Clone)] struct ResizePanel; @@ -144,9 +136,7 @@ impl DockAreaRenderer for SignedDockSkin { } fn split_frame(&self, node: NodeId, _: Axis, _: &mut Window, cx: &mut App) -> Stateful
{ - // `size_full` is what the old `StackPanel::render` carried; `flex_1` - // is belt and braces so the frame never collapses to zero height in - // an unsizing parent. + // `size_full` and `flex_1` stop the frame collapsing in an unsizing parent. div() .id(("dock-split-frame", node.as_u64())) .size_full() @@ -166,8 +156,8 @@ impl DockAreaRenderer for SignedDockSkin { let placement = dock.placement(); let open = dock.is_open(); - // A closed left or right dock takes no space at all; a closed bottom - // dock keeps a strip so its tab bar stays clickable. + // A closed left or right dock takes no space. + // A closed bottom dock keeps a strip so its tab bar stays clickable. if !open && !placement.is_bottom() { return div().into_any_element(); } @@ -183,8 +173,7 @@ impl DockAreaRenderer for SignedDockSkin { // Base never builds a dock for the centre. DockPlacement::Center => this, }) - // The closed bottom dock's strip is the tab bar itself, which is - // a full tab bar tall. + // The closed bottom dock's strip is the tab bar itself, a full tab bar tall. .when(!open && placement.is_bottom(), |this| { this.h(TAB_BAR_HEIGHT) }) @@ -197,9 +186,8 @@ impl DockAreaRenderer for SignedDockSkin { .into_any_element() } - /// The "unknown panel" message the old `InvalidPanel` drew. It answers - /// `dump` with the state it was handed, so a layout written by a build - /// that knows the panel survives a load and save here. + /// Placeholder for a panel this build cannot construct. + /// It dumps the state it was handed, so the layout survives a load and save. fn build_placeholder( &self, state: &PanelState, @@ -241,10 +229,8 @@ impl SignedDockSkin { } } -/// Turns the window's mouse stream into dock resizing. A resize is driven -/// by pointer moves anywhere in the window, so this paints nothing and -/// exists for its `paint` hook — the only place a window-level mouse -/// listener can be registered. +/// Turns the window's mouse stream into dock resizing. +/// It draws nothing, the `paint` hook is the only window listener registration point. struct DockResizeTracker { dock: DockContext, shared: Rc, @@ -310,10 +296,8 @@ impl Element for DockResizeTracker { if !phase.bubble() || shared.resizing_dock().get() != Some(placement) { return; } - // Dragging a closed dock's handle reopens it. The live - // state is read rather than the render-time snapshot in - // `dock`, which would still say closed for the rest of the - // frame and toggle it shut again on the next move. + // Dragging a closed dock's handle reopens it. + // Read the live state, the snapshot in `dock` would toggle it shut again. let open = shared .area() .upgrade() @@ -332,8 +316,8 @@ impl Element for DockResizeTracker { return; } shared.resizing_dock().set(None); - // The size lives on the dock, not in the layout tree, so - // nothing else tells a subscriber to persist it. + // The size lives on the dock, not the layout tree. + // Nothing else tells a subscriber to persist it. _ = shared .area() .update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged)); diff --git a/crates/dock/src/invalid_panel.rs b/crates/dock/src/invalid_panel.rs index 85675a5..e927315 100644 --- a/crates/dock/src/invalid_panel.rs +++ b/crates/dock/src/invalid_panel.rs @@ -7,10 +7,8 @@ use gpui_component::ActiveTheme as _; use crate::Panel; -/// Stands in for a panel this build cannot construct. It reports the -/// original [`PanelState`] from [`dump`](gpui_base::dock::Panel::dump), so -/// a layout written by a build that knows the panel survives a load and -/// save here. +/// Stands in for a panel this build cannot construct. +/// It returns the state it was handed, so the layout survives a load and save. pub(crate) struct InvalidPanel { name: SharedString, focus_handle: FocusHandle, diff --git a/crates/dock/src/lib.rs b/crates/dock/src/lib.rs index 2a566b2..70458ad 100644 --- a/crates/dock/src/lib.rs +++ b/crates/dock/src/lib.rs @@ -1,14 +1,3 @@ -//! The Signed dock skin. -//! -//! The dock engine lives upstream (`gpui_base::dock` owns the layout tree, -//! drags, zoom and persistence); this crate is the appearance the app used -//! to vendor from gpui-component — a 44px tab bar that doubles as the -//! window title bar, with pill tabs, window controls, title-bar dragging -//! and previous/next tab buttons. -//! -//! Everything `gpui_component::dock` exports is re-exported here, so the app -//! keeps importing the dock from a single place. - use gpui::{Pixels, px}; mod dock_area; @@ -28,8 +17,7 @@ pub use gpui_component::dock::{ /// The fixed height of the tab bar, which doubles as the window title bar. pub const TAB_BAR_HEIGHT: Pixels = px(44.); -/// Minimal i18n shim replacing gpui-component's `rust_i18n::t!()`, keeping the -/// same `Dock.*` keys resolved to English so the crate has no i18n dependency. +/// i18n shim resolving `Dock.*` keys to English, so the crate has no i18n dependency. pub(crate) fn t(key: &'static str) -> &'static str { match key { "Dock.Unnamed" => "Unnamed", diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index c6ed188..000f9f6 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -1,13 +1,3 @@ -//! The Signed appearance for a tab group. -//! -//! `gpui_base::dock::TabGroup` owns the behavior — membership, the displayed -//! tab, drag hit-testing, the zoom flag — and draws none of it. Everything -//! visible is here, ported from the vendored dock: the pill tab bar that -//! doubles as the window title bar (with window controls, title-bar -//! dragging, and previous/next tab buttons), the toolbar, the ellipsis menu, -//! the dock collapse affordance, the drop placeholder, and the styled drag -//! preview. - use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; @@ -38,12 +28,10 @@ use crate::{ ClosePanel, PanelControl, PanelHandle, TAB_BAR_HEIGHT, ToggleZoom, t, window_controls, }; -/// The size the styled drag preview occupies, reported to base so a drop -/// placeholder knows where to fly in from. +/// The drag preview's size, reported to base for the drop placeholder. const DRAG_PREVIEW_SIZE: gpui::Size = size(px(96.), px(30.)); -/// A panel's title, or its registered name when it reached base without this -/// crate's handle and so carries no presentation. See [`PanelHandle::of`]. +/// A panel's title, or its registered name when the panel has no handle. pub(crate) fn panel_title( panel: &Arc, window: &mut Window, @@ -56,9 +44,7 @@ pub(crate) fn panel_title( } /// The preview that follows the cursor while a panel is dragged. -/// -/// `gpui_base::dock::DragPanel` is the payload and draws nothing; this is the -/// appearance half, reintroduced here. +/// Base's `DragPanel` is the payload and draws nothing, this is the appearance half. struct DragPanelPreview { panel: Arc, } @@ -83,10 +69,8 @@ impl Render for DragPanelPreview { } } -/// Where the zoom affordance goes for the group's displayed panel, or `None` -/// when there is none to offer. Both [`Panel::zoom_control`] (where) and -/// [`gpui_base::dock::Panel::zoomable`] (whether) must pass; base refuses a -/// zoom that fails the latter. +/// The zoom affordance for the group's displayed panel, if it offers one. +/// The panel must offer a control and be zoomable, base refuses a zoom otherwise. fn zoom_control(group: &TabGroupContext, cx: &App) -> Option { let panel = group.active_panel()?; panel @@ -95,8 +79,8 @@ fn zoom_control(group: &TabGroupContext, cx: &App) -> Option { .flatten() } -/// The left-most, top-most tab group in a container — where a left dock's -/// collapse affordance goes. Mirrors the old `StackPanel::left_top_tab_panel`. +/// The left-most, top-most tab group in a container. +/// A left dock's collapse button lives in this group. fn left_top_group(node: &PaneNode) -> Option { match node.kind() { PaneRef::Tabs { .. } => Some(node.id()), @@ -105,9 +89,8 @@ fn left_top_group(node: &PaneNode) -> Option { } } -/// The right-most, top-most tab group. A vertical split stacks its children, -/// so its *first* child is the top one; a horizontal split's last child is -/// the right-most. Mirrors the old `StackPanel::right_top_tab_panel`. +/// The right-most, top-most tab group. +/// A vertical split picks its first child, a horizontal split picks its last. fn right_top_group(node: &PaneNode) -> Option { match node.kind() { PaneRef::Tabs { .. } => Some(node.id()), @@ -120,23 +103,17 @@ fn right_top_group(node: &PaneNode) -> Option { } } -/// One tab group's appearance. Built once per container, so the tab bar's -/// scroll position and measured title-bar geometry belong to the group. +/// One tab group's appearance, built once per container so its geometry is its own. pub(crate) struct SignedTabGroupSkin { shared: Rc, scroll_handle: ScrollHandle, - /// The displayed tab the last frame drew, so a change scrolls the new tab - /// into view. + /// The tab shown last frame, so a change scrolls the new one into view. last_active_ix: Cell>, - /// Bounds of the title bar row (the wrapper around the tab bar), in - /// window coordinates. Measured via `on_prepaint` to position the - /// title-bar drag overlay. + /// Bounds of the title bar row, measured to place the title-bar drag overlay. title_bar_bounds: Rc>>>, - /// Bounds of the tab bar's trailing empty space (right after the last - /// tab), which marks where the draggable region starts. + /// Bounds of the empty strip after the last tab, where the drag region starts. title_bar_strip_bounds: Rc>>>, - /// Bounds of the tab bar's suffix (toolbar) area, which marks where the - /// draggable region ends. + /// Bounds of the suffix area, where the drag region ends. title_bar_suffix_bounds: Rc>>>, } @@ -152,9 +129,8 @@ impl SignedTabGroupSkin { } } - /// A group that is the left dock's whole content with a single panel - /// draws no chrome at all — the vendored dock rendered such a panel bare, - /// and the sidebar is one. + /// A group that is the left dock's only group, with one panel, draws no chrome. + /// The vendored dock rendered such a panel bare and the sidebar is one. fn is_plain_sidebar_group(&self, group: &TabGroupContext, cx: &mut App) -> bool { let Some(area) = self.shared.area().upgrade() else { return false; @@ -169,11 +145,8 @@ impl SignedTabGroupSkin { left == group.node() && group.panels().len() == 1 } - /// The bottom or right dock whose root tab group this group is, if any. - /// - /// Base bars a dock's only group from being dragged or closed, so the - /// dock cannot be emptied — but a bottom/right panel is supposed to be - /// closable, so the skin routes around the bar for these groups. + /// The bottom or right dock whose root tab group is this one, if any. + /// Base keeps a dock's last group, so the skin removes these docks as a whole. fn is_dock_root_group(&self, group: &TabGroupContext, cx: &App) -> Option { let area = self.shared.area().upgrade()?; let area = area.read(cx); @@ -185,10 +158,8 @@ impl SignedTabGroupSkin { }) } - /// The drag payload for the tab at `ix`, or `None` when this group must - /// not be rearranged. A locked group is never draggable; a group that is - /// a bottom/right dock's only content still is, because the center is - /// always there to land in. + /// The tab's drag payload, or `None` when the group must not be rearranged. + /// A locked group never is, a bottom or right dock root always is. fn tab_drag(&self, group: &TabGroupContext, ix: usize, cx: &App) -> Option { if group.is_locked() { return None; @@ -199,8 +170,8 @@ impl SignedTabGroupSkin { group.drag_panel(ix, cx) } - /// Whether a dock's collapse affordance belongs in *this* group's tab - /// bar, and which way it points. `None` means this group draws none. + /// A dock's collapse button for this group's bar, or `None` when it does not belong. + /// The icon direction depends on whether the dock is open. fn dock_toggle_button( &self, placement: DockPlacement, @@ -213,8 +184,7 @@ impl SignedTabGroupSkin { let area = self.shared.area().upgrade()?; let area = area.read(cx); - // A dock that does not exist is not collapsible, so this covers the - // old `left_dock.is_some()` test too. + // A missing dock is not collapsible, this also covers the old `left_dock.is_some()` test. if !area.is_dock_collapsible(placement) { return None; } @@ -263,8 +233,8 @@ impl SignedTabGroupSkin { ) } - /// The previous/next tab buttons shown in the tab bar's leading prefix. - /// Always rendered, disabled at the ends of the strip (or collapsed). + /// The previous and next tab buttons in the tab bar's leading prefix. + /// Always rendered, disabled at the strip ends or when collapsed. fn render_prev_next_tab_buttons( &self, group: &TabGroupContext, @@ -306,8 +276,7 @@ impl SignedTabGroupSkin { ) } - /// The trailing controls: the panel's own buttons, the zoom affordance, - /// and the ellipsis menu. + /// The trailing controls, the panel's own buttons, zoom and the ellipsis menu. fn render_toolbar( &self, group: &TabGroupContext, @@ -323,9 +292,8 @@ impl SignedTabGroupSkin { let control = zoom_control(group, cx); let toolbar_zoom = control.is_some_and(|control| control.toolbar_visible()); let menu_zoom = control.is_some_and(|control| control.menu_visible()); - // A bottom/right dock's only panel cannot be closed through the - // group (base keeps a dock's last group), but the skin handles that - // close by removing the whole dock, so the item is offered. + // A bottom or right dock's only panel cannot close through the group. + // The close item is offered, the skin removes the whole dock instead. let closable = group.is_closable() || (self.is_dock_root_group(group, cx).is_some() && group.active_panel().is_some_and(|panel| panel.closable(cx))); @@ -397,9 +365,9 @@ impl SignedTabGroupSkin { ) } - /// One tab of the pill strip. While collapsed, tabs lose the active - /// style and all interactions, and the strip becomes the way a closed - /// bottom dock is opened again. + /// One tab of the pill strip. + /// While collapsed, tabs lose the active style and all interactions. + /// The strip is also how a closed bottom dock is opened again. #[allow(clippy::too_many_arguments)] fn render_tab( &self, @@ -433,8 +401,7 @@ impl SignedTabGroupSkin { Some(tab_name) => this.child(tab_name), None => this.child(panel_title(&panel, window, cx)), }) - // Pill presentation: the selected tab is the filled pill, the - // rest are transparent until hovered. + // Pill style, the selected tab is the filled pill, others show only on hover. .styles(|styles| { styles.selected(|style| { style @@ -457,8 +424,7 @@ impl SignedTabGroupSkin { move |_, window, cx| { group.select_tab(ix, window, cx); - // Clicking the strip of a collapsed bottom dock is how it - // is opened again. + // Clicking the strip of a collapsed bottom dock reopens it. if is_bottom_dock && collapsed { _ = area.update(cx, |area, cx| { area.toggle_dock(DockPlacement::Bottom, window, cx); @@ -509,9 +475,8 @@ impl SignedTabGroupSkin { }) } - /// The strip after the last tab: a drop target for panels and host-owned - /// drag items. Its left edge (right after the last tab) marks the start - /// of the title-bar drag overlay. + /// The strip after the last tab, a drop target for panels and other drag items. + /// Its left edge marks where the title-bar drag overlay starts. fn render_empty_space( &self, group: &TabGroupContext, @@ -541,9 +506,8 @@ impl SignedTabGroupSkin { let group = group.clone(); let node = group.node(); move |drag: &DragPanel, window, cx| { - // A panel dropped past its own last tab lands in the - // final slot; one from elsewhere is appended in the - // background. + // A panel dropped past its own last tab lands in the final slot. + // A panel from elsewhere is appended in the background. let ix = (drag.source() == node).then(|| tabs_count - 1); group.drop_panel(drag.clone(), ix, false, window, cx); } @@ -564,37 +528,30 @@ impl SignedTabGroupSkin { impl TabGroupRenderer for SignedTabGroupSkin { fn frame(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> Stateful
{ let control = zoom_control(group, cx); - // An emptied group — its last panel was dragged away — draws nothing, - // so an emptied dock does not leave a bare tab bar behind. + // An emptied group draws nothing, so no bare tab bar is left behind. if group.panels().is_empty() { return div().id("tab-panel"); } - // Closing the only panel of a bottom/right dock would leave an - // empty dock, which base refuses; the skin removes the dock instead. + // Base refuses an empty dock, so closing its only panel removes the dock. let dock_to_remove = (group.panels().len() <= 1) .then(|| self.is_dock_root_group(group, cx)) .flatten(); let shared = self.shared.clone(); - // `v_flex`, not `div`: gpui's default display is Block, and in block - // layout a child's `flex_grow` is ignored — the content region below - // the tab bar would resolve to zero height. + // `v_flex`, a plain `div` ignores `flex_grow` and the content would collapse. v_flex() .id("tab-panel") .size_full() .overflow_hidden() .bg(cx.theme().tokens.background) - // A collapsed group is a strip of tabs with no content, and the - // actions act on content. + // A collapsed group has no content, so these actions are not registered. .when(!group.is_collapsed(), |this| { this.on_action({ let group = group.clone(); move |_: &ToggleZoom, window, cx| { - // The affordance decides the control, so a panel - // offering none is not zoomed *in* by the keybinding - // either. Zooming out is never refused: a panel that - // stopped offering the control while zoomed would - // otherwise strand the user with no way back. + // A panel with no zoom control is not zoomed in by the keybinding. + // Zooming out is never refused. + // Otherwise a zoomed panel that lost its control would strand the user. if !group.is_zoomed() && control.is_none() { return; } @@ -628,8 +585,7 @@ impl TabGroupRenderer for SignedTabGroupSkin { fn content_frame(&self, group: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful
{ v_flex() .id("active-panel") - // A collapsed group draws its tab strip and nothing else, so the - // content region must not claim any space. + // A collapsed group draws its tab strip only, so the content claims no space. .when(!group.is_collapsed(), |this| this.flex_1()) } @@ -639,14 +595,12 @@ impl TabGroupRenderer for SignedTabGroupSkin { window: &mut Window, cx: &mut App, ) -> AnyElement { - // An emptied group draws no tab bar; the app prunes the emptied - // bottom/right dock a moment later. + // An emptied group draws no tab bar, the app prunes the emptied dock later. if group.panels().is_empty() { return Empty.into_any_element(); } - // The sidebar group draws no chrome at all, like the vendored dock's - // bare `DockItem::Panel`. + // The sidebar group draws no chrome, like the vendored `DockItem::Panel`. if self.is_plain_sidebar_group(group, cx) { return Empty.into_any_element(); } @@ -660,10 +614,9 @@ impl TabGroupRenderer for SignedTabGroupSkin { let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx); let is_bottom_dock = bottom_dock_button.is_some(); - // macOS: the traffic lights overlay the window's top-left corner. - // Only the group whose tab bar actually sits under them reserves the - // space — the center's left-most, top-most group when the left dock - // is closed or absent. + // On macOS the traffic lights overlay the window's top-left corner. + // Only the tab bar that sits under them reserves the space. + // That is the center's top-left group when the left dock is closed or absent. let needs_traffic_light_padding = cfg!(target_os = "macos") && self.shared.area().upgrade().is_some_and(|area| { let area = area.read(cx); @@ -674,8 +627,8 @@ impl TabGroupRenderer for SignedTabGroupSkin { == Some(group.node()) }); - // Bring a newly displayed tab into view. The group owns selection - // now, so the skin notices the change rather than being told about it. + // Bring a newly displayed tab into view. + // The group owns selection, so the skin watches for the change itself. let displayed = group.active_panel().map(|panel| panel.panel_id(cx)); let visible: Vec = group .panels() @@ -690,10 +643,8 @@ impl TabGroupRenderer for SignedTabGroupSkin { self.scroll_handle.scroll_to_item(visible_ix); } - // The tab strip lays out at content width, so the area after the - // last tab has no element. Cover that dead zone (last tab's right - // edge to suffix's left edge) with a measured overlay so the whole - // non-interactive area can drag the window. + // The tab strip ends at the last tab, the bar has no element after it. + // Cover that dead zone with an overlay so it can drag the window. let drag_overlay = match ( self.title_bar_bounds.get(), self.title_bar_strip_bounds.get(), @@ -728,8 +679,7 @@ impl TabGroupRenderer for SignedTabGroupSkin { if !panel.visible(cx) { return None; } - // A collapsed group shows no tab as active: the strip is a - // way back in, not a selection. + // Collapsed tabs never show as active, the strip only reopens the dock. if collapsed { active = false; } @@ -768,7 +718,7 @@ impl TabGroupRenderer for SignedTabGroupSkin { h_flex() .items_center() .top_0() - // Right -1 for avoid border overlap with the first tab + // -1 px so the border does not overlap the first tab. .right(-px(1.)) .h_full() .gap_2() @@ -852,9 +802,7 @@ impl TabGroupRenderer for SignedTabGroupSkin { cx: &mut App, ) -> Option { let (from, to) = (indicator.from(), indicator.to()); - // The placeholder animates from wherever it was to where the drop - // would land, so its own element is positioned at the destination and - // the animation only has to walk the difference back to zero. + // The element sits at the drop target, the animation walks back from the source. let offset = from.origin() - to.origin(); Some( diff --git a/crates/dock/src/tiles.rs b/crates/dock/src/tiles.rs index 1ac8d74..f8c4b21 100644 --- a/crates/dock/src/tiles.rs +++ b/crates/dock/src/tiles.rs @@ -1,11 +1,3 @@ -//! The Signed appearance for a tiles canvas. -//! -//! `gpui_base::dock::TilesState` owns the geometry — snapping, the resize -//! arithmetic, the undo stack, the zoom flag — and draws none of it. The tile -//! frame, its title bar and its resize affordances are here, ported from -//! gpui-component's `TilesSkin` (the vendored dock had no tiles canvas, so -//! there is no local look to preserve). - use std::rc::Rc; use gpui::prelude::FluentBuilder as _; @@ -52,9 +44,7 @@ impl Render for DragResizing { } /// One tiles canvas's appearance. -/// -/// Built per canvas — `DockAreaRenderer::tiles_renderer` is called once per -/// container — so the scroll position belongs to the canvas it scrolls. +/// Built once per container, so its scroll position belongs to the canvas it scrolls. pub(crate) struct SignedTilesSkin { shared: Rc, scroll_handle: ScrollHandle, @@ -101,11 +91,8 @@ impl SignedTilesSkin { }) } - /// The trailing controls of a tile's title bar: zoom, close and the - /// ellipsis menu. They use click handlers rather than the - /// [`ToggleZoom`](crate::ToggleZoom)/[`ClosePanel`](crate::ClosePanel) - /// actions, which are dispatched to a focused tab group — a tile is - /// not one. + /// The trailing controls of a tile's title bar, zoom, close and the ellipsis menu. + /// They use click handlers, the zoom and close actions target a focused tab group. fn render_tile_controls( &self, tile: &TileContext, @@ -213,21 +200,17 @@ impl TilesRenderer for SignedTilesSkin { .border_1() .border_color(cx.theme().border) .rounded(cx.theme().tile_radius) - // Room for the title bar, which is positioned over the padding so - // the panel below it is never covered. Base draws the panel view - // as a plain child, so this is the only way to keep the two from - // overlapping. + // Room for the title bar, which overlays the top padding. + // Base draws the panel as a plain child, this keeps them apart. .pt(DRAG_BAR_HEIGHT) - // Base installs the stored bounds on an ordinary tile and nothing - // at all on a zoomed one — how a zoomed tile fills the dock is - // this skin's decision. + // Base stores no bounds on a zoomed tile, the skin decides how it fills the dock. .when(tile.is_zoomed(), |this| this.size_full()) .on_mouse_down(MouseButton::Left, { let tile = tile.clone(); move |_, window, cx| tile.bring_to_front(window, cx) }) - // A gesture can end with the pointer anywhere, so both halves are - // needed; each is a no-op unless this tile is the one moving. + // A gesture can end anywhere, so both mouse-up hooks run. + // Each is a no-op unless this tile is the one that moved. .on_mouse_up(MouseButton::Left, { let tile = tile.clone(); move |_, window, cx| { @@ -274,8 +257,7 @@ impl TilesRenderer for SignedTilesSkin { ) .children(handle.and_then(|handle| handle.title_suffix(window, cx))) .child(self.render_tile_controls(tile, window, cx)) - // A zoomed tile is not at its stored bounds, so there is nothing - // for a move to mean; base refuses the gesture too. + // A zoomed tile is not at its stored bounds, so moving it would mean nothing. .when(!tile.is_zoomed(), |this| { this.cursor_grab() .on_mouse_down(MouseButton::Left, { @@ -309,10 +291,8 @@ impl TilesRenderer for SignedTilesSkin { ) -> AnyElement { let bounds = tile.bounds(); - // A passive full-tile box so each handle is positioned against the - // tile rather than against whatever the flow put it next to. It - // registers no interaction of its own, so it does not shadow the panel - // underneath. + // A passive full-tile box, so handles sit against the tile, not its flow neighbours. + // It registers no interaction, so it does not shadow the panel underneath. div() .absolute() .top_0() @@ -376,9 +356,7 @@ impl TilesRenderer for SignedTilesSkin { .into_any_element() } - /// The panel of a tile gets `size_full` here; base draws the panel as a - /// plain child, so without it a panel that does not size itself has no - /// size. + /// Gives the tile's panel `size_full`, base draws it as a plain child otherwise. fn panel_frame(&self, tile: &TileContext, _: &mut Window, _: &mut App) -> Stateful
{ h_flex() .id(("tile-panel", tile.panel_id().as_u64())) @@ -386,9 +364,8 @@ impl TilesRenderer for SignedTilesSkin { .size_full() } - /// The canvas scrollbar. It must be an overlay: the frame is the scroll - /// container and base appends the tiles after it, so a scrollbar placed - /// inside would paint and hit-test underneath every tile. + /// The canvas scrollbar, as an overlay. + /// Placed inside the frame it would end up underneath every tile. fn render_overlay( &self, content: Size, diff --git a/crates/dock/src/window_controls.rs b/crates/dock/src/window_controls.rs index 4a695be..1f9e833 100644 --- a/crates/dock/src/window_controls.rs +++ b/crates/dock/src/window_controls.rs @@ -147,8 +147,7 @@ pub(crate) fn window_controls(window: &mut Window, cx: &mut App) -> impl IntoEle .items_center() .flex_shrink_0() .h_full() - // Like native windows apps, the controls span the title bar but never - // grow past the tab bar height. + // The controls span the title bar but never grow past the tab bar height. .when(cfg!(target_os = "windows"), |this| { this.max_h(TAB_BAR_HEIGHT) }) diff --git a/crates/dock/tests/render_smoke.rs b/crates/dock/tests/render_smoke.rs index 02eed70..790077a 100644 --- a/crates/dock/tests/render_smoke.rs +++ b/crates/dock/tests/render_smoke.rs @@ -1,8 +1,3 @@ -//! Render-path smoke tests: the skin reads the dock area while rendering, and -//! GPUI panics if an entity is read while it is leased (being updated). These -//! pin that the first frame — docks, groups, tab bars — renders without -//! tripping the lease check. - use dock::{BasePanel, Panel, SignedDockSkin, panel_handle}; use gpui::{ App, AppContext, Context, Empty, EventEmitter, FocusHandle, Focusable, IntoElement, Render, @@ -84,12 +79,10 @@ fn the_first_frame_renders_the_area_and_its_docks(cx: &mut TestAppContext) { }); }); - // The first frame walks every render hook — the dock frame, each group's - // tab bar, the toolbar — all of which read the dock area. + // The first frame walks every render hook, all of which read the dock area. cx.update(|window, cx| window.draw(cx).clear(cx)); - // Emptying a dock leaves an empty group behind; its render must also be - // safe (and draw nothing). + // Emptying a dock leaves an empty group, its render must also be safe. cx.update(|window, cx| { area.update(cx, |area, cx| { area.remove_panel(bottom, window, cx); diff --git a/crates/paths/src/lib.rs b/crates/paths/src/lib.rs index bc3fe6d..96f89fb 100644 --- a/crates/paths/src/lib.rs +++ b/crates/paths/src/lib.rs @@ -1,18 +1,12 @@ -//! Paths to locations used by Signed. -//! -//! Follows the same pattern as Zed's `paths` crate: platform-correct base -//! directories, resolved once and cached, with an optional custom data dir -//! override for portable/dev installs. - use std::path::PathBuf; use std::sync::OnceLock; -/// The application name, used to derive platform-specific data, config and -/// cache directory paths. +/// The application name. +/// It derives the platform-specific data, config and cache directory paths. pub const APP_NAME: &str = "Signed"; -/// Lowercased form of [`APP_NAME`], for use in XDG-style paths on -/// Linux/FreeBSD and the macOS `~/.config` fallback. +/// Lowercased form of [`APP_NAME`]. +/// Used in XDG-style paths on Linux and FreeBSD, and the macOS `~/.config` fallback. pub const APP_NAME_LOWERCASE: &str = "signed"; /// A custom data directory override, set only by [`set_custom_data_dir`]. @@ -35,26 +29,24 @@ pub fn home_dir() -> PathBuf { dirs::home_dir().expect("failed to determine home directory") } -/// Returns the current user's Desktop folder, falling back to the home -/// directory (or an empty path) when it can't be determined. +/// Returns the current user's Desktop folder. +/// Falls back to the home directory or an empty path when it cannot be determined. pub fn desktop_dir() -> PathBuf { dirs::desktop_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default()) } -/// Returns the current user's Documents folder, falling back to the home -/// directory (or an empty path) when it can't be determined. +/// Returns the current user's Documents folder. +/// Falls back to the home directory or an empty path when it cannot be determined. pub fn documents_dir() -> PathBuf { dirs::document_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default()) } -/// Sets a custom directory for all user data, overriding the default data -/// directory. Must be called before any other path operation. The directory -/// is created if it doesn't exist and canonicalized to an absolute path. -/// +/// Sets a custom directory for all user data, overriding the default data directory. +/// Must be called before any other path operation. +/// The directory is created when missing and canonicalized to an absolute path. /// # Panics -/// -/// Panics if called after [`data_dir`] or [`config_dir`] was initialized, or -/// if the directory cannot be created/canonicalized. +/// Panics when called after [`data_dir`] or [`config_dir`] was initialized. +/// Panics when the directory cannot be created or canonicalized. pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() { panic!("set_custom_data_dir called after data_dir or config_dir was initialized"); @@ -153,13 +145,13 @@ pub fn logs_dir() -> &'static PathBuf { }) } -/// Returns the path to the nostr database directory (LMDB). +/// Returns the path to the nostr database directory, LMDB. pub fn nostr_dir() -> &'static PathBuf { static NOSTR_DIR: OnceLock = OnceLock::new(); NOSTR_DIR.get_or_init(|| data_dir().join("nostr")) } -/// Returns the path to the local git clone cache (grasp mirrors). +/// Returns the path to the local git clone cache, the grasp mirrors. pub fn repos_dir() -> &'static PathBuf { static REPOS_DIR: OnceLock = OnceLock::new(); REPOS_DIR.get_or_init(|| data_dir().join("repos")) diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 228e05d..36154d0 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,9 +1,3 @@ -//! Persisted application settings for Signed. -//! -//! The [`Settings`] model holds the user-configurable values that survive -//! restarts, and [`SettingsStore`] loads them from and saves them to a JSON -//! file on disk (see [`paths::settings_file`]). - mod settings; mod store; diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index edd38ee..122981b 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -/// The default grasp servers offered when the user hasn't published a -/// grasp list (kind `10317`) yet. +/// The default grasp servers. +/// Offered while the user has not published a grasp list, kind `10317`. pub const DEFAULT_GRASP_SERVERS: [&str; 3] = [ "wss://relay.ngit.dev", "wss://gitnostr.com", @@ -14,7 +14,7 @@ pub const DEFAULT_GRASP_SERVERS: [&str; 3] = [ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AppearanceMode { - /// Follow the system appearance (light/dark) at runtime. + /// Follow the system appearance, light or dark, at runtime. #[default] System, /// Always use the light theme. @@ -24,11 +24,9 @@ pub enum AppearanceMode { } /// Theme configuration. -/// -/// The fields mirror the gpui-component `Theme` surface the application -/// customizes at startup, so applying the settings is a plain field-for-field -/// copy. The theme names identify entries in the gpui-component theme -/// registry. +/// Fields mirror the gpui-component `Theme` surface customized at startup. +/// Applying the settings is then a field-for-field copy. +/// Theme names identify entries in the gpui-component theme registry. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct ThemeSettings { @@ -42,7 +40,7 @@ pub struct ThemeSettings { pub mono_font_size: f32, /// Corner radius for general elements in pixels. pub radius: f32, - /// Corner radius for large elements (dialogs, notifications) in pixels. + /// Corner radius for large elements, dialogs and notifications, in pixels. pub radius_lg: f32, /// Whether focused controls draw a ring outside their border. pub focus_ring: bool, @@ -69,8 +67,7 @@ impl Default for ThemeSettings { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct GraspServersSettings { - /// The servers offered when the user hasn't published a grasp list - /// (kind `10317`) yet. + /// Servers offered while the user has not published a grasp list, kind `10317`. pub default_servers: Vec, } @@ -90,7 +87,6 @@ impl Default for GraspServersSettings { #[serde(default)] pub struct LocalReposSettings { /// The directories scanned for local git repositories. - /// /// Defaults to the user's Desktop and Documents folders. pub scan_paths: Vec, } @@ -107,16 +103,15 @@ impl Default for LocalReposSettings { } } -/// A remembered association between a local checkout folder and an -/// announced repository. Recorded when the user clones a repository from -/// the app or picks a folder in the New PR panel, so the panel can prefill -/// the folder later without asking again. +/// A remembered association between a local checkout folder and an announced repository. +/// Recorded when the user clones a repository or picks a folder in the New PR panel. +/// The panel can then prefill the folder later without asking again. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct CheckoutRecord { /// Local folder of the checkout. pub path: PathBuf, - /// Repository address (`30617::`) as a string. + /// Repository address as a string, `30617::`. pub addr: String, /// Unix seconds of the last use, for freshest-first ordering. pub last_used: u64, @@ -132,12 +127,12 @@ impl Default for CheckoutRecord { } } -/// Remembered local checkouts (see [`CheckoutRecord`]). +/// Remembered local checkouts, see [`CheckoutRecord`]. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct CheckoutsSettings { - /// The remembered records; the latest use of a path+repo pair replaces - /// the older record. + /// The remembered records. + /// The latest use of a path and repo pair replaces the older record. pub records: Vec, } @@ -145,8 +140,7 @@ pub struct CheckoutsSettings { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct CreateRepositorySettings { - /// The folder the create-repository dialog defaults to; the user's - /// Desktop when unset. + /// The folder the create-repository dialog defaults to, the user's Desktop when unset. pub default_folder: Option, } diff --git a/crates/settings/src/store.rs b/crates/settings/src/store.rs index 6245bfe..37898ae 100644 --- a/crates/settings/src/store.rs +++ b/crates/settings/src/store.rs @@ -9,7 +9,7 @@ struct GlobalSettingsStore(Entity); impl Global for GlobalSettingsStore {} -/// The application settings, loaded from disk at startup and persisted whenever they change. +/// The application settings, loaded from disk at startup and saved whenever they change. /// Installed as a global by the app so any part of the UI can read and edit them. pub struct SettingsStore { path: PathBuf, @@ -17,7 +17,7 @@ pub struct SettingsStore { } impl SettingsStore { - /// Retrieve the global settings store (created at startup by the app). + /// Retrieve the global settings store, created at startup by the app. pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } @@ -27,9 +27,10 @@ impl SettingsStore { cx.set_global(GlobalSettingsStore(entity)); } - /// Load the settings from `path`, falling back to the defaults when the - /// file is missing or unreadable. Missing keys merge with the defaults, - /// so older settings files keep working as new settings are added. + /// Load the settings from `path`. + /// Falls back to defaults when the file is missing or unreadable. + /// Missing keys merge with the defaults. + /// Older settings files keep working as new settings are added. pub fn new(path: impl AsRef, _cx: &mut Context) -> Self { Self { path: path.as_ref().to_path_buf(), @@ -78,8 +79,8 @@ impl SettingsStore { } } - /// Write the settings to disk, replacing the file atomically - /// so a crash mid-write cannot corrupt the settings. + /// Write the settings to disk, replacing the file atomically. + /// A crash mid-write cannot corrupt the settings. fn save(&self) -> Result<()> { if let Some(parent) = self.path.parent() { std::fs::create_dir_all(parent)?; diff --git a/crates/signed_core/src/addr.rs b/crates/signed_core/src/addr.rs index 8617de8..43bbf2d 100644 --- a/crates/signed_core/src/addr.rs +++ b/crates/signed_core/src/addr.rs @@ -1,9 +1,8 @@ use nostr::prelude::*; -/// Address of a NIP-34 repository announcement: `30617::`. -/// -/// The Rust Nostr SDK's [`Coordinate`] already provides parsing, formatting and hashing for this. -/// The alias keeps the repository-specific vocabulary while reusing the SDK type. +/// Address of a NIP-34 repository announcement, `30617::`. +/// The Rust Nostr SDK's [`Coordinate`] parses, formats and hashes this. +/// The alias reuses the SDK type while keeping repository-specific vocabulary. pub type RepoAddr = Coordinate; /// Build the address of a NIP-34 repository announcement. diff --git a/crates/signed_core/src/annotations.rs b/crates/signed_core/src/annotations.rs index 1541b22..456c343 100644 --- a/crates/signed_core/src/annotations.rs +++ b/crates/signed_core/src/annotations.rs @@ -1,13 +1,13 @@ use nostr::prelude::*; -/// ngit / GitWorkshop cover-note extension (kind 1624): a markdown note -/// attached to an issue, patch or PR by its author or a repository -/// maintainer. Not part of the NIP-34 draft; read support for interop. +/// ngit and GitWorkshop cover-note extension, kind 1624. +/// A markdown note attached to an issue, patch or PR by its author or a maintainer. +/// Not part of the NIP-34 draft, read support for interop. pub const COVER_NOTE_KIND: Kind = Kind::Custom(1624); -/// Whether a kind-1985 label event is a valid annotation of `root`: it -/// references the root via a lowercase `e` tag and was authored by the root -/// author or a maintainer. +/// Whether a kind-1985 label event is a valid annotation of `root`. +/// The event references the root with a lowercase `e` tag. +/// Its author must be the root author or a maintainer. fn label_targets_root(event: &Event, root: &Event, maintainers: &[PublicKey]) -> bool { if event.kind != Kind::Label { return false; @@ -22,8 +22,8 @@ fn label_targets_root(event: &Event, root: &Event, maintainers: &[PublicKey]) -> .any(|tag| tag.kind() == "e" && tag.content().is_some_and(|content| content == root_id)) } -/// Whether a kind-1985 label event declares the `#t` namespace and carries at -/// least one `["l", "", "#t"]` label. +/// Whether a kind-1985 label event declares the `#t` namespace. +/// It must also carry at least one `["l", "", "#t"]` label. fn has_hashtag_labels(event: &Event) -> bool { event.tags.iter().any(|tag| tag.as_slice() == ["L", "#t"]) && event.tags.iter().any(|tag| { @@ -32,10 +32,11 @@ fn has_hashtag_labels(event: &Event) -> bool { }) } -/// The effective hashtag labels of `root`: the `t` tags on the event itself -/// (self-reported by its author) plus all labels attached via authorized -/// NIP-32 kind-1985 events in the `#t` namespace. Labels are additive — all -/// valid label events contribute (no latest-wins semantics). +/// Effective hashtag labels of `root`. +/// The `t` tags on the event itself, self-reported by its author. +/// Authorized NIP-32 kind-1985 events in the `#t` namespace add more. +/// Labels are additive, so all valid label events contribute. +/// There is no latest-wins semantics. pub fn labels(root: &Event, label_events: &[Event], maintainers: &[PublicKey]) -> Vec { let mut labels: Vec = root .tags @@ -61,10 +62,11 @@ pub fn labels(root: &Event, label_events: &[Event], maintainers: &[PublicKey]) - labels } -/// The effective subject/title override of `root`, from authorized kind-1985 -/// label events in the `#subject` namespace. Only the latest event wins -/// (tiebreak: lexicographically larger event id, per NIP-01 replaceable -/// semantics). Returns `None` when no valid override exists. +/// Subject or title override of `root` from authorized kind-1985 label events. +/// Only label events in the `#subject` namespace count. +/// The latest event wins, per NIP-01 replaceable semantics. +/// The tiebreak is the lexicographically larger event id. +/// Returns `None` when no valid override exists. pub fn subject_override( root: &Event, label_events: &[Event], @@ -103,8 +105,8 @@ pub fn subject_override( }) } -/// The effective hashtag labels and subject override of `root` in one pass -/// (mirrors ngit's `get_labels_and_subject`). +/// Effective hashtag labels and subject override of `root` in one pass. +/// Mirrors ngit's `get_labels_and_subject`. pub fn labels_and_subject( root: &Event, label_events: &[Event], @@ -116,9 +118,10 @@ pub fn labels_and_subject( ) } -/// The effective cover note of `root`: the latest authorized kind-1624 event -/// (tiebreak: lexicographically larger event id, per NIP-01 replaceable -/// semantics). Returns `None` when no valid cover note exists. +/// Effective cover note of `root`. +/// The latest authorized kind-1624 event wins. +/// The tiebreak is the lexicographically larger event id, per NIP-01 replaceable semantics. +/// Returns `None` when no valid cover note exists. pub fn cover_note<'a>( root: &Event, cover_notes: &'a [Event], diff --git a/crates/signed_core/src/clone_url.rs b/crates/signed_core/src/clone_url.rs index ca5cc94..e212b28 100644 --- a/crates/signed_core/src/clone_url.rs +++ b/crates/signed_core/src/clone_url.rs @@ -2,10 +2,10 @@ use nostr::prelude::*; use crate::RepoAddr; -/// Target of a `nostr://` clone URL (NIP-34 "Nostr Clone URL format"). +/// Target of a `nostr://` clone URL, as defined by NIP-34. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CloneTarget { - /// `nostr://` — direct repository address. + /// `nostr://` encodes a direct repository address. Addr(RepoAddr), /// `nostr:///[relay-hint/]` UserRepo { diff --git a/crates/signed_core/src/comments.rs b/crates/signed_core/src/comments.rs index 1715cf6..d664ae8 100644 --- a/crates/signed_core/src/comments.rs +++ b/crates/signed_core/src/comments.rs @@ -2,8 +2,8 @@ use std::collections::{HashMap, HashSet}; use nostr::prelude::*; -/// A NIP-22 comment thread: a top-level comment on the root event and its -/// nested replies (oldest first at every level). +/// A NIP-22 comment thread, a top-level comment on the root event. +/// Nested replies are ordered oldest first at every level. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CommentThread { /// The thread's top-level comment. @@ -12,8 +12,8 @@ pub struct CommentThread { pub replies: Vec, } -/// The direct parent of a comment (NIP-22 lowercase `e` tag), or `None` for -/// comments without one. +/// The direct parent id of a comment, from its NIP-22 lowercase `e` tag. +/// `None` when no `e` tag is present. fn comment_parent(event: &Event) -> Option { event .tags @@ -23,14 +23,14 @@ fn comment_parent(event: &Event) -> Option { .and_then(|id| EventId::parse(id).ok()) } -/// Group the comments on a root event (issue / patch / PR) into NIP-22 -/// threads. A comment whose parent is the root itself starts a thread; other -/// comments nest under their parent comment. Threads and replies are ordered -/// oldest-first. Replies whose parent comment is missing (e.g. not fetched) -/// are placed as top-level threads so they are not dropped. +/// Group the comments on a root issue, patch or PR into NIP-22 threads. +/// A comment whose parent is the root starts a thread. +/// Other comments nest under their parent comment. +/// Threads and replies are ordered oldest first. +/// Replies with a missing parent are made top-level threads, so none are dropped. pub fn comment_threads(root: &Event, comments: &[Event]) -> Vec { - // Index comments by their parent id. Comments without a parent tag are - // treated as replying to the root event itself. + // Index comments by their parent id. + // Comments without a parent tag reply to the root event itself. let mut children: HashMap> = HashMap::new(); for comment in comments { let parent = comment_parent(comment).unwrap_or(root.id); @@ -65,8 +65,8 @@ pub fn comment_threads(root: &Event, comments: &[Event]) -> Vec { let mut threads = build(root.id, &children, &mut visited); - // Orphan replies: their parent comment is unknown, so they never appear - // in the tree rooted at the root event; surface them as top-level threads. + // Orphan replies have an unknown parent comment, so they never reach the root tree. + // Surface them as top-level threads so they are not dropped. let mut orphans: Vec<&Event> = comments .iter() .filter(|event| !visited.contains(&event.id)) @@ -148,7 +148,8 @@ mod tests { .expect("signed event"); let a = comment(&keys, Some(&root), "a", 100); - // `missing` is not in the comment set; its reply should still show up. + // `missing` is not in the comment set. + // Its reply should still show up. let missing = EventBuilder::new(Kind::Comment, "missing") .finalize(&keys) .expect("signed event"); diff --git a/crates/signed_core/src/deletions.rs b/crates/signed_core/src/deletions.rs index 131c1b0..bf1d5ed 100644 --- a/crates/signed_core/src/deletions.rs +++ b/crates/signed_core/src/deletions.rs @@ -2,11 +2,10 @@ use std::collections::HashSet; use nostr::prelude::*; -/// NIP-09 deletion requests and NIP-62 vanish requests, used to hide -/// deleted events before they reach the UI. -/// -/// Built from the kind-5 / kind-62 events stored in the local database; -/// pass any event through [`Deletions::is_deleted`] before displaying it. +/// NIP-09 deletion requests and NIP-62 vanish requests. +/// Built from the kind-5 and kind-62 events in the local database. +/// Deleted events are hidden before they reach the UI. +/// Pass any event through [`Deletions::is_deleted`] before showing it. pub struct Deletions { /// `(deleted event id, expected author)` from `e` tags of kind-5 events. ids: HashSet<(EventId, PublicKey)>, @@ -34,8 +33,8 @@ impl Deletions { .map(|c| (c, event.pubkey, event.created_at)), ); } else if event.kind == Kind::RequestToVanish { - // Client-side we can't verify which relay the request targeted, - // so any vanish request is honored for the author's events. + // Client-side we can't verify which relay the request targeted. + // Any vanish request is then honored for the author's events. vanished.push((event.pubkey, event.created_at)); } } @@ -48,10 +47,8 @@ impl Deletions { } /// Whether the event is covered by a valid deletion or vanish request. - /// - /// A request is only valid when its author matches the deleted event's - /// author (NIP-09); addressable events are deleted up to the request's - /// `created_at`. + /// A request is valid when its author matches the deleted event's author, per NIP-09. + /// Addressable events are deleted up to the request's `created_at`. pub fn is_deleted(&self, event: &Event) -> bool { if self .vanished diff --git a/crates/signed_core/src/filters.rs b/crates/signed_core/src/filters.rs index b8f3253..a82049f 100644 --- a/crates/signed_core/src/filters.rs +++ b/crates/signed_core/src/filters.rs @@ -25,7 +25,7 @@ pub fn announcement(addr: &RepoAddr) -> Filter { .identifier(addr.identifier.clone()) } -/// Latest state event (refs / HEAD) for a repository. +/// Latest state event for a repository, carrying refs and HEAD. pub fn state(addr: &RepoAddr) -> Filter { Filter::new() .kind(Kind::RepoState) @@ -33,18 +33,18 @@ pub fn state(addr: &RepoAddr) -> Filter { .identifier(addr.identifier.clone()) } -/// All NIP-34 activity addressed to a repository (`#a` tag): issues, PRs, -/// patches, statuses and comments (kind 1111). -/// -/// Note: the `a` tag on status events is optional per NIP-34, so statuses -/// published without it won't be matched here. +/// All NIP-34 activity addressed to a repository via its `#a` tag. +/// Covers issues, PRs, patches, statuses and kind-1111 comments. +/// The `a` tag is optional on status events per NIP-34. +/// Statuses published without it are not matched here. pub fn activity(addr: &RepoAddr) -> Filter { Filter::new().kinds(ACTIVITY_KINDS).coordinate(addr) } -/// Status events (`1630..=1633`) referencing any of the given root events -/// (`#e` tag). Batched: one filter covers all roots, so a negentropy sync -/// reconciles them in a single session instead of one per root. +/// Status events, kinds `1630..=1633`, referencing any of the given root events. +/// They are matched via the `#e` tag. +/// One filter covers all roots. +/// A negentropy sync reconciles them in a single session, not one per root. pub fn statuses_for(roots: impl IntoIterator) -> Filter { Filter::new() .kinds([ @@ -56,33 +56,29 @@ pub fn statuses_for(roots: impl IntoIterator) -> Filter { .events(roots) } -/// Cover notes (kind 1624) and NIP-32 label events (kind 1985) referencing -/// any of the given root events (`#e` tag), fetched per root like comments -/// and statuses because they carry no repository `a` tag. Batched, like -/// [`statuses_for`]. +/// Cover notes and NIP-32 label events referencing any of the given root events. +/// These are kinds 1624 and 1985, matched via the `#e` tag. +/// Because they carry no repository `a` tag, they are fetched by root like comments. +/// Batched, like [`statuses_for`]. pub fn annotations_for(roots: impl IntoIterator) -> Filter { Filter::new() .kinds([crate::COVER_NOTE_KIND, Kind::Label]) .events(roots) } -/// A user's grasp list (kind `10317`). +/// A user's grasp list, kind `10317`. pub fn grasp_list(public_key: PublicKey) -> Filter { Filter::new() .kind(Kind::GitUserGraspList) .author(public_key) } -/// NIP-22 comments (kind `1111`) referencing any of the given root events -/// (issues, patches, PRs). -/// -/// Comments carry no repository `a` tag, so they must be fetched by their -/// root reference. NIP-22 defines the uppercase `E` tag as the thread root -/// (used by ngit), but some clients (including Signed) use a lowercase `e` -/// tag, so both are matched. -/// -/// Returns two filters because `#E` and `#e` conditions would be ANDed if -/// combined into one. +/// NIP-22 comments, kind `1111`, referencing any of the given root events. +/// The roots are issues, patches and PRs. +/// Comments carry no repository `a` tag, so they are fetched by root reference. +/// NIP-22 names the uppercase `E` tag as the thread root, used by ngit. +/// Some clients, including Signed, use a lowercase `e` tag, so both are matched. +/// Returns two filters, since combining `#E` and `#e` would AND the conditions. pub fn comments_for(roots: impl IntoIterator) -> Vec { let roots: Vec = roots.into_iter().map(|id| id.to_hex()).collect(); if roots.is_empty() { @@ -105,42 +101,41 @@ pub fn announcements_by(public_key: PublicKey) -> Filter { .author(public_key) } -/// All repository announcements (for global discovery). -/// -/// Unbounded: intended for negentropy sync, which reconciles sets -/// efficiently regardless of size. Local database queries with this -/// filter are served by LMDB, so they stay fast as the database grows. +/// All repository announcements, for global discovery. +/// Unbounded, intended for negentropy sync, which reconciles sets regardless of size. +/// Local queries with this filter are served by LMDB, staying fast as the database grows. pub fn all_announcements() -> Filter { Filter::new().kind(Kind::GitRepoAnnouncement) } /// How far back deletion requests are fetched and stored. -/// -/// A deletion request can only target events created before it, and NIP-34 -/// events are all far younger than this window, so older requests can never -/// match anything shown. Bounding the window keeps the kind-5/62 set (one of -/// the largest on public relays) from being fully reconciled on every sync. +/// A deletion request can only target events created before it. +/// NIP-34 events are far younger than this window. +/// Older requests can never match anything shown. +/// Bounding the window keeps the kind-5 and kind-62 set from a full sync reconciliation. +/// That set is one of the largest on public relays. const DELETIONS_LOOKBACK: Duration = Duration::from_secs(3 * 365 * 86_400); -/// `now` minus [`DELETIONS_LOOKBACK`], quantized to whole days so identical -/// filters hash the same and the backend's sync dedup can match them. +/// `now` minus [`DELETIONS_LOOKBACK`]. +/// Quantized to whole days so identical filters hash the same. +/// This lets the backend's sync dedup match identical filters. fn deletions_since() -> Timestamp { let now = Timestamp::now().as_secs(); Timestamp::from_secs(now - now % 86_400) - DELETIONS_LOOKBACK } -/// All deletion-related events (NIP-09 kind `5`, NIP-62 kind `62`) within -/// [`DELETIONS_LOOKBACK`]. Deletion requests must be known before any other -/// event can be shown. +/// All deletion-related events within [`DELETIONS_LOOKBACK`]. +/// These are NIP-09 kind `5` and NIP-62 kind `62`. +/// Deletion requests must be known before any other event is shown. pub fn deletions() -> Filter { Filter::new() .kinds([Kind::EventDeletion, Kind::RequestToVanish]) .since(deletions_since()) } -/// Deletion events relevant to a single repository: requests authored by -/// the repository owner and requests addressed to the repository -/// coordinate (`#a` tag). +/// Deletion events relevant to a single repository. +/// Requests authored by the repository owner. +/// Requests addressed to the repository coordinate via its `#a` tag. pub fn deletions_for_repo(addr: &RepoAddr) -> Vec { vec![ Filter::new() diff --git a/crates/signed_core/src/model.rs b/crates/signed_core/src/model.rs index 3c91795..2da218a 100644 --- a/crates/signed_core/src/model.rs +++ b/crates/signed_core/src/model.rs @@ -5,16 +5,16 @@ use nostr::prelude::*; use crate::RepoAddr; -/// Parsed NIP-34 repository announcement (plain data, ready for the UI). +/// Parsed NIP-34 repository announcement, plain data ready for the UI. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Announcement { /// ID of the announcement event itself. pub event_id: EventId, - /// Repository ID (`d` tag). + /// Repository ID, the `d` tag. pub id: String, /// Author of the announcement event. pub owner: PublicKey, - /// When the announcement was published (for latest-wins resolution). + /// When the announcement was published, used for latest-wins resolution. pub created_at: Timestamp, pub name: Option, pub description: Option, @@ -24,36 +24,38 @@ pub struct Announcement { pub clone: Vec, /// Relays the repository monitors for patches and issues. pub relays: Vec, - /// Earliest unique commit ID (`r` tag with `euc` marker). + /// Earliest unique commit ID, the `r` tag with `euc` marker. pub euc: Option, /// Other recognized maintainers. pub maintainers: Vec, - /// Value of a `u` tag, if any: this repository is a subordinate fork of - /// the referenced upstream (NIP-34). + /// Value of a `u` tag, if any. + /// Marks the repository as a subordinate fork of the upstream, per NIP-34. pub upstream: Option, - /// Hashtags labelling the repository (`t` tags). + /// Hashtags labelling the repository, the `t` tags. pub hashtags: Vec, } -/// The `u` tag of a fork announcement (NIP-34) -/// the repository this one is a subordinate fork of. The first value is -/// the upstream coordinate (`30617::`) or a git URL. -/// The second is an optional relay hint for the upstream. +/// The `u` tag of a fork announcement, per NIP-34. +/// The first value is the upstream coordinate or a git URL. +/// The coordinate form is `30617::`. +/// The second value is an optional relay hint for the upstream. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Upstream { - /// Raw first value of the `u` tag (coordinate or git URL). + /// Raw first value of the `u` tag, a coordinate or git URL. pub raw: String, - /// The upstream `30617::` coordinate, when the `u` tag - /// references a NIP-34 repository; `None` for the git-URL form. + /// Upstream repository coordinate when the `u` tag names a NIP-34 repository. + /// `None` for the git-URL form. pub addr: Option, /// Relay hint for the upstream, if the `u` tag carries one. pub relay_hint: Option, } impl Upstream { - /// Parse the `u` tag values. The first is the upstream coordinate or a - /// git URL (the coordinate form may append `|git-url`; the coordinate is - /// the part before the first `|`), the second an optional relay hint. + /// Parse the `u` tag values. + /// The first value is the upstream coordinate or a git URL. + /// The coordinate form may append `|git-url`. + /// The coordinate is the part before the first `|`. + /// The second value is an optional relay hint. fn parse(raw: &str, relay_hint: Option<&str>) -> Self { let coordinate = raw.split('|').next().unwrap_or(raw); let addr = coordinate @@ -67,8 +69,8 @@ impl Upstream { } } - /// Text for display: the upstream coordinate when it is a NIP-34 - /// repository, otherwise the raw `u` value (git-URL form). + /// Text for display. + /// The upstream coordinate for a NIP-34 repository, else the raw `u` value. pub fn display(&self) -> SharedString { match &self.addr { Some(addr) => SharedString::from(addr.to_string()), @@ -77,8 +79,8 @@ impl Upstream { } } -/// Subject of a NIP-34 issue or pull request event: the `subject` tag, -/// falling back to the first non-empty line of the content. +/// Subject of a NIP-34 issue or pull request event. +/// Taken from the `subject` tag, else the first non-empty line of the content. pub fn activity_subject(event: &Event) -> SharedString { let subject = event .tags @@ -101,13 +103,13 @@ pub fn activity_subject(event: &Event) -> SharedString { .unwrap_or(SharedString::from("Untitled")) } -/// The patch set of a pull request: the root patch event (kind `1617`) the -/// PR references via its `e` tag, plus every patch of the set chained to it -/// with NIP-10 `e` reply tags, in series order (oldest first). When the PR -/// has no `e` tag, falls back to the patch producing the PR's tip commit -/// (its `commit`/`r` tag, per NIP-34) and walks the reply chain backward to -/// the root. -/// +/// The patch set of a pull request. +/// The PR references the root patch event, kind `1617`, via its `e` tag. +/// Every patch of the set is chained to the previous one with NIP-10 `e` reply tags. +/// They are returned in series order, oldest first. +/// A PR without an `e` tag falls back to the patch producing its tip commit. +/// The tip commit is the PR's `commit` or `r` tag, per NIP-34. +/// The reply chain is then walked backward to the root. /// Returns an empty list when no patch event can be linked to the PR. pub fn pull_request_patches<'a>( pr: &Event, @@ -115,17 +117,18 @@ pub fn pull_request_patches<'a>( ) -> Vec<&'a Event> { let patches: Vec<&'a Event> = patches.into_iter().collect(); - // The PR references its root patch via an `e` tag; follow the NIP-10 - // reply chain forward from there (each patch of the set replies to the - // previous one). Among several replies (a revision), the newest wins. + // The PR references its root patch via an `e` tag. + // Follow the NIP-10 reply chain forward from there. + // Each patch replies to the previous one, and among several replies the newest wins. if let Some(root_id) = pr.tags.event_ids().next() && let Some(root) = patches.iter().find(|patch| patch.id == root_id) { return forward_series(root, &patches); } - // No `e` tag: the last patch of the set carries the PR's tip commit in - // its `commit`/`r` tag; walk the reply chain backward to the root. + // The PR has no `e` tag. + // The last patch of the set carries the tip commit in its `commit` or `r` tag. + // Walk the reply chain backward to the root. let Some(tip) = current_commit_of(pr) else { return Vec::new(); }; @@ -156,10 +159,9 @@ pub fn pull_request_patches<'a>( series } -/// The patch content of a pull request: the contents of every patch event of -/// its patch set (see [`pull_request_patches`]) joined in series order, -/// falling back to the PR's own content for older PRs that carried the -/// patch inline. +/// The patch content of a pull request. +/// The contents of its patch set, see [`pull_request_patches`], joined in series order. +/// Older PRs that carried the patch inline fall back to their own content. pub fn pull_request_patch<'a>(pr: &Event, patches: impl IntoIterator) -> String { let patches: Vec<&'a Event> = patches.into_iter().collect(); let series = pull_request_patches(pr, patches.iter().copied()); @@ -173,7 +175,7 @@ pub fn pull_request_patch<'a>(pr: &Event, patches: impl IntoIterator(root: &'a Event, patches: &[&'a Event]) -> Vec<&'a Event> { let mut series = vec![root]; loop { @@ -195,7 +197,7 @@ fn forward_series<'a>(root: &'a Event, patches: &[&'a Event]) -> Vec<&'a Event> series } -/// The `c` tag of an event (tip of the proposed branch), as hex. +/// The `c` tag of an event, the tip of the proposed branch, as hex. fn current_commit_of(event: &Event) -> Option { event .tags @@ -206,8 +208,8 @@ fn current_commit_of(event: &Event) -> Option { }) } -/// Whether `patch` produces `commit` (its `commit` or `r` tag), so clients -/// can find existing patches for a specific commit. +/// Whether `patch` produces `commit`, found via its `commit` or `r` tag. +/// It lets clients find existing patches for a specific commit. fn patch_produces_commit(patch: &Event, commit: &str) -> bool { patch .tags @@ -219,7 +221,8 @@ fn patch_produces_commit(patch: &Event, commit: &str) -> bool { } impl Announcement { - /// Parse a kind `30617` event. Returns `None` if the kind is wrong or the `d` tag is missing. + /// Parse a kind `30617` event. + /// Returns `None` when the kind is wrong or the `d` tag is missing. pub fn from_event(event: &Event) -> Option { if event.kind != Kind::GitRepoAnnouncement { return None; @@ -251,8 +254,8 @@ impl Announcement { _ => {} } - // The `u` tag is not modelled by the SDK's `Nip34Tag`; parse it - // manually (first wins). + // The SDK's `Nip34Tag` does not model the `u` tag, so parse it manually. + // Only the first `u` tag is used. if upstream.is_none() && tag.kind() == "u" { let values = tag.as_slice(); let raw = values.get(1).map(String::as_str).unwrap_or_default(); @@ -284,11 +287,10 @@ impl Announcement { crate::repo_addr(self.owner, self.id.clone()) } - /// Whether this announcement is a fork of the repository at `base`: - /// its `u` tag points at `base` (also covers permanent forks whose EUC - /// diverged), or it shares `base`'s earliest unique commit (EUC) and is - /// not the base repository itself. Read-only discovery input: nothing - /// here is published back to nostr. + /// Whether this announcement is a fork of the repository at `base`. + /// Its `u` tag points at `base`, which also covers permanent forks whose EUC diverged. + /// Or it shares `base`'s earliest unique commit and is not the base itself. + /// Read-only discovery input, nothing here is published back to nostr. pub fn is_fork_of(&self, base: &RepoAddr, base_euc: Option<&str>) -> bool { if self.addr() == *base { return false; @@ -306,10 +308,10 @@ impl Announcement { .unwrap_or(SharedString::from("No description")) } - /// The effective maintainers of this repository: the announced - /// `maintainers` plus the announcement author, who asserts themselves as - /// a maintainer of the primary project unless a `u` tag marks this - /// repository as a subordinate fork (NIP-34). + /// The effective maintainers of this repository. + /// The announced `maintainers` plus the announcement author. + /// The author asserts themselves as a maintainer of the primary project. + /// A `u` tag that marks the repository as a subordinate fork excludes them, per NIP-34. pub fn effective_maintainers(&self) -> Vec { let mut maintainers = self.maintainers.clone(); if self.upstream.is_none() && !maintainers.contains(&self.owner) { @@ -318,8 +320,8 @@ impl Announcement { maintainers } - /// The `git clone` URLs for this repository, deduplicated while - /// preserving the announced order (deterministic across calls). + /// The `git clone` URLs for this repository, deduplicated. + /// The announced order is preserved, making output deterministic across calls. pub fn clone_urls(&self) -> Vec { let mut seen = HashSet::new(); self.clone @@ -464,8 +466,8 @@ mod tests { let announcement = Announcement::from_event(&event).expect("parses"); let upstream = announcement.upstream.expect("parses the u tag"); - // The coordinate part resolves to a repository address; the raw - // value keeps the `|git-url` suffix. + // The coordinate part resolves to a repository address. + // The raw value keeps the `|git-url` suffix. assert_eq!( upstream.addr, Some(crate::repo_addr( @@ -489,8 +491,8 @@ mod tests { #[test] fn parses_git_url_upstream() { - // The `u` tag may reference a non-nostr upstream by git URL only; - // there is no repository address to navigate to. + // The `u` tag may reference a non-nostr upstream by git URL only. + // There is no repository address to navigate to. let event = announcement_event(&[ &["d", "my-fork"], &["u", "https://example.com/upstream.git"], @@ -508,7 +510,7 @@ mod tests { #[test] fn is_fork_of_matches_the_u_tag_coordinate() { - // The base repository (announced by the `u`-tag's owner). + // The base repository, announced by the `u` tag's owner. let base = crate::repo_addr( PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"), "upstream", @@ -516,21 +518,21 @@ mod tests { let event = announcement_event(&[&["d", "my-fork"], &["u", &base.to_string()]]); let fork = Announcement::from_event(&event).expect("parses"); - // A `u` tag pointing at the base address marks a fork even when - // neither side announces an EUC. + // A `u` tag pointing at the base address marks a fork. + // This holds even when neither side announces an EUC. assert!(fork.is_fork_of(&base, None)); } #[test] fn is_fork_of_matches_a_shared_euc() { let euc = "aa231c4c6a5777dc89b42207b499891a344add5c"; - // The base repo has no `u` tag; it announces the family EUC. + // The base repo has no `u` tag. It announces the family EUC. let base_event = announcement_event(&[&["d", "upstream"], &["r", euc, "euc"]]); let base = Announcement::from_event(&base_event).expect("parses"); let base_addr = base.addr(); - // A fork (no `u` tag; a pure mirror or cross-hosted clone) shares - // the EUC, so clients of the family can find it. + // A fork with no `u` tag, a pure mirror or cross-hosted clone, shares the EUC. + // Clients of the family can then find it. let fork_event = announcement_event(&[&["d", "mirror"], &["r", euc, "euc"]]); let fork = Announcement::from_event(&fork_event).expect("parses"); assert!(fork.is_fork_of(&base_addr, base.euc.as_deref())); @@ -549,8 +551,8 @@ mod tests { #[test] fn is_fork_of_matches_permanent_forks_with_a_diverged_euc() { - // A permanent fork re-announces its EUC (first commit after the - // fork); only the `u` tag still relates it to the base. + // A permanent fork re-announces its EUC, the first commit after the fork. + // Only the `u` tag still relates it to the base. let base = crate::repo_addr( PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"), "upstream", @@ -573,8 +575,7 @@ mod tests { let base = Announcement::from_event(&event).expect("parses"); let base_addr = base.addr(); - // The base announcement matches its own EUC, but is not a fork of - // itself. + // The base announcement matches its own EUC but is not a fork of itself. assert!(!base.is_fork_of(&base_addr, base.euc.as_deref())); } @@ -585,8 +586,8 @@ mod tests { let announcement = Announcement::from_event(&event).expect("parses"); let maintainers = announcement.effective_maintainers(); - // The owner asserts themselves as a maintainer of the primary - // project (NIP-34), alongside the announced co-maintainers. + // The owner asserts themselves as a maintainer of the primary project, per NIP-34. + // Announced co-maintainers are included too. assert_eq!(maintainers.len(), 2); assert!(maintainers.contains(&announcement.owner)); assert!(maintainers.contains(&PublicKey::from_hex(MAINTAINER_HEX).expect("valid pubkey"))); @@ -603,8 +604,8 @@ mod tests { let announcement = Announcement::from_event(&event).expect("parses"); let maintainers = announcement.effective_maintainers(); - // A `u` tag marks the repository as a subordinate fork: the author - // is not a maintainer of the primary project (NIP-34). + // A `u` tag marks the repository as a subordinate fork. + // The author is then not a maintainer of the primary project, per NIP-34. assert!(!maintainers.contains(&announcement.owner)); assert_eq!( maintainers, @@ -632,7 +633,7 @@ mod tests { #[test] fn pull_request_patch_falls_back_to_inline_content() { - // Older PRs carried the patch in the content; no linked patch event. + // Older PRs carried the patch in the content and link no patch event. let pr = pr_event("patch-inline", vec![]); assert_eq!(pull_request_patch(&pr, [] as [&Event; 0]), "patch-inline"); @@ -659,8 +660,8 @@ mod tests { #[test] fn pull_request_patch_joins_the_whole_patch_set() { - // NIP-34: a PR references the root patch; later patches of the set - // reply to the previous one (NIP-10 `e` tags). + // A PR references the root patch, per NIP-34. + // Later patches of the set reply to the previous one via NIP-10 `e` tags. let root = patch_event("patch-one", vec![], 100); let second = patch_event("patch-two", vec![Tag::event(root.id)], 200); let pr = pr_event("description", vec![Tag::event(root.id)]); @@ -712,8 +713,8 @@ mod tests { #[test] fn pull_request_patches_finds_the_set_via_the_tip_commit() { - // PRs without an `e` tag: the last patch of the set carries the tip - // commit in its `r` tag; walk the reply chain backward to the root. + // PRs without an `e` tag fall back to the patch producing the tip commit. + // Walk the reply chain backward to the root. let root = patch_event("patch-one", vec![], 100); let tip = "1111111111111111111111111111111111111111"; let last = patch_event( diff --git a/crates/signed_core/src/state.rs b/crates/signed_core/src/state.rs index 6032748..258487a 100644 --- a/crates/signed_core/src/state.rs +++ b/crates/signed_core/src/state.rs @@ -1,10 +1,10 @@ use nostr::prelude::*; /// Build a kind `30618` repository state event from refs and HEAD. -/// -/// `refs` are `(refname, commit-id)` pairs (e.g. `refs/heads/main`); `head` -/// is the short branch name HEAD points to, published as -/// `ref: refs/heads/`. The `d` tag matches the repository id. +/// `refs` are `(refname, commit-id)` pairs, e.g. `refs/heads/main`. +/// `head` is the short branch name HEAD points to. +/// It is published as `ref: refs/heads/`. +/// The `d` tag matches the repository id. pub fn build_state(id: &str, refs: &[(String, String)], head: Option<&str>) -> EventBuilder { let mut tags: Vec = vec![Tag::identifier(id.to_owned())]; for (name, commit) in refs { @@ -19,9 +19,8 @@ pub fn build_state(id: &str, refs: &[(String, String)], head: Option<&str>) -> E } /// Parse a kind `30618` repository state event into refs and HEAD. -/// -/// `refs` are `(refname, commit-id)` pairs; `head` is the branch pointed to -/// by the `HEAD` tag, if any. +/// `refs` are `(refname, commit-id)` pairs. +/// `head` is the branch pointed to by the `HEAD` tag, if any. pub fn parse_state(event: &Event) -> (Vec<(String, String)>, Option) { let mut refs = Vec::new(); let mut head = None; diff --git a/crates/signed_core/src/status.rs b/crates/signed_core/src/status.rs index 01b3d3f..26498b9 100644 --- a/crates/signed_core/src/status.rs +++ b/crates/signed_core/src/status.rs @@ -1,6 +1,6 @@ use nostr::prelude::*; -/// Status of a root patch, pull request or issue (kinds `1630..=1633`). +/// Status of a root patch, pull request or issue, kinds `1630..=1633`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum RepoStatus { Open, @@ -30,9 +30,9 @@ impl RepoStatus { } } -/// Check whether an event references the given root event via an `e` or `E` -/// tag. NIP-10 / NIP-34 use the lowercase `e` tag; NIP-22 comments (kind -/// `1111`) use the uppercase `E` tag for the root of the thread. +/// Whether an event references the given root event via an `e` or `E` tag. +/// NIP-10 and NIP-34 use the lowercase `e` tag. +/// NIP-22 comments, kind `1111`, use the uppercase `E` tag for the thread root. pub fn references_root(event: &Event, root: &EventId) -> bool { let root = root.to_hex(); event @@ -41,8 +41,8 @@ pub fn references_root(event: &Event, root: &EventId) -> bool { .any(|tag| matches!(tag.kind(), "e" | "E") && tag.content() == Some(root.as_str())) } -/// Resolve the status of a root event per NIP-34: -/// the most recent status event from the root author or a maintainer wins. +/// Resolve the status of a root event per NIP-34. +/// The most recent status event from the root author or a maintainer wins. /// Defaults to [`RepoStatus::Open`]. pub fn resolve_status<'a, I>( status_events: I, diff --git a/crates/signed_git/src/lib.rs b/crates/signed_git/src/lib.rs index 05bb5d4..748ecd1 100644 --- a/crates/signed_git/src/lib.rs +++ b/crates/signed_git/src/lib.rs @@ -43,8 +43,9 @@ impl GitCache { } } - /// Open the local clone if it exists (fetching first), otherwise clone - /// from the first working URL in `clone_urls` (the announcement's `clone` tag). + /// Open the existing clone, fetching it first. + /// Otherwise clone from the first working URL in `clone_urls`. + /// `clone_urls` holds the announcement's `clone` tag. pub fn ensure_clone(&self, addr: &RepoAddr, clone_urls: &[String]) -> Result { let path = self.repo_path(addr); @@ -64,19 +65,19 @@ impl GitCache { } } -/// Maximum directory nesting depth when scanning for local repositories, -/// so pathological trees can't stall the scan. +/// Maximum directory nesting depth when scanning for local repositories. +/// Pathological trees can't stall the scan. const SCAN_MAX_DEPTH: usize = 12; -/// Directories never descended into during a scan: dependency caches that -/// can be enormous without ever containing user repositories. +/// Directories never descended into during a scan. +/// Dependency caches can be enormous without ever containing user repositories. const SCAN_SKIPPED_DIRS: [&str; 1] = ["node_modules"]; -/// Walk `root` recursively and collect the paths of git repositories -/// (directories containing a `.git` entry) below it. -/// -/// Hidden entries and symlinks are skipped; repositories are not descended -/// into, so nested ones (e.g. submodule worktrees) are not reported. +/// Walk `root` recursively and collect the paths of git repositories below it. +/// A repository is a directory containing a `.git` entry. +/// Hidden entries and symlinks are skipped. +/// Repositories are not descended into. +/// Nested ones like submodule worktrees are not reported. /// Results are canonicalized, deduplicated and sorted. pub fn find_git_repos(root: &Path) -> Vec { let mut repos = Vec::new(); @@ -89,8 +90,9 @@ pub fn find_git_repos(root: &Path) -> Vec { if depth > SCAN_MAX_DEPTH { continue; } - // A directory containing a `.git` entry is a repository (a linked - // worktree has a `.git` file instead of a directory); don't descend. + // A directory containing a `.git` entry is a repository. + // A linked worktree has a `.git` file instead of a directory. + // Don't descend into repositories. if dir.join(".git").exists() { if let Ok(path) = dir.canonicalize() { repos.push(path); @@ -122,11 +124,11 @@ pub fn find_git_repos(root: &Path) -> Vec { repos } -/// Clone a repository into `path` from the first working URL in -/// `clone_urls` (the announcement's `clone` tag), then fetch the -/// `refs/nostr/*` PR refs like the cache clone does. The destination must -/// not exist yet. When no URL works, the last error is returned. -/// +/// Clone into `path` from the first working URL in `clone_urls`. +/// `clone_urls` holds the announcement's `clone` tag. +/// Then fetch the `refs/nostr/*` PR refs like the cache clone does. +/// The destination must not exist yet. +/// When no URL works, the last error is returned. /// Unlike [`GitCache::ensure_clone`], the clone is not kept in any cache. pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> { if path.exists() { @@ -138,8 +140,8 @@ pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> { for url in clone_urls { match clone(url, path) { Ok(repo) => { - // The initial clone uses the default refspecs; also - // fetch the `refs/nostr/*` PR refs. + // The initial clone uses the default refspecs. + // Also fetch the `refs/nostr/*` PR refs. fetch_all(&repo).ok(); return Ok(()); } @@ -153,9 +155,9 @@ pub fn clone_repo(clone_urls: &[String], path: &Path) -> Result<()> { } } -/// Fetch all configured refspecs from `origin`, plus the `refs/nostr/*` -/// namespace where GRASP mirrors serve pull request branches (one ref per -/// PR event id, as used by ngit). +/// Fetch all configured refspecs from `origin`, plus the `refs/nostr/*` namespace. +/// GRASP mirrors serve pull request branches there, one ref per PR event id. +/// This mirrors the layout used by ngit. pub fn fetch_all(repo: &gix::Repository) -> Result<()> { let options = gix::remote::ref_map::Options { extra_refspecs: vec![ @@ -174,10 +176,9 @@ pub fn fetch_all(repo: &gix::Repository) -> Result<()> { Ok(()) } -/// Apply a `git format-patch` patch (or series) with `git am`. -/// -/// Uses the git CLI because it handles the mbox format natively; can be -/// replaced with a pure-Rust implementation later without changing callers. +/// Apply a `git format-patch` patch or series with `git am`. +/// Uses the git CLI because it handles the mbox format natively. +/// Can be replaced with a pure-Rust implementation later without changing callers. pub fn apply_patch(repo_path: &Path, patch: &str) -> Result<()> { let mut child = Command::new("git") .arg("am") @@ -200,9 +201,10 @@ pub fn apply_patch(repo_path: &Path, patch: &str) -> Result<()> { Ok(()) } -/// The merge base of two revisions (branch names, remote-tracking refs or -/// commit ids) in the repository at `repo_path`. `Ok(None)` when the -/// revisions share no common ancestor; unresolvable revisions are errors. +/// The merge base of two revisions in the repository at `repo_path`. +/// Revisions may be branch names, remote-tracking refs or commit ids. +/// `Ok(None)` when the revisions share no common ancestor. +/// Unresolvable revisions are errors. pub fn merge_base(repo_path: &Path, a: &str, b: &str) -> Result> { let output = Command::new("git") .arg("-C") @@ -214,7 +216,7 @@ pub fn merge_base(repo_path: &Path, a: &str, b: &str) -> Result> .context("failed to spawn `git merge-base`")?; match output.status.code() { - // Exit 1: no common ancestor (a valid outcome for a proposal). + // Exit 1 means no common ancestor, a valid outcome for a proposal. Some(1) => Ok(None), Some(0) => Ok(Some( String::from_utf8_lossy(&output.stdout).trim().to_owned(), @@ -226,9 +228,10 @@ pub fn merge_base(repo_path: &Path, a: &str, b: &str) -> Result> } } -/// The `git format-patch` series of `base..tip` (mbox), like -/// `git format-patch --stdout`. Fails when the range has no commits. The -/// mbox is returned untrimmed; trailing newlines are part of the format. +/// The `git format-patch` mbox series of `base..tip`, like `git format-patch --stdout`. +/// Fails when the range has no commits. +/// The mbox is returned untrimmed. +/// Trailing newlines are part of the format. pub fn format_patch_between(repo_path: &Path, base: &str, tip: &str) -> Result { let output = Command::new("git") .arg("-C") @@ -252,10 +255,10 @@ pub fn format_patch_between(repo_path: &Path, base: &str, tip: &str) -> Result Result<()> { let mut child = Command::new("git") .arg("apply") @@ -282,11 +285,11 @@ pub fn patch_applies(repo_path: &Path, patch: &str) -> Result<()> { Ok(()) } -/// Push `commit` to `reference` (e.g. `refs/nostr/`) on the git -/// server at `url`, from the repository at `repo_path`. GRASP servers host -/// the `refs/nostr` namespace so anyone can contribute a commit; nak pushes -/// pull request tips there before publishing the PR event, and readers -/// fetch the ref to get the commit behind a PR's `c` tag. +/// Push `commit` to `reference` on the server at `url`, from `repo_path`. +/// `reference` is a ref name like `refs/nostr/`. +/// GRASP servers host the `refs/nostr` namespace so anyone can contribute a commit. +/// nak pushes pull request tips there before publishing the PR event. +/// Readers fetch the ref to get the commit behind a PR's `c` tag. pub fn push_commit_ref(repo_path: &Path, url: &str, commit: &str, reference: &str) -> Result<()> { let output = Command::new("git") .arg("-C") @@ -308,10 +311,11 @@ pub fn push_commit_ref(repo_path: &Path, url: &str, commit: &str, reference: &st Ok(()) } -/// Split a `git format-patch` series into its individual patches (mbox -/// messages). Each message begins with a `From <40-hex> ` boundary line; -/// `>From` quoting inside bodies means no false positives. A single patch -/// yields one element; a malformed input yields one element covering it. +/// Split a `git format-patch` series into its individual patches, mbox messages. +/// Each message begins with a `From <40-hex> ` boundary line. +/// `>From` quoting inside bodies means no false positives. +/// A single patch yields one element. +/// A malformed input yields one element covering it. pub fn split_patch_series(patch: &str) -> Vec<&str> { let mut starts = vec![0usize]; let mut search_from = 1; @@ -337,8 +341,8 @@ pub fn split_patch_series(patch: &str) -> Vec<&str> { .collect() } -/// The commit HEAD points to in the repository at `repo_path`, or `None` -/// when the repository has no commits yet (unborn HEAD). +/// The commit HEAD points to in the repository at `repo_path`. +/// `None` when the repository has no commits yet, an unborn HEAD. pub fn head_commit_id(repo_path: &Path) -> Result> { let output = Command::new("git") .arg("-C") @@ -357,16 +361,17 @@ pub fn head_commit_id(repo_path: &Path) -> Result> { )) } -/// The commits in `base..HEAD` of the repository at `repo_path`, oldest -/// first (the order `git am` creates them); `HEAD` alone when `base` is -/// `None`. An empty range yields an empty list. +/// The commits in `base..HEAD` of the repository at `repo_path`, oldest first. +/// This is the order `git am` creates them. +/// `HEAD` alone when `base` is `None`. +/// An empty range yields an empty list. pub fn commits_since(repo_path: &Path, base: Option<&str>) -> Result> { let output = match base { Some(base) => git_in( repo_path, &["rev-list", "--reverse", &format!("{base}..HEAD")], )?, - // No `base` (unborn HEAD): there is nothing to walk yet. + // Without `base`, an unborn HEAD means there is nothing to walk. None => match git_in(repo_path, &["rev-parse", "HEAD"]) { Ok(head) => head, Err(_) => return Ok(Vec::new()), @@ -380,8 +385,8 @@ pub fn commits_since(repo_path: &Path, base: Option<&str>) -> Result } fn clone(url: &str, path: &Path) -> Result { - // GRASP servers announce `grasp:////` clone URLs; - // the transport is git smart HTTP, so rewrite the scheme for gix. + // GRASP servers announce `grasp:////` clone URLs. + // The transport is git smart HTTP, so the scheme is rewritten for gix. let url = url .strip_prefix("grasp://") .map(|rest| format!("https://{rest}")) @@ -395,12 +400,11 @@ fn clone(url: &str, path: &Path) -> Result { Ok(repo) } -/// Create a new repository at `path`: initialize a `main` branch, write a -/// `README.md` derived from `name`/`description`, and create the initial -/// commit. Returns the initial commit id. -/// -/// Uses the git CLI (like [`apply_patch`]), which handles index writes, -/// ref updates and default branch selection natively. +/// Create a repository at `path` with an initial `main` branch. +/// Write a `README.md` from `name` and `description`, then create the initial commit. +/// Returns the initial commit id. +/// Uses the git CLI, like [`apply_patch`]. +/// The CLI handles index writes, ref updates and default branch selection natively. pub fn init_repository(path: &Path, name: &str, description: &str) -> Result { std::fs::create_dir_all(path) .with_context(|| format!("failed to create {}", path.display()))?; @@ -415,8 +419,8 @@ pub fn init_repository(path: &Path, name: &str, description: &str) -> Result Result<()> { let url = format!("{base_url}/{owner}/{repo_id}.git"); @@ -491,9 +495,10 @@ pub fn push_all(repo_path: &Path, base_url: &str, owner: &str, repo_id: &str) -> Ok(()) } -/// The earliest unique commit of the repository at `repo_path` (a root -/// commit, like `git rev-list --max-parents=0 HEAD`), used as the NIP-34 -/// announcement's `euc` marker. `None` for a repository without commits. +/// The earliest unique commit of the repository at `repo_path`. +/// A root commit, like `git rev-list --max-parents=0 HEAD`. +/// Used as the NIP-34 announcement's `euc` marker. +/// `None` for a repository without commits. pub fn root_commit(repo_path: &Path) -> Result> { let output = Command::new("git") .arg("-C") @@ -504,8 +509,8 @@ pub fn root_commit(repo_path: &Path) -> Result> { .output() .context("failed to spawn `git rev-list`")?; - // An unborn HEAD (no commits yet) makes `rev-list` fail, - // there is no unique commit to report then. + // An unborn HEAD with no commits yet makes `rev-list` fail. + // There is no unique commit to report then. if !output.status.success() { return Ok(None); } @@ -517,10 +522,10 @@ pub fn root_commit(repo_path: &Path) -> Result> { .filter(|id| id.len() == 40)) } -/// Add `origin` pointing at `url` when the repository has no remote yet, -/// with the standard fetch mapping so later `git fetch origin` (and the -/// cache's `fetch_all`) updates `refs/remotes/origin/*`. No-op if `origin` -/// already exists. +/// Add `origin` pointing at `url` when the repository has no remote yet. +/// Uses the standard fetch mapping. +/// Later `git fetch origin` and the cache's `fetch_all` update `refs/remotes/origin/*`. +/// No-op if `origin` already exists. pub fn ensure_origin(repo_path: &Path, url: &str) -> Result<()> { // `git remote get-url origin` exits non-zero when the remote is absent. if git_in(repo_path, &["remote", "get-url", "origin"]).is_ok() { @@ -538,9 +543,9 @@ pub fn ensure_origin(repo_path: &Path, url: &str) -> Result<()> { Ok(()) } -/// Point `origin` at `url`, replacing an existing remote. Used after a -/// clone whose `origin` points at the cloned-from path (e.g. a working -/// copy cloned from a local mirror), to re-target it at the grasp server. +/// Point `origin` at `url`, replacing an existing remote. +/// Used after a clone whose `origin` points at the cloned-from path. +/// A working copy cloned from a local mirror is re-targeted at the grasp server. pub fn set_origin(repo_path: &Path, url: &str) -> Result<()> { // `git remote get-url origin` exits non-zero when the remote is absent. if git_in(repo_path, &["remote", "get-url", "origin"]).is_ok() { @@ -551,11 +556,12 @@ pub fn set_origin(repo_path: &Path, url: &str) -> Result<()> { Ok(()) } -/// Fetch `refspec` (e.g. `+refs/heads/*:refs/fork///*`) into the -/// repository at `repo_path` from the first working URL in `urls`, like -/// [`clone_repo`]: `grasp://` URLs are rewritten to `https://`, the -/// terminal prompt is disabled, and when no URL works the last error is -/// returned. Never touches the checked-out refs or the worktree. +/// Fetch `refspec` into `repo_path` from the first working URL in `urls`. +/// An example refspec is `+refs/heads/*:refs/fork///*`. +/// Like [`clone_repo`], `grasp://` URLs are rewritten to `https://`. +/// The terminal prompt is disabled. +/// When no URL works, the last error is returned. +/// Never touches the checked-out refs or the worktree. pub fn fetch_repo_refs(repo_path: &Path, urls: &[String], refspec: &str) -> Result<()> { let mut last_err: Option = None; @@ -591,12 +597,12 @@ pub fn fetch_repo_refs(repo_path: &Path, urls: &[String], refspec: &str) -> Resu } } -/// Full ref names under `prefix` (e.g. `refs/fork//`), sorted -/// lexicographically, like `git for-each-ref`. An empty list when nothing -/// matches. +/// Full ref names under `prefix`, sorted lexicographically, like `git for-each-ref`. +/// `prefix` is a ref namespace like `refs/fork//`. +/// Returns an empty list when nothing matches. pub fn refs_with_prefix(repo_path: &Path, prefix: &str) -> Result> { - // `for-each-ref` patterns match whole path components, so a trailing - // slash would silently change what is matched. + // `for-each-ref` patterns match whole path components. + // A trailing slash would silently change what is matched. let pattern = prefix.trim_end_matches('/'); let output = Command::new("git") .arg("-C") @@ -621,9 +627,10 @@ pub fn refs_with_prefix(repo_path: &Path, prefix: &str) -> Result> { .collect()) } -/// Delete every ref under `prefix` (e.g. `refs/fork//`) of the -/// repository at `repo_path`, so a stale import can be pruned before a -/// re-import. No-op when nothing matches. +/// Delete every ref under `prefix` of the repository at `repo_path`. +/// `prefix` is a ref namespace like `refs/fork//`. +/// Lets a stale import be pruned before a re-import. +/// No-op when nothing matches. pub fn delete_refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<()> { let refs = refs_with_prefix(repo_path, prefix)?; if refs.is_empty() { @@ -658,8 +665,8 @@ pub fn delete_refs_with_prefix(repo_path: &Path, prefix: &str) -> Result<()> { Ok(()) } -/// The URL of the `origin` remote of the repository at `workdir`, or `None` -/// when it has no `origin` yet. +/// The URL of the `origin` remote of the repository at `workdir`. +/// `None` when it has no `origin` yet. pub fn origin_url(workdir: &Path) -> Result> { let output = Command::new("git") .arg("-C") @@ -677,16 +684,16 @@ pub fn origin_url(workdir: &Path) -> Result> { Ok((!url.trim().is_empty()).then(|| url.trim().to_owned())) } -/// Fast-forward every local branch of the repository at `workdir` that is -/// behind its remote-tracking counterpart (`refs/remotes/origin/`), -/// like a `git pull --ff-only` on each branch, so a mirror clone used for -/// browsing catches up with the remote without ever rewriting history. -/// -/// The checked-out branch is moved with a merge so its worktree follows -/// (a dirty worktree fails the merge cleanly and is left for the next -/// refresh); other branches are updated directly. Branches without a -/// remote-tracking counterpart, or with local commits of their own, are -/// left alone. Returns whether any branch moved. +/// Fast-forward local branches that trail their remote-tracking counterpart. +/// The counterpart ref is `refs/remotes/origin/` in the repository at `workdir`. +/// Like a `git pull --ff-only` on each branch. +/// A mirror clone used for browsing catches up without rewriting history. +/// The checked-out branch is moved with a merge so its worktree follows. +/// A dirty worktree fails the merge cleanly and is left for the next refresh. +/// Other branches are updated directly. +/// Branches without a remote-tracking counterpart are left alone. +/// Local commits of their own also keep a branch untouched. +/// Returns whether any branch moved. pub fn fast_forward_branches(workdir: &Path) -> Result { let current = git_in(workdir, &["branch", "--show-current"]).unwrap_or_default(); let heads = refs_with_prefix(workdir, "refs/heads")?; @@ -697,7 +704,7 @@ pub fn fast_forward_branches(workdir: &Path) -> Result { continue; }; let remote = format!("refs/remotes/origin/{branch}"); - // No remote-tracking counterpart: the remote does not have it. + // No remote-tracking counterpart means the remote lacks this branch. let Ok(remote_oid) = git_in(workdir, &["rev-parse", "--verify", "--quiet", &remote]) else { continue; }; @@ -707,8 +714,8 @@ pub fn fast_forward_branches(workdir: &Path) -> Result { if local_oid == remote_oid { continue; } - // Only fast-forward: local-only commits (or diverged history) must - // never be rewritten by a refresh. + // Only fast-forward. + // Local-only commits or diverged history must never be rewritten by a refresh. if git_in(workdir, &["merge-base", "--is-ancestor", &head, &remote]).is_err() { continue; } @@ -726,8 +733,8 @@ pub fn fast_forward_branches(workdir: &Path) -> Result { Ok(moved) } -/// Run a git command in `dir`, returning trimmed stdout. The terminal prompt -/// is disabled so a credential request fails instead of hanging. +/// Run a git command in `dir`, returning trimmed stdout. +/// The terminal prompt is disabled so a credential request fails instead of hanging. fn git_in(dir: &Path, args: &[&str]) -> Result { let output = Command::new("git") .arg("-C") @@ -749,9 +756,9 @@ fn git_in(dir: &Path, args: &[&str]) -> Result { Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) } -/// Map an untrusted repository id (or display name) to a safe single path -/// component: everything outside `[A-Za-z0-9._-]` becomes `_`, and the -/// special components `.` and `..` are rejected. +/// Map an untrusted repository id or display name to a safe single path component. +/// Everything outside `[A-Za-z0-9._-]` becomes `_`. +/// An id that maps to exactly `.` or `..` becomes `_`. pub fn sanitize_path_component(id: &str) -> String { let sanitized: String = id .chars() @@ -771,20 +778,19 @@ pub fn sanitize_path_component(id: &str) -> String { sanitized } -/// In-memory object cache for history walks (see [`open_with_cache`]). -/// Without one, every walk re-decodes the same commit objects from the -/// object database. +/// In-memory object cache for history walks, see [`open_with_cache`]. +/// Without one, every walk re-decodes the same commit objects from the object database. const OBJECT_CACHE_BYTES: usize = 64 * 1024 * 1024; /// Metadata of a commit, as shown in the repository browser's file header. #[derive(Debug, Clone)] pub struct FileCommit { - /// Shortened commit id (7+ hex chars, disambiguated if needed). + /// Shortened commit id, 7+ hex chars, disambiguated if needed. pub id: String, /// First line of the commit message. pub summary: String, - /// Rest of the commit message after the title; `None` when there is no - /// body (single-line commit messages). + /// Rest of the commit message after the title. + /// `None` for single-line commit messages. pub description: Option, /// Author name. pub author: String, @@ -792,9 +798,9 @@ pub struct FileCommit { pub time: i64, } -/// Relative paths of all entries in the worktree (files and directories), -/// directories first, then alphabetically within each group. The `.git` -/// directory is skipped. +/// Relative paths of all entries in the worktree, files and directories. +/// Directories first, then alphabetically within each group. +/// The `.git` directory is skipped. pub fn worktree_entries(repo: &gix::Repository) -> Result> { let workdir = repo.workdir().context("repository has no worktree")?; @@ -809,8 +815,8 @@ pub fn worktree_entries(repo: &gix::Repository) -> Result> { Ok(entries.into_iter().map(|(path, _)| path).collect()) } -/// Read a file from the worktree. Returns `Ok(None)` if the path is missing -/// or not a regular file. +/// Read a file from the worktree. +/// Returns `Ok(None)` if the path is missing or not a regular file. pub fn worktree_read(repo: &gix::Repository, rel: &Path) -> Result>> { let workdir = repo.workdir().context("repository has no worktree")?; let path = workdir.join(rel); @@ -823,9 +829,11 @@ pub fn worktree_read(repo: &gix::Repository, rel: &Path) -> Result Result> { let Some(workdir) = repo.workdir() else { return Ok(None); @@ -861,18 +869,17 @@ pub fn find_readme(repo: &gix::Repository) -> Result> { .and_then(|path| path.strip_prefix(workdir).ok().map(Path::to_path_buf))) } -/// Open the repository at `workdir` with an in-memory object cache sized for -/// history walks. +/// Open the repository at `workdir` with an in-memory object cache sized for history walks. fn open_with_cache(workdir: &Path) -> Result { let mut repo = gix::open(workdir)?; repo.object_cache_size_if_unset(OBJECT_CACHE_BYTES); Ok(repo) } -/// A [`FileCommit`] from a walk commit: author, message title and shortened -/// id. `include_description` controls whether the message body is copied; -/// history lists never display it, so skipping it saves a string allocation -/// per listed commit (the diff panel fetches the full commit on demand). +/// A [`FileCommit`] from a walk commit, with author, message title and shortened id. +/// `include_description` controls whether the message body is copied. +/// History lists never display it, so skipping it saves an allocation per listed commit. +/// The diff panel fetches the full commit on demand. fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result { let author = commit.author()?; let message = commit.message()?; @@ -892,10 +899,11 @@ fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result`: the first commit, walking from -/// `HEAD` newest-first, whose tree entry for `rel` differs from its first -/// parent's. `Ok(None)` when no commit touched the file (e.g. untracked). +/// Find the most recent commit that changed `rel`, a path relative to the worktree. +/// Like `git log -1 -- `. +/// Walks newest-first from `HEAD`. +/// Reports the first commit whose tree entry for `rel` differs from its first parent's. +/// `Ok(None)` when no commit touched the file, e.g. an untracked file. pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result> { let rel = rel.to_path_buf(); Ok(last_commits(repo, std::slice::from_ref(&rel))? @@ -904,10 +912,10 @@ pub fn last_commit(repo: &gix::Repository, rel: &Path) -> Result` per path, found in a single history walk: every -/// commit is decoded once and shared across all paths. Paths without any -/// commit (e.g. untracked files) are absent from the result. +/// Newest commit touching each of `rels`, like `git log -1 -- ` per path. +/// `rels` are paths relative to the worktree. +/// A single walk decodes every commit once and shares it across all paths. +/// Paths without any commit, like untracked files, are absent from the result. pub fn worktree_last_commits( workdir: &Path, rels: &[PathBuf], @@ -915,8 +923,8 @@ pub fn worktree_last_commits( last_commits(&open_with_cache(workdir)?, rels) } -/// The walk behind [`last_commit`] and [`worktree_last_commits`], stopping as -/// soon as every pending path has its commit. +/// The walk behind [`last_commit`] and [`worktree_last_commits`]. +/// Stops as soon as every pending path has its commit. fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result> { use gix::traverse::commit::simple::CommitTimeOrder; @@ -952,8 +960,8 @@ fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result None, }; - // Compare each still-unresolved path against this commit and its - // first parent; resolved paths leave the pending set. + // Compare each unresolved path against this commit and its first parent. + // Resolved paths leave the pending set. let mut ix = 0; while ix < pending.len() { let rel = &pending[ix]; @@ -976,14 +984,14 @@ fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result Result { Ok(CommitList { total, commits }) } -/// Like [`all_commits`], but opens the repository located at `workdir` -/// (for non-bare clones the clone root is the worktree) first. +/// Like [`all_commits`], but opens the repository at `workdir` first. +/// For non-bare clones the clone root is the worktree. pub fn worktree_all_commits(workdir: &Path) -> Result { all_commits(&open_with_cache(workdir)?) } @@ -1049,8 +1057,8 @@ pub struct DiffLine { pub text: String, } -/// A hunk of a file diff, like `@@ -a,b +c,d @@`, with the lines between the -/// two headers (context around the change, then removals and additions). +/// A hunk of a file diff, like `@@ -a,b +c,d @@`. +/// Context around each change, then removals and additions. #[derive(Debug, Clone)] pub struct DiffHunk { /// 1-based start line in the old version. @@ -1077,31 +1085,32 @@ pub enum DiffStatus { /// The diff of one file in a commit. #[derive(Debug, Clone)] pub struct FileDiff { - /// Path of the file relative to the repo root (the destination path for - /// renames and copies). + /// Path of the file relative to the repo root. + /// For renames and copies, this is the destination path. pub path: String, /// Previous path, for renames and copies. pub old_path: Option, pub status: DiffStatus, - /// Number of added lines; 0 for binary files. + /// Number of added lines, 0 for binary files. pub insertions: usize, - /// Number of removed lines; 0 for binary files. + /// Number of removed lines, 0 for binary files. pub deletions: usize, - /// True if either version is binary (then `hunks` is empty). + /// True if either version is binary, then `hunks` is empty. pub binary: bool, pub hunks: Vec, } -/// The changes of one commit: every file it added, modified, deleted or -/// renamed, with line-level hunks for text files. +/// The changes of one commit. +/// Lists every file it added, modified, deleted or renamed. +/// Text files carry line-level hunks. #[derive(Debug, Clone)] pub struct CommitDiff { pub files: Vec, } -/// The changes of the commit `id` (short or full) in the repository at -/// `workdir`, compared against its first parent (the empty tree for the -/// root commit), like `git show`. Files are sorted by path. +/// The changes of the commit `id`, short or full, in the repository at `workdir`. +/// Compared against its first parent, the empty tree for the root commit. +/// Like `git show`, files are sorted by path. pub fn worktree_commit_diff(workdir: &Path, id: &str) -> Result { commit_diff(&open_with_cache(workdir)?, id) } @@ -1117,9 +1126,9 @@ fn commit_diff(repo: &gix::Repository, id: &str) -> Result { tree_diff(repo, old_tree.as_ref(), &new_tree) } -/// The changes between two commits (`base`..`tip`), like `git diff base tip`. -/// Same file handling as [`worktree_commit_diff`] (directories and -/// submodules are skipped, files are sorted by path). +/// The changes between two commits, `base`..`tip`, like `git diff base tip`. +/// Same file handling as [`worktree_commit_diff`]. +/// Directories and submodules are skipped, files are sorted by path. pub fn worktree_commit_range_diff(workdir: &Path, base: &str, tip: &str) -> Result { let repo = open_with_cache(workdir)?; let base_tree = repo @@ -1161,8 +1170,8 @@ pub fn worktree_commit_range_commits( Ok(commits) } -/// The changes between two trees, used by both [`commit_diff`] and -/// [`worktree_commit_range_diff`]. +/// The changes between two trees. +/// Used by both [`commit_diff`] and [`worktree_commit_range_diff`]. fn tree_diff( repo: &gix::Repository, old_tree: Option<&gix::Tree<'_>>, @@ -1179,8 +1188,7 @@ fn tree_diff( for change in changes { let attached = Change::from_change_ref(change.to_ref(), repo, repo); - // The tree diff also reports directory entries; only their contents - // are listed, so skip trees and submodule gitlinks. + // Skip directory trees and submodule gitlinks, only files are listed. let (path, old_path, status) = match attached { Change::Addition { location, @@ -1236,8 +1244,8 @@ fn tree_diff( _ => continue, }; - // Always diff with the built-in algorithm: external diff drivers - // would shell out, which is out of scope for a read-only viewer. + // Always diff with the built-in algorithm. + // External diff drivers would shell out, out of scope for a read-only viewer. let platform = attached.diff(&mut cache)?; platform .resource_cache @@ -1281,14 +1289,13 @@ fn tree_diff( Ok(CommitDiff { files }) } -/// Parse a `git format-patch` output (a single patch or a patch series) -/// into the same [`CommitDiff`] structure used for commit diffs. -/// -/// The mbox envelope (From/Subject/... headers, commit body and diffstat) -/// is skipped; every `diff --git` section becomes one [`FileDiff`]. Paths -/// are taken from the section headers, with git's C-style quoting undone. -/// Sections without hunks (pure renames, mode changes, binary files) are -/// reported without lines. +/// Parse `git format-patch` output, a single patch or a series. +/// Produces the same [`CommitDiff`] structure used for commit diffs. +/// The mbox envelope is skipped, the From and Subject headers, commit body and diffstat. +/// Every `diff --git` section becomes one [`FileDiff`]. +/// Paths come from the section headers, with git's C-style quoting undone. +/// Sections without hunks are reported without lines. +/// That covers pure renames, mode changes and binary files. pub fn patch_diffs(patch: &str) -> Result { let lines: Vec<&str> = patch.lines().collect(); let mut files = Vec::new(); @@ -1307,10 +1314,10 @@ pub fn patch_diffs(patch: &str) -> Result { Ok(CommitDiff { files }) } -/// Commits of a `git format-patch` output (a single patch or a patch -/// series), parsed from the mbox envelope headers of each patch: commit id, -/// author, summary and author time. Entries appear in patch order (oldest -/// first, as produced by `git format-patch`). +/// Commits of a `git format-patch` output, a single patch or a series. +/// Parsed from each patch's mbox envelope headers. +/// Yields the commit id, author, summary and author time. +/// Entries appear in patch order, oldest first as `git format-patch` produces them. pub fn patch_commits(patch: &str) -> Vec { let lines: Vec<&str> = patch.lines().collect(); let mut commits = Vec::new(); @@ -1335,8 +1342,7 @@ pub fn patch_commits(patch: &str) -> Vec { let mut summary = String::new(); let mut time = 0i64; - // Envelope headers of this patch, up to the blank line separating - // them from the commit message. + // Envelope headers run up to the blank line before the commit message. i += 1; while i < lines.len() && !lines[i].is_empty() { let header = lines[i]; @@ -1372,8 +1378,8 @@ fn name_from_address(from: &str) -> String { } } -/// Strip the `[PATCH]`, `[PATCH 1/2]`, `[RFC PATCH]` ... prefix from a patch -/// `Subject:` header. +/// Strip the patch prefix from a `Subject:` header. +/// Examples are `[PATCH]`, `[PATCH 1/2]` and `[RFC PATCH]`. fn strip_patch_prefix(subject: &str) -> String { let trimmed = subject.trim(); let Some(rest) = trimmed.strip_prefix('[') else { @@ -1389,14 +1395,14 @@ fn strip_patch_prefix(subject: &str) -> String { } } -/// Parse one file's diff section: everything after its `diff --git` header -/// up to the next section (or the end of the patch). Returns the section -/// and the index of the first unconsumed line. +/// Parse one file's diff section. +/// Everything after the `diff --git` header up to the next section or the end of the patch. +/// Returns the section and the index of the first unconsumed line. fn parse_diff_section(header: &str, lines: &[&str], start: usize) -> Result<(FileDiff, usize)> { let (header_old, header_new) = header_paths(header)?; - // The `---`/`+++` lines name the two sides unambiguously (the header - // can't distinguish spaces); fall back to the header for sections - // without them (pure renames, mode changes). + // The `---` and `+++` lines name the two sides unambiguously. + // The `diff --git` header cannot distinguish spaces in paths. + // Fall back to the header for sections without them, pure renames and mode changes. let mut old_path = header_old; let mut new_path = header_new; @@ -1452,14 +1458,14 @@ fn parse_diff_section(header: &str, lines: &[&str], start: usize) -> Result<(Fil status = DiffStatus::Renamed; } else if line.starts_with("Binary files ") || line.starts_with("GIT binary patch") { binary = true; - // A literal binary patch may follow; skip it without consuming - // the next section's header. + // A literal binary patch may follow. + // Skip it without consuming the next section's header. while i < lines.len() && !lines[i].starts_with("diff --git ") { i += 1; } break; } - // Everything else (index/mode/similarity lines) is ignored. + // Everything else, index, mode and similarity lines, is ignored. } Ok(( @@ -1477,9 +1483,9 @@ fn parse_diff_section(header: &str, lines: &[&str], start: usize) -> Result<(Fil )) } -/// Parse one hunk: the `@@ -a,b +c,d @@` header plus every body line up to -/// the next hunk header, the next `diff --git` section or the end of the -/// patch. Returns the hunk and the index of the first unconsumed line. +/// Parse one hunk, the `@@ -a,b +c,d @@` header plus every body line. +/// Lines end at the next hunk header, `diff --git` section or the end of the patch. +/// Returns the hunk and the index of the first unconsumed line. fn parse_hunk(lines: &[&str], start: usize) -> Result<(DiffHunk, usize)> { let (old_start, old_lines, new_start, new_lines) = hunk_header(lines[start])?; @@ -1495,9 +1501,9 @@ fn parse_hunk(lines: &[&str], start: usize) -> Result<(DiffHunk, usize)> { }; i += 1; - // Context lines advance both counters, deletions only the old one - // and additions only the new one, so every line ends up with its - // real line number in both versions. + // Context lines advance both counters. + // Deletions advance only the old counter, additions only the new one. + // Every line then carries its real number in both versions. let (old_no, new_no) = match kind { DiffLineKind::Context => { let numbers = (Some(old), Some(new)); @@ -1536,9 +1542,8 @@ fn parse_hunk(lines: &[&str], start: usize) -> Result<(DiffHunk, usize)> { )) } -/// The kind of a hunk body line, from its first character; lines that don't -/// belong to the hunk (headers, `\ No newline...`, the next section) yield -/// `None`. +/// The kind of a hunk body line, from its first character. +/// Lines outside a hunk, headers, `\ No newline...` and the next section, yield `None`. fn line_prefix_kind(line: &str) -> Option { match line.as_bytes().first()? { b' ' => Some(DiffLineKind::Context), @@ -1548,8 +1553,8 @@ fn line_prefix_kind(line: &str) -> Option { } } -/// Parse a unified-diff hunk header `@@ -a,b +c,d @@`; omitted line counts -/// default to 1. +/// Parse a unified-diff hunk header, `@@ -a,b +c,d @@`. +/// Omitted line counts default to 1. fn hunk_header(header: &str) -> Result<(u32, u32, u32, u32)> { let rest = header .strip_prefix("@@ ") @@ -1571,12 +1576,10 @@ fn hunk_header(header: &str) -> Result<(u32, u32, u32, u32)> { Ok((old_start, old_lines, new_start, new_lines)) } -/// The old and new paths of a `diff --git a/X b/Y` header, with git's -/// C-style quoting undone. -/// -/// Git only quotes paths containing characters that need escaping (non-ASCII -/// bytes, `"`, `\`); plain spaces are left unquoted, so the two sides of an -/// unquoted header are split at the last ` b/`. +/// The old and new paths of a `diff --git a/X b/Y` header. +/// Git's C-style quoting is undone. +/// Git only quotes paths that need escaping, non-ASCII bytes, `"` and `\`. +/// Plain spaces stay unquoted, so an unquoted header splits at the last ` b/`. fn header_paths(header: &str) -> Result<(String, String)> { if header.starts_with('"') { // Quoted paths include the `a/` / `b/` prefix inside the quotes. @@ -1605,10 +1608,10 @@ fn header_paths(header: &str) -> Result<(String, String)> { } } -/// The path of a `--- a/X` / `+++ b/Y` line: the prefix stripped, git's -/// trailing padding tab (for paths containing spaces) removed and C-style -/// quoting undone. These lines name the two sides unambiguously, unlike the -/// `diff --git` header. +/// The path of a `--- a/X` or `+++ b/Y` line. +/// The prefix is stripped, the trailing tab removed, C-style quoting undone. +/// Git adds a trailing padding tab for paths containing spaces. +/// These lines name the two sides unambiguously, unlike the `diff --git` header. fn diff_line_path(line: &str, prefix: &str) -> Result { let line = line.trim_end_matches('\t'); if line.starts_with('"') { @@ -1625,11 +1628,10 @@ fn diff_line_path(line: &str, prefix: &str) -> Result { } } -/// The content of a git C-style quoted path (opening `"`, escaped content, -/// closing `"`) and the rest of the input; `None` if unterminated. -/// -/// Iterates by character so the returned slices always land on UTF-8 -/// boundaries, even for non-ASCII paths. +/// The content of a git C-style quoted path and the rest of the input. +/// The path spans the opening `"`, escaped content and closing `"`. +/// `None` if unterminated. +/// Iterates by character so slices land on UTF-8 boundaries even for non-ASCII paths. fn take_quoted(input: &str) -> Option<(&str, &str)> { let mut end = 1; // byte after the opening quote let mut rest = &input[1..]; @@ -1637,7 +1639,7 @@ fn take_quoted(input: &str) -> Option<(&str, &str)> { let len = ch.len_utf8(); match ch { '\\' => { - // Consume the escaped character too (it may be multi-byte). + // Consume the escaped character too, it may be multi-byte. let escaped = rest[len..].chars().next()?; let consumed = len + escaped.len_utf8(); end += consumed; @@ -1653,7 +1655,7 @@ fn take_quoted(input: &str) -> Option<(&str, &str)> { None } -/// Undo git's C-style path quoting (`\NNN` octal escapes, `\"`, `\\`). +/// Undo git's C-style path quoting, `\NNN` octal escapes, `\"` and `\\`. fn unquote_path(path: &str) -> Result { if !path.contains('\\') { return Ok(path.to_owned()); @@ -1706,11 +1708,10 @@ fn unquote_path(path: &str) -> Result { } /// Collects the hunks of one blob diff while tracking per-line numbers. -/// -/// The unified-diff headers give the 1-based start line of the hunk in each -/// file; context lines advance both counters, removals only the old one and -/// additions only the new one, so each line ends up with its real line -/// numbers in both versions. +/// The unified-diff headers give the 1-based start line of the hunk in each file. +/// Context lines advance both counters. +/// Removals advance only the old counter, additions only the new one. +/// Each line then carries its real numbers in both versions. struct HunkCollector<'a> { hunks: &'a mut Vec, insertions: &'a mut usize, @@ -1782,8 +1783,8 @@ impl ConsumeHunk for HunkCollector<'_> { fn finish(self) {} } -/// The commit HEAD points to, like `git log -1`. Returns `Ok(None)` for a -/// repository without commits yet (unborn HEAD). +/// The commit HEAD points to, like `git log -1`. +/// `Ok(None)` for a repository without commits yet, an unborn HEAD. pub fn head_commit(repo: &gix::Repository) -> Result> { let Some(head) = repo.head_id().ok() else { return Ok(None); @@ -1792,12 +1793,11 @@ pub fn head_commit(repo: &gix::Repository) -> Result> { Ok(Some(file_commit(&commit, true)?)) } -/// Full metadata of the commit `id` (short or full) in the repository at -/// `workdir`, like [`head_commit`] for an arbitrary commit. Returns +/// Full metadata of the commit `id`, short or full, in the repository at `workdir`. +/// Like [`head_commit`] for an arbitrary commit. /// `Ok(None)` when the id cannot be resolved. -/// -/// The commit list ([`all_commits`]) omits message bodies to keep the walk -/// cheap; the diff panel uses this to fetch the full commit on demand. +/// The commit list, [`all_commits`], omits message bodies to keep the walk cheap. +/// The diff panel uses this to fetch the full commit on demand. pub fn worktree_commit(workdir: &Path, id: &str) -> Result> { let repo = open_with_cache(workdir)?; match repo.rev_parse_single(id.as_bytes()) { @@ -1809,8 +1809,7 @@ pub fn worktree_commit(workdir: &Path, id: &str) -> Result> { } } -/// Short names of local branches (`refs/heads/*`) of `repo`, sorted -/// alphabetically. +/// Short names of local branches, `refs/heads/*`, of `repo`, sorted alphabetically. pub fn repo_branches(repo: &gix::Repository) -> Result> { let mut names = Vec::new(); for reference in repo.references()?.local_branches()? { @@ -1821,7 +1820,7 @@ pub fn repo_branches(repo: &gix::Repository) -> Result> { Ok(names) } -/// Short names of tags (`refs/tags/*`) of `repo`, sorted alphabetically. +/// Short names of tags, `refs/tags/*`, of `repo`, sorted alphabetically. pub fn repo_tags(repo: &gix::Repository) -> Result> { let mut names = Vec::new(); for reference in repo.references()?.tags()? { @@ -1832,18 +1831,18 @@ pub fn repo_tags(repo: &gix::Repository) -> Result> { Ok(names) } -/// Short names of local branches (`refs/heads/*`), sorted alphabetically. +/// Short names of local branches, `refs/heads/*`, sorted alphabetically. pub fn worktree_branches(workdir: &Path) -> Result> { repo_branches(&open_with_cache(workdir)?) } -/// Short names of tags (`refs/tags/*`), sorted alphabetically. +/// Short names of tags, `refs/tags/*`, sorted alphabetically. pub fn worktree_tags(workdir: &Path) -> Result> { repo_tags(&open_with_cache(workdir)?) } -/// Short name of the branch HEAD points to, or `None` when detached (e.g. -/// after checking out a tag or a commit directly). +/// Short name of the branch HEAD points to, or `None` when detached. +/// Detached after checking out a tag or a commit directly. pub fn current_branch(repo: &gix::Repository) -> Result> { let head = repo.head()?; let Some(name) = head.referent_name() else { @@ -1852,8 +1851,8 @@ pub fn current_branch(repo: &gix::Repository) -> Result> { Ok(Some(String::from_utf8_lossy(name.shorten()).into_owned())) } -/// Branch, tag and HEAD refs of a repository, ready for a NIP-34 kind-30618 -/// repository state announcement. +/// Branch, tag and HEAD refs of a repository. +/// Ready for a NIP-34 kind-30618 repository state announcement. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RepoRefState { /// `(full refname, commit id)` pairs for heads and tags, sorted. @@ -1862,8 +1861,9 @@ pub struct RepoRefState { pub head: Option, } -/// Collect the refs of `repo`: local branches and tags as -/// `(refname, commit-id)` pairs, plus the branch HEAD points to. +/// Collect the refs of `repo`. +/// Local branches and tags become `(refname, commit-id)` pairs. +/// Also reports the branch HEAD points to. pub fn repo_ref_state(repo: &gix::Repository) -> Result { let mut refs = Vec::new(); @@ -1907,14 +1907,15 @@ pub struct WorktreeSnapshot { pub readme_path: Option, /// Contents of the README, if any. pub readme: Option>, - /// Branch HEAD points to (`None` when detached, e.g. on a tag). + /// Branch HEAD points to, `None` when detached, for example on a tag. pub current_branch: Option, - /// Commit HEAD points to, if any (see [`head_commit`]). + /// Commit HEAD points to, if any, see [`head_commit`]. pub head_commit: Option, } -/// Snapshot the worktree after a branch/tag switch: entries, README, the -/// branch HEAD points to and its commit, opening the repository once. +/// Snapshot the worktree after a branch or tag switch. +/// Collects entries, the README, the branch HEAD points to and its commit. +/// Opens the repository once. pub fn worktree_snapshot(workdir: &Path) -> Result { let repo = open_with_cache(workdir)?; let readme_path = find_readme(&repo)?; @@ -1931,9 +1932,8 @@ pub fn worktree_snapshot(workdir: &Path) -> Result { }) } -/// Switch the checked-out ref and update the worktree to match, like -/// `git checkout --force`. Local modifications are discarded since these -/// clones are read-only browser copies. +/// Switch the checked-out ref and update the worktree, like `git checkout --force`. +/// Local modifications are discarded, these clones are read-only browser copies. fn checkout(workdir: &Path, args: &[&str]) -> Result<()> { let output = Command::new("git") .arg("checkout") @@ -1952,15 +1952,15 @@ fn checkout(workdir: &Path, args: &[&str]) -> Result<()> { Ok(()) } -/// Check out the local branch `name`; HEAD stays attached to it. +/// Check out the local branch `name`, HEAD stays attached to it. pub fn worktree_checkout_branch(workdir: &Path, name: &str) -> Result<()> { - // The short name (not `refs/heads/`) keeps HEAD attached; the - // full ref name would be treated as a commit-ish and detach it. + // The short name, not `refs/heads/`, keeps HEAD attached. + // The full ref name would be treated as a commit-ish and detach it. checkout(workdir, &[name]) } -/// Check out the tag `name`; HEAD becomes detached at the tagged commit, -/// which [`current_branch`] reports as `None`. +/// Check out the tag `name`, HEAD becomes detached at the tagged commit. +/// [`current_branch`] reports this as `None`. pub fn worktree_checkout_tag(workdir: &Path, name: &str) -> Result<()> { // `--detach` pins the full tag ref so HEAD always ends up detached. checkout(workdir, &["--detach", &format!("refs/tags/{name}")]) @@ -2034,8 +2034,8 @@ mod tests { let temp = tempfile::tempdir().unwrap(); let root = temp.path(); - // Repositories are found at any depth; a linked worktree (a `.git` - // file instead of a directory) counts too. + // Repositories are found at any depth. + // A linked worktree, with a `.git` file instead of a directory, counts too. let nested = root.join("a/b/project"); std::fs::create_dir_all(nested.join(".git")).unwrap(); let worktree = root.join("wt"); @@ -2053,8 +2053,8 @@ mod tests { std::fs::create_dir_all(root.join(".hidden/repo/.git")).unwrap(); std::fs::create_dir_all(root.join("node_modules/pkg/.git")).unwrap(); - // A repository is not descended into, so repositories inside it - // (submodule worktrees) are not reported. + // A repository is not descended into. + // Repositories inside it, like submodule worktrees, are not reported. let outer = root.join("outer"); std::fs::create_dir_all(outer.join(".git")).unwrap(); std::fs::create_dir_all(outer.join("sub/other/.git")).unwrap(); @@ -2098,8 +2098,8 @@ mod tests { #[test] fn push_all_mirrors_branches_and_tags() { - // A bare "server" repository reachable via a `file://` URL, like a - // grasp server's `{base}/{owner}/{repo-id}.git` layout. + // A bare server repository reachable via a `file://` URL. + // Mirrors a grasp server's `{base}/{owner}/{repo-id}.git` layout. let server = tempfile::tempdir().unwrap(); let server_repo = server.path().join("npub1test").join("my-repo.git"); std::fs::create_dir_all(server_repo.parent().unwrap()).unwrap(); @@ -2132,8 +2132,8 @@ mod tests { #[test] fn push_all_tolerates_a_missing_ref_kind() { - // A repository with only tags (no branches) still pushes: wildcard - // refspecs without a local match are ignored. + // A repository with only tags and no branches still pushes. + // Wildcard refspecs without a local match are ignored. let server = tempfile::tempdir().unwrap(); let server_repo = server.path().join("npub1test").join("my-repo.git"); std::fs::create_dir_all(server_repo.parent().unwrap()).unwrap(); @@ -2199,7 +2199,7 @@ mod tests { assert_eq!(state.refs.len(), 3); } - /// Build a throwaway non-bare repository with the given files (rel → bytes). + /// Build a throwaway non-bare repository from `(rel, bytes)` file pairs. fn fixture(files: &[(&str, &[u8])]) -> (tempfile::TempDir, gix::Repository) { let dir = tempfile::tempdir().expect("tempdir"); let repo = gix::init(&dir).expect("init"); @@ -2259,8 +2259,8 @@ mod tests { ); } - /// Stage everything and create a commit with the git CLI (like - /// [`apply_patch`], the crate already shells out to the CLI). + /// Stage everything and create a commit with the git CLI. + /// Like [`apply_patch`], the crate already shells out to the CLI. fn commit_all(repo: &gix::Repository, message: &str) { git_run(repo.workdir().expect("workdir"), &["add", "-A"]); git_run(repo.workdir().expect("workdir"), &["commit", "-m", message]); @@ -2272,8 +2272,8 @@ mod tests { let path = dir.path().join("repo"); let initial = init_repository(&path, "My Repo", "desc").expect("init"); - // A feature branch and a mainline commit diverge from the initial - // commit; it is their merge base. + // A feature branch and a mainline commit diverge from the initial commit. + // The initial commit is their merge base. git_run(&path, &["checkout", "-b", "feature"]); std::fs::write(path.join("feature.txt"), "feature\n").expect("write"); commit_all(&gix::open(&path).expect("open"), "feature commit"); @@ -2288,7 +2288,7 @@ mod tests { Some(initial.as_str()) ); - // An orphan branch shares no history with main: `Ok(None)`. + // An orphan branch shares no history with main, so `Ok(None)`. git_run(&path, &["checkout", "--orphan", "orphan"]); std::fs::write(path.join("orphan.txt"), "orphan\n").expect("write"); commit_all(&gix::open(&path).expect("open"), "orphan commit"); @@ -2327,7 +2327,7 @@ mod tests { commit_all(&gix::open(&path).expect("open"), "feature commit"); let patch = format_patch_between(&path, &initial, "feature").expect("patch"); - // A clone of the initial state accepts the series... + // A clone of the initial state accepts the series. let clone = dir.path().join("clone"); git_run( dir.path(), @@ -2340,7 +2340,7 @@ mod tests { ); git_run(&clone, &["checkout", "-q", &initial]); assert!(patch_applies(&clone, &patch).is_ok()); - // ...and the check must not have modified the working tree. + // The check must not have modified the working tree. assert!(!clone.join("feature.txt").exists()); // A conflicting file makes the same series fail the check. @@ -2350,8 +2350,8 @@ mod tests { #[test] fn push_commit_ref_pushes_to_the_event_namespace() { - // A bare "server" repository reachable via a `file://` URL, like a - // grasp server's `{base}/{owner}/{repo-id}.git` layout. + // A bare server repository reachable via a `file://` URL. + // Mirrors a grasp server's `{base}/{owner}/{repo-id}.git` layout. let server = tempfile::tempdir().unwrap(); let server_repo = server.path().join("npub1test").join("my-repo.git"); std::fs::create_dir_all(server_repo.parent().unwrap()).unwrap(); @@ -2417,7 +2417,7 @@ mod tests { head_commit_id(&path).expect("head").as_deref(), Some(initial.as_str()) ); - // No commits yet: `HEAD` alone. + // No commits yet, `HEAD` alone. assert_eq!( commits_since(&path, None).expect("commits"), vec![initial.clone()] @@ -2478,7 +2478,7 @@ mod tests { let branch = current_branch(&repo).expect("branch").expect("on a branch"); assert_eq!(branch, "main"); - // [`FileCommit`] carries the short id; the full id is 40 chars. + // [`FileCommit`] carries the short id, the full id is 40 chars. assert_eq!( head_commit(&repo).expect("head").expect("commit").id, &commit[..7] @@ -2515,8 +2515,8 @@ mod tests { git_in(&path, &["remote", "get-url", "origin"]).expect("url"), "https://gitnostr.com/npub1test/repo.git" ); - // The standard fetch mapping is configured with the remote, so a - // later `git fetch origin` updates `refs/remotes/origin/*`. + // The standard fetch mapping is configured with the remote. + // Later `git fetch origin` updates `refs/remotes/origin/*`. assert_eq!( git_in(&path, &["config", "remote.origin.fetch"]).expect("refspec"), "+refs/heads/*:refs/remotes/origin/*" @@ -2552,16 +2552,16 @@ mod tests { let path = dir.path().join("my-repo"); init_repository(&path, "My Repo", "").expect("init"); - // No origin yet: added. + // No origin yet, so one is added. set_origin(&path, "https://gitnostr.com/npub1test/repo.git").expect("add"); assert_eq!( origin_url(&path).expect("url").as_deref(), Some("https://gitnostr.com/npub1test/repo.git") ); - // An existing origin is replaced, not duplicated (a clone's origin - // points at the cloned-from path; it is re-targeted at the grasp - // server). + // An existing origin is replaced, not duplicated. + // A clone's origin points at the cloned-from path. + // It is re-targeted at the grasp server. set_origin(&path, "https://grasp.example/npub1test/repo.git").expect("replace"); assert_eq!( origin_url(&path).expect("url").as_deref(), @@ -2571,17 +2571,17 @@ mod tests { #[test] fn working_copy_cloned_from_the_mirror_matches_head_and_origin() { - // The mirror: a freshly initialized repository whose `origin` - // points at the grasp server, like `Backend::create_repository` - // leaves it in the GitCache. + // The mirror is a freshly initialized repository. + // Its `origin` points at the grasp server. + // `Backend::create_repository` leaves it in the GitCache. let dir = tempfile::tempdir().expect("tempdir"); let mirror = dir.path().join("mirror"); let commit = init_repository(&mirror, "My Repo", "Does things.").expect("init"); ensure_origin(&mirror, "https://gitnostr.com/npub1test/my-repo.git").expect("origin"); - // The working copy: cloned from the mirror (so it shares the - // announced history exactly), then `origin` re-pointed at the grasp - // server instead of the mirror path. + // The working copy is cloned from the mirror. + // It then shares the announced history exactly. + // `origin` is re-pointed at the grasp server instead of the mirror path. let destination = dir.path().join("folder").join("My_Repo"); std::fs::create_dir_all(destination.parent().unwrap()).expect("parent"); clone_repo(&[format!("file://{}", mirror.display())], &destination).expect("clone"); @@ -2600,8 +2600,7 @@ mod tests { #[test] fn fast_forward_branches_moves_the_mirror_and_keeps_local_work() { - // A bare "server" like a grasp server's `{base}/{owner}/{repo}.git` - // layout. + // A bare server, like a grasp server's `{base}/{owner}/{repo}.git` layout. let dir = tempfile::tempdir().expect("tempdir"); let base_server = dir.path().join("npub1test").join("repo.git"); std::fs::create_dir_all(base_server.parent().unwrap()).unwrap(); @@ -2632,8 +2631,8 @@ mod tests { ); let initial = git_in(&mirror, &["rev-parse", "HEAD"]).expect("initial"); - // The owner pushes a new commit; the mirror fetches it but its - // local `main` (and worktree) stay behind. + // The owner pushes a new commit. + // The mirror fetches it, but its local `main` and worktree stay behind. std::fs::write(work.join("new.txt"), b"new\n").expect("write"); commit_all(&gix::open(work).expect("open"), "new commit"); push_all(work, &base_url, "npub1test", "repo").expect("push"); @@ -2645,8 +2644,8 @@ mod tests { ); assert_ne!(remote, initial); - // Fast-forwarding catches the branch and its worktree up; the - // second call has nothing left to move. + // Fast-forwarding catches the branch and its worktree up. + // The second call has nothing left to move. assert!(fast_forward_branches(&mirror).expect("ff")); assert_eq!( git_in(&mirror, &["rev-parse", "HEAD"]).expect("local"), @@ -2671,8 +2670,8 @@ mod tests { fn fetch_repo_refs_imports_heads_under_a_prefix() { let dir = tempfile::tempdir().expect("tempdir"); - // A bare "base" server holding the initial commit, like a grasp - // server's `{base}/{owner}/{repo-id}.git` layout. + // A bare base server holding the initial commit. + // Like a grasp server's `{base}/{owner}/{repo-id}.git` layout. let base_server = dir.path().join("npub1base").join("base.git"); std::fs::create_dir_all(base_server.parent().unwrap()).unwrap(); let init_status = Command::new("git") @@ -2694,7 +2693,7 @@ mod tests { ) .expect("push"); - // The base mirror: a plain clone of the base server. + // The base mirror is a plain clone of the base server. let base_url = format!("file://{}", base_server.display()); let mirror = dir.path().join("mirror"); git_run( @@ -2702,8 +2701,8 @@ mod tests { &["clone", "-q", &base_url, mirror.to_str().unwrap()], ); - // The fork server: the same initial commit plus a feature commit on - // its own `feature` branch. + // The fork server has the same initial commit. + // It also carries a feature commit on its own `feature` branch. let fork_work = dir.path().join("fork-work"); git_run( dir.path(), @@ -2730,8 +2729,8 @@ mod tests { ) .expect("push"); - // Import the fork's heads into the mirror under a private prefix; - // the first (dead) URL is skipped, the second works. + // Import the fork's heads into the mirror under a private prefix. + // The first dead URL is skipped, the second works. let dead = format!("file://{}/missing.git", dir.path().display()); fetch_repo_refs( &mirror, @@ -2751,8 +2750,8 @@ mod tests { Vec::::new() ); - // The mirror can now range across both histories: the fork point is - // the shared initial commit, and the proposal covers the fork commit. + // The mirror can now range across both histories. + // The fork point is the shared initial commit, the proposal covers the fork commit. assert_eq!( merge_base( &mirror, @@ -2898,7 +2897,7 @@ mod tests { std::fs::write(dir.path().join("a.txt"), b"feature").expect("write"); commit_all(&repo, "feature change"); run(&["checkout", "-"]); - // --no-ff forces a merge commit; it is the latest commit changing a.txt. + // `--no-ff` forces a merge commit, it is the latest commit changing a.txt. run(&["merge", "--no-ff", "--no-edit", "feature"]); let commit = last_commit(&repo, Path::new("a.txt")) @@ -2926,7 +2925,7 @@ mod tests { &[ PathBuf::from("a.txt"), PathBuf::from("b.txt"), - // Untracked paths are simply absent from the result. + // Untracked paths are absent from the result. PathBuf::from("missing.txt"), ], ) @@ -2972,7 +2971,7 @@ mod tests { fn head_commit_reports_head() { let (_dir, repo) = fixture(&[("a.txt", b"one")]); - // Unborn HEAD: no commit yet. + // Unborn HEAD means no commit yet. assert!(head_commit(&repo).expect("head").is_none()); commit_all(&repo, "initial"); @@ -2995,8 +2994,8 @@ mod tests { git_run(dir, &["tag", "v0.9"]); git_run(dir, &["tag", "v1.0"]); - // The initial branch name depends on git configuration; only the - // branch we created is fixed. + // The initial branch name depends on git configuration. + // Only the branch we created is fixed. let branches = worktree_branches(dir).expect("branches"); assert_eq!(branches.len(), 2); assert!(branches.contains(&"feature".to_string())); @@ -3133,8 +3132,8 @@ mod tests { assert_eq!(modified.deletions, 1); assert!(!modified.binary); let lines = &modified.hunks[0].lines; - // One hunk with context around the single-line change: the removed - // line is old 2, the added line is new 2. + // One hunk with context around the single-line change. + // The removed line is old 2, the added line is new 2. assert!(lines.iter().any(|line| { line.kind == DiffLineKind::Deletion && line.old == Some(2) @@ -3231,7 +3230,7 @@ mod tests { let (dir, repo) = fixture(&[("a.txt", b"one\n")]); commit_all(&repo, "initial"); - // The root commit diffs against the empty tree: everything is added. + // The root commit diffs against the empty tree, everything is added. let head = repo.head_id().expect("head").shorten_or_id().to_string(); let diff = worktree_commit_diff(dir.path(), &head).expect("diff"); assert_eq!(diff.files.len(), 1); @@ -3293,7 +3292,7 @@ mod tests { .expect("file"); assert_eq!(file.status, DiffStatus::Renamed); assert_eq!(file.old_path.as_deref(), Some("old.txt")); - // A pure rename has no content change; the file is still listed. + // A pure rename has no content change, the file is still listed. assert!(file.hunks.is_empty()); assert_eq!(file.insertions, 0); assert_eq!(file.deletions, 0); @@ -3499,8 +3498,8 @@ Subject: [RFC PATCH v3 4/7] the real title #[test] fn patch_commits_handles_missing_headers() { - // A hand-written patch without author/date headers still lists a - // commit; time stays 0 and the author falls back to the raw value. + // A hand-written patch without author or date headers still lists a commit. + // Time stays 0 and the author stays empty. let patch = r#"From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001 Subject: [PATCH] plain @@ -3518,7 +3517,7 @@ Subject: [PATCH] plain fn patch_commits_ignores_non_patch_lines() { assert!(patch_commits("").is_empty()); assert!(patch_commits("just some text\nFrom 123\n").is_empty()); - // A diff-only body (no mbox envelope) has no commits. + // A diff-only body without an mbox envelope has no commits. let patch = "diff --git a/x b/x\n--- a/x\n+++ b/x\n"; assert!(patch_commits(patch).is_empty()); } @@ -3598,10 +3597,10 @@ index 123..456 100644 #[test] fn parses_real_format_patch_output() { - // Build a commit touching a mix of file kinds, then feed genuine - // `git format-patch` output through the parser: quoted paths (space - // in the name), octal-escaped paths (UTF-8 name), a rename-free - // modification, an addition and a binary deletion. + // Build a commit touching a mix of file kinds. + // Feed genuine `git format-patch` output through the parser. + // It covers quoted and octal-escaped paths. + // There are also a rename-free modification, an addition and a binary deletion. let (dir, repo) = fixture(&[ ("src/main.rs", b"fn main() {\n println!(\"one\");\n}\n"), ("my file.txt", b"hello\n"), @@ -3646,12 +3645,12 @@ index 123..456 100644 .unwrap_or_else(|| panic!("missing file {path:?}")) }; - // Space in the name: git quotes the path in the header. + // Space in the name makes git quote the path in the header. let file = by_path("my file.txt"); assert_eq!(file.status, DiffStatus::Modified); assert_eq!(file.insertions, 1); - // UTF-8 name: git emits the path as octal escapes. + // UTF-8 names are emitted as octal escapes. let file = by_path("\u{8bf4}\u{660e}.md"); assert_eq!(file.status, DiffStatus::Modified); assert_eq!(file.insertions, 1); @@ -3666,8 +3665,8 @@ index 123..456 100644 assert_eq!(file.status, DiffStatus::Added); assert_eq!(file.insertions, 1); - // Binary deletion: git emits no ---/+++ lines, only the mode and - // the "Binary files" marker. + // A binary deletion emits no `---` or `+++` lines. + // Only the mode line and the `Binary files` marker remain. let file = by_path("img.png"); assert_eq!(file.status, DiffStatus::Deleted); assert!(file.binary); diff --git a/crates/signed_nostr/src/backend.rs b/crates/signed_nostr/src/backend.rs index 2770f29..75cf19a 100644 --- a/crates/signed_nostr/src/backend.rs +++ b/crates/signed_nostr/src/backend.rs @@ -12,11 +12,10 @@ use nostr_sdk::prelude::*; use crate::signer::UniversalSigner; -/// Open (or create) the LMDB database at `db_path` and build a client -/// configured for Signed, together with a fresh signer. -/// +/// Open or create the LMDB database at `db_path`. +/// Build a Signed client and a fresh signer for it. /// The SDK manages its own internal tokio runtime. -/// the returned client can be driven by GPUI's executors. +/// The returned client can be driven by GPUI's executors. #[cfg(not(target_arch = "wasm32"))] pub async fn new_backend(db_path: impl AsRef) -> Result<(Client, UniversalSigner)> { let signer = UniversalSigner::new(Keys::generate()); @@ -26,7 +25,7 @@ pub async fn new_backend(db_path: impl AsRef) -> Result<(Client, Universal Ok(with_database(signer, database)) } -/// In-memory database on wasm (no LMDB available). +/// In-memory database on wasm, LMDB is unavailable there. #[cfg(target_arch = "wasm32")] pub fn new_backend() -> Result<(Client, UniversalSigner)> { let signer = UniversalSigner::new(Keys::generate()); diff --git a/crates/signed_nostr/src/signer.rs b/crates/signed_nostr/src/signer.rs index 9f8e592..d85c8e5 100644 --- a/crates/signed_nostr/src/signer.rs +++ b/crates/signed_nostr/src/signer.rs @@ -31,8 +31,9 @@ impl UniversalSignerError { } } -/// A type-erased signer whose inner signer can be swapped in-place -/// (e.g. after login/logout). All clones see the swap. +/// A type-erased signer whose inner signer can be swapped in-place. +/// Swaps happen after login or logout. +/// All clones see the swap. #[derive(Clone, Debug)] pub struct UniversalSigner { inner: Arc>>, diff --git a/crates/signed_nostr/src/update.rs b/crates/signed_nostr/src/update.rs index 1b3f1de..4b605bb 100644 --- a/crates/signed_nostr/src/update.rs +++ b/crates/signed_nostr/src/update.rs @@ -1,12 +1,11 @@ use nostr_sdk::prelude::*; -/// A lightweight "something changed" signal for the UI. -/// -/// Heavy data stays in the database; consumers re-query on receipt. +/// A lightweight change notification for the UI. +/// Heavy data stays in the database, consumers re-query on receipt. #[derive(Debug, Clone)] pub struct Update { pub kind: Kind, - /// First `a` tag value of the event, if any (e.g. the repository coordinate). + /// First `a` tag value of the event, if any, for example the repository coordinate. pub coordinate: Option, pub author: PublicKey, pub event_id: EventId, diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index 507b0b0..e38c111 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -18,8 +18,9 @@ use signed_nostr::{SignedAuthUrlHandler, UniversalSigner, Update}; use crate::git_store::GitStore; -/// Keyring entry holding the user credential (`nsec1...` or `bunker://...` -/// with an embedded `?master=` NIP-46 session key). +/// Keyring entry for the user credential. +/// It is an `nsec1...` key or a `bunker://...` URI. +/// The URI embeds a `?master=` NIP-46 session key. pub const USER_KEYRING: &str = "Signed Safe Storage"; /// Timeout for NIP-46 signer responses. pub const NOSTR_CONNECT_TIMEOUT: u64 = 60; @@ -32,34 +33,35 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [ "wss://profiles.nostr1.com", ]; -/// Relays used for indexing user's relay list (NIP-65). +/// Relays used to index the user's NIP-65 relay list. pub const INDEXER_RELAYS: [&str; 2] = ["wss://indexer.coracle.social", "wss://user.kindpag.es"]; -/// How long an identical fetch/sync request is suppressed after it started. -/// A second panel for the same repository (or the global and per-author -/// list stores at login) doesn't duplicate a sync that just ran; after the -/// window, re-fetching is allowed again so data stays fresh. +/// How long an identical fetch or sync request is suppressed after it started. +/// A second panel for the same repository does not duplicate a live sync. +/// The global and per-author list stores at login share this dedup. +/// After the window, re-fetching is allowed again so data stays fresh. const FETCH_DEDUP_WINDOW: Duration = Duration::from_secs(5 * 60); #[derive(Debug, Clone)] pub enum BackendEvent { /// User has no signer configured. SignerRequired, - /// The stored identity is NIP-49 encrypted (`ncryptsec1...`); a - /// passphrase is required to decrypt it before the session can resume. + /// The stored identity is NIP-49 encrypted, an `ncryptsec1...` key. + /// A passphrase is required to decrypt it before the session can resume. PassphraseRequired, - /// The signer has changed (login/logout/account switch). + /// The signer changed on login, logout or account switch. SignerChanged, /// Relay bootstrap finished. Connected, /// A new event was received from a relay and stored in the database. NostrUpdate(Update), - /// A negentropy sync completed; the database was updated directly, - /// so stores should re-query (no [`BackendEvent::NostrUpdate`] is fired - /// for synced events). + /// A negentropy sync completed. + /// The database was updated directly, so stores should re-query. + /// No [`BackendEvent::NostrUpdate`] is fired for synced events. Synced, - /// A negentropy sync is in flight. Stores may re-query to render - /// incrementally; UI can show `current`/`total` progress. + /// A negentropy sync is in flight. + /// Stores may re-query to render incrementally. + /// UI can show `current` and `total` progress. SyncProgress { /// Total events to process. total: u64, @@ -81,28 +83,29 @@ impl BackendEvent { } } -/// Global backend entity: owns the nostr client, the signer and the -/// notification pump. Stores subscribe to [`BackendEvent`] and re-query the -/// local database when relevant updates arrive. +/// The global backend entity. +/// Owns the nostr client, the signer and the notification pump. +/// Stores subscribe to [`BackendEvent`]. +/// They re-query the local database when relevant updates arrive. pub struct Backend { client: Client, signer: UniversalSigner, current_user: Option, connected: bool, sync_progress: Option<(u64, u64)>, - /// Whether the stored credential is NIP-49 encrypted and a passphrase - /// is still needed to resume the session. + /// True when the stored credential is NIP-49 encrypted. + /// A passphrase is still needed to resume the session. passphrase_required: bool, - /// Fingerprints of recently started fetches/syncs (relay + filter set), - /// so duplicate requests within [`FETCH_DEDUP_WINDOW`] collapse into - /// one. Entries are pruned lazily on the next request. + /// Fingerprints of recently started fetches and syncs, a relay plus filter set. + /// Duplicate requests within [`FETCH_DEDUP_WINDOW`] collapse into one. + /// Entries are pruned lazily on the next request. recent_fetches: HashMap, - /// Repositories a push (mirror or checkout) is currently in flight - /// for. Concurrent pushes of the same refs — two panels of the same - /// repository, or the banner push racing the header's Republish — make - /// the losing push fail server-side with a compare-and-swap rejection - /// ("cannot lock ref … is at … but expected …"), so pushes are - /// single-flight per repository. + /// Repositories with a push in flight, mirror or checkout based. + /// Concurrent pushes of the same refs make the losing push fail server-side. + /// The rejection is a compare-and-swap error from the server. + /// Two panels of the same repository can race. + /// The banner push can also race the header's Republish. + /// Pushes are single-flight per repository. pushing_repos: Arc>>, tasks: Vec>>, } @@ -111,9 +114,8 @@ struct GlobalBackend(Entity); impl Global for GlobalBackend {} -/// Removes its repository from the in-flight push set when dropped, so a -/// push task that is cancelled (e.g. its panel closed mid-push) can never -/// leave the repository locked for the rest of the session. +/// Removes its repository from the in-flight push set when dropped. +/// A push task cancelled by its panel closing cannot leave the repository locked. struct PushGuard { repos: Arc>>, addr: RepoAddr, @@ -179,8 +181,9 @@ impl Backend { this } - /// Bootstrap the client: connect to the default relays (indexers as - /// discovery-only) and restore the saved session, if any. + /// Bootstrap the client. + /// Connect to the default relays, with the indexers as discovery-only. + /// Restore the saved session, if any. fn bootstrap(&mut self, cx: &mut Context) { let client = self.client.clone(); @@ -217,10 +220,9 @@ impl Backend { self.restore_session(cx); } - /// Restore the saved session from the keyring. Emits - /// [`BackendEvent::SignerRequired`] if no credential is stored, or - /// [`BackendEvent::PassphraseRequired`] if the stored identity is - /// NIP-49 encrypted. + /// Restore the saved session from the keyring. + /// Emits [`BackendEvent::SignerRequired`] when no credential is stored. + /// Emits [`BackendEvent::PassphraseRequired`] for a NIP-49 encrypted identity. pub fn restore_session(&mut self, cx: &mut Context) { if cfg!(target_arch = "wasm32") { cx.emit(BackendEvent::SignerRequired); @@ -254,8 +256,8 @@ impl Backend { signer.auth_url_handler(SignedAuthUrlHandler); this.update(cx, |this, cx| this.set_signer(signer, cx))?; } else if content.starts_with("ncryptsec1") { - // Encrypted identity: a passphrase is required to - // decrypt it before the session can resume. + // Encrypted identity. + // A passphrase is required to decrypt it before the session can resume. log::warn!("stored identity is ncryptsec-encrypted; waiting for passphrase"); this.update(cx, |this, cx| { this.passphrase_required = true; @@ -280,11 +282,10 @@ impl Backend { })); } - /// Decrypt the NIP-49 encrypted credential stored in the keyring with - /// the given passphrase and resume the session. - /// - /// The scrypt decryption runs off the UI thread. The task yields the - /// public key, or the failure reason (e.g. wrong passphrase). + /// Decrypt the NIP-49 keyring credential with the given passphrase. + /// Resume the session on success. + /// The scrypt decryption runs off the UI thread. + /// The task yields the public key or the failure reason, e.g. a wrong passphrase. pub fn restore_with_passphrase( &mut self, password: &str, @@ -319,12 +320,12 @@ impl Backend { }) } - /// Create a new identity: generate keys, encrypt the secret key with the - /// passphrase (NIP-49) and persist it in the keyring, then publish the - /// user's NIP-65 relay list, metadata and grasp list. - /// - /// The encryption runs off the UI thread; the task yields the new - /// public key. + /// Create a new identity. + /// Generate keys and encrypt the secret key with the passphrase, NIP-49. + /// Persist it in the keyring. + /// Then publish the NIP-65 relay list, metadata and grasp list. + /// The encryption runs off the UI thread. + /// The task yields the new public key. pub fn create_identity( &mut self, name: &str, @@ -360,8 +361,7 @@ impl Backend { write.await?; this.update(cx, |this, cx| { - // Become the new identity, so the publishes below are - // signed with the new keys. + // Become the new identity so later publishes are signed with the new keys. this.signer.swap_inner(keys); this.current_user = Some(public_key); this.bootstrap_user(public_key, cx); @@ -412,21 +412,20 @@ impl Backend { }) } - /// Create a new repository: initialize a local clone with a `main` - /// branch and a `README.md`, publish the NIP-34 announcement and the - /// repository state to the grasp relays, then push the initial commit - /// to each grasp server. A working copy of the repository is also - /// created at `/` (named like the repo header's Clone - /// action), with `origin` pointing at the first grasp server, so the - /// new project exists in the chosen folder right away. - /// - /// The events must reach the grasp servers *before* the push: GRASP - /// servers hold the signed state event in "purgatory" and only accept - /// a push for a not-yet-existing repository while that authorization is - /// pending (it expires after 30 minutes), like gitworkshop and ngit. - /// - /// The git work runs on background threads; the task yields the - /// published announcement and the path of the created working copy. + /// Create a repository. + /// Initialize a local clone with a `main` branch and a `README.md`. + /// Publish the NIP-34 announcement and the repository state to the grasp relays. + /// Push the initial commit to each grasp server. + /// Also create a working copy at `/`, like the header's Clone action. + /// Its `origin` points at the first grasp server. + /// The new project exists in the chosen folder right away. + /// The events must reach the grasp relays before the push. + /// GRASP servers hold the signed state event in purgatory. + /// They accept the push only while the authorization is pending. + /// The pushed repository must not exist yet. + /// The authorization expires after 30 minutes, like gitworkshop and ngit. + /// The git work runs on background threads. + /// The task yields the announcement and the path of the working copy. pub fn create_repository( &mut self, name: &str, @@ -454,9 +453,10 @@ impl Backend { return Task::ready(Err(anyhow!("Sign in to create a repository"))); }; - // The repository identifier is derived from the name, like ngit and - // gitworkshop: spaces become hyphens, other non-alphanumeric - // characters (except `/`) become hyphens, case is preserved. + // The repository identifier is derived from the name, like ngit and gitworkshop. + // Spaces become hyphens. + // Other non-alphanumeric characters become hyphens, except `/`. + // Case is preserved. let repo_id = identifier_from_name(&name); if repo_id.is_empty() || repo_id.len() > 100 { return Task::ready(Err(anyhow!( @@ -495,18 +495,16 @@ impl Backend { std::fs::create_dir_all(parent)?; let commit = signed_git::init_repository(&path, &name, &description)?; - // Point `origin` at the first grasp server so later - // fetches (and pushes) have a target, like ngit. + // Point `origin` at the first grasp server. + // Later fetches and pushes have a target, like ngit. if let Some(base) = servers.first().and_then(grasp_base_url) { let url = format!("{base}/{owner}/{repo_id}.git"); signed_git::ensure_origin(&path, &url).ok(); } - // A working copy at `/` (the same naming - // as the header's Clone action), cloned from the mirror - // above so it shares the announced history exactly; - // `origin` is re-pointed at the first grasp server - // instead of the mirror path. + // A working copy at `/`, like the header's Clone action. + // Cloned from the mirror above so it shares the announced history. + // `origin` is set to the first grasp server, not the mirror path. let destination = { let dir_name = signed_git::sanitize_path_component(&name); let dir_name = if dir_name.is_empty() { @@ -546,8 +544,8 @@ impl Backend { this.add_relays(urls, cx); })?; - // The state event is the push authorization ("purgatory"), so - // it must be accepted before the push below. + // The state event is the push authorization. + // It must be accepted before the push below. let announcement = GitRepositoryAnnouncement { id: repo_id.clone(), name: Some(name.clone()), @@ -593,8 +591,7 @@ impl Backend { } }; - // Push to every grasp server; creation only fails when no - // server accepted it. + // Push to every grasp server. Creation fails only when no server accepted it. let push = cx.background_spawn({ let path = path.clone(); let owner = owner.clone(); @@ -604,8 +601,8 @@ impl Backend { }); if let Err(e) = push.await { - // The events are already published; retract them so the - // repository doesn't remain announced without content. + // The events are already published. + // Retract them so the repository is not left announced without content. this.update(cx, |this, cx| { this.retract_events(&[event.clone(), state_event.clone()], cx); }) @@ -624,13 +621,12 @@ impl Backend { }) } - /// Publish an existing local repository to NIP-34: read its current - /// branches, tags and HEAD, publish the announcement and the repository - /// state to the grasp relays, then push every branch and tag to each - /// grasp server. Also points `origin` at the first grasp server. - /// - /// Same ordering constraint as [`Self::create_repository`]: the state - /// event ("purgatory") must be accepted before the push. + /// Publish an existing local repository to NIP-34. + /// Read its current branches, tags and HEAD. + /// Publish the announcement and the repository state to the grasp relays. + /// Then push every branch and tag to each grasp server. + /// Also point `origin` at the first grasp server. + /// The state event must be accepted before the push, like [`Self::create_repository`]. pub fn publish_local_repo( &mut self, path: PathBuf, @@ -654,8 +650,7 @@ impl Backend { return Task::ready(Err(anyhow!("Sign in to publish a repository"))); }; - // The repository identifier is derived from the name as in - // [`Self::create_repository`]. + // The identifier derives from the name, as in [`Self::create_repository`]. let repo_id = identifier_from_name(&name); if repo_id.is_empty() || repo_id.len() > 100 { @@ -690,8 +685,8 @@ impl Backend { this.add_relays(urls, cx); })?; - // The state event is the push authorization ("purgatory"), so - // it must be accepted before the push below. + // The state event is the push authorization. + // It must be accepted before the push below. let announcement = GitRepositoryAnnouncement { id: repo_id.clone(), name: Some(name.clone()), @@ -735,9 +730,9 @@ impl Backend { } }; - // Push every branch and tag to each grasp server; the init - // only fails when no server accepted it. An empty repository - // has nothing to push. + // Push every branch and tag to each grasp server. + // The push fails only when no server accepted it. + // An empty repository has nothing to push. if !refs.is_empty() { let push = cx.background_spawn({ let path = path.clone(); @@ -759,8 +754,7 @@ impl Backend { } } - // Point `origin` at the first grasp server so later pushes - // have a target. + // Point `origin` at the first grasp server so later pushes have a target. if let Some(base) = servers.first().and_then(grasp_base_url) { let url = format!("{base}/{owner}/{repo_id}.git"); let path = path.clone(); @@ -774,10 +768,10 @@ impl Backend { }) } - /// Re-push the repository's current refs to the grasp servers announced - /// in its `relays` tag: publishes a fresh state event (the push - /// authorization), then pushes every branch and tag, like the init - /// flow. The repository must have a local clone in the cache. + /// Re-push the repository's current refs to the grasp servers in its `relays` tag. + /// Publish a fresh state event, the push authorization. + /// Then push every branch and tag, like the init flow. + /// The repository must have a local clone in the cache. pub fn push_repository( &mut self, announcement: Announcement, @@ -788,12 +782,12 @@ impl Backend { self.push_repo_from(announcement, path, None, cx) } - /// Push the refs of a local checkout (the working copy of the user's - /// own repository) to the grasp servers announced in the `relays` tag: - /// publishes a fresh state event, then pushes every branch and tag of - /// the checkout, like the init flow. `announced_head` keeps the state - /// event's `HEAD` on the repository's announced default branch when the - /// checkout is on a different branch. + /// Push the refs of a local checkout to the grasp servers in its `relays` tag. + /// The checkout is the working copy of the user's own repository. + /// Publish a fresh state event, then push every branch and tag of the checkout. + /// That mirrors the init flow. + /// `announced_head` keeps the state event's `HEAD` on the announced default branch. + /// That matters when the checkout is on a different branch. pub fn push_checkout( &mut self, announcement: Announcement, @@ -804,12 +798,13 @@ impl Backend { self.push_repo_from(announcement, checkout, announced_head, cx) } - /// Shared body of the mirror-based and checkout-based pushes: publish - /// the repository state (the push authorization), then push every - /// branch and tag of `path` to each announced grasp server. Pushes are - /// single-flight per repository: two concurrent pushes of the same refs - /// (e.g. two panels of the same repository) make the losing push fail - /// server-side with a compare-and-swap rejection. + /// Shared body of the mirror-based and checkout-based pushes. + /// Publish the repository state, the push authorization. + /// Then push every branch and tag of `path` to each announced grasp server. + /// Pushes are single-flight per repository. + /// Concurrent pushes of the same refs fail server-side. + /// The rejection is a compare-and-swap error from the server. + /// Two panels of the same repository can produce the race. fn push_repo_from( &mut self, announcement: Announcement, @@ -841,8 +836,8 @@ impl Backend { let relays = announcement.relays.clone(); cx.spawn(async move |this, cx| { - // Held for the whole task; dropped (and the lock released) on - // completion, on error and on cancellation alike. + // Held for the whole task. + // Dropped on completion, on error and on cancellation alike. let _guard = guard; let mut state = { @@ -853,10 +848,9 @@ impl Backend { work.await? }; - // The state event announces the pushed refs. When the source is - // a checkout on a side branch, keep the repository's announced - // default branch (its `HEAD`) when that branch is among the - // pushed refs; otherwise the checkout's current branch. + // The state event announces the pushed refs. + // Keep the announced default branch in `HEAD` when it is among the pushed refs. + // Otherwise `HEAD` stays the checkout's current branch. let heads: Vec<&str> = state .refs .iter() @@ -895,10 +889,10 @@ impl Backend { }) } - /// Delete the repository from nostr: publish NIP-09 deletions for its - /// announcement, state and activity events (issues, pull requests, - /// patches, statuses, comments). Only the repository owner may delete - /// it. + /// Delete the repository from nostr. + /// Publish NIP-09 deletions for its announcement, state and activity events. + /// Those are issues, pull requests, patches, statuses and comments. + /// Only the repository owner may delete it. pub fn delete_repository( &mut self, addr: RepoAddr, @@ -939,8 +933,8 @@ impl Backend { }) } - /// Login with an `nsec1...` key or a `bunker://...` URI, dispatching on - /// the credential's prefix. + /// Login with an `nsec1...` key or a `bunker://...` URI. + /// Dispatch on the credential's prefix. pub fn login(&mut self, credential: &str, cx: &mut Context) { let credential = credential.trim(); @@ -955,8 +949,8 @@ impl Backend { } } - /// Create a fresh identity and login with it. The generated key is - /// persisted in the keyring like any other `nsec` credential. + /// Create a fresh identity and login with it. + /// The generated key is persisted in the keyring like any other `nsec` credential. pub fn login_with_new_identity(&mut self, cx: &mut Context) { let nsec = Keys::generate() .secret_key() @@ -965,8 +959,8 @@ impl Backend { self.login_with_nsec(&nsec, cx); } - /// Login with an `nsec1...` secret key. The credential is verified by - /// the signer flow and persisted in the keyring. + /// Login with an `nsec1...` secret key. + /// The credential is verified by the signer flow and persisted in the keyring. pub fn login_with_nsec(&mut self, nsec: &str, cx: &mut Context) { let keys = match SecretKey::parse(nsec) { Ok(secret) => Keys::new(secret), @@ -990,11 +984,11 @@ impl Backend { })); } - /// Login with a `bunker://...` URI (NIP-46). A fresh session key is - /// generated and embedded into the stored URI as `?master=`, so - /// no separate keyring entry is needed. The auth URL, if any, is opened - /// in the default browser. The credential is persisted in the keyring - /// after the signer proves reachable. + /// Login with a `bunker://...` URI, NIP-46. + /// A fresh session key is embedded into the stored URI as `?master=`. + /// No separate keyring entry is needed. + /// The auth URL, if any, is opened in the default browser. + /// The credential is persisted in the keyring after the signer proves reachable. pub fn login_with_bunker(&mut self, uri: &str, cx: &mut Context) { let uri_string = uri.trim().to_owned(); @@ -1058,8 +1052,8 @@ impl Backend { })); } - /// Fetch the user's grasp list (kind `10317`) and add the listed grasp - /// servers as relays. + /// Fetch the user's grasp list of kind `10317`. + /// Add the listed grasp servers as relays. fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context) { let client = self.client.clone(); @@ -1111,8 +1105,8 @@ impl Backend { self.current_user } - /// Whether the stored credential is NIP-49 encrypted and a passphrase - /// is still needed to resume the session. + /// True when the stored credential is NIP-49 encrypted. + /// A passphrase is still needed to resume the session. pub fn passphrase_required(&self) -> bool { self.passphrase_required } @@ -1127,13 +1121,15 @@ impl Backend { self.connected } - /// Progress of the in-flight negentropy sync, if any: `(total, current)`. + /// Progress of the in-flight negentropy sync, if any. + /// Reported as `total` and `current`. pub fn sync_progress(&self) -> Option<(u64, u64)> { self.sync_progress } - /// Update the signer (any type implementing the async signer traits, - /// e.g. `Keys`, `NostrConnect`, a browser extension proxy). + /// Update the signer. + /// Any type implementing the async signer traits works. + /// Examples are `Keys`, `NostrConnect` and a browser extension proxy. pub fn set_signer(&mut self, new_signer: T, cx: &mut Context) where T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static, @@ -1194,8 +1190,8 @@ impl Backend { })); } - /// Add relays used only for discovery (e.g. NIP-65 indexers) and - /// connect to them. No subscriptions or writes are routed through them. + /// Add discovery-only relays, e.g. NIP-65 indexers, and connect to them. + /// No subscriptions or writes are routed through them. pub fn add_discovery_relays(&mut self, urls: Vec, cx: &mut Context) { let client = self.client.clone(); @@ -1218,8 +1214,9 @@ impl Backend { })); } - /// Start a persistent subscription. Matching events are stored in the - /// database automatically and surface as [`BackendEvent::NostrUpdate`]. + /// Start a persistent subscription. + /// Matching events are stored in the database automatically. + /// They surface as [`BackendEvent::NostrUpdate`]. pub fn subscribe(&mut self, filter: Filter, cx: &mut Context) { let client = self.client.clone(); @@ -1233,9 +1230,8 @@ impl Backend { })); } - /// Whether an identical fetch was started within [`FETCH_DEDUP_WINDOW`] - /// and is still recent enough to suppress a duplicate. Records the - /// fingerprint (after pruning expired entries) when returning `false`. + /// Whether an identical fetch started within [`FETCH_DEDUP_WINDOW`] is still recent. + /// Records the fingerprint when returning `false`, pruning expired entries first. fn fetch_recently_started(&mut self, fingerprint: u64) -> bool { self.recent_fetches .retain(|_, started| started.elapsed() < FETCH_DEDUP_WINDOW); @@ -1246,17 +1242,13 @@ impl Backend { false } - /// Connect to relays announced by a repository (NIP-34 `relays` tag) and - /// fetch its events from them: a one-shot auto-closing subscription for - /// `filters`, plus a negentropy sync so issues, patches and PRs stored - /// only on those relays are not missed. - /// - /// Deduplicated: an identical request (same relays and filters) started - /// within [`FETCH_DEDUP_WINDOW`] is skipped, so a second panel for the - /// same repository doesn't re-run the fetch. - /// - /// Best-effort: failures are logged, not surfaced. The relays stay in - /// the pool, so later publishes for this repository also reach them. + /// Connect to a repository's announced relays, its NIP-34 `relays` tag. + /// Fetch the repository's events from them. + /// Run a one-shot auto-closing subscription for `filters`. + /// Then a negentropy sync covers issues, patches and PRs stored only on those relays. + /// An identical request within [`FETCH_DEDUP_WINDOW`] is skipped. + /// The relays stay in the pool, so later publishes for this repository reach them too. + /// Failures are logged, not surfaced. pub fn connect_repo_relays( &mut self, relays: Vec, @@ -1285,10 +1277,10 @@ impl Backend { })); } - /// Start a one-shot subscription targeted only at the bootstrap relays, - /// auto-closing after EOSE or a short timeout. Matching events are stored - /// in the database and surface as [`BackendEvent::NostrUpdate`] while the - /// subscription is open. + /// One-shot subscription on the bootstrap relays only. + /// Auto-closes after EOSE or a short timeout. + /// Matching events are stored in the database. + /// They surface as [`BackendEvent::NostrUpdate`] while the subscription is open. pub fn subscribe_bootstrap(&mut self, filters: Vec, cx: &mut Context) { let client = self.client.clone(); @@ -1303,14 +1295,13 @@ impl Backend { })); } - /// Negentropy-sync the given filter against the bootstrap relays: - /// reconciles the local database with the relays in both directions. - /// Emits [`BackendEvent::SyncProgress`] while running (throttled to - /// whole-percent changes) and [`BackendEvent::Synced`] on completion. - /// - /// Deduplicated: an identical sync started within - /// [`FETCH_DEDUP_WINDOW`] is skipped. Observers still see the original - /// sync's progress and completion events. + /// Negentropy-sync the given filter against the bootstrap relays. + /// Reconciles the local database with the relays in both directions. + /// Emits [`BackendEvent::SyncProgress`] while running. + /// Throttled to whole-percent changes. + /// Emits [`BackendEvent::Synced`] on completion. + /// An identical sync started within [`FETCH_DEDUP_WINDOW`] is skipped. + /// Observers still see the original sync's progress and completion events. pub fn sync_bootstrap(&mut self, filter: Filter, cx: &mut Context) { let fingerprint = fetch_fingerprint(&BOOTSTRAP_RELAYS, std::slice::from_ref(&filter)); if self.fetch_recently_started(fingerprint) { @@ -1385,12 +1376,10 @@ impl Backend { })); } - /// Sign, broadcast and locally store an event. Emits - /// [`BackendEvent::Published`] on success so stores can refresh. - /// - /// The task yields the outcome of this specific action (for inline - /// progress/errors) and is owned by the caller; dropping it cancels - /// the publish. + /// Sign, broadcast and locally store an event. + /// Emits [`BackendEvent::Published`] on success so stores can refresh. + /// The task yields the outcome of this specific action for inline progress or errors. + /// The caller owns the task, dropping it cancels the publish. pub fn send( &mut self, builder: EventBuilder, @@ -1400,8 +1389,8 @@ impl Backend { let signer = self.signer.clone(); cx.spawn(async move |this, cx| { - // Sign with the current signer, broadcast, and save locally so - // the event is immediately visible to database queries. + // Sign with the current signer, broadcast and save locally. + // The event is immediately visible to database queries. let work = cx.background_spawn(async move { let event = builder.finalize_async(&signer).await?; let output = client.send_event(&event).await?; @@ -1440,10 +1429,10 @@ impl Backend { }) } - /// Broadcast and locally store an already-signed event, like - /// [`Self::send`] without the signing step. Callers that signed early - /// (e.g. to learn the event id before pushing a commit to the grasp - /// servers) publish through this. + /// Broadcast and locally store an already-signed event. + /// Like [`Self::send`] without the signing step. + /// Callers that signed early use this. + /// They may need the event id before pushing a commit to the grasp servers. pub fn publish_event( &mut self, event: Event, @@ -1489,9 +1478,9 @@ impl Backend { }) } - /// Publish a NIP-34 repository announcement (kind 30617) with the - /// current signer. The returned task yields the published event, so - /// callers can show inline progress/errors. + /// Publish a NIP-34 repository announcement, kind 30617, with the current signer. + /// The returned task yields the published event. + /// Callers can show inline progress or errors. pub fn publish_announcement( &mut self, announcement: GitRepositoryAnnouncement, @@ -1500,9 +1489,9 @@ impl Backend { self.send(announcement.into_event_builder(), cx) } - /// Sign, broadcast and store an event without awaiting the result; - /// failures surface through [`BackendEvent::Error`]. The spawned task is - /// owned by the backend, so it is cancelled when the backend is dropped. + /// Sign, broadcast and store an event without awaiting the result. + /// Failures surface through [`BackendEvent::Error`]. + /// The backend owns the spawned task, so dropping it cancels the task. fn send_fire_and_forget(&mut self, builder: EventBuilder, cx: &mut Context) { let task = self.send(builder, cx); @@ -1517,10 +1506,10 @@ impl Backend { })); } - /// Publish a NIP-09 deletion event for `events` (best-effort), so a - /// publish that fails midway can retract the events that were already - /// broadcast to relays. Failures are logged, not surfaced: the caller's - /// error already told the user what happened. + /// Publish NIP-09 deletions for `events`, best-effort. + /// A publish that fails midway retracts the events already broadcast to relays. + /// Failures are logged, not surfaced. + /// The caller's error already told the user what happened. fn retract_events(&mut self, events: &[Event], cx: &mut Context) { if events.is_empty() { return; @@ -1544,8 +1533,8 @@ impl Backend { } } -/// Fingerprint of a relay + filter set, for fetch dedup. Relays and -/// filters are sorted first so the fingerprint is order-independent. +/// Fingerprint of a relay and filter set, for fetch dedup. +/// Relays and filters are sorted first, so the fingerprint is order-independent. fn fetch_fingerprint(relays: &[&str], filters: &[Filter]) -> u64 { let mut relays: Vec<&str> = relays.to_vec(); relays.sort_unstable(); @@ -1558,11 +1547,10 @@ fn fetch_fingerprint(relays: &[&str], filters: &[Filter]) -> u64 { hasher.finish() } -/// Add the given relays, connect to them, and fetch the filters: a one-shot -/// subscription (auto-closing after EOSE) plus a negentropy sync per filter -/// as a second pass, so events that race with the subscription or relays -/// with flaky EOSE behavior can't be missed. Relays without NEG-XX support -/// just fail the sync step; the subscription already covered them. +/// Add the given relays, connect and fetch the filters. +/// Run a one-shot subscription, auto-closing after EOSE, then a negentropy sync per filter. +/// The second pass catches events that race the subscription or flaky EOSE behavior. +/// Relays without NEG-XX support fail the sync step, the subscription already covered them. async fn connect_repo_relays_only( client: &Client, relays: Vec, @@ -1576,8 +1564,8 @@ async fn connect_repo_relays_only( for url in &relays { added |= client.add_relay(url).await?; } - // Connecting is only needed when the pool grew; connected relays no-op, - // but the call still iterates every relay in the pool. + // Connect only when the pool grew. + // Connected relays no-op, but the call still iterates every relay in the pool. if added { client.connect().await; } @@ -1592,9 +1580,9 @@ async fn connect_repo_relays_only( .collect(); client.subscribe(target).close_on(opts).await?; - // Sync the filters concurrently: each reconciles against every relay - // either way, and a relay without NEG-XX support otherwise serializes - // its initial timeout behind every other filter. + // Sync the filters concurrently. + // Each reconciles against every relay either way. + // Without NEG-XX a relay would serialize its initial timeout behind every other filter. let sync_opts = SyncOptions::default().initial_timeout(Duration::from_secs(5)); let syncs = filters.into_iter().map(|filter| { let client = &client; @@ -1616,9 +1604,10 @@ async fn connect_repo_relays_only( Ok(()) } -/// Subscribe only on the bootstrap relays, auto-closing after EOSE or a -/// short timeout. Use for one-shot data fetches (repo events, profiles) -/// instead of persistent gossip-routed subscriptions. +/// Subscribe only on the bootstrap relays. +/// Auto-closes after EOSE or a short timeout. +/// Use for one-shot data fetches, repo events and profiles. +/// Not for persistent gossip-routed subscriptions. pub(crate) async fn subscribe_bootstrap_only( client: &Client, filters: Vec, @@ -1658,17 +1647,17 @@ fn with_master_key(uri: &str, keys: &Keys) -> String { format!("{uri}{separator}master={nsec}") } -/// A `https://` (or `http://` for `ws://` grasp servers, like -/// ngit) base URL for a grasp server. The repository then lives at -/// `{base}/{npub}/{repo-id}.git`. +/// Base URL of a grasp server, `https://`. +/// `ws://` grasp servers use `http://`, like ngit. +/// The repository then lives at `{base}/{npub}/{repo-id}.git`. pub(crate) fn grasp_base_url(relay: &RelayUrl) -> Option { - // `domain()` drops the port; parse the full URL to keep it (local dev - // grasp servers commonly run on a custom port). + // `domain()` drops the port. + // Parse the full URL to keep it, local dev grasp servers often run on a custom port. let parsed = Url::parse(relay.as_str()).ok()?; let host = parsed.host_str()?; let port = parsed.port().map(|p| format!(":{p}")).unwrap_or_default(); - // `ws://` grasp servers (e.g. local dev relays) speak plain HTTP; - // everything else is HTTPS, matching ngit. + // `ws://` grasp servers, e.g. local dev relays, speak plain HTTP. + // Everything else is HTTPS, matching ngit. let scheme = if relay.scheme().is_secure() { "https" } else { @@ -1677,25 +1666,25 @@ pub(crate) fn grasp_base_url(relay: &RelayUrl) -> Option { Some(format!("{scheme}://{host}{port}")) } -/// The GRASP clone URL of a repository on a grasp server, matching the -/// format ngit announces: `https:////.git`. +/// GRASP clone URL of a repository on a grasp server. +/// Matches the format ngit announces, `https:////.git`. fn grasp_clone_url(relay: &RelayUrl, owner: &str, repo_id: &str) -> Option { let base = grasp_base_url(relay)?; Url::parse(&format!("{base}/{owner}/{repo_id}.git")).ok() } -/// The GRASP-06 contributor namespace URL of a pull request tip on the -/// author's grasp server: `{base}/prs//.git` (npub in -/// the URL; the server stores it under the hex form). Anyone may push there; -/// no announcement or maintainer rights are involved. +/// GRASP-06 contributor namespace URL of a pull request tip. +/// The pattern is `{base}/prs//.git`. +/// The npub sits in the URL, the server stores it under the hex form. +/// Anyone may push there, no announcement or maintainer rights are involved. pub(crate) fn grasp06_prs_url(base_url: &str, npub: &str, repo_id: &str) -> String { format!("{base_url}/prs/{npub}/{repo_id}.git") } -/// Assemble the `clone` URLs of a pull request: the author's GRASP-06 -/// `/prs/` URLs first (author-controlled, most likely to accept the tip -/// push), then the base announcement's clone URLs, deduplicated while -/// preserving that order. +/// Assemble the `clone` URLs of a pull request. +/// The author's GRASP-06 `/prs/` URLs come first. +/// They are author-controlled and most likely to accept the tip push. +/// The base announcement's clone URLs follow, deduplicated while preserving order. pub(crate) fn pr_clone_urls(prs_urls: Vec, base_clone_urls: Vec) -> Vec { let mut seen = std::collections::HashSet::new(); let mut urls = Vec::new(); @@ -1708,7 +1697,7 @@ pub(crate) fn pr_clone_urls(prs_urls: Vec, base_clone_urls: Vec) -> Ve } /// The `g` tag servers of one kind-10317 grasp list event, in tag order. -/// Unparseable URLs are dropped (the UI only writes well-formed servers). +/// Unparseable URLs are dropped, the UI only writes well-formed servers. fn grasp_list_servers(event: &Event) -> Vec { event .tags @@ -1719,10 +1708,9 @@ fn grasp_list_servers(event: &Event) -> Vec { .collect() } -/// The grasp servers of the newest kind-10317 grasp list among `events` -/// (latest event wins, like every other latest-wins resolution in the app); -/// empty when there is no list, so the caller falls back to the settings -/// defaults. +/// Grasp servers of the newest kind-10317 grasp list among `events`. +/// The latest event wins, like other latest-wins resolutions in the app. +/// Empty when there is no list, so the caller falls back to the settings defaults. fn latest_grasp_list_servers(events: Vec) -> Vec { events .into_iter() @@ -1731,10 +1719,10 @@ fn latest_grasp_list_servers(events: Vec) -> Vec { .unwrap_or_default() } -/// Resolve the user's published grasp servers: the `g` tags (in order) of -/// their latest kind-10317 grasp list in the local database. Returns an -/// empty list when the user has no published list, so the caller can fall -/// back to the settings defaults. +/// Resolve the user's published grasp servers. +/// Read the `g` tags of their latest kind-10317 grasp list in the local database. +/// Returns an empty list when the user has no published list. +/// The caller can then fall back to the settings defaults. pub(crate) async fn user_grasp_list_servers( client: Client, user: PublicKey, @@ -1748,11 +1736,11 @@ pub(crate) async fn user_grasp_list_servers( Ok(latest_grasp_list_servers(events)) } -/// Push the repository at `path` to every grasp server: a server that -/// rejects the push is logged, but the push only fails when no server -/// accepted it. `push` performs the single-server push (e.g. -/// [`signed_git::push_main`] for the create flow, [`signed_git::push_all`] -/// for the init flow). +/// Push the repository at `path` to every grasp server. +/// Rejecting servers are logged, the push only fails when no server accepted it. +/// `push` performs the single-server push. +/// [`signed_git::push_main`] serves the create flow. +/// [`signed_git::push_all`] serves the init flow. async fn push_to_grasp_servers( path: PathBuf, owner: String, @@ -1789,7 +1777,7 @@ async fn push_to_grasp_servers( } /// Split a stored bunker credential into the plain URI and the session key. -/// Credentials without an embedded key (legacy) get a fresh one. +/// Credentials without an embedded key, legacy, get a fresh one. fn extract_master_key(credential: &str) -> (&str, Keys) { match credential.split_once("master=") { Some((base, nsec)) => { @@ -1837,7 +1825,7 @@ mod tests { grasp06_prs_url("https://relay.ngit.dev", "npub1author", "my-repo"), "https://relay.ngit.dev/prs/npub1author/my-repo.git" ); - // `ws://` grasp servers (local dev) keep their plain-HTTP base. + // `ws://` grasp servers, local dev, keep their plain-HTTP base. assert_eq!( grasp06_prs_url("http://localhost:8080", "npub1author", "my-repo"), "http://localhost:8080/prs/npub1author/my-repo.git" @@ -1913,7 +1901,7 @@ mod tests { vec!["wss://fresh.example", "wss://also.example"] ); - // No list at all: empty, so the caller falls back to the defaults. + // No list at all, empty, so the caller falls back to the defaults. assert!(latest_grasp_list_servers(Vec::new()).is_empty()); } } diff --git a/crates/signed_state/src/checkouts.rs b/crates/signed_state/src/checkouts.rs index 72d1bce..cefba37 100644 --- a/crates/signed_state/src/checkouts.rs +++ b/crates/signed_state/src/checkouts.rs @@ -1,28 +1,3 @@ -//! Local checkout associations ("remember" tier of the PR suggestions): -//! which local folders are checkouts of which announced repositories. -//! -//! Two sources feed the resolution: -//! -//! - **Remembered records** (settings, [`settings::CheckoutRecord`]): -//! recorded when the user clones a repository from the app or picks a -//! folder in the New PR panel. -//! - **Implicit matches** over the local scan ([`LocalReposStore`]): a -//! scanned repository whose `origin` URL matches an announcement `clone` -//! URL (scheme-insensitive), or whose root commit equals an announcement -//! EUC, is a checkout of that announced repository. -//! -//! The store also computes per-checkout statuses for two surfaces: -//! -//! - **"Ready to contribute"** (pull-request banner of repositories the -//! user does not own): branch, base and commits ahead of the base. -//! - **"Ready to push"** (sidebar badge and banner of the user's own -//! repositories): the checked-out branch has commits the grasp servers -//! do not have yet (counted against the refreshed remote-tracking -//! refs), so the user can push their local work from the app. -//! -//! Everything is resolved on background threads and swapped in as -//! [`Arc`]s; the UI never waits for git. - use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -40,18 +15,17 @@ use crate::git_store::GitStore; use crate::local_repos::LocalReposStore; use crate::repo_list::RepoListStore; -/// Delay between a refresh request and the actual re-computation, so bursts -/// of notifications (settings edits, rescan ticks) collapse into one pass. +/// Delay between a refresh request and the actual re-computation. +/// Bursts of notifications, settings edits and rescan ticks, collapse into one pass. const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300); -/// How often the statuses of open repository panels are refreshed, so a -/// checkout committed to or pulled in external git surfaces in the banner -/// without reopening the panel. +/// How often the statuses of open repository panels are refreshed. +/// A commit or pull in external git surfaces in the banner without reopening the panel. const STATUS_POLL: Duration = Duration::from_secs(15); -/// Background poll interval for the "ready to push" badges of the user's -/// own repositories when no repository panel is open (each cycle refreshes -/// the remote view of the checkouts with a git fetch). +/// Background poll interval for the `ready to push` badges of the user's own repositories. +/// Used when no repository panel is open. +/// Each cycle refreshes the remote view of the checkouts with a git fetch. const PUSH_POLL: Duration = Duration::from_secs(60); /// Maximum checkouts considered per repository when computing statuses. @@ -61,24 +35,25 @@ struct GlobalCheckoutsStore(Entity); impl Global for GlobalCheckoutsStore {} -/// One associated local checkout of a repository, with the git facts needed -/// to suggest a pull request. +/// One associated local checkout of a repository. +/// Carries the git facts needed to suggest a pull request. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CheckoutStatus { /// The checkout folder. pub path: PathBuf, - /// The branch checked out (`None`-less: detached checkouts are idle). + /// The branch checked out. A detached checkout is idle and yields no status. pub branch: String, /// Commit the branch points at, for tip-based PR dedupe. pub head: String, - /// What the branch is compared against. For ready-to-contribute - /// statuses: the announced HEAD branch (else `main`, else the first - /// local branch). For ready-to-push statuses: the remote-tracking ref - /// the unpushed commits are counted against - /// (`refs/remotes/origin/`, or `origin/HEAD` for branches the - /// remote does not have yet). + /// What the branch is compared against. + /// For ready-to-contribute statuses, the announced HEAD branch. + /// The fallbacks are `main`, then the first local branch. + /// For ready-to-push statuses, the remote-tracking ref. + /// Unpushed commits are counted against it. + /// It is `refs/remotes/origin/`, else `origin/HEAD` for new branches. pub base: String, - /// Commits in `base..branch`; always > 0 (even checkouts are dropped). + /// Commits in `base..branch`. + /// Zero-ahead checkouts are dropped, so this is always above zero. pub ahead: u32, } @@ -91,23 +66,23 @@ struct Remembered { /// Global store of local-checkout associations and per-checkout statuses. pub struct CheckoutsStore { - /// Checkout paths per announced repository: remembered records - /// (freshest first) plus scanned repos matched implicitly, deduplicated - /// by path. Missing directories are dropped before publishing. + /// Checkout paths per announced repository. + /// Remembered records, freshest first, plus scanned repos matched implicitly. + /// Deduplicated by path. + /// Missing directories are dropped before publishing. by_repo: Arc>>, /// Ready-to-contribute statuses of the requested repositories. statuses: Arc>>, - /// Repositories whose statuses are recomputed whenever the inputs - /// change (the repository detail panels currently open). + /// Repositories whose statuses are recomputed on every input change. + /// Those are the repository detail panels currently open. status_requested: HashSet, - /// Repositories whose "ready to push" statuses are recomputed on the - /// same cycle (the sidebar rows of the user's own repositories, plus - /// the detail panels of those repositories). + /// Repositories whose `ready to push` statuses are recomputed on the same cycle. + /// The sidebar rows of the user's own repositories and their detail panels. push_requested: HashSet, /// Ready-to-push statuses of the requested own repositories. push_statuses: Arc>>, - /// Announced head branch last provided per requested repository, so a - /// recompute defaults the base the same way. + /// Last announced head branch per requested repository. + /// A recompute defaults the base the same way. requested_head: HashMap>, refreshing: bool, refresh_dirty: bool, @@ -127,9 +102,10 @@ impl CheckoutsStore { cx.set_global(GlobalCheckoutsStore(entity)); } - /// Create the store: observe the inputs (settings records, the local - /// scan, the announcement list, signer changes) and resolve the - /// associations right away. + /// Create the store. + /// Observe the inputs, settings records, the local scan and the announcement list. + /// Signer changes also trigger a refresh. + /// Associations are resolved right away. pub fn new(cx: &mut Context) -> Self { let mut subscriptions = Vec::new(); @@ -148,8 +124,8 @@ impl CheckoutsStore { subscriptions.push(cx.observe(&repos, |this, _repos, cx| { this.refresh(cx); })); - // Another identity's repositories must not keep the previous - // user's statuses (or polls) alive. + // Another identity's repositories must not keep the old statuses alive. + // Their polls stop too. subscriptions.push(cx.subscribe(&backend, |this, _backend, event, cx| { if matches!(event, BackendEvent::SignerChanged) { this.status_requested.clear(); @@ -182,8 +158,9 @@ impl CheckoutsStore { store } - /// Remember a successful local-checkout use: (re)insert the record with - /// a fresh timestamp, so freshest-first ordering follows actual use. + /// Remember a successful local-checkout use. + /// Re-insert the record with a fresh timestamp. + /// Freshest-first ordering then follows actual use. pub fn record(&mut self, path: PathBuf, addr: RepoAddr, cx: &mut Context) { if cfg!(target_arch = "wasm32") { return; @@ -212,16 +189,15 @@ impl CheckoutsStore { }); } - /// The associated checkouts of `addr`, freshest first. Empty when none - /// are known (or the resolution has not run yet). + /// The associated checkouts of `addr`, freshest first. + /// Empty when none are known or the resolution has not run yet. pub fn associations_of(&self, addr: &RepoAddr) -> Vec { self.by_repo.get(addr).cloned().unwrap_or_default() } - /// Ask for the "ready to contribute" statuses of `addr` to be kept - /// current (called while the repository's detail panel is open). - /// `announced_head` is the announced HEAD branch of the repository - /// (from its state announcement), used to default the base. + /// Ask for the `ready to contribute` statuses of `addr` to stay current. + /// Called while the repository's detail panel is open. + /// `announced_head` is the announced HEAD branch, used to default the base. pub fn request_statuses( &mut self, addr: &RepoAddr, @@ -235,33 +211,32 @@ impl CheckoutsStore { self.refresh(cx); } - /// The ready-to-contribute statuses of `addr`; empty while none are - /// known or nothing is ahead. + /// The ready-to-contribute statuses of `addr`. + /// Empty while none are known or nothing is ahead. pub fn statuses_of(&self, addr: &RepoAddr) -> Vec { self.statuses.get(addr).cloned().unwrap_or_default() } - /// Ask for the "ready to push" statuses of `addr` to be kept current - /// (called by the sidebar for the signed-in user's own repositories and - /// by the detail panels of those repositories). Recomputed on every - /// input change and on a background poll; each cycle refreshes the - /// remote view of the checkouts first, so a commit made in external - /// git surfaces within one poll interval. + /// Ask for the `ready to push` statuses of `addr` to stay current. + /// The sidebar and the detail panels call this for the user's own repositories. + /// Recomputed on every input change and on a background poll. + /// Each cycle refreshes the remote view first. + /// A commit made in external git surfaces within one poll interval. pub fn request_push_statuses(&mut self, addr: &RepoAddr, cx: &mut Context) { self.push_requested.insert(addr.clone()); self.refresh(cx); } - /// The ready-to-push statuses of `addr` (only meaningful for - /// repositories announced by the signed-in user); empty while none are - /// known or nothing is unpushed. + /// The ready-to-push statuses of `addr`. + /// Only meaningful for repositories announced by the signed-in user. + /// Empty while none are known or nothing is unpushed. pub fn push_statuses_of(&self, addr: &RepoAddr) -> Vec { self.push_statuses.get(addr).cloned().unwrap_or_default() } - /// Re-resolve associations (and the requested statuses). Debounced: - /// bursts of notifications collapse into one pass; requests arriving - /// while a pass runs are folded into a follow-up. + /// Re-resolve the associations and the requested statuses. + /// Debounced, bursts of notifications collapse into one pass. + /// Requests arriving while a pass runs fold into a follow-up. pub fn refresh(&mut self, cx: &mut Context) { if self.refreshing { self.refresh_dirty = true; @@ -284,7 +259,7 @@ impl CheckoutsStore { self.tasks.push(task); } - /// One resolve + apply cycle (debounced entry point). + /// One resolve and apply cycle, the debounced entry point. fn run_refresh(&mut self, cx: &mut Context) { self.refreshing = true; @@ -321,12 +296,12 @@ impl CheckoutsStore { let poll = !self.status_requested.is_empty() || !self.push_requested.is_empty(); let work = cx.background_spawn(async move { - // Read the git facts of every scanned repository off the main - // thread: origin URL and root commit (both CLI reads). + // Read the git facts of every scanned repository off the main thread. + // The facts are the origin URL and the root commit, both CLI reads. let mut facts: Vec<(PathBuf, Option, Option)> = Vec::new(); for path in scanned.iter() { - // The browser's mirror clones share the announce URLs and - // EUCs; they are not user checkouts. + // The browser's mirror clones share the announce URLs and EUCs. + // They are not user checkouts. if cache_root .as_ref() .is_some_and(|root| path.starts_with(root)) @@ -339,7 +314,7 @@ impl CheckoutsStore { } let associations = resolve_associations(&remembered, &facts, announcements.iter()); - // Missing directories are stale records; drop them. + // Missing directories are stale records, drop them. let associations: HashMap> = associations .into_iter() .map(|(addr, paths)| (addr, paths.into_iter().filter(|p| p.is_dir()).collect())) @@ -382,7 +357,7 @@ impl CheckoutsStore { let (associations, statuses, push_statuses) = match work.await { Ok(results) => results, Err(_) => { - // Git reads are best-effort; keep the last results. + // Git reads are best-effort, keep the last results. return this.update(cx, |this, _cx| { this.refreshing = false; }); @@ -408,16 +383,16 @@ impl CheckoutsStore { this.update(cx, |this, cx| this.refresh(cx))?; } - // While any repository panel is open (or any of the user's own - // repositories is watched for the sidebar badge), keep the - // statuses current: local commits, pulls and branch switches - // happen outside the app and are not otherwise observable. + // Keep the statuses current while any repository panel is open. + // The user's own repositories also count when watched for the sidebar badge. + // Local commits, pulls and branch switches happen outside the app. + // They are not otherwise observable. this.update(cx, |this, cx| { if poll && !this.debouncing && !this.refreshing { this.debouncing = true; - // Open panels get the fast cadence; the sidebar badges - // alone poll less aggressively (each cycle fetches - // every watched checkout's remote). + // Open panels get the fast cadence. + // Sidebar-only badges poll less aggressively. + // Each cycle fetches every watched checkout's remote. let delay = if this.status_requested.is_empty() { PUSH_POLL } else { @@ -439,11 +414,11 @@ impl CheckoutsStore { } } -/// The identity of a repository URL: host, explicit port and path with a -/// trailing `.git` (and slashes) stripped. Scheme-insensitive, so -/// `ws`/`wss`/`http`/`https`/`grasp` are equivalent transports of the same -/// grasp server. `None` for URLs that cannot be parsed (e.g. `git@`-style -/// or plain paths), which then compare by raw string. +/// Identity of a repository URL. +/// Host, explicit port and path count, with a trailing `.git` and slashes stripped. +/// Scheme-insensitive, so `ws`, `wss`, `http`, `https` and `grasp` are one transport. +/// `None` for unparseable URLs, e.g. `git@`-style or plain paths. +/// Those then compare by raw string. fn url_identity(url: &str) -> Option<(String, Option, String)> { let parsed = Url::parse(url).ok()?; let host = parsed.host_str()?.to_ascii_lowercase(); @@ -454,8 +429,8 @@ fn url_identity(url: &str) -> Option<(String, Option, String)> { Some((host, parsed.port(), path)) } -/// Whether two repository URLs point at the same repository, ignoring the -/// transport scheme (see [`url_identity`]). +/// Whether two repository URLs point at the same repository. +/// Ignores the transport scheme, see [`url_identity`]. fn same_repo_url(a: &str, b: &str) -> bool { match (url_identity(a), url_identity(b)) { (Some(a), Some(b)) => a == b, @@ -463,10 +438,10 @@ fn same_repo_url(a: &str, b: &str) -> bool { } } -/// Resolve the associations between local checkouts and announced -/// repositories: remembered records (freshest first per repository), -/// followed by scanned repositories matched by origin URL or EUC. -/// Deduplicated by path, keeping the first (remembered) occurrence. +/// Resolve the associations between local checkouts and announced repositories. +/// Remembered records come first, freshest first per repository. +/// Scanned repositories matched by origin URL or EUC follow. +/// Deduplicated by path, remembered entries win. fn resolve_associations<'a>( remembered: &[Remembered], scanned: &[(PathBuf, Option, Option)], @@ -507,8 +482,9 @@ fn resolve_associations<'a>( out } -/// Whether the worktree of `path` has uncommitted changes (a dirty -/// checkout is never suggested: the proposal should cover committed work). +/// Whether the worktree of `path` has uncommitted changes. +/// A dirty checkout is never suggested. +/// The proposal should cover committed work. fn worktree_dirty(path: &Path) -> bool { let output = Command::new("git") .arg("-C") @@ -522,8 +498,9 @@ fn worktree_dirty(path: &Path) -> bool { } } -/// Commits in `base..branch` of the checkout at `path` (`git rev-list -/// --count`); `0` when the range is empty or cannot be computed. +/// Commits in `base..branch` of the checkout at `path`. +/// Reads `git rev-list --count`. +/// `0` when the range is empty or cannot be computed. fn commits_ahead(path: &Path, base: &str, branch: &str) -> u32 { let output = Command::new("git") .arg("-C") @@ -540,8 +517,8 @@ fn commits_ahead(path: &Path, base: &str, branch: &str) -> u32 { } } -/// The branch checked out at `path` (`git branch --show-current`), `None` -/// when detached. +/// The branch checked out at `path`, read via `git branch --show-current`. +/// `None` when detached. fn current_branch_of(path: &Path) -> Option { let output = Command::new("git") .arg("-C") @@ -554,10 +531,11 @@ fn current_branch_of(path: &Path) -> Option { (!branch.is_empty()).then_some(branch) } -/// The ready-to-contribute status of one checkout, or `None` when it is -/// idle: detached HEAD, no branches, a dirty worktree, or nothing ahead of -/// its base. The base defaults like the New PR panel: the announced HEAD -/// branch when the checkout has it, else `main`, else the first branch. +/// The ready-to-contribute status of one checkout. +/// `None` when idle. +/// Idle means detached HEAD, no branches, a dirty worktree or nothing ahead of its base. +/// The base defaults like the New PR panel. +/// The announced HEAD branch when the checkout has it, else `main`, else the first branch. fn checkout_status(path: &Path, announced_head: Option<&str>) -> Option { let branches = signed_git::worktree_branches(path).ok()?; if branches.is_empty() || worktree_dirty(path) { @@ -583,8 +561,8 @@ fn checkout_status(path: &Path, announced_head: Option<&str>) -> Option bool { let output = Command::new("git") .arg("-C") @@ -595,13 +573,12 @@ fn ref_exists(path: &Path, name: &str) -> bool { matches!(output, Ok(output) if output.status.success()) } -/// The "ready to push" status of one checkout of the user's own -/// repository: the checked-out branch has commits the grasp servers do not -/// have yet. The remote view is refreshed first (best-effort: offline, the -/// last known remote state still counts the commits made since). Detached -/// checkouts, dirty worktrees and branches with no remote state at all -/// (the remote HEAD is unknown) are never suggested; branches the remote -/// does not have yet are counted against the remote HEAD. +/// The `ready to push` status of one checkout of the user's own repository. +/// The checked-out branch has commits the grasp servers do not have yet. +/// The remote view is refreshed first, best-effort. +/// Offline, the last known remote state still counts commits made since. +/// Detached checkouts, dirty worktrees and an unknown remote state yield no status. +/// Branches the remote does not have yet are counted against the remote HEAD. fn checkout_push_status(path: &Path) -> Option { if worktree_dirty(path) { return None; @@ -610,13 +587,13 @@ fn checkout_push_status(path: &Path) -> Option { let head = signed_git::head_commit_id(path).ok().flatten()?; let origin = signed_git::origin_url(path).ok().flatten()?; - // Refresh the remote heads so a commit made elsewhere (or pushed from - // another machine) does not show as "to push" forever. + // Refresh the remote heads first. + // Commits made elsewhere or pushed from another machine must not linger as `to push`. signed_git::fetch_repo_refs(path, &[origin], "+refs/heads/*:refs/remotes/origin/*").ok(); let remote = format!("refs/remotes/origin/{branch}"); - // A branch that has never been fetched/pushed yet is compared against - // the remote HEAD (its fork point in practice). + // A branch never fetched or pushed yet compares against the remote HEAD. + // The remote HEAD is the fork point in practice. let base = if ref_exists(path, &remote) { remote } else if ref_exists(path, "refs/remotes/origin/HEAD") { @@ -634,10 +611,10 @@ fn checkout_push_status(path: &Path) -> Option { }) } -/// Whether the pull request `pr` (a kind-1618 root, resolved `open` by the -/// caller) already proposes the same change as `checkout`: authored by -/// `user`, with a matching `branch-name` tag, or — for renamed branches — a -/// `c` tip tag matching the checkout's HEAD commit. +/// Whether the pull request `pr` already proposes the same change as `checkout`. +/// `pr` is a kind-1618 root, resolved `open` by the caller. +/// Matches when authored by `user` with a matching `branch-name` tag. +/// For renamed branches, a `c` tip tag matching the checkout's HEAD commit counts. pub fn pr_proposes_checkout( pr: &Event, open: bool, @@ -724,8 +701,8 @@ mod tests { repo_addr(owner(), id) } - /// Build one announcement by the fixed test owner with `clone` URLs and - /// an EUC. + /// Build one announcement by the fixed test owner. + /// Takes `clone` URLs and an EUC. fn announcement(id: &str, clones: &[&str], euc: Option<&str>) -> Announcement { let keys = Keys::new(SecretKey::from_hex(KEY).expect("secret")); let mut tags = vec![Tag::parse(vec!["d", id]).expect("tag")]; @@ -807,8 +784,8 @@ mod tests { )]; let base = addr("repo"); - // The same path is both remembered and scanned (its origin matches); - // the remembered occurrence wins and it is listed once. + // The same path is both remembered and scanned, its origin matches. + // The remembered occurrence wins and the path is listed once. let resolved = resolve_associations( &[remembered("/shared", "repo", 100)], &[ @@ -848,7 +825,7 @@ mod tests { run(&["commit", "-m", message]); }; - // A feature branch ahead of main: ready to contribute. + // A feature branch ahead of main, ready to contribute. run(&["checkout", "-b", "feature"]); std::fs::write(path.join("feature.txt"), "x\n").expect("write"); commit("feature work"); @@ -863,17 +840,17 @@ mod tests { assert!(checkout_status(&path, Some("main")).is_none()); run(&["checkout", "--", "."]); - // Even with main: nothing to propose. + // Even on main, nothing to propose. run(&["checkout", "main"]); assert_eq!(checkout_status(&path, Some("main")), None); } #[test] fn checkout_push_status_counts_unpushed_commits_only() { - // The "grasp remote": a plain repository the checkout clones from - // (origin URL = local path, so the whole cycle runs offline). Git - // refuses pushes to its checked-out branch by default; act like a - // grasp server and allow them. + // The `grasp remote` is a plain repository the checkout clones from. + // Its origin URL is a local path, so the whole cycle runs offline. + // Git refuses pushes to a checked-out branch by default. + // Act like a grasp server and allow them. let dir = tempfile::tempdir().expect("tempdir"); let remote = dir.path().join("remote"); signed_git::init_repository(&remote, "My Repo", "").expect("init"); @@ -913,7 +890,7 @@ mod tests { // A fresh clone has nothing to push. assert_eq!(checkout_push_status(&checkout), None); - // One local commit: ready to push, counted against the remote. + // One local commit, ready to push, counted against the remote. std::fs::write(checkout.join("work.txt"), "x\n").expect("write"); run(&["add", "-A"]); run(&["commit", "-m", "local work"]); @@ -923,12 +900,12 @@ mod tests { assert_eq!(status.ahead, 1); assert_eq!(status.head.len(), 40); - // After the push the same commit is on the remote: idle again. + // After the push the same commit is on the remote, idle again. run(&["push", "origin", "main"]); assert_eq!(checkout_push_status(&checkout), None); - // A commit made by someone else on the remote must not count as - // local work (it is behind, not ahead). + // A commit made by someone else on the remote must not count as local work. + // It is behind, not ahead. let remote_run = |args: &[&str]| { let status = Command::new("git") .current_dir(&remote) @@ -979,15 +956,15 @@ mod tests { let status = status("feature", "bb231c4c6a5777dc89b42207b499891a344add5c"); assert!(pr_proposes_checkout(&pr, true, pr.pubkey, &status)); - // Without the branch name (renamed), the `c` tip still matches. + // Without a branch-name tag, the `c` tip still matches for a renamed branch. let pr = pr_event( author, &[&["c", "bb231c4c6a5777dc89b42207b499891a344add5c"]], ); assert!(pr_proposes_checkout(&pr, true, pr.pubkey, &status)); - // Someone else's PR, a closed PR, a different branch and a missing - // tip all leave the checkout uncovered. + // Someone else's PR, a closed PR, a different branch and a missing tip. + // They all leave the checkout uncovered. let pr = pr_event(author, &[&["branch-name", "feature"]]); assert!(!pr_proposes_checkout(&pr, false, pr.pubkey, &status)); let other = pr_event( diff --git a/crates/signed_state/src/git_store.rs b/crates/signed_state/src/git_store.rs index ab7b5df..5dd0173 100644 --- a/crates/signed_state/src/git_store.rs +++ b/crates/signed_state/src/git_store.rs @@ -7,17 +7,15 @@ struct GlobalGitStore(GitCache); impl Global for GlobalGitStore {} -/// Global access to the on-disk git clone cache (grasp mirrors). -/// -/// Installed at startup via [`GitStore::set_global`]; see also -/// [`signed_state::init`]. +/// Global access to the on-disk git clone cache, the grasp mirrors. +/// Installed at startup via [`GitStore::set_global`]. +/// See also [`signed_state::init`]. #[derive(Debug, Clone)] pub struct GitStore(GitCache); impl GitStore { /// Register the clone cache rooted at `root` as an app-wide global. - /// Replaces any previously installed store (see [`signed_state::init`], which - /// installs an empty one). + /// Replaces any installed store, [`signed_state::init`] installs an empty one. pub fn set_global(root: impl Into, cx: &mut App) -> Self { let store = Self::new(root); cx.set_global(GlobalGitStore(store.0.clone())); @@ -25,9 +23,6 @@ impl GitStore { } /// The app-wide clone cache. - /// - /// # Panics - /// /// Panics if [`GitStore::set_global`] was never called. pub fn global(cx: &App) -> Self { Self(cx.global::().0.clone()) diff --git a/crates/signed_state/src/local_repos.rs b/crates/signed_state/src/local_repos.rs index a1335ae..6f5a377 100644 --- a/crates/signed_state/src/local_repos.rs +++ b/crates/signed_state/src/local_repos.rs @@ -45,9 +45,10 @@ impl LocalReposStore { store } - /// Forget a repository that has just been published to NIP-34, - /// so it leaves the local list immediately. A later rescan re-discovers it from disk, - /// the sidebar additionally hides published repositories by identifier. + /// Forget a repository that has just been published to NIP-34. + /// It leaves the local list immediately. + /// A later rescan re-discovers it from disk. + /// The sidebar also hides published repositories by identifier. pub fn remove(&mut self, path: &Path, cx: &mut Context) { self.repos = Arc::new( self.repos @@ -95,8 +96,7 @@ impl LocalReposStore { dirty })?; - // Scans requested while this one was running are coalesced into - // a single follow-up scan. + // Scans requested while this one ran are coalesced into one follow-up scan. if again { this.update(cx, |this, cx| this.rescan(cx))?; } diff --git a/crates/signed_state/src/profile.rs b/crates/signed_state/src/profile.rs index 22624f9..61040af 100644 --- a/crates/signed_state/src/profile.rs +++ b/crates/signed_state/src/profile.rs @@ -10,7 +10,7 @@ use utils::shorten_pubkey; use crate::backend::{Backend, BackendEvent, sync_bootstrap_only}; -/// A user profile (kind `0` metadata), as plain data for the UI. +/// A user profile as plain data for the UI, from the kind-0 metadata. #[derive(Debug, Clone)] pub struct Profile { public_key: PublicKey, @@ -62,18 +62,20 @@ impl Profile { /// Message from the fetch task to the main thread. enum Dispatch { - /// A batched sync finished; re-read seen profiles from the database. + /// A batched sync finished. + /// Re-read seen profiles from the database. Synced, } /// How long to wait for more requests before firing a batched sync. const BATCH_TIMEOUT: Duration = Duration::from_millis(500); -/// Global profile cache. Profiles are fetched in batches and kept as plain -/// data; the whole store notifies on change. +/// Global profile cache. +/// Profiles are fetched in batches and kept as plain data. +/// The whole store notifies on change. pub struct ProfileStore { profiles: HashMap, - /// Public keys we've already requested this session (main thread only). + /// Public keys requested this session, main thread only. seen: RefCell>, /// Sender for queuing fetch requests, batched by a background task. sender: Sender, @@ -111,8 +113,8 @@ impl ProfileStore { _ => {} }); - // Fetch requests are queued on a channel and synced in batches by a - // background task. + // Fetch requests are queued on a channel. + // A background task syncs them in batches. let client = backend.read(cx).client(); let (sender, receiver) = flume::unbounded::(); let (dispatch_tx, dispatch_rx) = flume::unbounded::(); @@ -143,8 +145,9 @@ impl ProfileStore { store } - /// Get a profile. Returns a placeholder (default metadata) and queues a - /// fetch if the profile isn't cached yet. + /// Get a profile. + /// Returns a placeholder with default metadata. + /// Queues a fetch when the profile is not cached yet. pub fn get(&self, public_key: &PublicKey) -> Profile { if let Some(profile) = self.profiles.get(public_key) { return profile.clone(); @@ -170,7 +173,8 @@ impl ProfileStore { let filter = Filter::new().kind(Kind::Metadata).limit(200); let events = client.database().query(filter).await?; - // Parse off the main thread; only plain profiles cross back. + // Parse off the main thread. + // Only plain profiles cross back. let profiles: Vec = events .into_iter() .map(|event| { @@ -205,7 +209,8 @@ impl ProfileStore { let filter = Filter::new().kind(Kind::Metadata).author(public_key); let events = client.database().query(filter).await?; - // Parse off the main thread; only the profile crosses back. + // Parse off the main thread. + // Only the profile crosses back. let profile = events .into_iter() .max_by_key(|e| e.created_at) @@ -231,8 +236,8 @@ impl ProfileStore { })); } - /// Re-read the latest metadata of every requested author from the local - /// database (used after a sync, which produces no NostrUpdate events). + /// Re-read the latest metadata of every requested author from the local database. + /// Used after a sync, which produces no NostrUpdate events. fn apply_seen(&mut self, cx: &mut Context) { let authors: Vec = self.seen.borrow().iter().copied().collect(); @@ -286,9 +291,9 @@ impl ProfileStore { })); } - /// Sync metadata for requested authors in batches, debounced to collect - /// requests. Runs on a background thread; results are dispatched to the - /// main thread, which re-reads the database. + /// Sync metadata for requested authors in batches, debounced to collect requests. + /// Runs on a background thread. + /// Results are dispatched to the main thread, which re-reads the database. async fn handle_requests( client: &Client, dispatch: &Sender, @@ -316,9 +321,9 @@ impl ProfileStore { .kind(Kind::Metadata) .authors(batch.drain().collect::>()); - // Negentropy-sync with the bootstrap relays. Synced events are - // written to the database directly (no NostrUpdate), so re-apply - // from the database afterwards. + // Negentropy-sync with the bootstrap relays. + // Synced events are written to the database directly, no NostrUpdate. + // Re-apply from the database afterwards. match sync_bootstrap_only(client, filter, SyncOptions::default()).await { Ok(_) => { if dispatch.send(Dispatch::Synced).is_err() { diff --git a/crates/signed_state/src/repo.rs b/crates/signed_state/src/repo.rs index 372c550..45c61c6 100644 --- a/crates/signed_state/src/repo.rs +++ b/crates/signed_state/src/repo.rs @@ -19,17 +19,17 @@ use crate::backend::{ }; use crate::git_store::GitStore; -/// Delay between a refresh request and the actual re-query, so bursts of -/// events (e.g. per-event `NostrUpdate`s) collapse into one query. +/// Delay between a refresh request and the actual re-query. +/// Bursts of events, e.g. per-event `NostrUpdate`s, collapse into one query. const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300); -/// Maximum size of one patch event, following NIP-34's guidance that -/// patches should be used when each event is under 60kb. +/// Maximum size of one patch event. +/// NIP-34 suggests patches when each event is under 60kb. const MAX_PATCH_EVENT_BYTES: usize = 60 * 1024; -/// Per-repository store: announcement, state, issues, patches, PRs, -/// comments and their resolved statuses. Always derived from the local -/// database. +/// Per-repository store. +/// Holds the announcement, state, issues, patches, PRs, comments and resolved statuses. +/// Always derived from the local database. pub struct RepoStore { addr: RepoAddr, pub announcement: Option, @@ -42,34 +42,33 @@ pub struct RepoStore { pub pull_requests: Vec, /// Comments on issues / PRs, oldest first. pub comments: Vec, - /// Resolved status per root event (issue / patch / PR), recomputed on - /// every refresh so render paths are HashMap lookups instead of - /// scanning all status events per root. + /// Resolved status per root event, issue, patch or PR. + /// Recomputed on every refresh. + /// Render paths are HashMap lookups instead of per-root status scans. + /// Those scans are quadratic, with an allocation per pair. status_by_root: HashMap, - /// Open issue / root PR counts, computed with [`Self::status_by_root`] - /// on every refresh. + /// Open issue and root PR counts. + /// Computed with [`Self::status_by_root`] on every refresh. open_issue_count: usize, open_pr_count: usize, - /// Kind-1624 cover notes and kind-1985 label events referencing this - /// repository's roots (ngit / GitWorkshop extensions). + /// Kind-1624 cover notes and kind-1985 label events. + /// They reference this repository's roots, used by ngit and GitWorkshop. cover_notes: Vec, labels: Vec, - /// Incremented on every applied refresh; views key their derived-data - /// caches to it instead of recomputing on every render. + /// Incremented on every applied refresh. + /// Views key their derived-data caches to it instead of recomputing on every render. version: u64, /// Error of the last action initiated from this store, if any. pub last_error: Option, - /// Non-fatal warning of the last action (e.g. a PR published without - /// its commit reaching a grasp server), if any. + /// Non-fatal warning of the last action, if any. + /// Example, a PR published without its commit reaching a grasp server. pub last_warning: Option, - /// Relays announced by this repository (NIP-34 `relays` tag) that we - /// have already been asked to connect to and fetch from, to avoid - /// re-subscribing on every refresh. + /// Relays already asked to connect to, from this repository's NIP-34 `relays` tag. + /// Avoids re-subscribing and re-fetching on every refresh. repo_relays: HashSet, - /// Root events (issues, patches, PRs) for which the per-root fetches - /// (NIP-22 comments, statuses without an `a` tag, cover notes and - /// labels) have already been requested, to avoid re-fetching on every - /// refresh. + /// Root events, issues, patches and PRs, already fetched per root. + /// The per-root fetches cover NIP-22 comments and statuses without an `a` tag. + /// Also kind-1624 cover notes and kind-1985 labels. root_fetches: HashSet, refreshing: bool, refresh_dirty: bool, @@ -92,12 +91,12 @@ impl RepoStore { 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, so they can't be - // matched by coordinate; any comment may reference this - // repository's roots. + // 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), so any - // status event may reference a root of this repository. + // 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(); // Cover notes and labels carry no `a` tag either. let annotation = update.kind == COVER_NOTE_KIND || update.kind == Kind::Label; @@ -108,9 +107,8 @@ impl RepoStore { 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. + // 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; @@ -151,9 +149,9 @@ impl RepoStore { }; store.subscribe_remote(cx); - // The announcement we opened the repo from may already list its - // relays; connect to them right away instead of waiting for the - // bootstrap fetch to return the same event. + // The announcement we opened the repo from may already list its relays. + // Connect to them right away. + // Do not wait for the bootstrap fetch to return the same event. store.connect_announced_relays(&announced_relays, cx); store.refresh(cx); store @@ -164,7 +162,7 @@ impl RepoStore { &self.addr } - /// Returns the repository's name, or "Unknown" if not known. + /// Returns the repository's name, or `Unknown` when not known. pub fn name(&self) -> SharedString { self.announcement .as_ref() @@ -173,29 +171,26 @@ impl RepoStore { }) } - /// Filters that make up a repository: announcement, state, - /// activity and deletions targeting it. + /// Filters that make up a repository. + /// Announcement, state, activity and deletions targeting it. fn repo_filters(addr: &RepoAddr) -> Vec { let mut filters = vec![ - // Announcement and state share author and identifier, so they - // combine into one filter: one fewer negentropy reconciliation - // per relay when fetching from the repo's announced relays. + // Announcement and state share author and identifier. + // They combine into one filter, one fewer negentropy reconciliation per relay. Filter::new() .kinds([Kind::GitRepoAnnouncement, Kind::RepoState]) .author(addr.public_key) .identifier(addr.identifier.clone()), filters::activity(addr), ]; - // Deletion requests (NIP-09/62) must be known before any event of - // this repository can be shown. + // Deletion requests, NIP-09/62, must be known before any event is shown. filters.extend(filters::deletions_for_repo(addr)); filters } - /// Fetch this repository's events from the relays announced in its - /// NIP-34 `relays` tag. Deduplicated: each relay is only contacted once - /// per store, so refreshes after the first are no-ops unless the - /// announcement lists new relays. + /// Fetch this repository's events from the relays in its NIP-34 `relays` tag. + /// Deduplicated, each relay is contacted once per store. + /// Refreshes after the first are no-ops unless the announcement lists new relays. fn connect_announced_relays(&mut self, relays: &[RelayUrl], cx: &mut Context) { let new: Vec = relays .iter() @@ -214,7 +209,7 @@ 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) { let backend = Backend::global(cx); let addr = self.addr.clone(); @@ -225,9 +220,8 @@ impl RepoStore { } /// Re-query the local database and update all fields. - /// - /// The query and processing run on a background thread, - /// only the results are applied on the main thread. + /// The query and processing run on a background thread. + /// Only the results are applied on the main thread. pub fn refresh(&mut self, cx: &mut Context) { if self.refreshing { self.refresh_dirty = true; @@ -272,8 +266,8 @@ impl RepoStore { let deletions = Deletions::from_events(deletion_events); - // Parse and sort off the main thread; only plain data - // crosses back into the entity. + // Parse and sort off the main thread. + // Only plain data crosses back into the entity. let all_announcements = announcements .into_iter() .filter(|e| !deletions.is_deleted(e)); @@ -302,9 +296,8 @@ impl RepoStore { } } - // NIP-22 comments reference their root via an `E`/`e` tag rather - // than the repository's `a` tag, so query them by the root events - // of this repository. + // NIP-22 comments reference their root via an `E` or `e` tag. + // Not the repository's `a` tag, so query them by the root events. let mut seen_comments: HashSet = comments.iter().map(|e| e.id).collect(); let db = client.database(); @@ -322,8 +315,8 @@ impl RepoStore { } } - // Status events may omit their `a` tag, - // so also query them by the root events they reference. + // Status events may omit their `a` tag. + // Query them by the root events they reference too. let mut seen_statuses: HashSet = statuses.iter().map(|e| e.id).collect(); let db = client.database(); @@ -341,8 +334,8 @@ impl RepoStore { } } - // Cover notes (1624) and label events (1985) reference - // so query them per root like comments and statuses. + // Cover notes, 1624, and label events, 1985, carry no `a` tag. + // Query them per root like comments and statuses. let mut seen_cover_notes: HashSet = cover_notes.iter().map(|e| e.id).collect(); let mut seen_labels: HashSet = labels.iter().map(|e| e.id).collect(); let db = client.database(); @@ -373,9 +366,9 @@ impl RepoStore { sort_newest_first(&mut cover_notes); sort_newest_first(&mut labels); - // Resolve every root's status once here; render paths do - // HashMap lookups instead of scanning all status events per - // root (quadratic, with an allocation per pair). + // Resolve every root's status once here. + // Render paths do HashMap lookups instead of per-root status scans. + // Those scans are quadratic, with an allocation per pair. let maintainers = announcement .as_ref() .map(Announcement::effective_maintainers) @@ -441,8 +434,8 @@ impl RepoStore { let again = this.update(cx, |this, cx| { this.announcement = announcement; - // The announcement may list relays for this repository's activity, - // connect to any we haven't fetched from yet. + // The announcement may list relays for this repository's activity. + // Connect to any we have not fetched from yet. let relays = this .announcement .as_ref() @@ -466,10 +459,10 @@ impl RepoStore { this.labels = labels; this.version = this.version.wrapping_add(1); - // Comments, statuses without an `a` tag, cover notes and - // labels are not addressed to the repository, so fetch them - // by the root events they reference, on the bootstrap relays - // and on the relays this repository announced. + // Comments, statuses without an `a` tag, cover notes and labels. + // None are addressed to the repository. + // Fetch them by the root events they reference. + // Use the bootstrap relays and the relays this repository announced. let roots = this .issues .iter() @@ -486,10 +479,9 @@ impl RepoStore { if !new_roots.is_empty() { this.root_fetches.extend(new_roots.iter().copied()); - // Batch the per-root filters: one statuses filter and one - // annotations filter covering all new roots, instead of - // one filter per root (each filter is a separate - // negentropy reconciliation per relay). + // Batch the per-root filters. + // One statuses filter and one annotations filter cover all new roots. + // One filter per root costs a negentropy reconciliation per relay. let mut root_filters = filters::comments_for(new_roots.clone()); root_filters.push(filters::statuses_for(new_roots.iter().copied())); root_filters.push(filters::annotations_for(new_roots)); @@ -513,7 +505,8 @@ impl RepoStore { } })?; - // Requests that arrived while the refresh was running are coalesced into one follow-up refresh. + // Requests that arrived while the refresh was running. + // They are coalesced into one follow-up refresh. if again { this.update(cx, |this, cx| this.refresh(cx))?; } @@ -522,7 +515,7 @@ impl RepoStore { })); } - /// Resolve the status of a root event (issue / patch / PR) per NIP-34 + /// 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) } @@ -533,8 +526,8 @@ impl RepoStore { self.version } - /// The effective cover note of `root` (kind 1624), if any: - /// the latest note authored by the root author or a maintainer. + /// The effective cover note of `root`, kind 1624, if any. + /// The latest note authored by the root author or a maintainer. pub fn cover_note_of(&self, root: &Event) -> Option<&Event> { let maintainers = self .announcement @@ -545,8 +538,8 @@ impl RepoStore { cover_note(root, &self.cover_notes, &maintainers) } - /// The effective hashtag labels of `root`: its own `t` tags plus labels - /// from authorized NIP-32 kind-1985 events (`#t` namespace). + /// The effective hashtag labels of `root`. + /// Its own `t` tags plus labels from NIP-32 kind-1985 events in the `#t` namespace. pub fn labels_of(&self, root: &Event) -> Vec { let maintainers = self .announcement @@ -558,8 +551,8 @@ impl RepoStore { labels } - /// The effective subject/title override of `root` from authorized - /// kind-1985 events (`#subject` namespace), if any. + /// The effective subject or title override of `root`, if any. + /// Comes from authorized kind-1985 events in the `#subject` namespace. pub fn subject_of(&self, root: &Event) -> Option { let maintainers = self .announcement @@ -570,22 +563,23 @@ impl RepoStore { subject_override(root, &self.labels, &maintainers) } - /// Number of open issues: issues whose resolved status is - /// [`RepoStatus::Open`] (issues without status events default to open). + /// Number of open issues. + /// Issues whose resolved status is [`RepoStatus::Open`]. + /// Issues without status events default to open. pub fn issue_count(&self) -> usize { self.open_issue_count } - /// Number of open pull requests: root PR events (not PR updates, whose - /// status is carried by the root) with a resolved status of - /// [`RepoStatus::Open`]. + /// Number of open pull requests. + /// Only root PR events count, PR updates do not. + /// They must resolve to [`RepoStatus::Open`]. pub fn pull_request_count(&self) -> usize { self.open_pr_count } - /// Whether `user` is the author (owner) of this repository: the public - /// key of the repository address. Only the author may manage the - /// repository's pull requests (close / reopen / merge). + /// Whether `user` is the author or owner of this repository. + /// The author is the public key of the repository address. + /// Only the author may manage pull requests, close, reopen or merge. pub fn is_author(&self, user: &PublicKey) -> bool { &self.addr.public_key == user } @@ -603,19 +597,19 @@ impl RepoStore { self.send(builder, cx); } - /// Comments on a root event (issue / PR), oldest first. + /// Comments on a root event, an issue or PR, oldest first. pub fn comments_of(&self, root: &EventId) -> impl Iterator { self.comments .iter() .filter(move |e| signed_core::references_root(e, root)) } - /// Comment on a root event (issue / PR) per NIP-34 (kind 1111) + /// 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.reply(root, None, content, cx); } - /// Reply to `parent` (a comment on `root`) with a NIP-22 threaded comment, + /// Reply to `parent`, a comment on `root`, with a NIP-22 threaded comment. /// `None` publishes a top-level comment on the root itself. pub fn reply( &mut self, @@ -636,31 +630,32 @@ impl RepoStore { ); } - /// Open a pull request on this repository: a root PR event (kind 1618) - /// whose content is the markdown description, plus a root patch event - /// (kind 1617) carrying the `git format-patch` output, which the PR - /// references via an `e` tag (NIP-34). - /// - /// The patch series is published first (one kind-1617 event per commit, - /// chained with NIP-10 `e` replies, each under [`MAX_PATCH_EVENT_BYTES`]) - /// so the PR can reference the root patch's id. The proposed commit is - /// parsed from the series' last `From ` header (the tip); without - /// one publishing is refused, because the PR's `c` tag must carry a real - /// commit id for other NIP-34 clients to verify and apply the proposal. - /// - /// The `clone` tag carries the author's GRASP-06 `/prs/` URLs first - /// (resolved from their kind-10317 grasp list, falling back to the - /// settings defaults) plus the announced mirror URLs, so the tip is - /// downloadable on the author's own hosting even when the base project - /// accepts nothing. When `push_from` is set, the tip is pushed to those - /// servers under `refs/nostr/` (best-effort, author servers - /// first) before the PR is published; the linked patch stays the source - /// of truth either way. - /// - /// `branch_name` lands in the PR's `branch-name` tag (NIP-34); `draft` - /// publishes a kind-1633 status right after the PR event. `merge_base` - /// is the hex commit the proposed branch forked from, computed from a - /// local checkout when the patch was generated there. + /// Open a pull request on this repository. + /// A root PR event, kind 1618, carries the markdown description. + /// A root patch event, kind 1617, carries the `git format-patch` output. + /// The PR references the patch via an `e` tag, NIP-34. + /// The patch series is published first. + /// One kind-1617 event per commit, chained with NIP-10 `e` replies. + /// Each event stays under [`MAX_PATCH_EVENT_BYTES`]. + /// The PR then references the root patch's id. + /// The proposed commit is the series tip. + /// It comes from the last `From ` header. + /// Publishing is refused without one. + /// The PR's `c` tag must carry a real commit id. + /// Other NIP-34 clients verify and apply the proposal from it. + /// The `clone` tag lists the author's GRASP-06 `/prs/` URLs first. + /// Taken from the author's kind-10317 grasp list, else the settings defaults. + /// The announced mirror URLs follow. + /// The tip stays downloadable on the author's hosting. + /// This holds even when the base project accepts nothing. + /// With `push_from` set, the tip is pushed to those servers. + /// The ref is `refs/nostr/`, author servers first, best-effort. + /// The push happens before the PR is published. + /// The linked patch stays the source of truth either way. + /// `branch_name` lands in the PR's `branch-name` tag, NIP-34. + /// `draft` publishes a kind-1633 status right after the PR event. + /// `merge_base` is the hex commit the proposed branch forked from. + /// It is computed from a local checkout when the patch was generated there. #[allow(clippy::too_many_arguments)] pub fn open_pull_request( &mut self, @@ -694,7 +689,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 .last() .and_then(|part| patch_current_commit(part)) @@ -715,7 +711,7 @@ impl RepoStore { cx.notify(); return; }; - // 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_or_else(|_| user.to_hex()); let addr = self.addr.clone(); @@ -729,8 +725,8 @@ impl RepoStore { .map(|a| a.relays.clone()) .unwrap_or_default(); - // GRASP-06 hosting falls back to the settings defaults when - // the author has no published grasp list + // GRASP-06 hosting falls back to the settings defaults. + // That happens when the author has no published grasp list. let defaults: Vec = { let settings = settings::SettingsStore::global(cx).read(cx).settings(); let urls: Vec = if settings.grasp_servers.default_servers.is_empty() { @@ -747,7 +743,8 @@ impl RepoStore { }; self.tasks.push(cx.spawn(async move |this, cx| { - // The PR references the root patch event so viewers can find the patch without carrying it inline. + // The PR references the root patch event. + // Viewers can then find the patch without carrying it inline. let root_patch = match publish_patch_series( &this, cx, @@ -769,12 +766,11 @@ impl RepoStore { } }; - // GRASP-06: the tip is pushed to the author's own grasp servers - // under `/prs//.git`, so contributing to - // someone else's project never depends on their servers - // accepting the push. Resolve them 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. + // The path is `/prs//.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. let author_servers = { let query = this.update(cx, |_this, cx| { let client = Backend::global(cx).read(cx).client(); @@ -814,13 +810,15 @@ impl RepoStore { }; let builder = this.update(cx, |this, _cx| { - // NIP-34: PRs carry at least one clone URL where the tip - // commit can be downloaded. The author's `/prs/` URLs come - // first (author-controlled, most likely alive), then the - // announced mirrors. The list is fixed before signing: the - // pushed ref name embeds the event id, so every candidate - // URL is listed up front; dead URLs are inert, the linked - // patch stays the source of truth. + // NIP-34 PRs carry at least one clone URL. + // The tip commit is downloadable from it. + // The author's `/prs/` URLs come first. + // They are author-controlled and most likely alive. + // The announced mirrors follow. + // The list is fixed before signing. + // The pushed ref name embeds the event id. + // Every candidate URL is listed up front. + // Dead URLs are inert, the linked patch stays the source of truth. let prs_urls: Vec = author_targets .iter() .filter_map(|(url, _)| Url::parse(url).ok()) @@ -846,16 +844,16 @@ impl RepoStore { } .into_event_builder(); - // NIP-34: the `r` EUC tag lets clients subscribe to all PRs of this repository + // The `r` EUC tag lets clients subscribe to all PRs of the repository. match this.announcement.as_ref().and_then(|a| a.euc.clone()) { Some(euc) => builder.tag(Tag::parse(["r", &euc]).expect("valid r tag")), None => builder, } })?; - // Sign before publishing so the tip can be pushed to the grasp - // servers under `refs/nostr/` (nak's convention): - // readers fetch that ref to get the commit behind the `c` tag. + // Sign before publishing. + // The tip is pushed to the grasp servers under `refs/nostr/`. + // Nak's convention, readers fetch that ref for the commit behind the `c` tag. let event = cx .background_spawn({ let signer = signer.clone(); @@ -871,8 +869,8 @@ impl RepoStore { let path = path.clone(); let tip = tip.clone(); let reference = reference.clone(); - // Author servers first, then the base repository's - // announced grasp servers (best-effort redundancy). + // Author servers first, then the announced base grasp servers. + // The extra targets are best-effort redundancy. let targets: Vec<(String, String)> = author_targets .into_iter() .chain(base_targets) @@ -919,8 +917,8 @@ impl RepoStore { } }; - // NIP-34: a draft PR carries a kind-1633 status event, - // publish it right after the PR event so viewers never show it open. + // 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); @@ -931,11 +929,12 @@ impl RepoStore { })); } - /// Update a pull request: publish revision patch events chained to the - /// original root patch (`t root-revision` and a NIP-10 `e` reply on the - /// first, per NIP-34), then a kind-1619 PR update event carrying the new tip. - /// - /// Only the PR author may update it; other authors must open a new PR. + /// Update a pull request. + /// Publish revision patch events chained to the original root patch. + /// The first event carries `t root-revision` and a NIP-10 `e` reply, per NIP-34. + /// Then a kind-1619 PR update event carries the new tip. + /// Only the PR author may update it. + /// Other authors must open a new PR. pub fn update_pull_request(&mut self, root: &Event, patch: String, cx: &mut Context) { self.last_error = None; self.last_warning = None; @@ -984,9 +983,8 @@ impl RepoStore { return; }; - // NIP-34: the first patch of a revision replies to the original - // root patch (the PR's `e` tag; fall back to the oldest patch of - // the linked set for PRs without one). + // 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. let root_patch_id = root.tags.event_ids().next().or_else(|| { pull_request_patches(root, self.patches.iter()) .first() @@ -1033,8 +1031,8 @@ impl RepoStore { } .into_event_builder(); - // NIP-34: the `r` EUC tag lets clients subscribe to all PR - // updates of this repository; the SDK builder omits it. + // The `r` EUC tag lets clients subscribe to all PR updates. + // The SDK builder omits it. let builder = match euc.as_deref() { Some(euc) => builder.tag(Tag::parse(["r", euc]).expect("valid r tag")), None => builder, @@ -1055,9 +1053,9 @@ impl RepoStore { })); } - /// Set the status of a root event. Per NIP-34 only the root author or a - /// repository maintainer may set the status; status events from anyone - /// else are ignored by clients, so refuse them up front. + /// Set the status of a root event. + /// Only the root author or a maintainer may set it, per NIP-34. + /// Status events from anyone else are ignored by clients, so refuse them up front. pub fn set_status(&mut self, root: &Event, status: RepoStatus, cx: &mut Context) { self.last_error = None; @@ -1094,9 +1092,10 @@ impl RepoStore { self.send(builder, cx); } - /// Publish a repository state announcement (kind 30618) with the refs of - /// the local clone: branches, tags and HEAD. Only the repository owner - /// may publish state, and a local clone must exist to read the refs from. + /// Publish a repository state announcement, kind 30618. + /// It carries the local clone's branches, tags and HEAD. + /// Only the repository owner may publish state. + /// A local clone must exist to read the refs from. pub fn publish_state(&mut self, cx: &mut Context) { self.last_error = None; @@ -1146,17 +1145,17 @@ impl RepoStore { })); } - /// Merge a pull request: apply its patch (the content of the linked - /// root patch event) to the local clone of this repository, then publish - /// a kind-1631 (Applied) status event with merge provenance: the commits - /// `git am` created (`applied-as-commits` + `r` tags) and the applied - /// patch events (`q` tags, plus `e` reply tags for every patch beyond - /// the root, per NIP-34). - /// - /// Only the repository author may merge. The clone is created on demand - /// from the announcement's clone URLs when needed. Patch application - /// (`git am`) runs on a background thread; failures (e.g. a patch that - /// no longer applies) surface in [`Self::last_error`]. + /// Merge a pull request. + /// Apply its patch, the linked root patch event's content, to the local clone. + /// Then publish a kind-1631 Applied status event with merge provenance. + /// The provenance covers the commits `git am` created. + /// They appear as `applied-as-commits` and `r` tags. + /// It also tags the applied patch events. + /// `q` tags per event and `e` replies for every patch beyond the root, NIP-34. + /// Only the repository author may merge. + /// The clone is created on demand from the announcement's clone URLs. + /// Patch application, `git am`, runs on a background thread. + /// Failures, e.g. a patch that no longer applies, surface in [`Self::last_error`]. pub fn merge_pull_request(&mut self, root: &Event, cx: &mut Context) { self.last_error = None; self.last_warning = None; @@ -1198,8 +1197,8 @@ impl RepoStore { .workdir() .ok_or_else(|| anyhow::anyhow!("repository has no worktree"))? .to_path_buf(); - // The commits created by the apply: everything between the - // previous HEAD and the new one, oldest first. + // The commits the apply created. + // Everything between the previous HEAD and the new one, oldest first. let previous = signed_git::head_commit_id(&workdir)?; signed_git::apply_patch(&workdir, &patch)?; let applied = signed_git::commits_since(&workdir, previous.as_deref())?; @@ -1231,10 +1230,10 @@ impl RepoStore { })); } - /// Publish a kind-1631 (Applied) status event for `root` after a merge: - /// `applied-as-commits` + `r` tags for the commits `git am` created, - /// `q` tags for the applied patch events, and `e` reply tags for every - /// patch of the series beyond the root (NIP-34). + /// Publish a kind-1631 Applied status event for `root` after a merge. + /// `applied-as-commits` and `r` tags name the commits `git am` created. + /// `q` tags name the applied patch events. + /// `e` reply tags cover every patch of the series beyond the root, NIP-34. fn publish_applied_status( &mut self, root: &Event, @@ -1255,9 +1254,9 @@ impl RepoStore { { tags.push(tag); } - // The applied patch events: a `q` tag per event, plus an `e` reply - // for every event beyond the root (chain parts and revisions), so - // their statuses resolve to Applied too. + // Tag each applied patch event. + // `q` per event, `e` reply for events beyond the root, chain parts and revisions. + // Their statuses then resolve to Applied too. for (ix, patch) in patches.iter().enumerate() { if let Ok(tag) = Tag::parse(["q", &patch.id.to_hex(), relay_hint, &patch.pubkey.to_hex()]) @@ -1312,8 +1311,9 @@ where events.into_iter().max_by_key(|e| e.created_at) } -/// Status of `root` from the precomputed map; roots without status events -/// default to [`RepoStatus::Open`], like [`signed_core::resolve_status`]. +/// Status of `root` from the precomputed map. +/// Roots without status events default to [`RepoStatus::Open`]. +/// Matches [`signed_core::resolve_status`]. fn status_of(status_by_root: &HashMap, root: &Event) -> RepoStatus { status_by_root .get(&root.id) @@ -1321,10 +1321,10 @@ fn status_of(status_by_root: &HashMap, root: &Event) -> Rep .unwrap_or(RepoStatus::Open) } -/// Resolve the status of every root event in one pass: status events are -/// indexed by the root they reference (`e`/`E` tag), then each root -/// resolves against its own slice. O(roots + statuses) instead of the -/// O(roots × statuses) of resolving per root on demand. +/// Resolve every root event's status in one pass. +/// Status events are indexed by the root they reference, the `e` or `E` tag. +/// Each root resolves against its own slice. +/// Linear in roots and statuses, per-root resolution is their product. fn resolve_statuses( issues: &[Event], patches: &[Event], @@ -1364,20 +1364,21 @@ fn sort_oldest_first(events: &mut [Event]) { events.sort_by_key(|e| e.created_at); } -/// The proposed commit of a `git format-patch` output: the `From ` -/// header on its first line. +/// The proposed commit of a `git format-patch` output. +/// It is the `From ` 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) } -/// Publish a `git format-patch` series as chained kind-1617 events and -/// return the root event (the one a PR references). The first part carries -/// `first_marker` (`t root`, or `t root-revision` with an `e` reply to -/// `reply_to` for revisions); every later part replies to the previous one -/// (NIP-34). Every part gets the repository coordinate, the owner, its own -/// `commit`/`r` tags, and the repository EUC when known. +/// Publish a `git format-patch` series as chained kind-1617 events. +/// Returns the root event, the one a PR references. +/// The first part carries `first_marker`. +/// That is `t root`, or `t root-revision` with an `e` reply to `reply_to` for revisions. +/// Every later part replies to the previous one, NIP-34. +/// Every part gets the repository coordinate, the owner and its `commit` and `r` tags. +/// The repository EUC is added when known. #[allow(clippy::too_many_arguments)] async fn publish_patch_series( this: &WeakEntity, @@ -1444,10 +1445,11 @@ async fn publish_patch_series( root.ok_or_else(|| anyhow::anyhow!("patch series is empty")) } -/// Build a NIP-22 kind-1111 comment: uppercase `E`/`K`/`P` tags scope the -/// thread root, lowercase `e`/`k`/`p` the direct parent (or the root for a -/// top-level comment). An `a` tag with the repository coordinate (not part -/// of NIP-22) is added so Signed's own activity subscriptions also match. +/// Build a NIP-22 kind-1111 comment. +/// Uppercase `E`, `K` and `P` tags scope the thread root. +/// Lowercase `e`, `k` and `p` tag the direct parent, or the root for a top-level comment. +/// An `a` tag with the repository coordinate is added, not part of NIP-22. +/// Signed's own activity subscriptions then match it too. fn comment_builder( root: &Event, parent: Option<&Event>, @@ -1514,15 +1516,15 @@ mod tests { assert!(kinds.contains(&expected), "missing {expected} tag"); } - // The uppercase `E` tag scopes the root: id, relay hint and author. + // 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, which for a top-level - // comment is the root itself. + // 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()); @@ -1545,8 +1547,8 @@ mod tests { .finalize(&keys) .expect("signed event"); - // The uppercase `E` tag still scopes the root event, while the - // lowercase `e` tag references the parent comment. + // The uppercase `E` tag still scopes the root event. + // The lowercase `e` tag references the parent comment. let root_ref = event.tags.iter().find(|t| t.kind() == "E").expect("E tag"); let parent_ref = event.tags.iter().find(|t| t.kind() == "e").expect("e tag"); assert_eq!(root_ref.as_slice()[1], root.id.to_hex()); diff --git a/crates/signed_state/src/repo_list.rs b/crates/signed_state/src/repo_list.rs index 4b31623..981014d 100644 --- a/crates/signed_state/src/repo_list.rs +++ b/crates/signed_state/src/repo_list.rs @@ -9,8 +9,8 @@ use signed_core::{Announcement, Deletions, RepoAddr, filters, repo_addr}; use crate::backend::{Backend, BackendEvent}; -/// Delay between a refresh request and the actual re-query, so bursts of -/// events (e.g. sync progress ticks) collapse into one query. +/// Delay between a refresh request and the actual re-query. +/// Bursts of events, e.g. sync progress ticks, collapse into one query. const REFRESH_DEBOUNCE: Duration = Duration::from_millis(300); /// How far back activity events count toward a repository's last activity. @@ -20,40 +20,39 @@ struct GlobalRepoListStore(Entity); impl Global for GlobalRepoListStore {} -/// Counts of NIP-34 activity events per repository, used to rank the -/// explore list by popularity. Each patch event is a pushed commit (or a -/// small series), the closest proxy for commit count in the event data. +/// NIP-34 activity event counts per repository, ranking the explore list by popularity. +/// Each patch event is a pushed commit or a small series. +/// That is the closest proxy for commit count in the event data. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct RepoActivityCounts { /// Root `30611` issue events addressed to the repository. pub issues: u32, - /// Root `3063` pull request events addressed to the repository - /// (updates to a PR are not new PRs and don't count). + /// Root `3063` pull request events addressed to the repository. + /// PR updates are not new PRs and do not count. pub pull_requests: u32, /// `1617` patch events addressed to the repository. pub commits: u32, } impl RepoActivityCounts { - /// Total issues + pull requests + commits; the popularity ranking key. + /// Total issues, pull requests and commits, the popularity ranking key. pub fn score(self) -> u32 { self.issues + self.pull_requests + self.commits } } -/// Store listing repository announcements (global discovery or per-author). -/// -/// The all-repos store (`author: None`) is created at startup by -/// [`crate::init`] and installed as a global, so the explore panel renders -/// what's in the local database without waiting for relays. +/// Store listing repository announcements, global discovery or per-author. +/// The all-repos store, `author: None`, is created at startup by [`crate::init`]. +/// Installed as a global. +/// The explore panel renders from the local database without waiting for relays. pub struct RepoListStore { /// Shared so views can clone the list per frame without a deep copy. pub announcements: Arc>, - /// Latest known activity timestamp per repository - /// (announcements, state updates, patches, PRs, issues, statuses). + /// Latest known activity timestamp per repository. + /// Covers announcements, state updates, patches, PRs, issues and statuses. pub last_activity: Arc>, - /// Issues + pull requests + commits per repository, for the Popular - /// ranking of the explore list. + /// Issues, pull requests and commits per repository. + /// Used for the Popular ranking of the explore list. pub counts: Arc>, author: Option, refreshing: bool, @@ -65,8 +64,8 @@ pub struct RepoListStore { } impl RepoListStore { - /// Retrieve the global explore store (all announcements, created at - /// startup by [`crate::init`]). + /// Retrieve the global explore store. + /// It lists all announcements and is created at startup by [`crate::init`]. pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } @@ -82,12 +81,12 @@ impl RepoListStore { let subscription = cx.subscribe(&backend, |this, _backend, event, cx| { let relevant = match event { BackendEvent::NostrUpdate(update) => { - // Deletions may target anything we list; always refresh. + // Deletions may target anything we list, always refresh. if update.kind == Kind::EventDeletion || update.kind == Kind::RequestToVanish { true } else if filters::ACTIVITY_KINDS.contains(&update.kind) { - // Activity (patches, issues, ...) is addressed to repos via - // `a` tags, so its author isn't the repo owner; always refresh. + // Activity events are addressed to repos via `a` tags. + // Their author is not the repo owner, always refresh. true } else { let is_announcement = update.kind == Kind::GitRepoAnnouncement; @@ -99,9 +98,8 @@ impl RepoListStore { BackendEvent::Published(event) => { let announcement = event.kind == Kind::GitRepoAnnouncement && this.author.is_none_or(|a| a == event.pubkey); - // Locally published deletions (e.g. deleting a repo) - // are already in the local database; refresh so they - // take effect immediately, like relay deletions. + // Locally published deletions are already in the local database. + // Refresh so they take effect immediately, like relay deletions. let deletion = event.kind == Kind::EventDeletion || event.kind == Kind::RequestToVanish; announcement || deletion @@ -128,13 +126,13 @@ impl RepoListStore { }; store.subscribe_remote(cx); - // Query the local database right away; the list never waits for the - // relay syncs started above to finish. + // Query the local database right away. + // The list never waits for the relay syncs started above to finish. store.refresh_initial(cx); store } - /// Scope the list to an author (or clear the scope with `None`). + /// Scope the list to an author, or clear the scope with `None`. pub fn set_author(&mut self, author: Option, cx: &mut Context) { self.author = author; self.subscribe_remote(cx); @@ -152,14 +150,14 @@ impl RepoListStore { None => filters::all_announcements(), }; backend.sync_bootstrap(filter, cx); - // Deletion requests (NIP-09/62) must be known before any - // announcement can be shown. + // Deletion requests, NIP-09/62, must be known before any announcement is shown. backend.sync_bootstrap(filters::deletions(), cx); }); } - /// One-shot initial load: query the local database immediately (no - /// debounce), so stored announcements appear as soon as the app opens. + /// One-shot initial load. + /// Query the local database immediately, no debounce. + /// Stored announcements appear as soon as the app opens. /// Only called from [`Self::new`], before any refresh can be pending. fn refresh_initial(&mut self, cx: &mut Context) { debug_assert!(!self.debouncing); @@ -170,12 +168,12 @@ impl RepoListStore { self.run_refresh(cx); } - /// Re-query the local database. Latest announcement per repository wins. - /// - /// Debounced: a short delay collapses bursts of requests (e.g. sync - /// progress ticks), and requests that arrive while a query is running - /// are folded into one follow-up query. The query and processing run on - /// a background thread; only the results are applied on the main thread. + /// Re-query the local database. + /// The latest announcement per repository wins. + /// A short debounce collapses bursts of requests, e.g. sync progress ticks. + /// Requests that arrive while a query runs fold into one follow-up query. + /// The query and processing run on a background thread. + /// Only the results are applied on the main thread. pub fn refresh(&mut self, cx: &mut Context) { if self.refreshing { self.refresh_dirty = true; @@ -198,7 +196,7 @@ impl RepoListStore { self.tasks.push(task); } - /// One query + apply cycle (debounced entry point). + /// One query and apply cycle, the debounced entry point. fn run_refresh(&mut self, cx: &mut Context) { self.refreshing = true; @@ -216,8 +214,8 @@ impl RepoListStore { let deletion_events = client.database().query(filters::deletions()).await?; let deletions = Deletions::from_events(deletion_events); - // Dedup and sort off the main thread; only the final list - // crosses back into the entity. + // Dedup and sort off the main thread. + // Only the final list crosses back into the entity. let mut by_repo: HashMap = HashMap::new(); for event in events { @@ -242,8 +240,9 @@ impl RepoListStore { let mut announcements: Vec = by_repo.into_values().collect(); announcements.sort_by_key(|a| std::cmp::Reverse(a.created_at)); - // Last activity per repository: state updates plus all NIP-34 - // activity events (patches, PRs, issues, statuses). + // Last activity per repository. + // State updates count, and all NIP-34 activity events. + // The activity events are patches, PRs, issues and statuses. let mut last_activity: HashMap = announcements .iter() .map(|a| (a.addr(), a.created_at)) @@ -264,8 +263,8 @@ impl RepoListStore { *entry = (*entry).max(event.created_at); } - // Bound the activity query to a recent window; older repos fall - // back to their announcement / state timestamps. + // Bound the activity query to a recent window. + // Older repos fall back to their announcement or state timestamps. let activity_filter = Filter::new() .kinds(filters::ACTIVITY_KINDS) .since(Timestamp::now() - ACTIVITY_WINDOW); @@ -277,8 +276,8 @@ impl RepoListStore { if addr.kind != Kind::GitRepoAnnouncement { continue; } - // Skip events for repos we don't list, so the map can't - // grow beyond the number of announcements. + // Skip events for repos we do not list. + // The map cannot grow beyond the number of announcements. let Some(entry) = last_activity.get_mut(&addr) else { continue; }; @@ -286,9 +285,8 @@ impl RepoListStore { } } - // Popularity counts per repository (issues, pull requests and - // patches). Unbounded, unlike the windowed activity query - // above, so totals are exact. + // Popularity counts per repository, issues, pull requests and patches. + // Unbounded, unlike the windowed activity query above, so totals are exact. let mut counts: HashMap = HashMap::new(); let count_filter = Filter::new().kinds([Kind::GitIssue, Kind::GitPullRequest, Kind::GitPatch]); @@ -297,8 +295,8 @@ impl RepoListStore { continue; } for addr in event.tags.coordinates() { - // Skip events for repos we don't list, so the map can't - // grow beyond the number of announcements. + // Skip events for repos we do not list. + // The map cannot grow beyond the number of announcements. if addr.kind != Kind::GitRepoAnnouncement || !last_activity.contains_key(&addr) { continue; @@ -319,7 +317,7 @@ impl RepoListStore { self.tasks.push(cx.spawn(async move |this, cx| { let (announcements, last_activity, counts) = match work.await { Ok(results) => results, - // Database errors are transient; keep the last list. + // Database errors are transient, keep the last list. Err(_) => { return this.update(cx, |this, _cx| { this.refreshing = false; @@ -342,8 +340,8 @@ impl RepoListStore { } })?; - // Requests that arrived while the refresh was running are - // coalesced into one follow-up refresh. + // Requests that arrived while the refresh was running. + // They are coalesced into one follow-up refresh. if again { this.update(cx, |this, cx| this.refresh(cx))?; } diff --git a/crates/signed_ui/src/copy_row.rs b/crates/signed_ui/src/copy_row.rs index 8610c16..a5b136b 100644 --- a/crates/signed_ui/src/copy_row.rs +++ b/crates/signed_ui/src/copy_row.rs @@ -4,8 +4,8 @@ use gpui_component::clipboard::Clipboard; use gpui_component::menu::PopupMenuItem; use gpui_component::{ActiveTheme, StyledExt, h_flex}; -/// A muted command row with a copy button: the value in a mono-friendly, -/// truncated line, with a [`Clipboard`] button copying the full value. +/// A muted command row with a copy button. +/// The value renders truncated and a [`Clipboard`] button copies the full value. pub fn copy_row(copy_id: E, command: &SharedString, cx: &App) -> Div where E: Into, @@ -34,10 +34,11 @@ where ) } -/// One row of a copy menu: a small title above the compact label, with -/// a copy button that flips to a check while the value is on the clipboard. -/// Clicking the row copies and dismisses the menu; the copy button stops -/// propagation so the menu stays open. Both copy `copy`, never the label. +/// One row of a copy menu, with a small title above the compact label. +/// A copy button flips to a check while the value is on the clipboard. +/// Clicking the row copies and dismisses the menu. +/// The copy button stops propagation so the menu stays open. +/// Both copy `copy`, never the label. pub fn menu_copy_row( id: &'static str, title: &'static str, diff --git a/crates/signed_ui/src/dropdown_button.rs b/crates/signed_ui/src/dropdown_button.rs index f83aa4d..ae7126a 100644 --- a/crates/signed_ui/src/dropdown_button.rs +++ b/crates/signed_ui/src/dropdown_button.rs @@ -7,11 +7,10 @@ use gpui_base::{Button as BaseButton, Popover, Selectable, StyledExt}; use gpui_component::menu::PopupMenu; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex}; -/// A split dropdown button built on `gpui_base::Popover`: an action element -/// with a separate caret trigger that opens a [`PopupMenu`]. -/// -/// The action and the caret are caller-supplied elements, so the look stays -/// in the application; this component only owns the popover wiring. +/// A split dropdown button built on `gpui_base::Popover`. +/// An action element with a separate caret trigger that opens a [`PopupMenu`]. +/// The action and the caret are caller-supplied elements, so the look stays in the app. +/// This component only owns the popover wiring. #[derive(IntoElement)] pub struct DropdownButton { id: ElementId, @@ -38,15 +37,16 @@ impl DropdownButton { } } - /// The action half of the button. It keeps its own icon, label, tooltip - /// and click handler. + /// The action half of the button. + /// It keeps its own icon, label, tooltip and click handler. pub fn action(mut self, action: impl IntoElement + 'static) -> Self { self.action = Some(action.into_any_element()); self } - /// The menu built by `builder` — the same signature as gpui-component's - /// `DropdownButton::dropdown_menu`, so existing menu code keeps working. + /// The menu built by `builder`. + /// Matches gpui-component's `DropdownButton::dropdown_menu` signature. + /// Existing menu code keeps working. pub fn dropdown_menu( mut self, builder: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, @@ -55,10 +55,9 @@ impl DropdownButton { self } - /// Which corner of the caret the menu anchors to. Defaults to - /// [`Anchor::TopRight`], so the menu's right edge lines up with the - /// caret's. - #[allow(dead_code)] // API knob; current call sites use the default anchor. + /// Which corner of the caret the menu anchors to. + /// Defaults to [`Anchor::TopRight`], lining the menu's right edge up with the caret's. + #[allow(dead_code)] // API knob, current call sites use the default anchor. pub fn anchor(mut self, anchor: impl Into) -> Self { self.anchor = anchor.into(); self @@ -71,8 +70,8 @@ impl Styled for DropdownButton { } } -/// Holds the [`PopupMenu`] entity of one popover between renders. Dismissal -/// drops it, so the menu is rebuilt with fresh items on the next open. +/// Holds the [`PopupMenu`] entity of one popover between renders. +/// Dismissal drops it, so the menu is rebuilt with fresh items on the next open. #[derive(Default)] struct DropdownMenuState { menu: Option>, @@ -85,7 +84,8 @@ impl RenderOnce for DropdownButton { "a DropdownButton needs a `dropdown_menu`" ); - // The popover needs its own id: both the container and the popover register keyed state on this window. + // The popover needs its own id. + // The container and the popover both register keyed state on this window. let popover_id = SharedString::from(format!("{}-popover", self.id)); let anchor = self.anchor; let menu_state = @@ -109,8 +109,8 @@ impl RenderOnce for DropdownButton { this.child( Popover::new(popover_id) .anchor(anchor) - // The menu dismisses itself on outside click or Escape; - // the subscription below closes the popover along with it. + // The menu dismisses itself on outside click or Escape. + // The subscription below closes the popover along with it. .overlay_closable(false) .trigger_with(caret) .content( @@ -148,8 +148,8 @@ impl RenderOnce for DropdownButton { } } -/// The default caret: a chevron button the height of a medium button, tinted -/// by the theme, with hover and menu-open states. +/// The default caret, a chevron button the height of a medium button. +/// It is tinted by the theme and styled for hover and menu-open states. fn default_caret(id: impl Into, cx: &App) -> BaseButton { BaseButton::new(id) .h(px(32.)) diff --git a/crates/signed_ui/src/image_cache.rs b/crates/signed_ui/src/image_cache.rs index 58d0ff2..26da318 100644 --- a/crates/signed_ui/src/image_cache.rs +++ b/crates/signed_ui/src/image_cache.rs @@ -7,8 +7,8 @@ use gpui::{ ImageCacheItem, ImageCacheProvider, ImageSource, Resource, hash, }; -/// Default number of images each view's cache retains. Loading a new image -/// evicts the least recently used entry once this is reached. +/// Default number of images each view's cache retains. +/// Loading a new image evicts the least recently used entry once this is reached. pub const MAX_IMAGES: usize = 128; pub fn image_cache(id: impl Into, max_items: usize) -> AppImageCacheProvider { diff --git a/crates/signed_ui/src/lib.rs b/crates/signed_ui/src/lib.rs index 3af2bd6..f9cfafe 100644 --- a/crates/signed_ui/src/lib.rs +++ b/crates/signed_ui/src/lib.rs @@ -1,33 +1,3 @@ -//! Reusable UI components and elements for Signed. -//! -//! Everything here is presentation-only: the components read theme tokens -//! through [`gpui_component::ActiveTheme`] and render with plain GPUI -//! elements, so any view in the app can use them without depending on app -//! state. They are built on the unstyled `gpui-base` primitives and the -//! styled `gpui-component` library. -//! -//! The crate is organized by component: -//! -//! - [`PixelAvatar`] — deterministic, offline "pixel art" avatar -//! - [`NavItem`] — sidebar navigation row (leading element + label + suffix) -//! - [`DropdownButton`] — split button: an action plus a caret that opens a -//! [`PopupMenu`], wired through `gpui_base::Popover` -//! - [`SegmentButton`] / [`CountBadge`] — segmented filter/tab button with an -//! optional count badge -//! - [`UserAvatar`] — user picture avatar with a name-initials fallback -//! - [`status_badge`] — NIP-34 issue/PR status badge -//! - [`placeholder`] — centered muted placeholder message -//! - [`copy_row`] / [`menu_copy_row`] — rows with a copy-to-clipboard button -//! - [`tree_row`] — one row of a file tree -//! - [`setting_row`] / [`setting_block`] — label + description rows for a -//! settings dialog, with the control on the right (row) or below (block) -//! - [`SelectOption`] — dropdown option with a display label and a stored -//! value -//! - [`title_bar_drag_handlers`] — make an element behave like a window -//! title bar (drag moves the window, double-click zooms) -//! - [`image_cache`] — per-view LRU image cache provider -//! - [`middle_truncate`] — `[head]...[tail]` string truncation - mod dropdown_button; mod nav_item; mod pixel_avatar; diff --git a/crates/signed_ui/src/nav_item.rs b/crates/signed_ui/src/nav_item.rs index 69195e6..f59a13f 100644 --- a/crates/signed_ui/src/nav_item.rs +++ b/crates/signed_ui/src/nav_item.rs @@ -2,9 +2,10 @@ use gpui::prelude::*; use gpui::{App, ClickEvent, ElementId, SharedString, StyleRefinement, Window, div}; use gpui_component::{ActiveTheme, StyledExt, h_flex}; -/// A single navigation entry in a sidebar: an arbitrary leading element -/// (an icon, avatar, ...) and a text label with a hover highlight, -/// an optional trailing suffix (e.g. a status icon) and an optional click handler. +/// A single navigation entry in a sidebar. +/// It has an arbitrary leading element, such as an icon or avatar, and a text label. +/// Hover highlights the row. +/// It can carry a trailing suffix, such as a status icon, and an optional click handler. #[allow(clippy::type_complexity)] #[derive(IntoElement)] pub struct NavItem { @@ -12,7 +13,7 @@ pub struct NavItem { style: StyleRefinement, icon: gpui::AnyElement, label: SharedString, - /// Trailing element rendered at the right edge of the row, after the (ellipsized) label. + /// Trailing element at the right edge of the row, after the ellipsized label. suffix: Option, on_click: Option>, } diff --git a/crates/signed_ui/src/pixel_avatar.rs b/crates/signed_ui/src/pixel_avatar.rs index 601ba35..1e984b1 100644 --- a/crates/signed_ui/src/pixel_avatar.rs +++ b/crates/signed_ui/src/pixel_avatar.rs @@ -9,13 +9,15 @@ const GRID_SIZE: usize = 8; const FILL_PROBABILITY: f32 = 0.42; /// Probability that a filled cell uses the accent shade instead of the main color. const ACCENT_PROBABILITY: f32 = 0.25; -/// Minimum number of filled left-half cells, so a sparse roll still yields a -/// recognizable shape (each left-half cell is mirrored to a right-half one). +/// Minimum number of filled left-half cells. +/// A sparse roll still yields a recognizable shape. +/// Each left-half cell is mirrored to a right-half one. const MIN_FILLED: usize = 5; -/// A deterministic, offline "pixel art" avatar: an 8×8 grid with horizontal -/// mirror symmetry, seeded from a stable string such as the repository id and -/// owner public key. The same seed always renders the same avatar. +/// A deterministic, offline pixel-art avatar. +/// An 8×8 grid with horizontal mirror symmetry. +/// Seeded from a stable string such as the repository id and owner public key. +/// The same seed always renders the same avatar. #[derive(IntoElement)] pub struct PixelAvatar { seed: u64, @@ -24,8 +26,8 @@ pub struct PixelAvatar { } impl PixelAvatar { - /// Create an avatar seeded from `seed`. The seed should be a stable string - /// unique to the entity the avatar represents. + /// Create an avatar seeded from `seed`. + /// The seed should be a stable string unique to the entity the avatar represents. pub fn new(seed: impl AsRef) -> Self { Self { seed: fnv1a(seed.as_ref().as_bytes()), @@ -85,8 +87,9 @@ impl RenderOnce for PixelAvatar { } } -/// Generate the 8×8 cell pattern for `seed`. Cells are `0` (empty), `1` -/// (main color) or `2` (accent shade); the right half mirrors the left half. +/// Generate the 8×8 cell pattern for `seed`. +/// Cells are `0` for empty, `1` for main color and `2` for accent shade. +/// The right half mirrors the left half. fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] { let mut rng = PixelRng::new(seed); let mut pattern = [0u8; GRID_SIZE * GRID_SIZE]; @@ -102,8 +105,8 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] { } } - // Sparse rolls can come out nearly empty; top the pattern up to the - // minimum fill, scanning from a seeded starting cell. + // Sparse rolls can come out nearly empty. + // Top the pattern up to the minimum fill, scanning from a seeded starting cell. if filled < MIN_FILLED { let half = GRID_SIZE * GRID_SIZE / 2; let start = (rng.next() % half as u64) as usize; @@ -130,7 +133,7 @@ fn set_cell(pattern: &mut [u8; GRID_SIZE * GRID_SIZE], row: usize, col: usize, v pattern[row * GRID_SIZE + (GRID_SIZE - 1 - col)] = value; } -/// FNV-1a 64-bit hash; stable across platforms and runs. +/// FNV-1a 64-bit hash, stable across platforms and runs. fn fnv1a(bytes: &[u8]) -> u64 { let mut hash = 0xcbf2_9ce4_8422_2325u64; for &byte in bytes { diff --git a/crates/signed_ui/src/segment_button.rs b/crates/signed_ui/src/segment_button.rs index e13aa84..2160158 100644 --- a/crates/signed_ui/src/segment_button.rs +++ b/crates/signed_ui/src/segment_button.rs @@ -3,9 +3,9 @@ use gpui::{App, ClickEvent, ElementId, SharedString, StyleRefinement, Window, di use gpui_base::{Button as BaseButton, StyledExt}; use gpui_component::ActiveTheme; -/// A small count badge shown after a label, e.g. on a segmented filter -/// button ("All 12") or a tab. Rendered from theme tokens; sized for the -/// compact header buttons it lives on. +/// A small count badge shown after a label. +/// Used on segmented filter buttons, like `All 12`, and on tabs. +/// Rendered from theme tokens and sized for the compact header buttons it lives on. #[derive(IntoElement)] pub struct CountBadge { count: usize, @@ -46,12 +46,11 @@ impl RenderOnce for CountBadge { } } -/// A segmented filter/tab button: an icon, a label, an optional [`CountBadge`] -/// and a selected (pressed) state, styled from the theme's button tokens. -/// -/// Built on the unstyled `gpui_base::Button`, like the app's other custom -/// controls; the `primary` variant uses the primary button tokens for -/// call-to-action buttons ("New issue", "New PR"). +/// A segmented filter/tab button with an icon, a label and an optional [`CountBadge`]. +/// The selected state renders the button pressed, styled from theme button tokens. +/// Built on the unstyled `gpui_base::Button`, like the app's other custom controls. +/// The `primary` variant uses the primary button tokens. +/// It suits call-to-action buttons such as `New issue` and `New PR`. #[allow(clippy::type_complexity)] #[derive(IntoElement)] pub struct SegmentButton { @@ -101,7 +100,7 @@ impl SegmentButton { self } - /// Use the primary button tokens (for call-to-action buttons). + /// Use the primary button tokens, for call-to-action buttons. pub fn primary(mut self) -> Self { self.primary = true; self diff --git a/crates/signed_ui/src/setting.rs b/crates/signed_ui/src/setting.rs index e24c727..94a539c 100644 --- a/crates/signed_ui/src/setting.rs +++ b/crates/signed_ui/src/setting.rs @@ -1,15 +1,11 @@ -//! Reusable settings UI: rows and blocks for building a settings dialog, plus -//! a labeled dropdown option. - use gpui::prelude::*; use gpui::{App, SharedString, div}; use gpui_component::searchable_list::SearchableListItem; use gpui_component::{ActiveTheme, StyledExt, h_flex, v_flex}; /// A dropdown option with a display label and a stored value. -/// -/// Renders the `label` in the trigger and the menu, while `value` is what a -/// [`gpui_component::select::SelectState`] reports as the selection. +/// The trigger and menu render the `label`. +/// The `value` is what [`gpui_component::select::SelectState`] reports as the selection. #[derive(Clone)] pub struct SelectOption { value: SharedString, @@ -48,7 +44,7 @@ impl SearchableListItem for SelectOption { } } -/// A settings row: label + description on the left, control on the right. +/// A settings row with the label and description on the left and the control on the right. pub fn setting_row( cx: &App, title: impl Into, @@ -85,8 +81,8 @@ pub fn setting_row( ) } -/// A full-width settings block: title + subtitle in one header, `gap_3` -/// between the header and the control below. +/// A full-width settings block with title and subtitle in one header. +/// `gap_3` separates the header from the control below. pub fn setting_block( cx: &App, title: impl Into, diff --git a/crates/signed_ui/src/status_badge.rs b/crates/signed_ui/src/status_badge.rs index 771b023..e458c0c 100644 --- a/crates/signed_ui/src/status_badge.rs +++ b/crates/signed_ui/src/status_badge.rs @@ -5,8 +5,8 @@ use gpui_component::tooltip::Tooltip; use gpui_component::{ActiveTheme, Icon, Sizable, v_flex}; use signed_core::RepoStatus; -/// The status badge shown next to an issue or pull request: icon + colored square, -/// with a tooltip describing the status. +/// The status badge shown next to an issue or pull request. +/// It has an icon and a colored square, with a tooltip describing the status. pub fn status_badge(status: RepoStatus, cx: &App) -> AnyElement { let (icon, label, tooltip, bg, fg) = match status { RepoStatus::Open => ( diff --git a/crates/signed_ui/src/title_bar.rs b/crates/signed_ui/src/title_bar.rs index b194596..e9302f1 100644 --- a/crates/signed_ui/src/title_bar.rs +++ b/crates/signed_ui/src/title_bar.rs @@ -8,12 +8,12 @@ struct WindowDragState { should_move: bool, } -/// Make an element behave like a window title bar: dragging it moves the -/// window, and double-clicking zooms the window (or performs the platform's -/// default title-bar double-click action on macOS). -/// -/// Only the bar's non-interactive areas should get this — tabs are draggable -/// (to reorder panels) and must not move the window. +/// Make an element behave like a window title bar. +/// Dragging it moves the window. +/// Double-clicking zooms the window. +/// On macOS it runs the platform's default title-bar double-click action. +/// Only the bar's non-interactive areas should get this. +/// Tabs are draggable to reorder panels and must not move the window. pub fn title_bar_drag_handlers( this: Stateful
, window: &mut Window, diff --git a/crates/signed_ui/src/tree_row.rs b/crates/signed_ui/src/tree_row.rs index d2598d8..8dd2657 100644 --- a/crates/signed_ui/src/tree_row.rs +++ b/crates/signed_ui/src/tree_row.rs @@ -4,8 +4,9 @@ use gpui_component::list::ListItem; use gpui_component::tree::TreeEntry; use gpui_component::{Icon, IconName, Sizable, h_flex}; -/// One row of a file tree: icon + name, indented by depth. -/// Clicking a file runs `on_click`; folders expand/collapse via the tree itself. +/// One row of a file tree, an icon and a name indented by depth. +/// Clicking a file runs `on_click`. +/// Folders expand and collapse via the tree itself. pub fn tree_row(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem where F: Fn(&mut Window, &mut App) + 'static, diff --git a/crates/signed_ui/src/user_avatar.rs b/crates/signed_ui/src/user_avatar.rs index ec51c24..6d986a9 100644 --- a/crates/signed_ui/src/user_avatar.rs +++ b/crates/signed_ui/src/user_avatar.rs @@ -3,8 +3,8 @@ use gpui::{App, SharedString, StyleRefinement, Window}; use gpui_component::avatar::Avatar; use gpui_component::{ActiveTheme, Sizable, StyledExt}; -/// A user avatar: the gpui-component [`Avatar`] sized small and rounded with -/// the theme radius, showing the user's picture or a name-initials fallback. +/// A small user avatar from gpui-component [`Avatar`], rounded with the theme radius. +/// It shows the user's picture or falls back to name initials. #[derive(IntoElement)] pub struct UserAvatar { name: SharedString, @@ -13,8 +13,8 @@ pub struct UserAvatar { } impl UserAvatar { - /// Create an avatar for `name`; the name seeds the initials fallback - /// shown when no picture is set. + /// Create an avatar for `name`. + /// The name seeds the initials fallback shown when no picture is set. pub fn new(name: impl Into) -> Self { Self { name: name.into(), diff --git a/crates/signed_ui/src/util.rs b/crates/signed_ui/src/util.rs index e5d33ef..da9fe33 100644 --- a/crates/signed_ui/src/util.rs +++ b/crates/signed_ui/src/util.rs @@ -1,5 +1,5 @@ -/// `[head chars]...[tail chars]` middle truncation; the value is left alone -/// when it is too short for the ellipsis to save space. +/// `[head chars]...[tail chars]` middle truncation. +/// Values too short for the ellipsis to save space are left alone. pub fn middle_truncate(value: &str, head: usize, tail: usize) -> String { let len = value.chars().count(); if len <= head + tail + 3 { @@ -32,7 +32,7 @@ mod tests { ), "30617:a008...3564d:ngit" ); - // Too short to save space with the ellipsis: left alone. + // Too short to save space with the ellipsis, left alone. assert_eq!(middle_truncate("short", 10, 10), "short"); } } diff --git a/crates/utils/src/time.rs b/crates/utils/src/time.rs index b48310c..c2c39b6 100644 --- a/crates/utils/src/time.rs +++ b/crates/utils/src/time.rs @@ -1,6 +1,6 @@ use nostr::prelude::*; -/// Format a timestamp as a short relative time (e.g. "3h ago"). +/// Format a timestamp as a short relative time, e.g. `3h ago`. pub fn relative_time(timestamp: Timestamp) -> String { let now = Timestamp::now().as_secs(); let secs = now.saturating_sub(timestamp.as_secs()); @@ -20,7 +20,7 @@ pub fn relative_time(timestamp: Timestamp) -> String { } } -/// Format a unix timestamp in seconds as a short relative time (e.g. "3h ago"). +/// Format a unix timestamp in seconds as a short relative time, e.g. `3h ago`. pub fn relative_time_secs(secs: i64) -> String { relative_time(Timestamp::from_secs(secs.max(0) as u64)) } diff --git a/crates/workspace/src/views/repo_detail/about.rs b/crates/workspace/src/views/repo_detail/about.rs index e0b9535..7b19be1 100644 --- a/crates/workspace/src/views/repo_detail/about.rs +++ b/crates/workspace/src/views/repo_detail/about.rs @@ -7,8 +7,8 @@ use signed_core::Announcement; use signed_state::ProfileStore; use signed_ui::{UserAvatar, middle_truncate}; -/// Open the "About" dialog: every field of the repository's announcement -/// event (NIP-34, kind 30617), as parsed into [`Announcement`]. +/// Open the About dialog showing every field of the announcement event. +/// The event is NIP-34 kind 30617, parsed into [`Announcement`]. pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, cx: &mut App) { window.open_dialog(cx, move |dialog, _window, cx| { let announcement = announcement.clone(); @@ -22,8 +22,8 @@ pub(super) fn open_about_dialog(announcement: Announcement, window: &mut Window, }); } -/// The announcement's fields as labeled rows; hex identifiers carry a copy -/// button, multi-value tags one line per value. +/// The announcement's fields as labeled rows. +/// Hex identifiers carry a copy button, multi-value tags one line per value. fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement { let mut rows: Vec = Vec::new(); @@ -116,7 +116,7 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement { v_flex().gap_3().w_full().children(rows).into_any_element() } -/// One info row: a small muted label above the value. +/// One info row with a small muted label above the value. fn row(label: &'static str, value: AnyElement, cx: &App) -> AnyElement { v_flex() .gap_1() @@ -159,8 +159,9 @@ fn copy_value(id: &'static str, value: String, cx: &App) -> AnyElement { .into_any_element() } -/// One row per maintainer: avatar and display name (falling back to a -/// shortened npub), with a copy button for the full pubkey. +/// One row per maintainer with avatar and display name. +/// The display name falls back to a shortened npub. +/// A copy button copies the full pubkey. fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement { let profile_store = ProfileStore::global(cx); v_flex() @@ -190,8 +191,8 @@ fn maintainers(maintainers: &[PublicKey], cx: &App) -> AnyElement { .into_any_element() } -/// One row per item of a multi-value tag: the value is truncated to a single -/// line, with a copy button that copies the full value. +/// One row per item of a multi-value tag. +/// The value is truncated to a single line, with a copy button for the full value. fn list(id: &'static str, items: impl IntoIterator, cx: &App) -> AnyElement { v_flex() .gap_2() diff --git a/crates/workspace/src/views/repo_detail/browser.rs b/crates/workspace/src/views/repo_detail/browser.rs index eeda566..40cf3aa 100644 --- a/crates/workspace/src/views/repo_detail/browser.rs +++ b/crates/workspace/src/views/repo_detail/browser.rs @@ -16,8 +16,8 @@ use super::helpers::{code_language, is_markdown_path}; const TREE_WIDTH: f32 = 240.; /// Files larger than this are not previewed. pub(super) const MAX_PREVIEW_BYTES: usize = 1024 * 1024; -/// Preview cache caps: at most this many files (or this many text bytes) -/// are kept in memory at once; the oldest previews are evicted beyond that. +/// Preview cache caps, a file count and a text byte count. +/// The oldest previews are evicted beyond the caps. pub(super) const MAX_PREVIEWED_FILES: usize = 32; pub(super) const MAX_PREVIEW_CACHE_BYTES: usize = 8 * 1024 * 1024; @@ -34,19 +34,19 @@ pub(super) enum FileContent { } /// A markdown document loaded into a persistent [`TextViewState`]. -/// -/// The state is owned by the view rather than created per render: GPUI -/// drops keyed element state after one absent frame, which would re-parse -/// the whole document on every pane switch (README / file / spinner). +/// The state lives in the view rather than being created per render. +/// GPUI drops keyed element state after one absent frame. +/// A per-render state would re-parse the whole document on every pane switch. pub(super) struct MarkdownView { - /// Source path; `None` means the repository README. + /// Source path, `None` means the repository README. pub(super) path: Option, pub(super) state: Entity, } -/// A code file loaded into a persistent [`InputState`], rendered as a -/// disabled (read-only) code editor with syntax highlighting, line numbers -/// and search. Persistent for the same reason as [`MarkdownView`]. +/// A code file loaded into a persistent [`InputState`]. +/// It renders as a disabled, read-only code editor. +/// Syntax highlighting, line numbers and search are included. +/// Persistent for the same reason as [`MarkdownView`]. pub(super) struct CodeView { /// Source path, relative to the worktree root. pub(super) path: SharedString, @@ -64,7 +64,7 @@ fn preview_spinner() -> AnyElement { } impl RepoDetailView { - /// One row of the file tree: icon + name, indented by depth. + /// One row of the file tree with icon and name, indented by depth. fn render_tree_item( ix: usize, entry: &TreeEntry, @@ -81,7 +81,7 @@ impl RepoDetailView { }) } - /// Left column: the file tree. + /// Left column showing the file tree. pub(super) fn render_tree_column( tree_state: Entity, view: WeakEntity, @@ -102,7 +102,7 @@ impl RepoDetailView { ))) } - /// Right column: README, selected file preview, or status text. + /// Right column, README, selected file preview or status text. pub(super) fn render_content_column( &self, pane_title: SharedString, @@ -159,9 +159,8 @@ impl RepoDetailView { placeholder("No README found", cx) }; - // Latest commit for the current pane: the selected file, or the README - // while nothing is selected. Computed after the body above, which - // needs `&mut self`. + // Latest commit for the current pane, the selected file or the README. + // Computed after the body above, which needs `&mut self`. let commit = match &self.selected_file { Some(path) => self.commits.get(path.as_ref()), None => self @@ -217,9 +216,8 @@ impl RepoDetailView { } /// Load `text` into the persistent markdown TextView state. - /// - /// The state is created empty and fed via `push_str`, which parses on a - /// background task, so switching files never blocks the main thread. + /// The state is created empty and fed via `push_str`, which parses on a background task. + /// Switching files never blocks the main thread. pub(super) fn set_markdown( &mut self, path: Option, @@ -231,8 +229,8 @@ impl RepoDetailView { self.md = Some(MarkdownView { path, state }); } - /// The persistent markdown TextView for `path` (`None` = README), or a - /// spinner while the document is being loaded/parsed. + /// The persistent markdown TextView for `path`, where `None` is the README. + /// Shows a spinner while the document is being loaded or parsed. fn markdown_element(&self, path: Option<&str>, _cx: &mut Context) -> AnyElement { let Some(md) = &self.md else { return preview_spinner(); @@ -254,10 +252,8 @@ impl RepoDetailView { } /// Load `text` into the persistent code editor state for `path`. - /// - /// The state is created in code editor mode so the Input renders it as - /// a syntax-highlighted, read-only editor; the tree-sitter parse runs - /// on a background task like [`set_markdown`]'s. + /// Code editor mode makes the Input render it read-only and highlighted. + /// The tree-sitter parse runs on a background task like [`set_markdown`]'s. pub(super) fn set_code( &mut self, path: SharedString, @@ -276,8 +272,7 @@ impl RepoDetailView { self.code = Some(CodeView { path, state }); } - /// The persistent code editor for `path`, or a spinner while the file is - /// being loaded/parsed. + /// The persistent code editor for `path`, or a spinner while the file loads or parses. fn code_element(&self, path: &str, _cx: &mut Context) -> AnyElement { let Some(code) = &self.code else { return preview_spinner(); diff --git a/crates/workspace/src/views/repo_detail/commits.rs b/crates/workspace/src/views/repo_detail/commits.rs index 14ce94c..5d7eb7b 100644 --- a/crates/workspace/src/views/repo_detail/commits.rs +++ b/crates/workspace/src/views/repo_detail/commits.rs @@ -70,8 +70,8 @@ pub(super) fn commit_row( } impl RepoDetailView { - /// Full-height body of the Commits tab: all commits in a virtual - /// list, or a status message while loading / when there are none. + /// Full-height body of the Commits tab. + /// All commits in a virtual list, or a status message while loading or empty. pub(super) fn render_commits_tab(&self, cx: &mut Context) -> AnyElement { let Some(list) = self.all_commits.as_ref() else { return if self.loading_all_commits { @@ -90,9 +90,9 @@ impl RepoDetailView { return placeholder("No commits found", cx); } - // Copy only the values the element tree needs; the list itself is - // borrowed inside the renderer below instead of being cloned per - // frame (a full history can be tens of thousands of commits). + // Copy only the values the element tree needs. + // The list is borrowed by the renderer below instead of cloned per frame. + // A full history can be tens of thousands of commits. let view = cx.entity().clone(); let sizes = self.item_sizes.clone(); let scroll_handle = self.scroll_handle.clone(); @@ -137,7 +137,8 @@ impl RepoDetailView { .size_full(), ) .when(shown < total, |this| { - // The history is capped; tell the user the list is truncated. + // The history is capped. + // Tell the user the list is truncated. this.child( div() .py_2() diff --git a/crates/workspace/src/views/repo_detail/diff.rs b/crates/workspace/src/views/repo_detail/diff.rs index e724352..9aca129 100644 --- a/crates/workspace/src/views/repo_detail/diff.rs +++ b/crates/workspace/src/views/repo_detail/diff.rs @@ -28,19 +28,18 @@ use super::helpers::{ /// Width of the changed-files column. const TREE_WIDTH: f32 = 260.; -/// The tree + per-file diff body shared by the commit diff panel and the -/// compare view of the new-pull-request panel. Owns the changed-files -/// explorer and the virtual list of the selected file's hunks; the host -/// feeds it a [`CommitDiff`] via [`DiffPane::set_diff`]. +/// Tree and per-file diff body, shared by the commit diff and compare views. +/// Owns the changed-files explorer and the virtual list of the selected file's hunks. +/// The host feeds it a [`CommitDiff`] via [`DiffPane::set_diff`]. pub struct DiffPane { - /// Loaded diff; `None` until [`Self::set_diff`] is called. + /// Loaded diff, `None` until [`Self::set_diff`] is called. diff: Option, /// Changed-files explorer state. tree_state: Entity, /// Path of the file whose diff is shown in the detail column. selected_file: Option, - /// Rows of the selected file's diff (hunk headers + lines), backing the - /// virtual list in the detail column. + /// Rows of the selected file's diff, hunk headers and lines. + /// Backing the virtual list in the detail column. rows: Vec, /// Per-row heights of [`Self::rows`]. item_sizes: Rc>>, @@ -90,8 +89,8 @@ impl DiffPane { } } - /// Forget the diff (e.g. when the compared branches changed): clear the - /// tree, the selection and the diff rows. + /// Forget the diff, e.g. when the compared branches changed. + /// Clears the tree, the selection and the diff rows. pub fn clear(&mut self, cx: &mut Context) { self.diff = None; self.selected_file = None; @@ -102,15 +101,14 @@ impl DiffPane { }); } - /// Show the diff of the file at `path` (selected in the tree). + /// Show the diff of the file at `path`, selected in the tree. fn select_file(&mut self, path: &str, cx: &mut Context) { self.selected_file = Some(path.into()); self.set_diff_rows(path); cx.notify(); } - /// Rebuild the virtual list state for the file at `path` and scroll back - /// to the top. + /// Rebuild the virtual list state for `path` and scroll back to the top. fn set_diff_rows(&mut self, path: &str) { let Some(diff) = self.diff.as_ref() else { return; @@ -123,7 +121,7 @@ impl DiffPane { self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top); } - /// One row of the changed-files tree: icon + name, indented by depth. + /// One row of the changed-files tree, icon and name, indented by depth. fn render_tree_item( ix: usize, entry: &TreeEntry, @@ -140,7 +138,7 @@ impl DiffPane { }) } - /// Left column: the changed-files tree. + /// Left column showing the changed-files tree. fn render_tree_column(&self, cx: &mut Context) -> AnyElement { let tree_state = self.tree_state.clone(); let view = cx.entity().downgrade(); @@ -170,7 +168,7 @@ impl DiffPane { .into_any_element() } - /// Right column: header of the selected file plus its diff. + /// Right column, header of the selected file plus its diff. fn render_detail_column(&self, cx: &mut Context) -> AnyElement { let Some(diff) = self.diff.as_ref() else { return placeholder("No changes", cx); @@ -188,8 +186,9 @@ impl DiffPane { self.render_file_diff(file, cx.entity(), cx) } - /// The diff of one file: a header with status and stats, then the hunks - /// in a virtual list (a large diff is never materialized per frame). + /// The diff of one file, with a header showing status and stats. + /// The hunks render in a virtual list. + /// A large diff is never materialized per frame. fn render_file_diff(&self, file: &FileDiff, view: Entity, cx: &App) -> AnyElement { let status_label = match file.status { DiffStatus::Added => "A", @@ -316,25 +315,24 @@ impl Render for DiffPane { } } -/// Detail panel showing the diff of one commit: a metadata header plus the -/// shared [`DiffPane`] body. +/// Detail panel showing the diff of one commit. +/// A metadata header plus the shared [`DiffPane`] body. pub struct CommitDiffView { focus_handle: FocusHandle, /// Local clone the commit lives in. worktree: PathBuf, /// Display name of the repository the commit belongs to. repo_name: SharedString, - /// The commit being shown (header and tab title). Starts as an id-only - /// stub; [`Self::load`] replaces it with the full metadata, which the - /// history list intentionally omits. + /// The commit being shown in the header and tab title. + /// Starts as an id-only stub, the history list omits the full metadata. + /// [`Self::load`] replaces the stub with the full metadata. commit: FileCommit, /// The diff is being computed on a background task. loading: bool, error: Option, - /// Changed-files explorer and per-file diff, shared with the compare - /// view of the new-pull-request panel. + /// Changed-files explorer and per-file diff, also used by the new PR panel's compare view. pane: Entity, - /// In-flight tasks; pruned on every push (see [`helpers::track`]). + /// In-flight tasks, pruned on every push, see [`helpers::track`]. tasks: Vec>>, } @@ -371,8 +369,8 @@ impl CommitDiffView { } } - /// Load the commit diff (and the full commit metadata) on a background - /// task and populate the tree. + /// Load the commit diff and the full commit metadata on a background task. + /// Then populate the tree. fn load(&mut self, window: &mut Window, cx: &mut Context) { self.loading = true; self.error = None; @@ -419,7 +417,7 @@ impl CommitDiffView { self.tasks.push(task); } - /// Header: commit id, summary, author/time and overall change stats. + /// Header with the commit id, summary, author/time and overall change stats. fn render_header(&self, cx: &mut Context) -> AnyElement { let commit = &self.commit; let (files, insertions, deletions) = self.pane.read(cx).diff().map_or((0, 0, 0), |diff| { diff --git a/crates/workspace/src/views/repo_detail/helpers.rs b/crates/workspace/src/views/repo_detail/helpers.rs index 7c51eb3..d70ab9b 100644 --- a/crates/workspace/src/views/repo_detail/helpers.rs +++ b/crates/workspace/src/views/repo_detail/helpers.rs @@ -11,9 +11,9 @@ use signed_core::Announcement; use signed_git::{DiffHunk, DiffLine, DiffLineKind, FileDiff}; use signed_ui::{menu_copy_row, middle_truncate}; -/// A `Send` file-tree node: the tree is built on a background thread and -/// converted into [`TreeItem`]s (which hold `Rc` state, -/// so they cannot cross threads) on the main thread. +/// A `Send` file-tree node, the build runs on a background thread. +/// The main thread converts the seeds into [`TreeItem`]s. +/// [`TreeItem`]s hold `Rc` state and cannot cross threads. pub(super) struct TreeItemSeed { /// Path of the node, relative to the worktree root. id: String, @@ -22,12 +22,10 @@ pub(super) struct TreeItemSeed { children: Vec, } -/// Convert tree seeds into [`TreeItem`]s, expanding every folder -/// when `expand_folders` is set. -/// -/// The commit diff explorer shows only changed files, -/// which is typically a handful of paths, so its folders start expanded; -/// the worktree explorer starts collapsed instead. +/// Convert tree seeds into [`TreeItem`]s. +/// Every folder is expanded when `expand_folders` is set. +/// The commit diff explorer shows only changed files, typically a handful. +/// Its folders start expanded, the worktree explorer's folders collapsed. pub(super) fn tree_items(seeds: Vec, expand_folders: bool) -> Vec { fn convert(seed: TreeItemSeed, expand_folders: bool) -> TreeItem { let mut item = TreeItem::new(seed.id, seed.label); @@ -48,14 +46,13 @@ pub(super) fn tree_items(seeds: Vec, expand_folders: bool) -> Vec< .collect() } -/// Build nested tree items from a flat, sorted (dirs-first) entry list. -/// -/// Returns [`TreeItemSeed`]s so the build can run off the main thread; a -/// worktree walk can yield tens of thousands of entries. Nodes live in an -/// arena and parents are found via a path -> index map, which keeps the -/// build linear in the number of path components. +/// Build nested tree items from a flat entry list sorted dirs-first. +/// Returns [`TreeItemSeed`]s so the build can run off the main thread. +/// A worktree walk can yield tens of thousands of entries. +/// Nodes live in an arena, parents are found via a path-to-index map. +/// That keeps the build linear in the number of path components. pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec { - // Node indices by full path, for O(1) parent lookup while inserting. + // Node indices by full path, so parents resolve in constant time while inserting. let mut index: HashMap = HashMap::new(); let mut nodes: Vec<(String, String, Vec)> = Vec::new(); let mut roots: Vec = Vec::new(); @@ -99,9 +96,8 @@ pub(super) fn build_tree_items(entries: &[PathBuf]) -> Vec { } /// The markdown fence language for a file path, or `None` for plain text. -/// -/// Names are chosen so `gpui_component`'s highlighter can resolve them -/// (`highlighter::Language::from_name` accepts short aliases such as `rs` and `js`). +/// Names resolve in `gpui_component`'s highlighter. +/// `highlighter::Language::from_name` accepts short aliases like `rs` and `js`. pub(super) fn code_language(path: &str) -> Option<&'static str> { let name = Path::new(path) .file_name() @@ -166,7 +162,7 @@ pub(super) fn is_markdown_path(path: &str) -> bool { } pub(super) struct ShareTargets { - /// NIP-19 `naddr1...` of the announcement (with its announced relays). + /// NIP-19 `naddr1...` of the announcement, with its announced relays. pub(super) naddr: String, /// Hex ID of the announcement event itself. pub(super) event_id: String, @@ -195,8 +191,8 @@ impl ShareTargets { } } - /// The share dropdown menu: one row per target, each showing a compact - /// label while the copy button (and row click) copy the full value. + /// The share dropdown menu, one row per target. + /// Each shows a compact label, the copy button and row click copy the full value. pub(super) fn menu(&self, menu: PopupMenu) -> PopupMenu { menu.min_w(px(340.)) .item(menu_copy_row( @@ -226,9 +222,9 @@ impl ShareTargets { } } -/// Shorten an naddr link to `/naddr1...[last tail chars]`, e.g. -/// `https://gitworkshop.dev/naddr1...abcd`. Only the label is shortened; -/// the value to be copied stays the full URL. +/// Shorten an naddr link to `/naddr1...[last tail chars]`. +/// `https://gitworkshop.dev/naddr1...abcd` is an example. +/// Only the label is shortened, the copied value stays the full URL. fn truncate_naddr_link(url: &str, tail: usize) -> String { let Some(end) = url.find("naddr1").map(|i| i + "naddr1".len()) else { return url.to_string(); @@ -244,7 +240,7 @@ pub(super) const GUTTER_WIDTH: f32 = 44.; /// Height of one row in a virtual diff list. pub(super) const DIFF_ROW_HEIGHT: f32 = 20.; -/// One row of a virtual diff list: a hunk header, or a line of a hunk. +/// One row of a virtual diff list, a hunk header or a line of a hunk. /// Shared by the commit diff and pull request diff viewers. #[derive(Clone, Copy)] pub(super) enum DiffRow { @@ -258,7 +254,7 @@ pub(super) enum DiffRow { Line { hunk: usize, line: usize }, } -/// The rows of `file`'s diff: one header row per hunk, then its lines. +/// The rows of `file`'s diff, one header row per hunk then its lines. pub(super) fn diff_rows(file: &FileDiff) -> Vec { let mut rows = Vec::new(); for (hunk_ix, hunk) in file.hunks.iter().enumerate() { @@ -276,7 +272,7 @@ pub(super) fn diff_rows(file: &FileDiff) -> Vec { rows } -/// One row of the virtual diff list: a hunk header or a single line. +/// One row of the virtual diff list, a hunk header or a single line. pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement { match row { DiffRow::Hunk { @@ -303,8 +299,8 @@ pub(super) fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> Any } } -/// One diff line: old and new line numbers in gutters, then the content, -/// tinted by kind (addition / deletion / context). +/// One diff line, old and new line numbers in the gutters. +/// The content is tinted by kind, addition, deletion or context. pub(super) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement { let bg = match line.kind { DiffLineKind::Addition => Some(cx.theme().success.opacity(0.2)), @@ -313,8 +309,8 @@ pub(super) fn render_diff_line(line: &DiffLine, cx: &App) -> AnyElement { }; let gutter = cx.theme().muted_foreground; - // Fixed height and nowrap: the virtual list assumes every row has - // the same height, so long lines are clipped instead of wrapped. + // Fixed height and nowrap, the virtual list assumes every row has the same height. + // Long lines are clipped instead of wrapped. h_flex() .w_full() .h(px(DIFF_ROW_HEIGHT)) @@ -379,7 +375,7 @@ mod tests { let items = build_tree_items(&entries); - // Input order is preserved (dirs-first, as produced by worktree_entries). + // Input order is preserved, dirs-first as produced by worktree_entries. assert_eq!(items.len(), 3); assert_eq!(items[0].label, "src"); assert_eq!(items[0].id, "src"); @@ -411,9 +407,9 @@ mod tests { #[test] fn tree_builder_merges_shared_prefixes() { - // File children of a directory arrive after other directories' - // entries (the worktree list is dirs-first globally); the shared - // prefix must still resolve to one node. + // File children of a directory arrive after other directories' entries. + // The worktree list is dirs-first globally. + // The shared prefix must still resolve to one node. let entries = vec![ PathBuf::from("a/x.txt"), PathBuf::from("b/y.txt"), @@ -461,7 +457,7 @@ mod tests { truncate_naddr_link("https://gitworkshop.dev/naddr1qqqxyzabc1234", 4), "https://gitworkshop.dev/naddr1...1234" ); - // No naddr1 prefix: unchanged. + // Without the naddr1 prefix, unchanged. assert_eq!( truncate_naddr_link("https://example.com/x", 4), "https://example.com/x" diff --git a/crates/workspace/src/views/repo_detail/init_dialog.rs b/crates/workspace/src/views/repo_detail/init_dialog.rs index 99c8c3c..f5edc7e 100644 --- a/crates/workspace/src/views/repo_detail/init_dialog.rs +++ b/crates/workspace/src/views/repo_detail/init_dialog.rs @@ -26,10 +26,9 @@ pub struct InitRepoState { } /// Open the Init dialog for the local repository at `local_path`. -/// -/// The dialog loads the user's default grasp servers (kind `10317` grasp -/// list) and falls back to the shared defaults when none are set. On -/// success the dialog closes and `view` switches into NIP-34 mode. +/// The dialog loads the user's default grasp servers, a kind `10317` grasp list. +/// It falls back to the shared defaults when the user has none set. +/// On success the dialog closes and `view` switches into NIP-34 mode. pub fn open( local_path: PathBuf, view: WeakEntity, @@ -153,8 +152,8 @@ pub fn open( }); } -/// Run the init flow; closes the dialog and switches the repository into -/// its NIP-34 mode on success. +/// Run the init flow. +/// Closes the dialog and switches the repository into NIP-34 mode on success. fn init_repository( local_path: PathBuf, inputs: (Entity, Entity), diff --git a/crates/workspace/src/views/repo_detail/issue_detail.rs b/crates/workspace/src/views/repo_detail/issue_detail.rs index 1cfe6bf..972b87f 100644 --- a/crates/workspace/src/views/repo_detail/issue_detail.rs +++ b/crates/workspace/src/views/repo_detail/issue_detail.rs @@ -25,7 +25,7 @@ pub struct IssueDetailView { store: Entity, issue_id: EventId, contents: HashMap, - /// Input state of the "leave a comment" textarea. + /// Input state of the comment textarea. comment_input: Entity, focus_handle: FocusHandle, } @@ -58,7 +58,7 @@ impl IssueDetailView { return div().into_any_element(); }; - // Participants: the issue author plus everyone who commented. + // Participants, the issue author plus everyone who commented. let mut participants: Vec = vec![issue.pubkey]; participants.extend(store.comments_of(&issue.id).map(|comment| comment.pubkey)); participants.sort_by_key(PublicKey::to_hex); @@ -138,8 +138,7 @@ impl IssueDetailView { let author = profile.name(); let picture = profile.picture(); let age = relative_time(comment.created_at); - // Comment bodies are cloned into shared strings once per - // comment, not on every render. + // Comment bodies become shared strings once per comment, not per render. let content = self .contents .entry(comment.id) diff --git a/crates/workspace/src/views/repo_detail/issues.rs b/crates/workspace/src/views/repo_detail/issues.rs index c07988b..77fd4e3 100644 --- a/crates/workspace/src/views/repo_detail/issues.rs +++ b/crates/workspace/src/views/repo_detail/issues.rs @@ -24,8 +24,9 @@ use utils::relative_time; use super::issue_detail::IssueDetailView; -/// Height of one issue row in the virtual list: `py_2` padding, a 32px -/// title line (`h_8`), a 24px meta line (`h_6`) and the 1px bottom border. +/// Height of one issue row in the virtual list. +/// `py_2` padding, a 32px `h_8` title line and a 24px `h_6` meta line. +/// Plus the 1px bottom border. const ISSUE_ROW_HEIGHT: f32 = 73.; /// Status filter of the issues list, chosen via the header's filter buttons. @@ -35,8 +36,8 @@ enum IssueFilter { All, /// Issues whose resolved status is [`RepoStatus::Open`]. Open, - /// Issues whose resolved status is [`RepoStatus::Closed`] or - /// [`RepoStatus::Applied`] (both are "done" states). + /// Issues whose resolved status is [`RepoStatus::Closed`]. + /// [`RepoStatus::Applied`] counts too, both are done states. Closed, } @@ -63,14 +64,14 @@ pub struct IssuesView { filter: IssueFilter, /// Per-row heights of the virtual list. item_sizes: Rc>>, - /// Number of rows [`Self::item_sizes`] was built for (the filtered issue count). + /// The filtered issue count [`Self::item_sizes`] was built for. issue_len: usize, - /// Indices into the store's `issues` matching [`Self::filter`]; the - /// virtual list renders this slice. Rebuilt only when the store - /// version or the filter changes, keyed by [`Self::cache_key`]. + /// Indices into the store's `issues` matching [`Self::filter`]. + /// The virtual list renders this slice. + /// Rebuilt only when the store version or the filter changes. + /// Keyed by [`Self::cache_key`]. visible_issues: Vec, - /// Header counts `(total, open, closed)`, rebuilt with - /// [`Self::visible_issues`]. + /// Header counts `(total, open, closed)`, rebuilt with [`Self::visible_issues`]. counts: (usize, usize, usize), /// Store version and filter the cached rows/counts were built from. cache_key: Option<(u64, IssueFilter)>, @@ -102,7 +103,7 @@ impl IssuesView { } } - /// Open the detail panel of `issue_id` at the bottom of the dock area. + /// Open the detail panel of `issue_id` in the dock area. fn open_issue_detail( &mut self, issue_id: EventId, @@ -120,8 +121,8 @@ impl IssuesView { }); } - /// Render one row of the issue list; `ix` is the row index and - /// `issue_ix` the index of the issue in the store's `issues`. + /// Render one row of the issue list. + /// `ix` is the row index, `issue_ix` the index in the store's `issues`. fn render_row(&self, ix: usize, issue_ix: usize, cx: &mut Context) -> AnyElement { let issue = &self.store.read(cx).issues[issue_ix]; let title = activity_subject(issue); @@ -184,8 +185,8 @@ impl IssuesView { } fn render_header(&self, cx: &mut Context) -> AnyElement { - // Counts of the last list rebuild (`render` rebuilds first when the - // store version or filter changed, so this is never stale). + // Counts of the last list rebuild. + // `render` rebuilds first when the store version or filter changed, so never stale. let (total, open, closed) = self.counts; h_flex() @@ -243,8 +244,8 @@ impl IssuesView { } } -/// Open the "new issue" dialog: a title and a content input that submit -/// through [`RepoStore::open_issue`] when confirmed. +/// Open the new issue dialog, a title and a content input. +/// Confirming submits through [`RepoStore::open_issue`]. pub(super) fn open_new_issue_dialog(store: Entity, window: &mut Window, cx: &mut App) { let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title")); let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue...")); @@ -333,8 +334,8 @@ impl Render for IssuesView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let filter = self.filter; - // Rebuild the filtered rows and header counts only when the store - // refreshed or the filter changed; other renders reuse the cache. + // Rows and counts are rebuilt only when the store refreshed or filter changed. + // Other renders reuse the cache. let version = self.store.read(cx).version(); if self.cache_key != Some((version, filter)) { let store = self.store.read(cx); @@ -360,8 +361,8 @@ impl Render for IssuesView { let count = self.visible_issues.len(); - // The virtual list's item count comes from `item_sizes`; rebuild it - // whenever the filtered issue count changes. + // The virtual list's item count comes from `item_sizes`. + // Rebuild it whenever the filtered issue count changes. if count != self.issue_len { self.issue_len = count; self.item_sizes = Rc::new(vec![size(px(0.), px(ISSUE_ROW_HEIGHT)); count]); diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index c726fc2..31e3dd9 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -66,35 +66,36 @@ use crate::views::repo_detail::new_pull_request::open_new_pull_panel; /// What kind of ref the header selectors switch to. #[derive(Clone, Copy, PartialEq, Eq)] enum RefKind { - /// A local branch (`refs/heads/*`); HEAD stays attached. + /// A local branch `refs/heads/*`, HEAD stays attached. Branch, - /// A tag (`refs/tags/*`); HEAD becomes detached. + /// A tag `refs/tags/*`, HEAD becomes detached. Tag, } /// Header actions dispatched by the dropdown menus of the header buttons. -/// `pub(super)`: the pull-request list panel offers the same New-PR / Send- -/// patch actions in its own dropdown. +/// `pub(super)` because the pull-request list panel shares this action set. +/// It offers the New-PR and Send-patch actions in its own dropdown. #[derive(Clone, Action, PartialEq, Eq)] #[action(namespace = repo_detail, no_json)] pub(super) enum RepoAction { - /// Open the "new issue" dialog. + /// Open the new issue dialog. NewIssue, - /// Open the "new pull request" dialog. + /// Open the new pull request dialog. NewPR, - /// Open the "send patch" panel. + /// Open the send patch panel. SendPatch, /// Open the about dialog. About, /// Re-push the repository to its grasp servers. Push, - /// Delete the repository from nostr (owner only). + /// Delete the repository from nostr, owner only. Delete, } -/// Everything loaded from the local clone for the explorer: the tree seeds, -/// README, refs and HEAD commit. Computed on a background thread (see -/// [`load_repo_data`]) and applied on the main thread. +/// Everything loaded from the local clone for the explorer. +/// The tree seeds, README, refs and HEAD commit. +/// Computed on a background thread, see [`load_repo_data`]. +/// Applied on the main thread. struct RepoData { tree: Vec, readme_path: Option, @@ -106,12 +107,13 @@ struct RepoData { head_commit: Option, } -/// Derived NIP-34 header data, cached so renders don't re-encode bech32 -/// share targets and rebuild clone command strings on every frame. +/// Derived NIP-34 header data. +/// Renders avoid re-encoding bech32 share targets per frame. +/// They also avoid rebuilding the clone command strings. struct HeaderCache { - /// Announcement event ID and owner NIP-05 this cache was built from; - /// rebuilt when either changes (a new announcement version, or the - /// owner's profile arriving with a NIP-05 identifier). + /// Announcement event ID and owner NIP-05 this cache was built from. + /// Rebuilt when either changes. + /// A new announcement version, or the owner's profile arriving with a NIP-05 identifier. key: (EventId, Option), announcement: Rc, share: Rc, @@ -120,54 +122,54 @@ struct HeaderCache { git_commands: Rc>, } -/// Detail view of a repository: header, stats, a file explorer with README -/// preview (cloned from the announcement's `clone` URLs), and metadata. +/// Detail view of a repository, header, stats and metadata. +/// A file explorer with README preview, cloned from the announcement's `clone` URLs. pub struct RepoDetailView { focus_handle: FocusHandle, - /// Dock area the detail view lives in; new panels (commit diffs) are - /// added there. + /// Dock area the detail view lives in. + /// New panels, commit diffs, are added there. dock_area: WeakEntity, - /// Snapshot taken at open time, shown until the store's first refresh - /// completes (and as a fallback while the store has no announcement). + /// Snapshot taken at open time. + /// Shown until the store's first refresh completes. + /// Also a fallback while the store has no announcement. /// `None` for local repositories that haven't been published yet. initial: Option, - /// Per-repository nostr store (announcement, issues, PRs, statuses). - /// `None` until a local repository is initialized (published) to - /// NIP-34. + /// Per-repository nostr store, holding announcement, issues, PRs and statuses. + /// `None` until a local repository is initialized to NIP-34. store: Option>, - /// Path of the local repository when opened from the scan; `None` once - /// it has been initialized to NIP-34 (or for announced repositories). + /// 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, - /// File explorer state (worktree of the local clone). + /// File explorer state, the worktree of the local clone. tree_state: Entity, /// Root of the local clone, for reading files on demand. worktree: Option, - /// Markdown document currently in the preview pane (README or a file). + /// Markdown document currently in the preview pane, README or a file. md: Option, /// Code file currently in the preview pane. code: Option, readme_name: Option, - /// Currently previewed file (relative path) and its contents. + /// Currently previewed file, a relative path, and its contents. selected_file: Option, files: HashMap, - /// Paths of cached previews, oldest first; feeds the eviction caps in - /// [`Self::evict_previews`]. + /// Paths of cached previews, oldest first. + /// Feeds the eviction caps in [`Self::evict_previews`]. file_order: VecDeque, /// Total text bytes held by [`Self::files`]. preview_bytes: usize, /// Reads in flight, to avoid duplicate loads. loading_files: HashSet, - /// Latest commit touching a previewed file (or the README), keyed by path. + /// Latest commit touching a previewed file or the README, keyed by path. commits: HashMap, - /// Paths queued for the next batched commit query (see [`Self::load_commits`]). + /// Paths queued for the next batched commit query, see [`Self::load_commits`]. pending_commits: Vec, /// A batched commit query is in flight. loading_commits: bool, - /// Active header tab: 0 = Files (tree), 1 = Commits. + /// Active header tab, 0 = Files tree, 1 = Commits. active_tab: usize, - /// Commits reachable from HEAD, newest first; `None` until the walk - /// finishes (or fails). `commits` may be capped by - /// [`CommitList`]; `total` feeds the tab badge. + /// Commits reachable from HEAD, newest first. + /// `None` until the walk finishes or fails. + /// [`CommitList`] caps the list, `total` feeds the tab badge. all_commits: Option, /// Commit walk in flight. loading_all_commits: bool, @@ -183,52 +185,51 @@ pub struct RepoDetailView { error: Option, /// Commit HEAD currently points to, shown in the header button. head_commit: Option, - /// Branch selector (header): local branches, searchable. + /// Branch selector in the header, local branches, searchable. branch_select: Entity>>, - /// Tag selector (header): tags, searchable. + /// Tag selector in the header, tags, searchable. tag_select: Entity>>, - /// A branch/tag switch is in flight (checkout plus explorer reload). + /// A branch/tag switch is in flight, checkout plus explorer reload. switching_ref: bool, - /// Bumped on every branch/tag switch; in-flight loads tagged with an - /// older generation are discarded when they complete. + /// Bumped on every branch/tag switch. + /// In-flight loads with an older generation are discarded when they complete. ref_generation: u64, - /// Derived NIP-34 header data (share targets, clone commands), - /// rebuilt only when the announcement or the owner's NIP-05 changes - /// instead of on every render. + /// Derived NIP-34 header data, share targets and clone commands. + /// Rebuilt only when the announcement or the owner's NIP-05 changes. + /// Not on every render. header_cache: Option, - /// In-flight tasks; finished tasks are pruned on every push, so the vec - /// stays bounded by the number of concurrent loads. + /// In-flight tasks, finished tasks are pruned on every push. + /// The vec stays bounded by the number of concurrent loads. tasks: Vec>>, /// Subscriptions keeping the selectors' confirm events alive. _subscriptions: Vec, - /// Observes the checkouts store, whose statuses feed the "ready to - /// contribute" banner of the repository panel. + /// Observes the checkouts store. + /// Its statuses feed the ready-to-contribute banner of the repository panel. _checkouts_subscription: Subscription, /// `(path, branch)` ready-suggestions dismissed by the user, per panel. banner_dismissed: HashSet<(PathBuf, String)>, - /// The announced HEAD the ready-statuses were last requested with, and - /// whether they were requested at all (re-requested only when the HEAD - /// — the base default — changes, e.g. when the store's first refresh - /// lands). + /// The announced HEAD the ready statuses were last requested with. + /// Whether they were requested at all. + /// Re-requested only when the HEAD, the base default, changes. + /// E.g. when the store's first refresh lands. ready_requested: bool, ready_head: Option, - /// Upstream repository (from this fork's `u` tag) the user asked to - /// open, while its announcement is still being fetched. + /// Upstream repository, from this fork's `u` tag, the user asked to open. + /// Its announcement is still being fetched. pending_upstream: Option, } impl RepoDetailView { - /// Open a repository announced on NIP-34: the store connects to the - /// announcement's relays and loads issues, PRs and statuses. + /// Open a repository announced on NIP-34. + /// The store connects to the announcement's relays and loads issues, PRs and statuses. pub fn new( dock_area: WeakEntity, initial: Announcement, window: &mut Window, cx: &mut Context, ) -> Self { - // The announcement we opened from already carries the repository's - // NIP-34 `relays` tag, so the store can connect to those relays - // immediately instead of waiting for the bootstrap fetch. + // The announcement we opened from already carries the NIP-34 `relays` tag. + // The store connects to those relays immediately, no bootstrap fetch wait. let addr = initial.addr(); let relays = initial.relays.clone(); let store = cx.new(|cx| RepoStore::new(addr, relays, cx)); @@ -245,10 +246,9 @@ impl RepoDetailView { view } - /// Open a local repository discovered by the scan. There is no - /// announcement and no nostr store until the user initializes - /// (publishes) it to NIP-34, so the header shows an Init button - /// instead of the NIP-34 actions. + /// Open a local repository discovered by the scan. + /// There is no announcement and no nostr store until the user publishes it to NIP-34. + /// The header shows an Init button instead of the NIP-34 actions. pub fn new_local( dock_area: WeakEntity, local_path: PathBuf, @@ -258,8 +258,8 @@ impl RepoDetailView { Self::new_common(dock_area, None, None, Some(local_path), window, cx) } - /// Shared construction: file explorer state, ref selectors and the - /// deferred repository load. + /// Shared construction. + /// File explorer state, ref selectors and the deferred repository load. fn new_common( dock_area: WeakEntity, initial: Option, @@ -270,7 +270,7 @@ impl RepoDetailView { ) -> Self { let tree_state = cx.new(|cx| TreeState::new(cx)); - // Empty until the clone completes; populated with the local refs. + // Empty until the clone completes, then filled with the local refs. let branch_select: Entity>> = cx.new(|cx| { ComboboxState::new( SearchableVec::new(Vec::::new()), @@ -292,9 +292,9 @@ impl RepoDetailView { let subscriptions = vec![ cx.subscribe_in(&branch_select, window, |this, _state, event, window, cx| { - // `Change` fires only when the selection actually changed - // (picking the already-selected branch emits nothing), so a - // confirmed value always means a switch. + // `Change` fires only when the selection actually changed. + // Picking the already-selected branch emits nothing. + // A confirmed value always means a switch. if let ComboboxEvent::Change(values) = event && let Some(name) = values.first() { @@ -360,18 +360,18 @@ impl RepoDetailView { } } - /// Load the repository and populate the file explorer. A local - /// (not yet published) repository is opened straight from disk. An - /// announced repository's local clone (if any) is loaded first without - /// touching the network, so an unreachable server can't block the - /// panel; a background fetch then refreshes the refs and commit list. + /// Load the repository and populate the file explorer. + /// A local, not yet published, repository opens straight from disk. + /// An announced repository's clone, if any, loads first without touching the network. + /// An unreachable server can't block the panel. + /// A background fetch then refreshes the refs and commit list. fn load_repo(&mut self, window: &mut Window, cx: &mut Context) { self.loading = true; self.error = None; cx.notify(); - // Local repositories live on disk at their scan path; there is no - // clone to ensure and no network refresh. + // Local repositories live on disk at their scan path. + // No clone step or network refresh applies here. if let Some(local_path) = self.local_path.clone() { let task = cx.spawn_in(window, async move |this, cx| { let data = cx @@ -401,8 +401,8 @@ impl RepoDetailView { let cache = GitStore::global(cx).cache().clone(); let addr = initial.addr(); let clone_urls: Vec = initial.clone.iter().map(ToString::to_string).collect(); - // Captured before the loads start: a branch/tag switch bumps it, and - // the refresh below is discarded when that happens. + // Captured before the loads start. + // A branch/tag switch bumps the generation, discarding the refresh below. let refresh_generation = self.ref_generation; let disk = { @@ -420,7 +420,7 @@ impl RepoDetailView { let disk = disk.await; let had_clone = matches!(&disk, Ok(Some(_))); - // No local clone yet: clone from the network (blocking), then load. + // No local clone yet, so clone from the network then load. let data = match disk { Ok(Some(data)) => Ok(data), Ok(None) => { @@ -445,9 +445,9 @@ impl RepoDetailView { cx.notify(); })?; - // Refresh the clone from the network in the background; when it - // completes, update the refs and commit list. Loads started - // before a branch/tag switch are discarded via the generation. + // Refresh the clone from the network in the background. + // When it completes, update the refs and commit list. + // Loads started before a branch/tag switch are discarded via the generation. if !had_clone { return Ok(()); } @@ -458,16 +458,15 @@ impl RepoDetailView { let Some(repo) = cache.open(&addr)? else { return Ok::<_, Error>(None); }; - // Best-effort: a failed fetch (e.g. offline) keeps the - // cached state, which is already shown. + // Best-effort, a fetch failure, e.g. offline, keeps the cached state. + // The state is already shown. signed_git::fetch_all(&repo).ok(); let worktree = repo.workdir().map(Path::to_path_buf); - // A fetch never moves a mirror's local branches, so a - // push landing on the grasp servers (own repo pushed - // from a checkout, or an update fetched here) would - // never show up. Fast-forward them from the remote, - // like `git pull --ff-only` on every branch; only the - // checked-out branch's worktree can change on disk. + // A fetch never moves a mirror's local branches. + // A push landing on the grasp servers would never show up. + // That covers own repo pushes from a checkout and updates fetched here. + // Fast-forward branches from the remote, like `git pull --ff-only`. + // Only the checked-out branch's worktree can change on disk. let moved = match &worktree { Some(worktree) => { signed_git::fast_forward_branches(worktree).unwrap_or(false) @@ -494,10 +493,9 @@ impl RepoDetailView { } if let Ok(Some((moved, branches, tags, current_branch, head_commit))) = refresh { if moved { - // The mirror caught up with the remote (e.g. the - // push of an owned checkout just landed): rebuild - // the explorer, previews and commit list from the - // updated worktree. + // The mirror caught up with the remote. + // E.g. the push of an owned checkout just landed. + // Rebuild the explorer, previews and commit list from the worktree. this.reload_worktree(cx); cx.notify(); return; @@ -539,8 +537,9 @@ impl RepoDetailView { self.tasks.push(task); } - /// Apply the loaded repository data: explorer tree, README preview, - /// ref selectors and HEAD commit, then start the commit-list walk. + /// Apply the loaded repository data. + /// Sets the explorer tree, README preview, ref selectors and HEAD commit. + /// Then starts the commit-list walk. fn apply_repo_data(&mut self, data: RepoData, window: &mut Window, cx: &mut Context) { let RepoData { tree, @@ -563,8 +562,8 @@ impl RepoDetailView { state.set_items(tree_items(tree, false), cx); }); - // Populate the branch/tag selectors with the local refs, - // selecting the branch HEAD points to. + // Populate the branch/tag selectors with the local refs. + // Select the branch HEAD points to. let branches: Vec = branches.into_iter().map(Into::into).collect(); let tags: Vec = tags.into_iter().map(Into::into).collect(); @@ -591,8 +590,8 @@ impl RepoDetailView { } } - /// Clone the repository into a folder chosen by the user (outside the cache), - /// then open the new clone in the system file manager. + /// Clone the repository into a user-chosen folder outside the cache. + /// Then open the new clone in the system file manager. fn clone_to_folder(&mut self, window: &mut Window, cx: &mut Context) { if self.cloning { return; @@ -605,8 +604,8 @@ impl RepoDetailView { let addr = announcement.addr(); let clone_urls: Vec = announcement.clone.iter().map(ToString::to_string).collect(); - // Directory name: the display name, falling back to the repo id; - // both sanitized to a safe single path component. + // Directory name, the display name falling back to the repo id. + // Both are sanitized to a safe single path component. let name = announcement .name .as_ref() @@ -633,8 +632,8 @@ impl RepoDetailView { }); let task = cx.spawn_in(window, async move |this, cx| { - // `Ok(Ok(Some(paths)))` means the user picked a folder; a - // cancel (or a picker failure) resolves to anything else. + // `Ok(Ok(Some(paths)))` means the user picked a folder. + // A cancel or picker failure resolves to anything else. let picked = match prompt.await { Ok(Ok(Some(mut paths))) => paths.pop(), _ => None, @@ -659,8 +658,8 @@ impl RepoDetailView { match result { Ok(_) => { cx.open_with_system(&destination_for_open); - // Remember the clone as a checkout of this - // repository, so the New PR panel pre-fills it. + // Remember the clone as a checkout of this repository. + // The New PR panel pre-fills it. let checkouts = CheckoutsStore::global(cx); checkouts.update(cx, |store, cx| { store.record(destination, addr, cx); @@ -679,15 +678,14 @@ impl RepoDetailView { self.tasks.push(task); } - /// Preview the file at `path` (relative to the worktree root). + /// Preview the file at `path`, relative to the worktree root. fn open_file(&mut self, path: &str, window: &mut Window, cx: &mut Context) { self.selected_file = Some(path.into()); if self.files.contains_key(path) { - // The file is cached, but the persistent markdown/code state may - // still hold a different file; re-point it at this one (the parse - // runs on a background task either way). Without this, the pane - // would show a spinner forever. + // The file is cached, but the markdown or code state may hold a different file. + // Re-point it at this one, the parse runs on a background task either way. + // Without this, the pane would show a spinner forever. if let Some(FileContent::Text(text)) = self.files.get(path) { let text = text.clone(); if is_markdown_path(path) { @@ -706,8 +704,8 @@ impl RepoDetailView { return; } - // Paths come from our own tree walk, but never trust them: refuse - // anything that could escape the worktree. + // Paths come from our own tree walk, but never trust them. + // Refuse anything that could escape the worktree. let rel = Path::new(path); let unsafe_path = rel.is_absolute() || rel.components().any(|c| { @@ -735,9 +733,9 @@ impl RepoDetailView { let content = cx .background_spawn(async move { let full = worktree.join(&path_for_read); - // Refuse oversized files before reading them: reading a - // multi-gigabyte file just to classify it as too large - // would waste the disk and memory bandwidth. + // Refuse oversized files before reading them. + // Reading a multi-gigabyte file just to classify it is wasteful. + // It would burn disk and memory bandwidth. let metadata = match std::fs::metadata(&full) { Ok(metadata) => metadata, Err(error) => return Err(anyhow::anyhow!("{}", error)), @@ -757,10 +755,10 @@ impl RepoDetailView { .await; this.update_in(cx, |this, window, cx| { - // The worktree was switched while this file was reading; - // the result belongs to the previous branch. Clear the - // in-flight marker either way, or the path could never be - // loaded again. + // The worktree was switched while this file was reading. + // The result belongs to the previous branch. + // Clear the in-flight marker either way. + // Otherwise the path could never be loaded again. if generation != this.ref_generation { this.loading_files.remove(&path); return; @@ -802,8 +800,8 @@ impl RepoDetailView { self.tasks.push(task); } - /// Queue `path` for the per-file commit query; requests are batched into - /// one history walk (see [`Self::load_commits`]). + /// Queue `path` for the per-file commit query. + /// Requests are batched into one history walk, see [`Self::load_commits`]. fn load_commit(&mut self, path: &str, cx: &mut Context) { if self.commits.contains_key(path) || self.pending_commits.iter().any(|p| p == path) { return; @@ -814,10 +812,10 @@ impl RepoDetailView { } } - /// Walk history once for every queued path on a background task, and - /// cache the latest commit touching each of them in [`Self::commits`] - /// (for the file header in the content column). Batching shares one - /// walk across all paths queued while the previous walk was in flight. + /// Walk history once for every queued path on a background task. + /// Cache the latest commit touching each path in [`Self::commits`]. + /// That feeds the file header in the content column. + /// Batching shares one walk across paths queued while the previous walk ran. fn load_commits(&mut self, cx: &mut Context) { if self.pending_commits.is_empty() || self.loading_commits { return; @@ -849,10 +847,9 @@ impl RepoDetailView { .insert(path.to_string_lossy().into_owned(), commit); } } - // Paths queued while the walk was in flight start the next - // batch. A stale walk (branch switched mid-flight) must not - // strand them, so this runs under the current generation - // regardless of whether the result was applied. + // Paths queued while the walk was in flight start the next batch. + // A stale walk, branch switched mid-flight, must not strand them. + // This runs under the current generation regardless of the result. if !this.pending_commits.is_empty() { this.load_commits(cx); } @@ -865,9 +862,9 @@ impl RepoDetailView { self.tasks.push(task); } - /// Walk all commits reachable from HEAD on a background task, for the - /// Commits tab and its total-count badge. The list is capped by - /// [`CommitList`]; only the newest commits are materialized. + /// Walk all commits reachable from HEAD on a background task. + /// For the Commits tab and its total-count badge. + /// [`CommitList`] caps the list, only the newest commits are materialized. fn load_all_commits(&mut self, cx: &mut Context) { if self.loading_all_commits || self.all_commits.is_some() { return; @@ -886,8 +883,8 @@ impl RepoDetailView { .await; this.update(cx, |this, cx| { - // A stale walk (branch switched mid-flight) must not leave - // the flag set, or the Commits tab would spin forever. + // A stale walk, branch switched mid-flight, must not leave the flag set. + // Otherwise the Commits tab would spin forever. if generation != this.ref_generation { this.loading_all_commits = false; return; @@ -907,9 +904,9 @@ impl RepoDetailView { self.tasks.push(task); } - /// Open a new panel showing the diff of `commit_id` (all files it - /// changed, with the line diff of each). Called from the Commits tab - /// rows and the latest-commit button in the header. + /// Open a new panel showing the diff of `commit_id`. + /// All files it changed, with the line diff of each. + /// Called from the Commits tab rows and the latest-commit button. fn open_commit_diff(&mut self, commit_id: &str, window: &mut Window, cx: &mut Context) { let Some(worktree) = self.worktree.clone() else { return; @@ -930,9 +927,9 @@ impl RepoDetailView { }); } - /// Re-push the repository's refs to its announced grasp servers; the - /// menu trigger shows a spinner while the push is in flight, failures - /// appear in the panel's error banner. + /// Re-push the repository's refs to its announced grasp servers. + /// The menu trigger shows a spinner while the push is in flight. + /// Failures appear in the panel's error banner. fn push_repository(&mut self, window: &mut Window, cx: &mut Context) { if self.pushing { return; @@ -960,10 +957,10 @@ impl RepoDetailView { })); } - /// Push the unpushed commits of the local checkout at - /// `path` (an owned repository's working copy) to the announced grasp servers, - /// failures appear in the panel's error banner, - /// and on success the push statuses are recomputed so the banner clears. + /// Push the unpushed commits of the local checkout at `path`. + /// The checkout is an owned repository's working copy. + /// Failures appear in the panel's error banner. + /// On success the push statuses are recomputed so the banner clears. fn push_unpushed_checkout( &mut self, path: PathBuf, @@ -978,8 +975,8 @@ impl RepoDetailView { return; }; - // Keep the repository's announced default branch as the state - // event's `HEAD` when the checkout is on a side branch. + // The state event's `HEAD` stays the announced default branch. + // The checkout may be on a side branch. let head = self .store .as_ref() @@ -1003,10 +1000,10 @@ impl RepoDetailView { this.update_in(cx, |this, window, cx| { match result { Ok(()) => { - // The remote moved; recompute the push statuses so - // the banner disappears, and refresh the mirror so - // the pushed commits appear in the panel right away - // (fetch + fast-forward + explorer reload). + // The remote moved, so recompute the push statuses. + // The banner disappears. + // Refresh the mirror, fetch, fast-forward and an explorer reload. + // The pushed commits then show in the panel. checkout.update(cx, |store, cx| { store.request_push_statuses(&addr, cx); }); @@ -1024,9 +1021,9 @@ impl RepoDetailView { })); } - /// Delete the repository from nostr (announcement, state and activity); - /// only offered to the repository owner. The sidebar list updates when - /// the deletion events arrive. + /// Delete the repository from nostr, announcement, state and activity. + /// Only offered to the repository owner. + /// The sidebar list updates when the deletion events arrive. fn delete_repository(&mut self, window: &mut Window, cx: &mut Context) { let Some(announcement) = self.announcement(cx).cloned() else { return; @@ -1048,7 +1045,7 @@ impl RepoDetailView { })); } - /// Open the issues panel at the bottom of the dock area. + /// Open the issues list panel in the dock area. fn open_issue_detail(&mut self, window: &mut Window, cx: &mut Context) { let Some(store) = self.store.clone() else { return; @@ -1064,7 +1061,7 @@ impl RepoDetailView { }); } - /// Open the pull requests panel at the bottom of the dock area. + /// Open the pull requests list panel in the dock area. fn open_pull_request_detail(&mut self, window: &mut Window, cx: &mut Context) { let Some(store) = self.store.clone() else { return; @@ -1080,9 +1077,9 @@ impl RepoDetailView { }); } - /// Open the upstream repository (the `u` tag of this fork's announcement). - /// When the upstream announcement is not in the local database yet, - /// subscribe for it and open the panel as soon as it lands. + /// Open the upstream repository, the `u` tag of this fork's announcement. + /// The upstream announcement may not be in the local database yet. + /// Subscribe for it and open the panel as soon as it lands. fn open_upstream(&mut self, window: &mut Window, cx: &mut Context) { if self.pending_upstream.is_some() { return; @@ -1151,8 +1148,8 @@ impl RepoDetailView { self.tasks.push(task); } - /// Check out `name` (a branch or tag picked in the header) and refresh - /// the explorer once the switch completes. + /// Check out `name`, a branch or tag picked in the header. + /// Refresh the explorer once the switch completes. fn switch_ref( &mut self, kind: RefKind, @@ -1167,9 +1164,9 @@ impl RepoDetailView { return; }; - // Branches and tags are mutually exclusive states of HEAD: selecting - // one clears the other selector. Remember the previous selections so - // they can be restored if the checkout fails. + // Branches and tags are mutually exclusive states of HEAD. + // Selecting one clears the other selector. + // Remember the previous selections to restore them if the checkout fails. let previous_branch = self.branch_select.read(cx).selected_value(); let previous_tag = self.tag_select.read(cx).selected_value(); @@ -1184,8 +1181,7 @@ impl RepoDetailView { } } self.switching_ref = true; - // In-flight loads of the previous branch are discarded when they - // complete. + // In-flight loads of the previous branch are discarded when they complete. self.ref_generation += 1; cx.notify(); @@ -1223,7 +1219,7 @@ impl RepoDetailView { self.tasks.push(task); } - /// Restore a selector to `previous`, or clear it (after a failed switch). + /// Restore a selector to `previous`, or clear it after a failed switch. fn restore_selection( &self, select: &Entity>>, @@ -1237,9 +1233,10 @@ impl RepoDetailView { }); } - /// Trigger body for the branch/tag selectors: the kind icon, the - /// selection (or placeholder) and the caret. `Combobox` replaces its - /// default trigger entirely, the only way to show an icon inside it. + /// Trigger body for the branch/tag selectors. + /// The kind icon, the selection or placeholder, and the caret. + /// `Combobox` replaces its default trigger entirely. + /// That is the only way to show an icon inside it. fn render_ref_trigger( ctx: &ComboboxTriggerContext>, icon: CustomIconName, @@ -1273,10 +1270,10 @@ impl RepoDetailView { .into_any_element() } - /// Refresh the file explorer, preview pane and commit list after a - /// successful branch or tag switch. The selectors were already updated - /// by [`Self::switch_ref`]; [`Self::switching_ref`] stays set until this - /// reload finishes, so a second switch cannot interleave. + /// Refresh the file explorer, preview pane and commit list after a successful switch. + /// The selectors were already updated by [`Self::switch_ref`]. + /// [`Self::switching_ref`] stays set until this reload finishes. + /// A second switch cannot interleave. fn reload_worktree(&mut self, cx: &mut Context) { let Some(worktree) = self.worktree.clone() else { return; @@ -1297,9 +1294,9 @@ impl RepoDetailView { match result { Ok((snapshot, tree)) => { this.head_commit = snapshot.head_commit; - // Rebuild the tree from scratch: entries of the - // previous branch are gone, and with them the - // expansion state. + // Rebuild the tree from scratch. + // Entries of the previous branch are gone. + // The expansion state goes with them. this.tree_state.update(cx, |state, cx| { state.set_items(tree_items(tree, false), cx); }); @@ -1346,9 +1343,10 @@ impl RepoDetailView { self.tasks.push(task); } - /// Drop the oldest previews beyond the cache caps, keeping the currently - /// selected file. The parsed editor state of an evicted file is dropped - /// along with its entry, so re-opening it re-parses on a background task. + /// Drop the oldest previews beyond the cache caps. + /// Keep the currently selected file. + /// An evicted file's parsed editor state drops with its entry. + /// Re-opening it re-parses on a background task. fn evict_previews(&mut self) { while (self.files.len() > MAX_PREVIEWED_FILES || self.preview_bytes > MAX_PREVIEW_CACHE_BYTES) @@ -1376,7 +1374,7 @@ impl RepoDetailView { } } - /// The latest announcement from the store, or the open-time snapshot; + /// The latest announcement from the store or the open-time snapshot. /// `None` for local repositories that haven't been published yet. fn announcement<'a>(&'a self, cx: &'a App) -> Option<&'a Announcement> { let store = self.store.as_ref()?; @@ -1387,8 +1385,8 @@ impl RepoDetailView { .or(self.initial.as_ref()) } - /// Display name: the announcement's name (or ID) for announced - /// repositories, the directory name for local ones. + /// Display name, the announcement's name or ID for announced repositories. + /// The directory name for local ones. fn display_name(&self, cx: &App) -> SharedString { if let Some(path) = &self.local_path { return SharedString::from( @@ -1407,9 +1405,8 @@ impl RepoDetailView { .unwrap_or_default() } - /// The NIP-34 header (actions, issues/PR counts) or, for a local - /// repository that hasn't been published yet, the local header with an - /// Init button. + /// The NIP-34 header, actions and issues/PR counts. + /// Or the local header with an Init button for an unpublished repository. fn render_header(&mut self, cx: &mut Context) -> AnyElement { if self.local_path.is_some() { return self.render_local_header(cx); @@ -1426,9 +1423,9 @@ impl RepoDetailView { return div().into_any_element(); }; - // The header derives bech32 share targets and clone command strings - // from the announcement; rebuild them only when the announcement or - // the owner's NIP-05 changes, not on every render. + // The header derives bech32 share targets and clone commands. + // Rebuild them only when the announcement or the owner's NIP-05 changes. + // Not on every render. let nip05 = ProfileStore::global(cx) .read(cx) .get(&source.owner) @@ -1815,9 +1812,8 @@ impl RepoDetailView { .into_any_element() } - /// Header for a local (not yet published) repository: the directory - /// name and path with an Init button instead of the NIP-34 actions - /// (issues, pull requests, share, info, clone). + /// Header for a local, not yet published, repository. + /// The directory name and path with an Init button instead of the NIP-34 actions. fn render_local_header(&self, cx: &mut Context) -> AnyElement { let name = self.display_name(cx); let path = self @@ -1879,8 +1875,7 @@ impl RepoDetailView { .into_any_element() } - /// 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. fn open_init_dialog(&mut self, window: &mut Window, cx: &mut Context) { let Some(local_path) = self.local_path.clone() else { return; @@ -1889,47 +1884,47 @@ impl RepoDetailView { init_dialog::open(local_path, view, window, cx); } - /// Switch the repository into its NIP-34 mode after a successful init: - /// create the nostr store for the announced repository and drop the - /// local (scan) identity. The worktree is unchanged, so the file - /// explorer keeps its loaded content. + /// Switch the repository into its NIP-34 mode after a successful init. + /// Creates the nostr store for the announced repository. + /// Drops the local scan identity. + /// The worktree is unchanged, so the explorer keeps its loaded content. pub(crate) fn apply_announcement( &mut self, announcement: Announcement, cx: &mut Context, ) { - // The repository is no longer a bare local repo: drop it from the - // scan results so it leaves the sidebar's local section immediately. + // The repository is no longer a bare local repo. + // Drop it from the scan results so it leaves the sidebar's local section. if let Some(path) = self.local_path.take() { LocalReposStore::global(cx).update(cx, |store, cx| store.remove(&path, cx)); } let store = cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx)); - // Re-render on store refreshes (issues, PRs, statuses) and keep the - // "ready to contribute" statuses of this repository requested. + // Re-render on store refreshes, issues, PRs and statuses. + // Keep the ready-to-contribute statuses of this repository requested. self.attach_store(&store, cx); self.store = Some(store); self.initial = Some(announcement); cx.notify(); } - /// Observe the repository's store (re-render on refreshes) and request - /// the "ready to contribute" statuses for it. + /// Observe the repository's store, re-render on refreshes. + /// Request the ready-to-contribute statuses for it. fn attach_store(&mut self, store: &Entity, cx: &mut Context) { self._subscriptions .push(cx.observe(store, |this, _store, cx| { cx.notify(); - // The first refresh fills the announced HEAD, which defaults - // the banner's base branch; re-request when it changes. + // The first refresh fills the announced HEAD. + // It defaults the banner's base branch, re-request when it changes. this.refresh_ready_statuses(cx); })); self.refresh_ready_statuses(cx); } - /// (Re)request the statuses of this repository when the announced - /// HEAD - the base the checkouts are compared against, changed since the last request. - /// Repositories the user owns are watched for unpushed commits, - /// other repositories for ready-to-contribute checkouts. + /// Request the statuses of this repository again when the announced HEAD changes. + /// The HEAD is the base the checkouts are compared against. + /// Owned repositories are watched for unpushed commits. + /// Other repositories for ready-to-contribute checkouts. fn refresh_ready_statuses(&mut self, cx: &mut Context) { let Some(entity) = self.store.clone() else { return; @@ -1954,8 +1949,8 @@ impl RepoDetailView { .is_some_and(|user| entity.read(cx).is_author(&user)); checkout.update(cx, |store, cx| { - // The ready statuses also keep the fast poll running while the - // panel is open (the sidebar's push watch alone polls slower). + // The ready statuses keep the fast poll running while the panel is open. + // The sidebar's push watch alone polls slower. store.request_statuses(&addr, head, cx); if owned { @@ -1964,10 +1959,11 @@ impl RepoDetailView { }); } - /// The first checkout ready for a pull request on this repository, - /// not covered by an open PR of the signed-in user and not dismissed in - /// this panel. The repository's own checkouts are not suggested here: - /// their work is pushed (see [`Self::push_suggestion`]). + /// The first checkout ready for a pull request on this repository. + /// Not covered by an open PR of the signed-in user. + /// Not dismissed in this panel. + /// The repository's own checkouts are not suggested here. + /// Their work is pushed, see [`Self::push_suggestion`]. fn ready_suggestion(&self, cx: &App) -> Option { let store = self.store.as_ref()?; let addr = store.read(cx).addr().clone(); @@ -1998,8 +1994,8 @@ impl RepoDetailView { None } - /// The first checkout of this owned repository with unpushed commits, - /// not dismissed in this panel. + /// The first checkout of this owned repository with unpushed commits. + /// Not dismissed in this panel. fn push_suggestion(&self, cx: &App) -> Option { let entity = self.store.as_ref()?; let user = Backend::global(cx).read(cx).current_user()?; @@ -2015,8 +2011,8 @@ impl RepoDetailView { }) } - /// The "ready to push" banner of an owned repository: a local checkout - /// has unpushed commits, with a Push action and a dismiss control. + /// The ready-to-push banner of an owned repository. + /// A local checkout has unpushed commits, with a Push action and a dismiss control. fn render_push_banner(&self, cx: &Context) -> Option { let status = self.push_suggestion(cx)?; let commits = if status.ahead == 1 { @@ -2067,9 +2063,9 @@ impl RepoDetailView { ) } - /// The "ready to contribute" banner of the repository panel: message, - /// a Create action opening the prefilled New PR panel, and a dismiss - /// control. + /// The ready-to-contribute banner of the repository panel. + /// A message, a Create action opening the prefilled New PR panel. + /// Plus a dismiss control. fn render_ready_banner(&self, cx: &Context) -> Option { let status = self.ready_suggestion(cx)?; let commits = if status.ahead == 1 { @@ -2119,8 +2115,8 @@ impl RepoDetailView { ) } - /// The tab row shared by both header variants: Files/Commits tabs, the - /// HEAD commit button and the branch/tag selectors. + /// The tab row shared by both header variants. + /// Files and Commits tabs, the HEAD commit button and the branch/tag selectors. fn render_header_tabs(&self, cx: &mut Context) -> AnyElement { let commits_count = self.all_commits.as_ref().map(|list| list.total); let worktree_empty = self.switching_ref || self.worktree.is_none(); @@ -2379,8 +2375,8 @@ impl Render for RepoDetailView { } } -/// Read the worktree state of `repo` (no network): entries, README, refs -/// and HEAD commit. +/// Read the worktree state of `repo`, no network. +/// Entries, README, refs and HEAD commit. fn load_repo_data(repo: &Repository) -> Result { let entries = signed_git::worktree_entries(repo)?; let tree = build_tree_items(&entries); @@ -2390,8 +2386,9 @@ fn load_repo_data(repo: &Repository) -> Result { None => None, }; let worktree = repo.workdir().map(Path::to_path_buf); - // Ref listing is auxiliary UI: a broken ref must not prevent the - // explorer from loading, so failures degrade to empty selectors. + // Ref listing is auxiliary UI. + // A broken ref must not prevent the explorer from loading. + // Failures degrade to empty selectors. let (branches, tags, current_branch) = match &worktree { Some(_) => ( signed_git::repo_branches(repo).unwrap_or_default(), @@ -2414,10 +2411,10 @@ fn load_repo_data(repo: &Repository) -> Result { }) } -/// The `nostr://...` clone URL of an announcement (NIP-34): the owner as a -/// NIP-05 identifier when known (npub otherwise), the first announced relay -/// as a hint, and the repository identifier. `nip05` is the owner's -/// NIP-05 identifier from the profile store, already blank-filtered. +/// The `nostr://...` clone URL of an announcement, NIP-34. +/// The owner as a NIP-05 identifier when known, npub otherwise. +/// The first announced relay is a hint, plus the repository identifier. +/// `nip05` is the owner's NIP-05 identifier from the profile store, already blank-filtered. fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedString { let owner = announcement.owner; let user = nip05 @@ -2435,16 +2432,16 @@ fn nostr_clone_url(announcement: &Announcement, nip05: Option<&str>) -> SharedSt SharedString::from(url) } -/// The "Forked from …" row of the detail header: a clickable link to the -/// upstream repository when the `u` tag references a NIP-34 repo, -/// plain text when it only carries a git URL. +/// The forked-from row of the detail header. +/// Clickable link to the upstream repository when the `u` tag references a NIP-34 repo. +/// Plain text when it only carries a git URL. fn fork_row(announcement: &Announcement, cx: &mut Context) -> Option { let upstream = announcement.upstream.as_ref()?; let (label, clickable) = match &upstream.addr { Some(addr) => { - // Prefer the upstream's display name when its announcement - // is already known locally fall back to its repository id. + // Prefer the upstream's display name when its announcement is known locally. + // Fall back to its repository id otherwise. let name = RepoListStore::global(cx) .read(cx) .announcements @@ -2481,9 +2478,10 @@ fn fork_row(announcement: &Announcement, cx: &mut Context) -> Op }) } -/// Open `announcement` as a repository panel in the dock's center, returning -/// the new detail view. Shared by the explore list, the sidebar and fork -/// links so every entry point opens repositories identically. +/// Open `announcement` as a repository panel in the dock's center. +/// Returns the new detail view. +/// Shared by the explore list, the sidebar and fork links. +/// Every entry point opens repositories identically. pub(crate) fn open_repo_panel( dock_area: &WeakEntity, announcement: &Announcement, diff --git a/crates/workspace/src/views/repo_detail/new_pull_request.rs b/crates/workspace/src/views/repo_detail/new_pull_request.rs index 0de4a5d..c67e1da 100644 --- a/crates/workspace/src/views/repo_detail/new_pull_request.rs +++ b/crates/workspace/src/views/repo_detail/new_pull_request.rs @@ -1,9 +1,3 @@ -//! The "new pull request" panel: pick a compare source — a local checkout -//! or an announced fork of the repository — a base and a compare branch -//! (GitHub-style), review the diff and the commit list, then publish the PR -//! with only a title and an optional description. The patch series is -//! generated at submit time; there is no patch input. - use std::path::{Path, PathBuf}; use std::rc::Rc; @@ -41,47 +35,44 @@ use signed_ui::placeholder; use super::commits::{COMMIT_ROW_HEIGHT, commit_row}; use super::diff::{CommitDiffView, DiffPane}; -/// The "new pull request" panel of a repository. -/// -/// The compare side of the PR comes from one of two sources: -/// -/// - **Local checkout**: both branch selectors list a user-picked local -/// checkout's branches; git ops and the tip push run in the checkout. -/// - **Announced fork**: the fork's branches are fetched into the target -/// repository's GitCache mirror under `refs/fork///*`, the base -/// selector lists the mirror's `refs/remotes/origin/*` branches, and all -/// git ops run in the mirror. -/// -/// The compare view (Files/Commits tabs) is built from `merge-base..compare` -/// of the chosen refs, and the patch series published with the PR is -/// generated from the same range at submit time. +/// The new pull request panel of a repository. +/// The compare side comes from a local checkout or an announced fork. +/// A local checkout lists its own branches in both selectors. +/// Git ops and the tip push run in the checkout. +/// An announced fork imports its branches into the target's GitCache mirror. +/// The base selector lists the mirror's `refs/remotes/origin/*` branches. +/// All git ops run in the mirror. +/// The Files and Commits tabs are built from `merge-base..compare` of the chosen refs. +/// The patch series published with the PR comes from the same range at submit time. pub struct NewPullRequestView { focus_handle: FocusHandle, - /// Dock area the panel lives in; commit diffs are opened there. + /// Dock area the panel lives in, commit diffs are opened there. dock_area: WeakEntity, - /// Store of the target repository (for the announced HEAD default). + /// Store of the target repository, source of the announced HEAD default. store: Entity, /// Display name of the repository, for the panel title. repo_name: SharedString, - /// The user's local checkout: where both branches live in checkout mode - /// and where the tip is pushed from. `None` until a folder is picked. + /// The user's local checkout. + /// Both branches live there in checkout mode and the tip is pushed from there. + /// `None` until a folder is picked. repo_path: Option, /// Branches of the checkout, backing both selectors in checkout mode. branches: Vec, - /// Fork-backed compare state; `Some` switches the panel into fork mode - /// (the checkout above is kept so the user can switch back). + /// Fork-backed compare state. + /// `Some` switches the panel into fork mode. + /// The checkout above is kept so the user can switch back. fork: Option, - /// Selected base branch (the target of the PR), short name. + /// Selected base branch, the PR target, stored as a short name. base: SharedString, - /// Selected compare branch (the source of the PR), short name. + /// Selected compare branch, the PR source, stored as a short name. compare: SharedString, base_select: Entity>>, compare_select: Entity>>, - /// Title input (required). + /// Title input, required. subject: Entity, - /// Description input (optional). + /// Description input, optional. description: Entity, - /// Merge base of the selected branches; `None` until the compare loads. + /// Merge base of the selected branches, `None` until the compare loads. merge_base: Option, /// Commits in `merge_base..compare`, newest first. commits: Option>, @@ -89,13 +80,13 @@ pub struct NewPullRequestView { loading: bool, /// Error of the last compare or submit attempt. error: Option, - /// A submit (patch generation + publish) is in flight. + /// A submit, patch generation and publish, is in flight. submitting: bool, - /// Bumped on every branch switch; stale compare results are discarded. + /// Bumped on every branch switch, stale compare results are discarded. compare_generation: u64, - /// Active tab: 0 = Files, 1 = Commits. + /// Active tab, 0 = Files and 1 = Commits. active_tab: usize, - /// The compare diff (Files tab). + /// The compare diff, the Files tab body. pane: Entity, /// Virtual list state of the Commits tab. scroll_handle: VirtualListScrollHandle, @@ -104,13 +95,13 @@ pub struct NewPullRequestView { tasks: Vec>>, } -/// A fork-backed compare: the fork's heads are imported into the target -/// repository's GitCache mirror under `refs/fork//*`, and the -/// mirror's own `refs/remotes/origin/*` track the base branches. +/// A fork-backed compare. +/// The fork's heads are imported into the target mirror under `refs/fork//*`. +/// The mirror's own `refs/remotes/origin/*` refs track the base branches. struct ForkCompare { /// Fork announcement the compare branch is imported from. announcement: Announcement, - /// Import namespace: `/`. + /// Import namespace of the form `/`. namespace: String, /// Path of the target repository's GitCache mirror. mirror_path: PathBuf, @@ -137,11 +128,11 @@ fn fork_namespace(announcement: &Announcement) -> String { ) } -/// The announced forks of `base` a New PR compare can be built from: -/// announcements related by `u` tag or shared EUC, excluding the base -/// itself and announcements without `clone` URLs (unfetchable). Own forks -/// (announced by `user`) come first; the input order (newest first, as -/// `RepoListStore` keeps it) is preserved within each group. +/// The announced forks of `base` a New PR compare can be built from. +/// Related by `u` tag or shared EUC, excluding the base itself. +/// Announcements without `clone` URLs are unfetchable and excluded. +/// Own forks, announced by `user`, come first. +/// Newest first as `RepoListStore` keeps them, order is preserved within each group. fn fork_candidates<'a>( announcements: &'a [Announcement], base: &RepoAddr, @@ -162,8 +153,8 @@ fn fork_candidates<'a>( own.into_iter().chain(others).collect() } -/// The display name of an announcement: its human-readable name, falling -/// back to the repository id. +/// The display name of an announcement. +/// Its human-readable name, falling back to the repository id. fn fork_display_name(announcement: &Announcement) -> SharedString { announcement .name @@ -171,7 +162,7 @@ fn fork_display_name(announcement: &Announcement) -> SharedString { .unwrap_or_else(|| SharedString::from(announcement.id.clone())) } -/// A short label of a fork's owner for the source picker (hex prefix). +/// A short label of a fork's owner for the source picker, a hex prefix. fn shorten_owner(owner: &PublicKey) -> String { let hex = owner.to_hex(); hex.chars().take(10).collect() @@ -190,8 +181,8 @@ fn truncate_label(label: &str) -> SharedString { SharedString::from(label) } -/// The compare-source menu entry of one local checkout folder: applies the -/// folder directly (no picker). +/// The compare-source menu entry of one local checkout folder. +/// Applies the folder directly, no picker. fn checkout_source_item( view: WeakEntity, path: PathBuf, @@ -237,8 +228,8 @@ fn choose_folder_source_item(view: WeakEntity) -> PopupMenuI }) } -/// The compare-source menu entry of one announced fork: -/// imports its branches into the target's mirror and switches the panel to fork mode. +/// The compare-source menu entry of one announced fork. +/// Imports its branches into the target's mirror and switches the panel to fork mode. fn fork_source_item( view: WeakEntity, announcement: Announcement, @@ -264,7 +255,7 @@ fn fork_source_item( }) } -/// One row of the compare-source menu: icon, title and a muted subtitle. +/// One row of the compare-source menu, icon, title and a muted subtitle. fn source_row(icon: impl Into, title: T, subtitle: T, cx: &App) -> AnyElement where T: Into, @@ -340,8 +331,7 @@ impl NewPullRequestView { }); let subscriptions = vec![ - // Re-evaluate the Create button's enabled state as the title - // changes. + // Re-evaluate the Create button's enabled state as the title changes. cx.subscribe(&subject, |_this, _state, _event: &InputEvent, cx| { cx.notify(); }), @@ -395,8 +385,7 @@ impl NewPullRequestView { tasks: Vec::new(), }; - // Prefill: when the store knows an associated checkout of this repository, - // apply the freshest one right away (no folder dialog). + // 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) @@ -410,13 +399,13 @@ impl NewPullRequestView { view } - /// Whether a compare source (a checkout or a fork) is applied. + /// Whether a compare source, a checkout or a fork, is applied. fn has_source(&self) -> bool { self.repo_path.is_some() || self.fork.is_some() } - /// The path git ops run against: the target's mirror in fork mode, the - /// user's checkout otherwise. + /// The path git ops run against. + /// The target's mirror in fork mode, the user's checkout otherwise. fn work_path(&self) -> Option { match &self.fork { Some(fork) => Some(fork.mirror_path.clone()), @@ -424,9 +413,9 @@ impl NewPullRequestView { } } - /// The full ref the selected base branch resolves to: the mirror's - /// remote-tracking ref in fork mode, the plain branch name in checkout - /// mode (where git resolves it through `refs/heads`). + /// The full ref the selected base branch resolves to. + /// The mirror's remote-tracking ref in fork mode. + /// The plain branch name in checkout mode, git resolves it through `refs/heads`. fn base_ref(&self) -> String { match &self.fork { Some(_) => ForkCompare::base_ref(&self.base), @@ -434,9 +423,9 @@ impl NewPullRequestView { } } - /// The full ref the selected compare branch resolves to: the imported - /// `refs/fork/` ref in fork mode, the plain branch name in - /// checkout mode. + /// The full ref the selected compare branch resolves to. + /// The imported `refs/fork/` ref in fork mode. + /// The plain branch name in checkout mode. fn compare_ref(&self) -> String { match &self.fork { Some(fork) => fork.compare_ref(&self.compare), @@ -444,9 +433,10 @@ impl NewPullRequestView { } } - /// Prompt for a local checkout; on success populate the branch selectors - /// (defaults: the announced HEAD branch for the base, the checkout's - /// current branch for the compare) and load the compare. + /// Prompt for a local checkout. + /// On success populate the branch selectors and load the compare. + /// Defaults are the announced HEAD branch for the base. + /// The checkout's current branch is the default for the compare. fn choose_checkout(&mut self, window: &mut Window, cx: &mut Context) { let prompt = cx.prompt_for_paths(PathPromptOptions { files: false, @@ -456,8 +446,8 @@ impl NewPullRequestView { }); let task = cx.spawn_in(window, async move |this, cx| { - // `Ok(Ok(Some(paths)))` means the user picked a folder; a - // cancel (or a picker failure) resolves to anything else. + // `Ok(Ok(Some(paths)))` means the user picked a folder. + // A cancel or picker failure resolves to anything else. let picked = match prompt.await { Ok(Ok(Some(mut paths))) => paths.pop(), _ => None, @@ -475,8 +465,8 @@ impl NewPullRequestView { self.tasks.push(task); } - /// Apply `path` as the local checkout (no picker): read its branches - /// and current branch off the UI thread, then apply. + /// Apply `path` as the local checkout, no picker. + /// Branches and current branch are read off the UI thread, then applied. fn apply_folder_path(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context) { let path = path.to_string_lossy().to_string(); @@ -504,9 +494,10 @@ impl NewPullRequestView { self.tasks.push(task); } - /// Apply a picked checkout: fill the selectors and load the compare. - /// Leaves fork mode; a fork applied earlier keeps its import in the - /// mirror (harmless) but the panel switches back to the checkout. + /// Apply a picked checkout, filling the selectors and loading the compare. + /// Leaves fork mode. + /// A fork applied earlier keeps its import in the mirror, harmless. + /// The panel switches back to the checkout. fn apply_checkout( &mut self, path: String, @@ -533,9 +524,9 @@ impl NewPullRequestView { return; } - // Defaults: the announced HEAD branch when the checkout has it - // (falling back to `main`, then the first branch); the checkout's - // current branch for the compare side. + // Defaults, the announced HEAD branch when the checkout has it. + // Falling back to `main`, then the first branch. + // The checkout's current branch is the compare side default. let announced = self.store.read(cx).head.clone(); let base = announced .as_ref() @@ -551,8 +542,8 @@ impl NewPullRequestView { self.error = None; self.branches = branches.into_iter().map(SharedString::from).collect(); - // Learning: remember this folder as a checkout of the target - // repository, so the next panel pre-fills it. + // Remember this folder as a checkout of the target repository. + // The next panel pre-fills it. let addr = self.store.read(cx).addr().clone(); CheckoutsStore::global(cx).update(cx, |store, cx| { store.record(PathBuf::from(&path), addr, cx); @@ -575,16 +566,16 @@ impl NewPullRequestView { self.reload_compare(window, cx); } - /// The base repository of the panel: its address and announced EUC, - /// used to find fork candidates. + /// The base repository of the panel, its address and announced EUC. + /// Used to find fork candidates. fn base_repo(&self, cx: &App) -> (RepoAddr, Option) { let store = self.store.read(cx); let euc = store.announcement.as_ref().and_then(|a| a.euc.clone()); (store.addr().clone(), euc) } - /// Announced forks of the target repository the compare can be built - /// from (own forks first), re-read whenever the picker opens. + /// Announced forks of the target repository a compare can use, own first. + /// Re-read whenever the picker opens. fn fork_candidates(&self, cx: &App) -> Vec { let (base, euc) = self.base_repo(cx); let user = Backend::global(cx).read(cx).current_user(); @@ -595,11 +586,12 @@ impl NewPullRequestView { .collect() } - /// Compare against an announced fork: ensure the target's GitCache - /// mirror, import the fork's heads under `refs/fork/…`, then fill the - /// selectors (base from `refs/remotes/origin/*`, compare from the - /// import) and load the compare. Picking the fork already applied - /// refreshes it instead (re-import + reload), keeping the branch selection. + /// Compare against an announced fork. + /// The target's GitCache mirror is ensured, then the fork's heads land under `refs/fork/…`. + /// The base selector lists `refs/remotes/origin/*`, the compare the import. + /// Then the compare loads. + /// Picking the fork already applied refreshes it, re-import and reload. + /// The branch selection is kept. fn choose_fork( &mut self, announcement: Announcement, @@ -625,13 +617,13 @@ impl NewPullRequestView { .map(|a| a.clone.iter().map(ToString::to_string).collect()) .unwrap_or_default(); - // The default compare branch of the fork, if the refreshed fork is - // the one applied and its branch still exists. + // Keep the current compare and base when the fork is already applied. + // apply_fork drops them when the branch no longer exists. let keep_compare = refresh.then(|| self.compare.clone()); let keep_base = refresh.then(|| self.base.clone()); - // The fork applied when the fetch started; if the user switches the - // source mid-flight, the result must not clobber the newer state. + // The fork applied when the fetch started. + // A source switch mid-flight must not let the stale result clobber the newer state. let expected_fork = self.fork.as_ref().map(|fork| fork.announcement.addr()); self.loading = true; @@ -639,9 +631,9 @@ impl NewPullRequestView { cx.notify(); let task = cx.spawn_in(window, async move |this, cx| { - // The fork and the base must share history for a merge-base to exist, - // so the target's mirror is the object store both sides land in. - // `ensure_clone` fetches `origin` when the mirror exists already. + // The fork and base must share history for a merge-base to exist. + // The target's mirror is the object store both sides land in. + // `ensure_clone` fetches `origin` when the mirror already exists. let result = cx .background_spawn({ let cache = cache.clone(); @@ -651,13 +643,13 @@ impl NewPullRequestView { let clone_urls = clone_urls.clone(); let mirror_path = mirror_path.clone(); async move { - // The fork and the base must share history for a - // merge-base to exist, so the target's mirror is the - // object store both sides land in. `ensure_clone` - // fetches `origin` when the mirror exists already. + // The fork and base must share history for a merge-base to exist. + // The target's mirror is the object store both sides land in. + // `ensure_clone` fetches `origin` when the mirror already exists. cache.ensure_clone(&base, &base_clone_urls)?; - // Prune stale imports of any fork, then import this fork's heads under its namespace. + // Prune stale imports of any fork. + // Then import this fork's heads under its namespace. delete_refs_with_prefix(&mirror_path, "refs/fork")?; fetch_repo_refs( @@ -666,7 +658,7 @@ impl NewPullRequestView { &format!("+refs/heads/*:refs/fork/{namespace}/*"), )?; - // Both branch lists are short names, kept sorted like the checkout's. + // Both branch lists are short names, sorted like the checkout's. let strip = |refs: Vec, prefix: &str| { let mut names: Vec = refs .into_iter() @@ -696,7 +688,8 @@ impl NewPullRequestView { .await; this.update_in(cx, |this, window, cx| { - // A source switch mid-flight (e.g. the user picked a folder while the fork was fetching) discards the stale result. + // A source switch mid-flight discards the stale result. + // E.g. the user picked a folder while the fork was fetching. let applied = this.fork.as_ref().map(|fork| fork.announcement.addr()); if applied != expected_fork { this.loading = false; @@ -721,7 +714,7 @@ impl NewPullRequestView { self.tasks.push(task); } - /// Apply an imported fork: fill the selectors and load the compare. + /// Apply an imported fork, filling the selectors and loading the compare. #[allow(clippy::too_many_arguments)] fn apply_fork( &mut self, @@ -738,8 +731,8 @@ impl NewPullRequestView { let (base_branches, compare_branches) = match result { Ok(branches) => branches, Err(error) => { - // Keep the previous source (if any); the error is shown - // inline next to the compare bar. + // Keep the previous source, if any. + // The error shows inline next to the compare bar. self.error = Some(format!("Could not compare against the fork: {error}").into()); cx.notify(); return; @@ -764,11 +757,10 @@ impl NewPullRequestView { .map(SharedString::from) .collect(); - // Defaults: the announced HEAD branch when the mirror has it - // (falling back to `main`, then the first branch); the fork's - // `main` for the compare side (falling back to the first branch). - // A refresh keeps the previous selection when the branch still - // exists. + // Base defaults to the announced HEAD branch when the mirror has it. + // Otherwise `main`, then the first branch. + // The fork's `main` is the compare default, else the first branch. + // A refresh keeps the previous selection when the branch still exists. let announced = self.store.read(cx).head.clone(); let contains = |name: &str, list: &[SharedString]| list.iter().any(|branch| branch.as_ref() == name); @@ -817,18 +809,17 @@ impl NewPullRequestView { self.reload_compare(window, cx); } - /// (Re)compute `merge_base..compare` of the selected branches on a - /// background task: the merge base, the commit list and the diff. Runs - /// against the work path (the checkout, or the mirror in fork mode) - /// using the full refs of both branches, so base `main` and fork `main` - /// stay distinct. + /// Recompute `merge_base..compare` of the selected branches on a background task. + /// Computes the merge base, the commit list and the diff. + /// Runs against the work path, the checkout or the mirror in fork mode. + /// Full refs keep base `main` and fork `main` distinct. fn reload_compare(&mut self, window: &mut Window, cx: &mut Context) { let Some(repo_path) = self.work_path() else { return; }; let base = self.base_ref(); let compare = self.compare_ref(); - // Short names for the error copy; the full refs go to git. + // Short names for the error copy, the full refs go to git. let base_name = self.base.to_string(); let compare_name = self.compare.to_string(); @@ -879,8 +870,8 @@ impl NewPullRequestView { .await; this.update_in(cx, |this, _window, cx| { - // A stale result (the branches changed mid-flight) must not - // clobber a newer compare; the newer task clears the flag. + // A stale result, branches changed mid-flight, must not clobber a newer compare. + // The newer task clears the flag. if generation != this.compare_generation { return; } @@ -908,9 +899,10 @@ impl NewPullRequestView { self.tasks.push(task); } - /// Publish the pull request: generate the patch series from the checkout - /// on a background task, hand it to the store, and close the panel once - /// the publish is underway (errors surface in the pull request list). + /// Publish the pull request. + /// Generate the patch series on a background task and hand it to the store. + /// Close the panel once the publish is underway. + /// Errors surface in the pull request list. fn submit(&mut self, window: &mut Window, cx: &mut Context) { if self.submitting || self.loading { return; @@ -930,8 +922,8 @@ impl NewPullRequestView { // The published `branch-name` is the compare branch's short name. let branch_name = self.compare.to_string(); - // The patch is generated from the compare ref: a plain branch name - // in checkout mode, the imported `refs/fork/…` ref in fork mode. + // The patch comes from the compare ref. + // Plain branch name in checkout mode, imported `refs/fork/…` ref in fork mode. let compare_ref = self.compare_ref(); let store = self.store.clone(); let dock_area = self.dock_area.clone(); @@ -942,7 +934,8 @@ impl NewPullRequestView { cx.notify(); let task = cx.spawn_in(window, async move |this, cx| { - // Regenerate the series at submit time so the published patch covers the current tip of the compare branch. + // Regenerate the series at submit time. + // The published patch covers the current tip of the compare branch. let patch = cx .background_spawn({ let repo_path = repo_path.clone(); @@ -1009,7 +1002,7 @@ impl NewPullRequestView { self.tasks.push(task); } - /// Open the diff of `commit_id` (from the Commits tab) in a new panel. + /// Open the diff of `commit_id`, from the Commits tab, in a new panel. fn open_commit_diff(&mut self, commit_id: &str, window: &mut Window, cx: &mut Context) { let Some(repo_path) = self.work_path() else { return; @@ -1033,8 +1026,8 @@ impl NewPullRequestView { }); } - /// The compare bar: base/compare selectors, the source picker (local - /// checkout / announced fork) and the Create button. + /// The compare bar, base and compare selectors. + /// Plus the source picker, local checkout or announced fork, and the Create button. fn render_compare_bar(&self, cx: &mut Context) -> AnyElement { let has_source = self.has_source(); let can_submit = has_source @@ -1047,8 +1040,8 @@ impl NewPullRequestView { .is_some_and(|commits| !commits.is_empty()) && !self.subject.read(cx).value().is_empty(); - // Source-picker data, snapshotted when the menu is built - // (each open rebuilds the items from the live announcements). + // Source-picker data snapshotted when the menu is built. + // Each open rebuilds the items from the live announcements. let source_menu = self.source_menu(cx); let source_label = self.source_trigger(); let source_tooltip = match &self.fork { @@ -1169,7 +1162,7 @@ impl NewPullRequestView { .into_any_element() } - /// The source picker's trigger: truncated label of the applied source. + /// The source picker's trigger, a truncated label of the applied source. fn source_trigger(&self) -> SharedString { match &self.fork { Some(fork) => truncate_label(&fork_display_name(&fork.announcement)), @@ -1180,18 +1173,18 @@ impl NewPullRequestView { } } - /// Build the compare-source menu: switching back to the local checkout, - /// then the announced forks of the target repository (own forks first). - /// Picking the fork already applied re-fetches it. Rebuilt every time - /// the menu opens, so the candidates are always current. + /// Build the compare-source menu. + /// The local checkout entries first, then the announced forks, own forks first. + /// Picking the fork already applied re-fetches it. + /// Rebuilt every time the menu opens, so the candidates stay current. fn source_menu( &self, cx: &Context, ) -> impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static { let view = cx.entity().downgrade(); - // Associated local checkouts of the target repository, freshest - // first; the applied one is checked. The picker prompt stays - // available underneath for arbitrary folders. + // Associated local checkouts of the target repository, freshest first. + // The applied one is checked. + // The picker prompt stays available underneath for arbitrary folders. let addr = self.store.read(cx).addr().clone(); let associated = CheckoutsStore::global(cx).read(cx).associations_of(&addr); let active_path = (self.fork.is_none()) @@ -1345,8 +1338,8 @@ impl NewPullRequestView { } } - /// The Commits tab: `merge_base..compare` in a virtual list; clicking a - /// row opens the commit's diff in a new panel. + /// The Commits tab, `merge_base..compare` in a virtual list. + /// Clicking a row opens the commit's diff in a new panel. fn render_commits_tab(&self, cx: &mut Context) -> AnyElement { let Some(commits) = self.commits.as_ref() else { return placeholder("No commits", cx); @@ -1423,8 +1416,9 @@ fn count_badge(count: usize, cx: &App) -> impl IntoElement { .child(SharedString::from(count.to_string())) } -/// The trigger of a branch selector: icon + current selection (or -/// placeholder) + caret. `Combobox` replaces its default trigger entirely. +/// The trigger of a branch selector. +/// Shows the icon, the current selection or placeholder, and the caret. +/// `Combobox` replaces its default trigger entirely. fn render_ref_trigger( ctx: &ComboboxTriggerContext>, icon: CustomIconName, @@ -1458,7 +1452,7 @@ fn render_ref_trigger( .into_any_element() } -/// Open the "new pull request" panel in the center dock. +/// Open the new pull request panel in the center dock. pub(super) fn open_new_pull_panel( dock_area: WeakEntity, store: Entity, @@ -1570,8 +1564,8 @@ mod tests { PublicKey::from_hex(OWNER_KEYS[0]).expect("pubkey"), "upstream", ); - // Newest first, as RepoListStore keeps them: an unrelated repo, the - // user's own fork (shared EUC), someone else's fork (u tag). + // Newest first, as RepoListStore keeps them. + // Unrelated repo, the user's fork with the shared EUC, another fork with a `u` tag. let all = vec![ announcements( 2, diff --git a/crates/workspace/src/views/repo_detail/pull_request_detail.rs b/crates/workspace/src/views/repo_detail/pull_request_detail.rs index 927790f..1d66f36 100644 --- a/crates/workspace/src/views/repo_detail/pull_request_detail.rs +++ b/crates/workspace/src/views/repo_detail/pull_request_detail.rs @@ -41,60 +41,61 @@ use super::helpers::{ /// Width of the changed-files column. const TREE_WIDTH: f32 = 260.; -/// Height of one commit row in the commits tab's virtual list: a single -/// text line plus the 1px bottom border. +/// Height of one commit row in the commits tab's virtual list. +/// A single text line plus the 1px bottom border. const PR_COMMIT_ROW_HEIGHT: f32 = 37.; /// Detail panel of a single pull request. pub struct PullRequestDetailView { focus_handle: FocusHandle, - /// Dock area new panels (commit diffs) are added to. + /// Dock area where new panels, e.g. commit diffs, are added. dock_area: WeakEntity, /// Repo store holding the PR, its status and comments. store: Entity, - /// Event id of the root PR event (kind 1618; updates are revisions). + /// Event id of the root PR event, kind 1618. + /// Updates are revisions. pr_id: EventId, - /// Input state of the "leave a comment" textarea. + /// Input state of the comment textarea. comment_input: Entity, /// Display name of the repository, for panels opened from here. repo_name: SharedString, - /// Local clone the PR's git changes come from; `None` while the diff is - /// parsed from the nostr patch set (no commit diff viewer then). + /// Local clone the PR's git changes come from. + /// `None` when the diff is parsed from the nostr patch set. + /// No commit diff viewer in that case. worktree: Option, /// Root PR's content, shown as plain text. description: SharedString, - /// Tip commit of the PR: the latest update's `c` tag, else the root's. + /// Tip commit of the PR, the latest update's `c` tag or the root's. current_commit: Option, - /// Commits of the patch series, in patch order (oldest first). + /// Commits of the patch series, in patch order, oldest first. commits: Vec, - /// Parsed file changes of the patch; `None` while loading or on failure. + /// Parsed file changes of the patch, `None` while loading or on failure. diff: Option, /// The patch is being parsed on a background task. loading: bool, error: Option, - /// Active header tab: 0 = Discussion, 1 = Files, 2 = Commits. + /// Active header tab, 0 = Discussion, 1 = Files, 2 = Commits. active_tab: usize, /// Changed-files explorer state. tree_state: Entity, /// Path of the file whose diff is shown in the detail column. selected_file: Option, - /// Rows of the selected file's diff (hunk headers + lines). + /// Rows of the selected file's diff, hunk headers and lines. rows: Vec, /// Per-row heights of [`Self::rows`]. item_sizes: Rc>>, /// Virtual list state of the diff rows. scroll_handle: VirtualListScrollHandle, - /// Per-row heights of the commits tab's virtual list, built when the - /// patch series is loaded. + /// Per-row heights of the commits tab's virtual list, built when the patch series loads. commit_item_sizes: Rc>>, /// Virtual list state of the commits tab. commit_scroll_handle: VirtualListScrollHandle, - /// Comment bodies as shared strings, keyed by comment event ID, so - /// re-renders don't clone full contents again (events are immutable, - /// so the cache never needs invalidation). + /// Comment bodies as shared strings, keyed by comment event ID. + /// Re-renders don't clone full contents again. + /// Events are immutable, so the cache never needs invalidation. contents: HashMap, - /// In-flight tasks; finished tasks are pruned on every push, so the vec - /// stays bounded by the number of concurrent loads. + /// In-flight tasks, finished tasks are pruned on every push. + /// The vec stays bounded by the number of concurrent loads. tasks: Vec>>, /// Subscriptions keeping the view live as the store refreshes. _subscriptions: Vec, @@ -125,7 +126,7 @@ impl PullRequestDetailView { }) .unwrap_or_default(); - // Re-render when the store refreshes (new comments, status changes). + // Re-render when the store refreshes, new comments or status changes. let subscriptions = vec![cx.observe(&store, |_this, _store, cx| cx.notify())]; // Defer loading until the window is ready, like the commit diff view. @@ -161,12 +162,12 @@ impl PullRequestDetailView { } } - /// Snapshot the PR events from the store, then compute the file changes - /// and commit list on a background task and populate the tree. - /// - /// The changes come from the PR's patch set (NIP-34 `e`-linked patch - /// events) when present; otherwise from the git repository (`c`, - /// `clone` and `merge-base` tags), diffing the `merge-base..tip` range. + /// Snapshot the PR events from the store. + /// File changes and the commit list are computed on a background task. + /// The tree is populated from the result. + /// The changes come from the PR's patch set, NIP-34 `e`-linked patch events, when present. + /// Otherwise from the git repository, `c`, `clone` and `merge-base` tags. + /// Diffing the `merge-base..tip` range. fn load(&mut self, window: &mut Window, cx: &mut Context) { self.loading = true; self.error = None; @@ -226,9 +227,8 @@ impl PullRequestDetailView { }) .await; - // PRs without patch events (e.g. published by ngit) carry their - // changes in the git repository: fetch the clone and diff the - // `merge-base..tip` range. + // PRs without patch events, e.g. published by ngit, carry their changes in git. + // Fetch the clone and diff the `merge-base..tip` range. let use_nostr = match &nostr_diff { Ok(diff) => has_patch_link || !diff.files.is_empty(), Err(_) => true, @@ -252,8 +252,8 @@ impl PullRequestDetailView { tip.ok_or_else(|| anyhow::anyhow!("pull request has no tip commit"))?; let base = match base { Some(base) => base, - // No `merge-base` tag: use the merge base of the - // tip with the default branch. + // No `merge-base` tag. + // Use the merge base of the tip and the default branch. None => { let head = repo .head_id() @@ -322,15 +322,14 @@ impl PullRequestDetailView { self.tasks.push(task); } - /// Show the diff of the file at `path` (selected in the tree). + /// Show the diff of the file at `path`, selected in the tree. fn select_file(&mut self, path: &str, cx: &mut Context) { self.selected_file = Some(path.into()); self.set_diff_rows(path); cx.notify(); } - /// Rebuild the virtual list state for the file at `path` and scroll back - /// to the top. + /// Rebuild the virtual list state for the file at `path` and scroll back to the top. fn set_diff_rows(&mut self, path: &str) { let Some(diff) = self.diff.as_ref() else { return; @@ -370,7 +369,7 @@ impl PullRequestDetailView { }); } - /// One row of the changed-files tree: icon + name, indented by depth. + /// One row of the changed-files tree, icon and name, indented by depth. fn render_tree_item( ix: usize, entry: &TreeEntry, @@ -387,7 +386,7 @@ impl PullRequestDetailView { }) } - /// Left column: the changed-files tree. + /// Left column showing the changed-files tree. fn render_tree_column(&self, cx: &mut Context) -> AnyElement { let tree_state = self.tree_state.clone(); let view = cx.entity().downgrade(); @@ -417,7 +416,7 @@ impl PullRequestDetailView { .into_any_element() } - /// Right column: header of the selected file plus its diff. + /// Right column, header of the selected file plus its diff. fn render_detail_column(&self, cx: &mut Context) -> AnyElement { if self.loading { return v_flex() @@ -446,8 +445,9 @@ impl PullRequestDetailView { self.render_file_diff(file, cx.entity(), cx) } - /// The diff of one file: a header with status and stats, then the hunks - /// in a virtual list (a large diff is never materialized per frame). + /// The diff of one file, with a header showing status and stats. + /// The hunks render in a virtual list. + /// A large diff is never materialized per frame. fn render_file_diff(&self, file: &FileDiff, view: Entity, cx: &App) -> AnyElement { let status_label = match file.status { signed_git::DiffStatus::Added => "A", @@ -564,7 +564,7 @@ impl PullRequestDetailView { .into_any_element() } - /// Underline tab bar: Discussion, Files and Commits. + /// Underline tab bar with the Discussion, Files and Commits tabs. fn render_tabs(&self, cx: &mut Context) -> AnyElement { let active = self.active_tab; let files_count = self.diff.as_ref().map(|diff| diff.files.len()); @@ -610,8 +610,8 @@ impl PullRequestDetailView { .into_any_element() } - /// Discussion tab: author, description and comments like the issue - /// panel, with the comment form at the end and a sidebar on the right. + /// Discussion tab, author, description and comments like the issue panel. + /// The comment form sits at the end, a sidebar on the right. fn render_discussion(&mut self, cx: &mut Context) -> AnyElement { if self.loading { return v_flex() @@ -694,7 +694,7 @@ impl PullRequestDetailView { .into_any_element() } - /// Right sidebar: participants and labels, like the issue panel. + /// Right sidebar with participants and labels, like the issue panel. fn render_sidebar(&self, cx: &mut Context) -> AnyElement { let profile_store = ProfileStore::global(cx); let store = self.store.read(cx); @@ -708,7 +708,7 @@ impl PullRequestDetailView { return div().into_any_element(); }; - // Participants: the PR author plus everyone who commented. + // Participants, the PR author plus everyone who commented. let mut participants: Vec = vec![root.pubkey]; participants.extend(store.comments_of(&root.id).map(|comment| comment.pubkey)); participants.sort_by_key(PublicKey::to_hex); @@ -776,8 +776,8 @@ impl PullRequestDetailView { .into_any_element() } - /// Files tab: the changed-files tree on the left, the diff of the - /// selected file on the right. + /// Files tab, the changed-files tree on the left. + /// The diff of the selected file on the right. fn render_files_tab(&self, cx: &mut Context) -> AnyElement { h_flex() .flex_1() @@ -789,8 +789,8 @@ impl PullRequestDetailView { .into_any_element() } - /// Full-height Commits tab: every commit of the patch series, or a - /// status message while loading / when there are none. + /// Full-height Commits tab. + /// Every commit of the patch series, or a status message while loading or empty. fn render_commits_tab(&self, cx: &mut Context) -> AnyElement { if self.loading { return v_flex() @@ -838,8 +838,8 @@ impl PullRequestDetailView { .into_any_element() } - /// One row of the commits tab: id, summary, author and time. Clicking a - /// row opens the commit's diff in the bottom dock. + /// One row of the commits tab, id, summary, author and time. + /// Clicking a row opens the commit's diff in the bottom dock. fn render_commit_row( &self, ix: usize, @@ -882,8 +882,8 @@ impl PullRequestDetailView { .child(SharedString::from(meta)), ) }) - // Commits parsed from the nostr patch set may not exist in any - // local clone; only git-backed PRs open a diff viewer. + // Commits parsed from the nostr patch set may not exist in any local clone. + // Only git-backed PRs open a diff viewer. .when_some(self.worktree.clone(), |this, worktree| { this.on_click(cx.listener(move |this, _event, window, cx| { this.open_commit_diff(worktree.clone(), &id, window, cx); @@ -892,8 +892,8 @@ impl PullRequestDetailView { .into_any_element() } - /// One comment card, same design as the issue panel: avatar, author, - /// "commented" and age on the header row, content below. + /// One comment card, same design as the issue panel. + /// Header row holds the avatar, author, commented and age, content below. fn render_comments(&mut self, id: &EventId, cx: &mut Context) -> AnyElement { let store = self.store.read(cx); let comments: Vec<&Event> = store.comments_of(id).collect(); @@ -907,8 +907,7 @@ impl PullRequestDetailView { let author = profile.name(); let picture = profile.picture(); let age = relative_time(comment.created_at); - // Comment bodies are cloned into shared strings once per - // comment, not on every render. + // Comment bodies become shared strings once per comment, not per render. let content = self .contents .entry(comment.id) @@ -1002,7 +1001,7 @@ impl PullRequestDetailView { .into_any_element() } - /// Always-visible header: status badge and title, like the issue panel. + /// Always-visible header with a status badge and title, like the issue panel. fn render_header(&self, cx: &mut Context) -> AnyElement { let current_commit = self.current_commit.clone(); let (title, status, branch, author) = { @@ -1022,7 +1021,7 @@ impl PullRequestDetailView { ) }; - // Only the PR author may publish revisions (kind 1619, NIP-34). + // Only the PR author may publish revisions, NIP-34 kind 1619. let backend = Backend::global(cx); let can_update = backend.read(cx).current_user() == Some(author); @@ -1104,8 +1103,9 @@ impl PullRequestDetailView { } } -/// Open the "update pull request" dialog: a patch input that submits a new -/// revision through [`RepoStore::update_pull_request`] when confirmed. +/// Open the update pull request dialog. +/// The patch input supplies the new revision. +/// Confirming calls [`RepoStore::update_pull_request`]. fn open_update_pull_request_dialog( store: Entity, root: Event, @@ -1115,8 +1115,8 @@ fn open_update_pull_request_dialog( let patch = cx.new(|cx| { TextareaState::new(window, cx).placeholder("Paste the updated `git format-patch` output...") }); - // Both the dialog body and the submit button capture the root event; - // share it instead of cloning into each closure. + // Both the dialog body and submit button capture the root event. + // Share it instead of cloning into each closure. let root = Rc::new(root); window.open_dialog(cx, move |dialog, _window, _cx| { @@ -1176,7 +1176,7 @@ fn sidebar_title(text: &str, cx: &App) -> AnyElement { .into_any_element() } -/// The `c` tag of a PR event (tip of the proposed branch), as hex. +/// The `c` tag of a PR event, the tip of the proposed branch, as hex. fn current_commit_of(event: &Event) -> Option { event .tags @@ -1187,8 +1187,8 @@ fn current_commit_of(event: &Event) -> Option { }) } -/// The `merge-base` tag of a PR event (most recent common ancestor with the -/// target branch), as hex. +/// The `merge-base` tag of a PR event, as hex. +/// The most recent common ancestor with the target branch. fn merge_base_of(event: &Event) -> Option { event .tags @@ -1199,8 +1199,8 @@ fn merge_base_of(event: &Event) -> Option { }) } -/// The `clone` tag of a PR event (URLs where the proposed branch can be -/// fetched), or `None` if the PR has none. +/// The `clone` tag of a PR event. +/// URLs where the proposed branch can be fetched, or `None` if the PR has none. fn clone_urls_of(event: &Event) -> Option> { event .tags @@ -1222,9 +1222,10 @@ fn branch_name_of(event: &Event) -> Option { }) } -/// The latest PR update (kind 1619) revising `root`, found via its NIP-22 -/// `E` tag pointing at the root PR event. Only updates by the PR author -/// count: the tip of a PR is only mutable by its author (NIP-34). +/// The latest PR update, kind 1619, revising `root`. +/// Found via its NIP-22 `E` tag pointing at the root PR event. +/// Only updates by the PR author count. +/// The tip of a PR is only mutable by its author, NIP-34. fn latest_update<'a>(events: impl Iterator, root: &Event) -> Option<&'a Event> { let root_hex = root.id.to_hex(); events @@ -1238,8 +1239,8 @@ fn latest_update<'a>(events: impl Iterator, root: &Event) -> O .max_by_key(|e| e.created_at) } -/// One-line commit metadata for the commits list: author and relative time, -/// whichever is available. +/// One-line commit metadata for the commits list. +/// Author and relative time, whichever is available. fn commit_meta(commit: &FileCommit) -> String { let author = commit.author.trim(); let time = commit.time > 0; @@ -1357,8 +1358,7 @@ mod tests { created_at, ) }; - // An update revising a different PR must be ignored even though it - // is newer. + // An update revising a different PR must be ignored even though it is newer. let unrelated = signed( Kind::GitPullRequestUpdate, vec![Tag::parse(["E", OTHER_ROOT_HEX]).expect("valid tag")], @@ -1386,8 +1386,8 @@ mod tests { .finalize(&other) .expect("signed event"); - // The tip of a PR is only mutable by its author: a newer update - // from anyone else must not win. + // The tip of a PR is only mutable by its author. + // A newer update from anyone else must not win. assert!(latest_update([&stranger, &root].into_iter(), &root).is_none()); } diff --git a/crates/workspace/src/views/repo_detail/pull_requests.rs b/crates/workspace/src/views/repo_detail/pull_requests.rs index a2e4b1e..f87b414 100644 --- a/crates/workspace/src/views/repo_detail/pull_requests.rs +++ b/crates/workspace/src/views/repo_detail/pull_requests.rs @@ -25,12 +25,11 @@ use super::new_pull_request::open_new_pull_panel; use super::pull_request_detail::PullRequestDetailView; use super::send_patch::open_send_patch_panel; -/// Height of one pull request row in the virtual list; same layout as an -/// issue row. +/// Height of one pull request row in the virtual list. +/// Same layout as an issue row. const PR_ROW_HEIGHT: f32 = 73.; -/// Status filter of the pull request list, chosen via the header's filter -/// buttons. +/// Status filter of the pull request list, chosen via the header's filter buttons. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PullRequestFilter { /// Every pull request, regardless of status. @@ -70,17 +69,18 @@ pub struct PullRequestsView { filter: PullRequestFilter, /// Per-row heights of the virtual list. item_sizes: Rc>>, - /// Number of rows [`Self::item_sizes`] was built for (the filtered - /// pull request count); rebuilt on change. + /// The filtered pull request count [`Self::item_sizes`] was built for. + /// Rebuilt on change. pr_len: usize, - /// Indices into the store's `pull_requests` matching [`Self::filter`] - /// (root PR events only; updates are revisions of the root); the - /// virtual list renders this slice. Rebuilt only when the store - /// version or the filter changes, keyed by [`Self::cache_key`]. + /// Indices into the store's `pull_requests` matching [`Self::filter`]. + /// Root PR events only, updates are revisions of the root. + /// The virtual list renders this slice. + /// Rebuilt only when the store version or the filter changes. + /// Keyed by [`Self::cache_key`]. visible_prs: Vec, - /// Header counts `(total, open, closed, draft, merged)` of the root - /// pull requests only (revisions are not separate PRs), rebuilt with - /// [`Self::visible_prs`]. + /// Header counts `(total, open, closed, draft, merged)`. + /// Root pull requests only, revisions are not separate PRs. + /// Rebuilt with [`Self::visible_prs`]. counts: (usize, usize, usize, usize, usize), /// Store version and filter the cached rows/counts were built from. cache_key: Option<(u64, PullRequestFilter)>, @@ -112,7 +112,7 @@ impl PullRequestsView { } } - /// Open the detail panel of `pr_id` at the bottom of the dock area. + /// Open the detail panel of `pr_id` in the dock area. fn open_pull_request_detail( &mut self, pr_id: EventId, @@ -138,8 +138,8 @@ impl PullRequestsView { }); } - /// Render one row of the pull request list; `ix` is the row index and - /// `pr_ix` the index of the pull request in the store's `pull_requests`. + /// Render one row of the pull request list. + /// `ix` is the row index, `pr_ix` the index in the store's `pull_requests`. fn render_row(&self, ix: usize, pr_ix: usize, cx: &mut Context) -> AnyElement { let pr = &self.store.read(cx).pull_requests[pr_ix]; let pr_id = pr.id; @@ -204,8 +204,8 @@ impl PullRequestsView { } fn render_header(&self, cx: &mut Context) -> AnyElement { - // Counts of the last list rebuild (`render` rebuilds first when the - // store version or filter changed, so this is never stale). + // Counts of the last list rebuild. + // `render` rebuilds first when the store version or filter changed, so never stale. let (total, open, closed, draft, merged) = self.counts; h_flex() @@ -340,8 +340,8 @@ impl Render for PullRequestsView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let filter = self.filter; - // Rebuild the filtered rows and header counts only when the store - // refreshed or the filter changed; other renders reuse the cache. + // Rows and counts are rebuilt only when the store refreshed or filter changed. + // Other renders reuse the cache. let version = self.store.read(cx).version(); if self.cache_key != Some((version, filter)) { let store = self.store.read(cx); @@ -351,10 +351,10 @@ impl Render for PullRequestsView { .iter() .enumerate() .filter_map(|(ix, pr)| { - // Kind-30620 patches are revisions of a root PR (NIP-34), - // not separate pull requests: count only root events, or - // the header counts inflate with every revision (which - // also default to `Open` in `status_of`). + // Kind-30620 patches are revisions of a root PR, NIP-34. + // They are not separate pull requests. + // Count root events only, or the counts inflate with every revision. + // Revisions also default to `Open` in `status_of`. if pr.kind != Kind::GitPullRequest { return None; } @@ -375,8 +375,8 @@ impl Render for PullRequestsView { let count = self.visible_prs.len(); - // The virtual list's item count comes from `item_sizes`; rebuild it - // whenever the filtered pull request count changes. + // The virtual list's item count comes from `item_sizes`. + // Rebuild it whenever the filtered pull request count changes. if count != self.pr_len { self.pr_len = count; self.item_sizes = Rc::new(vec![size(px(0.), px(PR_ROW_HEIGHT)); count]); @@ -386,8 +386,8 @@ impl Render for PullRequestsView { let scroll_handle = self.scroll_handle.clone(); let view = cx.entity().clone(); - // Non-fatal warnings and errors of the last action (e.g. creating - // or updating a PR), shown as dismissible banners above the list. + // Non-fatal warnings and errors of the last action, like creating or updating a PR. + // Shown as dismissible banners above the list. let (last_error, last_warning) = { let store = self.store.read(cx); (store.last_error.clone(), store.last_warning.clone()) diff --git a/crates/workspace/src/views/repo_detail/send_patch.rs b/crates/workspace/src/views/repo_detail/send_patch.rs index f9279f4..fa13b52 100644 --- a/crates/workspace/src/views/repo_detail/send_patch.rs +++ b/crates/workspace/src/views/repo_detail/send_patch.rs @@ -19,15 +19,15 @@ pub struct SendPatchView { store: Entity, /// Display name of the repository, for the panel title. repo_name: SharedString, - /// Title input (required). + /// Title input, required. subject: Entity, - /// Description input (optional). + /// Description input, optional. description: Entity, - /// The pasted `git format-patch` output (required). + /// The pasted `git format-patch` output, required. patch: Entity, /// A submit is in flight. submitting: bool, - /// Error of the last submit attempt (keeps the panel open). + /// Error of the last submit attempt, it keeps the panel open. error: Option, _subscriptions: Vec, } @@ -71,10 +71,11 @@ impl SendPatchView { } } - /// Publish the pull request from the pasted patch. The store validates - /// synchronously (patch shape, per-part size, sign-in); on failure the - /// panel stays open with the error inline, on success it closes — async - /// publish failures surface in the pull request list's banner. + /// Publish the pull request from the pasted patch. + /// The store validates synchronously, patch shape, per-part size and sign-in. + /// On failure the panel stays open with the error inline. + /// On success it closes. + /// Async publish failures surface in the pull request list's banner. fn submit(&mut self, window: &mut Window, cx: &mut Context) { if self.submitting { return; @@ -93,8 +94,8 @@ impl SendPatchView { self.error = None; cx.notify(); - // Errors the store detects before publishing are returned - // synchronously through `last_error`. + // Errors the store detects before publishing. + // Returned synchronously through `last_error`. let sync_error = store.update(cx, |store, cx| { store.open_pull_request( (!subject.is_empty()).then_some(subject), diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index 7008656..a5ec859 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -24,13 +24,13 @@ use super::open_repo_panel; const COLUMNS: usize = 2; const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.; -/// How many of the newest repositories the "Recent" sort shows. +/// How many of the newest repositories the `Recent` sort shows. const RECENT_COUNT: usize = 10; /// Sort of the explore list, chosen via the header's filter buttons. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] enum RepoFilter { - /// Every repository, newest first (the store's default order). + /// Every repository in the store's default order, newest first. All, #[default] /// Repositories ranked by total issues + pull requests + commits. @@ -40,15 +40,15 @@ enum RepoFilter { } impl RepoFilter { - /// Indices into the store's `announcements` included by this filter, in - /// display order, narrowed to repositories whose name (or id) contains - /// `query`; an empty query matches everything. + /// Indices into the store's `announcements` this filter includes, in display order. + /// Narrowed to repositories whose name or id contains `query`. + /// An empty query matches everything. fn visible(self, store: &RepoListStore, query: &str) -> Vec { let announcements = &store.announcements; let mut indices: Vec = (0..announcements.len()).collect(); - // Narrow by the search query first, so "Recent" limits the matches - // and "Popular" ranks them. + // Narrow by the search query first. + // Recent then limits the matches and Popular ranks them. let query = query.trim().to_lowercase(); if !query.is_empty() { indices.retain(|&ix| { @@ -93,10 +93,10 @@ pub struct RepoListView { filter: RepoFilter, /// Per-row heights of the virtual list. item_sizes: Rc>>, - /// Number of rows [`Self::item_sizes`] was built for (the filtered repo count). + /// Number of rows [`Self::item_sizes`] was built for, the filtered repo count. repo_len: usize, - /// Indices into the store's `announcements` matching [`Self::filter`], - /// in display order; the virtual list renders this slice. + /// Indices matching [`Self::filter`] into the store's `announcements`. + /// The virtual list renders this slice in display order. visible: Vec, /// Search box filtering repositories by name. search: Entity, @@ -121,8 +121,8 @@ impl RepoListView { } }); - // Keep the visible slice and row sizes in sync with the store, - // so newly announced repositories appear without waiting for a click. + // Keep the visible slice and row sizes in sync with the store. + // Newly announced repositories appear without waiting for a click. let subscription = cx.observe(&store, |this, _store, cx| { this.rebuild_rows(cx); }); @@ -141,16 +141,16 @@ impl RepoListView { _subscription: subscription, }; - // Seed the rows right away; the store may already hold announcements - // (it loaded before the panel opened), and the first render must not - // depend on a later store update. + // Seed the rows right away. + // The store may already hold announcements from before the panel opened. + // The first render must not depend on a later store update. this.rebuild_rows(cx); this } - /// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the current - /// store contents, [`Self::filter`] and the search query. + /// Rebuild [`Self::visible`] and [`Self::item_sizes`] from the store. + /// Uses the store contents, [`Self::filter`] and the search query. fn rebuild_rows(&mut self, cx: &mut Context) { let filter = self.filter; let query = self.search.read(cx).value(); @@ -201,8 +201,8 @@ impl RepoListView { .map(|label| SharedString::from(format!("Updated {label}"))) .unwrap_or_default(); - // Fork badge: the upstream's display name when its announcement is - // known locally, otherwise its repository id from the `u` tag. + // The fork badge shows the upstream name when its announcement is known locally. + // Otherwise it shows the repository id from the `u` tag. let fork_label: Option = announcement.upstream.as_ref().and_then(|upstream| { let addr = upstream.addr.as_ref()?; @@ -347,8 +347,7 @@ impl RepoListView { .into_any_element() } - /// One segmented filter button of the header, styled like the issues - /// list's status filter buttons. + /// One segmented header filter button, like the issues list's status filter buttons. fn filter_button( &self, filter: RepoFilter, diff --git a/crates/workspace/src/views/sidebar/create_repo_dialog.rs b/crates/workspace/src/views/sidebar/create_repo_dialog.rs index 32ed2d8..68d6f12 100644 --- a/crates/workspace/src/views/sidebar/create_repo_dialog.rs +++ b/crates/workspace/src/views/sidebar/create_repo_dialog.rs @@ -24,10 +24,9 @@ pub struct CreateRepoState { } /// Open the Create Repository dialog. -/// -/// The dialog loads the user's default grasp servers (kind `10317` grasp -/// list) and falls back to the shared defaults when none are set. On -/// success the dialog closes and the new repository opens in the dock. +/// Loads the user's default grasp servers, a kind `10317` grasp list. +/// Falls back to the shared defaults when none are set. +/// On success the dialog closes and the new repository opens in the dock. pub fn open(dock_area: WeakEntity, window: &mut Window, cx: &mut App) { let settings = SettingsStore::global(cx); let default_folder = settings @@ -160,10 +159,9 @@ pub fn open(dock_area: WeakEntity, window: &mut Window, cx: &mut App) }); } -/// Prompt the user to pick the folder the repository will be stored in, using -/// the platform's native folder picker, and show the result in the disabled -/// folder input. The picked folder is remembered in the settings so it -/// becomes the default next time. +/// Pick the repository's storage folder with the platform's native folder picker. +/// Show the result in the disabled folder input. +/// The settings remember the picked folder as the default next time. fn choose_folder(folder_input: &Entity, window: &mut Window, cx: &mut App) { let handle = window.window_handle(); let folder_input = folder_input.clone(); @@ -198,8 +196,8 @@ fn choose_folder(folder_input: &Entity, window: &mut Window, cx: &mu .detach(); } -/// Run the create-repository flow, -/// opens the new working copy and the repository panel on success. +/// Run the create-repository flow. +/// Opens the new working copy and the repository panel on success. #[allow(clippy::too_many_arguments)] fn create_repository( name_input: Entity, @@ -247,8 +245,8 @@ fn create_repository( Ok((announcement, local_path)) => { cx.update_window(handle, |_, window, cx| { window.close_dialog(cx); - // Remember the new working copy as a checkout of this - // repository, so the New PR panel pre-fills it. + // Record the new working copy as a checkout of this repository. + // The New PR panel then pre-fills it. let checkouts = CheckoutsStore::global(cx); checkouts.update(cx, |store, cx| { store.record(local_path.clone(), announcement.addr(), cx); diff --git a/crates/workspace/src/views/sidebar/grasp_servers.rs b/crates/workspace/src/views/sidebar/grasp_servers.rs index fdfe212..0a824cf 100644 --- a/crates/workspace/src/views/sidebar/grasp_servers.rs +++ b/crates/workspace/src/views/sidebar/grasp_servers.rs @@ -9,24 +9,21 @@ use settings::{DEFAULT_GRASP_SERVERS, GraspServersSettings}; use signed_core::filters; use signed_state::Backend; -/// State of the grasp-server section of a publish dialog, so async -/// results can be rendered. +/// State of the grasp-server section of a publish dialog, so async results can be rendered. #[derive(Default)] pub struct GraspServersState { - /// The user's grasp list (kind `10317`) is being loaded. + /// The user's grasp list of kind `10317` is being loaded. pub loading_servers: bool, pub grasp_servers: Vec, - /// Whether the grasp server section is shown; defaults to shown. + /// Whether the grasp server section is shown. Defaults to shown. pub servers_enabled: bool, - /// Error of the last grasp-server edit (e.g. an invalid relay URL). + /// Error of the last grasp-server edit, an invalid relay URL for example. pub error: Option, } impl GraspServersState { - /// Defaults until the user's grasp list arrives; replaced by it when it lists any servers. - /// - /// The servers come from the persisted settings, falling back to the - /// built-in defaults when the configured list is empty. + /// Defaults used until the user's grasp list loads, which replaces them when non-empty. + /// Persisted settings supply the defaults, an empty list falls back to the built-ins. pub fn new_default(settings: &GraspServersSettings) -> Self { let urls: Vec = if settings.default_servers.is_empty() { DEFAULT_GRASP_SERVERS @@ -48,10 +45,9 @@ impl GraspServersState { } } -/// The "Grasp servers" form field shared by the publish dialogs: an -/// expandable toggle, the configured servers (each removable) and an -/// add-relay input, with a loading hint while the user's grasp list -/// (kind `10317`) is being fetched. +/// The Grasp servers form field shared by the publish dialogs. +/// An expandable toggle, the configured servers each removable, and an add-relay input. +/// Shows a loading hint while the user's kind `10317` grasp list is fetched. pub fn grasp_servers_field( state: &Entity, relay_input: &Entity, @@ -143,7 +139,7 @@ pub fn grasp_servers_field( })) } -/// A grasp server row: the host as a tag plus a remove button. +/// One grasp server row, the host in a tag plus a remove button. fn render_server_row( ix: usize, relay: &RelayUrl, @@ -182,7 +178,7 @@ fn render_server_row( ) } -/// The bare host of a grasp server (defaults are entered without a scheme). +/// The bare host of a grasp server, defaults are entered without a scheme. fn display_server(relay: &RelayUrl) -> SharedString { relay .domain() @@ -190,7 +186,7 @@ fn display_server(relay: &RelayUrl) -> SharedString { .unwrap_or_else(|| SharedString::from(relay.to_string())) } -/// Parse the relay input (accepting a bare host) and append it to the list. +/// Parse the relay input, accepting a bare host, and append it to the list. fn add_relay( state: &Entity, input: &Entity, @@ -226,8 +222,8 @@ fn add_relay( } } -/// Load the user's grasp list (kind `10317`) from the local database and -/// replace the defaults with it when it lists any servers. +/// Load the user's grasp list of kind `10317` from the local database. +/// It replaces the defaults when it lists any servers. pub fn load_user_grasp_servers( state: Entity, window: &mut Window, diff --git a/crates/workspace/src/views/sidebar/import_dialog.rs b/crates/workspace/src/views/sidebar/import_dialog.rs index cfe8c37..5f5c8de 100644 --- a/crates/workspace/src/views/sidebar/import_dialog.rs +++ b/crates/workspace/src/views/sidebar/import_dialog.rs @@ -2,8 +2,7 @@ use gpui::{App, Window, px}; use gpui_component::WindowExt; /// Open the Import Identity dialog. -/// -/// Currently a placeholder — the dialog only shows a title for now. +/// Currently a placeholder, the dialog only shows a title. pub fn open(window: &mut Window, cx: &mut App) { window.open_dialog(cx, move |dialog, _window, _cx| { dialog.title("Import identity").width(px(400.)) diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs index 7c9db02..6d4004b 100644 --- a/crates/workspace/src/views/sidebar/mod.rs +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -32,25 +32,25 @@ mod settings_dialog; use self::onboarding_dialog::OnboardingState; -/// Left-dock panel with navigation entries. Entries open content panels in -/// the dock area. +/// Left-dock panel with navigation entries. +/// Entries open content panels in the dock area. pub struct SidebarPanel { focus_handle: FocusHandle, dock_area: WeakEntity, explore: Option>, logged_in: bool, - /// Repositories announced by the current user, listed under - /// "All Repositories". Recreated when the signer changes. + /// Repositories the current user announced, listed under the All Repositories heading. + /// Recreated when the signer changes. my_repos: Option>, /// Observes the current user's repo store so the list re-renders. my_repos_subscription: Option, - /// Banner artwork shown behind the sign-in screen, - /// picked at random from the bundled `backgrounds/` assets. + /// Banner artwork behind the sign-in screen. + /// Picked at random from the bundled `backgrounds/` assets. banner: SharedString, /// Observes the local-repository scan so new discoveries re-render. _local_repos_subscription: Subscription, - /// Observes the checkouts store, whose ready-to-push statuses feed the - /// badges on the user's repository rows. + /// Observes the checkouts store. + /// Its ready-to-push statuses feed the badges on the user's repo rows. _checkouts_subscription: Subscription, _subscription: Subscription, } @@ -107,8 +107,8 @@ impl SidebarPanel { panel } - /// (Re)create the store listing the current user's repositories, - /// and watch each of them for unpushed local work. + /// Recreate the store listing the current user's repositories. + /// Watch each repository for unpushed local work. fn refresh_my_repos(&mut self, cx: &mut Context) { self.my_repos_subscription = None; @@ -119,9 +119,9 @@ impl SidebarPanel { if let Some(store) = self.my_repos.as_ref() { self.my_repos_subscription = Some(cx.observe(store, |_this, store, cx| { cx.notify(); - // These are the signed-in user's own repositories; request - // their ready-to-push statuses (deduplicated per repo) so - // the rows carry a badge while local work is unpushed. + // These are the signed-in user's own repositories. + // Request their ready-to-push statuses, deduplicated per repository. + // The rows carry a badge while local work is unpushed. let addrs: Vec<_> = store .read(cx) .announcements @@ -138,8 +138,8 @@ impl SidebarPanel { } } - /// Open the Explore (repository list) panel in the center of the dock - /// area. No-op if it's already open. + /// Open the Explore repository list panel in the dock area's center. + /// No-op if it is already open. pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context) { if self .explore @@ -191,8 +191,8 @@ impl SidebarPanel { open_repo_panel(&self.dock_area, announcement, window, &mut *cx); } - /// Open a local repository's detail view in the dock's center; the - /// detail view offers to publish it to NIP-34. + /// Open a local repository's detail view in the dock's center. + /// The detail view offers to publish it to NIP-34. fn open_local_repo(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context) { let detail = cx.new(|cx| RepoDetailView::new_local(self.dock_area.clone(), path, window, cx)); @@ -208,10 +208,10 @@ impl SidebarPanel { }); } - /// The "All Repositories" section: header with the create button and - /// the current user's repositories below it, lazily rendered through a - /// [`uniform_list`], followed by the local git repositories discovered - /// by the startup scan. + /// The All Repositories section of the sidebar. + /// A header with the create button above the current user's repositories. + /// Rendered lazily through a [`uniform_list`]. + /// Followed by local git repositories from the startup scan. fn render_my_repos(&self, cx: &mut Context) -> impl IntoElement { let store = self.my_repos.as_ref(); let local = LocalReposStore::global(cx); @@ -265,11 +265,10 @@ impl SidebarPanel { ) .when_some(store, |builder, store| { let announcements = store.read(cx).announcements.clone(); - // Local repositories that have already been published to - // NIP-34 are listed among the user's repositories above; - // hide them from the local section (matched by the - // identifier derived from the directory name, like the - // init dialog's default name). + // Local repositories already published to NIP-34 appear above. + // Hide them from the local section here. + // Matched by the identifier derived from the directory name. + // Same derivation as the init dialog's default name. let announced_ids: HashSet = announcements.iter().map(|a| a.id.clone()).collect(); let local_repos: Vec = local_repos @@ -282,8 +281,8 @@ impl SidebarPanel { }) .cloned() .collect(); - // One merged list: the user's NIP-34 repositories first, - // then the local repositories discovered by the scan. + // One merged list, the user's NIP-34 repositories first. + // Local repositories discovered by the scan follow. let total = announcements.len() + local_repos.len(); if total == 0 { @@ -326,8 +325,7 @@ impl SidebarPanel { }) } - /// One row of the merged sidebar list: a NIP-34 repository or a local - /// repository. + /// One row of the merged sidebar list, a NIP-34 or a local repository. fn render_repo_row_at( &self, announcements: &[Announcement], @@ -358,8 +356,8 @@ impl SidebarPanel { .unwrap_or_else(|| SharedString::from(announcement.id.clone())); let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)); - // A small badge with the unpushed commit count of the repository's - // local checkouts (ready to push to the grasp servers). + // Badge with the unpushed commit count of the repository's local checkouts. + // The commits are ready to push to the grasp servers. let unpushed: usize = CheckoutsStore::global(cx) .read(cx) .push_statuses_of(&announcement.addr()) @@ -379,10 +377,9 @@ impl SidebarPanel { ) } - /// One local repository row: a deterministic pixel avatar seeded from - /// the path, the directory name, and a warning suffix marking it as - /// not yet set up for NIP-34. Clicking it opens the repository's - /// detail view, which offers to initialize it. + /// One local repository row, a deterministic pixel avatar seeded from the path. + /// The directory name and a warning suffix, the repo is not yet set up for NIP-34. + /// Clicking opens the detail view, which offers to initialize it. fn render_local_row(&self, path: &Path, cx: &mut Context) -> impl IntoElement { let name = path .file_name() @@ -410,7 +407,7 @@ impl SidebarPanel { import_dialog::open(window, cx); } - /// Render the user avatar and name in the sidebar, wrapped in the window titlebar drag area. + /// Render the user avatar and name in the sidebar, inside the titlebar drag area. fn render_user( &self, profile: &Profile, @@ -440,8 +437,8 @@ impl SidebarPanel { ) } - /// Sign-in placeholder shown while logged out: banner artwork behind a - /// scrim so the CTA buttons stay readable in both themes. + /// Sign-in placeholder shown while logged out. + /// Banner artwork behind a scrim keeps the CTA buttons readable in both themes. fn render_sign_in(&self, window: &mut Window, cx: &mut Context) -> Div { v_flex() .size_full() diff --git a/crates/workspace/src/views/sidebar/onboarding_dialog.rs b/crates/workspace/src/views/sidebar/onboarding_dialog.rs index 20112f8..4eca90d 100644 --- a/crates/workspace/src/views/sidebar/onboarding_dialog.rs +++ b/crates/workspace/src/views/sidebar/onboarding_dialog.rs @@ -15,10 +15,8 @@ pub struct OnboardingState { } /// Open the Onboarding dialog for creating a new identity. -/// -/// The caller is responsible for creating the input and state entities and -/// passing them in. This function only builds the dialog UI and wires up -/// the continue-button handler. +/// The caller creates the input and state entities and passes them in. +/// This function only builds the dialog UI and wires up the continue-button handler. pub fn open( name_input: Entity, pass_input: Entity, diff --git a/crates/workspace/src/views/sidebar/passphrase_dialog.rs b/crates/workspace/src/views/sidebar/passphrase_dialog.rs index ead19f3..f425f2f 100644 --- a/crates/workspace/src/views/sidebar/passphrase_dialog.rs +++ b/crates/workspace/src/views/sidebar/passphrase_dialog.rs @@ -17,9 +17,8 @@ pub struct PassphraseState { _enter_subscription: Option, } -/// Open the dialog asking for the passphrase that protects the stored -/// NIP-49 encrypted identity (`ncryptsec1...`). -/// +/// Open the dialog asking for the passphrase that protects the stored identity. +/// The identity is NIP-49 encrypted, for example `ncryptsec1...`. /// Called when the backend emits [`signed_state::BackendEvent::PassphraseRequired`]. pub fn open(window: &mut Window, cx: &mut App) { let pass_input = cx.new(|cx| { @@ -98,8 +97,9 @@ pub fn open(window: &mut Window, cx: &mut App) { }); } -/// Submit the passphrase to the backend. On success the dialog is closed; -/// on failure the error is rendered inline and the dialog stays open. +/// Submit the passphrase to the backend. +/// On success the dialog closes. +/// On failure the error is rendered inline and the dialog stays open. fn unlock( pass_input: &Entity, state: &Entity, diff --git a/crates/workspace/src/views/sidebar/settings_dialog.rs b/crates/workspace/src/views/sidebar/settings_dialog.rs index 2d58011..8fceb54 100644 --- a/crates/workspace/src/views/sidebar/settings_dialog.rs +++ b/crates/workspace/src/views/sidebar/settings_dialog.rs @@ -1,10 +1,3 @@ -//! The Settings dialog, opened from the sidebar's Settings entry. -//! -//! A custom settings layout that divides related settings into sections -//! separated by simple horizontal lines — no `GroupBox` boxes and no settings -//! navigation sidebar. Every control edits the persisted [`SettingsStore`] -//! and applies the change to the live theme immediately. - use std::cell::Cell; use std::path::PathBuf; use std::rc::Rc; @@ -55,8 +48,8 @@ fn theme_options(cx: &App) -> (Vec, Vec) { (light, dark) } -/// Stateful controls of the settings dialog, created once when it opens so -/// their values survive re-renders of the dialog content. +/// Stateful controls of the settings dialog, created once when it opens. +/// Their values survive re-renders of the dialog content. struct SettingsControls { appearance: Entity>>, light_theme: Entity>>, @@ -66,8 +59,7 @@ struct SettingsControls { radius: Entity, radius_lg: Entity, grasp_server_input: Entity, - /// The effective default create-repository folder, shown in the disabled - /// folder selector. + /// The effective default create-repository folder, shown in the disabled input. default_folder: Entity, /// Keeps the control subscriptions alive for the dialog's lifetime. _subscriptions: Vec, @@ -296,8 +288,8 @@ pub fn open(window: &mut Window, cx: &mut App) { }); } -/// The settings content: one section per related setting, divided by -/// horizontal separator lines. +/// The settings content, one section per related setting. +/// Sections are divided by horizontal separator lines. fn settings_view(controls: &SettingsControls, cx: &mut App) -> impl IntoElement { let store = SettingsStore::global(cx); let settings = store.read(cx).settings().clone(); @@ -325,8 +317,7 @@ fn appearance_section(controls: &SettingsControls, cx: &App) -> impl IntoElement )) } -/// Theme configuration: the theme names in the registry plus -/// the visual tweaks the application customizes at startup. +/// Theme configuration, the registry theme names plus tweaks the app customizes at startup. fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> impl IntoElement { v_flex() .gap_3() @@ -397,7 +388,7 @@ fn theme_section(settings: &Settings, controls: &SettingsControls, cx: &App) -> )) } -/// The default grasp servers offered when the user hasn't published a grasp list (kind `10317`) yet. +/// Default grasp servers offered until the user publishes a kind `10317` grasp list. fn grasp_servers_section( settings: &Settings, controls: &SettingsControls, @@ -413,8 +404,8 @@ fn grasp_servers_section( )) } -/// The editable list of default grasp servers plus an add-relay input, -/// styled like the grasp-server section of the publish dialogs. +/// The editable list of default grasp servers plus an add-relay input. +/// Styled like the grasp-server section of the publish dialogs. fn grasp_server_editor( servers: &[String], controls: &SettingsControls, @@ -478,8 +469,8 @@ fn grasp_server_editor( ) } -/// The bare host of a grasp server (defaults are entered without a scheme), -/// matching how the publish dialogs display servers. +/// The bare host of a grasp server, defaults are entered without a scheme. +/// Matches how the publish dialogs display servers. fn display_server(server: &str) -> SharedString { RelayUrl::parse(server) .ok() @@ -513,8 +504,8 @@ fn repositories_section( )) } -/// The editable list of scan directories plus an add-directory button, -/// styled like the grasp-server list. +/// The editable list of scan directories plus an add-directory button. +/// Styled like the grasp-server list. fn scan_paths_editor(scan_paths: &[PathBuf], cx: &App) -> impl IntoElement { v_flex() .w_full() @@ -566,8 +557,8 @@ fn scan_paths_editor(scan_paths: &[PathBuf], cx: &App) -> impl IntoElement { ) } -/// The default-folder selector: a disabled input showing the effective -/// folder plus a picker button, matching the create-repository dialog. +/// The default-folder selector, a disabled input plus a picker button. +/// Matches the create-repository dialog. fn folder_selector(controls: &SettingsControls) -> impl IntoElement { let default_folder = controls.default_folder.clone(); h_flex() @@ -590,8 +581,8 @@ fn folder_selector(controls: &SettingsControls) -> impl IntoElement { ) } -/// Parse the server input (accepting a bare host) and append it to the -/// default grasp servers. +/// Parse the server input and append it to the default grasp servers. +/// A bare host is accepted. fn add_server(input: &Entity, window: &mut Window, cx: &mut App) { let value = input.read(cx).value().trim().to_owned(); if value.is_empty() { @@ -660,8 +651,8 @@ fn add_scan_path(cx: &mut App) { .detach(); } -/// Prompt for the folder the Create Repository dialog should default to, -/// remembering it in the settings and showing it in the disabled input. +/// Prompt for the Create Repository dialog's default folder. +/// Remember it in the settings and show it in the disabled input. fn choose_default_folder(default_folder: &Entity, window: &mut Window, cx: &mut App) { let handle = window.window_handle(); let default_folder = default_folder.clone(); @@ -695,8 +686,9 @@ fn choose_default_folder(default_folder: &Entity, window: &mut Windo .detach(); } -/// Wire a number input to the settings: steps clamp and persist, typed -/// changes parse, clamp and persist. +/// Wire a number input to the settings. +/// Step actions clamp and persist the value. +/// Typed changes parse, clamp and persist. fn wire_number_input( state: &Entity, subscriptions: &mut Vec, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3f63701..73c87fe 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -45,10 +45,9 @@ impl Workspace { let mut subscriptions = vec![]; - // A bottom/right dock whose last panel was dragged away is removed - // entirely: base keeps the emptied region, which would otherwise - // linger as a bare strip. Deferred, because the event arrives while - // the area is mid-update. + // A bottom or right dock whose last panel was dragged away is removed entirely. + // The emptied region would otherwise linger as a bare strip. + // The removal is deferred, the event arrives while the area is mid-update. let dock_for_pruning = dock.clone(); subscriptions.push(cx.subscribe_in( &dock, @@ -78,9 +77,8 @@ impl Workspace { let backend = Backend::global(cx); - // Ask for the passphrase when the stored identity is NIP-49 - // encrypted. Subscribed via the window, since opening a dialog - // needs one. + // Ask for the passphrase when the stored identity is NIP-49 encrypted. + // Subscribed via the window, since opening a dialog needs a window. let passphrase_subscription = window.subscribe(&backend, cx, |_backend, event, window, cx| { if matches!(event, BackendEvent::PassphraseRequired) { @@ -88,9 +86,9 @@ impl Workspace { } }); - // The event may have fired before this window existed (the backend - // is initialized before the first window opens); fall back to the - // backend state in that case. + // The event may have fired before this window existed. + // The backend is initialized before the first window opens. + // Fall back to the backend state in that case. if backend.read(cx).passphrase_required() { passphrase_dialog::open(window, cx); } diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 9f6aa79..990cb3d 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -17,14 +17,14 @@ fn main() { gpui_component::init(cx); theme::init(cx); - // Load the persisted settings before applying the theme, - // so the stored appearance and theme configuration take effect at startup. + // Load the persisted settings before applying the theme. + // Stored appearance and theme settings then take effect at startup. let store = cx.new(|cx| SettingsStore::new(paths::settings_file(), cx)); SettingsStore::set_global(store.clone(), cx); let settings = store.read(cx).settings().clone(); - // Register the built-in "Signed" theme (light + dark variants) - // and make it the active theme, following the stored appearance. + // Register the built-in Signed theme, light and dark variants. + // The stored appearance then selects the active theme. let registry = ThemeRegistry::global_mut(cx); for (name, content) in Assets.themes() { if let Err(err) = registry.load_themes_from_str(&content) {