clean up
This commit is contained in:
Generated
-1
@@ -7900,7 +7900,6 @@ dependencies = [
|
|||||||
name = "signed_core"
|
name = "signed_core"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
|
||||||
"nostr",
|
"nostr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ use std::path::PathBuf;
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
/// The application name.
|
/// The application name.
|
||||||
|
///
|
||||||
/// It derives the platform-specific data, config and cache directory paths.
|
/// It derives the platform-specific data, config and cache directory paths.
|
||||||
pub const APP_NAME: &str = "Signed";
|
pub const APP_NAME: &str = "Signed";
|
||||||
|
|
||||||
/// Lowercased form of [`APP_NAME`].
|
/// Lowercased form of [`APP_NAME`].
|
||||||
|
///
|
||||||
/// Used in XDG-style paths on Linux and FreeBSD, and the macOS `~/.config` fallback.
|
/// Used in XDG-style paths on Linux and FreeBSD, and the macOS `~/.config` fallback.
|
||||||
pub const APP_NAME_LOWERCASE: &str = "signed";
|
pub const APP_NAME_LOWERCASE: &str = "signed";
|
||||||
|
|
||||||
@@ -27,12 +29,14 @@ pub fn home_dir() -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current user's Desktop folder.
|
/// Returns the current user's Desktop folder.
|
||||||
|
///
|
||||||
/// Falls back to the home directory or an empty path when it cannot be determined.
|
/// Falls back to the home directory or an empty path when it cannot be determined.
|
||||||
pub fn desktop_dir() -> PathBuf {
|
pub fn desktop_dir() -> PathBuf {
|
||||||
dirs::desktop_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
|
dirs::desktop_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current user's Documents folder.
|
/// Returns the current user's Documents folder.
|
||||||
|
///
|
||||||
/// Falls back to the home directory or an empty path when it cannot be determined.
|
/// Falls back to the home directory or an empty path when it cannot be determined.
|
||||||
pub fn documents_dir() -> PathBuf {
|
pub fn documents_dir() -> PathBuf {
|
||||||
dirs::document_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
|
dirs::document_dir().unwrap_or_else(|| dirs::home_dir().unwrap_or_default())
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
|
||||||
nostr.workspace = true
|
nostr.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use gpui::SharedString;
|
|
||||||
use nostr::prelude::*;
|
use nostr::prelude::*;
|
||||||
|
|
||||||
use crate::RepoAddr;
|
use crate::RepoAddr;
|
||||||
@@ -16,8 +15,8 @@ pub struct Announcement {
|
|||||||
pub owner: PublicKey,
|
pub owner: PublicKey,
|
||||||
/// When the announcement was published, used for latest-wins resolution.
|
/// When the announcement was published, used for latest-wins resolution.
|
||||||
pub created_at: Timestamp,
|
pub created_at: Timestamp,
|
||||||
pub name: Option<SharedString>,
|
pub name: Option<String>,
|
||||||
pub description: Option<SharedString>,
|
pub description: Option<String>,
|
||||||
/// Webpage URLs for browsing.
|
/// Webpage URLs for browsing.
|
||||||
pub web: Vec<Url>,
|
pub web: Vec<Url>,
|
||||||
/// URLs for `git clone`.
|
/// URLs for `git clone`.
|
||||||
@@ -62,17 +61,17 @@ impl Upstream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Text for display.
|
/// Text for display.
|
||||||
pub fn display(&self) -> SharedString {
|
pub fn display(&self) -> String {
|
||||||
match &self.addr {
|
match &self.addr {
|
||||||
Some(addr) => SharedString::from(addr.to_string()),
|
Some(addr) => addr.to_string(),
|
||||||
None => SharedString::from(self.raw.clone()),
|
None => self.raw.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subject of a NIP-34 issue or pull request event.
|
/// Subject of a NIP-34 issue or pull request event.
|
||||||
/// Taken from the `subject` tag, else the first non-empty line of the content.
|
/// Taken from the `subject` tag, else the first non-empty line of the content.
|
||||||
pub fn activity_subject(event: &Event) -> SharedString {
|
pub fn activity_subject(event: &Event) -> String {
|
||||||
let subject = event
|
let subject = event
|
||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
@@ -82,16 +81,15 @@ pub fn activity_subject(event: &Event) -> SharedString {
|
|||||||
});
|
});
|
||||||
|
|
||||||
subject
|
subject
|
||||||
.map(SharedString::from)
|
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
event
|
event
|
||||||
.content
|
.content
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.find(|line| !line.is_empty())
|
.find(|line| !line.is_empty())
|
||||||
.map(SharedString::from)
|
.map(|value| value.to_string())
|
||||||
})
|
})
|
||||||
.unwrap_or(SharedString::from("Untitled"))
|
.unwrap_or("Untitled".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The patch set of a pull request.
|
/// The patch set of a pull request.
|
||||||
@@ -219,8 +217,8 @@ impl Announcement {
|
|||||||
let mut hashtags: Vec<String> = Vec::new();
|
let mut hashtags: Vec<String> = Vec::new();
|
||||||
hashtags.extend(event.tags.hashtags().map(|t| t.to_string()));
|
hashtags.extend(event.tags.hashtags().map(|t| t.to_string()));
|
||||||
|
|
||||||
let mut name: Option<SharedString> = None;
|
let mut name: Option<String> = None;
|
||||||
let mut description: Option<SharedString> = None;
|
let mut description: Option<String> = None;
|
||||||
let mut web: Vec<Url> = Vec::new();
|
let mut web: Vec<Url> = Vec::new();
|
||||||
let mut clone: Vec<Url> = Vec::new();
|
let mut clone: Vec<Url> = Vec::new();
|
||||||
let mut relays: Vec<RelayUrl> = Vec::new();
|
let mut relays: Vec<RelayUrl> = Vec::new();
|
||||||
@@ -230,8 +228,8 @@ impl Announcement {
|
|||||||
|
|
||||||
for tag in event.tags.iter() {
|
for tag in event.tags.iter() {
|
||||||
match Nip34Tag::parse(tag.as_slice()) {
|
match Nip34Tag::parse(tag.as_slice()) {
|
||||||
Ok(Nip34Tag::Name(value)) => name = Some(value.into()),
|
Ok(Nip34Tag::Name(value)) => name = Some(value),
|
||||||
Ok(Nip34Tag::Description(value)) => description = Some(value.into()),
|
Ok(Nip34Tag::Description(value)) => description = Some(value),
|
||||||
Ok(Nip34Tag::Web(urls)) => web.extend(urls),
|
Ok(Nip34Tag::Web(urls)) => web.extend(urls),
|
||||||
Ok(Nip34Tag::Clone(urls)) => clone.extend(urls),
|
Ok(Nip34Tag::Clone(urls)) => clone.extend(urls),
|
||||||
Ok(Nip34Tag::Relays(urls)) => relays.extend(urls),
|
Ok(Nip34Tag::Relays(urls)) => relays.extend(urls),
|
||||||
@@ -288,10 +286,10 @@ impl Announcement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The description of the repository, or a default if none is provided.
|
/// The description of the repository, or a default if none is provided.
|
||||||
pub fn description(&self) -> SharedString {
|
pub fn description(&self) -> String {
|
||||||
self.description
|
self.description
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or(SharedString::from("No description"))
|
.unwrap_or("No description".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The effective maintainers of this repository,
|
/// The effective maintainers of this repository,
|
||||||
@@ -307,11 +305,11 @@ impl Announcement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `git clone` URLs for this repository, deduplicated.
|
/// The `git clone` URLs for this repository, deduplicated.
|
||||||
pub fn clone_urls(&self) -> Vec<SharedString> {
|
pub fn clone_urls(&self) -> Vec<String> {
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
self.clone
|
self.clone
|
||||||
.iter()
|
.iter()
|
||||||
.map(|url| SharedString::from(format!("git clone {url}")))
|
.map(|url| format!("git clone {url}"))
|
||||||
.filter(|command| seen.insert(command.clone()))
|
.filter(|command| seen.insert(command.clone()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ impl RepoStore {
|
|||||||
self.announcement
|
self.announcement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(SharedString::default(), |a| {
|
.map_or(SharedString::default(), |a| {
|
||||||
a.name.clone().unwrap_or(SharedString::from("Unknown"))
|
SharedString::from(a.name.as_deref().unwrap_or("Unknown"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ use gpui_component::menu::PopupMenuItem;
|
|||||||
use gpui_component::{ActiveTheme, StyledExt, h_flex};
|
use gpui_component::{ActiveTheme, StyledExt, h_flex};
|
||||||
|
|
||||||
/// A muted command row with a copy button.
|
/// A muted command row with a copy button.
|
||||||
pub fn copy_row<E>(copy_id: E, command: &SharedString, cx: &App) -> Div
|
pub fn copy_row<E, T>(copy_id: E, command: T, cx: &App) -> Div
|
||||||
where
|
where
|
||||||
E: Into<ElementId>,
|
E: Into<ElementId>,
|
||||||
|
T: Into<SharedString>,
|
||||||
{
|
{
|
||||||
|
let command = command.into();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.h_8()
|
.h_8()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
|
|||||||
text(
|
text(
|
||||||
announcement
|
announcement
|
||||||
.name
|
.name
|
||||||
.clone()
|
.as_deref()
|
||||||
.unwrap_or_else(|| SharedString::from("—")),
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from("-")),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
@@ -41,8 +42,9 @@ fn announcement_rows(announcement: &Announcement, cx: &App) -> AnyElement {
|
|||||||
text(
|
text(
|
||||||
announcement
|
announcement
|
||||||
.description
|
.description
|
||||||
.clone()
|
.as_deref()
|
||||||
.unwrap_or_else(|| SharedString::from("—")),
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from("-")),
|
||||||
),
|
),
|
||||||
cx,
|
cx,
|
||||||
));
|
));
|
||||||
@@ -131,7 +133,12 @@ fn row(label: &'static str, value: AnyElement, cx: &App) -> AnyElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Plain text value, wrapping within the dialog.
|
/// Plain text value, wrapping within the dialog.
|
||||||
fn text(value: SharedString) -> AnyElement {
|
fn text<T>(value: T) -> AnyElement
|
||||||
|
where
|
||||||
|
T: Into<SharedString>,
|
||||||
|
{
|
||||||
|
let value = value.into();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ impl RepoDetailView {
|
|||||||
self.code_element(path.as_ref(), cx)
|
self.code_element(path.as_ref(), cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(FileContent::Binary) => placeholder("Binary file — preview not supported", cx),
|
Some(FileContent::Binary) => placeholder("Binary file - preview not supported", cx),
|
||||||
Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx),
|
Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx),
|
||||||
Some(FileContent::Failed(message)) => placeholder(message, cx),
|
Some(FileContent::Failed(message)) => placeholder(message, cx),
|
||||||
None => preview_spinner(),
|
None => preview_spinner(),
|
||||||
|
|||||||
@@ -1342,7 +1342,8 @@ impl RepoDetailView {
|
|||||||
.map(|announcement| {
|
.map(|announcement| {
|
||||||
announcement
|
announcement
|
||||||
.name
|
.name
|
||||||
.clone()
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -2359,13 +2360,14 @@ fn fork_row(announcement: &Announcement, cx: &mut Context<RepoDetailView>) -> Op
|
|||||||
.find(|a| a.addr() == *addr)
|
.find(|a| a.addr() == *addr)
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
a.name
|
a.name
|
||||||
.clone()
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| SharedString::from(addr.identifier.clone()));
|
.unwrap_or_else(|| SharedString::from(addr.identifier.clone()));
|
||||||
(SharedString::from(format!("Forked from {name}")), true)
|
(SharedString::from(format!("Forked from {name}")), true)
|
||||||
}
|
}
|
||||||
None => (upstream.display(), false),
|
None => (SharedString::from(upstream.display().as_str()), false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let row = h_flex()
|
let row = h_flex()
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ fn fork_candidates<'a>(
|
|||||||
fn fork_display_name(announcement: &Announcement) -> SharedString {
|
fn fork_display_name(announcement: &Announcement) -> SharedString {
|
||||||
announcement
|
announcement
|
||||||
.name
|
.name
|
||||||
.clone()
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,12 +187,14 @@ impl RepoListView {
|
|||||||
|
|
||||||
let name = announcement
|
let name = announcement
|
||||||
.name
|
.name
|
||||||
.clone()
|
.as_deref()
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
.map(SharedString::from)
|
||||||
|
.unwrap_or(SharedString::from(announcement.id.clone()));
|
||||||
|
|
||||||
let description = announcement
|
let description = announcement
|
||||||
.description
|
.description
|
||||||
.clone()
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
.unwrap_or(SharedString::from("No description"));
|
.unwrap_or(SharedString::from("No description"));
|
||||||
|
|
||||||
let activity = last_activity
|
let activity = last_activity
|
||||||
@@ -213,7 +215,8 @@ impl RepoListView {
|
|||||||
.find(|a| a.addr() == *addr)
|
.find(|a| a.addr() == *addr)
|
||||||
.map(|a| {
|
.map(|a| {
|
||||||
a.name
|
a.name
|
||||||
.clone()
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
.unwrap_or_else(|| SharedString::from(a.id.clone()))
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| SharedString::from(addr.identifier.clone()));
|
.unwrap_or_else(|| SharedString::from(addr.identifier.clone()));
|
||||||
|
|||||||
@@ -347,7 +347,8 @@ impl SidebarPanel {
|
|||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let name = announcement
|
let name = announcement
|
||||||
.name
|
.name
|
||||||
.clone()
|
.as_deref()
|
||||||
|
.map(SharedString::from)
|
||||||
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
|
||||||
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
|
||||||
|
|
||||||
|
|||||||
+9
-9
@@ -55,19 +55,19 @@ Key points of the write side:
|
|||||||
branches; all git ops run in that folder. Checkouts of the target repo
|
branches; all git ops run in that folder. Checkouts of the target repo
|
||||||
are remembered (folder pick + app clones) and matched implicitly
|
are remembered (folder pick + app clones) and matched implicitly
|
||||||
(origin URL or EUC against the announcement), so the panel prefills the
|
(origin URL or EUC against the announcement), so the panel prefills the
|
||||||
freshest one — no folder dialog for the common case.
|
freshest one - no folder dialog for the common case.
|
||||||
- *Announced fork*: the fork's heads are fetched into the target repo's
|
- *Announced fork*: the fork's heads are fetched into the target repo's
|
||||||
GitCache mirror under `refs/fork/<owner-hex>/<id>/*` (private
|
GitCache mirror under `refs/fork/<owner-hex>/<id>/*` (private
|
||||||
namespace; the browser never sees them). "Merge Into" lists the
|
namespace; the browser never sees them). "Merge Into" lists the
|
||||||
mirror's `refs/remotes/origin/*`, "Pull From" the imported fork
|
mirror's `refs/remotes/origin/*`, "Pull From" the imported fork
|
||||||
branches, and every git op — merge-base, range diff/commits,
|
branches, and every git op - merge-base, range diff/commits,
|
||||||
format-patch, tip push — runs in the mirror, which holds both
|
format-patch, tip push - runs in the mirror, which holds both
|
||||||
histories. Fork candidates are announcements related to the target by
|
histories. Fork candidates are announcements related to the target by
|
||||||
`u` tag or shared EUC, own forks first, without `clone` URLs excluded.
|
`u` tag or shared EUC, own forks first, without `clone` URLs excluded.
|
||||||
- **GRASP-06 hosting**: the tip is pushed under `refs/nostr/<event-id>`
|
- **GRASP-06 hosting**: the tip is pushed under `refs/nostr/<event-id>`
|
||||||
(nak's convention) to the *author's* grasp servers first —
|
(nak's convention) to the *author's* grasp servers first -
|
||||||
`https://<host>/prs/<author-npub>/<repo-id>.git`, resolved from the
|
`https://<host>/prs/<author-npub>/<repo-id>.git`, resolved from the
|
||||||
author's kind-10317 grasp list, falling back to the settings defaults —
|
author's kind-10317 grasp list, falling back to the settings defaults -
|
||||||
then to the base repository's announced grasp servers. The `clone` tag
|
then to the base repository's announced grasp servers. The `clone` tag
|
||||||
lists those `/prs/` URLs first, then the announced clone URLs (fixed
|
lists those `/prs/` URLs first, then the announced clone URLs (fixed
|
||||||
before signing; dead URLs are inert, the patches stay the source of
|
before signing; dead URLs are inert, the patches stay the source of
|
||||||
@@ -77,8 +77,8 @@ Key points of the write side:
|
|||||||
grows past NIP-34's 60 KB guidance; the PR's `c` tag carries the *last*
|
grows past NIP-34's 60 KB guidance; the PR's `c` tag carries the *last*
|
||||||
commit of the series (the tip), and each part carries its own
|
commit of the series (the tip), and each part carries its own
|
||||||
`commit`/`r` tags.
|
`commit`/`r` tags.
|
||||||
- **Push before publish**: failure is non-fatal — the patch events remain
|
- **Push before publish**: failure is non-fatal - the patch events remain
|
||||||
the source of truth — and surfaces as a `last_warning` banner.
|
the source of truth - and surfaces as a `last_warning` banner.
|
||||||
- **1619 updates are paste-only today** (no repo path holds the new tip's
|
- **1619 updates are paste-only today** (no repo path holds the new tip's
|
||||||
objects), so updates are not pushed; hosting them is deferred until the
|
objects), so updates are not pushed; hosting them is deferred until the
|
||||||
update dialog gains a local-checkout source.
|
update dialog gains a local-checkout source.
|
||||||
@@ -120,7 +120,7 @@ thread: current branch vs its base (announced HEAD, else `main`, else the
|
|||||||
first branch), commits ahead, dirty worktrees excluded. A banner in the
|
first branch), commits ahead, dirty worktrees excluded. A banner in the
|
||||||
repository panel then offers a prefilled New PR panel for the first branch
|
repository panel then offers a prefilled New PR panel for the first branch
|
||||||
that is ahead with **no open PR by you** proposing it (`branch-name` tag,
|
that is ahead with **no open PR by you** proposing it (`branch-name` tag,
|
||||||
falling back to the `c` tip tag) — NIP-34-native dedupe, refreshed
|
falling back to the `c` tip tag) - NIP-34-native dedupe, refreshed
|
||||||
periodically and whenever the checkouts/announcements change. The panel
|
periodically and whenever the checkouts/announcements change. The panel
|
||||||
never submits anything on its own; suggestions only navigate and prefill.
|
never submits anything on its own; suggestions only navigate and prefill.
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ Reader rules that keep the flow consistent:
|
|||||||
|
|
||||||
- **Status**: only status events by the root author or a repository
|
- **Status**: only status events by the root author or a repository
|
||||||
maintainer count; the newest wins, `Open` is the default.
|
maintainer count; the newest wins, `Open` is the default.
|
||||||
- **Tip**: only kind-1619 updates by the PR author move the tip — a
|
- **Tip**: only kind-1619 updates by the PR author move the tip - a
|
||||||
stranger's update is ignored.
|
stranger's update is ignored.
|
||||||
- **Diff**: the patch set is preferred (NIP-34 `e`-linked chain); PRs from
|
- **Diff**: the patch set is preferred (NIP-34 `e`-linked chain); PRs from
|
||||||
other clients without patch events fall back to diffing
|
other clients without patch events fall back to diffing
|
||||||
|
|||||||
Reference in New Issue
Block a user