add send patch panel
This commit is contained in:
@@ -6,11 +6,11 @@ use crate::RepoAddr;
|
|||||||
|
|
||||||
/// Kinds that make up the activity of a repository.
|
/// Kinds that make up the activity of a repository.
|
||||||
pub const ACTIVITY_KINDS: [Kind; 9] = [
|
pub const ACTIVITY_KINDS: [Kind; 9] = [
|
||||||
|
Kind::Comment,
|
||||||
Kind::GitPatch,
|
Kind::GitPatch,
|
||||||
Kind::GitPullRequest,
|
Kind::GitPullRequest,
|
||||||
Kind::GitPullRequestUpdate,
|
Kind::GitPullRequestUpdate,
|
||||||
Kind::GitIssue,
|
Kind::GitIssue,
|
||||||
Kind::Comment,
|
|
||||||
Kind::GitStatusOpen,
|
Kind::GitStatusOpen,
|
||||||
Kind::GitStatusApplied,
|
Kind::GitStatusApplied,
|
||||||
Kind::GitStatusClosed,
|
Kind::GitStatusClosed,
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ pub struct Announcement {
|
|||||||
pub hashtags: Vec<String>,
|
pub hashtags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `u` tag of a fork announcement (NIP-34): the repository this one is a
|
/// The `u` tag of a fork announcement (NIP-34)
|
||||||
/// subordinate fork of. The first value is the upstream coordinate
|
/// the repository this one is a subordinate fork of. The first value is
|
||||||
/// (`30617:<pubkey>:<id>`) or a git URL; the second is an optional relay hint
|
/// the upstream coordinate (`30617:<pubkey>:<id>`) or a git URL.
|
||||||
/// for the upstream.
|
/// The second is an optional relay hint for the upstream.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Upstream {
|
pub struct Upstream {
|
||||||
/// Raw first value of the `u` tag (coordinate or git URL).
|
/// Raw first value of the `u` tag (coordinate or git URL).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
use bitcoin_hashes::sha1::Hash as Sha1Hash;
|
||||||
use gpui::{AppContext, AsyncApp, Context, Subscription, Task, WeakEntity};
|
use gpui::{AppContext, AsyncApp, Context, SharedString, Subscription, Task, WeakEntity};
|
||||||
use nostr::event::IntoEventBuilder;
|
use nostr::event::IntoEventBuilder;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use signed_core::{
|
use signed_core::{
|
||||||
@@ -157,12 +157,22 @@ impl RepoStore {
|
|||||||
store
|
store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the repository's address.
|
||||||
pub fn addr(&self) -> &RepoAddr {
|
pub fn addr(&self) -> &RepoAddr {
|
||||||
&self.addr
|
&self.addr
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filters that make up a repository: announcement, state, activity and
|
/// Returns the repository's name, or "Unknown" if not known.
|
||||||
/// deletions targeting it.
|
pub fn name(&self) -> SharedString {
|
||||||
|
self.announcement
|
||||||
|
.as_ref()
|
||||||
|
.map_or(SharedString::default(), |a| {
|
||||||
|
a.name.clone().unwrap_or(SharedString::from("Unknown"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters that make up a repository: announcement, state,
|
||||||
|
/// activity and deletions targeting it.
|
||||||
fn repo_filters(addr: &RepoAddr) -> Vec<Filter> {
|
fn repo_filters(addr: &RepoAddr) -> Vec<Filter> {
|
||||||
let mut filters = vec![
|
let mut filters = vec![
|
||||||
// Announcement and state share author and identifier, so they
|
// Announcement and state share author and identifier, so they
|
||||||
@@ -202,8 +212,7 @@ impl RepoStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch this repository's events from the bootstrap relays (one-shot,
|
/// Fetch this repository's events from the bootstrap relays
|
||||||
/// auto-closing subscription).
|
|
||||||
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
let addr = self.addr.clone();
|
let addr = self.addr.clone();
|
||||||
@@ -215,10 +224,8 @@ impl RepoStore {
|
|||||||
|
|
||||||
/// Re-query the local database and update all fields.
|
/// Re-query the local database and update all fields.
|
||||||
///
|
///
|
||||||
/// Debounced: a short delay collapses bursts of requests (e.g. per-event
|
/// The query and processing run on a background thread,
|
||||||
/// `NostrUpdate`s), and requests that arrive while a query is running are
|
/// only the results are applied on the main thread.
|
||||||
/// folded 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<Self>) {
|
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.refreshing {
|
if self.refreshing {
|
||||||
self.refresh_dirty = true;
|
self.refresh_dirty = true;
|
||||||
@@ -242,7 +249,6 @@ impl RepoStore {
|
|||||||
self.tasks.push(task);
|
self.tasks.push(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One query + apply cycle (debounced entry point).
|
|
||||||
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
fn run_refresh(&mut self, cx: &mut Context<Self>) {
|
||||||
self.refreshing = true;
|
self.refreshing = true;
|
||||||
|
|
||||||
@@ -297,13 +303,15 @@ impl RepoStore {
|
|||||||
// NIP-22 comments reference their root via an `E`/`e` tag rather
|
// 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
|
// than the repository's `a` tag, so query them by the root events
|
||||||
// of this repository.
|
// of this repository.
|
||||||
let db = client.database();
|
|
||||||
let mut seen_comments: HashSet<EventId> = comments.iter().map(|e| e.id).collect();
|
let mut seen_comments: HashSet<EventId> = comments.iter().map(|e| e.id).collect();
|
||||||
|
let db = client.database();
|
||||||
|
|
||||||
let roots = issues
|
let roots = issues
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&patches)
|
.chain(&patches)
|
||||||
.chain(&pull_requests)
|
.chain(&pull_requests)
|
||||||
.map(|e| e.id);
|
.map(|e| e.id);
|
||||||
|
|
||||||
for filter in filters::comments_for(roots) {
|
for filter in filters::comments_for(roots) {
|
||||||
for event in db.query(filter).await? {
|
for event in db.query(filter).await? {
|
||||||
if seen_comments.insert(event.id) {
|
if seen_comments.insert(event.id) {
|
||||||
@@ -312,16 +320,17 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status events may omit their `a` tag (NIP-34 makes it
|
// Status events may omit their `a` tag,
|
||||||
// optional), so also query them by the root events they
|
// so also query them by the root events they reference.
|
||||||
// reference.
|
|
||||||
let db = client.database();
|
|
||||||
let mut seen_statuses: HashSet<EventId> = statuses.iter().map(|e| e.id).collect();
|
let mut seen_statuses: HashSet<EventId> = statuses.iter().map(|e| e.id).collect();
|
||||||
|
let db = client.database();
|
||||||
|
|
||||||
let roots = issues
|
let roots = issues
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&patches)
|
.chain(&patches)
|
||||||
.chain(&pull_requests)
|
.chain(&pull_requests)
|
||||||
.map(|e| e.id);
|
.map(|e| e.id);
|
||||||
|
|
||||||
for root in roots {
|
for root in roots {
|
||||||
for event in db.query(filters::statuses_for([root])).await? {
|
for event in db.query(filters::statuses_for([root])).await? {
|
||||||
if seen_statuses.insert(event.id) {
|
if seen_statuses.insert(event.id) {
|
||||||
@@ -330,17 +339,18 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cover notes (1624) and label events (1985) reference their
|
// Cover notes (1624) and label events (1985) reference
|
||||||
// target via an `e` tag, so query them per root like comments
|
// so query them per root like comments and statuses.
|
||||||
// and statuses.
|
|
||||||
let db = client.database();
|
|
||||||
let mut seen_cover_notes: HashSet<EventId> = cover_notes.iter().map(|e| e.id).collect();
|
let mut seen_cover_notes: HashSet<EventId> = cover_notes.iter().map(|e| e.id).collect();
|
||||||
let mut seen_labels: HashSet<EventId> = labels.iter().map(|e| e.id).collect();
|
let mut seen_labels: HashSet<EventId> = labels.iter().map(|e| e.id).collect();
|
||||||
|
let db = client.database();
|
||||||
|
|
||||||
let roots = issues
|
let roots = issues
|
||||||
.iter()
|
.iter()
|
||||||
.chain(&patches)
|
.chain(&patches)
|
||||||
.chain(&pull_requests)
|
.chain(&pull_requests)
|
||||||
.map(|e| e.id);
|
.map(|e| e.id);
|
||||||
|
|
||||||
for root in roots {
|
for root in roots {
|
||||||
for event in db.query(filters::annotations_for([root])).await? {
|
for event in db.query(filters::annotations_for([root])).await? {
|
||||||
if deletions.is_deleted(&event) {
|
if deletions.is_deleted(&event) {
|
||||||
@@ -368,12 +378,15 @@ impl RepoStore {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(Announcement::effective_maintainers)
|
.map(Announcement::effective_maintainers)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let status_by_root =
|
let status_by_root =
|
||||||
resolve_statuses(&issues, &patches, &pull_requests, &statuses, &maintainers);
|
resolve_statuses(&issues, &patches, &pull_requests, &statuses, &maintainers);
|
||||||
|
|
||||||
let open_issue_count = issues
|
let open_issue_count = issues
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|issue| status_of(&status_by_root, issue) == RepoStatus::Open)
|
.filter(|issue| status_of(&status_by_root, issue) == RepoStatus::Open)
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
let open_pr_count = pull_requests
|
let open_pr_count = pull_requests
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|pr| {
|
.filter(|pr| {
|
||||||
@@ -426,8 +439,8 @@ impl RepoStore {
|
|||||||
let again = this.update(cx, |this, cx| {
|
let again = this.update(cx, |this, cx| {
|
||||||
this.announcement = announcement;
|
this.announcement = announcement;
|
||||||
|
|
||||||
// The announcement may list relays for this repository's
|
// The announcement may list relays for this repository's activity,
|
||||||
// activity; connect to any we haven't fetched from yet.
|
// connect to any we haven't fetched from yet.
|
||||||
let relays = this
|
let relays = this
|
||||||
.announcement
|
.announcement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -462,11 +475,13 @@ impl RepoStore {
|
|||||||
.chain(&this.pull_requests)
|
.chain(&this.pull_requests)
|
||||||
.map(|e| e.id)
|
.map(|e| e.id)
|
||||||
.collect::<HashSet<EventId>>();
|
.collect::<HashSet<EventId>>();
|
||||||
|
|
||||||
let new_roots: Vec<EventId> = roots
|
let new_roots: Vec<EventId> = roots
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|id| !this.root_fetches.contains(id))
|
.filter(|id| !this.root_fetches.contains(id))
|
||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !new_roots.is_empty() {
|
if !new_roots.is_empty() {
|
||||||
this.root_fetches.extend(new_roots.iter().copied());
|
this.root_fetches.extend(new_roots.iter().copied());
|
||||||
// Batch the per-root filters: one statuses filter and one
|
// Batch the per-root filters: one statuses filter and one
|
||||||
@@ -476,6 +491,7 @@ impl RepoStore {
|
|||||||
let mut root_filters = filters::comments_for(new_roots.clone());
|
let mut root_filters = filters::comments_for(new_roots.clone());
|
||||||
root_filters.push(filters::statuses_for(new_roots.iter().copied()));
|
root_filters.push(filters::statuses_for(new_roots.iter().copied()));
|
||||||
root_filters.push(filters::annotations_for(new_roots));
|
root_filters.push(filters::annotations_for(new_roots));
|
||||||
|
|
||||||
let announced: Vec<RelayUrl> = this.repo_relays.iter().cloned().collect();
|
let announced: Vec<RelayUrl> = this.repo_relays.iter().cloned().collect();
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
backend.update(cx, |backend, cx| {
|
backend.update(cx, |backend, cx| {
|
||||||
@@ -495,8 +511,7 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Requests that arrived while the refresh was running are
|
// Requests that arrived while the refresh was running are coalesced into one follow-up refresh.
|
||||||
// coalesced into one follow-up refresh.
|
|
||||||
if again {
|
if again {
|
||||||
this.update(cx, |this, cx| this.refresh(cx))?;
|
this.update(cx, |this, cx| this.refresh(cx))?;
|
||||||
}
|
}
|
||||||
@@ -505,21 +520,19 @@ impl RepoStore {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the status of a root event (issue / patch / PR) per NIP-34:
|
/// Resolve the status of a root event (issue / patch / PR) per NIP-34
|
||||||
/// a lookup into the map built on the last refresh.
|
|
||||||
pub fn status_of(&self, root: &Event) -> RepoStatus {
|
pub fn status_of(&self, root: &Event) -> RepoStatus {
|
||||||
status_of(&self.status_by_root, root)
|
status_of(&self.status_by_root, root)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh generation, incremented on every applied refresh. Views use
|
/// Refresh generation, incremented on every applied refresh.
|
||||||
/// it to key their derived-data caches (filtered lists, counts) so
|
/// Views use it to key their derived-data caches.
|
||||||
/// renders that change nothing stay O(1).
|
|
||||||
pub fn version(&self) -> u64 {
|
pub fn version(&self) -> u64 {
|
||||||
self.version
|
self.version
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The effective cover note of `root` (kind 1624), if any: the latest
|
/// The effective cover note of `root` (kind 1624), if any:
|
||||||
/// note authored by the root author or a maintainer.
|
/// the latest note authored by the root author or a maintainer.
|
||||||
pub fn cover_note_of(&self, root: &Event) -> Option<&Event> {
|
pub fn cover_note_of(&self, root: &Event) -> Option<&Event> {
|
||||||
let maintainers = self
|
let maintainers = self
|
||||||
.announcement
|
.announcement
|
||||||
@@ -557,14 +570,13 @@ impl RepoStore {
|
|||||||
|
|
||||||
/// Number of open issues: issues whose resolved status is
|
/// Number of open issues: issues whose resolved status is
|
||||||
/// [`RepoStatus::Open`] (issues without status events default to open).
|
/// [`RepoStatus::Open`] (issues without status events default to open).
|
||||||
/// Cached on the last refresh.
|
|
||||||
pub fn issue_count(&self) -> usize {
|
pub fn issue_count(&self) -> usize {
|
||||||
self.open_issue_count
|
self.open_issue_count
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Number of open pull requests: root PR events (not PR updates, whose
|
/// Number of open pull requests: root PR events (not PR updates, whose
|
||||||
/// status is carried by the root) with a resolved status of
|
/// status is carried by the root) with a resolved status of
|
||||||
/// [`RepoStatus::Open`]. Cached on the last refresh.
|
/// [`RepoStatus::Open`].
|
||||||
pub fn pull_request_count(&self) -> usize {
|
pub fn pull_request_count(&self) -> usize {
|
||||||
self.open_pr_count
|
self.open_pr_count
|
||||||
}
|
}
|
||||||
@@ -596,15 +608,13 @@ impl RepoStore {
|
|||||||
.filter(move |e| signed_core::references_root(e, root))
|
.filter(move |e| signed_core::references_root(e, root))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Comment on a root event (issue / PR) per NIP-34 (kind 1111), using
|
/// Comment on a root event (issue / PR) per NIP-34 (kind 1111)
|
||||||
/// the SDK's NIP-22 `CommentBuilder` so other NIP-34 clients (ngit,
|
|
||||||
/// GitWorkshop) can thread the comment.
|
|
||||||
pub fn comment(&mut self, root: &Event, content: String, cx: &mut Context<Self>) {
|
pub fn comment(&mut self, root: &Event, content: String, cx: &mut Context<Self>) {
|
||||||
self.reply(root, None, content, cx);
|
self.reply(root, None, content, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reply to `parent` (a comment on `root`) with a NIP-22 threaded
|
/// Reply to `parent` (a comment on `root`) with a NIP-22 threaded comment,
|
||||||
/// comment; `None` publishes a top-level comment on the root itself.
|
/// `None` publishes a top-level comment on the root itself.
|
||||||
pub fn reply(
|
pub fn reply(
|
||||||
&mut self,
|
&mut self,
|
||||||
root: &Event,
|
root: &Event,
|
||||||
@@ -664,6 +674,7 @@ impl RepoStore {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if let Some(oversized) = series
|
if let Some(oversized) = series
|
||||||
.iter()
|
.iter()
|
||||||
.find(|part| part.len() > MAX_PATCH_EVENT_BYTES)
|
.find(|part| part.len() > MAX_PATCH_EVENT_BYTES)
|
||||||
@@ -677,8 +688,7 @@ impl RepoStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tip of the series is its last commit; `git format-patch`
|
// The tip of the series is its last commit; `git format-patch` orders patches oldest first.
|
||||||
// orders patches oldest first.
|
|
||||||
let Some(current_commit) = series
|
let Some(current_commit) = series
|
||||||
.last()
|
.last()
|
||||||
.and_then(|part| patch_current_commit(part))
|
.and_then(|part| patch_current_commit(part))
|
||||||
@@ -692,16 +702,18 @@ impl RepoStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
|
let signer = backend.read(cx).signer();
|
||||||
|
|
||||||
if backend.read(cx).current_user().is_none() {
|
if backend.read(cx).current_user().is_none() {
|
||||||
self.last_error = Some("Sign in to open a pull request".into());
|
self.last_error = Some("Sign in to open a pull request".into());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let signer = backend.read(cx).signer();
|
|
||||||
|
|
||||||
let addr = self.addr.clone();
|
let addr = self.addr.clone();
|
||||||
let owner = self.addr.public_key;
|
let owner = self.addr.public_key;
|
||||||
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
let euc = self.announcement.as_ref().and_then(|a| a.euc.clone());
|
||||||
|
|
||||||
let (push_owner, push_repo_id, push_relays) = self
|
let (push_owner, push_repo_id, push_relays) = self
|
||||||
.announcement
|
.announcement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -742,9 +754,8 @@ impl RepoStore {
|
|||||||
subject,
|
subject,
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
branch_name,
|
branch_name,
|
||||||
// NIP-34: PRs carry at least one clone URL where the
|
// NIP-34: PRs carry at least one clone URL where the tip commit can be downloaded,
|
||||||
// tip commit can be downloaded; the announced mirrors
|
// the announced mirrors are also the servers the tip is pushed to below.
|
||||||
// are also the servers the tip is pushed to below.
|
|
||||||
clone: this
|
clone: this
|
||||||
.announcement
|
.announcement
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -757,8 +768,7 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
.into_event_builder();
|
.into_event_builder();
|
||||||
|
|
||||||
// NIP-34: the `r` EUC tag lets clients subscribe to all
|
// NIP-34: the `r` EUC tag lets clients subscribe to all PRs of this repository
|
||||||
// PRs of this repository; the SDK builder omits it.
|
|
||||||
match this.announcement.as_ref().and_then(|a| a.euc.clone()) {
|
match this.announcement.as_ref().and_then(|a| a.euc.clone()) {
|
||||||
Some(euc) => builder.tag(Tag::parse(["r", &euc]).expect("valid r tag")),
|
Some(euc) => builder.tag(Tag::parse(["r", &euc]).expect("valid r tag")),
|
||||||
None => builder,
|
None => builder,
|
||||||
@@ -803,6 +813,7 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if pushed == 0 {
|
if pushed == 0 {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.last_warning = Some(format!(
|
this.last_warning = Some(format!(
|
||||||
@@ -818,6 +829,7 @@ impl RepoStore {
|
|||||||
let backend = Backend::global(cx);
|
let backend = Backend::global(cx);
|
||||||
backend.update(cx, |backend, cx| backend.publish_event(event, cx))
|
backend.update(cx, |backend, cx| backend.publish_event(event, cx))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let pr_event = match publish_task.await {
|
let pr_event = match publish_task.await {
|
||||||
Ok(event) => event,
|
Ok(event) => event,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -828,8 +840,8 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// NIP-34: a draft PR carries a kind-1633 status event; publish
|
// NIP-34: a draft PR carries a kind-1633 status event,
|
||||||
// it right after the PR event so viewers never show it open.
|
// publish it right after the PR event so viewers never show it open.
|
||||||
if draft {
|
if draft {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_status(&pr_event, RepoStatus::Draft, cx);
|
this.set_status(&pr_event, RepoStatus::Draft, cx);
|
||||||
@@ -842,8 +854,7 @@ impl RepoStore {
|
|||||||
|
|
||||||
/// Update a pull request: publish revision patch events chained to the
|
/// 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
|
/// 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
|
/// first, per NIP-34), then a kind-1619 PR update event carrying the new tip.
|
||||||
/// new tip.
|
|
||||||
///
|
///
|
||||||
/// Only the PR author may update it; other authors must open a new PR.
|
/// 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>) {
|
pub fn update_pull_request(&mut self, root: &Event, patch: String, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ use signed_core::RepoStatus;
|
|||||||
pub fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
pub fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
||||||
let (icon, label, tooltip, bg, fg) = match status {
|
let (icon, label, tooltip, bg, fg) = match status {
|
||||||
RepoStatus::Open => (
|
RepoStatus::Open => (
|
||||||
CustomIconName::GitIssueDone,
|
CustomIconName::GitIssueOpen,
|
||||||
"open",
|
"open",
|
||||||
"Issue is open",
|
"Issue is open",
|
||||||
cx.theme().primary,
|
cx.theme().secondary,
|
||||||
cx.theme().primary_foreground,
|
cx.theme().secondary_foreground,
|
||||||
),
|
),
|
||||||
RepoStatus::Closed => (
|
RepoStatus::Closed => (
|
||||||
CustomIconName::GitIssueClosed,
|
CustomIconName::GitIssueClosed,
|
||||||
@@ -31,11 +31,11 @@ pub fn status_badge(status: RepoStatus, cx: &App) -> AnyElement {
|
|||||||
cx.theme().accent_foreground,
|
cx.theme().accent_foreground,
|
||||||
),
|
),
|
||||||
RepoStatus::Applied => (
|
RepoStatus::Applied => (
|
||||||
CustomIconName::GitIssueOpen,
|
CustomIconName::GitIssueDone,
|
||||||
"applied",
|
"applied",
|
||||||
"Issue is completed",
|
"Issue is completed",
|
||||||
cx.theme().secondary,
|
cx.theme().primary,
|
||||||
cx.theme().secondary_foreground,
|
cx.theme().primary_foreground,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ mod issues;
|
|||||||
mod new_pull_request;
|
mod new_pull_request;
|
||||||
mod pull_request_detail;
|
mod pull_request_detail;
|
||||||
mod pull_requests;
|
mod pull_requests;
|
||||||
|
mod send_patch;
|
||||||
|
|
||||||
use about::open_about_dialog;
|
use about::open_about_dialog;
|
||||||
use browser::{
|
use browser::{
|
||||||
@@ -54,8 +55,10 @@ use commits::COMMIT_ROW_HEIGHT;
|
|||||||
use diff::CommitDiffView;
|
use diff::CommitDiffView;
|
||||||
use helpers::{ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
|
use helpers::{ShareTargets, TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
|
||||||
use issues::{IssuesView, open_new_issue_dialog};
|
use issues::{IssuesView, open_new_issue_dialog};
|
||||||
use new_pull_request::open_new_pull_request_panel;
|
|
||||||
use pull_requests::PullRequestsView;
|
use pull_requests::PullRequestsView;
|
||||||
|
use send_patch::open_send_patch_panel;
|
||||||
|
|
||||||
|
use crate::views::repo_detail::new_pull_request::open_new_pull_panel;
|
||||||
|
|
||||||
/// What kind of ref the header selectors switch to.
|
/// What kind of ref the header selectors switch to.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -67,13 +70,17 @@ enum RefKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Header actions dispatched by the dropdown menus of the header buttons.
|
/// 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.
|
||||||
#[derive(Clone, Action, PartialEq, Eq)]
|
#[derive(Clone, Action, PartialEq, Eq)]
|
||||||
#[action(namespace = repo_detail, no_json)]
|
#[action(namespace = repo_detail, no_json)]
|
||||||
enum RepoAction {
|
pub(super) enum RepoAction {
|
||||||
/// Open the "new issue" dialog.
|
/// Open the "new issue" dialog.
|
||||||
NewIssue,
|
NewIssue,
|
||||||
/// Open the "new pull request" dialog.
|
/// Open the "new pull request" dialog.
|
||||||
NewPR,
|
NewPR,
|
||||||
|
/// Open the "send patch" panel.
|
||||||
|
SendPatch,
|
||||||
/// Open the about dialog.
|
/// Open the about dialog.
|
||||||
About,
|
About,
|
||||||
/// Re-push the repository to its grasp servers.
|
/// Re-push the repository to its grasp servers.
|
||||||
@@ -1363,13 +1370,12 @@ impl RepoDetailView {
|
|||||||
}
|
}
|
||||||
RepoAction::NewPR => {
|
RepoAction::NewPR => {
|
||||||
if let Some(store) = this.store.clone() {
|
if let Some(store) = this.store.clone() {
|
||||||
open_new_pull_request_panel(
|
open_new_pull_panel(this.dock_area.clone(), store, window, cx);
|
||||||
this.dock_area.clone(),
|
}
|
||||||
store,
|
}
|
||||||
this.display_name(cx),
|
RepoAction::SendPatch => {
|
||||||
window,
|
if let Some(store) = this.store.clone() {
|
||||||
cx,
|
open_send_patch_panel(this.dock_area.clone(), store, window, cx);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RepoAction::About => {
|
RepoAction::About => {
|
||||||
@@ -1517,8 +1523,18 @@ impl RepoDetailView {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.child(Icon::new(IconName::Plus))
|
.child(Icon::new(IconName::Plus))
|
||||||
.child("New PR")
|
.child("New Pull Request")
|
||||||
})
|
})
|
||||||
|
.menu_element(
|
||||||
|
Box::new(RepoAction::SendPatch),
|
||||||
|
|_, _| {
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(Icon::new(IconName::File))
|
||||||
|
.child("Send Patch")
|
||||||
|
},
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ impl NewPullRequestView {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
repo_name: SharedString,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let repo_name = store.read(cx).name();
|
||||||
let pane = cx.new(DiffPane::new);
|
let pane = cx.new(DiffPane::new);
|
||||||
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
||||||
let description = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe..."));
|
let description = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe..."));
|
||||||
@@ -796,16 +796,14 @@ fn render_ref_trigger(
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the "new pull request" panel for `store` in the center dock.
|
/// Open the "new pull request" panel in the center dock.
|
||||||
pub(super) fn open_new_pull_request_panel(
|
pub(super) fn open_new_pull_panel(
|
||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
store: Entity<RepoStore>,
|
store: Entity<RepoStore>,
|
||||||
repo_name: SharedString,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
let panel =
|
let panel = cx.new(|cx| NewPullRequestView::new(dock_area.clone(), store, window, cx));
|
||||||
cx.new(|cx| NewPullRequestView::new(dock_area.clone(), store, repo_name, window, cx));
|
|
||||||
|
|
||||||
let _ = dock_area.update(cx, |dock_area, cx| {
|
let _ = dock_area.update(cx, |dock_area, cx| {
|
||||||
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
|
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
|
||||||
|
|||||||
@@ -7,18 +7,23 @@ use gpui::{
|
|||||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
||||||
SharedString, Size, WeakEntity, Window, div, px, size,
|
SharedString, Size, WeakEntity, Window, div, px, size,
|
||||||
};
|
};
|
||||||
|
use gpui_base::Button as BaseButton;
|
||||||
use gpui_component::alert::Alert;
|
use gpui_component::alert::Alert;
|
||||||
use gpui_component::scroll::Scrollbar;
|
use gpui_component::scroll::Scrollbar;
|
||||||
use gpui_component::{ActiveTheme, Icon, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list};
|
use gpui_component::{
|
||||||
|
ActiveTheme, Icon, IconName, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
||||||
|
};
|
||||||
use nostr::prelude::{EventId, Kind};
|
use nostr::prelude::{EventId, Kind};
|
||||||
use signed_core::{RepoStatus, activity_subject};
|
use signed_core::{RepoStatus, activity_subject};
|
||||||
use signed_state::{ProfileStore, RepoStore};
|
use signed_state::{ProfileStore, RepoStore};
|
||||||
use signed_ui::image_cache::{MAX_IMAGES, image_cache};
|
use signed_ui::image_cache::{MAX_IMAGES, image_cache};
|
||||||
use signed_ui::{SegmentButton, UserAvatar, placeholder, status_badge};
|
use signed_ui::{DropdownButton, SegmentButton, UserAvatar, placeholder, status_badge};
|
||||||
use utils::relative_time;
|
use utils::relative_time;
|
||||||
|
|
||||||
use super::new_pull_request::open_new_pull_request_panel;
|
use super::RepoAction;
|
||||||
|
use super::new_pull_request::open_new_pull_panel;
|
||||||
use super::pull_request_detail::PullRequestDetailView;
|
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
|
/// Height of one pull request row in the virtual list; same layout as an
|
||||||
/// issue row.
|
/// issue row.
|
||||||
@@ -36,8 +41,7 @@ enum PullRequestFilter {
|
|||||||
Closed,
|
Closed,
|
||||||
/// Pull requests whose resolved status is [`RepoStatus::Draft`].
|
/// Pull requests whose resolved status is [`RepoStatus::Draft`].
|
||||||
Draft,
|
Draft,
|
||||||
/// Pull requests whose resolved status is [`RepoStatus::Applied`]
|
/// Pull requests whose resolved status is [`RepoStatus::Applied`].
|
||||||
/// (i.e. merged).
|
|
||||||
Merged,
|
Merged,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +217,7 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.h_12()
|
.h_12()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
SegmentButton::new("all", "All")
|
SegmentButton::new("all", "All")
|
||||||
.icon(Icon::new(CustomIconName::GitPullRequest))
|
.icon(Icon::new(CustomIconName::GitPullRequest))
|
||||||
@@ -267,19 +271,43 @@ impl PullRequestsView {
|
|||||||
)
|
)
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
.child(
|
.child(
|
||||||
SegmentButton::new("new-pr", "New pull request")
|
h_flex().items_center().child(
|
||||||
.icon(Icon::new(CustomIconName::CirclePlus))
|
DropdownButton::new("new-pr-actions")
|
||||||
.primary()
|
.action(
|
||||||
|
BaseButton::new("new-pr")
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.h_8()
|
||||||
|
.px_2()
|
||||||
|
.gap_1()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().primary)
|
||||||
|
.hover(|this| this.bg(cx.theme().primary_hover))
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().primary_foreground)
|
||||||
|
.child(Icon::new(IconName::Plus))
|
||||||
|
.child("New"),
|
||||||
|
)
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
open_new_pull_request_panel(
|
open_new_pull_panel(
|
||||||
this.dock_area.clone(),
|
this.dock_area.clone(),
|
||||||
this.store.clone(),
|
this.store.clone(),
|
||||||
this.repo_name.clone(),
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
.dropdown_menu(|menu, _, _| {
|
||||||
|
menu.menu_element(Box::new(RepoAction::SendPatch), |_, _| {
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(Icon::new(IconName::File))
|
||||||
|
.child("Send Patch")
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,6 +395,11 @@ impl Render for PullRequestsView {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.image_cache(image_cache("pull-requests", MAX_IMAGES))
|
.image_cache(image_cache("pull-requests", MAX_IMAGES))
|
||||||
|
.on_action(cx.listener(|this, action: &RepoAction, window, cx| {
|
||||||
|
if action == &RepoAction::SendPatch {
|
||||||
|
open_send_patch_panel(this.dock_area.clone(), this.store.clone(), window, cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
.child(self.render_header(cx))
|
.child(self.render_header(cx))
|
||||||
.when_some(last_warning, |this, warning| {
|
.when_some(last_warning, |this, warning| {
|
||||||
this.child(Alert::warning("pr-warning", warning).banner().on_close({
|
this.child(Alert::warning("pr-warning", warning).banner().on_close({
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
use assets::CustomIconName;
|
||||||
|
use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
|
||||||
|
use gpui::prelude::*;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
|
||||||
|
Subscription, WeakEntity, Window, div, px,
|
||||||
|
};
|
||||||
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
|
use gpui_component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||||
|
use gpui_component::{ActiveTheme, Disableable, Icon, IconName, Sizable, h_flex, v_flex};
|
||||||
|
use signed_state::RepoStore;
|
||||||
|
|
||||||
|
pub struct SendPatchView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
/// Dock area the panel lives in.
|
||||||
|
dock_area: WeakEntity<DockArea>,
|
||||||
|
/// Store of the target repository.
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
/// Display name of the repository, for the panel title.
|
||||||
|
repo_name: SharedString,
|
||||||
|
/// Title input (required).
|
||||||
|
subject: Entity<InputState>,
|
||||||
|
/// Description input (optional).
|
||||||
|
description: Entity<TextareaState>,
|
||||||
|
/// The pasted `git format-patch` output (required).
|
||||||
|
patch: Entity<TextareaState>,
|
||||||
|
/// A submit is in flight.
|
||||||
|
submitting: bool,
|
||||||
|
/// Error of the last submit attempt (keeps the panel open).
|
||||||
|
error: Option<SharedString>,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendPatchView {
|
||||||
|
pub fn new(
|
||||||
|
dock_area: WeakEntity<DockArea>,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let repo_name = store.read(cx).name();
|
||||||
|
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Title"));
|
||||||
|
let description = cx
|
||||||
|
.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change (optional)"));
|
||||||
|
let patch = cx.new(|cx| {
|
||||||
|
TextareaState::new(window, cx).placeholder("Paste `git format-patch` output here...")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-evaluate the Send button's enabled state as the inputs change.
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.subscribe(&subject, |_this, _state, _event: &InputEvent, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
cx.subscribe(&patch, |_this, _state, _event: &InputEvent, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
dock_area,
|
||||||
|
store,
|
||||||
|
repo_name,
|
||||||
|
subject,
|
||||||
|
description,
|
||||||
|
patch,
|
||||||
|
submitting: false,
|
||||||
|
error: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.submitting {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let subject = self.subject.read(cx).value().to_string();
|
||||||
|
let description = self.description.read(cx).value().to_string();
|
||||||
|
let patch = self.patch.read(cx).value().to_string();
|
||||||
|
if patch.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let store = self.store.clone();
|
||||||
|
let dock_area = self.dock_area.clone();
|
||||||
|
let entity = cx.entity().clone();
|
||||||
|
|
||||||
|
self.submitting = true;
|
||||||
|
self.error = None;
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
// Errors the store detects before publishing are returned
|
||||||
|
// synchronously through `last_error`.
|
||||||
|
let sync_error = store.update(cx, |store, cx| {
|
||||||
|
store.open_pull_request(
|
||||||
|
(!subject.is_empty()).then_some(subject),
|
||||||
|
description,
|
||||||
|
None,
|
||||||
|
patch,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
store.last_error.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(error) = sync_error {
|
||||||
|
self.submitting = false;
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the panel once the publish is underway.
|
||||||
|
cx.defer_in(window, {
|
||||||
|
let dock_area = dock_area.clone();
|
||||||
|
let entity = entity.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
if let Some(dock_area) = dock_area.upgrade() {
|
||||||
|
dock_area.update(cx, |dock, cx| {
|
||||||
|
dock.remove_panel(entity, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top bar: a short caption and the Send button.
|
||||||
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
let can_submit = !self.submitting
|
||||||
|
&& !self.subject.read(cx).value().is_empty()
|
||||||
|
&& !self.patch.read(cx).value().is_empty();
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.px_4()
|
||||||
|
.h_12()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.flex_1()
|
||||||
|
.min_w_0()
|
||||||
|
.gap_2()
|
||||||
|
.items_center()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(Icon::new(IconName::FileText).small().flex_shrink_0())
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.text_ellipsis()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child("Send a patch from `git format-patch` output"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("send-patch")
|
||||||
|
.icon(CustomIconName::CirclePlus)
|
||||||
|
.label("Send patch")
|
||||||
|
.primary()
|
||||||
|
.loading(self.submitting)
|
||||||
|
.disabled(!can_submit)
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.submit(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Title and description inputs.
|
||||||
|
fn render_inputs(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
v_flex()
|
||||||
|
.px_4()
|
||||||
|
.py_2()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(Input::new(&self.subject))
|
||||||
|
.child(Textarea::new(&self.description).h(px(64.)))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The patch textarea, the main content of the panel.
|
||||||
|
fn render_patch(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
|
v_flex()
|
||||||
|
.px_4()
|
||||||
|
.py_2()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("Patch — `git format-patch` output"),
|
||||||
|
)
|
||||||
|
.child(Textarea::new(&self.patch).h(px(240.)))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the "send patch" panel for `store` in the center dock.
|
||||||
|
pub(super) fn open_send_patch_panel(
|
||||||
|
dock_area: WeakEntity<DockArea>,
|
||||||
|
store: Entity<RepoStore>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let panel = cx.new(|cx| SendPatchView::new(dock_area.clone(), store, window, cx));
|
||||||
|
|
||||||
|
let _ = dock_area.update(cx, |dock_area, cx| {
|
||||||
|
dock_area.add_panel_view(panel_handle(panel), DockPlacement::Center, None, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BasePanel for SendPatchView {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"send-patch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for SendPatchView {
|
||||||
|
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div().child(SharedString::from(format!("{}/send-patch", self.repo_name)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for SendPatchView {}
|
||||||
|
|
||||||
|
impl Focusable for SendPatchView {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SendPatchView {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.id("send-patch")
|
||||||
|
.size_full()
|
||||||
|
.child(self.render_header(cx))
|
||||||
|
.child(self.render_inputs(cx))
|
||||||
|
.when_some(self.error.clone(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
h_flex()
|
||||||
|
.px_4()
|
||||||
|
.py_1()
|
||||||
|
.w_full()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().danger)
|
||||||
|
.child(error),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(self.render_patch(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@
|
|||||||
- [x] Patch is generated from the checkout at submit time (`format_patch_between` on the stored merge base); panel closes after publishing, errors surface in the PR list banner.
|
- [x] Patch is generated from the checkout at submit time (`format_patch_between` on the stored merge base); panel closes after publishing, errors surface in the PR list banner.
|
||||||
- [x] Removed with the dialog: paste textarea, draft checkbox, branch-name input and the mirror-clone apply-check hint (store behavior unchanged: `open_pull_request` still publishes the series + `branch-name`/`merge-base`/`r` tags and pushes the tip).
|
- [x] Removed with the dialog: paste textarea, draft checkbox, branch-name input and the mirror-clone apply-check hint (store behavior unchanged: `open_pull_request` still publishes the series + `branch-name`/`merge-base`/`r` tags and pushes the tip).
|
||||||
|
|
||||||
|
### Send patch panel (classic paste flow)
|
||||||
|
|
||||||
|
- [x] "Send patch" entry in the repo header PRs dropdown (`RepoAction::SendPatch`) and a "New pull request ▾ Send patch" dropdown replacing the PR list's plain new-PR button.
|
||||||
|
- [x] `send_patch.rs` center panel: title + optional description + `git format-patch` paste area; submits through `RepoStore::open_pull_request` (no checkout, no `branch-name`/`merge-base`). Synchronous store errors (malformed/oversized patch, sign-in) keep the panel open with an inline error; the panel closes once the publish is underway.
|
||||||
|
|
||||||
- [x] P1: `branch-name` tag + `r` EUC tag on PR creation; draft checkbox in the new-PR dialog (dialog since replaced by the panel above).
|
- [x] P1: `branch-name` tag + `r` EUC tag on PR creation; draft checkbox in the new-PR dialog (dialog since replaced by the panel above).
|
||||||
- [x] P1: `RepoStore::update_pull_request` (kind 1619 + root-revision patch) with an author-only "Update" button on the PR detail header.
|
- [x] P1: `RepoStore::update_pull_request` (kind 1619 + root-revision patch) with an author-only "Update" button on the PR detail header.
|
||||||
- [x] P1: `latest_update` filters by PR author.
|
- [x] P1: `latest_update` filters by PR author.
|
||||||
|
|||||||
Reference in New Issue
Block a user